diff --git a/.github/workflows/node-github-release.yml b/.github/workflows/node-github-release.yml index d8552b35..fd417b23 100644 --- a/.github/workflows/node-github-release.yml +++ b/.github/workflows/node-github-release.yml @@ -16,8 +16,8 @@ permissions: contents: read concurrency: - group: node-github-release - queue: max + group: node-github-release-${{ inputs.tag }} + cancel-in-progress: false jobs: release: diff --git a/.github/workflows/node-release-cut.yml b/.github/workflows/node-release-cut.yml index 6edec563..f289171d 100644 --- a/.github/workflows/node-release-cut.yml +++ b/.github/workflows/node-release-cut.yml @@ -11,8 +11,8 @@ permissions: contents: read concurrency: - group: node-release-cut - queue: max + group: node-release-cut-${{ github.event.workflow_run.head_sha || github.sha }} + cancel-in-progress: false jobs: cut: diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index 84ef6889..e27a2dd9 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -7,8 +7,8 @@ on: workflow_dispatch: concurrency: - group: ${{ github.workflow }} - queue: max + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false jobs: verify: @@ -218,6 +218,7 @@ jobs: permissions: contents: read id-token: write + actions: read steps: - name: Checkout release verification source @@ -385,6 +386,43 @@ jobs: fi echo "Protected npm release tag matches the verified commit." + - name: Wait for older npm releases + if: needs.verify.outputs.mode == 'publish' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ needs.verify.outputs.version }} + run: | + set -euo pipefail + + for attempt in {1..120}; do + waiting= + while IFS=$'\t' read -r run_id ref_name; do + [[ -n "$run_id" && "$run_id" != "$GITHUB_RUN_ID" ]] || continue + [[ "$ref_name" == npm-v* ]] || continue + candidate="${ref_name#npm-v}" + if node sdk/typescript/scripts/release-automation.mjs \ + require-increase "$RELEASE_VERSION" "$candidate" >/dev/null 2>&1; then + waiting="$run_id" + break + fi + done < <( + gh api --paginate \ + "repos/$GITHUB_REPOSITORY/actions/workflows/node-release.yml/runs?per_page=100" \ + --jq '.workflow_runs[] | select(.status != "completed") | [.id, (.head_branch // "")] | @tsv' + ) + + if [[ -z "$waiting" ]]; then + echo "No older npm release is still running." + break + fi + if [[ "$attempt" == 120 ]]; then + echo "Timed out waiting for older npm release run $waiting." >&2 + exit 1 + fi + sleep 15 + done + - name: Publish initial npm release if: needs.verify.outputs.mode == 'publish' && github.ref_name == 'npm-v0.1.0' env: diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 7c4a4b6a..45a5fa1a 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2046,13 +2046,32 @@ describe("GitHub release workflow safeguards", () => { ); }); - test("durably queues every release-cut and protected publishing run", () => { - expect(releaseCutWorkflow).toMatch( - /concurrency:\s*\n\s+group: node-release-cut\s*\n\s+queue: max/u, + test("keeps distinct release commits and tags in separate concurrency groups", () => { + expect(releaseCutWorkflow).toContain( + "group: node-release-cut-${{ github.event.workflow_run.head_sha || github.sha }}", ); - expect(protectedReleaseWorkflow).toMatch( - /concurrency:\s*\n\s+group: \$\{\{ github\.workflow \}\}\s*\n\s+queue: max/u, + expect(protectedReleaseWorkflow).toContain( + "group: ${{ github.workflow }}-${{ github.ref }}", ); + expect(releaseCutWorkflow).toContain("cancel-in-progress: false"); + expect(protectedReleaseWorkflow).toContain("cancel-in-progress: false"); + expect(releaseCutWorkflow).not.toMatch(/^\s+queue:/mu); + expect(protectedReleaseWorkflow).not.toMatch(/^\s+queue:/mu); + }); + + test("waits for older npm publishers without dropping pending release tags", () => { + const ordering = workflowStepShell( + protectedReleaseWorkflow, + "Wait for older npm releases", + ); + expect(ordering).toContain( + "actions/workflows/node-release.yml/runs?per_page=100", + ); + expect(ordering).toContain( + 'require-increase "$RELEASE_VERSION" "$candidate"', + ); + expect(ordering).toContain("sleep 15"); + expect(protectedReleaseWorkflow).toContain(" actions: read"); }); test("dispatches GitHub releases after publishing with isolated permissions", () => { @@ -2109,13 +2128,12 @@ describe("GitHub release workflow safeguards", () => { ]); }); - test("serializes every GitHub release and historical backfill", () => { - expect(githubReleaseWorkflow).toMatch( - /concurrency:\s*\n\s+group: node-github-release\s*\n\s+queue: max/u, - ); - expect(githubReleaseWorkflow).not.toContain( - "group: node-github-release-${{", + test("preserves GitHub releases and historical backfills for distinct tags", () => { + expect(githubReleaseWorkflow).toContain( + "group: node-github-release-${{ inputs.tag }}", ); + expect(githubReleaseWorkflow).toContain("cancel-in-progress: false"); + expect(githubReleaseWorkflow).not.toMatch(/^\s+queue:/mu); }); test("runs manually dispatched GitHub releases from trusted main", () => {