diff --git a/.github/workflows/node-release-cut.yml b/.github/workflows/node-release-cut.yml index 6d8b8d0..ef3e8a4 100644 --- a/.github/workflows/node-release-cut.yml +++ b/.github/workflows/node-release-cut.yml @@ -109,11 +109,35 @@ jobs: run: | set -euo pipefail - existing_ref="$( - gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" \ - --jq '[.object.type, .object.sha] | @tsv' 2>/dev/null || true + 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 IFS=$'\t' read -r tag_type tag_object <<< "$existing_ref" if [[ ! "$tag_object" =~ ^[0-9a-f]{40}$ ]]; then diff --git a/sdk/typescript/tests-ts/release-automation.test.ts b/sdk/typescript/tests-ts/release-automation.test.ts index 9261914..96d5237 100644 --- a/sdk/typescript/tests-ts/release-automation.test.ts +++ b/sdk/typescript/tests-ts/release-automation.test.ts @@ -1566,6 +1566,83 @@ describe("GitHub release workflow safeguards", () => { }, ); + test.each([ + { + 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, lookupHttpStatus, status }) => { + const script = workflowStepShell( + releaseCutWorkflow, + "Create the exact merged release tag", + ); + const mock = [ + "gh() {", + ' if [[ "$1" != "api" ]]; then return 64; fi', + " shift", + ' 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" ||', + ' "$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_HTTP_STATUS: lookupHttpStatus, + 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", @@ -1599,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\"", " ;;",