Task · TXE-0009

Migrate the repo task surface to just and retire Makefiles and ad-hoc scripts

Description

Migrate task surface to just

1. Outcome

transceiver-exporter gains a top-level justfile implementing the fleet-mandatory recipe vocabulary (default, setup, fmt, fmt-check, lint, test, check) plus build and run. just --list is the one true answer to “what can I do in this repo”. CI’s tests job in .github/workflows/ci.yml calls just check instead of inlining go test -v ./... and the golangci-lint-action. AGENTS.md’s “The gate” section and backlog/config.yml’s definition_of_done both name just recipes instead of raw go/golangci-lint invocations.

This repo has no Makefile and no tracked shell/helper scripts (verified: find . -iname Makefile -o -iname GNUmakefile and git ls-files | grep -E '\.(sh|bash|zsh|ps1)$' both return empty, excluding vendor/). There is nothing to delete and nothing to classify ABSORB/KEEP. This is a pure “author the justfile from scratch and wire it into CI/docs” task — do not go looking for a Makefile or scripts that do not exist.

2. The complete justfile

Create justfile at repo root with exactly this content (adjust only if go.mod’s Go version or golangci-lint’s pinned version has since changed):

set shell := ["bash", "-euo", "pipefail", "-c"]

# show the task surface
default:
    @just --list

# install toolchain + deps into the repo-local environment
[group('dev')]
setup:
    go mod download

# format source in place
[group('check')]
fmt:
    gofmt -l -s -w .
    just --fmt

# verify formatting without mutating
[group('check')]
fmt-check:
    @test -z "$(gofmt -l -s .)" || (gofmt -l -s . && echo 'gofmt: files need formatting, run `just fmt`' >&2 && exit 1)
    just --fmt --check

# static analysis
[group('check')]
[no-exit-message]
lint:
    go vet ./...
    golangci-lint run

# run the full test suite (race + verbose); optional substring filter
[group('check')]
[no-exit-message]
test filter="":
    #!/usr/bin/env bash
    set -euo pipefail
    if [ -n "{{filter}}" ]; then
        go test -race -v -run "{{filter}}" ./...
    else
        go test -race -v ./...
    fi

# build the binary into bin/
[group('build')]
build:
    go build -trimpath -o bin/transceiver-exporter .

# run the exporter locally (needs CAP_NET_ADMIN to read real EEPROM data)
[group('dev')]
run *args:
    go run . {{args}}

# the full PR gate — exactly what CI enforces
[group('check')]
check: fmt-check lint test

Notes for the implementing agent:

3. Makefile disposition

None. No Makefile or GNUmakefile exists anywhere in this repo (verified, excluding vendor/). No git rm step needed for this section.

4. Script disposition

None. git ls-files | grep -E '\.(sh|bash|zsh|ps1)$' returns empty. No scripts/ directory exists. No ABSORB or KEEP classification needed.

5. CI changes

.github/workflows/ci.yml

Only the tests job changes. docker-build-verify, coverage, and ci-success are untouched — do not touch the needs: [tests, docker-build-verify] list on ci-success, permissions:, concurrency:, persist-credentials: false, or any SHA-pinned uses:.

Before (tests job steps, lines 19–35 of the current file):

      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
        with:
          go-version-file: go.mod

      - name: Run linters
        uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
        with:
          version: latest
          args: --verbose

      - name: Run tests
        run: go test -v ./...

After:

      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
        with:
          go-version-file: go.mod

      - name: Install golangci-lint
        uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
        with:
          version: latest
          install-mode: binary

      - uses: extractions/setup-just@<pin-current-sha-here> # v4
        with:
          just-version: '1.58.0'

      - name: Run gate
        run: just check

Do not delete the golangci-lint-action step outright — it is the fleet’s existing mechanism for pinning/caching the golangci-lint binary in CI. Switch it to install-mode: binary (installs the tool onto PATH without running the lint itself) so just check’s lint recipe can then invoke the already-installed golangci-lint run. Resolve extractions/setup-just’s pinned SHA at implementation time (gh api repos/extractions/setup-just/git/refs/tags/v4 --jq .object.sha or check what SHA other already-migrated fleet repos are using) and add the matching # v4 comment per the fleet’s SHA-pin convention.

Everything else in ci.ymldocker-build-verify, coverage, ci-success — is unchanged.

Other workflow files — no changes

actionlint.yml, arm-automerge.yml, auto-rc.yml, codeql.yml, dependency-review.yml, docker-security.yml, ghcr-cleanup.yml, publish.yml, release-please.yml, scorecard.yml, trigger-docs-sync.yml, zizmor.yml — none contain build/test/lint/format/generate run: bodies that belong in just. Verified: grep -rn "make \|\.sh" .github/workflows/ returns empty across the whole directory. Do not touch these files.

6. Docs and agent-contract changes

AGENTS.md — “The gate” section

Current (lines 9–20 approx):

## The gate

