Task · GRT-0003

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

Description

Migrate grotTrack’s task surface to just

1. Outcome

grotTrack has no Makefile today, so this migration is scoped to: introduce a top-level justfile covering both the Swift/macOS app and the Chrome extension (grot-track-extension/); absorb scripts/generate-icons.mjs’s invocation and scripts/update-appcast.sh’s invocation into recipes (both scripts themselves are KEEP — real program / control-flow script — only their call-sites change); rewrite the relevant run: steps in .github/workflows/build.yml and .github/workflows/release.yml to call just <recipe>; and update AGENTS.md, README.md, and backlog/config.yml to reference just instead of raw xcodegen/xcodebuild/swiftlint/npm invocations. When done, just --list is the single answer to “what can I run in this repo”, and just check is exactly what CI enforces on every PR.

No Makefile exists anywhere in this repo (verified: find . -iname Makefile -o -iname GNUmakefile returns nothing outside node_modules), so there is no Makefile disposition table and no git rm of a Makefile in this task.

2. The complete justfile

Create justfile at the repo root:

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

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

# install toolchain + project dependencies (idempotent)
setup:
    brew install xcodegen swiftlint
    npm ci
    cd grot-track-extension && npm ci
    just xcodeproj

# regenerate the local Xcode project from project.yml (gitignored, not committed)
[group('dev')]
[macos]
xcodeproj:
    xcodegen generate

# open the generated Xcode project (long-running once the app is launched)
[group('dev')]
[macos]
run: xcodeproj
    open GrotTrack.xcodeproj

# run the Chrome extension dev server with hot reload (long-running)
[group('dev')]
extension-dev:
    cd grot-track-extension && npx wxt

# auto-fix swiftlint violations in place and format this justfile
[group('check')]
[macos]
fmt:
    swiftlint --fix --quiet
    just --fmt

# verify formatting without mutating (swiftlint has no separate check mode; lint covers style)
[group('check')]
[no-exit-message]
fmt-check:
    just --fmt --check

# run swiftlint static analysis on the Swift sources
[group('check')]
[macos]
[no-exit-message]
lint:
    swiftlint lint --strict

# type-check the Chrome extension (generates WXT types first)
[group('check')]
[no-exit-message]
typecheck:
    cd grot-track-extension && npx wxt prepare && npx tsc --noEmit

# run the Swift test suite (optional `filter` narrows via -only-testing)
[group('check')]
[macos]
[no-exit-message]
test filter="": xcodeproj
    #!/usr/bin/env bash
    set -euo pipefail
    if [ -n "{{filter}}" ]; then
      xcodebuild test -project GrotTrack.xcodeproj -scheme GrotTrackTests \
        -destination 'platform=macOS' -only-testing "{{filter}}" \
        CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=NO
    else
      xcodebuild test -project GrotTrack.xcodeproj -scheme GrotTrackTests \
        -destination 'platform=macOS' \
        CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=NO
    fi

# regenerate committed icon assets (Chrome extension + macOS AppIcon) from assets/icon.svg
[group('gen')]
gen:
    npm ci
    node scripts/generate-icons.mjs

# regenerate icons and fail if the tree goes dirty (drift gate)
[group('gen')]
[no-exit-message]
gen-check: gen
    git diff --exit-code -- grot-track-extension/public GrotTrack/Assets.xcassets/AppIcon.appiconset

# the full local gate — exactly what CI's build-extension job + release.yml's test-gate enforce
[group('check')]
check: fmt-check lint typecheck gen-check test

# CI-only superset of check: split build-for-testing + coverage-instrumented run (build.yml build-swift job)
[group('check')]
[macos]
ci: xcodeproj lint
    xcodebuild build-for-testing -project GrotTrack.xcodeproj -scheme GrotTrackTests \
      -destination 'platform=macOS' -derivedDataPath ./build \
      CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=NO
    xcodebuild test-without-building -project GrotTrack.xcodeproj -scheme GrotTrackTests \
      -destination 'platform=macOS' -derivedDataPath ./build \
      -resultBundlePath TestResults.xcresult -enableCodeCoverage YES \
      CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=NO

# build the unsigned macOS app for local testing
[group('build')]
[macos]
build: xcodeproj
    xcodebuild build -project GrotTrack.xcodeproj -scheme GrotTrack \
      -destination 'platform=macOS' CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=NO

# build the Chrome extension for production (MV3, output in .output/chrome-mv3/)
[group('build')]
build-extension: gen
    cd grot-track-extension && npm ci && npx wxt build

# package the Chrome extension as a distributable zip (for release / Chrome Web Store upload)
[group('build')]
extension-zip: gen
    cd grot-track-extension && npm ci && npx wxt zip

