Repository navigation
fix(k9): ratchet pushes, escape Markdown in the grid, reject bad findings, end options #1310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,10 @@ name: k9-conformance | |
| # the PR base: an entry the PR adds exempts nothing unless a commit message or | ||
| # the PR body carries `Ratchet-exception: .machine_readable/k9-contract-debt.txt | ||
| # — <why>` (the governance exemption ratchet's form; it watches this ledger | ||
| # too). On a pull request the checkout is full so the changed-file list is the | ||
| # merge-base diff and the PR's commit messages are readable. | ||
| # too). The checkout is full so the changed-file list is the merge-base diff | ||
| # and the commit messages are readable; on a push the range is the push's own | ||
| # (`before`..`sha`), so a direct push to a ledgered contract, or a direct | ||
| # ledger addition, is ratcheted exactly as a pull request is. | ||
| # | ||
| # Outputs: the `k9-grid` artifact (report.json and badge.json as I-JSON/JCS, | ||
| # report.md, the validator logs) and the `verdict` output. The caller posts | ||
|
|
@@ -49,7 +51,6 @@ on: | |
| paths: | ||
| - '**.k9.ncl' | ||
| - '**.k9' | ||
| - '**.k9.lock' | ||
| - '1-formats/k9/**' | ||
| - 'scripts/k9-grid.sh' | ||
| - 'scripts/lib/k9-grid.jq' | ||
|
|
@@ -60,7 +61,6 @@ on: | |
| paths: | ||
| - '**.k9.ncl' | ||
| - '**.k9' | ||
| - '**.k9.lock' | ||
| - '1-formats/k9/**' | ||
| - 'scripts/k9-grid.sh' | ||
| - 'scripts/lib/k9-grid.jq' | ||
|
|
@@ -112,10 +112,10 @@ jobs: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | ||
| # Full history on a pull request: the changed-file list is the | ||
| # merge-base diff, and the ledger ratchet reads the PR's commit | ||
| # messages. Strings, not numbers: a bare 0 is falsy in an expression. | ||
| fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }} | ||
| # Full history: the changed-file list is the merge-base diff and the | ||
| # ledger ratchet reads the commit messages, on a pull request (base | ||
| # ..head) and on a push (the push's before..sha) alike. | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: checkout the K9 validator, contract and renderer at this workflow's commit | ||
|
|
@@ -186,34 +186,41 @@ jobs: | |
| echo "TYPECHECK_CODE=$?" >> "$GITHUB_ENV" | ||
| cat "$RUNNER_TEMP/typecheck.log" | ||
| - name: Enumerate the committed K9 files, the PR's changed files and ledger growth | ||
| - name: Enumerate the committed K9 files, the changed files and ledger growth | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| # A pull request compares with its base; a push with the commit the | ||
| # push replaced (all zeros when the ref is new: nothing to compare). | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | ||
| LEDGER: .machine_readable/k9-contract-debt.txt | ||
| run: | | ||
| set -euo pipefail | ||
| # Tracked files only: an untracked file is not a committed contract. | ||
| # The two extensions K9-CONTRACT-SPEC.adoc reserves, nothing else. | ||
| # A caller with no K9 file at all is a legitimate (grey) corpus, so | ||
| # grep's no-match exit is not an error here. | ||
| git ls-files -z -- '*.k9.ncl' '*.k9' '*.k9.lock' \ | ||
| git ls-files -z -- '*.k9.ncl' '*.k9' \ | ||
| | tr '\0' '\n' \ | ||
| | { grep -v -E '^(\.k9-canon/|9-archive/|1-formats/k9/tools/fixtures/)' || [ $? -eq 1 ]; } \ | ||
| | sort > "$RUNNER_TEMP/expected" | ||
| echo "$(wc -l < "$RUNNER_TEMP/expected") committed K9 file(s)" | ||
| : > "$RUNNER_TEMP/changed" | ||
| : > "$RUNNER_TEMP/ledger-growth" | ||
| echo "BASE_UNRESOLVED=" >> "$GITHUB_ENV" | ||
| case "$BASE_SHA" in *[!0]*) ;; *) BASE_SHA="" ;; esac | ||
| [ -n "$BASE_SHA" ] || exit 0 | ||
| # The PR's own changes are those since its merge base, not since the | ||
| # base branch tip: a file the base branch changed after the PR | ||
| # branched is not this PR's edit. The full checkout already holds | ||
| # the base commit; the fetch is a fallback only, and runs without | ||
| # credentials (persist-credentials is off), so it serves public | ||
| # repositories. | ||
| # branched is not this PR's edit. A push compares with `before` | ||
| # itself, never a merge base: after a force push the fork point is | ||
| # not the state the push replaced, and a ledger entry the old tip | ||
| # had removed would come back unseen. The full checkout already | ||
| # holds the base commit; the fetch | ||
| # is a fallback only, and runs without credentials | ||
| # (persist-credentials is off), so it serves public repositories. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| if { git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null || git fetch --quiet origin "$BASE_SHA" 2>/dev/null; } \ | ||
| && merge_base=$(git merge-base "$BASE_SHA" "$HEAD_SHA" 2>/dev/null) \ | ||
| && merge_base=$(if [ "$GITHUB_EVENT_NAME" = pull_request ]; then git merge-base "$BASE_SHA" "$HEAD_SHA"; else git rev-parse --verify "$BASE_SHA^{commit}"; fi 2>/dev/null) \ | ||
| && git diff --name-only "$merge_base" "$HEAD_SHA" > "$RUNNER_TEMP/changed" 2>/dev/null; then | ||
| echo "$(wc -l < "$RUNNER_TEMP/changed") file(s) changed since the merge base $merge_base" | ||
| else | ||
|
|
@@ -260,7 +267,9 @@ jobs: | |
| worst=0 | ||
| while IFS= read -r f; do | ||
| [ -n "$f" ] || continue | ||
| bash "$CANON/1-formats/k9/tools/k9-validate.sh" --strict --layer L2 --json --quiet "$f" >> "$RUNNER_TEMP/results.jsonl" 2>> "$RUNNER_TEMP/corpus.log" | ||
| # `--` ends the options: a tracked file whose name starts with `-` | ||
| # is a path, not a flag. | ||
| bash "$CANON/1-formats/k9/tools/k9-validate.sh" --strict --layer L2 --json --quiet -- "$f" >> "$RUNNER_TEMP/results.jsonl" 2>> "$RUNNER_TEMP/corpus.log" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The added Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reproduced ( Generated by Claude Code
coderabbitai[bot] marked this conversation as resolved.
|
||
| rc=$? | ||
| [ "$rc" -gt "$worst" ] && worst=$rc | ||
| done < "$RUNNER_TEMP/expected" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,7 +112,8 @@ def corpus_items(results; expected; ledger; growth; changed; require_files): | |
| .rows += [item("validator output"; "indeterminate"; "Malformed JSON result.")] | ||
| elif (($r.file | type) != "string") or ($expected[$r.file] | not) or (.seen[$r.file] // false) | ||
| or (($r.verdict // "") | IN("pass", "fail", "error", "routed") | not) | ||
| or (($r.findings | type) != "array") then | ||
| or (($r.findings | type) != "array") | ||
| or (($r.findings | type) == "array" and ($r.findings | any(type != "object"))) then | ||
| .rows += [item((if ($r.file | type) == "string" then $r.file else "validator output" end); "indeterminate"; "Unexpected, duplicate or invalid result.")] | ||
| else | ||
| .seen[$r.file] = true | ||
|
|
@@ -213,8 +214,15 @@ def state_label: {red: "🔴 failing", orange: "🟠 indeterminate", yellow: " | |
| def colour: {red: "red", orange: "orange", yellow: "yellow", green: "brightgreen", grey: "lightgrey"}[.]; | ||
| def word: {red: "failing", orange: "indeterminate", yellow: "advisory", green: "conformant", grey: "not_applicable"}[.]; | ||
| # Escape text for grid cells and finding summaries, flattening line breaks. | ||
| # Ids and reasons carry contributor-controlled file names and validator text, | ||
| # and the comment is posted by the lane's own account: every character that | ||
| # could start Markdown or HTML syntax, or an @-mention, becomes an entity, so a file named | ||
| # `.k9.ncl` is shown, not rendered. Entities display as | ||
| # the character itself in a GitHub table cell. | ||
| def esc: tostring | gsub("&"; "&") | gsub("<"; "<") | gsub(">"; ">") | gsub("\\|"; "|") | ||
| | gsub("`"; "`") | gsub("\r?\n"; " "); | ||
| | gsub("`"; "`") | gsub("\\\\"; "\") | gsub("\\["; "[") | gsub("\\]"; "]") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When contributor-controlled text contains a bare Useful? React with 👍 / 👎. |
||
| | gsub("\\("; "(") | gsub("\\)"; ")") | gsub("\\*"; "*") | gsub("_"; "_") | ||
| | gsub("!"; "!") | gsub("~"; "~") | gsub("@"; "@") | gsub("\r?\n"; " "); | ||
| def grid_label: "K9 L0–L2"; | ||
| def badge_url(st): "https://img.shields.io/badge/\(grid_label | @uri)-\(st | word | @uri)-\(st | colour)"; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When ledger growth is authorized only by the supported
Ratchet-exceptionin the PR body, the PR run clears the growth, but the post-mergepushrun now comparesbeforeto the merged SHA and detects it again. A push payload has no.pull_request.body, and squash/rebase or configured merge messages need not retain that body or the original trailer, so lines 248–251 leave the growth undeclared and turn main red immediately after an accepted merge. The push path needs to recover the associated PR exemption or otherwise avoid invalidating an exemption already accepted by the PR run.Useful? React with 👍 / 👎.