From 93509ba263732482df8f00ced036aa24683acdca Mon Sep 17 00:00:00 2001 From: Vivswan Shah <58091053+Vivswan@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:20:28 -0400 Subject: [PATCH 1/2] fix(release): follow the attestation bundle rename in the repo-owned asset check and SECURITY.md The template sync in #74 renamed the release attestation bundle the managed publish stage attaches from attestation.jsonl to attestation.json. The repo-owned pre-publish asset check still ignored only the old name, so a rerun after the publish stage had attached the new bundle would count it as an unexpected third asset and stop the pipeline; SECURITY.md still told readers to verify against the old asset name. The check now ignores both names: the managed cleanup deletes only the current one, so a draft a pre-rename attempt left an attestation.jsonl on keeps it. SECURITY.md and the workflow's comments name the new asset. --- .github/workflows/update-release.yml | 13 ++++++++----- SECURITY.md | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-release.yml b/.github/workflows/update-release.yml index cfd5379..d12f600 100644 --- a/.github/workflows/update-release.yml +++ b/.github/workflows/update-release.yml @@ -20,7 +20,7 @@ # bundle and schema to the draft (draft-first because published # releases freeze their assets), # 4. confirms origin's refs and the draft's assets before the managed -# publish stage attests every asset (attestation.jsonl; public +# publish stage attests every asset (attestation.json; public # repositories only, by fleet policy) and flips the release live. # When the draft is published, GitHub binds it to the existing # vX.Y.Z tag, so the release lands on the packaged commit. @@ -38,7 +38,7 @@ # Finish a dead run by re-running its FAILED jobs. The same steps by hand # ( is the draft release's target commitish; a hand-packaged # commit carries no workflow-run trailer, and a hand-finished release no -# attestation.jsonl - both need the workflow's identity): +# attestation.json - both need the workflow's identity): # git checkout # bun install --frozen-lockfile --ignore-scripts && bun run build # TAG=vX.Y.Z GITHUB_SHA= bun .github/scripts/release-pipeline.ts package @@ -159,16 +159,19 @@ jobs: run: GITHUB_SHA="$SOURCE_SHA" bun .github/scripts/release-pipeline.ts verify # Publishing freezes the asset list, so an incomplete one must stop # the pipeline here, while the release is still a draft (v2.0.0 - # shipped assetless exactly this way). attestation.jsonl is the + # shipped assetless exactly this way). attestation.json is the # managed publish stage's asset: attached after this job, replaced on - # its reruns - ignore it rather than racing it. + # its reruns - ignore it rather than racing it. attestation.jsonl is + # its name before the template renamed it; a draft a pre-rename + # attempt left one on keeps it (the publish stage deletes only the + # current name), so it is ignored the same way. - name: Confirm the release carries both packaged assets env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.tag }} run: | assets="$(gh release view "$TAG" --json assets \ - --jq '[.assets[].name | select(. != "attestation.jsonl")] | sort | join(" ")')" + --jq '[.assets[].name | select(. != "attestation.json" and . != "attestation.jsonl")] | sort | join(" ")')" expected="index.js settings.schema.json" if [ "$assets" != "$expected" ]; then echo "::error::release $TAG carries assets [$assets], expected [$expected]; re-run package-release before anything publishes." diff --git a/SECURITY.md b/SECURITY.md index 9d07641..94ad024 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -31,7 +31,7 @@ This action holds a repository-admin token and writes repository settings, so th - Settings escalation. A crafted settings file should never be able to touch a repository or setting it does not declare, nor bypass the preflight barrier or the required-sections policy. - Supply chain. Every ref a `uses:` pin can name - the `vX.Y.Z` release tags and the moving major - points at a packaged commit parented on the audited release commit, produced by the release workflow run named in its provenance message; main carries no executable bundle. The release-tags ruleset freezes version tags for everything except deliberate repository-admin action (the bypass exists for repair; the release workflow itself never moves a version tag, reruns verify the existing one byte-for-byte instead). A packaged commit whose bundle a rebuild of its parent's src/ does not reproduce is a vulnerability. The release pipeline also attests every release asset (`lib/index.js` and `lib/settings.schema.json`) in one build-provenance attestation while the repository is public (GitHub offers no attestations elsewhere): fetch either from the `vX.Y.Z` tag or the release assets, then check it with `gh attestation verify -R vivswan/github-settings-as-code` (add - `--signer-workflow vivswan/github-settings-as-code/.github/workflows/release.yml` to also pin the producing workflow), or without the attestations API via `--bundle` against the `attestation.jsonl` release asset. A release finished by hand carries no attestation at all, and its packaged commit no workflow-run trailer: both need the workflow's OIDC identity. + `--signer-workflow vivswan/github-settings-as-code/.github/workflows/release.yml` to also pin the producing workflow), or without the attestations API via `--bundle` against the `attestation.json` release asset. A release finished by hand carries no attestation at all, and its packaged commit no workflow-run trailer: both need the workflow's OIDC identity. Fixes ship in the next release and are not backported; upgrade the `uses:` pin to pick them up. From fd119af67c64dd4c344b328729f701bd7a28b810 Mon Sep 17 00:00:00 2001 From: Vivswan Shah <58091053+Vivswan@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:36:09 -0400 Subject: [PATCH 2/2] fix(release): delete a legacy attestation.jsonl from the draft instead of ignoring it A leftover from a pre-rename attempt is stale provenance the managed publish stage no longer cleans up (it deletes only the current bundle name), so the repo-owned asset check deletes it before comparing and ignores only attestation.json. --- .github/workflows/update-release.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/update-release.yml b/.github/workflows/update-release.yml index d12f600..80771e5 100644 --- a/.github/workflows/update-release.yml +++ b/.github/workflows/update-release.yml @@ -159,19 +159,21 @@ jobs: run: GITHUB_SHA="$SOURCE_SHA" bun .github/scripts/release-pipeline.ts verify # Publishing freezes the asset list, so an incomplete one must stop # the pipeline here, while the release is still a draft (v2.0.0 - # shipped assetless exactly this way). attestation.json is the - # managed publish stage's asset: attached after this job, replaced on - # its reruns - ignore it rather than racing it. attestation.jsonl is - # its name before the template renamed it; a draft a pre-rename - # attempt left one on keeps it (the publish stage deletes only the - # current name), so it is ignored the same way. + # shipped assetless exactly this way). attestation.json is the managed + # publish stage's asset, attached after this job and replaced on its + # reruns, so the check ignores it. A legacy attestation.jsonl (the + # name before the template renamed it) is stale provenance that stage + # no longer cleans up, so it is deleted here before the check. - name: Confirm the release carries both packaged assets env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.tag }} run: | + if gh release view "$TAG" --json assets --jq '.assets[].name' | grep -qx 'attestation.jsonl'; then + gh release delete-asset "$TAG" attestation.jsonl --yes + fi assets="$(gh release view "$TAG" --json assets \ - --jq '[.assets[].name | select(. != "attestation.json" and . != "attestation.jsonl")] | sort | join(" ")')" + --jq '[.assets[].name | select(. != "attestation.json")] | sort | join(" ")')" expected="index.js settings.schema.json" if [ "$assets" != "$expected" ]; then echo "::error::release $TAG carries assets [$assets], expected [$expected]; re-run package-release before anything publishes."