# update appcast.xml with a new Sparkle release entry — run from CI only, expects _site/appcast.xml
[group('release')]
[working-directory('_site')]
appcast version sig length:
    ../scripts/update-appcast.sh {{version}} '{{sig}}' {{length}}

3. Makefile disposition

Not applicable. No Makefile / GNUmakefile exists anywhere in this repo. Skip this step entirely — there is nothing to git rm.

4. Script disposition

Script Disposition Replacement Why
scripts/update-appcast.sh KEEP just appcast <version> <sig> <length> (§2, [working-directory('_site')]) Non-trivial control flow — mktemp, awk with a getline loop, an if/else creating vs. patching appcast.xml. Per §6 this is “anything with non-trivial control flow” and stays a file; the recipe is the entry point.
scripts/generate-icons.mjs KEEP just gen runs node scripts/generate-icons.mjs (§2) A real Node program (uses sharp to rasterize SVG → 13 PNG sizes across two output directories) — a generator, not a task sequencer. Per §6, real programs of substance stay files.

Both scripts are already invoked only as node scripts/generate-icons.mjs / ./scripts/update-appcast.sh <args> from CI and package.json’s generate-icons npm script — nothing here has meaningfully complex CLI wrapping to strip out; the change is purely at the call-sites (§5, §6).

5. CI changes

.github/workflows/build.yml

build-swift job — add a setup-just step right after checkout:

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

Then:

build-extension job — add the same setup-just step after checkout. Then:

ci-success job — unchanged. needs: [build-swift, build-extension] and the job name stay exactly as-is.

.github/workflows/release.yml

test-gate job — add setup-just after checkout. Then:

build-release job — add setup-just after checkout. Then:

update-appcast job — add setup-just after checkout (runs on macos-latest). Then replace “Generate appcast entry”:

Current:

      - name: Generate appcast entry
        run: |
          VERSION="${NEEDS_RELEASE_PLEASE_OUTPUTS_TAG_NAME}"
          VERSION="${VERSION#v}"  # strip leading 'v'
          mkdir -p _site
          curl -fsSL "https://rknightion.github.io/grotTrack/appcast.xml" -o _site/appcast.xml 2>/dev/null || true
          cd _site
          ../scripts/update-appcast.sh \
            "$VERSION" \
            "${STEPS_SIGN_OUTPUTS_SIGNATURE}" \
            "${STEPS_SIGN_OUTPUTS_LENGTH}"
        env:
          NEEDS_RELEASE_PLEASE_OUTPUTS_TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
          STEPS_SIGN_OUTPUTS_SIGNATURE: ${{ steps.sign.outputs.signature }}
          STEPS_SIGN_OUTPUTS_LENGTH: ${{ steps.sign.outputs.length }}

Becomes:

      - name: Generate appcast entry
        run: |
          VERSION="${NEEDS_RELEASE_PLEASE_OUTPUTS_TAG_NAME}"
          VERSION="${VERSION#v}"  # strip leading 'v'
          mkdir -p _site
          curl -fsSL "https://rknightion.github.io/grotTrack/appcast.xml" -o _site/appcast.xml 2>/dev/null || true
          just appcast "$VERSION" "${STEPS_SIGN_OUTPUTS_SIGNATURE}" "${STEPS_SIGN_OUTPUTS_LENGTH}"
        env:
          NEEDS_RELEASE_PLEASE_OUTPUTS_TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
          STEPS_SIGN_OUTPUTS_SIGNATURE: ${{ steps.sign.outputs.signature }}
          STEPS_SIGN_OUTPUTS_LENGTH: ${{ steps.sign.outputs.length }}

(just appcast uses [working-directory('_site')], so _site must exist and hold appcast.xml before the call — the mkdir -p _site and curl lines stay exactly where they are, before the just appcast line.)

publish-extension job — add setup-just after checkout. Then replace “Install icon dependencies” + “Generate icons” + the npm ci && npx wxt zip lines inside “Build extension zip” with:

      - name: Build extension zip
        working-directory: grot-track-extension
        run: cd .. && just extension-zip

(extension-zip is defined at repo root and expects to run from there; since the step already sets working-directory: grot-track-extension, either drop that working-directory: and run just extension-zip directly from the job’s default root, or keep the cd .. shown above. Prefer dropping working-directory: grot-track-extension entirely and using run: just extension-zip — simpler, no cd.)

Workflows explicitly NOT touched

actionlint.yml, zizmor.yml, codeql.yml, dependency-review.yml, scorecard.yml, arm-automerge.yml, notarize-log.yml, trigger-docs-sync.yml — all either call a rknightion/.github reusable workflow (uses:) or are GitHub-native/dispatch-only. Do not add setup-just or touch a single line in these eight files.

6. Docs and agent-contract changes

AGENTS.md

