From 4bf379c56e6173d3c324f2ab591b9f7fb70c2d93 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 31 Jul 2026 09:25:23 -0700 Subject: [PATCH 1/2] fix: handle missing release tags safely --- .github/workflows/node-release-cut.yml | 15 ++++- .../tests-ts/release-automation.test.ts | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node-release-cut.yml b/.github/workflows/node-release-cut.yml index 6d8b8d08..5112a689 100644 --- a/.github/workflows/node-release-cut.yml +++ b/.github/workflows/node-release-cut.yml @@ -109,10 +109,19 @@ jobs: run: | set -euo pipefail - existing_ref="$( + if ! existing_ref="$( gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" \ - --jq '[.object.type, .object.sha] | @tsv' 2>/dev/null || true - )" + --jq '[.object.type, .object.sha] | @tsv' 2>/dev/null + )"; then + if [[ "$( + printf '%s\n' "$existing_ref" | + jq -r '.status // empty' 2>/dev/null || true + )" != 404 ]]; then + echo "Unable to query release tag $RELEASE_TAG." >&2 + exit 1 + fi + existing_ref="" + fi if [[ -n "$existing_ref" ]]; then IFS=$'\t' read -r tag_type tag_object <<< "$existing_ref" diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 92619145..eee46359 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -1566,6 +1566,69 @@ describe("GitHub release workflow safeguards", () => { }, ); + test.each([ + { + kind: "missing tag with GitHub's 404 error body", + lookupResponse: JSON.stringify({ message: "Not Found", status: "404" }), + status: 0, + }, + { + kind: "forbidden tag lookup", + lookupResponse: JSON.stringify({ message: "Forbidden", status: "403" }), + status: 1, + }, + { + kind: "unavailable tag lookup", + lookupResponse: "", + status: 1, + }, + ])( + "handles $kind safely before cutting a release", + ({ lookupResponse, status }) => { + const script = workflowStepShell( + releaseCutWorkflow, + "Create the exact merged release tag", + ); + const mock = [ + "gh() {", + ' if [[ "$1" != "api" ]]; then return 64; fi', + " shift", + ' if [[ "$1" == "repos/test/codex-security/git/ref/tags/npm-v0.1.2" ]]; then', + " printf '%s\\n' \"$MOCK_LOOKUP_RESPONSE\"", + " return 1", + " fi", + ' if [[ "$1" != "--method" || "$2" != "POST" ||', + ' "$3" != "repos/test/codex-security/git/refs" ||', + ' "$4" != "-f" || "$5" != "ref=refs/tags/npm-v0.1.2" ||', + ' "$6" != "-f" || "$7" != "sha=$GITHUB_SHA" ||', + ' "$8" != "--silent" ]]; then return 65; fi', + " printf 'created exact release tag\\n'", + "}", + ].join("\n"); + const result = spawnSync("bash", ["-c", `${mock}\n${script}`], { + encoding: "utf8", + env: { + ...process.env, + GITHUB_REPOSITORY: "test/codex-security", + GITHUB_SHA: releaseCommit, + MOCK_LOOKUP_RESPONSE: lookupResponse, + RELEASE_TAG: "npm-v0.1.2", + }, + timeout: 10_000, + }); + + expect(result.status).toBe(status); + if (status === 0) { + expect(result.stdout).toContain("created exact release tag"); + } else { + expect(result.stderr).toContain( + "Unable to query release tag npm-v0.1.2.", + ); + expect(result.stdout).not.toContain("created exact release tag"); + } + }, + ); + test.each([ { kind: "existing lightweight tag", From 7e84eca99f08325cf8a2d76aaeec2de6128eba9e Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 31 Jul 2026 09:33:37 -0700 Subject: [PATCH 2/2] fix: inspect raw release-tag lookup responses --- .github/workflows/node-release-cut.yml | 31 ++++++++++++----- .../tests-ts/release-automation.test.ts | 33 +++++++++++++++---- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.github/workflows/node-release-cut.yml b/.github/workflows/node-release-cut.yml index 5112a689..ef3e8a43 100644 --- a/.github/workflows/node-release-cut.yml +++ b/.github/workflows/node-release-cut.yml @@ -109,18 +109,33 @@ jobs: run: | set -euo pipefail - if ! existing_ref="$( - gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" \ - --jq '[.object.type, .object.sha] | @tsv' 2>/dev/null - )"; then - if [[ "$( - printf '%s\n' "$existing_ref" | - jq -r '.status // empty' 2>/dev/null || true - )" != 404 ]]; then + query_status=0 + tag_response="$( + gh api --include \ + "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>/dev/null + )" || query_status=$? + + IFS=' ' read -r _ response_status _ <<< "$tag_response" || true + response_body="$( + awk 'body { print } /^\r?$/ { body = 1 }' <<< "$tag_response" + )" + + if [[ "$query_status" == 0 && "$response_status" =~ ^2[0-9]{2}$ ]]; then + if ! existing_ref="$( + jq -er '[.object.type, .object.sha] | @tsv' <<< "$response_body" + )"; then echo "Unable to query release tag $RELEASE_TAG." >&2 exit 1 fi + elif [[ "$query_status" != 0 && "$response_status" == 404 && + "$( + jq -r '.status // empty' <<< "$response_body" \ + 2>/dev/null || true + )" == 404 ]]; then existing_ref="" + else + echo "Unable to query release tag $RELEASE_TAG." >&2 + exit 1 fi if [[ -n "$existing_ref" ]]; then diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index eee46359..96d52379 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -1570,21 +1570,30 @@ describe("GitHub release workflow safeguards", () => { { kind: "missing tag with GitHub's 404 error body", lookupResponse: JSON.stringify({ message: "Not Found", status: "404" }), + lookupHttpStatus: "404", status: 0, }, { kind: "forbidden tag lookup", lookupResponse: JSON.stringify({ message: "Forbidden", status: "403" }), + lookupHttpStatus: "403", + status: 1, + }, + { + kind: "malformed missing-tag response", + lookupResponse: "not JSON", + lookupHttpStatus: "404", status: 1, }, { kind: "unavailable tag lookup", lookupResponse: "", + lookupHttpStatus: "", status: 1, }, ])( "handles $kind safely before cutting a release", - ({ lookupResponse, status }) => { + ({ lookupResponse, lookupHttpStatus, status }) => { const script = workflowStepShell( releaseCutWorkflow, "Create the exact merged release tag", @@ -1593,8 +1602,12 @@ describe("GitHub release workflow safeguards", () => { "gh() {", ' if [[ "$1" != "api" ]]; then return 64; fi', " shift", - ' if [[ "$1" == "repos/test/codex-security/git/ref/tags/npm-v0.1.2" ]]; then', - " printf '%s\\n' \"$MOCK_LOOKUP_RESPONSE\"", + ' if [[ "$1" == "--include" &&', + ' "$2" == "repos/test/codex-security/git/ref/tags/npm-v0.1.2" ]]; then', + ' if [[ -n "$MOCK_LOOKUP_HTTP_STATUS" ]]; then', + " printf 'HTTP/2.0 %s Mock\\nContent-Type: application/json\\n\\n%s\\n' \\", + ' "$MOCK_LOOKUP_HTTP_STATUS" "$MOCK_LOOKUP_RESPONSE"', + " fi", " return 1", " fi", ' if [[ "$1" != "--method" || "$2" != "POST" ||', @@ -1611,6 +1624,7 @@ describe("GitHub release workflow safeguards", () => { ...process.env, GITHUB_REPOSITORY: "test/codex-security", GITHUB_SHA: releaseCommit, + MOCK_LOOKUP_HTTP_STATUS: lookupHttpStatus, MOCK_LOOKUP_RESPONSE: lookupResponse, RELEASE_TAG: "npm-v0.1.2", }, @@ -1662,10 +1676,17 @@ describe("GitHub release workflow safeguards", () => { "gh() {", ' if [[ "$1" != "api" ]]; then return 64; fi', " shift", + ' if [[ "$1" == "--include" ]]; then', + " shift", + ' if [[ "$1" != "repos/test/codex-security/git/ref/tags/npm-v0.1.2" ]]; then', + " return 65", + " fi", + " printf 'HTTP/2.0 200 OK\\nContent-Type: application/json\\n\\n'", + ' printf \'{"object":{"type":"%s","sha":"%s"}}\\n\' \\', + ' "$MOCK_TAG_TYPE" "$MOCK_TAG_OBJECT"', + " return 0", + " fi", ' case "$1" in', - ' "repos/test/codex-security/git/ref/tags/npm-v0.1.2")', - ' printf \'%s\\t%s\\n\' "$MOCK_TAG_TYPE" "$MOCK_TAG_OBJECT"', - " ;;", " repos/test/codex-security/git/tags/*)", " printf '%s\\n' \"$MOCK_PEELED_COMMIT\"", " ;;",