Skip to content

CI: install the codegen docs-only gate, with a same-repository guard for forks - #175

Merged
luisleo526 merged 1 commit into
mainfrom
ci/docs-gate-adopt
Oct 5, 2026
Merged

luisleo526 merged 1 commit into
mainfrom
ci/docs-gate-adopt

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Installs the codegen documentation/legal-only gate whose basis landed in pineforge-workflow #102 (codegen-docs-only/v1, the owner's 2026-09-30 rule: never sweep for docs). It adds a same-repository guard so a fork's code never runs in the base-context workflow, along with the fixes from the installation review. pineforge-workflow#106 carries the guard and the fixes to the templates, so template and install stay byte-equal.

This is a configuration PR. CI files are not docs-only, so it carries the ordinary measured pr-gate. Against engine b7ed0ee7 (engine main after #342; this head carries #173), the result is PASS (no-improve-no-regression), recorded as receipt 0be891353152e0b5379aa05d770ea318fccc25895b9e2f5ce64d45255b3ec06b, event verdict-b7ed0ee7920b-5b596a42f269-33525ec0a983.

The five files

Each file was copied with git show <sha>:<path>; modes stay 100644.

Template Installed here sha256 at #102 (d9f4f22) sha256 installed = template at #106 (8cbd5c6)
campaign/ci/codegen-gate.yml .github/workflows/gate.yml 90ed96ccde348a1a41b8a259e0073f5de1f78aa25b9542afa0d7ba78ef4973d6 839b523deb5a9ab7f4c10612933a864d40474ad592eed323178a947c3b3b3c7b
campaign/ci/codegen-test.yml .github/workflows/test.yml e5b0e9827084bb912a8856bca85d7e272501ebcc7fa38a03ea264b31789b90ac c4644af79e7efd3ca5613c67bcb630f753fe7fa99025531599c8dd53c982057c
campaign/ci/codegen-docs-check.mjs .github/pineforge/ci/codegen-docs-check.mjs 436a05bad672812b3d7972328687e2c674972ba0d9b489715cd763af5d3751c1 436a05bad672812b3d7972328687e2c674972ba0d9b489715cd763af5d3751c1 (unchanged)
campaign/src/codegen-docs-rule.mjs .github/pineforge/src/codegen-docs-rule.mjs 010ea1b6cb24fe7638ed2f7003e38c69384c40210a7c5ecc345c37aa86f6affd 2e31a997cbf5c2c15eaa5f9e43a34eeaa0df012af49f9b8e019a57955df870b6
campaign/ci/promote-baseline.yml .github/workflows/promote-baseline.yml 2bcb68a54dd0643b6dcae107046130a507b819a3ea43865a84015b15e68c3a13 1d86d78a5d27f7603997cf951bd5b95b8307c6c0c6dd5f19083e164f6f201ba7

What differs from #102's templates, all mirrored in #106:

  1. Same-repository guard (gate.yml, below).
  2. Promotion reads each check's newest run (promote-baseline.yml). The old check counted every failed or cancelled check-run on the head. So a stale red run of the same SHA skipped promotion silently: a fork's PR, an edit or a ready/reopen mid-run, a second manual run. Now only the newest run of each (app, name) counts.
  3. SHA-pinned actions in the promotion job, which holds id-token: write and the App key. Each is pinned to the commit its tag names today: checkout v4.4.0, create-github-app-token v1.12.0, setup-node v4.4.0, google-github-actions/auth v2.1.13, setup-gcloud v2.2.1. Its campaign-tooling checkout keeps no credentials (persist-credentials: false), so the App token is not on disk when npm ci runs install scripts.
  4. Docs rule: the root README.md and LICENSE count as documentation only as edits. pyproject.toml names both and the npm package copies LICENSE. Deleting either, renaming it away or changing its type therefore needs the full gate, and a receipt claiming such a change is refused. Any other README.md is Markdown as before. The rule id stays codegen-docs-only/v1: it only narrows, and the publish hook re-derives every diff.
  5. test.yml runs once per pushed commit (concurrency keyed by the SHA), so a docs-only push no longer cancels a code push's pytest.
  6. Every job is capped: 10 minutes for the classifiers and the reporter, 30 for the differential gate and each pytest job. A GitHub pytest job took about 2 minutes on Linear-time diagnostic codes, the PineForge Source License 1.1, and the scoreboard README at pineforge-release 03b8dcc #171's head.

The same-repository guard

This repository is public and takes fork PRs. differential and pytest are the two jobs that run the PR head's code under pull_request_target, with contents: read. Their if: gains:

&& (github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository)
  • Nightly (schedule) and manual runs still run both jobs.
  • A deleted fork's head.repo is null, so it counts as a fork.
  • The classifier (preflight-docs) and the reporter (gate) stay unguarded. They run trusted code from main, so every pull_request_target run posts gate: no hang, no silent pass.

The reporter takes the same expression as SAME_REPO:

  • It clears the full gate only for a same-repository head.
  • Whenever a fork's PR does not pass, its status says: Fork PR: a maintainer must push this branch to pineforge-4pass/pineforge-codegen-oss and open a PR from it for the full gate (124 characters, under GitHub's 140). The reporter job then fails.
  • If a run of this repository (github-actions[bot]) already posted a gate success on the same commit, the fork's run leaves it and exits 0. That success means a maintainer pushed the fork's branch here and opened a PR from it. Without this rule, any later edited / reopened / ready_for_review event on the fork PR would turn the maintainer's PR red, and anyone could do it on purpose by opening a fork PR at a same-repository PR's head.
  • Reusing that pass skips no check. The gate tests the head commit's own tree, and the strict up-to-date rule forces a new head whenever main moves.