Replace the entire “Build & Development” section (currently: xcodegen generate, an unsigned xcodebuild build block, swiftlint lint, a full-suite xcodebuild test block, a single-test xcodebuild test -only-testing block, and the Chrome-extension npm ci && npx wxt prepare && npx tsc --noEmit && npx wxt build block) with:

## 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 local gate — `fmt-check`, `lint`, `typecheck`, `gen-check`, `test` — and
  is a subset of what CI enforces (CI additionally runs `just ci`'s coverage-instrumented build in
  the macOS job). It must pass before you commit.
- Prefer `just <recipe>` over the underlying tool. If you are typing `xcodebuild` or `swiftlint`, you
  want `just build` / `just test` / `just lint`.
- `just setup` installs the toolchain (XcodeGen, SwiftLint, npm deps for both the root icon generator
  and `grot-track-extension/`) and regenerates the Xcode project. Idempotent — safe to re-run.
- Run `just` with stdin from /dev/null. No recipe in this repo is currently `[confirm]`-gated, 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 `xcodebuild`/`swiftlint`/`npm` command.

Do not paste the recipe list itself into AGENTS.md — it rots.

README.md

No other files reference make or a script path directly (CONTRIBUTING.md does not exist; docs.toml and docs/ contain no build instructions).

7. backlog/config.yml

Current definition_of_done:

definition_of_done:
  - "xcodebuild build -project GrotTrack.xcodeproj -scheme GrotTrack -destination 'platform=macOS' CODE_SIGN_IDENTITY=\"-\" CODE_SIGNING_ALLOWED=NO"
  - "xcodebuild test -project GrotTrack.xcodeproj -scheme GrotTrackTests -destination 'platform=macOS' CODE_SIGN_IDENTITY=\"-\" CODE_SIGNING_ALLOWED=NO"
  - "swiftlint lint"
  - "xcodegen generate (run before the first build, and again after any project.yml change; GrotTrack.xcodeproj is generated and gitignored — never commit it)"
  - "cd grot-track-extension && npx wxt prepare && npx tsc --noEmit (only if the extension changed)"

New:

definition_of_done:
  - "just build"
  - "just test"
  - "just lint"
  - "just xcodeproj (run before the first build, and again after any project.yml change; GrotTrack.xcodeproj is generated and gitignored — never commit it)"
  - "just typecheck (only if the extension changed)"

Edit this file by hand — backlog/config.yml is the documented exception to the “never hand-edit tracker markdown” rule (list-valued keys can’t be set through backlog config set).

8. Order of work

  1. Add justfile at repo root (§2). Do not touch CI or docs yet.
  2. Locally (macOS): just setup, then just check, then just ci, then just build, just build-extension, just extension-zip, just appcast <fake-version> <fake-sig> <fake-len> against a hand-created _site/appcast.xml — prove every recipe runs clean before touching CI.
  3. Run just --fmt --check and fix until clean.
  4. Update .github/workflows/build.yml (§5) on a branch/PR-style diff (even though this repo pushes straight to main — verify the workflow YAML is valid with actionlint/zizmor still passing, since both run on every push).
  5. Update .github/workflows/release.yml (§5). This path only executes on a real release-please release — cannot be fully exercised pre-merge; review the diff very carefully against the current file (reproduced in full above) since a mistake here is a broken release, not a broken PR check.
  6. Update AGENTS.md (§6).
  7. Update README.md (§6).
  8. Hand-edit backlog/config.yml’s definition_of_done (§7).
  9. Run just check one final time, then push. Watch the next build.yml run on main (build-swift + build-extension + ci-success) to confirm the migrated CI steps actually pass — this is the first real exercise of the [macos] ci recipe and the extension job’s collapsed steps.
  10. No deletions in this repo (no Makefile, no absorbed scripts to remove — both scripts are KEEP).

