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
23 changes: 18 additions & 5 deletions .github/workflows/know-code.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
name: know-code

# PR-only: grounded verification needs a merge-base ahead of HEAD. On a push
# to main, HEAD *is* origin/main (zero commits ahead), so there is no range to
# recompute and verify can never match a grounded hash.
# PR: checkout the tip (not pull/N/merge) and run default verify.
# Push: walk github.event.before..HEAD so landings on main still match
# per-run trailers (origin/main == HEAD has no merge-base range).
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]

jobs:
verify:
Expand All @@ -16,7 +18,8 @@ jobs:
fetch-depth: 0
# Default pull_request checkout is a merge commit (no trailers).
# Verify must run on the PR tip that carries Know-Code-Verified.
ref: ${{ github.event.pull_request.head.sha }}
# On push, head.sha is empty so this is github.sha (the new tip).
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- uses: actions/setup-node@v4
with:
Expand All @@ -35,4 +38,14 @@ jobs:
printf '{\n "level": "standard",\n "baseBranch": "main",\n "requireTrailer": true\n}\n' > .know-code/config.json

- name: Verify Know-Code-Verified trailer
run: know-code verify
run: |
if [ "${{ github.event_name }}" = "push" ]; then
BEFORE="${{ github.event.before }}"
if [[ "$BEFORE" =~ ^0+$ ]]; then
echo "know-code: new branch push — skip walk"
exit 0
fi
know-code verify --from "$BEFORE"
else
know-code verify
fi
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### CI verify on push (stacked-run walker)
- **`know-code verify --from <oid>`** walks `from..HEAD`, splits by `Know-Code-Verified` hash, and checks each run as a historical tree-pair (parent-of-first tree → last non-merge). Trailerless merges attach to the run but are not the hash tip, so a GitHub merge commit still matches after `main` moved. Linear commits without a trailer fail closed. One-non-merge runs also accept the empty-tree (index) hash of that feature tip.
- **Workflow + `init --workflow` + composite action** trigger on `push` to the base branch and pass `github.event.before`. PR verify is unchanged (`head.sha`, no `--from`). All-zeros `before` skips the walk.
- **Docs** describe the push walk and stop claiming verify cannot run after merge.

## 0.3.0

### Security — comprehensive exploit hardening (E01–E28)
Expand Down
19 changes: 15 additions & 4 deletions action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,42 @@ Verify `Know-Code-Verified` commit trailers in CI.

## Usage

Trigger the workflow on `pull_request` only. On a direct push to the base
branch, `HEAD` equals the base so there is no merge-base range to verify —
gate direct pushes locally with the pre-push hook instead.
On `pull_request`, checkout the PR tip. On `push` to the base branch, pass
`from:` with `github.event.before` so verify walks each landed run (HEAD
equals the base, so there is no merge-base range to recompute).

```yaml
on:
pull_request:
push:
branches: [main]

# ...

- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}

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

All-zeros `github.event.before` (new branch) skips the walk.

## Inputs

| Input | Default | Description |
|-------|---------|-------------|
| `base-branch` | `main` | Base branch for diff hashing |
| `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) |
| `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 |

## Quick add
Expand Down
23 changes: 19 additions & 4 deletions action/action.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: know-code verify
description: >-
Verify Know-Code-Verified commit trailers against grounded tree hashes
(index and merge-base→write-tree). Callers must checkout the PR tip
(ref: github.event.pull_request.head.sha) with fetch-depth: 0 — the
default merge commit has no trailers. Use on pull_request only.
Verify Know-Code-Verified commit trailers against grounded tree hashes.
On pull_request, checkout the PR tip (ref: github.event.pull_request.head.sha)
with fetch-depth: 0 — the default merge commit has no trailers. On push to
the base branch, pass from: github.event.before so verify walks each landed
run instead of treating HEAD as the merge-base.
author: chtnnh
branding:
icon: shield
Expand All @@ -22,6 +23,12 @@ inputs:
description: If true, verify every commit in the range has Know-Code-Verified trailers
required: false
default: "false"
from:
description: >-
Previous tip SHA for push jobs (github.event.before). Empty on
pull_request. All-zeros skips the walk (new branch).
required: false
default: ""
version:
description: npm version range for know-code when not building from this repo
required: false
Expand Down Expand Up @@ -63,7 +70,15 @@ runs:
- name: Verify Know-Code-Verified trailer
shell: bash
run: |
FROM="${{ inputs.from }}"
if [[ "$FROM" =~ ^0+$ ]]; then
echo "know-code: new branch push (zero before SHA) — skip walk"
exit 0
fi
ARGS=""
if [[ -n "$FROM" ]]; then
ARGS="$ARGS --from $FROM"
fi
if [[ "${{ inputs.require-all }}" == "true" ]]; then
ARGS="$ARGS --require-all"
fi
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/cli-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,19 @@ describe("cli surface (spawned)", () => {
cleanup();
}
});

it("verify --from requires a SHA; help lists the flag", () => {
const { root, cleanup } = withTempRepo("kc-cli-from-");
try {
setupRepo(root);
const missing = kc(root, ["verify", "--from"]);
assert.equal(missing.status, 1);
assert.match(missing.stderr, /requires a commit SHA/);
const help = kc(root, ["help"]);
assert.equal(help.status, 0);
assert.match(help.stdout, /verify \[--from <oid>\]/);
} finally {
cleanup();
}
});
});
39 changes: 33 additions & 6 deletions packages/cli/src/commands-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { runCheck } from "./commands/check.js";
import { buildAmendArgs } from "./commands/amend.js";
Expand Down Expand Up @@ -250,15 +252,40 @@ describe("commands: config / init / quiz / doctor / reset / ship", () => {
}
});

it("consumerWorkflowYaml pins action, base branch, and is PR-only", () => {
it("consumerWorkflowYaml pins action, base branch, PR tip, and push walk", () => {
const yml = consumerWorkflowYaml("develop");
assert.match(yml, /base-branch: develop/);
assert.match(yml, /chtnnh\/know-code\/action@v0\.3\.0/);
// Push-to-base has no merge-base ahead of HEAD — verify must be PR-only.
assert.match(yml, /pull_request:/);
assert.doesNotMatch(yml, /push:/);
// Default PR checkout is a merge commit without trailers — pin the tip.
assert.match(yml, /github\.event\.pull_request\.head\.sha/);
assert.match(yml, /push:/);
assert.match(yml, /branches:\n {6}- develop/);
assert.match(yml, /github\.event\.pull_request\.head\.sha \|\| github\.sha/);
assert.match(yml, /github\.event\.before/);
});

it("monorepo workflow and composite action wire push --from", () => {
const repoRoot = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
);
const workflow = readFileSync(
join(repoRoot, ".github", "workflows", "know-code.yml"),
"utf8",
);
assert.match(workflow, /push:/);
assert.match(workflow, /branches: \[main\]/);
assert.match(workflow, /verify --from/);
assert.match(workflow, /github\.event\.before/);
assert.match(workflow, /github\.event\.pull_request\.head\.sha \|\| github\.sha/);
assert.match(workflow, /new branch push/);
assert.match(workflow, /else\n know-code verify\n/);

const action = readFileSync(join(repoRoot, "action", "action.yml"), "utf8");
assert.match(action, /^ {2}from:/m);
assert.match(action, /zero before SHA/);
assert.match(action, /--from \$FROM/);
});

it("validateQuiz happy and sad", () => {
Expand Down
9 changes: 6 additions & 3 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const DOCS = "https://kc.chtnnhfoundation.org";
const ACTION_REF = "chtnnh/know-code/action@v0.3.0";

export function consumerWorkflowYaml(baseBranch: string): string {
// PR-only: on a push to the base branch there is no merge-base ahead of
// HEAD, so grounded verification has no range to recompute.
// PR: checkout the tip (not pull/N/merge). Push: walk github.event.before..HEAD.
return `name: know-code

on:
pull_request:
push:
branches:
- ${baseBranch}

jobs:
verify:
Expand All @@ -27,11 +29,12 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: \${{ github.event.pull_request.head.sha }}
ref: \${{ github.event.pull_request.head.sha || github.sha }}

- uses: ${ACTION_REF}
with:
base-branch: ${baseBranch}
from: \${{ github.event_name == 'push' && github.event.before || '' }}
`;
}

Expand Down
Loading
Loading