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
71 changes: 55 additions & 16 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,61 @@ name: docs
on:
push:
branches: [main]
paths:
- "website/**"
- ".github/workflows/docs.yml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
published-release:
runs-on: ubuntu-latest
outputs:
ready: ${{ steps.release.outputs.ready }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- id: release
run: |
VERSION="$(node -p 'require("./website/versions.json")[0]')"
verify_provenance() {
npm view "@chtnnh/know-code@${VERSION}" dist.attestations --json >/tmp/know-code-attestations.json || return 1
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
VERIFY_DIR="$(mktemp -d)" || return 1
trap 'rm -rf "${VERIFY_DIR}"' RETURN
(cd "${VERIFY_DIR}" && npm init --yes >/dev/null) || return 1
npm install --prefix "${VERIFY_DIR}" --ignore-scripts "@chtnnh/know-code@${VERSION}" || return 1
npm audit signatures --prefix "${VERIFY_DIR}" --json || return 1
}
if npm view "@chtnnh/know-code@${VERSION}" gitHead --json >/tmp/know-code-version.json 2>/tmp/know-code-version.err; then
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)"
verify_provenance
git fetch origin main --tags
TAG_GIT_HEAD="$(git rev-parse "v${VERSION}^{commit}")"
git merge-base --is-ancestor "${TAG_GIT_HEAD}" origin/main
[[ "${PUBLISHED_GIT_HEAD}" == "${TAG_GIT_HEAD}" ]]
node scripts/check-release-docs-provenance.mjs "v${VERSION}" "${GITHUB_SHA}"
echo "ready=true" >> "$GITHUB_OUTPUT"
elif grep -q "E404" /tmp/know-code-version.err; then
echo "ready=false" >> "$GITHUB_OUTPUT"
else
cat /tmp/know-code-version.err >&2
exit 1
fi

deploy:
needs: published-release
if: needs.published-release.outputs.ready == 'true'
runs-on: ubuntu-latest
concurrency:
group: pages
cancel-in-progress: false
queue: max
environment:
name: github-pages
steps:
- uses: actions/checkout@v5

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

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
- name: Confirm this is still main's latest commit
run: |
git fetch origin main
[[ "${GITHUB_SHA}" == "$(git rev-parse origin/main)" ]]

- name: Deploy Pages
uses: actions/deploy-pages@v5
with:
artifact_name: github-pages-${{ github.run_attempt }}
123 changes: 121 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ name: release
# Workflow filename: release.yml (filename only, not a path)
# Environment: npm-release (must match job.environment)
# No NPM_TOKEN / NODE_AUTH_TOKEN — those block OIDC.
# Protect v* tags from updates and deletion with a repository ruleset. The
# runtime checks below detect in-flight changes, but protection closes the
# unavoidable race after the final remote-tag fetch.
on:
push:
tags:
Expand All @@ -14,13 +17,16 @@ on:
permissions:
contents: write
id-token: write # required for npm OIDC
pages: write

jobs:
publish:
runs-on: ubuntu-latest
environment: npm-release
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

# Node 24 → npm ≥ 11.5.1 (required for trusted publishing).
# Do NOT set registry-url: setup-node would inject a dummy NODE_AUTH_TOKEN
Expand All @@ -35,20 +41,133 @@ jobs:
- run: npm run build
- run: npm test

- name: Verify release tag matches CLI package version
run: node scripts/check-release-tag.mjs

- name: Verify main contains and declares this release
run: |
git fetch origin main
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
[[ "$(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}" ]]
node scripts/check-release-docs-provenance.mjs "${GITHUB_SHA}" origin/main

- name: Build frozen release docs
env:
UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
run: npm run build:docs

- name: Reverify main immediately before publication
run: |
git fetch origin main
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
[[ "$(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}" ]]
node scripts/check-release-docs-provenance.mjs "${GITHUB_SHA}" origin/main
git fetch origin "+refs/tags/${GITHUB_REF_NAME}:refs/know-code/release-tag"
[[ "$(git rev-parse refs/know-code/release-tag^{commit})" == "$(git rev-parse "${GITHUB_SHA}^{commit}")" ]]

- name: Publish CLI to npm (OIDC)
working-directory: packages/cli
run: |
unset NODE_AUTH_TOKEN
npm publish --access public
PACKAGE_VERSION="$(npm pkg get version --workspaces=false | tr -d '"')"
verify_provenance() {
npm view "@chtnnh/know-code@${PACKAGE_VERSION}" dist.attestations --json >/tmp/know-code-attestations.json || return 1
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
VERIFY_DIR="$(mktemp -d)" || return 1
trap 'rm -rf "${VERIFY_DIR}"' RETURN
(cd "${VERIFY_DIR}" && npm init --yes >/dev/null) || return 1
npm install --prefix "${VERIFY_DIR}" --ignore-scripts "@chtnnh/know-code@${PACKAGE_VERSION}" || return 1
npm audit signatures --prefix "${VERIFY_DIR}" --json || return 1
}
if npm view "@chtnnh/know-code@${PACKAGE_VERSION}" gitHead --json >/tmp/know-code-version.json 2>/tmp/know-code-version.err; then
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)"
EXPECTED_GIT_HEAD="$(git rev-parse "${GITHUB_SHA}^{commit}")"
if [[ "${PUBLISHED_GIT_HEAD}" != "${EXPECTED_GIT_HEAD}" ]]; then
echo "@chtnnh/know-code@${PACKAGE_VERSION} belongs to ${PUBLISHED_GIT_HEAD}, not ${EXPECTED_GIT_HEAD}." >&2
exit 1
fi
verify_provenance
echo "@chtnnh/know-code@${PACKAGE_VERSION} is already published from this tag; skipping npm publish."
elif grep -q "E404" /tmp/know-code-version.err; then
npm publish --provenance --access public
for attempt in {1..12}; do
if verify_provenance; then exit 0; fi
sleep 5
done
echo "npm provenance did not become verifiable for @chtnnh/know-code@${PACKAGE_VERSION}." >&2
exit 1
else
cat /tmp/know-code-version.err >&2
exit 1
fi

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${GITHUB_REF_NAME}"
git fetch origin "+refs/tags/${TAG}:refs/know-code/release-tag"
[[ "$(git rev-parse refs/know-code/release-tag^{commit})" == "$(git rev-parse "${GITHUB_SHA}^{commit}")" ]]
NOTES="Release ${TAG}. See CHANGELOG.md."
if [[ -f CHANGELOG.md ]]; then
NOTES="$(sed -n "/^## ${TAG#v}/,/^## /p" CHANGELOG.md | sed '$d' || true)"
[[ -n "$NOTES" ]] || NOTES="Release ${TAG}. See CHANGELOG.md."
fi
gh release create "$TAG" --title "$TAG" --notes "$NOTES"
if gh api --include "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" >/tmp/know-code-release.response 2>/tmp/know-code-release.err; then
gh release edit "$TAG" --title "$TAG" --notes "$NOTES"
elif head -n 1 /tmp/know-code-release.response | grep -Eq '^HTTP/[^ ]+ 404( |$)'; then
gh release create "$TAG" --verify-tag --title "$TAG" --notes "$NOTES"
else
cat /tmp/know-code-release.err >&2
exit 1
fi

