From 83848e1eece67a4a3b0bcccc90f19da298b0af00 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 8 Aug 2026 00:26:01 -0700 Subject: [PATCH 1/5] fix(ci): use supported release workflow concurrency --- .github/workflows/node-github-release.yml | 2 +- .github/workflows/node-release-cut.yml | 2 +- .github/workflows/node-release.yml | 2 +- sdk/typescript/tests-ts/release-automation.test.ts | 11 +++++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/node-github-release.yml b/.github/workflows/node-github-release.yml index d8552b35..d176ce6e 100644 --- a/.github/workflows/node-github-release.yml +++ b/.github/workflows/node-github-release.yml @@ -17,7 +17,7 @@ permissions: concurrency: group: node-github-release - queue: max + cancel-in-progress: false jobs: release: diff --git a/.github/workflows/node-release-cut.yml b/.github/workflows/node-release-cut.yml index 6edec563..2b96ffb2 100644 --- a/.github/workflows/node-release-cut.yml +++ b/.github/workflows/node-release-cut.yml @@ -12,7 +12,7 @@ permissions: concurrency: group: node-release-cut - queue: max + cancel-in-progress: false jobs: cut: diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index 84ef6889..0f5ce6e7 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -8,7 +8,7 @@ on: concurrency: group: ${{ github.workflow }} - queue: max + cancel-in-progress: false jobs: verify: diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 7c4a4b6a..b354f996 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2046,13 +2046,15 @@ describe("GitHub release workflow safeguards", () => { ); }); - test("durably queues every release-cut and protected publishing run", () => { + test("serializes release-cut and protected publishing without canceling active runs", () => { expect(releaseCutWorkflow).toMatch( - /concurrency:\s*\n\s+group: node-release-cut\s*\n\s+queue: max/u, + /concurrency:\s*\n\s+group: node-release-cut\s*\n\s+cancel-in-progress: false/u, ); expect(protectedReleaseWorkflow).toMatch( - /concurrency:\s*\n\s+group: \$\{\{ github\.workflow \}\}\s*\n\s+queue: max/u, + /concurrency:\s*\n\s+group: \$\{\{ github\.workflow \}\}\s*\n\s+cancel-in-progress: false/u, ); + expect(releaseCutWorkflow).not.toMatch(/^\s+queue:/mu); + expect(protectedReleaseWorkflow).not.toMatch(/^\s+queue:/mu); }); test("dispatches GitHub releases after publishing with isolated permissions", () => { @@ -2111,8 +2113,9 @@ 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, + /concurrency:\s*\n\s+group: node-github-release\s*\n\s+cancel-in-progress: false/u, ); + expect(githubReleaseWorkflow).not.toMatch(/^\s+queue:/mu); expect(githubReleaseWorkflow).not.toContain( "group: node-github-release-${{", ); From b3b5f86a37612539f2b8f3f874aa30331387a2b6 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 10 Aug 2026 11:27:25 -0700 Subject: [PATCH 2/5] Order npm latest publication without dropping release tags --- .github/workflows/node-release.yml | 38 +++++++++++++++++++ .../tests-ts/release-automation.test.ts | 15 ++++++++ 2 files changed, 53 insertions(+) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index 03a5755b..e27a2dd9 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -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 179df71d..45a5fa1a 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2059,6 +2059,21 @@ describe("GitHub release workflow safeguards", () => { 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", () => { expect(protectedReleaseWorkflow).toContain( [ From effc83bf27e477b0e0dad7e48b960b3a17cf27b2 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 10 Aug 2026 11:37:01 -0700 Subject: [PATCH 3/5] Fail release ordering closed when workflow lookup fails --- .github/workflows/node-release.yml | 11 ++++++----- .../tests-ts/release-automation.test.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index e27a2dd9..ac309c38 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -397,6 +397,11 @@ jobs: for attempt in {1..120}; do waiting= + active_runs="$( + 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' + )" while IFS=$'\t' read -r run_id ref_name; do [[ -n "$run_id" && "$run_id" != "$GITHUB_RUN_ID" ]] || continue [[ "$ref_name" == npm-v* ]] || continue @@ -406,11 +411,7 @@ jobs: 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' - ) + done <<< "$active_runs" if [[ -z "$waiting" ]]; then echo "No older npm release is still running." diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 45a5fa1a..03686f8a 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2072,6 +2072,22 @@ describe("GitHub release workflow safeguards", () => { ); expect(ordering).toContain("sleep 15"); expect(protectedReleaseWorkflow).toContain(" actions: read"); + + const failedLookup = spawnSync( + "bash", + ["-c", `gh() { return 17; }\n${ordering}`], + { + encoding: "utf8", + env: { + ...process.env, + GITHUB_REPOSITORY: "test/codex-security", + GITHUB_RUN_ID: "1", + RELEASE_VERSION: "0.2.0", + }, + }, + ); + expect(failedLookup.status).toBe(17); + expect(failedLookup.stdout).not.toContain("No older npm release"); }); test("dispatches GitHub releases after publishing with isolated permissions", () => { From a023a419a25a870e1667409ac5c0b8437e7e7226 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 10 Aug 2026 11:45:18 -0700 Subject: [PATCH 4/5] Serialize GitHub releases by published version --- .github/workflows/node-github-release.yml | 42 +++++++++++++ .../tests-ts/release-automation.test.ts | 63 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/.github/workflows/node-github-release.yml b/.github/workflows/node-github-release.yml index fd417b23..7972c46a 100644 --- a/.github/workflows/node-github-release.yml +++ b/.github/workflows/node-github-release.yml @@ -1,4 +1,5 @@ name: node-github-release +run-name: GitHub release ${{ inputs.tag }} on: workflow_dispatch: @@ -392,6 +393,47 @@ jobs: printf 'make-latest=%s\n' "$make_latest" } >> "$GITHUB_OUTPUT" + - name: Wait for older GitHub releases + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ steps.release.outputs.version }} + run: | + set -euo pipefail + + for attempt in {1..120}; do + waiting= + active_runs="$( + gh api --paginate \ + "repos/$GITHUB_REPOSITORY/actions/workflows/node-github-release.yml/runs?per_page=100" \ + --jq '.workflow_runs[] | select(.status != "completed") | [.id, (.display_title // ""), .run_number] | @tsv' + )" + while IFS=$'\t' read -r run_id run_name run_number; do + [[ -n "$run_id" && "$run_id" != "$GITHUB_RUN_ID" ]] || continue + if [[ "$run_name" == "GitHub release npm-v"* ]]; then + candidate="${run_name#GitHub release npm-v}" + node sdk/typescript/scripts/release-automation.mjs \ + require-increase "$RELEASE_VERSION" "$candidate" \ + >/dev/null 2>&1 || continue + elif [[ ! "$run_number" =~ ^[0-9]+$ || + "$run_number" -ge "$GITHUB_RUN_NUMBER" ]]; then + continue + fi + waiting="$run_id" + break + done <<< "$active_runs" + + if [[ -z "$waiting" ]]; then + echo "No older GitHub release is still running." + break + fi + if [[ "$attempt" == 120 ]]; then + echo "Timed out waiting for older GitHub release run $waiting." >&2 + exit 1 + fi + sleep 15 + done + - name: Publish GitHub Release and generated notes shell: bash env: diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 03686f8a..d32c7551 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2090,6 +2090,69 @@ describe("GitHub release workflow safeguards", () => { expect(failedLookup.stdout).not.toContain("No older npm release"); }); + test("publishes GitHub releases in version order without dropping pending tags", () => { + expect(githubReleaseWorkflow).toContain( + "run-name: GitHub release ${{ inputs.tag }}", + ); + const ordering = workflowStepShell( + githubReleaseWorkflow, + "Wait for older GitHub releases", + ); + expect(ordering).toContain( + "actions/workflows/node-github-release.yml/runs?per_page=100", + ); + expect(ordering).toContain( + 'require-increase "$RELEASE_VERSION" "$candidate"', + ); + + const root = mkdtempSync(join(tmpdir(), "codex-security-github-order-")); + const mocks = [ + "gh() {", + ' if [[ ! -e "$RELEASE_RUN_CHECKED" ]]; then', + ' touch "$RELEASE_RUN_CHECKED"', + " printf '7\\tGitHub release npm-v0.1.0\\t7\\n'", + " fi", + "}", + 'sleep() { printf "waited %s\\n" "$1"; }', + ].join("\n"); + try { + const ordered = spawnSync("bash", ["-c", `${mocks}\n${ordering}`], { + encoding: "utf8", + cwd: fileURLToPath(new URL("../../../", import.meta.url)), + env: { + ...process.env, + GITHUB_REPOSITORY: "test/codex-security", + GITHUB_RUN_ID: "8", + GITHUB_RUN_NUMBER: "8", + RELEASE_RUN_CHECKED: join(root, "checked"), + RELEASE_VERSION: "0.2.0", + }, + }); + expect(ordered.status).toBe(0); + expect(ordered.stdout).toContain("waited 15"); + expect(ordered.stdout).toContain("No older GitHub release"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + + const failedLookup = spawnSync( + "bash", + ["-c", `gh() { return 17; }\n${ordering}`], + { + encoding: "utf8", + env: { + ...process.env, + GITHUB_REPOSITORY: "test/codex-security", + GITHUB_RUN_ID: "8", + GITHUB_RUN_NUMBER: "8", + RELEASE_VERSION: "0.2.0", + }, + }, + ); + expect(failedLookup.status).toBe(17); + expect(failedLookup.stdout).not.toContain("No older GitHub release"); + }); + test("dispatches GitHub releases after publishing with isolated permissions", () => { expect(protectedReleaseWorkflow).toContain( [ From f1faf71475af95bc13ee66ccdf3a78ff46e37e4b Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 10 Aug 2026 11:51:09 -0700 Subject: [PATCH 5/5] Revalidate release tags immediately after publication waits --- .github/workflows/node-release.yml | 76 +++++++++---------- .../tests-ts/release-automation.test.ts | 14 ++++ 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/.github/workflows/node-release.yml b/.github/workflows/node-release.yml index ac309c38..d2c4269f 100644 --- a/.github/workflows/node-release.yml +++ b/.github/workflows/node-release.yml @@ -332,6 +332,44 @@ jobs: "$GITHUB_SHA" \ "$GITHUB_REPOSITORY" + - 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= + active_runs="$( + 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' + )" + 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 <<< "$active_runs" + + 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: Revalidate protected release tag if: needs.verify.outputs.mode == 'publish' shell: bash @@ -386,44 +424,6 @@ 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= - active_runs="$( - 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' - )" - 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 <<< "$active_runs" - - 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 d32c7551..b33b0642 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -2072,6 +2072,20 @@ describe("GitHub release workflow safeguards", () => { ); expect(ordering).toContain("sleep 15"); expect(protectedReleaseWorkflow).toContain(" actions: read"); + expect( + protectedReleaseWorkflow.indexOf("- name: Wait for older npm releases"), + ).toBeLessThan( + protectedReleaseWorkflow.indexOf( + "- name: Revalidate protected release tag", + ), + ); + expect( + protectedReleaseWorkflow.indexOf( + "- name: Revalidate protected release tag", + ), + ).toBeLessThan( + protectedReleaseWorkflow.indexOf("- name: Publish initial npm release"), + ); const failedLookup = spawnSync( "bash",