Description
Migrate codexlb2otel’s task surface to just
1. Outcome
codexlb2otel has a single top-level justfile (no submodules — the repo is small enough that
mod/import add nothing). It defines the seven mandatory recipes plus repo-specific ones for the
corpus-drift tooling (probe, probe-sampled, baseline, probe-ci), the multi-binary build
(build), the live-run helper (run) and the archive sync helper (sync). just check is exactly
what ci.yml’s test job enforces — gofmt, vet, all seven CLI binaries build, the no-corpus test
suite, and the CI-flavoured drift probe. Makefile is gone. AGENTS.md, README.md and
backlog/config.yml reference just recipes, never make. ci.yml’s test job collapses to one
run: just check step behind a setup-just step. No shell scripts exist in this repo today (verified
via git ls-files), so there is nothing to absorb or keep beyond the Makefile itself.
2. The complete justfile
Drop this in at codexlb2otel/justfile, adjust nothing unless a later step in this doc says to.
set shell := ["bash", "-euo", "pipefail", "-c"]
# Go 1.27's json/v2-backed implementation regresses the multi-gigabyte corpus
# scans (see Makefile history / go.mod comment). Keep the established v1
# implementation until the archive decoder is migrated and measured against
# json/v2 explicitly. Exported so every recipe below inherits it.
export GOEXPERIMENT := env('GOEXPERIMENT', 'nojsonv2')
# Directory clbprobe/clbsync operate against. Override: `just probe corpus=other/dir`.
corpus := env('CORPUS', 'corpus/processed')
# show the task surface
default:
@just --list
# install go module dependencies into the local module cache
setup:
go mod download
# format all go source in place
[group('check')]
fmt:
gofmt -w .
# verify formatting (go source + this justfile); never mutates
[group('check')]
fmt-check:
test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
just --fmt --check
# static analysis (go vet; no golangci-lint config exists in this repo)
[group('check')]
[no-exit-message]
lint:
go vet ./...
# full go test suite (uses the local corpus if present); set filter="Name" for a subset
[group('check')]
[no-exit-message]
test filter="":
go test {{ if filter != "" { "-run " + filter } else { "" } }} ./...
# fast inner loop: same suite, corpus-backed tests forced to skip cleanly (CI's exact invocation)
[group('check')]
[no-exit-message]
test-short:
CLB_CORPUS=/nonexistent CLB_NO_CORPUS=1 go test ./...
# build all seven CLI tools into bin/ (gitignored)
[group('build')]
build:
mkdir -p bin
go build -o bin/codexlb2otel ./cmd/codexlb2otel
go build -o bin/clbsync ./cmd/clbsync
go build -o bin/clbfind ./cmd/clbfind
go build -o bin/clbsum ./cmd/clbsum
go build -o bin/clbprobe ./cmd/clbprobe
go build -o bin/clbprofile ./cmd/clbprofile
go build -o bin/clbstat ./cmd/clbstat
# remove bin/ (everything `setup` + `build` can reproduce)
[group('build')]
clean:
rm -rf bin
# THE GATE. Exactly what ci.yml's `test` job enforces.
[group('check')]
check: fmt-check lint build test-short probe-ci
# run codexlb2otel against a config file (long-running; set config=path to override)
[group('dev')]
run config="config.yaml":
mkdir -p bin
go build -o bin/codexlb2otel ./cmd/codexlb2otel
./bin/codexlb2otel -config {{ config }}
# pull new archives off the codex-lb host
[group('dev')]
sync:
mkdir -p bin
go build -o bin/clbsync ./cmd/clbsync
./bin/clbsync
# full drift check against corpus.sig.json (set corpus=path to override; exits 1 on breaking drift)
[group('check')]
probe:
mkdir -p bin
go build -o bin/clbprobe ./cmd/clbprobe
./bin/clbprobe {{ corpus }}
# sampled drift check (faster; cannot prove a shape is absent)
[group('check')]
probe-sampled:
mkdir -p bin
go build -o bin/clbprobe ./cmd/clbprobe
./bin/clbprobe -sampled {{ corpus }}
# accept the current corpus shape as the baseline (always run from a FULL scan)
[group('gen')]
[confirm('This overwrites corpus.sig.json from a full scan of the local corpus. Continue?')]
baseline:
mkdir -p bin
go build -o bin/clbprobe ./cmd/clbprobe
./bin/clbprobe -update corpus.sig.json {{ corpus }}
# CI's exact drift-probe invocation: builds clbprobe, scans corpus/, and treats
# "nothing to scan" (exit 3) as a documented pass rather than a failure. There
# is never a corpus in CI (it's gitignored, personal data) so this always
# exercises the exit-3 branch there; it exercises the real scan on a machine
# that does have corpus/ populated.
[group('check')]
[script('bash')]
probe-ci:
set -euo pipefail
go build -o /tmp/clbprobe ./cmd/clbprobe
set +e
/tmp/clbprobe -fail-on breaking corpus
status=$?
set -e
case "$status" in
0) echo "clbprobe: clean, no drift against corpus.sig.json" ;;
3) echo "clbprobe: nothing to scan - skipped" ;;
1) echo "clbprobe: drift at or above 'breaking' against corpus.sig.json"; exit 1 ;;
*) echo "clbprobe: exit $status (see output above)"; exit 1 ;;
esac
Notes on choices baked into that file (do not re-litigate without a new fact):
- No
typecheckrecipe. Go’s compiler is the type checker;buildalready exercises it across everycmd/*package. Adding a separate no-optypecheckwould violate the “no-op recipes only when the vocabulary slot is genuinely absent AND still worth documenting” spirit for no benefit —buildalready covers it and is incheck. - No
gen/gen-check.corpus.sig.jsonis a committed generated artifact in spirit, but regenerating it (baseline) requires the local, gitignored, personal-data corpus — it can never run in CI, so agen-checkdrift gate is structurally impossible here.baseline(repo-specific name,group('gen')) stays a manual,[confirm]-gated developer action instead. check’s test step istest-short, nottest. The oldmake checkran plaingo test ./...(full suite, corpus-dependent if a corpus happens to be present locally) while CI always forces the no-corpus path — seeci.yml’s own comment: “mirrors the Makefile’stest-shorttarget”. That asymmetry pre-exists this migration. Because the fleet contract requirescheckto equal CI exactly,checkhere depends ontest-short.test(the mandatory recipe) keeps the Makefile’s original full-suite contract for a developer who has synced the corpus locally — it is intentionally not wired intocheck. Preserve this distinction; don’t silently swapcheckto depend ontest.check’s build step isbuild(bin/ per tool), not a barego build ./.... Building the seven named binaries compiles every buildable package the module needs; functionally equivalent to CI’s plaingo build ./...step, and it’s the recipe developers actually run.
3. Makefile disposition
Makefile — absorb in full, then git rm Makefile.
| Make target | Replacement | Notes |
|---|---|---|
build (+ bin/% pattern rule) |
just build |
Pattern rule → explicit line per binary (7 fixed tools, enumerated in §11 of the fleet standard’s translation table: “no equivalent; write explicit rules”). Drops the GO_FILES-based smart-rebuild dependency tracking — go build’s own cache makes that redundant; the Makefile comment says as much (“go build caches, so the over-broad dependency costs nothing”). |
run (CONFIG ?= config.yaml) |
just run / just run config=other.yaml |
CONFIG ?= → a config="" recipe parameter with a default, per §12 of the standard. |
clean |
just clean |
Unchanged behavior. |
sync |
just sync |
Unchanged behavior. |
probe (CORPUS ?= corpus/processed) |
just probe / just probe corpus=other/dir |
CORPUS ?= → corpus := env('CORPUS', 'corpus/processed') plus a recipe parameter default of the same name, per §12. |
probe-sampled |
just probe-sampled |
Unchanged behavior. |
baseline |
just baseline |
Now [confirm]-gated (§5.4 of the standard: this is a deliberate, hard-to-undo overwrite of a committed baseline file — the Makefile’s own comment already calls it “a deliberate act”). |
test |
just test |
Unchanged behavior (full suite, optional filter= param added per the mandatory test contract). |
test-short |
just test-short |
Unchanged behavior; also now the thing just check actually runs (see justfile notes above). |
check (gofmt -l . ; go vet ./... ; go test ./...) |
just check |
Behavior changed deliberately: now runs fmt-check (which actually fails on unformatted files — the old gofmt -l . alone did not, see traps), lint, build, test-short (not test — see above), and probe-ci, to match what CI enforces exactly. |
GOEXPERIMENT ?= nojsonv2 (top-level export) |
export GOEXPERIMENT := env('GOEXPERIMENT', 'nojsonv2') in the justfile |
Applies to every recipe automatically, same as the Makefile’s global export. |
Instruction: git rm Makefile once the justfile is proven locally (§8, step order) and CI is green
on it.
4. Script disposition
git ls-files | grep -E '\.(sh|bash|zsh|ps1)$' returns nothing. There are no shell scripts tracked in
this repo. Nothing to ABSORB, nothing to KEEP. (There is also no scripts/ directory and no
non-trivial helper program in another language used as a dev/CI task — cmd/* are the CLI tools
themselves, not task scripts, and stay exactly as they are, invoked via the justfile recipes above.)
5. CI changes
.github/workflows/ci.yml
Current test job runs five separate run: steps (gofmt, go vet, go build, go test, clbprobe) after
checkout + setup-go. Replace all five with a setup-just step and one run: just check.
Remove the job-level env block:
env:
# Go 1.27's json/v2-backed implementation regresses multi-gigabyte archive
# scans; retain the established v1 behavior until that migration is measured.
GOEXPERIMENT: nojsonv2
— it’s redundant now: the justfile owns and exports GOEXPERIMENT itself (env('GOEXPERIMENT', 'nojsonv2')), so the workflow no longer needs to set it. (If you’d rather keep it as an explicit
belt-and-braces override, that’s harmless too — the justfile’s env() call still respects it. Default
recommendation: remove it, one source of truth.)
Steps section becomes:
steps:
- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: setup go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: go.sum
- name: setup just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0
with:
just-version: '1.58.0'
- name: just check
run: just check
Delete the five old run: steps (gofmt, go vet, go build, go test (non-corpus), clbprobe drift check) entirely — just check now runs all of it (fmt-check, lint, build, test-short,
probe-ci, in that dependency order).
Do NOT touch:
name: CI, theon:triggers,permissions: { contents: read }, theconcurrency:block — unchanged.timeout-minutes: 15— unchanged.- The job is named
test; there is no separateci-successaggregator job in this workflow today (single-job workflow) — nothing to preserve there beyond leaving the job name and structure alone. actions/checkoutandactions/setup-gopins — unchanged, not part of this migration.
.github/workflows/publish.yml
Out of scope entirely. No build/test/lint/format/generate/validate run: logic lives here — it
delegates to rknightion/.github/.github/workflows/container-publish.yml via uses: (a reusable
workflow call) and has one small inline step (read the go directive from go.mod, a two-line awk
that isn’t a build/test/lint task). Per the fleet standard: never convert a uses: into run: just,
and this repo’s own comment block already documents why this file is architecturally frozen (the
brewmdm/ARC blocker). Do not touch it.
.github/workflows/release-please.yml
Out of scope. GitHub-native release-please orchestration plus a uses: call into publish.yml.
No shell logic to migrate.
.github/workflows/trigger-docs-sync.yml
Out of scope. A broker-token mint (uses:) plus a repository_dispatch action (uses:). No
shell logic to migrate.
.github/workflows/scheduled-archive-probe.yml
Out of scope for CI wiring, but note it for later: this workflow is explicitly documented in its
own header comment as NOT YET LIVE (no self-hosted runner registered, placeholder archive path). It
contains the same clbprobe build+run+case-statement pattern as ci.yml’s old drift-check step, just
against a live archive with -sampled instead of a full committed corpus. When this workflow is
eventually activated, point it at the justfile too — the recipe body would be
clbprobe -sampled -fail-on breaking "$ARCHIVE_DIR" instead of probe-ci’s hardcoded corpus. Do
not build that recipe now — it targets an unresolved runner label and an unresolved archive path;
inventing a recipe for infrastructure that doesn’t exist yet is scope creep. Leave this workflow
exactly as-is; this is a note for whoever activates it later, not an action item for this task.
6. Docs and agent-contract changes
AGENTS.md (imported by CLAUDE.md via @AGENTS.md — edit only AGENTS.md)
Replace the “## The gate” section:
## The gate
```bash
make check # gofmt -l . ; go vet ./... ; go test ./...
go build ./...
make test-short is the fast inner loop and skips the corpus tests. A green test-short is not a
green gate.
with:
```markdown
## Task interface
This repo's task surface is a `justfile`. Discover it, don't guess it:
just --list # human-readable
just --dump --dump-format json # machine-readable
just --show <recipe> # what a recipe actually runs
- `just check` is the full gate and is exactly what CI enforces. It must pass before you commit.
- Prefer `just <recipe>` over the underlying tool. If you are typing `go test`, you want `just test`.
- Run `just` with stdin from /dev/null. `just baseline` is `[confirm]`-gated — it overwrites the
committed `corpus.sig.json`. Stop and ask before running it; never pass `--yes` or `JUST_YES=1`.
- If a task you need does not exist, add a recipe with a `#` doc comment and a `[group(...)]` rather
than running a bare command.
- `just test` is the full local suite (uses the corpus if you've synced one). `just check` runs
`just test-short` instead, matching CI exactly — CI never has a corpus (it's gitignored, personal
data). A green `just test-short` (or `just check`) is not proof the corpus-backed tests pass; that
only happens locally with `just test` against a synced corpus.
(This preserves the original “green test-short is not a green gate” warning, restated for just
naming, and folds in the fleet-standard §9 boilerplate.)
Do not paste the recipe list itself into AGENTS.md — same rot risk the standard warns about.
CLAUDE.md
No change — it’s a two-line pointer (@AGENTS.md) with no make or script references.
README.md
Replace lines 67, 74–82 (the make-based usage block):
Current (verbatim, includes a real discrepancy against the actual Makefile — probe is the full scan
and probe-sampled is the fast one, but this README text has the fast/full description backwards
relative to target names; see trap below):
make build && ./bin/clbfind resp_052b6a... # faster to re-run; bin/ is gitignored
...
| `make build` | build every tool into `bin/` |
| `make sync` | pull new archives off the codex-lb host |
| `make probe` | fast drift check against the baseline (exit 1 on anything new) |
| `make probe-full` | exhaustive drift check |
| `make baseline` | accept the current shape as the baseline (always from a full scan) |
| `make check` | gofmt + vet + tests |
| `make test-short` | tests without the corpus, the fast inner loop |
`make probe CORPUS=some/other/dir` overrides the directory.
Replace with (corrected to match actual behavior — probe is the full scan, probe-sampled is the
fast one):
just build && ./bin/clbfind resp_052b6a... # faster to re-run; bin/ is gitignored
...
| `just build` | build every tool into `bin/` |
| `just sync` | pull new archives off the codex-lb host |
| `just probe` | full drift check against the baseline (exit 1 on breaking drift) |
| `just probe-sampled` | faster sampled drift check; cannot prove a shape is absent |
| `just baseline` | accept the current shape as the baseline (always from a full scan; asks to confirm) |
| `just check` | the full gate: fmt-check, lint, build, test-short, probe-ci |
| `just test` | full test suite (uses the local corpus if synced) |
| `just test-short` | tests without the corpus, the fast inner loop |
`just probe corpus=some/other/dir` overrides the directory.
Search the rest of README.md for any other make occurrence before finalizing this edit — only
the block above was found by grep -n "make ", but re-grep at implementation time in case the file
changed since this analysis.
7. backlog/config.yml
Current:
definition_of_done: ["make check passes: gofmt -l . reports nothing, go vet ./... clean, go test ./... green", "go build ./... succeeds"]
New:
definition_of_done: ["just check passes: fmt-check, lint, build, test-short and probe-ci all clean"]
Edit this file by hand — backlog/config.yml’s list-valued keys cannot be set through backlog config set (per this repo’s own AGENTS.md, which explicitly carves this file out as the one deliberate
hand-edit exception). Do not run this through the backlog CLI.
8. Order of work
- Add
justfileat repo root (content in §2 above). Do not touch anything else yet. - Run
just --fmt --check— fix formatting if it fails, or runjust --fmtonce to auto-format, then re-check. - Prove every recipe locally:
just setup,just fmt-check,just lint,just build,just test-short,just probe-ci, thenjust checkend to end. Fix anything that doesn’t match the old Makefile/CI behavior before moving on. - Update
.github/workflows/ci.ymlper §5. Push and confirm thetestjob is green on the real runner (not just localjust check— CI’s Go version, cache behavior, and the always-exit-3probe-cibranch all need to be observed passing in the real environment). - Update
AGENTS.md,README.md,backlog/config.ymlper §6–7. - Only once CI is green on the justfile-based workflow and nothing else references
makeorMakefile(re-grep the whole repo:grep -rn "make " --include="*.md" --include="*.yml" --include="*.yaml" .andgrep -rn "Makefile" .), deleteMakefile:git rm Makefile. - Final full-repo grep for stray
makereferences (docs, comments, workflow files) before closing the task.
Justfile first and proven locally, CI switched second, deletion last — never delete Makefile before
CI is confirmed green on just check.
9. Traps specific to this repo
gofmt -l .exits 0 even when it lists files. The oldmake checktarget rangofmt -l .directly and would report unformatted files without ever failing the target — onlyci.yml’s inlinerun:block (which captures the output and checks it’s non-empty) actually enforced formatting.fmt-checkin the new justfile replicates the CI logic (test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }), not the old Makefile’s silently-toothless version. Don’t “simplify”fmt-checkback to baregofmt -l .— that reintroduces a check that can never fail.go runcollapses clbprobe’s exit codes.ci.yml’s own inline comment measured this directly:go run ./cmd/clbprobeturns every non-zero exit into a flat1, which would make the exit-3 (“nothing to scan”, the expected CI outcome) indistinguishable from exit-1 (“breaking drift, fail the build”) and turn a normal green CI run red on every single push.probe-ciandprobe/probe-sampledall build the binary first (go build -o ... ./cmd/clbprobe) and run the binary directly — nevergo run. Do not “simplify” any of these togo run.probe-cineeds a persistent shell for itscasestatement — that’s exactly why it’s a[script('bash')]recipe rather than plainjustlines; a line-based recipe would hit “extra leading whitespace” on the multi-linecase.check’s test dependency istest-short, nottest— this is deliberate, not a bug. See the justfile notes in §2 and the Makefile disposition table in §3. If a future editor “fixes”checkto depend ontestinstead because it looks more consistent with the mandatory-vocabulary naming, they will silently makejust checkdepend on the local corpus being present — breaking it in CI (no corpus ever exists there) and making local runs non-reproducible depending on whether a dev happens to have synced.- README’s
make probe/make probe-fulltable entries do not match the Makefile’s real target names or behavior (probe-fulldoesn’t exist;probeis already the full scan, not the fast one;probe-sampledis the fast one). §6 above rewrites the table to match actual behavior while renaming tojust. This is a genuine pre-existing doc bug, not introduced by the migration — fix it while you’re in the file rather than porting the error forward under a new name. GOEXPERIMENTmust reach every recipe that touches the corpus (build,run,sync,probe,probe-sampled,baseline,probe-ci,test,test-short) — the top-levelexport GOEXPERIMENT := env(...)line in the justfile handles this automatically for the whole file; don’t re-add it per-recipe.- No golangci-lint config exists in this repo (
.golangci.ymlabsent, confirmed byfind). Thelintrecipe isgo vet ./...only — do not invent a golangci-lint invocation that isn’t backed by a real config file; that would fail on a tool that was never installed or configured here. corpus/processedand the corpus data itself are gitignored, personal-data archives — never create, commit, or reference a stub/fixture corpus directory as part of this migration. Theprobe/probe-sampled/baseline/testrecipes only do anything meaningful on a machine that has runjust sync(or equivalent) against a real archive; that’s expected and matches current behavior, not a regression to fix.corpus.sig.jsonis a large (83KB) committed filebaselineoverwrites in place. The[confirm]gate onbaselineis there specifically because this is easy to run by accident and hard to notice went wrong (a sampled or partial corpus baked in as if it were confirmed-complete). Do not remove the[confirm]attribute.
10. Out of scope
- Every KEEP script — there are none; this repo has zero tracked shell scripts.
.github/workflows/publish.yml— reusable-workflow delegation torknightion/.github/.github/workflows/container-publish.yml; per fleet standard §8, never convert auses:intorun: just. Leave entirely as-is, including itspermissions:blocks and the go-version resolution step (not build/test/lint logic)..github/workflows/release-please.yml— GitHub-native release-please orchestration. Leave as-is..github/workflows/trigger-docs-sync.yml— broker-token mint + repository_dispatch. Leave as-is..github/workflows/scheduled-archive-probe.yml— not yet live (documented placeholder runner label and archive path in its own header comment). Leave entirely as-is; do not wire it tojustin this task — see §5’s note on it.Dockerfile/docker-compose.yml— no build/test logic lives here worth migrating; the Dockerfile’sGO_VERSIONbuild-arg is supplied bypublish.yml’sgo-versionjob, untouched by this migration.dashboards/,docs/,docs.toml— static content and Zensical config;docs.tomlhas no build/generate logic (the m7kni.io hub does the generation), nothing to migrate.corpus/,corpus.sig.json,shapes.json,testdata/,archive/— data, not task surface.release-please-config.json,.release-please-manifest.json— release-please’s own config; unrelated tojust.- CodeQL, zizmor, actionlint, scorecard, dependency-review, container-publish workflows — none of
these exist as separate workflow files in this repo today (only the five listed in §5 exist); if any
are added later they follow the same “GitHub-native, never folded into
just” rule from the fleet standard, not this task.
Acceptance Criteria
- #1 Top-level justfile defines all seven mandatory recipes (default, setup, fmt, fmt-check, lint, test, check) plus build, clean, run, sync, probe, probe-sampled, baseline, probe-ci, each with a # doc comment and a [group(…)]
- #2 just check passes locally and is exactly the sequence ci.yml’s test job enforces: fmt-check, lint, build, test-short, probe-ci
- #3 just –fmt –check passes on the justfile
- #4 just –list shows every public recipe with its doc comment and group; default and setup are ungrouped
- #5 Makefile is deleted via git rm only after ci.yml is confirmed green on the justfile-based workflow
- #6 No shell scripts needed absorption (git ls-files has none); confirmed none were left un-migrated
- #7 ci.yml’s test job calls a pinned setup-just step then a single run: just check step, replacing the five separate gofmt/vet/build/test/clbprobe run steps, with permissions, concurrency and the job name unchanged
- #8 publish.yml, release-please.yml, trigger-docs-sync.yml and scheduled-archive-probe.yml are untouched
- #9 AGENTS.md’s gate section and README.md’s usage table reference just recipes instead of make targets, with the pre-existing probe/probe-sampled description swapped to match real behavior
- #10 backlog/config.yml’s definition_of_done names just check instead of make check
Definition of Done
- #1 make check passes: gofmt -l . reports nothing, go vet ./… clean, go test ./… green
- #2 go build ./… succeeds
Implementation Plan
- Reconcile the frozen migration brief with the current codexlb2otel CI and the ratified 2otel alignment: retain the established shared-workflow pins and ci-success aggregator while mapping the required Go gates to documented just recipes.
- Add the top-level justfile and schema-v2 golangci-lint configuration, with gosec enabled within golangci-lint; add the Renovate manager/annotation only where the relocated govulncheck pin is actually managed.
- Convert ci.yml to the canonical GitHub-hosted 2otel job surface using setup-just and just recipes, preserving triggers, permissions, concurrency, out-of-scope workflows, and ci-success needs.
- Update task-interface documentation and Backlog definition of done; prove formatting, task-surface discovery, targeted recipes, and the complete local check.
- Stage named paths, run CodeRabbit before each code commit, push the justfile/CI transition, observe exact-head CI, then remove Makefile only after that evidence; run final checks, reference sweeps, task finalization, and push.
Implementation Notes
Implemented the justfile, schema-v2 golangci-lint gate with gosec, the pinned Renovate custom manager, canonical 2otel CI jobs, docs, and migration-related operational references. Observed passing: just –fmt –check; just –list; just –dump –dump-format json; just setup; just lint; just build; just test-short; just vuln; just snapshot; just image; and actionlint for ci.yml.
Full local probe-ci read 49 local archive files (4.8 GB) and correctly failed on pre-existing drift: one breaking safety-buffering bool-to-object shape change plus ten new paths/values. The baseline and decoder are out of this migration scope; CXO-0007 is still To Do but covers only older non-breaking baseline findings. No baseline was accepted or changed. Exact-head CI must still prove its intentionally archive-less exit-3 branch.
PARKED: CodeRabbit review is mandatory before commit, but two authenticated reviews were rate-limited because the organization has exhausted included reviews and has no assigned seat. Resume only once CodeRabbit can return a review; rerun it against the staged diff before any commit. Full local just check also remains blocked by genuine corpus drift (one breaking safety-buffering shape change and ten new findings). Do not accept a baseline or alter decoder behavior in this task; route that contract work separately.
Unparked and completed 2026-08-29. CodeRabbit found a misplaced gosec nosec directive and a missing goreleaser install-only, both fixed in a follow-up. The corpus drift against corpus.sig.json is deliberately NOT baselined and remains its own concern. Migration at 4b1cecd; exact-head CI green.
Comments
author: campaign-ordering created: 2026-08-29 09:18
Fleet ordering — WAVE 2. Starts after the Wave 0 pilot (sf2loki / SFL-0073) and the Wave 1 hubs land.
Within Wave 2 the order is free — these repos do not depend on each other. Batching by language is worthwhile so one lane reuses its Makefile-to-recipe mapping across similar repos.
Do not start before the pilot reports. The standard may be amended off the back of it, and picking this up early risks coding against a superseded seam.
Provisioning just in CI. Which mechanism depends on the runner, and the two must not be mixed:
| Runner | Mechanism |
|---|---|
arc-arm64 (m7kni self-hosted) |
just is baked into the runner image by m7kni/ci-tools (runner-image/Dockerfile, ARG JUST_VERSION). Do not add extractions/setup-just, and delete the step if this repo already has one — it installs a second just earlier on PATH and turns the image pin into a lie. |
GitHub-hosted (all rknightion repos) |
extractions/setup-just, SHA-pinned, with an explicit just-version:. |
Both sides currently sit on 1.58.0 and are Renovate-managed. ci-tools’ Tool version drift workflow fails if the Dockerfile ARG and the published image ever disagree, and lists any repo still carrying a second pin.
While you are in the workflow files, check the hub pin. On 2026-08-29 Renovate was unfrozen for rknightion/.github in m7kni/renovate-config — it had been enabled: false on the mistaken belief that callers tracked @main, which froze the fleet across 19 different hub SHAs (v1.3.1 June → v1.9.7 August) so that no hub fix ever propagated. Bumps now arrive as one grouped, CI-gated, automerged PR per repo. A uses: whose comment is not a real # vX.Y.Z still cannot be bumped (it resolves to a digest-only update, which the fleet rules disable) — if you find one, repair the comment as part of this task.
author: campaign-ordering created: 2026-08-29 10:42
Standard amendment — ci is the sanctioned superset of check (RATIFIED)
This supersedes the frozen wording “check is the complete local gate and reproduces every CI job that can run off a GitHub runner”, which several lanes could not honour without making the pre-commit gate depend on a Docker daemon.
The definitions now are:
check— everything that runs with only the language toolchain installed. This is the pre-commit gate. A leg that runs on a bare toolchain belongs here however long it takes.ci—checkplus the legs CI gates that need a Docker daemon, a service container, or cross-compilation, and nothing else. Written asci: check <heavy legs>.
Every leg you put in ci must carry a comment naming which of those three it needs. That comment is the guard: without it ci becomes the bin for anything slow or awkward, check quietly stops meaning much, and the fleet is back to a per-repo gate.
Eleven of the 42 lanes arrived at this shape independently before it was ratified, which is why it won.
If this repo has no such legs, it has no ci recipe at all and check is the whole gate. Do not add an empty one.
author: campaign-ordering created: 2026-08-29 10:57
Fleet alignment — the 2otel family converges on one CI shape
These seven Go repos are near-identical applications and had drifted into two naming dialects and materially different coverage. The migration rewrites every run: block anyway, so converge them in the same change rather than preserving the drift in new clothes.
Canonical job names — used by tailscale2otel, graph2otel, polylens2otel and rfc6035-2otel, so this is the majority convention, not an invention:
build-test · lint · govulncheck · goreleaser-snapshot · docker-build · coverage · ci-success
opnsense2otel and transceiver-exporter currently use a second dialect — tests, race, docker-build-verify. Rename to the canonical set as part of this task.
ci-success is the only check the branch ruleset gates, so jobs can be renamed or merged freely provided ci-success’s needs: list is updated in the same commit. Never rename ci-success itself.
Required gates, and where each lives after the migration:
| Gate | Recipe | Note |
|---|---|---|
build + test + -race |
just test |
-race belongs in the standard test run |
| golangci-lint | just lint |
needs a .golangci.yml, schema v2 |
| gosec | just lint |
a golangci-lint linter, NOT a separate job — enable it in .golangci.yml. Four of the seven already do it this way; a standalone gosec job would be a third dialect |
| govulncheck | just vuln |
pinned golang.org/x/vuln/cmd/govulncheck@v1.3.0, matching the family |
| goreleaser snapshot | just snapshot |
cross-compile ⇒ belongs in ci, not check |
| container build | just image |
needs a Docker daemon ⇒ belongs in ci, not check |
Already done for you (2026-08-29): govulncheck was added to opnsense2otel, transceiver-exporter and codexlb2otel ahead of the migration, because those three had no dependency vulnerability scanning at all. Convert those jobs to just vuln like any other; do not re-add them.
Still missing, fix as part of this task:
opnsense2otel— has.golangci.ymlbutgosecis not enabled in it.transceiver-exporter— no.golangci.ymlat all, and no-racein its test job.codexlb2otel— no.golangci.yml, no-race, no container build, and noci-successjob and no branch ruleset, so nothing gates its CI. Adding an aggregator is the right fix but is a separate decision; raise it rather than assuming.
One known trap: the govulncheck@v1.3.0 pins are invisible to Renovate — go install pkg@version inside a run: block matches no manager. All five are four minor versions behind (current is v1.7.0). Once the version moves into the justfile as a # renovate:-annotated := assignment, it becomes managed. That is a real benefit of this migration, not incidental.
author: campaign-ordering created: 2026-08-29 11:20
Correction — moving a pin into the justfile does NOT make it Renovate-managed
The 2otel alignment comment above ends with a claim that needs narrowing. It says the govulncheck@v1.3.0 pins become managed “once the version moves into the justfile as a # renovate:-annotated := assignment”. The conditional in that sentence is doing real work, and the first completed migration did not satisfy it.
Verified on tailscale2otel at origin/main after TSO-0025 closed:
justfile:21—govulncheck_version := "v1.3.0", with no# renovate:annotation above it.renovate.json— no justfile matcher at all: nocustomManagersentry, nothing pointing at/^justfile$/.justfile:17— a comment stating the pin tracks thego installline in the CI jobs, so the workflow is still the source of truth.
The version relocated and nothing else changed. It is exactly as invisible to Renovate as it was in the run: block, and still four minors behind (v1.3.0 against v1.7.0).
Two things are required, and neither is implied by “move the pin into the justfile”:
- The
:=assignment carries a# renovate: datasource=… depName=…annotation directly above it. renovate.jsonpoints a custom manager at the justfile — thecustomManagers:dockerfileVersionspreset does not cover justfiles; that one only matches Dockerfiles and Containerfiles.
Treat “the pin is now managed” as false unless you have done both and checked. Do not record it as a benefit of this migration in a final summary without verifying renovate.json yourself.