Skip to content

Commit 37d82df

Browse files
authored
Merge pull request #29 from chtnnh/release/0.3.1-prep
release: harden and prepare 0.3.1 publication
2 parents 32db54a + 15e0f44 commit 37d82df

34 files changed

Lines changed: 2403 additions & 47 deletions

.github/workflows/docs.yml

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,61 @@ name: docs
55
on:
66
push:
77
branches: [main]
8-
paths:
9-
- "website/**"
10-
- ".github/workflows/docs.yml"
118
workflow_dispatch:
129

1310
permissions:
1411
contents: read
1512
pages: write
1613
id-token: write
1714

18-
concurrency:
19-
group: pages
20-
cancel-in-progress: true
21-
2215
jobs:
23-
build:
16+
published-release:
2417
runs-on: ubuntu-latest
18+
outputs:
19+
ready: ${{ steps.release.outputs.ready }}
20+
steps:
21+
- uses: actions/checkout@v5
22+
with:
23+
fetch-depth: 0
24+
25+
- id: release
26+
run: |
27+
VERSION="$(node -p 'require("./website/versions.json")[0]')"
28+
verify_provenance() {
29+
npm view "@chtnnh/know-code@${VERSION}" dist.attestations --json >/tmp/know-code-attestations.json || return 1
30+
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
31+
VERIFY_DIR="$(mktemp -d)" || return 1
32+
trap 'rm -rf "${VERIFY_DIR}"' RETURN
33+
(cd "${VERIFY_DIR}" && npm init --yes >/dev/null) || return 1
34+
npm install --prefix "${VERIFY_DIR}" --ignore-scripts "@chtnnh/know-code@${VERSION}" || return 1
35+
npm audit signatures --prefix "${VERIFY_DIR}" --json || return 1
36+
}
37+
if npm view "@chtnnh/know-code@${VERSION}" gitHead --json >/tmp/know-code-version.json 2>/tmp/know-code-version.err; then
38+
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)"
39+
verify_provenance
40+
git fetch origin main --tags
41+
TAG_GIT_HEAD="$(git rev-parse "v${VERSION}^{commit}")"
42+
git merge-base --is-ancestor "${TAG_GIT_HEAD}" origin/main
43+
[[ "${PUBLISHED_GIT_HEAD}" == "${TAG_GIT_HEAD}" ]]
44+
node scripts/check-release-docs-provenance.mjs "v${VERSION}" "${GITHUB_SHA}"
45+
echo "ready=true" >> "$GITHUB_OUTPUT"
46+
elif grep -q "E404" /tmp/know-code-version.err; then
47+
echo "ready=false" >> "$GITHUB_OUTPUT"
48+
else
49+
cat /tmp/know-code-version.err >&2
50+
exit 1
51+
fi
52+
53+
deploy:
54+
needs: published-release
55+
if: needs.published-release.outputs.ready == 'true'
56+
runs-on: ubuntu-latest
57+
concurrency:
58+
group: pages
59+
cancel-in-progress: false
60+
queue: max
61+
environment:
62+
name: github-pages
2563
steps:
2664
- uses: actions/checkout@v5
2765

@@ -40,16 +78,17 @@ jobs:
4078
- name: Upload Pages artifact
4179
uses: actions/upload-pages-artifact@v5
4280
with:
81+
name: github-pages-${{ github.run_attempt }}
4382
path: website/build
4483
# v4+ excludes dotfiles by default; Docusaurus needs .nojekyll.
4584
include-hidden-files: true
4685

47-
deploy:
48-
needs: build
49-
runs-on: ubuntu-latest
50-
environment:
51-
name: github-pages
52-
url: ${{ steps.deployment.outputs.page_url }}
53-
steps:
54-
- id: deployment
86+
- name: Confirm this is still main's latest commit
87+
run: |
88+
git fetch origin main
89+
[[ "${GITHUB_SHA}" == "$(git rev-parse origin/main)" ]]
90+
91+
- name: Deploy Pages
5592
uses: actions/deploy-pages@v5
93+
with:
94+
artifact_name: github-pages-${{ github.run_attempt }}

.github/workflows/release.yml

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
912
on:
1013
push:
1114
tags:
@@ -14,13 +17,16 @@ on:
1417
permissions:
1518
contents: write
1619
id-token: write # required for npm OIDC
20+
pages: write
1721

