From 4b14175d077f5093f350e4a62411a689327edad4 Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 28 Jul 2026 14:23:48 +0200 Subject: [PATCH 1/2] Upgrade actions/cache from v3 to v4 actions/cache v3 is deprecated and its backing service is being retired, after which these steps fail outright rather than simply miss. v4 is a drop-in replacement for the path/key/restore-keys usage here. Co-Authored-By: Claude Opus 5 --- .github/workflows/chronicle-published.yml | 140 +++++++++++++++++++--- .github/workflows/javascript-build.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/pull-requests.yml | 2 +- 4 files changed, 127 insertions(+), 19 deletions(-) diff --git a/.github/workflows/chronicle-published.yml b/.github/workflows/chronicle-published.yml index 133bf831f..ae575bbd2 100644 --- a/.github/workflows/chronicle-published.yml +++ b/.github/workflows/chronicle-published.yml @@ -59,6 +59,19 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" echo "bump=$bump" >> "$GITHUB_OUTPUT" + # Read before the pins are rewritten - it is the lower bound of the range of Chronicle + # releases this update picks up, and it tells the supersede step which open pull + # requests main has already moved past. + - name: Read the currently pinned Chronicle version + id: pinned + run: | + pinned=$(grep -oE 'Include="Cratis\.Chronicle" Version="[^"]+"' Directory.Packages.props \ + | head -1 \ + | grep -oE 'Version="[^"]+"' \ + | cut -d'"' -f2) + echo "Currently pinned at ${pinned:-}." + echo "version=$pinned" >> "$GITHUB_OUTPUT" + - name: Update Chronicle package pins id: bump run: | @@ -73,6 +86,19 @@ jobs: git diff -- "$file" fi + # The pull request body is published verbatim as Arc's release notes when the pull + # request merges, so it is composed to follow pull_request_template.md and carries + # nothing about CI - that goes into a comment further down. + - name: Compose release notes + if: steps.bump.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + run: | + python3 .github/scripts/compose-chronicle-release-notes.py \ + --previous "${{ steps.pinned.outputs.version }}" \ + --target "${{ steps.inputs.outputs.version }}" \ + --output "${{ runner.temp }}/release-notes.md" + - name: Setup .Net if: steps.bump.outputs.changed == 'true' uses: actions/setup-dotnet@v4 @@ -154,28 +180,110 @@ jobs: commit-message: "Update Cratis.Chronicle to ${{ steps.inputs.outputs.version }}" title: "Update Cratis.Chronicle to ${{ steps.inputs.outputs.version }}" labels: ${{ steps.inputs.outputs.bump }} - body: | - Automated update of `Cratis.Chronicle`, `Cratis.Chronicle.AspNetCore` and `Cratis.Chronicle.Testing` to `${{ steps.inputs.outputs.version }}`, triggered by a Chronicle release. + body-path: ${{ runner.temp }}/release-notes.md + + # Only the newest Chronicle update is worth reviewing - each one pins the same three + # packages, so an older one has nothing left to contribute once a newer one exists. + # Runs whether or not this run produced a pull request, so that an update landing on + # main also clears out the open pull requests it made redundant. + - name: Supersede older Chronicle updates + id: supersede + env: + GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} + REPOSITORY: ${{ github.repository }} + PINNED: ${{ steps.pinned.outputs.version }} + run: | + set -euo pipefail - ### Verification gate + # Whether $1 is greater than $2, semantically rather than lexically (16.10.0 > 16.9.0). + is_newer_than() { + [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ] + } - | Step | Result | - |------|--------| - | Build | `${{ steps.build.outcome }}` | - | Specs | `${{ steps.specs.outcome }}` | + close_with_comment() { + local number="$1" branch="$2" reason="$3" + echo "Closing #$number — $reason" + gh pr close "$number" --repo "$REPOSITORY" --comment "$reason" + # The pull request is what matters; a lingering branch must not fail the run. + gh api -X DELETE "repos/$REPOSITORY/git/refs/heads/$branch" >/dev/null 2>&1 || true + } - ${{ (steps.gate.outputs.verified == 'true' && steps.inputs.outputs.bump != 'major') && '✅ All specs passed — auto-merge enabled.' || '⚠️ Requires human review before merging (verification failed or a major bump).' }} + mapfile -t open_updates < <(gh pr list --repo "$REPOSITORY" --state open --limit 100 \ + --json number,headRefName \ + --jq '.[] | select(.headRefName | startswith("chronicle-update/")) + | "\(.number) \(.headRefName | sub("^chronicle-update/"; "")) \(.headRefName)"') - - name: Enable auto-merge - if: steps.bump.outputs.changed == 'true' && steps.pr.outputs.pull-request-number && steps.gate.outputs.verified == 'true' && steps.inputs.outputs.bump != 'major' + winner_number="" + winner_version="" + candidates=() + + for update in "${open_updates[@]:-}"; do + [ -n "$update" ] || continue + read -r number version branch <<< "$update" + + # main already carries this version or newer, so the pull request has nothing to add. + if [ -n "$PINNED" ] && ! is_newer_than "$version" "$PINNED"; then + close_with_comment "$number" "$branch" \ + "\`Cratis.Chronicle\` is already at \`$PINNED\` on \`main\`, so this update no longer changes anything. Closing it." + continue + fi + + candidates+=("$number $version $branch") + if [ -z "$winner_version" ] || is_newer_than "$version" "$winner_version"; then + winner_number="$number" + winner_version="$version" + fi + done + + for candidate in "${candidates[@]:-}"; do + [ -n "$candidate" ] || continue + read -r number version branch <<< "$candidate" + [ "$number" = "$winner_number" ] && continue + close_with_comment "$number" "$branch" \ + "Superseded by #$winner_number, which updates \`Cratis.Chronicle\` to \`$winner_version\` and includes everything this update carried. Closing it so only the newest Chronicle update stays open." + done + + echo "winner=$winner_number" >> "$GITHUB_OUTPUT" + echo "Newest open Chronicle update: ${winner_number:-none} (${winner_version:-n/a})." + + # The verification result is CI bookkeeping, not release notes - it belongs in a + # comment so that it never reaches the published Arc release. + - name: Report the verification gate + if: steps.bump.outputs.changed == 'true' && steps.pr.outputs.pull-request-number env: GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - run: gh pr merge --auto --squash "${{ steps.pr.outputs.pull-request-number }}" --repo ${{ github.repository }} + SUPERSEDED: ${{ steps.supersede.outputs.winner != steps.pr.outputs.pull-request-number }} + run: | + if [ "$SUPERSEDED" = "true" ]; then + outcome="⏭️ Superseded by a newer Chronicle update — not auto-merged." + elif [ "${{ steps.gate.outputs.verified }}" != "true" ]; then + outcome="⚠️ Requires human review before merging — verification failed." + elif [ "${{ steps.inputs.outputs.bump }}" = "major" ]; then + outcome="⚠️ Requires human review before merging — major bump." + else + outcome="✅ All specs passed — auto-merge enabled." + fi + + gh pr comment "${{ steps.pr.outputs.pull-request-number }}" --repo "${{ github.repository }}" --body "$(cat <- + steps.bump.outputs.changed == 'true' && + steps.pr.outputs.pull-request-number && + steps.supersede.outputs.winner == steps.pr.outputs.pull-request-number && + steps.gate.outputs.verified == 'true' && + steps.inputs.outputs.bump != 'major' env: GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - run: | - gh pr comment "${{ steps.pr.outputs.pull-request-number }}" --repo ${{ github.repository }} --body \ - "This Chronicle update was **not** auto-merged. Build outcome: \`${{ steps.build.outcome }}\`, specs outcome: \`${{ steps.specs.outcome }}\`, bump: \`${{ steps.inputs.outputs.bump }}\`. Please review before merging." + run: gh pr merge --auto --squash "${{ steps.pr.outputs.pull-request-number }}" --repo ${{ github.repository }} diff --git a/.github/workflows/javascript-build.yml b/.github/workflows/javascript-build.yml index 6719e3ce6..2dca52426 100644 --- a/.github/workflows/javascript-build.yml +++ b/.github/workflows/javascript-build.yml @@ -36,7 +36,7 @@ jobs: node-version: 23.x registry-url: "https://registry.npmjs.org" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 372deacd2..5ef9fbffc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -94,7 +94,7 @@ jobs: node-version: 23.x registry-url: "https://registry.npmjs.org" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: | diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index e6e87589e..4c2368d1a 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -63,7 +63,7 @@ jobs: node-version: 23.x registry-url: "https://registry.npmjs.org" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: | From 126d0fc2aa5fc3d400084634820caf80ca19903b Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 28 Jul 2026 14:28:12 +0200 Subject: [PATCH 2/2] Restore chronicle-published.yml to its state on main The previous commit staged this file by directory and swept in in-progress changes that are unrelated to the cache upgrade. Put it back so this branch only carries the actions/cache v3 -> v4 change. Co-Authored-By: Claude Opus 5 --- .github/workflows/chronicle-published.yml | 140 +++------------------- 1 file changed, 16 insertions(+), 124 deletions(-) diff --git a/.github/workflows/chronicle-published.yml b/.github/workflows/chronicle-published.yml index ae575bbd2..133bf831f 100644 --- a/.github/workflows/chronicle-published.yml +++ b/.github/workflows/chronicle-published.yml @@ -59,19 +59,6 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" echo "bump=$bump" >> "$GITHUB_OUTPUT" - # Read before the pins are rewritten - it is the lower bound of the range of Chronicle - # releases this update picks up, and it tells the supersede step which open pull - # requests main has already moved past. - - name: Read the currently pinned Chronicle version - id: pinned - run: | - pinned=$(grep -oE 'Include="Cratis\.Chronicle" Version="[^"]+"' Directory.Packages.props \ - | head -1 \ - | grep -oE 'Version="[^"]+"' \ - | cut -d'"' -f2) - echo "Currently pinned at ${pinned:-}." - echo "version=$pinned" >> "$GITHUB_OUTPUT" - - name: Update Chronicle package pins id: bump run: | @@ -86,19 +73,6 @@ jobs: git diff -- "$file" fi - # The pull request body is published verbatim as Arc's release notes when the pull - # request merges, so it is composed to follow pull_request_template.md and carries - # nothing about CI - that goes into a comment further down. - - name: Compose release notes - if: steps.bump.outputs.changed == 'true' - env: - GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - run: | - python3 .github/scripts/compose-chronicle-release-notes.py \ - --previous "${{ steps.pinned.outputs.version }}" \ - --target "${{ steps.inputs.outputs.version }}" \ - --output "${{ runner.temp }}/release-notes.md" - - name: Setup .Net if: steps.bump.outputs.changed == 'true' uses: actions/setup-dotnet@v4 @@ -180,110 +154,28 @@ jobs: commit-message: "Update Cratis.Chronicle to ${{ steps.inputs.outputs.version }}" title: "Update Cratis.Chronicle to ${{ steps.inputs.outputs.version }}" labels: ${{ steps.inputs.outputs.bump }} - body-path: ${{ runner.temp }}/release-notes.md - - # Only the newest Chronicle update is worth reviewing - each one pins the same three - # packages, so an older one has nothing left to contribute once a newer one exists. - # Runs whether or not this run produced a pull request, so that an update landing on - # main also clears out the open pull requests it made redundant. - - name: Supersede older Chronicle updates - id: supersede - env: - GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - REPOSITORY: ${{ github.repository }} - PINNED: ${{ steps.pinned.outputs.version }} - run: | - set -euo pipefail - - # Whether $1 is greater than $2, semantically rather than lexically (16.10.0 > 16.9.0). - is_newer_than() { - [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ] - } - - close_with_comment() { - local number="$1" branch="$2" reason="$3" - echo "Closing #$number — $reason" - gh pr close "$number" --repo "$REPOSITORY" --comment "$reason" - # The pull request is what matters; a lingering branch must not fail the run. - gh api -X DELETE "repos/$REPOSITORY/git/refs/heads/$branch" >/dev/null 2>&1 || true - } - - mapfile -t open_updates < <(gh pr list --repo "$REPOSITORY" --state open --limit 100 \ - --json number,headRefName \ - --jq '.[] | select(.headRefName | startswith("chronicle-update/")) - | "\(.number) \(.headRefName | sub("^chronicle-update/"; "")) \(.headRefName)"') - - winner_number="" - winner_version="" - candidates=() - - for update in "${open_updates[@]:-}"; do - [ -n "$update" ] || continue - read -r number version branch <<< "$update" + body: | + Automated update of `Cratis.Chronicle`, `Cratis.Chronicle.AspNetCore` and `Cratis.Chronicle.Testing` to `${{ steps.inputs.outputs.version }}`, triggered by a Chronicle release. - # main already carries this version or newer, so the pull request has nothing to add. - if [ -n "$PINNED" ] && ! is_newer_than "$version" "$PINNED"; then - close_with_comment "$number" "$branch" \ - "\`Cratis.Chronicle\` is already at \`$PINNED\` on \`main\`, so this update no longer changes anything. Closing it." - continue - fi - - candidates+=("$number $version $branch") - if [ -z "$winner_version" ] || is_newer_than "$version" "$winner_version"; then - winner_number="$number" - winner_version="$version" - fi - done + ### Verification gate - for candidate in "${candidates[@]:-}"; do - [ -n "$candidate" ] || continue - read -r number version branch <<< "$candidate" - [ "$number" = "$winner_number" ] && continue - close_with_comment "$number" "$branch" \ - "Superseded by #$winner_number, which updates \`Cratis.Chronicle\` to \`$winner_version\` and includes everything this update carried. Closing it so only the newest Chronicle update stays open." - done + | Step | Result | + |------|--------| + | Build | `${{ steps.build.outcome }}` | + | Specs | `${{ steps.specs.outcome }}` | - echo "winner=$winner_number" >> "$GITHUB_OUTPUT" - echo "Newest open Chronicle update: ${winner_number:-none} (${winner_version:-n/a})." + ${{ (steps.gate.outputs.verified == 'true' && steps.inputs.outputs.bump != 'major') && '✅ All specs passed — auto-merge enabled.' || '⚠️ Requires human review before merging (verification failed or a major bump).' }} - # The verification result is CI bookkeeping, not release notes - it belongs in a - # comment so that it never reaches the published Arc release. - - name: Report the verification gate - if: steps.bump.outputs.changed == 'true' && steps.pr.outputs.pull-request-number + - name: Enable auto-merge + if: steps.bump.outputs.changed == 'true' && steps.pr.outputs.pull-request-number && steps.gate.outputs.verified == 'true' && steps.inputs.outputs.bump != 'major' env: GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - SUPERSEDED: ${{ steps.supersede.outputs.winner != steps.pr.outputs.pull-request-number }} - run: | - if [ "$SUPERSEDED" = "true" ]; then - outcome="⏭️ Superseded by a newer Chronicle update — not auto-merged." - elif [ "${{ steps.gate.outputs.verified }}" != "true" ]; then - outcome="⚠️ Requires human review before merging — verification failed." - elif [ "${{ steps.inputs.outputs.bump }}" = "major" ]; then - outcome="⚠️ Requires human review before merging — major bump." - else - outcome="✅ All specs passed — auto-merge enabled." - fi - - gh pr comment "${{ steps.pr.outputs.pull-request-number }}" --repo "${{ github.repository }}" --body "$(cat <- - steps.bump.outputs.changed == 'true' && - steps.pr.outputs.pull-request-number && - steps.supersede.outputs.winner == steps.pr.outputs.pull-request-number && - steps.gate.outputs.verified == 'true' && - steps.inputs.outputs.bump != 'major' + - name: Comment when review is required + if: steps.bump.outputs.changed == 'true' && steps.pr.outputs.pull-request-number && (steps.gate.outputs.verified != 'true' || steps.inputs.outputs.bump == 'major') env: GH_TOKEN: ${{ secrets.PAT_WORKFLOWS }} - run: gh pr merge --auto --squash "${{ steps.pr.outputs.pull-request-number }}" --repo ${{ github.repository }} + run: | + gh pr comment "${{ steps.pr.outputs.pull-request-number }}" --repo ${{ github.repository }} --body \ + "This Chronicle update was **not** auto-merged. Build outcome: \`${{ steps.build.outcome }}\`, specs outcome: \`${{ steps.specs.outcome }}\`, bump: \`${{ steps.inputs.outputs.bump }}\`. Please review before merging."