Compass Renovate: one PR per devenv fork bump (RIG-5045)
Problem / Intent
Section titled “Problem / Intent”Compass pins the RigelBuild/devenv fork rev in two locks, devenv.lock and agent-image/devenv.lock. Each lock has its own regex manager and its own packageRule, so one fork commit opens two PRs: #2064 and #2065 both moved the fork to a9d9df8. Matt ruled option B on RIG-5045: one Renovate PR per fork bump. Its branch task relocks every changed lock, root first, then runs the FOD refresh once.
Approach
Section titled “Approach”tools/renovate/config.json5 carries two fork packageRules. They differ only in depName, groupName and fileFilters:
matchDepNames: ["RigelBuild/devenv"],groupName: "devenv fork (root)",// …fileFilters: ["devenv.lock", "agent-image/entrypoint.nix"],matchDepNames: ["RigelBuild/devenv-agent-image"],groupName: "devenv fork (agent-image)",// …fileFilters: ["agent-image/devenv.lock", "agent-image/entrypoint.nix"],Both run bun tools/renovate/refresh-devenv-lock.ts, then bun tools/renovate/refresh-fod-hashes.ts. The relock picks one scope with changedDevenvLock in tools/renovate/refresh-devenv-lock.core.ts, which throws when both locks changed:
if (changed.length > 1) { throw new Error( `refresh-devenv-lock: ${changed.length} devenv locks changed vs the branch point ` +Why one rule, not two rules sharing a groupName
Section titled “Why one rule, not two rules sharing a groupName”Renovate builds the branch config from the first upgrade only. generateBranchConfig (renovate@44.46.2, dist/workers/repository/updates/generate.js) sorts the upgrades by fileReplacePosition, then depName, and then spreads the first one:
config = { ...config, ...config.upgrades[0],executePostUpgradeCommands (dist/workers/repository/update/branch/execute-post-upgrade-commands.js) builds exactly one branch-mode task from that config:
postUpgradeTasks: config.postUpgradeTasks.executionMode === "branch" ? config.postUpgradeTasks : void 0With two rules in one group, the task would come from whichever upgrade sorts first, and its fileFilters would name one lock. Renovate would drop the other lock’s relock from the commit. One packageRule that matches both depNames gives both upgrades the same postUpgradeTasks, so sort order cannot matter.
Change
Section titled “Change”-
One packageRule. The two fork rules become one, in the root rule’s position:
matchManagers: ["custom.regex"],matchDepNames: ["RigelBuild/devenv", "RigelBuild/devenv-agent-image"],groupName: "devenv fork",minimumReleaseAge: null,postUpgradeTasks: {commands: ["bun tools/renovate/refresh-devenv-lock.ts","bun tools/renovate/refresh-fod-hashes.ts",],fileFilters: ["devenv.lock", "agent-image/devenv.lock", "agent-image/entrypoint.nix"],executionMode: "branch",},The branch is
renovate/devenv-fork(the slug rule ingenerateBranchName,dist/workers/repository/updates/branch-name.js; “devenv fork (root)” gave #2064’srenovate/devenv-fork-(root)).minimumReleaseAge: nulland dailymainHEAD tracking carry over unchanged (DL-415, carried into DL-446). -
Keep both managers. The two regex managers keep their file patterns, matchStrings and distinct depNames, so each still extracts one rev from one file and the extraction tests stay valid. The distinct depName now only keeps the two updates apart in the PR body and on the dashboard; it no longer splits them into two groups. Only the comments that say otherwise change.
-
Relock every changed scope.
changedDevenvLockbecomeschangedDevenvLocks. It returns every changed scope inDEVENV_LOCK_SCOPESkey order (root, then agent-image), whatever order the diff lists them in, and[]when none changed. It never throws.refresh-devenv-lock.tsloops over the result and runs today’s per-scope body unchanged: read the lock,nix run <flakeref(devenvSource(lock))> -- update devenvin the scope’scwd, the byte-identical check, and the post-relock shape guard. Each lock is still written by the devenv CLI it pins. -
Fail fast. The first scope that fails throws, and the script exits non-zero before it touches the next scope. Two facts make this right:
- The failures are correlated.
flakerefintools/toolchain/devenv-cli/core.tsis`github:${src.owner}/${src.repo}/${src.rev}#devenv`, and after the regex bump both locks hold the same rev. A build or fetch that fails for root will almost always fail again for agent-image, so a second attempt spends a second cold build and fixes nothing. - The budget is shared.
getRawExecOptions(dist/util/exec/index.js) appliesexecutionTimeoutto each child process:timeout ??= defaultExecutionTimeout * 60 * 1e3. Bothnix runs now share one 45-minute budget inside onebun tools/renovate/refresh-devenv-lock.tsprocess. A second attempt after a slow first failure risks the hard kill.
The cost: if root fails, agent-image is never attempted, so both locks ship rev-bumped and not relocked. Renovate catches each command’s failure separately, so it still runs the FOD refresh and still commits the regex bumps (
postUpgradeCommandsExecutor, same file).renovate:lock-integrityin the requiredrollupthen names every half-relocked lock (DL-413), and a re-run of the branch retries both. - The failures are correlated.
-
FOD refresh once, last. In
FOD_ENTRIES(tools/renovate/refresh-fod-hashes.ts), only theagent-node-modules-agent-image-pkgsentry is triggered by either lock:file: "agent-image/entrypoint.nix",// …triggers: ["bun.lock", "devenv.lock", "agent-image/devenv.lock"],It has no
mirrorFiles, soagent-image/entrypoint.nixis the only FOD file the rule must commit. The refresh’s per-entry gate fires once and realises the pin once, against both written locks. Today the two PRs pay that realise twice.
Cost and limits
Section titled “Cost and limits”- After the regex bump both locks name the same fork rev, so both
nix runs use one flakeref. Both managers setpackageNameTemplate: "https://github.com/RigelBuild/devenv", and the git-refs datasource caches by that name (getRawRefsindist/modules/datasource/git-refs/base.js:withCache({ namespace: `datasource-${gitId}`, key: config.packageName }, …)). One run therefore resolvesmainonce for both upgrades. T2 pins the twopackageNameTemplates equal, because a split would break this. executionTimeout(45 minutes,tools/renovate/bot-config.json5) bounds each child process, and the relock is one process. So one 45-minute ceiling now covers both relocks together, and it does not change. That is safe only because the second relock is cheap. [INFERENCE] The secondnix runreuses the store path the first one built. T3 records each relock’s wall time, but a warm local store cannot show cold-runner timing, and the PR body says so.- No command string changes, so
allowedCommandsinbot-config.json5keeps its eleven entries. - Nothing forces equal revs. If a hand relock moves one lock alone, the lagging lock gets its own one-member
renovate/devenv-forkbranch at the next daily run, and the script relocks only that scope. - No fork PR is open today; #2064 and #2065 are merged.
pruneStaleBranchesdefaults totrue(dist/config/options/index.js), so Renovate deletes any leftoverrenovate/devenv-fork-(root)orrenovate/devenv-fork-(agent-image)branch.
Relation to RD-1
Section titled “Relation to RD-1”RD-1 in docs/designs/infra/ci/compass-devenv-source-dry/design.md (§“Resolved decisions”) rules:
unify root onto
github:RigelBuild/devenv, WITHOUT lock reconciliation — AND keep both locks updated through Renovate bumps
and gives this reason for no reconciliation:
reconciling would couple an image-motivated fork bump to a dev-shell relock.
Ruling B creates that coupling for the devenv input. This record supersedes RD-1’s independent-cadence clause for the devenv fork input only. The rest of RD-1 stands:
- One source: both locks resolve
github:RigelBuild/devenv. - Two lock files, each written by its own devenv CLI in its own directory.
- Independent cadence for every other input. The two nixpkgs channel rules (“devenv nixpkgs channel” and “devenv nixpkgs channel (agent-image)”) keep separate groups.
- Both locks stay current through Renovate.
No decision file records RD-1: no file under docs/designs/decisions/ points at the devenv-source record, so there is no row to flip. DL-446 records this ruling. The frozen RD-1 record is not edited.
DL-415 (“The two RigelBuild/devenv fork rules keep tracking main HEAD daily with no soak”) names two rules, so DL-446 supersedes it and carries its daily, no-soak ruling forward unchanged for the one merged rule.
Alternatives considered
Section titled “Alternatives considered”- Two rules with one shared groupName. Rejected: the branch task comes from
config.upgrades[0], so one lock’s relock would be dropped, depending on sort order. - One manager with a widened
managerFilePatternsand one depName. Saves one manager block, but rewrites the manager and its extraction tests for no gain. Rejected as churn. - Relock every scope, then report all failures. It would relock agent-image when only root fails. But both scopes run one flakeref, so their failures are correlated, and a second attempt eats into the one 45-minute per-process budget both relocks share. The PR is red either way, and lock-integrity names every unrelocked lock. Rejected (Change item 4).
Global Constraints
Section titled “Global Constraints”- Versions. Every Renovate claim is checked against
renovate@44.46.2, the pin in.github/workflows/renovate.yml. - Commands. Both command strings stay byte-identical:
bun tools/renovate/refresh-devenv-lock.tsandbun tools/renovate/refresh-fod-hashes.ts.bot-config.json5allowedCommandskeeps its eleven entries. - Scope order. Relock order is
DEVENV_LOCK_SCOPESkey order, root then agent-image. Never the ordergit diffprints. - Per-lock writer. Each scope builds its flakeref from its own lock (
flakeref(devenvSource(before))) and runs in its owncwd. No scope reuses another scope’s flakeref. - Exit codes and messages. Exit
0means relocked or no-op; exit1means a step failed. Keep the substrings the tests assert:nothing to do,byte-identical,devenv fork rev,base ref does not resolve,now at,changed vs <ref>;. - Clean cutover.
changedDevenvLockis deleted, with no alias. Its only importers arerefresh-devenv-lock.tsandrefresh-devenv-lock.core.test.ts.devenv-lock-integrity.tsimports onlyDEVENV_LOCK_PATHS, which does not change. - Comments. Explain why, not what (
AGENTS.md). Rewrite only the sentences that describe two fork PRs or independent fork cadences. Leave the rest of each block alone. - Frozen records. Do not edit
docs/designs/infra/ci/compass-devenv-source-dry/design.md,docs/designs/repo/compass-renovate-relock-fail-closed/design.mdordocs/designs/infra/runtime/compass-guest-image-artifact/design.md. - Landing. One implementation PR, with T1 then T2 as commits. T2 needs T1, because today’s script throws on a two-lock branch. T1 alone changes nothing live, so it does not ship alone.
- Lint. Run
biome checkon the touched TS and JSON5 files only.
T1: Relock every changed scope
Section titled “T1: Relock every changed scope”Files: tools/renovate/refresh-devenv-lock.core.ts, refresh-devenv-lock.core.test.ts, refresh-devenv-lock.ts, refresh-devenv-lock.test.ts, all in tools/renovate/.
Interfaces:
// refresh-devenv-lock.core.ts, unchangedexport type DevenvLockScope = "root" | "agent-image";export const DEVENV_LOCK_SCOPES: Record<DevenvLockScope, { readonly lock: string; readonly cwd: string }>;export const DEVENV_LOCK_PATHS: readonly string[];export function devenvForkLockedRev(devenvLockText: string): string;// replaces changedDevenvLock/** Every scope whose lock is in changedPaths (exact match), in DEVENV_LOCK_SCOPES key order; [] when none. Never throws. */export function changedDevenvLocks(changedPaths: readonly string[]): readonly DevenvLockScope[];
// refresh-devenv-lock.ts, module-private/** Today's Step 3 body for one scope; throws on any failed step. */async function relockScope(scope: DevenvLockScope, baseRef: string): Promise<void>;The CLI does not change: bun tools/renovate/refresh-devenv-lock.ts, optional env RENOVATE_BASE_BRANCH, exit 0 or 1. In main(), Step 2 becomes const scopes = changedDevenvLocks(changedPaths). An empty list keeps today’s nothing to do return. Then for (const scope of scopes) await relockScope(scope, baseRef). A throw ends main() before the next scope.
Test helpers in refresh-devenv-lock.test.ts:
const AGENT_BUMPED_REV = "4444444444444444444444444444444444444444";async function applyRegexBump(repo: string, lockRel: string, narHash: string, rev = BUMPED_REV): Promise<void>;/** Every flakeref rev the stub was run with, in call order. Replaces provisionedRev. */async function provisionedRevs(repo: string): Promise<string[]>;Test cycle: edit the tests first, run bun test tools/renovate/refresh-devenv-lock.core.test.ts tools/renovate/refresh-devenv-lock.test.ts, and see them fail. Then implement until they pass.
refresh-devenv-lock.core.test.ts:
- Rename the describe to
changedDevenvLocks. - “selects the root scope when only the root lock changed” expects
["root"]. “selects the agent-image scope when only that lock changed” expects["agent-image"]. - “returns null when no devenv lock changed (the self-gate no-op)” becomes “returns [] when no devenv lock changed (the self-gate no-op)”. It and “does not mistake a same-named lock elsewhere for a governed scope” assert
[]. - Replace “throws when BOTH locks changed (the groupName isolation broke)” with “returns both scopes, root first, whatever the diff order”.
[AGENT, ROOT],[ROOT, AGENT]and["bun.lock", AGENT, ROOT]all give["root", "agent-image"]. - Rename “the two scopes are distinct files (independent cadences, not reconciled)” to “the two scopes are distinct lock files”. Its comment says the fork rev now moves in one PR, but the locks stay two files.
refresh-devenv-lock.test.ts:
-
Replace “exits non-zero when BOTH locks changed on one branch” with “relocks BOTH locks, root first, each in its own directory under its own devenv”. Bump root to
BUMPED_REVand agent-image toAGENT_BUMPED_REV. Expect exit0, both locks atRELOCKED_REV, and.devenv-update-cwdsequal to[repo, join(repo, "agent-image")].provisionedRevsequals[BUMPED_REV, AGENT_BUMPED_REV], which proves each lock used its own flakeref. Stdout has twonow atlines. -
Add “stops at the first failing scope and leaves the second untouched”. Bump both and write the root scope’s fail marker,
join(repo, ".force-fail-relock"). Expect a non-zero exit, exactly one line in.devenv-update-cwds(the repo root), andagent-image/devenv.lockstill exactlydevenvLock(AGENT_BUMPED_REV, "BBBB"). The agent-image scope has no marker, so a loop that ran on past the failure would relock it and fail this test. -
Add “a second-scope failure exits non-zero after relocking the first”, as a
test.eachover two markers written tojoin(repo, "agent-image", …):.force-fail-relock, and.force-noop-relockwith stderr containingbyte-identical. Bump both. Expect a non-zero exit,devenv.lockexactlydevenvLock(RELOCKED_REV, "CCCC"),agent-image/devenv.lockexactlydevenvLock(AGENT_BUMPED_REV, "BBBB"), and.devenv-update-cwdsequal to[repo, join(repo, "agent-image")]. A loop that swallowed the second error and returned0fails here. -
In the per-scope happy path,
provisionedRev(repo)becomesprovisionedRevs(repo)equal to[BUMPED_REV]. Its title still holds: with one lock changed, the sibling is not relocked. -
STUB_NIXreads its markers per scope. Today it reads$REPO_ROOT/.force-…, and the marker applies to every run:Terminal window if [ -f "$REPO_ROOT/.force-fail-relock" ]; thenAll three markers (
.force-noop-relock,.force-fail-relock,.force-corrupt-relock) move to$PWD/.force-…, the directory the stub runs in. The root scope runs in the repo root, so the existing single-scope tests keep their marker paths unchanged. The comment above the stub (“Sentinel files in the repo root”) says the markers live in each scope’s own directory. The cwd and argv logs stay at$REPO_ROOT.
Comments to rewrite, in these files only:
refresh-devenv-lock.core.ts: the file header (“which of the two devenv locks a branch touched”; a wrong scope’s write “the fileFilters allowlist drops”), theDevenvLockScopedoc (“its own packageRule, and its own branch”), and thechangedDevenvLockdoc and throw, which go away.refresh-devenv-lock.core.test.ts: the file header and theDEVENV_LOCK_SCOPEScwd-test comment. The failure they name becomes “a wrongcwdrelocks the sibling and leaves this lock unrelocked”.refresh-devenv-lock.ts: the header lines that say one packageRule and branch per lock, and “a two-lock branch fails loud”; the Step 2 comment; and, in Step 3, “the two revs differ by design — RD-1 unifies the source, not the locks”. The reason that still holds is that a hand relock can leave the revs different.refresh-devenv-lock.test.ts: the header (“pick scope” becomes “pick scopes, relock each”); the happy-path comments “(fileFilters admits only the one)” and “the two locks are independent (RD-1)”; and the comment on the replaced both-locks test.
T2: One fork rule
Section titled “T2: One fork rule”Files: tools/renovate/config.json5, tools/renovate/config.test.ts, tools/renovate/bot-config.json5 (comments only), tools/renovate/refresh-fod-hashes.ts (header comment only).
Interfaces: the merged packageRule in the Approach, placed where the root fork rule is today. The agent-image fork packageRule (groupName: "devenv fork (agent-image)") is deleted. Both fork customManagers keep every key.
Test cycle: edit config.test.ts first, run bun test tools/renovate/config.test.ts, and see it fail. Then edit config.json5 until it passes.
config.test.ts:
FOD_COMMANDcomment: “SEVEN task sites” becomes six.- “the fod-hash refresh is declared at all seven task sites” becomes “…at all six task sites”, with
toHaveLength(6). Renumber its list: 1 top-level, 2 devenv-nixpkgs channel, 3 devenv fork (relocks both locks, each a declared trigger), 4 go lockstep, 5 catalog, 6 Meissa. “Sites 3, 4 and 6 carry it fail-safe” becomes “Sites 3 and 4”. - The allowlist describe comment says the relock rides the one devenv-fork rule. “declares eleven DISTINCT postUpgrade commands and eleven allowlist entries” does not change.
- The
coupledpopulation drops from 12 to 11, because two fork sites become one. “the coupled (site, entry) set has its expected shape (guard is not vacuous)” assertstoBe(11), and its comment says six sites name a trigger of the entrypoint.nix entry. The guard then checks the merged rule: refresh last,agent-image/entrypoint.nixinfileFilters. - In describe “tools/renovate devenv fork currency (RIG-2815, RIG-2546 T7)”:
forkScopeskeepslabel,depName,lockandpatternLiteral, and dropsgroupName,taskCommandsandtaskFileFilters. Addconst GROUP = "devenv fork"andconst forkRule = cfg.packageRules.find((r) => r.groupName === GROUP).- Keep “declares a git-refs regex manager for the $label lock’s fork rev” and “matchString extracts the fork rev from the real $label lock”. Reword the comment that says the depName keeps the rules “in separate branches”. The comment on its
packageNameTemplateassertion now gives the reason the two scopes must share that value: it is the git-refs cache key, so one run gives both upgrades the samenewDigest(Cost and limits). That per-scopetoBe("https://github.com/RigelBuild/devenv")already pins the two managers equal, so no separate equality test is added. - Replace “the $label fork rule is solo-grouped and cooldown-exempt” with “one fork rule matches both depNames, solo-grouped and cooldown-exempt”. Assert that
matchDepNamesequals both depNames, that exactly one rule hasGROUP, that exactly one rule names either depName (so no later rule can win last-match), and thatminimumReleaseAgeisnull. - Replace “the $label relock postUpgradeTask is branch-mode over the files it writes” with “the fork task relocks, then refreshes the FOD pin, branch-mode, over the three files it writes”. Assert
commandsequals[RELOCK, FOD_COMMAND]andfileFiltersequals["devenv.lock", "agent-image/devenv.lock", "agent-image/entrypoint.nix"]. - “both fork rules declare the SAME relock command (one allowlist entry)” becomes “one rule declares the relock command (one allowlist entry)”, with
declaringof length1. - “the $label fork digest resolves to its own solo branch, not the TS rollup” becomes “the $label fork digest resolves to the one fork group, not the TS rollup”. It is the same
resolveGroupNamereplay, and both scopes now expectGROUP. - Delete “the two fork scopes carry DISTINCT groupNames (independent cadences)”.
- Reword the describe header comment, and the “RD-1 forbids reconciling the rules” line on “both fork managers declare the IDENTICAL matchString literal”.
- “its group differs from the root channel and agent-image fork groups” does not change. It now reads “devenv fork” for the fork pin, which is still one of three distinct groups.
config.json5 comments:
- The merged rule’s header says why one group (one fork commit moves both locks) and why one rule (the branch task comes from the first upgrade only). Its
fileFiltersparagraph lists the agent-image lock and drops “Listing the agent-image lock would be dead surface”. - The agent-image fork manager drops “each tracks the fork on its own cadence” and “land in DIFFERENT groups/branches”.
- The agent-image channel manager drops the agent-image FORK manager from its list of fencing examples.
- The guest-rootfs rule says “the devenv-fork rule above”, not “rules”.
bot-config.json5 comments: the FOD-refresh site list names “devenv fork rule” once and says six sites. Item 6 says the one devenv-fork rule runs the relock, and the script relocks every changed lock, root first. executionTimeout stays 45.
refresh-fod-hashes.ts header: “FIVE sites … the devenv fork (root) rule” becomes the six sites in the renumbered list above.
T3: Smoke
Section titled “T3: Smoke”-
bun test tools/renovate/passes. -
bunx -p renovate@44.46.2 renovate-config-validator --no-global tools/renovate/config.json5prints “Config validated successfully”. Its migration diff has only today’s hunk,excludeDepNameson the GitHub Actions rule (measured onmainwhile writing this record). -
Renovate lookup and live relock, in a throwaway colocated clone at the PR head (
jj git clone --colocate). The scripts callgit rev-parse --show-toplevel, so a non-colocated jj workspace cannot run them.-
Precondition: fork
mainHEAD is stilla9d9df8e…(git ls-remote https://github.com/RigelBuild/devenv refs/heads/main), andjj log -r '1e4e1fe1098a..main@origin' -- devenv.lock agent-image/devenv.lockprints nothing. The step-6 oracle is valid only while both hold. If either has moved, skip step 6. Instead, require both locks’nodes.devenv.locked.revto equal the fork HEAD and step 7 to pass. -
Rewind each lock to the commit just before its own fork bump, so every other input still matches
main. Start withjj new <PR head>, then runjj restore --from 07d356f2f964- devenv.lock(before #2064) andjj restore --from 1e4e1fe1098a- agent-image/devenv.lock(after #2041, before #2065). Then runjj commit -m "smoke: rewind fork revs",jj bookmark create smoke-base -r @-andgit update-ref refs/remotes/origin/smoke-base smoke-base.07d356f2f964-itself is wrong for the agent-image lock: it predates #2041, so it would also rewind that lock’s nixpkgs channel, and the FOD refresh would realise against the old channel. -
Lookup, before any edit. This puts both upgrades through Renovate’s own grouping, which the validator and the hand-run scripts never touch:
Terminal window RENOVATE_PLATFORM=local RENOVATE_CONFIG_FILE_NAMES=tools/renovate/config.json5 \RENOVATE_INCLUDE_PATHS=devenv.lock,agent-image/devenv.lock RENOVATE_X_IGNORE_RE2=true \LOG_LEVEL=debug LOG_FORMAT=json bunx -p renovate@44.46.2 renovate \| jq -c 'select(.msg == "packageFiles with updates") | .config.regex[].deps[]| select(.depName | startswith("RigelBuild/devenv"))| {depName, branchName: [.updates[]?.branchName]}'Expect both
RigelBuild/devenvandRigelBuild/devenv-agent-imagewithbranchName["renovate/devenv-fork"]. Every input reaches Renovate through env, not a global config file.tools/renovate/bot-config.json5cannot be used here: it setsrepositories, andautodiscover.jsthrows “repositories list not supported when platform=local”.configFileNamesis global-only but has noenv: false, sogetEnvNamemaps it toRENOVATE_CONFIG_FILE_NAMES, andparseConfigspasses it tosetUserConfigFileNames. On the local platform,initPlatform(dist/modules/platform/local/index.js) forcesdryRunto"lookup", so the run never writes a branch.lookupstill callsbranchifyUpgradesand logs “packageFiles with updates”, andflattenUpdatesstampsupdate.branchNameon each dep’s update. -
Regex bump: in the working tree, set only
nodes.devenv.locked.revin each lock to the forkmainHEAD. -
Run
RENOVATE_BASE_BRANCH=smoke-base bun tools/renovate/refresh-devenv-lock.ts, thenRENOVATE_BASE_BRANCH=smoke-base bun tools/renovate/refresh-fod-hashes.ts. Expect exit0from both, the root relock logged before the agent-image one, and a wall time for each relock. -
Oracle:
git diff --exit-code origin/main -- devenv.lock agent-image/devenv.lock agent-image/entrypoint.nixexits0. Both locks and the FOD pin are then byte-equal to what #2064, #2065 and the FOD refresh produced onmain. -
GITHUB_EVENT_NAME=pull_request GITHUB_BASE_REF=smoke-base bun tools/renovate/devenv-lock-integrity.tsexits0and printsPR mode (base origin/smoke-base …). PR mode checks only the nodes changed since the merge-base, as in CI.
Measured while writing this record, on a clone at
main(1e4e1fe) built as in steps 1-2:- Step 3 control: today’s two rules print
renovate/devenv-fork-(root)andrenovate/devenv-fork-(agent-image), both atnewDigesta9d9df8e…. With the merged rule applied to the clone, both printrenovate/devenv-fork. - Steps 5-7: today’s script still throws on two locks, so the two per-scope commands T1 will run were run by hand, root first:
nix run github:RigelBuild/devenv/a9d9df8e…#devenv -- update devenv, in.and then inagent-image/. Each took about 1.3 s on a store that already held the fork build. The FOD refresh realised theagent-image/entrypoint.nixpin in 11 s. The oracle exited0, and lock-integrity checked 18 nodes in PR mode and exited0. - Negative control: with the root lock rev-bumped but not relocked, the oracle exits
1and lock-integrity exits1with “FAIL devenv.lock devenv … narHash expected …”.
-
- T1:
changedDevenvLocksplus the fail-fast per-scope loop inrefresh-devenv-lock.ts; per-scope stub markers; core and harness tests (both-locks, first-scope and second-scope failure) red, then green. - T2: One
devenv forkpackageRule;config.test.tsat six FOD sites, eleven coupled pairs and the merged-rule assertions;bot-config.json5andrefresh-fod-hashes.tscomments. - T3: Smoke steps 1-3: tests, validator, then the local-platform lookup, live relock, byte-exact oracle and PR-mode lock-integrity. Results go in the PR body, with the note that a warm store hides cold-runner timing.
Resolved decisions (Matt, 2026-10-10)
Section titled “Resolved decisions (Matt, 2026-10-10)”Ruled on RIG-5045: option B. One Renovate PR per devenv fork bump covers both locks. Its branch task relocks every changed lock, root first, and runs the FOD refresh once, last. Recorded as DL-446. It supersedes RD-1’s independent-cadence clause for the devenv fork input only (see Relation to RD-1).