Repository navigation
land.yml: the landing driver on Actions, with the token job running no tree code - #226
Merged
Merged
Conversation
…-checks.yml, the cross-host lock and safe dispatch Stacked on #225. The token job checks out its own commit without credentials, installs and runs only that checkout, points the merge drivers at it by absolute path and never pulls; a tree's install, typecheck, evidence stamp, lane judgement and device-outcome merge run in land-checks.yml (no secrets) and come back as data. LAND_TOKEN is in the land environment and the driver step only; the driver hands it per call to git push and its own gh calls. refs/tags/land-lock (LAND_GLOBAL_LOCK) keeps a Mac and an Actions driver apart; pnpm land:dispatch and the re-dispatch never queue a second pending land run.
…er in the token job, the lock read on every run, a required stop channel, land-checks timeouts - Records are written, read and removed in a tree's worktree only without following a symlink (writeInTree, readInTree, removeInTree), after refusing a mode-120000 entry at or above them in the trees they come from; core.symlinks=false under LAND_TRUSTED; a regen or checks patch with a symlink is refused; artifacts are read without following links. - The token job runs no pnpm: its scripts import only node: built-ins (tested by import closure); node scripts/land.ts, the merge drivers set by absolute path in the workflow, pr-review.ts run with node; treeCodeRefusal refuses any package manager. The trusted checkout is read-only (but .git) before the driver runs. package.json leaves .macroscope/ignore.md, so it is reviewed. - A run that does not take refs/tags/land-lock still reads it and refuses to start while a running land.yml holds it. - land.yml refuses a queue without LAND_STOP_ISSUE. - land-checks.yml: each tree command under timeout; results uploaded always; a failed Land checks step or a completed run with no result.json is the tree's failure. - land/proof: a status with no creator id is ignored as untrusted.
- After a dispatched run completed and passed, an artifact that is missing, not the workflow's shape, unreadable, or a patch that is refused (PatchRefused: a symlink, not a regular file, git apply failing on the tree it was made from) fails the PR at its step; the driver's own precondition and network errors stay CI trouble. Same for the regen patch and the device outcomes. - Each position build under LAND_CI=only is marked on the PR's head (land/outage: pending, error on a CI outage, success on a verdict); admission ejects a PR whose builds ended without a verdict LAND_OUTAGE_EJECT times in a row (default 2), naming the runs. - land-checks.yml kills every process the tree's commands left before it puts the results and the records patch together. - The symlink check of a patch normalises CRLF; the Mac's read of the lock tag retries transient errors.
… the PRs whose trees ran (#226 re-review) - A prepared position's CI regen is marked on its PR's head (land/outage pending, error on an outage). - A position's full test is marked on every PR its tree holds; a PR with any build or proof since its last verdict ended without one is built and proved alone (runBatches solo), so the next outage is pinned on it and the streak ejects the culprit. - land-checks.yml runs each tree command in a session of its own (setsid timeout) and kills that session (pkill -s) once it ends, instead of sweeping the user's processes. - pnpm land:clear-outage <pr> ends a streak at the current head after a real GitHub outage (trusted like every status).
Each build writes its statuses with its own id ([build <id>]: the land.yml run and attempt, or the host and pid, and a counter); a build is judged by its newest status, so a pending followed by its outage is one, and a pending with nothing after it (a killed run) is one. One real outage now leaves a count of 1 (the PR is rebuilt alone), and two in a row eject it.
… property tests (#226 re-review) The streak counts driver runs, not phases: each run records on a PR's head that it admitted it (in), an outage in a step that ran its tree (outage), a verdict about it (it failed, a full test passed on a tree holding it, it merged), or, at a normal end with neither, a neutral left. A run with an outage, or killed with only in, counts 1; a verdict ends the streak; passed phases write nothing. outageLedger keeps it; seeded property tests drive thousands of random runs of random phases through the real ledger, writers and streak, and check the count rule, the resets, the eject limit, solo pinning the culprit and every other PR landing. The Mac's run id holds the process start time.
… the real wiring tested (#226 re-review) - CiOutage takes the PRs whose code the failing step ran as a required argument; every site takes them from attribute(stage), over the PRs merged into each built position (Trees). The CI stages are CI_STAGES: the admission and landing-commit CI waits, the prepared regen, the regen, tree checks and devices of a position build, and the full test. A builder's outage carries them. - runBatches returns the outage's PRs; the driver records them once, at the run's end (recordOutage). The per-phase calls are gone. - A passed full test is no longer a verdict: the landing commit's CI wait can still hit an outage that must count. The run asking is left out of its own count (a PR requeued in a run is admitted again). - Tests: an outage injected at each CI stage through the real runBatches, attribute and recordOutage lands on exactly the right PRs; the seeded property tests run on the same; a TypeScript-AST check of land.ts finds every CI dispatch, wait and CiOutage and fails unless each is attributed through attribute() to a stage the injection test covers. - ci.yml's checks job has timeout-minutes: 30, so a hang is a failed check (a verdict), well inside LAND_CI_WAIT (90 min).
Commands: pnpm regen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cloud migration item 9:
.github/workflows/land.yml, stacked on #225 (item 8). The design is split so that the job holding the landing token runs no code from any PR or merged tree. This follows the PM's ruling on the Claude review of #225: both High findings and Medium 5. Mac runs behave as before: every new behaviour is behindLAND_TRUSTED=1orLAND_GLOBAL_LOCK=1, which only land.yml sets.The job that holds LAND_TOKEN runs no tree code (High 1)
ref: ${{ github.sha }}, master at dispatch time, withpersist-credentials: false. Only that checkout is installed (pnpm install --frozen-lockfile) and run. The job has no otheractions/checkout, noworking-directoryand nocd.LAND_TRUSTED=1inscripts/land.ts(it requiresLAND_CI=only):run, and somustandheavy, refuses any command whose cwd isn't the trusted checkout (treeCodeRefusal, a Fatal). The localparity:lanesinjudgeDevicesis refused the same way.trustedGitConfig, so each driver isnode '<trusted checkout>/scripts/floor-merge.ts' …(andsorted-merge.ts) by absolute path. Anything else is refused.git pull --ff-onlyin its checkout, so code that just merged never runs in this job..github/workflows/land-checks.yml. It is dispatched on master for the tree's commit, pushed apart toland-checks/c-<sha>, withcontents: read,actions: read, no secrets and no dependency cache.device-ci.ts mergeof a device run's outcomes,pnpm typecheck,evidence:stamp --compare <prev>andparity:lanes, and records each one's exit code and output tail inresult.json. The merged records come back asoutputs.patch.parseChecksResult) and applies the patch with the same tree check as the regen patch.install; a merge exit 3 is a refusal. Results that don't read, or Actions not running the workflow, stop the driver as a CI outage, with no PR blamed.LAND_CI=only.LAND_TOKENappears only in the token-check step (assecrets.LAND_TOKEN != '') and in theLand one batchstep's env, never in a job env.git push(aGIT_CONFIG_*extraheader in that call's env) and to its ownghcalls (GH_TOKENin that call's env). Children such aspr:reviewand the review lookup read with the job's read-onlyGITHUB_TOKEN.GITHUB_TOKENpermissions are allread.environment: landon the job (High 2).Queued runs and the cross-host lock (Medium 5)
landjob, not the run. Theredispatchjob runs after the land job ends and holds no secret: GITHUB_TOKEN withactions: writemay dispatch workflows.dispatchLand(scripts/land-state.ts) serves both the re-dispatch andpnpm land:dispatch. GitHub keeps one pending run per concurrency group and cancels the older, which would silently drop its queue. SodispatchLand:return_run_details;pnpm land:dispatch <branch>:<pr>:<clean-head> .... Nevergh workflow run land.ymlby hand while a land run is queued. To stop after the current batch, set theland-stoplabel on issuevars.LAND_STOP_ISSUE.LAND_GLOBAL_LOCK=1takesrefs/tags/land-lock. It is an annotated tag whose message names the holder: a land.yml run, or a pid on a host.git push --force-with-lease, which is atomic, rather than a label, which has no compare-and-set.LAND_GLOBAL_LOCK=1for the two to exclude each other. I left the Mac default off, per "Mac default unchanged".Second round: the security review of 18ff0b9 (2 High, 3 Medium, and the Low on #225)
writeInTree,readInTreeandremoveInTree(land-lib.ts). Every directory on the way must be a real directory, and the file is opened withO_NOFOLLOW. The two record write sites (the carried records, and the merge's records in a prepared tree) first callrefuseRecordSymlinks: a mode-120000 entry at or above a record, in the source trees or the worktree's index, fails the PR atrecords.LAND_TRUSTEDthe repository hascore.symlinks=false, so git writes a tree's links as plain files.applyRegenPatchrefuses a patch that creates or keeps a symlink. This covers both the regen patch and the land-checks records patch, on any host.result.json,outputs.patch,full-test-results.json) are read without following links.seedRegenCachedoesn't run underLAND_TRUSTED.chmod -R a-w) before the driver step, and checks that nothing outside.gitis still writable. Only.gitstays writable: the driver's worktrees, fetches, refs, config and merges live there. Its logs, run directory and locks are in$RUNNER_TEMPand/tmp.LAND_LOG_DIR, the run directory,/tmplocks,mkdtempdirectories, its own worktrees,.git), or goes through git. Git refuses to apply a patch beyond a symbolic link. The ignored-file cleanup inresetWorktreenow usesremoveInTreetoo.pnpm/action-setupand no install. The workflow sets the git config itself, withnode '<checkout>/scripts/floor-merge.ts'andsorted-merge.tsby absolute path. It runsnode scripts/land.ts.LAND_TRUSTED,pr:reviewruns asnode <checkout>/scripts/pr-review.ts, andtreeCodeRefusalrefuses pnpm, npx, npm, yarn and corepack anywhere. On the Mac,pnpm -s pr:reviewis unchanged.node:built-ins and repository scripts. A test walks the import closure withts.preProcessFileand fails on anything else.**/package.jsonis removed from.macroscope/ignore.md, so every package.json is now reviewed.pnpm-lock.yamlstays ignored, since the token job installs nothing. macroscope-ignore.test.ts now lists package.json among the reviewed paths.LAND_GLOBAL_LOCK=1still readsrefs/tags/land-lock(checkLockFree). If the tag is held by a land.yml run that hasn't completed, the run refuses to start. Any other holder is logged loudly. With no tag, behaviour is unchanged. Taking the lock on the Mac stays opt-in.vars.LAND_STOP_ISSUEis an issue number.Each tree command runs under
timeout --kill-after=60s. A hang becomes that command's recorded exit 124, and the driver fails the PR at that check. The limits, each several times a usual run:The job limit is now 150 min, and the driver's
LAND_CHECKS_WAITdefault is 165 min.Results upload with
if: always().A failed
Land checksstep is a verdict (VERDICT_STEP), and so is a completed run with noresult.json: the driver blames the tree. Only setup steps and the runner count as an outage.The new tests in land-trusted.test.ts:
git apply;symlinkEntriesandpatchHasSymlinkare checked directly.treeCodeRefusalrefuses a package manager;pr:reviewruns with node when trusted; and the import closure test.if: always(), and that the record lines are the only tree commands.Third round: the re-review of 350af11 (one Medium, two Lows)
A tree can't steer its own runs into an outage. Once a dispatched run has completed and passed, every byte of its artifact comes from where the tree's code ran. So these now fail the PR at that step instead of stopping the driver:
PatchRefused: a symlink, not a regular file, or onegit applycan't apply to the tree it was made from).This covers the land-checks results and records patch, the regen patch and the device outcomes. Network errors, and the driver's own precondition (its worktree is the tree it sent), are still CI trouble.
Per-PR outage count (
LAND_OUTAGE_EJECT, default 2).LAND_CI=only, each build of a PR's position is recorded as the commit statusland/outageon the PR's head: pending while it builds, error when it ended in a CI outage, success when it reached a verdict (built, or failed).ci-outage, with the landing-failed label and a comment naming each run and how it ended.Stray processes. land-checks.yml kills every process of the runner user that the tree's commands left behind (all but the step's ancestors and the runner's own) before it writes result.json and makes the records patch.
Lows.
Existing tests changed (in land-devices-ci.test.ts): a passed run with an empty device-outcomes artifact, and a regen artifact that isn't exactly
outputs.patch, were CiUnavailable and are now the PR's LandFailure, per this ruling. A failed download that isn't "missing" stays CiUnavailable. That test's title now says "a failed download" instead of "a bad artifact".Fourth round: the re-review of 1bb56c5 (one Medium, one Low)
verifyChain). It is pending during the proof, success on a pass or a test failure, and error on an outage. A batch can't pin a full-test outage on one PR.runBatchessolohook starts a batch of 1 for it, and never puts it into an existing batch. So the next outage is pinned on it, and the streak ejects the culprit.setsid timeout …, with no job control, so the session id is the process id). Once the command ends, it kills that session only (pkill -KILL -s <sid>). There is no name list and no user-wide sweep, so nothing of the runner's is at risk. A process that leaves the session through its ownsetsidcan still write the results. Whatever it writes is the tree's artifact, and a bad one fails the PR, never CI.pnpm land:clear-outage <pr>writes a trustedland/outagesuccess at the PR's current head after a real GitHub outage. It counts only from the driver's identity or aLAND_PROOF_WRITERSid, like every status. So the PM's account must be onLAND_PROOF_WRITERSfor its clear to count.LAND_OUTAGE_EJECToutages in a row.Fifth round: the re-review of 5f86880 (one Medium)
land/outagenow counts builds, not statuses. Before this, a single outage counted twice: once for the pending status and once for the error after it.newBuild(). On Actions the id is the land.yml run and attempt plus a counter; on a Mac it is the host and pid plus a counter. Every status of that build carries the id as[build <id>]in its description.outageStreakjudges each build by its newest status. A pending followed by its outage is one build. A pending with nothing after it (the run was killed) is one. A status with no id counts on its own.New tests run the real write sequence through
writeOutageinto a fake status store:They also check that every driver mark passes its build's id.
Sixth round: the re-review of b8fc65a (third streak finding): a design and a check for the whole class
The fourth and fifth rounds' build-level marks are replaced, per AGENTS.md step 6.
The model: count outages per driver run, not per phase (
outageLedgerin land-state.ts). Each run records on a PR's head, asland/outagewith[run <id> <kind>]in the description:in: the PR was admitted to the run;outage: a CI outage in a step that ran its tree (its build, with its regen, land-checks and devices; a prepared regen; a full test or bisect prefix of a tree holding it);verdict: a verdict about the PR (it failed, though not an ejection for outages; a full test passed on a tree holding it; it merged);left: the run ended normally with neither, as for a PR requeued behind a culprit.How a run counts toward the streak:
in(it was killed, maybe by the tree), and no verdict.leftrun is neutral: it neither counts nor resets.LAND_PROOF_WRITERScount.The check: seeded property tests (mulberry32, failing seed reported) drive random driver runs through the real ledger,
writeOutageandoutageStreak. Each run has admission with eject and solo, each member's phases (prepared regen, build, land-checks, devices), the full test and bisect prefixes, and publish. Each phase gets a random result: pass, verdict-fail, outage or killed. Outages include batch outages that mark several PRs.LAND_OUTAGE_EJECT.Seventh round: the re-review of fb002b1 (the CI waits were not attributed): attribution in the type
Attribution is part of the outage.
CiOutage(message, prs)now requires the PRs whose code the failing step ran, so TypeScript refuses an unattributed outage. Every site takes them fromattribute(stage, …)(land-lib.ts), using aTreesmap of the PRs merged into each position this run built. A builder's outage carries its PRs throughserializeFatalandparsePrepared. The stages areCI_STAGES:admission-ciprepared-regenregen,tree-checks,devicesfull-testpublish-ciOutages are recorded in one place.
runBatchesreturns the outage's PRs, and the driver records them once, at the run's end (recordOutage), before the ledger'send. The per-phase calls are gone.Two consequences of the real wiring:
inas an outage. Admission andsolonow leave the asking run out.The tests use the real wiring:
runBatches,attribute,TreesandrecordOutage. For a batch MIT LICENSE, and ship README and LICENSE in the dragon package #1 Land work through reviewed PRs (Macroscope, PR CI, pr:review) #2 README: Sponsors section #3, it must land on exactly the expected PRs (by hand: admission-ci [2], prepared-regen/regen/tree-checks/devices/publish-ci [1, 2], full-test [1, 2, 3]), minus any PR that merged before the outage.checkRuns). It fails unless every call to them, everynew CiOutageand every call to a function that takesprsis attributed throughattribute(), a forwardedprs, or a builder'sround.prs. It also fails unless the stages used are exactlyCI_STAGES, which the injection test iterates.publish-cifails the injection and culprit tests, and passing[]at the publish wait fails the AST check.ci.yml's checks job has
timeout-minutes: 30. Recent runs take 3 to 5 minutes, so that is six times the slowest, and it is well insideLAND_CI_WAIT(90 min). A hang becomes a failed check, which is a verdict. A test keeps the limit at least 15 minutes and at most half ofLAND_CI_WAIT.Owner-only setup (exactly)
landenvironment, restricted to master, and put theLAND_TOKENsecret in it, not at repository level. The proof run below auto-created the environment with no rules (deployment_branch_policy: null). Restricting it to master is required: until then, any branch's workflow could reference the environment.timeout-minutes: 350). Otherwise use a fine-grained PAT with the same permissions. For an App token, set the variableLAND_TOKEN_USER_IDto the App bot's user id, becauseGET userfails for App tokens.LAND_REVIEWERS(ids or logins) to the review agent's own account. The driver refuses to start if the token's identity is on that list.LAND_STOP_ISSUEto the tracking issue for theland-stoplabel. A queue is refused without it. Optionally setLAND_PROOF_WRITERS, extra trusted ids forland/proof.Proof without the secret
I dispatched an empty-queue run on this branch at the current head 7e31e21 (
gh workflow run land.yml --ref land-yml -f queue=''): https://github.com/compiled-run/dragoncss/actions/runs/37761499936. Earlier rounds' runs: 37759248154 at fb002b1, 37757864100 at b8fc65a, 37756732643 at 5f86880, 37754892663 at 1bb56c5 and 37753380953 at 350af11.LAND_TOKENis set, so the token check only warns for an empty queue. UnderLAND_TRUSTED=1, on ubuntu:GET userreturned 403 for GITHUB_TOKEN, so it took the github-actions[bot] identity (41898282);land/proof: "no record on master or the 20 commits it rests on; under LAND_CI=only that counts as unproved";land-checks.yml can only be dispatched from master, so it first runs once this lands.
Tests (no network)
All in the new packages/parity/test/land-trusted.test.ts.
environment: land.secrets.LAND_TOKENappears exactly twice, in the two steps named above, and in no job env.read.github.shawithpersist-credentials: false.run:command matches an exact allowlist, and no step changes directory.trustedGitConfigrewrites each driver to the trusted checkout and refuses anything else.treeCodeRefusalrefuses commands outside the trusted checkout.runrefuses first; the lane judgement, the installs and the pull are guarded; the only processes started are git, gh, ps, the trusted scripts and the local-only bash and pnpm (each behind its guard); and the token is read once and deleted.queue,redispatch(dispatch and the stop label) anddispatch, against a fakeghon PATH.Existing tests changed
Each change follows a code change; no check is loosened.
scratchRefrefusal message now listsland-checks/c-<sha>.CiUnavailablecatches, not 3. The tree-checks catch always stops as a CI outage, which is stricter than the ci-only checks.judgeDevicessource strings follow the newlanesRanargument.What passed
pnpm typecheck.pnpm exec vitest runon land-trusted, land, land-devices-ci, land-parallel, land-supervisor, chrome-ports, both registry-claims, macroscope-ignore, cloud-setup, floor-merge and pr-review: 12 files, 338 tests.actionlint1.7.12 on land.yml, land-checks.yml and ci.yml. ci.yml now lints the two landing workflows.🤖 Generated with Claude Code