1822
jobs:
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 }}

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## Unreleased
44

5+
## 0.3.1
6+
7+
### Fixes
8+
- Fix the Umami proxy Worker’s script and event request forwarding, including a cache-versioned loader so updated upstream scripts do not depend on a manual Cloudflare cache purge.
9+
- Make push verification walk stacked landed runs against their historical tree pairs, preserving grounded trailer verification after merge commits.
10+
- Harden range-seal binding and status diagnostics so signed receipts stay tied to the correct head and explain stale state precisely.
11+
12+
### Release safety
13+
- Verify that the pushed `v…` tag exactly matches the CLI package version before npm publication or GitHub Release creation.
14+
515
### Umami proxy provision
616
- **Deploy the Worker before binding `UMAMI_ORIGIN`.** wrangler-action’s `secrets:` input ran `secret bulk` first, which fails when the Worker does not exist yet — `wrangler.jsonc` still listed the `/s/*` route, so git looked provisioned while nothing was uploaded. CI now deploys, then `secret put`.
717
- PRs that touch the proxy run unit tests + `wrangler deploy --dry-run`; only `main` / `workflow_dispatch` deploy.

action/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ on:
2121
fetch-depth: 0
2222
ref: ${{ github.event.pull_request.head.sha || github.sha }}
2323

24-
- uses: chtnnh/know-code/action@v0.3.0
24+
- uses: chtnnh/know-code/action@v0.3.1
2525
with:
2626
base-branch: main
2727
from: ${{ github.event_name == 'push' && github.event.before || '' }}
2828
require-all: false
2929
require-range-trailers: false
30-
version: "^0.3.0"
30+
version: "^0.3.1"
3131
```
3232
3333
All-zeros `github.event.before` (new branch) skips the walk.
@@ -40,7 +40,7 @@ All-zeros `github.event.before` (new branch) skips the walk.
4040
| `from` | _(empty)_ | Previous tip for push jobs (`github.event.before`). Empty on `pull_request`. |
4141
| `require-all` | `false` | Stricter verify messaging |
4242
| `require-range-trailers` | `false` | Every commit in range must have trailer (rewrite teams; PR path) |
43-
| `version` | `^0.3.0` | npm version when not building from monorepo checkout |
43+
| `version` | `^0.3.1` | npm version when not building from monorepo checkout |
4444

4545
## Quick add
4646

action/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ inputs:
3232
version:
3333
description: npm version range for know-code when not building from this repo
3434
required: false
35-
default: "^0.3.0"
35+
default: "^0.3.1"
3636

3737
runs:
3838
using: composite

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "know-code-monorepo",
33
"private": true,
4-
"version": "0.3.0",
4+
"version": "0.3.1",
55
"description": "k(no)w-code — agents don't push until you know exactly what's changed",
66
"workspaces": [
77
"packages/cli",

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@chtnnh/know-code",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Gate git push / PR creation until the human passes a comprehension quiz about the diff",
55
"type": "module",
66
"bin": {

packages/cli/src/cli-surface.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { describe, it } from "node:test";
77
import assert from "node:assert/strict";
88
import { spawnSync } from "node:child_process";
9-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
9+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1010
import { tmpdir } from "node:os";
1111
import { dirname, join } from "node:path";
1212
import { fileURLToPath } from "node:url";
@@ -36,6 +36,11 @@ import {
3636
} from "./test-helpers.js";
3737

3838
const CLI = join(dirname(fileURLToPath(import.meta.url)), "index.js");
39+
const CLI_VERSION = (
40+
JSON.parse(
41+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8"),
42+
) as { version: string }
43+
).version;
3944

4045
interface CliResult {
4146
status: number;
@@ -72,13 +77,13 @@ function setupRepo(root: string, cfg = liteConfig()) {
7277
}
7378

7479
describe("cli surface (spawned)", () => {
75-
it("version matches the release", () => {
80+
it("version matches the CLI package manifest", () => {
7681
const { root, cleanup } = withTempRepo("kc-cli-ver-");
7782
try {
7883
setupRepo(root);
7984
const r = kc(root, ["version"]);
8085
assert.equal(r.status, 0);
81-
assert.match(r.stdout, /0\.3\.0/);
86+
assert.equal(r.stdout.trim(), CLI_VERSION);
8287
} finally {
8388
cleanup();
8489
}

0 commit comments

Comments
 (0)