A fork's accepted docs proof ran only main's checker on the PR head as data, so it clears as a same-repository one does.

How each event behaves

How this was checked: each run: script was extracted from this branch's YAML and run with bash, in the checkout layout the job builds (.docs-gate = main's checker; .candidate = the PR head as data, or the pushed commit with the previous main). The inputs were synthetic commits on top of this head. GitHub's job if: was applied as written, and gh was stubbed. The installation reviewer's independent reporter, push and promotion simulations were run on this head as well.

Event Definition that runs Classifier preflight-docs differential + pytest Reporter → gate PR code executed
Same-repo docs PR (pull_request_target: opened, synchronize, reopened, edited, ready_for_review) main's gate.yml main's checker judges the diff from the common ancestor with main to the head: accepted, docs_only=true; proof kept as codegen-docs-diff skipped success "docs-only preflight+docs; no sweep" on the exact head none
Same-repo code PR main's gate.yml refused (a non-doc path, or a root README.md / LICENSE deleted or retyped): docs_only=false run on the head: Pyodide selftest, npm audit, gate:full; pytest 3.11–3.14 success "Full codegen differential gate and pytest passed" only if both pass, else failure "Codegen gate did not pass" in the two contents: read jobs
Fork docs PR main's gate.yml same as same-repo (fork head fetched by SHA, read as data) skipped success docs-only none
Fork code PR main's gate.yml refused: docs_only=false skipped by the guard failure "Fork PR: a maintainer must push this branch … and open a PR from it for the full gate"; if the same commit already passed in a repository run, that success is left and the job exits 0 none
Push to main the pushed commit's test.yml (gate.yml has no push trigger) the previous main's checker (github.event.before) judges before..after pytest 3.11–3.14, skipped only for a docs-only push; this PR's own landing push runs it because the previous main has no checker; one run per commit no status (test.yml posts none); promote-baseline.yml runs from main's copy on the PR's closed event main's code
Nightly / manual gate.yml that ref's gate.yml not a PR: docs_only=false both run (the guard's first operand is true) job result only, no status that ref's code

