4747 if : >-
4848 vars.RELEASE_PLEASE_ENABLED == 'true' &&
4949 github.event_name == 'workflow_run' &&
50+ github.run_attempt == 1 &&
5051 github.event.workflow_run.conclusion == 'success' &&
5152 github.event.workflow_run.event == 'push' &&
5253 github.event.workflow_run.head_branch == 'main'
@@ -243,7 +244,17 @@ jobs:
243244 after_runs="$RUNNER_TEMP/tag-dispatch-runs-after.json"
244245 gh api --paginate --slurp \
245246 "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/runs?event=workflow_dispatch&per_page=100" \
246- | jq '[.[].workflow_runs[].id]' > "$before_runs"
247+ | jq '[.[].workflow_runs[]]' > "$before_runs"
248+ prior_count="$(jq \
249+ --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
250+ '[.[] | select(.actor.login == "github-actions[bot]" and
251+ .triggering_actor.login == "github-actions[bot]" and
252+ .event == "workflow_dispatch" and .head_branch == $tag and
253+ .head_sha == $commit)] | length' "$before_runs")"
254+ if [[ "$prior_count" != "0" ]]; then
255+ echo "An exact Publish run already exists for this immutable tag and commit." >&2
256+ exit 1
257+ fi
247258 gh api --method POST \
248259 "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/dispatches" \
249260 -f ref="$RELEASE_TAG" \
@@ -262,7 +273,7 @@ jobs:
262273 candidate_count="$(jq \
263274 --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
264275 --slurpfile before "$before_runs" \
265- '[.[] | select(.id as $id | ($before[0] | index($id) | not)) |
276+ '[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) |
266277 select(.actor.login == "github-actions[bot]" and
267278 .triggering_actor.login == "github-actions[bot]" and
268279 .event == "workflow_dispatch" and .head_branch == $tag and
@@ -275,7 +286,7 @@ jobs:
275286 publish_run_id="$(jq -r \
276287 --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
277288 --slurpfile before "$before_runs" \
278- '[.[] | select(.id as $id | ($before[0] | index($id) | not)) |
289+ '[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) |
279290 select(.actor.login == "github-actions[bot]" and
280291 .triggering_actor.login == "github-actions[bot]" and
281292 .event == "workflow_dispatch" and .head_branch == $tag and
@@ -296,6 +307,7 @@ jobs:
296307 if : >-
297308 vars.RELEASE_PLEASE_ENABLED == 'true' &&
298309 github.event_name == 'workflow_dispatch' &&
310+ github.run_attempt == 1 &&
299311 inputs.publish_operation == 'release' &&
300312 startsWith(github.ref, 'refs/tags/v0.1.') &&
301313 github.ref == format('refs/tags/{0}', inputs.release_tag) &&
@@ -312,11 +324,21 @@ jobs:
312324 artifact-name : ${{ steps.artifact-name.outputs.name }}
313325 control-commit : ${{ inputs.control_commit }}
314326 dist-tag : ${{ steps.version.outputs.dist-tag }}
327+ expected-next-version : ${{ steps.registry-baseline.outputs.next-version }}
315328 release-commit : ${{ steps.trust.outputs.release-commit }}
316329 release-tag : ${{ steps.trust.outputs.release-tag }}
317330 release-please-snapshot : ${{ steps.release-please-snapshot.outputs.digest }}
318331 version : ${{ steps.version.outputs.version }}
319332 steps :
333+ - name : Reject a repeated verification attempt
334+ env :
335+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
336+ run : |
337+ set -euo pipefail
338+ if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then
339+ echo "Exact-artifact verification is restricted to the initial run attempt." >&2
340+ exit 1
341+ fi
320342 - name : Check out the workflow control commit
321343 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
322344 with :
@@ -576,6 +598,18 @@ jobs:
576598 EOF
577599 - name : Use a Trusted Publishing-capable npm CLI
578600 run : npm install --global npm@11.12.1
601+ - name : Freeze the prerelease dist-tag
602+ id : registry-baseline
603+ shell : bash
604+ run : |
605+ set -euo pipefail
606+ next_version="$(npm view cometapi@next version)"
607+ NEXT_VERSION="$next_version" node --input-type=module <<'EOF'
608+ import { validatePrereleaseDistTagBaseline } from "./scripts/release-workflow-validation.mjs";
609+
610+ validatePrereleaseDistTagBaseline(process.env.NEXT_VERSION);
611+ EOF
612+ echo "next-version=${next_version}" >> "$GITHUB_OUTPUT"
579613 - name : Install locked dependencies
580614 run : npm ci
581615 - name : Run release checks
@@ -630,6 +664,9 @@ jobs:
630664
631665 live-smoke :
632666 name : Verify the release tag against CometAPI
667+ if : >-
668+ github.run_attempt == 1 &&
669+ needs.verify.result == 'success'
633670 needs :
634671 - verify
635672 concurrency :
@@ -641,6 +678,15 @@ jobs:
641678 # without required reviewers and add COMETAPI_KEY before publishing a release.
642679 environment : live-smoke
643680 steps :
681+ - name : Reject a repeated live-smoke attempt
682+ env :
683+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
684+ run : |
685+ set -euo pipefail
686+ if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then
687+ echo "The bounded live smoke is restricted to the initial run attempt." >&2
688+ exit 1
689+ fi
644690 - name : Check out the verified release tag
645691 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
646692 with :
@@ -668,6 +714,11 @@ jobs:
668714
669715 publish :
670716 name : Publish with npm Trusted Publishing
717+ if : >-
718+ always() &&
719+ (github.run_attempt == 1 || github.run_attempt == 2) &&
720+ needs.live-smoke.result == 'success' &&
721+ needs.verify.result == 'success'
671722 needs :
672723 - live-smoke
673724 - verify
@@ -684,6 +735,16 @@ jobs:
684735 deployments : read
685736 id-token : write
686737 steps :
738+ - name : Reject an out-of-bounds publication attempt
739+ env :
740+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
741+ run : |
742+ set -euo pipefail
743+ if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" &&
744+ "$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then
745+ echo "Publication permits only the initial attempt and one failed-job replay." >&2
746+ exit 1
747+ fi
687748 - name : Check out the verified release commit
688749 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
689750 with :
@@ -704,12 +765,14 @@ jobs:
704765 name : ${{ needs.verify.outputs.artifact-name }}
705766 path : release-artifacts
706767 - name : Reconfirm protected state immediately before publication
768+ id : pre-publish
707769 env :
708770 CONTROL_COMMIT : ${{ needs.verify.outputs.control-commit }}
709771 CONTROL_VALIDATOR : ${{ runner.temp }}/release-workflow-validation.mjs
710772 ENVIRONMENT_FILE : ${{ runner.temp }}/npm-environment.json
711773 EVENT_REF : ${{ github.ref }}
712774 EVENT_SHA : ${{ github.sha }}
775+ EXPECTED_NEXT_VERSION : ${{ needs.verify.outputs.expected-next-version }}
713776 GH_TOKEN : ${{ github.token }}
714777 POLICIES_FILE : ${{ runner.temp }}/npm-deployment-policies.json
715778 RELEASE_COMMIT : ${{ needs.verify.outputs.release-commit }}
@@ -722,6 +785,7 @@ jobs:
722785 RELEASE_PLEASE_RUNS : ${{ runner.temp }}/release-please-runs-before-publish.json
723786 RELEASE_PLEASE_SNAPSHOT : ${{ needs.verify.outputs.release-please-snapshot }}
724787 VERSION : ${{ needs.verify.outputs.version }}
788+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
725789 WORKFLOW_SHA : ${{ github.workflow_sha }}
726790 shell : bash
727791 run : |
@@ -793,6 +857,10 @@ jobs:
793857 exit 1
794858 fi
795859 fi
860+ if [[ "$WORKFLOW_RUN_ATTEMPT" == "2" && -z "$exact_version" ]]; then
861+ echo "The failed-job replay requires the exact registry version to exist." >&2
862+ exit 1
863+ fi
796864 latest_version="$(npm view cometapi@latest version)"
797865 next_version="$(npm view cometapi@next version)"
798866 gh api --paginate --slurp \
@@ -885,21 +953,29 @@ jobs:
885953 });
886954 validateRegistryStateBeforePublish({
887955 exactVersion: process.env.EXACT_VERSION || null,
956+ expectedNextVersion: process.env.EXPECTED_NEXT_VERSION,
888957 latestVersion: process.env.LATEST_VERSION,
889958 nextVersion: process.env.NEXT_VERSION,
890959 version,
891960 });
892961 EOF
962+ if [[ -n "$exact_version" ]]; then
963+ echo "expect-existing=true" >> "$GITHUB_OUTPUT"
964+ else
965+ echo "expect-existing=false" >> "$GITHUB_OUTPUT"
966+ fi
893967 - name : Publish the exact artifact with provenance
894968 env :
895969 DIST_TAG : ${{ needs.verify.outputs.dist-tag }}
970+ EXPECT_EXISTING : ${{ steps.pre-publish.outputs.expect-existing }}
896971 VERSION : ${{ needs.verify.outputs.version }}
897972 run : bash scripts/publish-artifact.sh
898973 - name : Verify the public registry artifact
899974 env :
900975 CONTROL_COMMIT : ${{ needs.verify.outputs.control-commit }}
901976 CONTROL_VALIDATOR : ${{ runner.temp }}/release-workflow-validation.mjs
902977 DIST_TAG : ${{ needs.verify.outputs.dist-tag }}
978+ EXPECTED_NEXT_VERSION : ${{ needs.verify.outputs.expected-next-version }}
903979 GH_TOKEN : ${{ github.token }}
904980 WORKFLOW_REF : ${{ github.ref }}
905981 VERSION : ${{ needs.verify.outputs.version }}
@@ -942,14 +1018,57 @@ jobs:
9421018 echo "Registry state did not converge for cometapi@${VERSION}, ${DIST_TAG}, integrity, and provenance." >&2
9431019 exit 1
9441020 fi
945- if [[ "$(npm view cometapi@next version)" != "0.1.0-alpha.3 " ]]; then
1021+ if [[ "$(npm view cometapi@next version)" != "$EXPECTED_NEXT_VERSION " ]]; then
9461022 echo "The next dist-tag changed during publication." >&2
9471023 exit 1
9481024 fi
9491025
9501026 attestations_url="$(REGISTRY_DIST="$registry_dist" node -e 'process.stdout.write(JSON.parse(process.env.REGISTRY_DIST).attestations.url)')"
9511027 attestations_file="$RUNNER_TEMP/npm-attestations.json"
952- curl --fail --silent --show-error "$attestations_url" > "$attestations_file"
1028+ ATTESTATIONS_URL="$attestations_url" \
1029+ node --input-type=module <<'EOF'
1030+ import { pathToFileURL } from "node:url";
1031+
1032+ const { validateRegistryAttestationUrl } = await import(
1033+ pathToFileURL(process.env.CONTROL_VALIDATOR)
1034+ );
1035+ validateRegistryAttestationUrl({
1036+ url: process.env.ATTESTATIONS_URL,
1037+ version: process.env.VERSION,
1038+ });
1039+ EOF
1040+ ATTESTATIONS_FILE="$attestations_file" \
1041+ ATTESTATIONS_URL="$attestations_url" \
1042+ bash scripts/fetch-attestations.sh
1043+
1044+ post_exact_version="$(npm view "cometapi@${VERSION}" version)"
1045+ post_tagged_version="$(npm view "cometapi@${DIST_TAG}" version)"
1046+ post_next_version="$(npm view cometapi@next version)"
1047+ post_registry_dist="$(npm view "cometapi@${VERSION}" dist --json)"
1048+ ATTESTATIONS_URL="$attestations_url" \
1049+ EXACT_VERSION="$post_exact_version" \
1050+ EXPECTED_INTEGRITY="$local_integrity" \
1051+ NEXT_VERSION="$post_next_version" \
1052+ REGISTRY_DIST="$post_registry_dist" \
1053+ TAGGED_VERSION="$post_tagged_version" \
1054+ node --input-type=module <<'EOF'
1055+ import { pathToFileURL } from "node:url";
1056+
1057+ const { validatePublishedRegistryState } = await import(
1058+ pathToFileURL(process.env.CONTROL_VALIDATOR)
1059+ );
1060+ validatePublishedRegistryState({
1061+ attestationUrl: process.env.ATTESTATIONS_URL,
1062+ dist: JSON.parse(process.env.REGISTRY_DIST),
1063+ exactVersion: process.env.EXACT_VERSION,
1064+ expectedNextVersion: process.env.EXPECTED_NEXT_VERSION,
1065+ expectedIntegrity: process.env.EXPECTED_INTEGRITY,
1066+ nextVersion: process.env.NEXT_VERSION,
1067+ taggedVersion: process.env.TAGGED_VERSION,
1068+ version: process.env.VERSION,
1069+ });
1070+ EOF
1071+ registry_dist="$post_registry_dist"
9531072 local_sha512="$(node -e 'const {createHash}=require("node:crypto");const {readFileSync}=require("node:fs");process.stdout.write(createHash("sha512").update(readFileSync(process.argv[1])).digest("hex"))' "${tarballs[0]}")"
9541073 provenance_identity="$(ATTESTATIONS_FILE="$attestations_file" \
9551074 LOCAL_SHA512="$local_sha512" node --input-type=module <<'EOF'
@@ -1176,3 +1295,36 @@ jobs:
11761295 process.exitCode = 1;
11771296 });
11781297 EOF
1298+
1299+ result :
1300+ name : Enforce the bounded publication result
1301+ if : >-
1302+ always() &&
1303+ github.event_name == 'workflow_dispatch' &&
1304+ inputs.publish_operation == 'release'
1305+ needs :
1306+ - live-smoke
1307+ - publish
1308+ - verify
1309+ runs-on : ubuntu-latest
1310+ permissions : {}
1311+ steps :
1312+ - name : Reject skipped or out-of-bounds publication
1313+ env :
1314+ LIVE_SMOKE_RESULT : ${{ needs.live-smoke.result }}
1315+ PUBLISH_RESULT : ${{ needs.publish.result }}
1316+ VERIFY_RESULT : ${{ needs.verify.result }}
1317+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
1318+ run : |
1319+ set -euo pipefail
1320+ if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" &&
1321+ "$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then
1322+ echo "Publication permits only the initial attempt and one failed-job replay." >&2
1323+ exit 1
1324+ fi
1325+ if [[ "$VERIFY_RESULT" != "success" ||
1326+ "$LIVE_SMOKE_RESULT" != "success" ||
1327+ "$PUBLISH_RESULT" != "success" ]]; then
1328+ echo "The bounded publication job set did not complete successfully." >&2
1329+ exit 1
1330+ fi
0 commit comments