\`\`\`bash
go build ./...
go vet ./...
go test -race ./...
golangci-lint run
\`\`\`

These are the four items every new task inherits as its definition of done. CI additionally
verifies the Docker build; `coverage` is deliberately not a required check.

Replace with:

## The gate

This repo's task surface is a `justfile`. Discover it, don't guess it:

\`\`\`bash
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. This repo has no `[confirm]` recipes today, but if one is
  added later, 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.

CI additionally verifies the Docker build; `coverage` is deliberately not a required check.

Do not paste the actual recipe list (build, test, lint, etc.) into AGENTS.md — only the contract paragraph above, per §9 of the fleet standard. CLAUDE.md needs no edit — it’s a one-line @AGENTS.md import and picks the change up automatically.

CONTRIBUTING.md

grep -n "make \|\.sh" CONTRIBUTING.md returns only “Make your change, adding or updating tests where it makes sense.” (line 33) — plain English “make”, not a make command reference. No edit needed there. Re-check at implementation time in case this has drifted.

README.md

No make or script-path references found (grep -n "make\|\.sh\b" README.md returned empty). No edit needed. Re-check at implementation time.

7. backlog/config.yml

Current:

definition_of_done:
  - "go build ./..."
  - "go vet ./..."
  - "go test -race ./..."
  - "golangci-lint run"

New:

definition_of_done:
  - "just check"

This file is the one Backlog.md markdown-adjacent file the fleet standard permits hand-editing (list-valued keys can’t be set through backlog config set) — see AGENTS.md’s own note on this. Edit it directly with a text editor, not the backlog CLI.

8. Order of work

  1. Add justfile at repo root (§2). Run just --fmt --check, then just check locally end to end. Fix anything that doesn’t pass before touching CI.
  2. Update .github/workflows/ci.yml’s tests job (§5). Push and confirm the tests job goes green and ci-success still gates on the same two job names.
  3. Update AGENTS.md’s “The gate” section (§6).
  4. Update backlog/config.yml’s definition_of_done (§7).
  5. There is nothing to delete (no Makefile, no scripts) — skip any “delete last” step.

9. Traps specific to this repo

10. Out of scope

Acceptance Criteria

Definition of Done

Implementation Plan

  1. Inventory the current task surface, documented references, hooks, and existing 2otel CI legs.
  2. Create the justfile and small adjacent config needed for the ratified check/ci split: mandatory recipes, Go race testing, gosec linting, vulnerability, snapshot, and image recipes.
  3. Rewrite the CI workflow into the binding canonical 2otel job shape while preserving its coverage and shared-workflow behavior; update task-interface documentation and Definition of Done.
  4. Sweep retired names, run isolated local validation and review, commit/push the named files, prove CI at the final SHA, then finalize only criteria backed by evidence.

Implementation Notes

Implemented the justfile migration and canonical 2otel CI shape: race-enabled build/test, golangci-lint v2 with gosec, vulnerability scan, GoReleaser snapshot, and Docker image recipes. Added bounded HTTP server setup after gosec exposed the prior unbounded listener; focused timeout test added.

Validation passed: just –fmt –check; just –dump –dump-format json; just check (fmt, vet, golangci-lint v2.13.2 plus gosec, race tests, govulncheck); filtered test recipe; GoReleaser snapshot; Docker image build; actionlint; zizmor on ci.yml. Snapshot initially showed GoReleaser v2.16.0 source incompatible with Go 1.27; v2.18.0 probe and snapshot passed, so the Renovate-managed justfile pin uses v2.18.0.

Repository-wide zizmor remains unproven because unchanged auto-rc.yml has an existing dangerous workflow_run trigger finding; ci.yml alone has no findings. CodeRabbit review was attempted before commit but the organisation review service returned a five-minute rate limit before producing findings; no commit or push has occurred.

Parked before any source commit or push because CodeRabbit returned an organisation-wide review rate limit before producing a completed review. The staged migration patch remains in this checkout and has passed its local validations.

Resume boundary: after the review quota is available, run coderabbit review --agent against the staged patch; address any findings; rerun the affected checks plus the final just check; commit the named source files; push main; verify CI by that commit SHA and then finalize the acceptance criteria. Do not treat the earlier rate-limit response as a completed review.

The uncommitted source patch has stable patch identity f9ffbfe7d91bc246f993ab3568abb7d6647c9cb0. Preserve it in this checkout while the CodeRabbit gate is unavailable; if it must be transferred, compare that patch identity before applying.

Correction after parking: the source patch was deliberately unstaged so only the tracker state could be committed. It remains intact in the working tree, not in the index; its full stable patch identity remains f9ffbfe7d91bc246f993ab3568abb7d6647c9cb0.

Unparked and completed 2026-08-29. CodeRabbit returned three findings, all fixed: a README prerequisite, a missing just fmt-check in CI, and an unset ReadTimeout on the metrics server pinned by a new test. Migration at 9b95a91; 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-toolsTool 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:43

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:

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:

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:

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”:

  1. The := assignment carries a # renovate: datasource=… depName=… annotation directly above it.
  2. renovate.json points a custom manager at the justfile — the customManagers:dockerfileVersions preset 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.

Credit: caught by the tailscale2otel lane on its closeout, against the claim as originally written here.

View the source file on GitHub