Simulated rows, all as designed:

  • Same-repo: docs; code passing; code with failing pytest; a CI-only change; an executable .md.
  • Fork: docs; code; code whose head already passed in a same-repo PR; deleted fork.
  • Nightly.
  • Pushes: this PR's landing, a later docs-only, code or CI-only push, a docs push right after a code push, a manual run without before.

The installed checker, run from this codegen path, refuses this PR's own diff (.github/** is outside the rule), so it takes the full gate.

This PR's own checks

No workflow triggers on this PR by itself:

  • pull_request reads the definitions in GitHub's test commit of this PR on main, whose gate.yml and test.yml no longer have that trigger.
  • pull_request_target reads main's old gate.yml, which has none.
  • A push to this branch reads its test.yml, which runs on main only.

The required gate therefore comes from one manual run of this branch's gate.yml, dispatched once right after this PR opened: run 37317597451.

  • gate.yml exists on main with workflow_dispatch, and the branch's copy has it too.
  • The run's head SHA is this PR's head, and its gate job check-run lands on it. The reporter posts no commit status on a manual run, and GitHub does not count a workflow_dispatch check-run toward a PR's required context; see Bootstrap below.

It runs this PR's own gate.yml, not main's. That is acceptable for this bootstrap PR because:

  1. No definition on main can gate it.
  2. Every earlier PR was gated by its own pull_request definition anyway.
  3. It is a same-repository branch: no fork code runs, and the run holds only the file's own permissions ({} at the workflow level, contents: read per job).
  4. It runs the real differential gate and pytest matrix, not a self-asserted pass. The measured pr-gate receipt remains the authority for landing it.

Three paths run only once this is on main: classifying a PR head in .candidate, the fork guard, and the status POST. The simulations above exercised them; a docs-only smoke PR afterwards proves them live.

Bootstrap

  • One manual run of this branch's gate.yml, run 37317597451 (workflow_dispatch, head 5b596a4), passed: preflight-docs, differential, pytest 3.11–3.14 and the gate job all succeeded.
  • Its check suite lists this PR, yet the PR's check rollup stayed empty and its state BLOCKED. GitHub does not count a workflow_dispatch check-run toward a PR's required context.
  • By the supervisor's ruling, one commit status was posted on 5b596a4: context gate, state success, target the run above, description "bootstrap: gate.yml run 37317597451 (workflow_dispatch) passed on this head" (status 55606203402, 2026-10-05 13:46:42Z). That is exactly what the installed reporter posts under pull_request_target.
  • This is a one-time gap: once this is on main, the reporter posts gate itself. No bypass actor or ruleset change was used.

Trust audit

Workflow / job Runs PR head code Token Secrets OIDC Cache Checkout credentials Cap
gate.yml preflight-docs no: main's checker; head read as data contents: read none no none persist-credentials: false (both checkouts) 10 min
gate.yml differential same-repository heads only contents: read none no (id-token not granted) none persist-credentials: false 30 min
gate.yml pytest same-repository heads only contents: read none no none persist-credentials: false 30 min
gate.yml gate (reporter) no (no checkout) statuses: write none (GITHUB_TOKEN) no none none 10 min
test.yml (push to main) main's code contents: read none no none persist-credentials: false 10 / 30 min
promote-baseline.yml no: fetches the PR head's objects to compare trees contents/statuses/checks: read, id-token: write App key, as before yes, main-only identity none default for the base checkout (its git fetch); persist-credentials: false for the tooling checkout none

Other properties:

  • gate.yml sets permissions: {} at the workflow level.
  • No ${{ }} appears inside any run: of gate.yml / test.yml; event data reaches scripts through env.
  • The pinned actions/setup-python v5.6.0 and actions/setup-node v4.4.0 restore a cache only when cache: is set, and nothing sets it. package.json has no packageManager field. No workflow here uses a cache.
  • Actions settings: enabled, all actions allowed, SHA pinning not required.

