Compass: migrate Dependabot → self-hosted Renovate in GitHub Actions
Status: Draft
Problem / Intent
Section titled “Problem / Intent”Compass runs GitHub Dependabot for its three ecosystems (.github/dependabot.yml:
github-actions at /, bun at /, gomod at /go, all weekly single-group). Matt
wants Dependabot off — it carries hidden GitHub-billed features, and the fleet
should run ONE dependency manager, not two. The internal monorepo already runs
self-hosted Renovate, proven through the catalog, devenv-nixpkgs, and toolchain-pin
lockstep machinery. Migrate compass onto the same Renovate, adapted to compass’s
layout — with the hard constraint that it runs in GitHub Actions (compass has
no Woodpecker; all its CI is GHA). The repo is pre-prepped: the design-ledger
gate already exempts renovate/ branches (tools/design-ledger-gate/index.ts:87,
EXEMPT_BRANCH_PREFIXES = ["renovate/"], commented “Renovate dependency bumps”).
Note “Dependabot off” has TWO halves: deleting .github/dependabot.yml stops
VERSION updates only; Dependabot security updates/alerts — the hidden
GitHub-billed feature driving this migration — is a repo Settings toggle the
yml deletion does not touch. The plan disables it explicitly (T8’s runbook),
with osvVulnerabilityAlerts: true replacing the coverage.
Global Constraints
Section titled “Global Constraints”- NEVER
vulnerabilityAlerts: { enabled: true }— a vuln fix injects a packageRule withforce.enabledtruthy, which clearsskipReasonand CANCELS the fork fence (the internal monorepo’s Renovate config). UseosvVulnerabilityAlerts: trueonly.config.test.tsmust guard both facts, as the internal monorepo’s does. minimumReleaseAge: "5 days"+internalChecksFilter: "strict"— consistent with compass’sbunfig.toml:6minimumReleaseAge = 432000(5 days). Mirror bunfig’s exact-name exemptions (bunfig.toml:20-24:@tanstack/virtual-core,@types/bun,bun-types) so Renovate and bun install agree on what soaks — implemented by a catalog-scoped packageRule (matchPackageNames: ["@types/bun", "bun-types"], minimumReleaseAge: null, see packageRules) paired to bunfig’s list by aconfig.test.tsguard;@tanstack/virtual-coreneeds no rule (anoverridespin, outside the catalog manager’s reach). The@types/bun/bun-typeshalf of that exclude list — and the catalog@types/bun: ^1.4.0pin — was landed by the bun 1.4 migration (RIG-2373, compass #461, merged), on the rationale that the types track the nix-pinned runtime (tools/toolchain/versions/bun.nix) and so must not soak behind every toolchain bump. The packageRule and guard 12 therefore mirror the current-mainbunfig; all line citations here are to that file.- Fork trees disabled — compass vendors
forks/{devenv,nix2container,oh-my-pi}(rootforks/, not a nested location). A packageRulematchFileNames: ["forks/*/**"], enabled: false(a scoped disable, neverignorePaths, which replaces Renovate’s safe defaults, mirroring the internal monorepo’s prior art). - Toolchain pins auto-open solo branches — Matt’s standing ruling: every toolchain bump (bun/node/moon/go) opens its own un-grouped PR (ported from the internal monorepo’s prior art).
- Every postUpgradeTasks command in bot-config
allowedCommands,^…$-anchored — a repo config can’t self-authorize a command;config.test.tspins the two lists together (as the internal monorepo’s does). - TypeScript
<7cap (SEA-1867) — compass’s catalog pins"typescript": "^6.0.3"(package.json:21), so the Project Corsa cap applies: TS 7.0 ships no stable programmatic API (ported from the internal monorepo). - Timezone/schedule alignment (SEA-1220) —
timezone: "America/New_York"in the repo config, and the GHA cron (UTC) must land inside theschedule:dailybefore-4am-ET window WITH margin: GHA scheduled runs are best-effort and routinely start 5-30+ minutes late, so the cron must not sit near the window edge or a delayed start silently opens zero PRs forever.0 6 * * *UTC = 02:00 EDT / 01:00 EST — 2-3h of margin in both DST phases. @tanstack/virtual-corestays pinned at 3.17.5 — a reviewed, record-mandated floor via rootoverrides(package.json:24-26,bunfig.toml:7-12), not a float Renovate may bump. (The catalog regex manager scopes to the"catalog": {…}block only, so theoverridesblock is structurally out of reach — no extra rule needed;config.test.tsshould pin this.)RENOVATE_X_IGNORE_RE2=trueon the runner —bunx renovateinstalls no native re2 addon; take the RegExp fallback deliberately (as the internal monorepo’s CI/CD does).- Writable HOME for postUpgradeTasks —
customEnvVariables: { HOME: … }in bot-config (RIG-2245:devenv update nixpkgspanics on an unwritable$HOME/.local/share/devenv; ported from the internal monorepo). GHA runners have a writable$HOMEnatively, but keep the declaration versioned and testable. - A new
tools/*test package is inert until registered in.moon/workspace.yml— moon discovers projects ONLY from the explicit map there (.moon/workspace.yml:10); an unregistered moon.yml “is silently inert — moon never discovers it, so [it] ships a functional-CI registration that gates nothing while reading as covered, which is worse than no gate” (.moon/workspace.yml:77-83). T5/T6 registertools/renovateandtools/renovate-preflightand verify withmoon query projects.
Approach
Section titled “Approach”Port the internal monorepo’s proven self-hosted Renovate (repo config + bot config + lockstep
scripts + config tests) into compass, adapted to compass’s paths and ecosystems,
and run it as a plain GitHub Actions workflow that provisions the language
toolchains the same way compass’s other CI jobs do, plus the devenv CLI
built from the vendored fork via a PATH shim (compass CI provides no PATH
devenv — see Runner shape). Delete .github/dependabot.yml in the same PR —
one manager, no overlap window.
Runner shape: a plain GHA job (option B)
Section titled “Runner shape: a plain GHA job (option B)”The workflow (.github/workflows/renovate.yml) is: actions/checkout →
cachix/install-nix-action → put the language toolchains on PATH via
tools/toolchain/gate-tools.nix (the exact two-phase bootstrap compass’s ci.yml
already uses at lines 144-212: nix eval -f tools/toolchain/gate-tools.nix langs,
nix build, append $out/bin to $GITHUB_PATH) → build the VENDORED devenv
fork and shim it onto PATH (nix build path:forks/devenv#devenv, symlink
$out/bin/devenv into a dir prepended to $GITHUB_PATH — see T6) →
RENOVATE_CONFIG_FILE=<bot-config> RENOVATE_X_IGNORE_RE2=true bunx renovate@44.33.1 (exact pin — see below).
Why B: compass’s postUpgradeTasks need nix (toolchain-hash prefetch), devenv
(devenv-nixpkgs relock shells devenv update nixpkgs from PATH, as in the
internal monorepo), and bun (all three scripts +
bun install --lockfile-only). Compass’s GHA CI already provisions the
language toolchains per job via cachix/install-nix-action@630ae543…
(.github/workflows/ci.yml:150) + gate-tools.nix — the Renovate job composes
with that idiom instead of inventing a second toolchain path. devenv is the
one binary that idiom does NOT provide: compass CI never puts a devenv on
PATH — its only devenv invocations run the vendored fork’s CLI by path
(ci.yml:812 nix run path:../forks/devenv#devenv -- container copy agent;
agent-image/moon.yml:44; devenv.nix:469). The Renovate job therefore
builds that same fork (the flake exports the CLI as
packages.<system>.devenv, forks/devenv/flake.nix:113-115) and shims it
onto PATH. This is FORCED by the frozen fork posture, not a fresh choice: the
image pipeline pins to “the vendored fork’s own CLI … so it cannot diverge
from the fork source” (devenv.nix:450-453;
docs/designs/platform/compass-forks-reversal/design.md:125-134 — “The
fork’s own CLI is invoked by path everywhere the image is built”). A nixpkgs
devenv doing the relock would be a SECOND, divergent devenv — the exact thing
the fork posture eliminates (see Alternatives §D). Nix’s store caching plus
the declared substituters (ci.yml:165-168) keep setup cost bounded.
Alternatives A and C lose (see Alternatives considered).
Renovate itself is pinned. Bare bunx renovate resolves LATEST from npm
on every scheduled run — fresh registry code executing with a repo:write
token, an unpinned dependency whose job is managing pinned dependencies,
bypassing every soak defense this record mandates (minimumReleaseAge and
bunfig’s cooldown govern bun install, not bunx). The GitHub App auth
(see Secrets) bounds this exposure: the token is minted per run and expires
~1h later — an ephemeral credential rather than a long-lived stored PAT —
but a per-run token doesn’t excuse running unpinned third-party code; the
pin stands.
The workflow runs bunx renovate@44.33.1 (npm’s latest stable at design time;
the initial pin). The pin line is itself a managed dependency: a custom.regex
manager on .github/workflows/renovate.yml (datasource npm, depName
renovate) bumps it through a reviewable PR under the normal soak, and a
config.test.ts guard asserts the workflow pins an exact version (no bare
bunx renovate). Note the internal monorepo has the same exposure — its meta job runs bare
bunx renovate (its publish image bakes devenv/skopeo, NOT Renovate) —
fix it there as a fleet follow-up, out of scope
here.
Triggers: on: schedule: - cron: "0 6 * * *" (06:00 UTC = 02:00 EDT / 01:00
EST — inside the before-4am-ET schedule:daily window with 2-3h margin per
the SEA-1220 constraint; GHA cron is best-effort and routinely 5-30+ minutes
late, so a tighter cron like 0 7 — 60 min of EDT margin — risks a delayed
start past 04:00 ET reproducing the SEA-1220 silent-zero-PR symptom) +
workflow_dispatch for manual runs (the GHA analogue of the internal
monorepo’s Woodpecker {event: manual} trigger; it also revives the
schedule if GHA auto-disables it after 60 days of repo inactivity — see T6).
Cadence: daily (resolved decision, OQ5 — Matt 2026-08-21), dropping
dependabot’s weekly.
Secrets/Auth: a GitHub App (resolved decision, OQ2 — see Resolved
decisions). The
workflow mints a per-run installation token with
actions/create-github-app-token@<pinned-sha> # vX (SHA-pinned with a
version comment per the repo invariant, dependabot.yml:1-8), reading the
App’s client-id from the repo variable RENOVATE_APP_CLIENT_ID and its
private key from the ONE stored secret, secrets.RENOVATE_APP_PRIVATE_KEY.
The minted token is exported as RENOVATE_TOKEN for bunx renovate and as
GH_TOKEN (with REPO) for the preflight. There is NO long-lived PAT — no
secrets.RENOVATE_TOKEN exists; the token is minted fresh each run and
expires ~1h later. Workflows permission (LOAD-BEARING): the
github-actions manager (see Managers) edits files under .github/workflows/,
and a GitHub App can only push workflow-file changes if it holds the
Workflows repository permission (GitHub Docs: “if your app specifically
needs to access or edit Actions files in the .github/workflows directory,
request the Workflows repository permission”) — without it every
github-actions bump PR fails to push with a workflows-scope error. Full App
permission set: Contents (read/write — git access + non-workflow commits),
Pull requests (read/write), Workflows (read/write), Issues (read/write — the
dependency dashboard is an issue). The internal monorepo’s second secret
RENOVATE_GITHUB_COM_TOKEN is a read-only github.com PAT for release-notes
lookups against github.com from a non-github.com platform host; compass IS on
github.com, so the App token covers it — do not port the second secret.
Registering/installing the App is a human action (T8;
skill://human-action-handoff).
Port the internal monorepo’s tools/renovate-preflight probe
so an expired/unscoped token fails with a named diagnosis instead of Renovate’s
opaque platform-unknown-error. The ported preflight reads REPO (owner/name)
from the environment and exits fail-closed (exit 2) when it is missing — “could
not evaluate (missing REPO env) — fail closed”. The internal monorepo’s CI/CD
supplies REPO from CI_REPO; GHA has no CI_REPO, so T6’s workflow sets
REPO: ${{ github.repository }}.
Managers
Section titled “Managers”enabledManagers: bun, npm, gomod, github-actions, custom.regex.
Dropped from the internal monorepo’s list: cargo, rust-toolchain
(compass has no Rust), woodpecker (no Woodpecker), and nix — Renovate’s
nix manager tracks flake.lock, and compass has NO root flake: the only
flake.lock files in the tree live under forks/devenv/ and
forks/nix2container/ (glob-verified), both inside the forks/*/** fence
this record mandates enabled: false; devenv.lock/devenv.yaml are not
flake.lock (the custom git-refs manager covers them), so a ported nix
manager would be dead config. Added: github-actions — the internal
monorepo deliberately omits it (its meta jobs moved off GHA), but compass
keeps every workflow uses: pinned to a commit SHA precisely so a reviewable
PR moves the pin forward (.github/dependabot.yml:1-8). Renovate’s
github-actions manager natively updates an existing SHA pin and keeps the
# vX.Y.Z comment current — dependabot parity for maintained pins — and,
going beyond dependabot (which never pinned NEW actions), extends: ["helpers:pinGitHubActionDigests"] pins any future un-pinned uses: on
sight (guarded in config.test.ts). Group all actions bumps into one PR
(“GitHub Actions”, mirroring dependabot’s single actions group), with one
exclusion: the postgres CI service image (see packageRules — the pgtest.go
coupling).
Also intentionally omitted: the dockerfile/docker manager — compass
has no first-party Dockerfile. Glob-verified against the clone: the only
Dockerfiles in the tree are forks/oh-my-pi/Dockerfile,
forks/oh-my-pi/Dockerfile.robomp, and
forks/devenv/containers/devcontainer/Dockerfile, all inside the
forks/*/** fence this record disables — a dockerfile manager would be dead
config, same reasoning as the nix-manager drop above. (Auto-updating the internal
monorepo’s harvester oven/bun base image is a separate fleet follow-up, filed separately.)
customManagers: 6 of the internal monorepo’s 7 port, +1 compass-new
Section titled “customManagers: 6 of the internal monorepo’s 7 port, +1 compass-new”| # | Internal-monorepo manager | Compass disposition |
|---|---|---|
| 1 | Root package.json catalog regex | Port unchanged. Compass has the same unmanaged-catalog gap: workspaces.catalog (package.json:12-22, 9 pins) with catalog: consumers; Renovate’s bun manager doesn’t extract it. Keep versioningTemplate: "npm" (range preservation) and the recursive two-stage matchStrings; port the truncation-guard tests. |
| 2 | devenv-nixpkgs channel git-refs digest | Port unchanged. Compass has the same shape: devenv.yaml:9-10 → github:cachix/devenv-nixpkgs/rolling, locked in devenv.lock; devenv.nix:75-81 bakes biome + markdownlint-cli2 from that channel while @biomejs/biome is also a catalog pin (package.json:15) — the same dual-source lockstep applies. Compass difference: only biome is dual-sourced (markdownlint-cli2 has no catalog pin — grep markdownlint compass/package.json → none), so the ported relock script rewrites one catalog pin, not two. |
| 3-5 | bun/node/moon toolchain pins | Port with path change: the internal monorepo’s versions/*.nix → tools/toolchain/versions/*.nix (compass pin files confirmed: tools/toolchain/versions/{bun,node,moon,go}.nix; same rec { version; srcs.{x86_64-linux,aarch64-linux,aarch64-darwin} } shape, e.g. bun.nix:2-17). |
| 6 | Go version attr in devenv.nix | Port, retargeted at tools/toolchain/versions/go.nix — see “Go source of truth” below. |
| 7 | googleworkspace provider lockstep | Drop. Compass has no pulumi and no provider.lock.json. |
Plus one compass-new customManager (not a port): the Renovate self-pin
regex on .github/workflows/renovate.yml’s bunx renovate@<version> line
(datasource npm, depName renovate) — see Approach §Runner shape.
Go source of truth: go.nix, one regex manager
Section titled “Go source of truth: go.nix, one regex manager”Compass differs from the internal monorepo: there the go version lives ONLY in devenv.nix as the
"go_1_26_5" attr string, so that manager regexes devenv.nix. Compass
single-sources the version in tools/toolchain/versions/go.nix
({ version = "1.26.6"; }, version-only — hashes come from go-overlay) and
derives the attr name in devenv.nix:30-31:
goPin = import ./tools/toolchain/versions/go.nix;goToolchain = inputs.go-overlay.packages.${pkgs.stdenv.system}."go_${lib.replaceStrings [ "." ] [ "_" ] goPin.version}";So devenv.nix contains no literal go_X_Y_Z string — the internal monorepo’s regex would match
nothing there. Track go.nix instead: one regex manager,
managerFilePatterns: ["/^tools/toolchain/versions/go\\.nix$/"],
matchStrings: ["version = \"(?<currentValue>[^\"]+)\""],
datasourceTemplate: "golang-version", depTypeTemplate: "toolchain". No
dots↔underscores gymnastics (the version is dotted in the file), no
postUpgradeTasks leg (go-overlay ships the hashes; the refresh script must
self-gate past go.nix exactly as the internal monorepo’s script no-ops on go,
and devenv.nix updates automatically at
eval time. One bump PR touches one line. This was OQ4, now decided (see
Resolved decisions). Note the go.nix:8-9 floor policy: the go directive in
go/go.mod “tracks the tools/toolchain/versions/go.nix pin minus at most one
minor, so an upstream Go security patch never blocks on a mod edit”
(go/go.mod:10-12) — a MANUAL policy by design. Renovate’s gomod manager
extracts that directive from the same golang-version datasource and could bump
it ahead of the pin, so the gomod go-directive update is disabled by
packageRule (see packageRules); a pin bump PR may occasionally need a manual
go.mod follow-up.
packageRules
Section titled “packageRules”Port from the internal monorepo, adapted:
- “TypeScript dependencies” rollup:
bun/npm/custom.regexpatch+minor. Drop the Rust rollup (no cargo). - “Go dependencies”: gomod patch+minor.
- “GitHub Actions” group (compass-specific add):
matchManagers: ["github-actions"], one rollup PR — dependabot parity. - Postgres service image excluded (compass-specific):
matchDepNames: ["postgres"], enabled: false, with the coupling rationale in a config comment. The github-actions manager WOULD bump theci.yml:133service image digest (image: postgres:16-alpine@sha256:57c72fd2…), butgo/internal/pgtest/pgtest.go:50hard-codes the SAME digest as a Go const (const pgImage = "docker.io/library/postgres:16-alpine@sha256:57c72fd2…") that Renovate cannot see, andci.yml:124declares the parity load-bearing (“Matches pgtest.go’s pinned image”). A one-sided bump silently desyncs the CI↔local Postgres the suites assert against, so this digest moves only via a manual two-file PR. - gomod
godirective disabled:matchManagers: ["gomod"], matchDepNames: ["go"], enabled: false+ aconfig.test.tsassertion — keeps thego/go.mod:10-12floor policy manual (see Go source of truth). - bunfig soak exemptions implemented:
matchPackageNames: ["@types/bun", "bun-types"], minimumReleaseAge: null, scoped to the catalogcustom.regexmanager.@types/bunis a catalog pin (package.json:16,"@types/bun": "^1.4.0") that would otherwise soak 5 days behind every bun-runtime bump — exactly the strandingbunfig.toml:13-19documents exempting (“it would strand the types packages behind the pin for 5 days on every bun upgrade”;minimumReleaseAgeExcludes,bunfig.toml:20-24; both the exclude-list types entries and the^1.4.0catalog pin were landed by RIG-2373/#461, see Global Constraints). Aconfig.test.tsguard pairs the rule’s names to bunfig’s exclude list so the two files can’t drift. (@tanstack/virtual-coreneeds no rule — anoverridespin, out of the catalog manager’s reach, as Global Constraints already argue.) - Toolchain un-grouping:
matchFileNames: ["tools/toolchain/versions/*.nix"], groupName: null(ported from the internal monorepo, path adapted). Because the go manager now targetsgo.nixunder the same glob, this one rule un-groups all four pins — the internal monorepo’s separate go un-group rule is NOT needed; note this in the config comment and pin it in config.test.ts. - TypeScript
<7cap: port as-is. - devenv-nixpkgs solo branch: own groupName,
schedule: ["before 4am"]— DAILY, not the internal monorepo’s weekly-Monday["before 4am on monday"](resolved decision, OQ5 — Matt: nixpkgs also daily; the deliberate divergence from the internal monorepo’s prior art gets a config comment), aligned with the0 6 * * *UTC cron inside the before-4am-ET window;minimumReleaseAge: null(a moving-branch digest never clears a release-age window — the SEA-1220 silent-pending shape), branch-mode postUpgradeTasks running the ported relock script withfileFilters: ["devenv.lock", "package.json", "bun.lock"]. - Catalog lockfile coupling:
matchDepTypes: ["workspaces.catalog"],postUpgradeTasks: { commands: ["bun install --lockfile-only"], fileFilters: ["bun.lock"], executionMode: "update" }.executionModeMUST stay"update"— the internal monorepo’s comment documents the one-branch-mode-task-per-branch collision this avoids; port that rationale. - Fork fence:
matchFileNames: ["forks/*/**"], enabled: false(adapted from the internal monorepo’s fork fence to compass’s rootforks/). - Drop: “Nix flake inputs” group (dead config with the nix manager omitted; see Managers), provider solo branch, pulumi SDK disable, container-images group (compass has no docker-datasource deps; the postgres service image is deliberately EXCLUDED above, not covered).
- Top-level postUpgradeTasks:
bun <dir>/refresh-toolchain-hashes.tsbranch-mode,fileFilters: ["tools/toolchain/versions/bun.nix", …/node.nix, …/moon.nix]— no rust-manifest leg (compass has norust-toolchain.toml).
Supporting scripts: port 3, drop 1
Section titled “Supporting scripts: port 3, drop 1”All live beside the config in the renovate directory (tools/renovate/ — a
resolved decision, see Resolved decisions):
refresh-toolchain-hashes.ts+ test — port with compass paths (BUN_NIX/NODE_NIX/MOON_NIX = "tools/toolchain/versions/*.nix") and the entire Rust FOD leg removed (TOOLCHAIN_TOML/MANIFEST_HASH_NIXconstants,readChannel,channelManifestUrl,renderManifestHashFile— from the internal monorepo’s script). Keep the self-gate, per-leg rewrite, fail-loud, idempotence contracts.refresh-devenv-nixpkgs.ts+.core.ts+ tests — port; compass adaptation: only the biome catalog pin is rewritten (markdownlint-cli2 has no catalog pin in compass —package.json:12-22), and compass’s baked-vs-catalog coupling is the dev-shell parity story, not the internal monorepo’s image gate; the relock still must refreshdevenv.lockconsistently (rev + narHash + inner nixpkgs-src) and re-resolvebun.lock.config.test.ts— port the guard suite: allowedCommands ↔ postUpgradeTasks pinning, the no-vulnerabilityAlerts.enabledinvariant, real-manifest catalog extraction recovery + the two truncation mutation tests, solo-branch grouping outcomes, adapted to compass’s config. Compass-specific additions: the workflow pins an exact Renovate version (no barebunx renovate); thehelpers:pinGitHubActionDigestspreset is extended; the postgres-image disable rule exists; the gomodgo-directive disable rule exists; the@types/bun/bun-typessoak-exemption rule’s names matchbunfig.toml’sminimumReleaseAgeExcludes(minus theoverrides-pinned@tanstack/virtual-core).- Drop
refresh-provider-manifest.ts+ test (no pulumi).
File locations
Section titled “File locations”Repo config + bot config + scripts in tools/renovate/ (resolved decision).
Compass has no ci/ directory — every first-party tool lives under tools/*
(tools/{toolchain,design-ledger-gate,stamp-gate,…}), and tools/* is a bun
workspace member (package.json:10), giving the scripts the standard
tsconfig/test wiring. The bot config’s configFileNames: ["tools/renovate/config.json5"] makes the repo-config path free (mirroring the
internal monorepo’s bot config). The workflow itself is .github/workflows/renovate.yml
(GHA requires that location). The preflight probe ports to
tools/renovate-preflight/ (the internal monorepo’s own location, already tools/-shaped).
Bot config
Section titled “Bot config”Port bot-config.json5 with: configFileNames: ["tools/renovate/config.json5"];
repositories: ["RigelBuild/compass"] (must match the live slug — a renamed repo
is silently skipped, as the internal monorepo’s bot config documents); platform: github;
gitAuthor = the App’s [bot] noreply identity
(<app-id>+<app-slug>[bot]@users.noreply.github.com — Renovate autodetects
it from the installation token; pin it explicitly here once T8 registers the
App and the slug is known); onboarding: false; requireConfig: "required";
customEnvVariables: { HOME: "/tmp/renovate-home" }; allowedCommands with
exactly the three anchored entries compass’s config declares:
^bun tools/renovate/refresh-toolchain-hashes\.ts$,
^bun install --lockfile-only$,
^bun tools/renovate/refresh-devenv-nixpkgs\.ts$.
Fleet note: the internal monorepo’s own bot config still pins the
retired pre-RigelBuild-rename bot identity as its gitAuthor — a separate
fleet cleanup, not fixed by this record. (For non-App contexts the fleet
agent identity is mintaka <mintaka@rigel.build>, GitHub rigel-mintaka,
per ~/.config/jj/config.toml:21,34-35; for THIS workflow the committer is
the App bot.)
Alternatives considered
Section titled “Alternatives considered”A — renovatebot/github-action (official action)
Section titled “A — renovatebot/github-action (official action)”Runs Renovate inside the official renovate/renovate container. Clean for a
config-only repo, but compass’s postUpgradeTasks execute INSIDE that container,
which ships none of nix, devenv, or the pinned bun — the toolchain-hash
prefetch (nix store prefetch-file), the devenv relock (devenv update nixpkgs), and the lockfile regeneration all fail. Making it work means either a
custom Renovate image (that’s option C) or mounting a host toolchain into the
container (fragile, and nix store paths don’t relocate). Loses to B: same
workflow-trigger surface, strictly less toolchain access.
C — bake a compass-ci image with renovate + devenv (the internal monorepo’s approach)
Section titled “C — bake a compass-ci image with renovate + devenv (the internal monorepo’s approach)”The internal monorepo runs Renovate in its Woodpecker publish monolith — a CI
image that bakes devenv. Compass has no equivalent image: its only
published image is the agent image (publish-agent-image.yml), not a CI
toolchain image — compass CI provisions per-job via install-nix-action +
gate-tools.nix instead. Building and publishing a dedicated Renovate image adds
a whole image-publish pipeline (registry, staleness, rebuild triggers on
devenv.lock bumps) to save per-run nix setup that the nix cache already bounds.
Heavier for no correctness gain. Loses to B.
B — plain GHA job on compass’s existing toolchain bootstrap + a vendored-devenv shim (CHOSEN)
Section titled “B — plain GHA job on compass’s existing toolchain bootstrap + a vendored-devenv shim (CHOSEN)”See Approach. Reuses .github/workflows/ci.yml’s exact idiom for the language
toolchains (ci.yml:144-212); devenv — which no compass CI job puts on PATH
(the only devenv invocations in CI run the vendored fork’s CLI by path,
ci.yml:812 nix run path:../forks/devenv#devenv -- …) — is built from the
vendored fork and shimmed onto $GITHUB_PATH, so the relock script runs the
one devenv the fork posture allows; zero new infrastructure.
D — provision devenv from nixpkgs (nix profile install nixpkgs#devenv or a gate-tools attr) — REJECTED
Section titled “D — provision devenv from nixpkgs (nix profile install nixpkgs#devenv or a gate-tools attr) — REJECTED”Rejected: a nixpkgs-built devenv doing the relock would be a SECOND devenv,
divergent from the vendored fork whose CLI compass pins by path everywhere
devenv runs (“pinned to the vendored fork’s own CLI … so it cannot diverge
from the fork source”, devenv.nix:450-453; “The fork’s own CLI is invoked
by path everywhere the image is built”,
docs/designs/platform/compass-forks-reversal/design.md:125-134) — exactly
the divergence the frozen fork posture exists to eliminate. The vendored-fork
shim in B costs one nix build (cache-bounded) and keeps a single devenv.
Task order is dependency order: configs and scripts (T1-T5) are pure additions testable in isolation; the workflow (T6) consumes them; the dependabot removal (T7) lands only once Renovate is runnable; the App provisioning (T8) is the one human action and gates first live run, not the merge.
T1 — Port the repo config: tools/renovate/config.json5
Section titled “T1 — Port the repo config: tools/renovate/config.json5”Adapt the internal monorepo’s repo config per Approach: extends
config:recommended + schedule:daily + helpers:pinGitHubActionDigests;
timezone: "America/New_York"; dependencyDashboard: true; rebaseWhen: "behind-base-branch"; osvVulnerabilityAlerts: true; minimumReleaseAge: "5 days" + internalChecksFilter: "strict"; labels: ["dependencies"];
enabledManagers: [bun, npm, gomod, github-actions, custom.regex] (no nix —
compass has no root flake; see Managers); the 7 customManagers (catalog,
devenv-nixpkgs, bun/node/moon pins at tools/toolchain/versions/, go at
tools/toolchain/versions/go.nix, the Renovate self-pin regex on
.github/workflows/renovate.yml); the packageRules set from Approach
(TS rollup + TS<7 cap, Go rollup, GitHub Actions group, postgres-image
disable, gomod go-directive disable, @types/bun/bun-types soak
exemption, toolchain un-group, devenv solo branch + relock task, catalog
lockfile coupling (executionMode: "update"), forks/*/** fence); top-level
branch-mode postUpgradeTasks running the hash refresh with the three compass
pin-file fileFilters.
Interfaces:
- Produces:
tools/renovate/config.json5. - Consumes (paths referenced in config):
package.json(catalog block),devenv.lock,tools/toolchain/versions/{bun,node,moon,go}.nix,forks/*/**(fence),.github/workflows/*.yml(github-actions manager + the Renovate self-pin regex),go/go.mod(gomod;godirective disabled),bunfig.toml(soak-exemption parity, enforced by T5’s guard). - Commands declared (must match T2’s allowlist exactly):
bun tools/renovate/refresh-toolchain-hashes.ts,bun install --lockfile-only,bun tools/renovate/refresh-devenv-nixpkgs.ts.
T2 — Port the bot config: tools/renovate/bot-config.json5
Section titled “T2 — Port the bot config: tools/renovate/bot-config.json5”Per Approach §Bot config: configFileNames: ["tools/renovate/config.json5"],
repositories: ["RigelBuild/compass"], platform: "github", gitAuthor = the
App’s [bot] noreply identity (autodetected from the installation token;
pinned explicitly once T8 yields the App slug), onboarding: false,
requireConfig: "required", customEnvVariables: { HOME: "/tmp/renovate-home" },
allowedCommands = the three anchored regexes from T1.
Interfaces:
- Produces:
tools/renovate/bot-config.json5. - Consumed by: T6’s workflow (
RENOVATE_CONFIG_FILE=tools/renovate/bot-config.json5) and T5’s config.test.ts (allowlist ↔ postUpgradeTasks pinning).
T3 — Port refresh-toolchain-hashes.ts + test
Section titled “T3 — Port refresh-toolchain-hashes.ts + test”Port the internal monorepo’s refresh-toolchain-hashes.ts (+ .test.ts) to
tools/renovate/: path constants become BUN_NIX/NODE_NIX/MOON_NIX = "tools/toolchain/versions/{bun,node,moon}.nix"; DELETE the Rust FOD leg
entirely (TOOLCHAIN_TOML, MANIFEST_HASH_NIX, readChannel,
channelManifestUrl, renderManifestHashFile and their main() wiring + tests —
compass has no rust-toolchain.toml). Preserve: per-file self-gate against the
base branch, marker-anchored rewriteHash fail-loud contract, readVersion,
sriForUrl via nix store prefetch-file, idempotence.
Interfaces:
- Produces:
tools/renovate/refresh-toolchain-hashes.ts,tools/renovate/refresh-toolchain-hashes.test.ts. - Exports (consumed by its test):
BUN_NIX: string,NODE_NIX: string,MOON_NIX: string,rewriteHash(fileText, marker, newSri, file): string,readVersion(nixText, file): string. - Reads/writes at runtime:
tools/toolchain/versions/{bun,node,moon}.nix. - Requires on PATH:
nix(nix-command),bun,git(base-branch self-gate).
T4 — Port refresh-devenv-nixpkgs.ts + .core.ts + tests
Section titled “T4 — Port refresh-devenv-nixpkgs.ts + .core.ts + tests”Port the internal monorepo’s refresh-devenv-nixpkgs{.ts,.core.ts,.test.ts,.core.test.ts}
to tools/renovate/. Compass adaptation: rewrite ONLY the @biomejs/biome
catalog pin (compass’s catalog has no markdownlint-cli2 entry — package.json:12-22;
drop MARKDOWNLINT_CATALOG_KEY and its rewrite leg). Preserve: devenv.lock
self-gate, devenv update nixpkgs relock, baked-version eval from the inner
nixpkgs-src rev, bun install --lockfile-only re-resolve, fail-loud exit
contract.
Interfaces:
- Produces:
tools/renovate/refresh-devenv-nixpkgs.ts,tools/renovate/refresh-devenv-nixpkgs.core.ts, both test files. - Core exports:
BIOME_CATALOG_KEY: string,innerNixpkgsRev(lockJson: string): string,rewriteCatalogPin(packageJsonText, key, version): string. - Reads/writes at runtime:
devenv.lock,package.json,bun.lock. - Requires on PATH:
nix,devenv(the VENDORED fork CLI via T6’s shim — the script shellsdevenv update nixpkgsfrom PATH, as in the internal monorepo, and a nixpkgs devenv is rejected per Alternatives §D),bun,git; writable$HOME(bot config sets/tmp/renovate-home).
T5 — Port config.test.ts
Section titled “T5 — Port config.test.ts”Port the internal monorepo’s config.test.ts guards, adapted: (1) every
postUpgradeTasks command in config.json5 has an anchored allowedCommands entry
in bot-config.json5 and vice versa; (2) vulnerabilityAlerts.enabled is absent
and osvVulnerabilityAlerts is true; (3) real-manifest catalog extraction —
the regex recovers every pin the JSON parser sees in compass’s actual
package.json, plus the two truncation mutation tests; (4) solo-branch
grouping outcomes (toolchain pins and devenv-nixpkgs never land in the TS
rollup); (5) the TS <7 cap rule exists; (6) the forks/*/** fence rule
exists with enabled: false; (7) devenv-nixpkgs extraction pinned against the
real devenv.lock. Compass-new guards: (8) the workflow pins an exact
Renovate version — no bare bunx renovate; (9) extends includes
helpers:pinGitHubActionDigests; (10) the postgres-image disable rule exists
(matchDepNames: ["postgres"], enabled: false); (11) the gomod
go-directive disable rule exists; (12) the @types/bun/bun-types
minimumReleaseAge: null rule’s names equal bunfig.toml:20-24’s
minimumReleaseAgeExcludes minus @tanstack/virtual-core. Wire into a
tools/renovate workspace package (package.json + moon.yml + tsconfig per
sibling tools/* convention) AND register it in .moon/workspace.yml’s
project map — moon discovers projects only from that explicit map
(.moon/workspace.yml:10, and its own warning at :77-83: an unregistered
moon.yml “is silently inert … gates nothing while reading as covered”);
without the entry, none of guards (1)-(12) ever runs. Verify: moon query projects lists tools/renovate.
Interfaces:
- Produces:
tools/renovate/config.test.ts,tools/renovate/package.json,tools/renovate/moon.yml,tools/renovate/tsconfig.json. - Modifies:
.moon/workspace.yml(project-map entry fortools/renovate). - Consumes:
tools/renovate/config.json5,tools/renovate/bot-config.json5,package.json,devenv.lock,bunfig.toml,.github/workflows/renovate.yml(guards 8-9, 12).
T6 — Author the workflow: .github/workflows/renovate.yml (+ preflight port)
Section titled “T6 — Author the workflow: .github/workflows/renovate.yml (+ preflight port)”Per Approach §Runner shape. Jobs: one renovate job on ubuntu-latest (or the
repo’s standard runner label per ci.yml), steps:
actions/checkout@3d3c42e5…(SHA-pinned, same pin asci.yml:144) withfetch-depth: 0(the refresh scripts’ self-gates diff against the base branch).cachix/install-nix-action@630ae543…with the sameextra_nix_configblock asci.yml:165-168(nix-command flakes + the two substituters).- Toolchain on PATH via
nix build -f tools/toolchain/gate-tools.nix(theci.yml:182-212idiom) — provides bun/node/moon/go. - Vendored devenv on PATH:
nix build path:forks/devenv#devenv(from the repo root — the flake exports the CLI aspackages.<system>.devenv,forks/devenv/flake.nix:113-115;ci.yml:812writes itpath:../forks/devenv#devenvonly because that step’s cwd isagent-image/), then symlink$out/bin/devenvinto a directory prepended to$GITHUB_PATH. This is the ONLY devenv the job gets — compass CI never provisions one — and the relock script requires it on PATH (T4). - Assert devenv on PATH:
command -v devenvas its own fail-loud step, so a missing shim reds at setup instead of exit-127ing silently on the first channel-bump branch (a known regression class: on an image without devenv the relock “exits 127 (devenv: command not found) on every channel-bump branch, shipping a half-refreshed lock”, the internal monorepo’s regression evidence, RIG-2245). - Mint the App installation token:
actions/create-github-app-token@<pinned-sha> # vX(SHA-pin + version comment per the repo invariant) withclient-id: ${{ vars.RENOVATE_APP_CLIENT_ID }}andprivate-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}. Itstokenoutput feeds every later step — no long-lived PAT exists (see Approach §Secrets/Auth). - Port
tools/renovate-preflight/from the internal monorepo and run it withGH_TOKEN=${{ steps.<mint>.outputs.token }}andREPO: ${{ github.repository }}— the preflight readsREPOand exits fail-closed when missing (as in the internal monorepo’s preflight; GHA has noCI_REPO, so the workflow must set it or every run dies at preflight). Adapt the ported index.ts comment (REPO - owner/name (from github.repository)). Registertools/renovate-preflightin.moon/workspace.yml’s project map (same inertness trap as T5; verifymoon query projectslists it). env: RENOVATE_CONFIG_FILE: tools/renovate/bot-config.json5, RENOVATE_X_IGNORE_RE2: "true", RENOVATE_TOKEN: ${{ steps.<mint>.outputs.token }}→bunx renovate@44.33.1(exact pin per Approach; the self-pin customManager bumps it). Triggers:schedule: [{cron: "0 6 * * *"}](margin per the SEA-1220 constraint) +workflow_dispatch. Two GHA scheduled-workflow caveats, in a workflow comment: (a) cron is best-effort — starts are routinely 5-30+ minutes late, which the 06:00 UTC margin absorbs; (b) GHA auto-disables a scheduled workflow after 60 days without repo activity —workflow_dispatch(or any commit) revives it, but someone must notice; the dependency dashboard going stale is the tell. Everyuses:SHA-pinned with a# vXcomment (repo invariant,dependabot.yml:1-8).
Interfaces:
- Produces:
.github/workflows/renovate.yml,tools/renovate-preflight/*(ported:index.ts,preflight.ts,preflight.test.ts,package.json,moon.yml,tsconfig.json). - Modifies:
.moon/workspace.yml(project-map entry fortools/renovate-preflight). - Consumes:
tools/renovate/bot-config.json5(T2),secrets.RENOVATE_APP_PRIVATE_KEY+vars.RENOVATE_APP_CLIENT_ID(T8),tools/toolchain/gate-tools.nix,forks/devenv/(the vendored devenv flake).
T7 — Delete .github/dependabot.yml
Section titled “T7 — Delete .github/dependabot.yml”Remove the file. Its SHA-pin rationale header (lines 1-8) moves to a comment on
the github-actions section of tools/renovate/config.json5 so the invariant’s
rationale survives the migration. Same PR as T1-T6: no window with two version
managers, and — because T8’s App registration + secret/variable are
provisioned during PR review, before merge — a ~zero window with none
(merge ≈ first live-capable run; if provisioning lags, the gap is loud, not
silent: the token-mint step and preflight fail each scheduled run with a
named diagnosis).
Interfaces:
- Deletes:
.github/dependabot.yml. - Rationale text lands in:
tools/renovate/config.json5(github-actions manager comment).
T8 — Human action: register + install the Renovate GitHub App; disable Dependabot in Settings
Section titled “T8 — Human action: register + install the Renovate GitHub App; disable Dependabot in Settings”File a Linear issue assigned to Matt, label human-action, per
skill://human-action-handoff, when the design PR opens — the App can be
registered and its secret/variable created before the workflow that reads
them exists, so provisioning during PR review collapses the zero-manager
window to ~zero (no second PR, merge ≈ live). Runbook, copy-pasteable:
- Register a GitHub App under the RigelBuild org (name e.g.
rigel-renovate) with repository permissions: Contents (read/write), Pull requests (read/write), Workflows (read/write), Issues (read/write). Workflows is load-bearing: the github-actions manager pushes commits to.github/workflows/*, which a GitHub App may only do with the Workflows permission (GitHub Docs: “if your app specifically needs to access or edit Actions files in the .github/workflows directory, request the Workflows repository permission”); Issues because the dependency dashboard is an issue. - Generate a private key for the App; store it as the Actions repo secret
RENOVATE_APP_PRIVATE_KEYon RigelBuild/compass. Store the App’s client-id as the repo variableRENOVATE_APP_CLIENT_ID. - Install the App on RigelBuild/compass (single-repo installation — the blast radius stays compass-only).
- Disable Dependabot alerts + security updates in RigelBuild/compass repo
Settings — the
.github/dependabot.ymldeletion (T7) stops version updates only; the hidden-billed-features driver (Problem / Intent) is the Settings-side security feature, replaced byosvVulnerabilityAlerts: true. Without this step “Dependabot fully off” is not achieved. Steps 1-3 are blocking for the first live run (not the merge — the workflow fails loud at token-mint/preflight until provisioned); step 4 is non-blocking but required to close the migration’s driver. Once the App is registered, record the App slug and pingitAuthorintools/renovate/bot-config.json5to the App’s[bot]noreply identity (T2).
Interfaces:
- Produces: a Linear issue (
human-action); no repo files. - Unblocks: first successful run of
.github/workflows/renovate.yml.
T9 — Design-ledger delta
Section titled “T9 — Design-ledger delta”This record freezes with its ledger delta in the same PR. Compass’s ledger gate
is product-scoped (tools/design-ledger-gate/index.ts:45 PRODUCT_DIR = "docs/designs/product"; touchesRecord at :204-210 matches only that tree),
and docs/designs/platform/ has no DECISIONS.md — so the touch-coupling leg
does not fire for this record. Declare Ledger-impact: platform record; product ledger untouched in the design PR body (the gate accepts an explicit
declaration), or append to a platform DECISIONS.md if one exists by
implementation time. The IMPLEMENTATION PR body carries the same declaration if
it touches this record.
Interfaces:
- Produces:
Ledger-impact:line in the design PR body (and adocs/designs/platform/DECISIONS.mdrow iff that ledger exists by then). - Consumed by:
tools/design-ledger-gateon the PR event.
- T1 —
tools/renovate/config.json5(managers, packageRules, postUpgradeTasks) - T2 —
tools/renovate/bot-config.json5(slug, allowedCommands, HOME) - T3 —
tools/renovate/refresh-toolchain-hashes.ts+ test (compass paths, no Rust leg) - T4 —
tools/renovate/refresh-devenv-nixpkgs{.ts,.core.ts}+ tests (biome-only pin) - T5 —
tools/renovate/config.test.ts+ workspace package wiring +.moon/workspace.ymlregistration - T6 —
.github/workflows/renovate.yml(vendored-devenv shim, pinned renovate,REPOenv) +tools/renovate-preflight/port +.moon/workspace.ymlregistration - T7 — delete
.github/dependabot.yml(rationale moves to config comment) - T8 — human-action Linear issue (filed at PR-open): register/install the GitHub App,
RENOVATE_APP_PRIVATE_KEYsecret +RENOVATE_APP_CLIENT_IDvariable, Settings-side Dependabot disable - T9 — ledger delta /
Ledger-impact:declaration
Resolved decisions
Section titled “Resolved decisions”Formerly open questions; resolved in design critique or by Matt’s answers and folded into the record as decisions:
- Runner shape: B (was OQ1) — plain GHA job: checkout → install-nix-action
→ gate-tools toolchain → vendored-devenv shim → pinned
bunx renovate. A’s container lacks nix/devenv/bun for postUpgradeTasks; C requires a compass CI image publish pipeline that doesn’t exist; a nixpkgs devenv is rejected on fork-posture grounds (Alternatives §D). See Approach §Runner shape. - File location:
tools/renovate/(was OQ3) — compass has noci/tree; all first-party tooling istools/*, a bun workspace glob (package.json:10), so the scripts and tests get standard wiring;.github/would strand TypeScript outside the workspace. The preflight keeps the internal monorepo’s owntools/renovate-preflight/naming. The bot config’sconfigFileNamesmakes any choice mechanically workable — convention only. - Go source of truth:
go.nix(was OQ4) — compass derives thego_X_Y_Zattr at eval time (devenv.nix:30-31); there is no literal attr string for the internal monorepo’s devenv.nix regex to match, andgo.nixis the declared single source (go.nix:1-9). One regex manager ongo.nix;devenv.nixuntouched by Renovate; the gomodgo-directive update disabled (see packageRules). - Grouping: internal-monorepo parity (was OQ6) — TS rollup, Go rollup, GitHub Actions group; majors solo; toolchain pins solo. Same review granularity across the fleet; dependabot’s old single-group-per-ecosystem shape maps 1:1 (actions→“GitHub Actions”, bun→“TypeScript dependencies”, gomod→“Go dependencies”).
- Auth: GitHub App, per-run minted token (was OQ2; Matt 2026-08-21) —
chosen over widening a shared bot PAT or a compass-scoped PAT: zero
long-lived secret and single-repo blast radius. No shared cross-repo
credential lives on two CI secret surfaces; the only stored material is
compass’s own App private key, scoped to compass’s installation, and the
token Renovate receives is minted fresh each run (~1h expiry). Permission
set: Contents, Pull requests, Workflows, Issues — all read/write; Workflows
is load-bearing for the github-actions manager (see Approach §Secrets/Auth,
T8). gitAuthor is the App’s
[bot]noreply identity, autodetected from the installation token. This also bounds the self-pin exposure (Approach §Runner shape) — an ephemeral token instead of a stored PAT — without relaxing thebunx renovate@<pin>requirement. - Cadence: daily — top-level AND devenv-nixpkgs (was OQ5; Matt
2026-08-21) —
schedule:dailywith the0 6 * * *UTC cron, and the devenv-nixpkgs solo branch drops the internal monorepo’s weekly-Monday restriction to["before 4am"]daily (a deliberate divergence from the internal monorepo’s prior art).minimumReleaseAge: nullstays on that branch — a moving-branch digest never clears a release-age window.