@@ -6,6 +6,9 @@ name: release
66# Workflow filename: release.yml (filename only, not a path)
77# Environment: npm-release (must match job.environment)
88# No NPM_TOKEN / NODE_AUTH_TOKEN — those block OIDC.
9+ # Protect v* tags from updates and deletion with a repository ruleset. The
10+ # runtime checks below detect in-flight changes, but protection closes the
11+ # unavoidable race after the final remote-tag fetch.
912on :
1013 push :
1114 tags :
1417permissions :
1518 contents : write
1619 id-token : write # required for npm OIDC
20+ pages : write
1721
1822jobs :
1923 publish :
2024 runs-on : ubuntu-latest
2125 environment : npm-release
2226 steps :
2327 - uses : actions/checkout@v5
28+ with :
29+ fetch-depth : 0
2430
2531 # Node 24 → npm ≥ 11.5.1 (required for trusted publishing).
2632 # Do NOT set registry-url: setup-node would inject a dummy NODE_AUTH_TOKEN
@@ -35,20 +41,133 @@ jobs:
3541 - run : npm run build
3642 - run : npm test
3743
44+ - name : Verify release tag matches CLI package version
45+ run : node scripts/check-release-tag.mjs
46+
47+ - name : Verify main contains and declares this release
48+ run : |
49+ git fetch origin main
50+ git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
51+ [[ "$(git show origin/main:website/versions.json | node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => console.log(JSON.parse(s)[0]))')" == "${GITHUB_REF_NAME#v}" ]]
52+ node scripts/check-release-docs-provenance.mjs "${GITHUB_SHA}" origin/main
53+
54+ - name : Build frozen release docs
55+ env :
56+ UMAMI_WEBSITE_ID : ${{ vars.UMAMI_WEBSITE_ID }}
57+ run : npm run build:docs
58+
59+ - name : Reverify main immediately before publication
60+ run : |
61+ git fetch origin main
62+ git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
63+ [[ "$(git show origin/main:website/versions.json | node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => console.log(JSON.parse(s)[0]))')" == "${GITHUB_REF_NAME#v}" ]]
64+ node scripts/check-release-docs-provenance.mjs "${GITHUB_SHA}" origin/main
65+ git fetch origin "+refs/tags/${GITHUB_REF_NAME}:refs/know-code/release-tag"
66+ [[ "$(git rev-parse refs/know-code/release-tag^{commit})" == "$(git rev-parse "${GITHUB_SHA}^{commit}")" ]]
67+
3868 - name : Publish CLI to npm (OIDC)
3969 working-directory : packages/cli
4070 run : |
4171 unset NODE_AUTH_TOKEN
42- npm publish --access public
72+ PACKAGE_VERSION="$(npm pkg get version --workspaces=false | tr -d '"')"
73+ verify_provenance() {
74+ npm view "@chtnnh/know-code@${PACKAGE_VERSION}" dist.attestations --json >/tmp/know-code-attestations.json || return 1
75+ node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(0, "utf8")); if (!value || typeof value !== "object" || !value.provenance || typeof value.provenance !== "object" || value.provenance.predicateType !== "https://slsa.dev/provenance/v1") process.exit(1)' < /tmp/know-code-attestations.json || return 1
76+ VERIFY_DIR="$(mktemp -d)" || return 1
77+ trap 'rm -rf "${VERIFY_DIR}"' RETURN
78+ (cd "${VERIFY_DIR}" && npm init --yes >/dev/null) || return 1
79+ npm install --prefix "${VERIFY_DIR}" --ignore-scripts "@chtnnh/know-code@${PACKAGE_VERSION}" || return 1
80+ npm audit signatures --prefix "${VERIFY_DIR}" --json || return 1
81+ }
82+ if npm view "@chtnnh/know-code@${PACKAGE_VERSION}" gitHead --json >/tmp/know-code-version.json 2>/tmp/know-code-version.err; then
83+ PUBLISHED_GIT_HEAD="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(0, "utf8")); if (typeof value !== "string" || !/^[0-9a-f]{40}$/i.test(value)) process.exit(1); process.stdout.write(value)' < /tmp/know-code-version.json)"
84+ EXPECTED_GIT_HEAD="$(git rev-parse "${GITHUB_SHA}^{commit}")"
85+ if [[ "${PUBLISHED_GIT_HEAD}" != "${EXPECTED_GIT_HEAD}" ]]; then
86+ echo "@chtnnh/know-code@${PACKAGE_VERSION} belongs to ${PUBLISHED_GIT_HEAD}, not ${EXPECTED_GIT_HEAD}." >&2
87+ exit 1
88+ fi
89+ verify_provenance
90+ echo "@chtnnh/know-code@${PACKAGE_VERSION} is already published from this tag; skipping npm publish."
91+ elif grep -q "E404" /tmp/know-code-version.err; then
92+ npm publish --provenance --access public
93+ for attempt in {1..12}; do
94+ if verify_provenance; then exit 0; fi
95+ sleep 5
96+ done
97+ echo "npm provenance did not become verifiable for @chtnnh/know-code@${PACKAGE_VERSION}." >&2
98+ exit 1
99+ else
100+ cat /tmp/know-code-version.err >&2
101+ exit 1
102+ fi
43103
44104 - name : Create GitHub Release
45105 env :
46106 GH_TOKEN : ${{ github.token }}
47107 run : |
48108 TAG="${GITHUB_REF_NAME}"
109+ git fetch origin "+refs/tags/${TAG}:refs/know-code/release-tag"
110+ [[ "$(git rev-parse refs/know-code/release-tag^{commit})" == "$(git rev-parse "${GITHUB_SHA}^{commit}")" ]]
49111 NOTES="Release ${TAG}. See CHANGELOG.md."
50112 if [[ -f CHANGELOG.md ]]; then
51113 NOTES="$(sed -n "/^## ${TAG#v}/,/^## /p" CHANGELOG.md | sed '$d' || true)"
52114 [[ -n "$NOTES" ]] || NOTES="Release ${TAG}. See CHANGELOG.md."
53115 fi
54- gh release create "$TAG" --title "$TAG" --notes "$NOTES"
116+ if gh api --include "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" >/tmp/know-code-release.response 2>/tmp/know-code-release.err; then
117+ gh release edit "$TAG" --title "$TAG" --notes "$NOTES"
118+ elif head -n 1 /tmp/know-code-release.response | grep -Eq '^HTTP/[^ ]+ 404( |$)'; then
119+ gh release create "$TAG" --verify-tag --title "$TAG" --notes "$NOTES"
120+ else
121+ cat /tmp/know-code-release.err >&2
122+ exit 1
123+ fi
124+
125+ deploy-docs :
126+ needs : publish
127+ runs-on : ubuntu-latest
128+ environment :
129+ name : github-pages
130+ concurrency :
131+ group : pages
132+ cancel-in-progress : false
133+ queue : max
134+
135+ steps :
136+ - uses : actions/checkout@v5
137+ with :
138+ ref : main
139+ fetch-depth : 0
140+
141+ - uses : actions/setup-node@v5
142+ with :
143+ node-version : " 22"
144+ cache : npm
145+
146+ - name : Verify main still declares this release
147+ run : |
148+ [[ "$(node -p 'require("./website/versions.json")[0]')" == "${GITHUB_REF_NAME#v}" ]]
149+ node scripts/check-release-docs-provenance.mjs "${GITHUB_REF_NAME}" HEAD
150+
151+ - name : Build current docs
152+ env :
153+ UMAMI_WEBSITE_ID : ${{ vars.UMAMI_WEBSITE_ID }}
154+ run : |
155+ npm install
156+ npm run build:docs
157+
158+ - name : Upload current docs artifact
159+ uses : actions/upload-pages-artifact@v5
160+ with :
161+ name : github-pages-${{ github.run_attempt }}
162+ path : website/build
163+ include-hidden-files : true
164+
165+ - name : Confirm main did not advance before deployment
166+ run : |
167+ git fetch origin main
168+ [[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]
169+
170+ - name : Deploy frozen release docs
171+ uses : actions/deploy-pages@v5
172+ with :
173+ artifact_name : github-pages-${{ github.run_attempt }}
0 commit comments