Verification

  • Lint (on an AWS spot box). actionlint 1.7.12 with shellcheck 0.9.0: 0 findings on all five workflows. zizmor 1.30.1 (offline):
    • gate.yml: only dangerous-triggers (pull_request_target, by design).
    • test.yml: no findings.
    • promote-baseline.yml: its trigger, artipacked on the base checkout (whose git fetch a private repository needs), and an info about the trusted ${{ steps.axis.outputs.repo }}.
  • pineforge-workflow tests against these exact bytes (build(deps): bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.2 #106 at 8cbd5c6, run on the same AWS box with Node 24.12: codegen-docs-gate, baseline-promote, pr-gate-hook, publish-reader, state-gcp-receipts-pg): 104 tests, 91 pass, 13 skipped (need Postgres), 0 fail. Each new test fails against the previous templates. The five files are byte-equal to build(deps): bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.2 #106's templates at this head.
  • pineforge-workflow's full campaign suite at build(deps): bump pypa/gh-action-pypi-publish from 1.14.0 to 1.14.2 #106's head a0117d0 (on an AWS spot box; a0117d0 changes only a Postgres test fixture, so the templates are as at 8cbd5c6):
    • npm test: 1,332 tests, 1,298 pass, 0 fail, 34 skipped.
    • make test-pg: 60 tests and 39 pipeline tests passed on a throwaway postgres:16 container, none skipped.
    • Base d9f4f22 on the same box: 1,329 / 1,295 / 0 / 34, and test-pg all passed.
  • lab remote on CLOUD. Job rj-20261005t130442-7b7aa5 ran at this head 5b596a4 (tree be3dab55, clean) against engine b7ed0ee7 (engine main), with --steps pytest,compile-corpus,gate,pyodide:
    • pytest: 5741 passed, 41 skipped in 1178.53 s; junit: tests=5782 failures=0 errors=0 skipped=41. The engine, version.h, Eigen, libpineforge.a, the 314-strategy corpus and the 1m feed all resolved. The skips: 35 compare against older commits the shipped bundle's history lacks, 2 need clang++ (the host has g++), 4 have empty parameter sets.
    • compile-corpus: 314 passed; junit: tests=314 failures=0 errors=0 skipped=0.
    • gate: gate selftest: 8 comparator + 10 verdict cases OK; gate: PARITY OK over 277 fixtures (verdicts asserted: ok/* succeed, err/* rejected).
    • pyodide: npm pack: @pineforge/[email protected], 65 files, 1532754 B packed.
    • Before the rebase onto Confirmed-bar receipt: versioned metadata from lowering decisions #173, the same four steps also passed against engine v1.2.0 (rj-20261005t112325-e8fd06).
  • test_many_bindings_are_walked_once_each stays at main's 60 s CPU budget. On spark2 (NVIDIA GB10: 10 Cortex-X925 + 10 Cortex-A725) it took 7.2 s of CPU alone on an X925 core and 13.7 s on an A725. Across 40 concurrent runs at up to 2.4× CPU oversubscription (1-min load 48.6 on 20 cores) it took 8.9–12.3 s of CPU and up to 49 s of wall clock, with 0 failures.
  • Measured pr-gate. Experiment exp-cgadopt-gate-20261005: 7,989 probes in 134 cases, attempt 1, 0 failed, engine tree ed8ac522 and codegen tree be3dab55 (both clean).
    • Candidate snapshot: 90fe216de76ac33929941b1a4aa09db32f0429ac1c20c1eb4b5b6bb8ffa0c2b0 (1,521,054 bytes; pineforge-publish-snapshot-c9jk5).
    • Baseline: the active pineforge-parity-baseline-20261005-engine-b7ed0ee7, snapshot f1892e49b520778c0942bcd5313cdec59395d27be9f514088d44fc505c5c02cf.
    • Population: 40c482c2… (3,731,436 bytes; content cf857209…).
    • pineforge-pr-gate-q5stx: verdict PASS (no-improve-no-regression), pineforge-pr-gate-verdict/v3. Changes: hard 0 improved / 0 worsened, target 0 / 0.
    • Recorded by lab gate record: receipt 0be89135….

Ruleset (unchanged here)

Read 2026-10-05:

  • PineForge strict CI base (21476387) requires context gate with no app pin, strict up-to-date, do_not_enforce_on_create: false. Bypass: one Integration (4105147).
  • Org default adds deletion protection, linear history and a PR rule with 0 approvals.

Pinning gate to GitHub Actions (app id 15368):

  • The CI reporter keeps working: its GITHUB_TOKEN statuses are created by github-actions[bot], and its job check-run comes from app 15368.
  • lab gate docs --post would no longer count. It posts with the operator's own gh login, and CI now posts the same docs-only verdict.
  • The measured pr-gate path is unaffected: it writes the registry only and posts nothing to this repository.

Planned once this lands: one docs-only smoke PR, then required_status_checks: [{"context": "gate", "integration_id": 15368}] (strict and bypass unchanged).

Notes, not changed here

  1. The reporter is if: always(). A run cancelled by a newer run on the same head (an edited event) still posts failure. The newer run normally posts later and wins; a re-run fixes the rare reverse order. Promotion now ignores the superseded red run.
  2. Dependabot now sends GitHub Actions bumps as one grouped PR a month (.github/dependabot.yml). The workflows are the templates', so each bump goes to pineforge-workflow too and needs a measured pr-gate here. npm and pip stay weekly. Dependabot PRs are same-repository branches and take the full gate. On pull_request_target, GitHub makes the token read-only only when the PR's base ref was created by Dependabot. Here that ref is main, so the reporter should be able to post; the first such PR will confirm it. A bumped action in gate.yml runs only once it is on main, because PRs run main's definition.

CONTRIBUTING.md gains one note on how CI treats fork PRs.

🤖 Generated with Claude Code

…for forks

Installs the five templates of pineforge-workflow's codegen docs-only gate
(workflow #102, with the guard and review fixes of workflow #106 at
8cbd5c6), byte for byte:

  campaign/ci/codegen-gate.yml       -> .github/workflows/gate.yml
  campaign/ci/codegen-test.yml       -> .github/workflows/test.yml
  campaign/ci/codegen-docs-check.mjs -> .github/pineforge/ci/codegen-docs-check.mjs
  campaign/src/codegen-docs-rule.mjs -> .github/pineforge/src/codegen-docs-rule.mjs
  campaign/ci/promote-baseline.yml   -> .github/workflows/promote-baseline.yml

gate.yml runs on pull_request_target from main's definition: a classifier
reads the PR head as data and runs only main's checker, an accepted
documentation/legal-only diff skips the Pyodide differential gate and the
pytest matrix, and a reporter that checks out nothing posts the required
`gate` commit status on the exact head. The root README.md and LICENSE,
which the build reads, count as documentation only as edits. test.yml runs
on pushes to main only, once per commit, and skips a docs-only push.
promote-baseline.yml reads the `gate` status and judges each check-run
name by its newest run; its actions are pinned to full commit SHAs and its
campaign-tooling checkout keeps no credentials. Every job is capped (10
minutes for the classifiers and the reporter, 30 for the heavy jobs).

This repository is public and takes fork pull requests, so `differential`
and `pytest`, the two jobs that run the PR head's code, run only when the
event is not pull_request_target or
github.event.pull_request.head.repo.full_name == github.repository. The
reporter clears the full gate only for a same-repository head. A fork's PR
that does not pass gets a failing `gate` whose description says a
maintainer must push the branch here and open a PR from it; when a run of
this repository already posted a `gate` success on the same commit, the
fork's run leaves it and exits 0 instead of turning that PR red. A fork's
docs-only proof runs no fork code and clears as a same-repository one
does. Nightly and manual runs stay full.

Dependabot groups its GitHub Actions bumps into one monthly PR: the
workflows are the templates', so every bump also goes to pineforge-workflow
and needs a measured pr-gate here. CONTRIBUTING.md says how CI treats pull
requests from forks.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@luisleo526
luisleo526 merged commit 609b351 into main Oct 5, 2026
8 checks passed
@luisleo526
luisleo526 deleted the ci/docs-gate-adopt branch October 5, 2026 14:08
@luisleo526

Copy link
Copy Markdown
Contributor Author

Record for the CI install, added 2026-10-05 after it landed.

Required-status pin. The ruleset on main (id 21476387) now requires the status context gate from GitHub Actions only (integration_id 15368), with the strict up-to-date policy; the ruleset was updated 2026-10-05 14:12:29Z, and before that the context was required with no app pin. Only the reporter job of .github/workflows/gate.yml satisfies it. A gate status posted by hand does not, so it can no longer unblock a merge.

One-time bootstrap (done). This PR could not be gated by its own workflow: no definition on main had the pull_request_target trigger yet. After one manual workflow_dispatch run of the branch's gate.yml passed on the head (run 37317597451, head 5b596a4), one gate commit status was posted by hand on that head (status 55606203402, 2026-10-05 13:46:42Z). No bypass actor and no ruleset change was used. It was a one-time gap: from the merge on, the reporter posts gate itself for every PR, and a PR that edits gate.yml is classified and gated by main's copy.

Live runs since the merge. #174 (README only) and #177 (LICENSE and Markdown): gate success "docs-only preflight+docs; no sweep", posted by github-actions[bot] (14:11:17Z and 14:38:53Z), differential and pytest skipped, no hand-posted status.

Still open: promote-baseline.yml reads the newest gate status of any creator; adding the creator check there is a separate CI change.

luisleo526 added a commit that referenced this pull request Oct 6, 2026
* docs: changelog and version scoping for 1.3.0

1.3.0 pairs only with engine v1.3.0. The changelog section takes in the
Unreleased note and covers #173, #178, #179, #177, #174 and #175: the
confirmed-bar receipt and what engine v1.3.0's live runner does with it,
the capability receipt's request entries now read from the lowering, the
order-shapes receipt, which no engine reads yet, the checked settings fix
for string dropdowns with built-in constants (now supported=false where
1.2.0 wrote an integer code or published the input as supported), the
PineForge Source License 1.2, and the pairing and migration steps. The
Python and JSON contract is unchanged; the engine's report harness is the
same file as in v1.2.0, so no report key changes.

The README, AGENTS.md / CLAUDE.md, CONTRIBUTING.md, LEGAL.md,
docs/PUBLIC_CONTRACT.md, docs/pine-cap-activation.md and npm/README.md
name 1.3.0 where they named 1.2.0 as the current release or pair, the
pairing table gets a 1.3.0 row, the "unreleased (after 1.2.0)" markers
name 1.3.0, and the license badge reads 1.2. The release scoreboard
sentence and the pf markers are left to the release lane.

Measured for 1.3.0: the engine's 325 corpus sources and the 277 gate
fixtures, each in a fresh process, transpile to 1.2.0's C++ byte for byte
once the confirmed-bar and order-shapes functions are removed (13
refusals, same messages), with transpile_full results and transpile_json
envelopes without cpp equal to 1.2.0's, each in under 0.1 s on CPython
3.14.6 on an Apple M4 Max. The tutorial run of the quick-start C++
against engine main 7a1f01c0 (Linux x86_64, GCC 13) prints 9 trades and
+738.20, and 13 trades with the old sizing declared, as 1.2.0 with engine
v1.2.0 does.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: release 1.3.0's grades in the README and the changelog, from the facts tokens

The README's release sentence moves to the releases[1.3.0] tokens and its
main-scoreboard lines re-render, and the 1.3.0 changelog says which registry
baseline grades the release and why that baseline is the release's code,
all rendered from pineforge-release facts/facts.json at ff21de0 (#32).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant