Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 157 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
if: >-
vars.RELEASE_PLEASE_ENABLED == 'true' &&
github.event_name == 'workflow_run' &&
github.run_attempt == 1 &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
Expand Down Expand Up @@ -243,7 +244,17 @@ jobs:
after_runs="$RUNNER_TEMP/tag-dispatch-runs-after.json"
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/runs?event=workflow_dispatch&per_page=100" \
| jq '[.[].workflow_runs[].id]' > "$before_runs"
| jq '[.[].workflow_runs[]]' > "$before_runs"
prior_count="$(jq \
--arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
'[.[] | select(.actor.login == "github-actions[bot]" and
.triggering_actor.login == "github-actions[bot]" and
.event == "workflow_dispatch" and .head_branch == $tag and
.head_sha == $commit)] | length' "$before_runs")"
if [[ "$prior_count" != "0" ]]; then
echo "An exact Publish run already exists for this immutable tag and commit." >&2
exit 1
fi
gh api --method POST \
"repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/dispatches" \
-f ref="$RELEASE_TAG" \
Expand All @@ -262,7 +273,7 @@ jobs:
candidate_count="$(jq \
--arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
--slurpfile before "$before_runs" \
'[.[] | select(.id as $id | ($before[0] | index($id) | not)) |
'[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) |
select(.actor.login == "github-actions[bot]" and
.triggering_actor.login == "github-actions[bot]" and
.event == "workflow_dispatch" and .head_branch == $tag and
Expand All @@ -275,7 +286,7 @@ jobs:
publish_run_id="$(jq -r \
--arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \
--slurpfile before "$before_runs" \
'[.[] | select(.id as $id | ($before[0] | index($id) | not)) |
'[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) |
select(.actor.login == "github-actions[bot]" and
.triggering_actor.login == "github-actions[bot]" and
.event == "workflow_dispatch" and .head_branch == $tag and
Expand All @@ -296,6 +307,7 @@ jobs:
if: >-
vars.RELEASE_PLEASE_ENABLED == 'true' &&
github.event_name == 'workflow_dispatch' &&
github.run_attempt == 1 &&
inputs.publish_operation == 'release' &&
startsWith(github.ref, 'refs/tags/v0.1.') &&
github.ref == format('refs/tags/{0}', inputs.release_tag) &&
Expand All @@ -312,11 +324,21 @@ jobs:
artifact-name: ${{ steps.artifact-name.outputs.name }}
control-commit: ${{ inputs.control_commit }}
dist-tag: ${{ steps.version.outputs.dist-tag }}
expected-next-version: ${{ steps.registry-baseline.outputs.next-version }}
release-commit: ${{ steps.trust.outputs.release-commit }}
release-tag: ${{ steps.trust.outputs.release-tag }}
release-please-snapshot: ${{ steps.release-please-snapshot.outputs.digest }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Reject a repeated verification attempt
env:
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then
echo "Exact-artifact verification is restricted to the initial run attempt." >&2
exit 1
fi
- name: Check out the workflow control commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -576,6 +598,18 @@ jobs:
EOF
- name: Use a Trusted Publishing-capable npm CLI
run: npm install --global npm@11.12.1
- name: Freeze the prerelease dist-tag
id: registry-baseline
shell: bash
run: |
set -euo pipefail
next_version="$(npm view cometapi@next version)"
NEXT_VERSION="$next_version" node --input-type=module <<'EOF'
import { validatePrereleaseDistTagBaseline } from "./scripts/release-workflow-validation.mjs";

validatePrereleaseDistTagBaseline(process.env.NEXT_VERSION);
EOF
echo "next-version=${next_version}" >> "$GITHUB_OUTPUT"
- name: Install locked dependencies
run: npm ci
- name: Run release checks
Expand Down Expand Up @@ -630,6 +664,9 @@ jobs:

live-smoke:
name: Verify the release tag against CometAPI
if: >-
github.run_attempt == 1 &&
needs.verify.result == 'success'
needs:
- verify
concurrency:
Expand All @@ -641,6 +678,15 @@ jobs:
# without required reviewers and add COMETAPI_KEY before publishing a release.
environment: live-smoke
steps:
- name: Reject a repeated live-smoke attempt
env:
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then
echo "The bounded live smoke is restricted to the initial run attempt." >&2
exit 1
fi
- name: Check out the verified release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -668,6 +714,11 @@ jobs:

publish:
name: Publish with npm Trusted Publishing
if: >-
always() &&
(github.run_attempt == 1 || github.run_attempt == 2) &&
needs.live-smoke.result == 'success' &&
needs.verify.result == 'success'
needs:
- live-smoke
- verify
Expand All @@ -684,6 +735,16 @@ jobs:
deployments: read
id-token: write
steps:
- name: Reject an out-of-bounds publication attempt
env:
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" &&
"$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then
echo "Publication permits only the initial attempt and one failed-job replay." >&2
exit 1
fi
- name: Check out the verified release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -704,12 +765,14 @@ jobs:
name: ${{ needs.verify.outputs.artifact-name }}
path: release-artifacts
- name: Reconfirm protected state immediately before publication
id: pre-publish
env:
CONTROL_COMMIT: ${{ needs.verify.outputs.control-commit }}
CONTROL_VALIDATOR: ${{ runner.temp }}/release-workflow-validation.mjs
ENVIRONMENT_FILE: ${{ runner.temp }}/npm-environment.json
EVENT_REF: ${{ github.ref }}
EVENT_SHA: ${{ github.sha }}
EXPECTED_NEXT_VERSION: ${{ needs.verify.outputs.expected-next-version }}
GH_TOKEN: ${{ github.token }}
POLICIES_FILE: ${{ runner.temp }}/npm-deployment-policies.json
RELEASE_COMMIT: ${{ needs.verify.outputs.release-commit }}
Expand All @@ -722,6 +785,7 @@ jobs:
RELEASE_PLEASE_RUNS: ${{ runner.temp }}/release-please-runs-before-publish.json
RELEASE_PLEASE_SNAPSHOT: ${{ needs.verify.outputs.release-please-snapshot }}
VERSION: ${{ needs.verify.outputs.version }}
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
shell: bash
run: |
Expand Down Expand Up @@ -793,6 +857,10 @@ jobs:
exit 1
fi
fi
if [[ "$WORKFLOW_RUN_ATTEMPT" == "2" && -z "$exact_version" ]]; then
echo "The failed-job replay requires the exact registry version to exist." >&2
exit 1
fi
latest_version="$(npm view cometapi@latest version)"
next_version="$(npm view cometapi@next version)"
gh api --paginate --slurp \
Expand Down Expand Up @@ -885,21 +953,29 @@ jobs:
});
validateRegistryStateBeforePublish({
exactVersion: process.env.EXACT_VERSION || null,
expectedNextVersion: process.env.EXPECTED_NEXT_VERSION,
latestVersion: process.env.LATEST_VERSION,
nextVersion: process.env.NEXT_VERSION,
version,
});
EOF
if [[ -n "$exact_version" ]]; then
echo "expect-existing=true" >> "$GITHUB_OUTPUT"
else
echo "expect-existing=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish the exact artifact with provenance
env:
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
EXPECT_EXISTING: ${{ steps.pre-publish.outputs.expect-existing }}
VERSION: ${{ needs.verify.outputs.version }}
run: bash scripts/publish-artifact.sh
- name: Verify the public registry artifact
env:
CONTROL_COMMIT: ${{ needs.verify.outputs.control-commit }}
CONTROL_VALIDATOR: ${{ runner.temp }}/release-workflow-validation.mjs
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
EXPECTED_NEXT_VERSION: ${{ needs.verify.outputs.expected-next-version }}
GH_TOKEN: ${{ github.token }}
WORKFLOW_REF: ${{ github.ref }}
VERSION: ${{ needs.verify.outputs.version }}
Expand Down Expand Up @@ -942,14 +1018,57 @@ jobs:
echo "Registry state did not converge for cometapi@${VERSION}, ${DIST_TAG}, integrity, and provenance." >&2
exit 1
fi
if [[ "$(npm view cometapi@next version)" != "0.1.0-alpha.3" ]]; then
if [[ "$(npm view cometapi@next version)" != "$EXPECTED_NEXT_VERSION" ]]; then
echo "The next dist-tag changed during publication." >&2
exit 1
fi

attestations_url="$(REGISTRY_DIST="$registry_dist" node -e 'process.stdout.write(JSON.parse(process.env.REGISTRY_DIST).attestations.url)')"
attestations_file="$RUNNER_TEMP/npm-attestations.json"
curl --fail --silent --show-error "$attestations_url" > "$attestations_file"
ATTESTATIONS_URL="$attestations_url" \
node --input-type=module <<'EOF'
import { pathToFileURL } from "node:url";

const { validateRegistryAttestationUrl } = await import(
pathToFileURL(process.env.CONTROL_VALIDATOR)
);
validateRegistryAttestationUrl({
url: process.env.ATTESTATIONS_URL,
version: process.env.VERSION,
});
EOF
ATTESTATIONS_FILE="$attestations_file" \
ATTESTATIONS_URL="$attestations_url" \
bash scripts/fetch-attestations.sh

post_exact_version="$(npm view "cometapi@${VERSION}" version)"
post_tagged_version="$(npm view "cometapi@${DIST_TAG}" version)"
post_next_version="$(npm view cometapi@next version)"
post_registry_dist="$(npm view "cometapi@${VERSION}" dist --json)"
ATTESTATIONS_URL="$attestations_url" \
EXACT_VERSION="$post_exact_version" \
EXPECTED_INTEGRITY="$local_integrity" \
NEXT_VERSION="$post_next_version" \
REGISTRY_DIST="$post_registry_dist" \
TAGGED_VERSION="$post_tagged_version" \
node --input-type=module <<'EOF'
import { pathToFileURL } from "node:url";

const { validatePublishedRegistryState } = await import(
pathToFileURL(process.env.CONTROL_VALIDATOR)
);
validatePublishedRegistryState({
attestationUrl: process.env.ATTESTATIONS_URL,
dist: JSON.parse(process.env.REGISTRY_DIST),
exactVersion: process.env.EXACT_VERSION,
expectedNextVersion: process.env.EXPECTED_NEXT_VERSION,
expectedIntegrity: process.env.EXPECTED_INTEGRITY,
nextVersion: process.env.NEXT_VERSION,
taggedVersion: process.env.TAGGED_VERSION,
version: process.env.VERSION,
});
EOF
registry_dist="$post_registry_dist"
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]}")"
provenance_identity="$(ATTESTATIONS_FILE="$attestations_file" \
LOCAL_SHA512="$local_sha512" node --input-type=module <<'EOF'
Expand Down Expand Up @@ -1176,3 +1295,36 @@ jobs:
process.exitCode = 1;
});
EOF

result:
name: Enforce the bounded publication result
if: >-
always() &&
github.event_name == 'workflow_dispatch' &&
inputs.publish_operation == 'release'
needs:
- live-smoke
- publish
- verify
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Reject skipped or out-of-bounds publication
env:
LIVE_SMOKE_RESULT: ${{ needs.live-smoke.result }}
PUBLISH_RESULT: ${{ needs.publish.result }}
VERIFY_RESULT: ${{ needs.verify.result }}
WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" &&
"$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then
echo "Publication permits only the initial attempt and one failed-job replay." >&2
exit 1
fi
if [[ "$VERIFY_RESULT" != "success" ||
"$LIVE_SMOKE_RESULT" != "success" ||
"$PUBLISH_RESULT" != "success" ]]; then
echo "The bounded publication job set did not complete successfully." >&2
exit 1
fi
27 changes: 25 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,32 @@ repository root.
- Prepare or refresh a release PR with a new first-attempt manual Release Please
dispatch. Do not rerun a preparation dispatch. Release Please same-run Release
reconciliation is allowed only under the exact conditions in `RELEASING.md`.
- The successful Release Please `workflow_run` handoff is attempt-1-only and
must refuse to dispatch when any exact Publish child already exists for the
immutable tag and commit. Never rerun a handoff to create a second child run.
- Never bypass a failed stable release with a manual or auxiliary tag, a
branch-context publish, a temporary `main` npm Environment policy, reused
artifact or live evidence, an arbitrary rerun, or a different patch version.
branch-context publish, a temporary `main` npm Environment policy, cross-run
artifact or live-evidence reuse, an arbitrary rerun, or a different patch
version.
- If `npm publish` succeeds but post-publication verification fails, first read
the exact public version, `latest`, `next`, integrity, and every readable
attestation, signature, and provenance field. When the exact version is
absent, `latest` must equal the previous patch. When the exact version equals
the candidate, `latest` may equal the previous patch or candidate. Final state
requires both exact and `latest` to equal the candidate, while `next` must
equal the prerelease value frozen by the initial verification job. A transient
early attestation-endpoint `404` is the known registry convergence condition;
all transport failures receive one wall-clock-bounded wait, and exhaustion is
terminal. Do not rerun to extend it. Only when attestations are readable and
independent signature and provenance checks have passed may exactly one
`rerun failed jobs` on the same immutable-tag run resume a different failed
post-publication gate. Confirm that GitHub preserves the successful
exact-artifact and live-smoke jobs; rerun-all must fail before live API access.
The attempt-2 publish job requires the exact version to exist, matching
integrity, and a fresh Environment approval; it must skip `npm publish`.
Attempt 3 or later, an exact-version metadata `E404`, a second replay request,
non-convergence, or any identity, integrity, provenance, dist-tag, or
configuration mismatch is a hard stop.
- After registry verification, immediately restore
`RELEASE_PLEASE_ENABLED=false`, keep `LIVE_SMOKE_ENABLED=true`, and require the
npm Environment deployment-policy set to contain only `tag:v*`.
Expand Down
21 changes: 20 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ the exact Release-producing attempt, SHA, tag, version, URL, repository,
workflow identity, run ID, and attempt in a schema-v2 exact-run artifact. An
unprivileged `workflow_run` handoff validates only that attempt's artifact,
exact tag, immutable Release, and current `main`, then dispatches `publish.yml`
with `ref=v<version>`. Only that tag-bound `workflow_dispatch` can reach fresh
with `ref=v<version>`. The handoff is attempt-1-only and refuses to dispatch if
an exact child run already exists for the tag and commit. Only that tag-bound
`workflow_dispatch` can reach fresh
artifact verification, bounded live smoke, the npm Environment, or OIDC. A
first-attempt manual run is explicitly release-inert and must succeed only after
independently validating one canonical action-created patch PR; its event cannot
Expand All @@ -153,6 +155,23 @@ Release Please and publication remain separate trust domains. Release Please
does not receive npm OIDC permission; `id-token: write` remains limited to the
protected publish job. Repository variables gate both flows, and reruns remain
fail-closed on exact tag, artifact, dist-tag, integrity, and provenance state.
Registry package metadata and its attestation endpoint can converge at
different times. Post-publication verification therefore gives attestation HTTP
failures one strict wall-clock-bounded retry window before failing; an early
`404` is the known convergence case, while persistent URL, authentication,
authorization, server, and transport failures remain terminal. The initial
verification job freezes the prerelease dist-tag value, and every later registry
gate requires it to remain unchanged rather than encoding a current package
version in durable workflow guidance. If publication succeeded before a later
gate failed, only one attempt-2 failed-job replay of that same immutable-tag run
may continue, and only after attestations, signature, and provenance are already
valid. Verification and bounded live smoke are attempt-1-only, so rerun-all
fails before live API access; attempt 3 or later also fails. The protected-state
step requires the exact version to exist on replay; `publish-artifact.sh` then
skips registry mutation only after its integrity matches the verified tarball
and refuses a later `E404` instead of republishing. The failed-job replay
preserves the same run's successful artifact and bounded live-smoke jobs rather
than borrowing evidence from another run.
Manual preparation rejects attempt 2 or later; restart uses a new dispatch with
Release creation disabled. A `push` rerun is bounded to the same run ID, SHA,
candidate, and final-head review. It may retry while the tag and Release remain
Expand Down
Loading