9. Traps specific to this repo

  1. Two-ecosystem repo, two CI runners. build-swift runs on macos-26; build-extension runs on ubuntu-latest. Every [macos]-tagged recipe (xcodeproj, run, fmt, lint, test, ci, build) will hard-fail with error: recipe ... requires ... os ... if invoked from the Linux-runner job — do not add setup-just + a [macos] recipe call to build-extension.
  2. .xcodeproj is gitignoredxcodeproj (the xcodegen generate wrapper) is deliberately NOT wired into gen/gen-check. gen/gen-check only cover the committed icon PNGs. Do not merge these two concepts even though both start with “regenerate a generated file”.
  3. swiftlint lint goes from advisory to blocking. Today’s CI has continue-on-error: true on the Lint step, so a swiftlint failure has never blocked a merge. Folding lint into check/ci removes that safety valve, per the standard’s “check must be complete” rule. Run just lint against the current tree BEFORE merging this migration — if it’s currently red, either fix the violations first or explicitly decide (and note in the PR) to keep continue-on-error: true a little longer, which would then mean check is knowingly ahead of CI rather than matching it.
  4. just appcast cannot run standalone from a clean checkout. It needs _site/appcast.xml to already exist (created by the mkdir -p _site + curl lines that remain directly in the workflow, immediately before the just appcast call). Don’t try to fold those two lines into the recipe itself — the recipe’s [working-directory('_site')] attribute requires the directory to already exist when just starts, or every recipe in the file fails to parse the [working-directory] target.
  5. EdDSA signature quoting. sig in the appcast recipe is base64 (+, /, = characters) — it is single-quoted in the recipe body ('{{sig}}') per the fleet-standard interpolation gotcha (§10 of the standard). Do not remove the quotes even though base64 rarely contains shell metacharacters — GitHub’s own token/signature values have occasionally broken unquoted recipe interpolation elsewhere in the fleet.
  6. gen-check regenerates real PNGs on every just check. This is per-contract (§1 of the standard: gen-check belongs inside check wherever gen exists) but means just check now shells out to npm ci + sharp + rewrites 13 PNG files + does a git diff --exit-code every single run. This is slower than the old swiftlint lint + xcodebuild test gate. If this becomes a real friction point, that’s a fleet-standard question (whether gen-check belongs in check vs. only in ci) — raise it, don’t silently drop gen-check from check unilaterally.
  7. gen-check’s diff scope must cover both icon output directoriesgrot-track-extension/public/*.png (Chrome icons) AND GrotTrack/Assets.xcassets/AppIcon.appiconset/*.png (macOS icons). generate-icons.mjs writes both from the same SVG in one invocation; scoping the git diff --exit-code to only one directory silently misses drift in the other.
  8. The Coverage Summary step in build.yml is untouched and depends on just ci’s exact -resultBundlePath TestResults.xcresult flag. If ci’s xcodebuild invocation is ever refactored, that path must stay TestResults.xcresult at the repo root or the (untouched) python3 coverage-parsing step silently reports N/A.
  9. Signing/notarizing/archiving stay raw CI script. build.yml’s “Archive & sign” and release.yml’s “Build Release Archive” / “Re-sign Sparkle framework binaries” / “Notarize App” steps need Apple secrets (APPLE_CERTIFICATE_BASE64, APPLE_TEAM_ID, APPLE_ID, NOTARY_PASSWORD) that don’t exist on a developer machine — deliberately not migrated into just recipes. Don’t “complete” this migration by wrapping them; they’re CI-only per §6 of the standard.
  10. Root package.json vs. extension package.json are two separate dependency sets — root has only sharp (for generate-icons.mjs); grot-track-extension/package.json has wxt, typescript, @types/chrome. just setup and just gen run npm ci at the root; extension recipes cd grot-track-extension && npm ci separately. Don’t merge these into one npm ci call.

10. Out of scope

Acceptance Criteria

Definition of Done

Implementation Plan

  1. Inventory the live task surface, workflow runners, shared-workflow calls, hook configuration, and all direct task/script references.
  2. Add a standards-compliant top-level justfile; validate every local, non-secret recipe and fix migration-exposed lint/config defects within scope.
  3. Route build/test/lint/generate CI through pinned just recipes while preserving shared reusable calls, signatures, job names, and secret-only release steps.
  4. Replace stale developer-facing command references with the task interface, run focused local and workflow gates, then review the named staged diff.
  5. Commit and push named paths to main; obtain a green repository CI run at the final SHA; finalize this task through the Backlog CLI.

Implementation Notes

Decision: the task originally required a ci recipe, but the ratified fleet amendment and its binding task comment prohibit ci without Docker, service-container, or cross-compilation work. This repository has none. Coverage remains in test/check so the build workflow retains TestResults.xcresult; the obsolete ci acceptance criterion was replaced through the CLI.

Repaired the strict-lint refactor by separating export models/support and session helpers; strict lint is clean and the focused LLM export suite passed (8 tests). Added the standards-compliant just task surface, routed eligible workflow steps through it, and updated developer documentation and definition of done. Broader build, extension, appcast, workflow, and final review gates remain.

Parked: the local implementation and workflow gates are complete, but the mandatory CodeRabbit review could not begin because its plan rate limit is exhausted and no on-demand review is available. Resume by retrying coderabbit review –agent –base main after review capacity is restored; fix any material findings, then commit the staged migration, push to main, wait for a successful CI run at that SHA, and finalize this task.

Unparked and completed 2026-08-29. CodeRabbit’s four doc findings were all fixed. A follow-up gated check on [macos]: it depends on [macos] recipes, and just validates the whole file at parse time, so every Linux invocation failed until then. Migration at 0ecbcd8; 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: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:

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-execution created: 2026-08-29 13:58

Ratified standard applied: the stale ci requirement was removed from acceptance criteria. Coverage instrumentation stays in test/check; no ci recipe will be added because no permitted heavy leg exists.

View the source file on GitHub