deploy-docs:
needs: publish
runs-on: ubuntu-latest
environment:
name: github-pages
concurrency:
group: pages
cancel-in-progress: false
queue: max

steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0

- uses: actions/setup-node@v5
with:
node-version: "22"
cache: npm

- name: Verify main still declares this release
run: |
[[ "$(node -p 'require("./website/versions.json")[0]')" == "${GITHUB_REF_NAME#v}" ]]
node scripts/check-release-docs-provenance.mjs "${GITHUB_REF_NAME}" HEAD

- name: Build current docs
env:
UMAMI_WEBSITE_ID: ${{ vars.UMAMI_WEBSITE_ID }}
run: |
npm install
npm run build:docs

- name: Upload current docs artifact
uses: actions/upload-pages-artifact@v5
with:
name: github-pages-${{ github.run_attempt }}
path: website/build
include-hidden-files: true

- name: Confirm main did not advance before deployment
run: |
git fetch origin main
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]

- name: Deploy frozen release docs
uses: actions/deploy-pages@v5
with:
artifact_name: github-pages-${{ github.run_attempt }}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

## 0.3.1

### Fixes
- 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.
- Make push verification walk stacked landed runs against their historical tree pairs, preserving grounded trailer verification after merge commits.
- Harden range-seal binding and status diagnostics so signed receipts stay tied to the correct head and explain stale state precisely.

### Release safety
- Verify that the pushed `v…` tag exactly matches the CLI package version before npm publication or GitHub Release creation.

### Umami proxy provision
- **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`.
- PRs that touch the proxy run unit tests + `wrangler deploy --dry-run`; only `main` / `workflow_dispatch` deploy.
Expand Down
6 changes: 3 additions & 3 deletions action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ on:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- uses: chtnnh/know-code/action@v0.3.0
- uses: chtnnh/know-code/action@v0.3.1
with:
base-branch: main
from: ${{ github.event_name == 'push' && github.event.before || '' }}
require-all: false
require-range-trailers: false
version: "^0.3.0"
version: "^0.3.1"
```

All-zeros `github.event.before` (new branch) skips the walk.
Expand All @@ -40,7 +40,7 @@ All-zeros `github.event.before` (new branch) skips the walk.
| `from` | _(empty)_ | Previous tip for push jobs (`github.event.before`). Empty on `pull_request`. |
| `require-all` | `false` | Stricter verify messaging |
| `require-range-trailers` | `false` | Every commit in range must have trailer (rewrite teams; PR path) |
| `version` | `^0.3.0` | npm version when not building from monorepo checkout |
| `version` | `^0.3.1` | npm version when not building from monorepo checkout |

## Quick add

Expand Down
2 changes: 1 addition & 1 deletion action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ inputs:
version:
description: npm version range for know-code when not building from this repo
required: false
default: "^0.3.0"
default: "^0.3.1"

runs:
using: composite
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "know-code-monorepo",
"private": true,
"version": "0.3.0",
"version": "0.3.1",
"description": "k(no)w-code — agents don't push until you know exactly what's changed",
"workspaces": [
"packages/cli",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chtnnh/know-code",
"version": "0.3.0",
"version": "0.3.1",
"description": "Gate git push / PR creation until the human passes a comprehension quiz about the diff",
"type": "module",
"bin": {
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/cli-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -36,6 +36,11 @@ import {
} from "./test-helpers.js";

const CLI = join(dirname(fileURLToPath(import.meta.url)), "index.js");
const CLI_VERSION = (
JSON.parse(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8"),
) as { version: string }
).version;

interface CliResult {
status: number;
Expand Down Expand Up @@ -72,13 +77,13 @@ function setupRepo(root: string, cfg = liteConfig()) {
}

describe("cli surface (spawned)", () => {
it("version matches the release", () => {
it("version matches the CLI package manifest", () => {
const { root, cleanup } = withTempRepo("kc-cli-ver-");
try {
setupRepo(root);
const r = kc(root, ["version"]);
assert.equal(r.status, 0);
assert.match(r.stdout, /0\.3\.0/);
assert.equal(r.stdout.trim(), CLI_VERSION);
} finally {
cleanup();
}
Expand Down
Loading