Task · GCI-0021

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

Description

Migrate grafana-cloud-org-insights to just

Consistent with the frozen fleet standard (JUST-FLEET-STANDARD.md). Do not re-litigate anything marked FROZEN there; this task instantiates it for this repo.

1. Outcome

A top-level justfile is the repo’s task surface. just --list shows every dev/CI task. just check is the exact local equivalent of what .github/workflows/ci.yml enforces (pytest, the stdlib-only dependency-file guard, the customer-identifier scan, the em-dash scan, and OpenTofu validate/fmt on both terraform/ and terraform/examples/standalone/). There is no Makefile to delete - this repo never had one. The five real shell scripts under bin/ all stay as files (each is a KEEP under §6 - argument parsing, loops, or a deployment-scoped audit) but are only ever invoked through a just recipe from here on. ci.yml’s three jobs (tests, identifiers, terraform) each call just instead of inlining shell. AGENTS.md gets the standard Task interface section. backlog/config.yml’s definition_of_done names just recipes instead of raw commands.

This repo is Python, stdlib-only by deliberate design (Dockerfile:5-8, enforced by ci.yml’s “Refuse a dependency file” step) - there is no pyproject.toml, no lockfile, no linter, no formatter, no type checker anywhere in the repo. pytest==9.1.1 is CI/dev-only tooling, pinned by version string in ci.yml:30, never a committed dependency file. The justfile below reflects that reality: lint and fmt/fmt-check exist (mandatory vocabulary) but lint is the dependency-file guard (there is no other static analysis in this repo) and fmt only formats the justfile itself. typecheck, build, gen/gen-check, docs are deliberately omitted - the repo has none of them.

2. The complete justfile

Create justfile at the repo root:

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

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

# create a repo-local virtualenv and install the pinned pytest test runner (idempotent)
setup:
    python3 -m venv .venv
    .venv/bin/pip install --disable-pip-version-check --quiet pytest==9.1.1

# format the justfile itself (this repo has no Python formatter - it ships zero third-party deps)
[group('check')]
fmt:
    just --fmt

# verify the justfile is formatted
[group('check')]
fmt-check:
    just --fmt --check

# refuse a stray Python dependency file - this collector is stdlib-only by design
[group('check')]
[script('bash')]
lint:
    for f in requirements.txt requirements-dev.txt pyproject.toml Pipfile poetry.lock; do
      if [ -e "$f" ]; then
        echo "error: $f exists - this project ships a stdlib-only collector; the container image" >&2
        echo "error: installs nothing. Adding a dependency needs a Dockerfile change and a review." >&2
        exit 1
      fi
    done
    echo "no dependency files present"

# run the pytest suite (offline by construction - no AWS, no network, no credentials)
[group('check')]
[no-exit-message]
test filter="":
    .venv/bin/python3 -m pytest tests -q {{ if filter == "" { "" } else { "-k " + quote(filter) } }}

# validate the reusable terraform module and the standalone example, and check formatting
[group('infra')]
[no-exit-message]
tf-validate:
    cd terraform && tofu init -backend=false && tofu validate
    cd terraform/examples/standalone && tofu init -backend=false && tofu validate
    tofu fmt -check -recursive terraform

# scan tracked files (and, with --history, all reachable git history) for leaked customer identifiers
# requires GCINSIGHT_CUSTOMER_IDENTIFIER_PATTERN in the environment (a repository secret in CI)
[group('check')]
check-identifiers:
    bin/check-customer-identifiers --history

# refuse em dashes in shipped text - house style is a spaced hyphen
[group('check')]
[script('bash')]
no-em-dashes:
    set -uo pipefail
    hits=$(grep -rIn $'—' . \
      --exclude-dir=.git --exclude-dir=backlog --exclude-dir=testdata \
      --exclude-dir=__pycache__ --exclude-dir=.terraform || true)
    if [ -n "$hits" ]; then
      echo "em dashes present, use a spaced hyphen:"
      echo "$hits"
      exit 1
    fi
    echo "clean"

# THE GATE - exactly what CI enforces
[group('check')]
check: fmt-check lint test tf-validate check-identifiers no-em-dashes

# audit (or, with --fix, repair) the one Cost Explorer allocation tag on a live deployment
# needs NAME_PREFIX and GCINSIGHT_S3_BUCKET in the environment - see bin/check-tags.sh header
[group('infra')]
check-tags *args:
    bin/check-tags.sh {{ args }}

# build the collector image locally without pushing (parity check)
[group('build')]
image:
    bin/build-and-push.sh --no-push

# build and push the collector image to ECR as an immutable sha-<commit> tag
# needs GCINSIGHT_ECR_REPOSITORY (or pass --repo via args); see bin/build-and-push.sh header for flags
[group('release')]
[confirm('push a new collector image to the configured ECR repository?')]
publish-image *args:
    bin/build-and-push.sh {{ args }}

# build and verify a local immutable consumer candidate (never pushes)
[group('build')]
consumer-build manifest deployment_root terraform:
    bin/consumer-build --manifest {{ quote(manifest) }} --deployment-root {{ quote(deployment_root) }} --terraform {{ quote(terraform) }}

# execute a tool under one validated non-secret consumer projection
[group('dev')]
consumer-exec manifest deployment_root terraform kind *args:
    bin/consumer-exec --manifest {{ quote(manifest) }} --deployment-root {{ quote(deployment_root) }} --terraform {{ quote(terraform) }} --kind {{ quote(kind) }} -- {{ args }}

# remove the local virtualenv (setup can always recreate it)
[group('dev')]
clean:
    rm -rf .venv

Notes on choices baked into the file above (do not change without a new fact):

3. Makefile disposition

None. find . -not -path '*/vendor/*' ... -iname Makefile -o -iname GNUmakefile returns nothing. No Makefile step is needed - do not create one to satisfy a template; this repo never had one.

4. Script disposition

Script Disposition Recipe Why
bin/build-and-push.sh (136 lines) KEEP image (no-push variant), publish-image ([confirm]) Argument parsing (--repo, --no-push, --allow-dirty, --publish-latest), conditional git-dirty checks, set -euo pipefail control flow. A real deployment tool, not a thin wrapper.
bin/check-tags.sh (200 lines) KEEP check-tags Loops over AWS resources (ECS cluster, 4 task-definition tiers, S3, Secrets Manager, Firehose), conditional --fix mode, requires deployment-identifying env vars with no defaults (NAME_PREFIX, GCINSIGHT_S3_BUCKET). Audits one live deployment; cannot be inlined into a generic recipe.
bin/check-customer-identifiers (101 lines) KEEP check-identifiers Argument parsing (--history, --patterns-file), a while read loop over git rev-list --all with per-commit git grep/git show/git ls-tree calls. Real control flow.
bin/consumer-build (68 lines) KEEP consumer-build Argument parsing over 5 flags (--manifest, --deployment-root, --terraform, --tag, --platform) with a docker build/verify sequence.
bin/consumer-exec (37 lines) KEEP consumer-exec Argument parsing including a -- passthrough separator for an arbitrary wrapped command.

No script in this repo qualifies for ABSORB. There is no thin sequencer script (a plan.sh/init.sh/ setup.sh that only chains a couple of commands) anywhere in bin/ - every shell script here does real argument parsing, loops, or both.

bin/*.py files (alerts.py, cost_model.py, dashboards.py, provision.py, trace.py, consumer_manifest.py, probe_*.py, make_local_views.py, make_compose_fixture.py) are real Python programs invoked directly (python3 bin/foo.py ... / ./bin/foo.py ...), documented that way in README.md and RUNBOOK.md with their own multi-flag CLIs. They are out of scope for this migration (§6’s KEEP category, “real programs”) - do not wrap them in just recipes; that would either duplicate their --help output in the justfile or force a narrowed shape onto tools with larger CLIs than the mandatory/optional vocabulary anticipates. scan.py at the repo root is the same category.

5. CI changes

.github/workflows/ci.yml

Add a setup-just step to each job that will call just (tests, identifiers, terraform). Pin by SHA matching this repo’s existing convention (see actions/checkout and actions/setup-python pins already in this file for the exact style):

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

tests job - replace the “Refuse a dependency file”, “Install test runner”, and “Tests” steps:

  tests:
    name: pytest
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: '3.14'
      - uses: extractions/setup-just@<pin> # v4
        with:
          just-version: '1.58.0'
      - name: Lint (refuse a dependency file)
        run: just lint
      - name: Set up test runner
        run: just setup
      - name: Tests
        # Offline by construction - no AWS, no network, no credentials. If this needs either, the
        # fixture wiring has regressed and the suite has stopped being reproducible.
        run: just test

identifiers job - replace “Scan” and “No em dashes in shipped text”:

  identifiers:
    name: no leaked identifiers
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - uses: extractions/setup-just@<pin> # v4
        with:
          just-version: '1.58.0'
      - name: Scan
        env:
          GCINSIGHT_CUSTOMER_IDENTIFIER_PATTERN: ${{ secrets.CUSTOMER_IDENTIFIER_PATTERN }}
        run: just check-identifiers
      - name: No em dashes in shipped text
        run: just no-em-dashes

terraform job - keep the opentofu/setup-opentofu step (the recipe still needs tofu on PATH; just orchestrates, it does not install tools), replace the three run: bodies:

  terraform:
    name: tofu validate
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
      - uses: extractions/setup-just@<pin> # v4
        with:
          just-version: '1.58.0'
      - name: Validate and check formatting
        run: just tf-validate

ci-success job: do not touch. Same needs: [tests, identifiers, terraform], same job name, same if: always() logic.

Other workflow files - do NOT touch

actionlint.yml, zizmor.yml, docker-security.yml, publish.yml, ghcr-cleanup.yml, arm-automerge.yml, auto-rc.yml, trigger-docs-sync.yml, release-please.yml, codeql.yml, dependency-review.yml, scorecard.yml contain no build/test/lint run: logic to migrate (verified: grep -n 'run:' across all of them returns nothing except publish.yml’s one-line git rev-parse HEAD output-capture and ghcr-cleanup.yml’s dry-run input plumbing - neither is task logic). These are GitHub-native workflows (release automation, security scanning, dependency review, cleanup) per §8 - leave every one exactly as-is.

6. Docs and agent-contract changes

7. backlog/config.yml

Current definition_of_done:

definition_of_done:
  - "python3 -m pytest tests -q"
  - "tofu fmt -check -recursive terraform; tofu init -backend=false and tofu validate pass for terraform/ and terraform/examples/standalone/"
  - "customer-identifier and shipped-text gates from .github/workflows/ci.yml return clean"

Replace with:

definition_of_done:
  - "just test"
  - "just tf-validate"
  - "just check-identifiers and just no-em-dashes both return clean"

Edit this file only through the backlog CLI’s config path if one exists for this field; if the CLI has no config-editing subcommand for definition_of_done, this is one of the rare fields edited by hand in backlog/config.yml itself (it is project configuration, not a tracked task/doc record) - confirm the CLI’s own docs before hand-editing, since [[operating-model]]’s “never hand-edit a tracker’s markdown” rule targets tasks/docs, not this settings file.

8. Order of work

  1. Add justfile at the repo root (§2), exactly as specified.
  2. Add .venv/ to .gitignore (currently absent - setup will create .venv/ and it must not be tracked or picked up by the customer-identifier / em-dash scans).
  3. Run just --fmt --check locally to confirm the file as authored is already formatted (or run just fmt once and commit the result).
  4. Run just setup && just check locally end-to-end. Fix anything that does not reproduce the current CI behavior exactly before touching CI.
  5. Update .github/workflows/ci.yml per §5. Push and confirm the tests, identifiers, terraform, and ci-success jobs all go green on a PR/branch before merging - do not edit CI and delete nothing else in the same step.
  6. Update AGENTS.md (§6) and backlog/config.yml (§7).
  7. Only once CI is green on the new recipes: there is nothing to delete (no Makefile, no ABSORB scripts). This migration adds a justfile and repoints CI/docs/config at it; it does not remove any existing file. Confirm this explicitly in the PR description so a reviewer does not go looking for a deletion step that does not exist.

9. Traps specific to this repo

10. Out of scope

Acceptance Criteria

Definition of Done

Implementation Plan

  1. Add the prescribed top-level justfile and ignore the repo-local test virtualenv.
  2. Repoint the three CI jobs, agent contract, and Backlog definition of done without changing shared-workflow calls or aggregator semantics.
  3. Verify justfile syntax and isolated local gates, account for the secret-backed identifier gate, then scan for stale entry points.
  4. Stage named paths, run proportionate review, commit and push main, obtain exact-head green CI, and finalize the task.

Implementation Notes

Campaign parking boundary: the migration is fully staged but no trustworthy final gate exists. A non-isolated just check resolved Playwright from another repository and was invalidated; the follow-up child then drifted into an unrelated BrewMDM polling command and was stopped to enforce repository ownership. Resume in this checkout only: first prove pwd and git top-level, use an absolute justfile/working-directory and unique JUST_TEMPDIR/TMPDIR, sanitize PATH and PYTHON/Node environment so every invoked binary resolves inside this repository or the system toolchain, then run just –fmt –check, JSON dump, just check, CodeRabbit, commit/push, exact-head CI, and finalization. Preserve the current staged migration and do not count any earlier cross-repository process as evidence.

Resumed from the documented park boundary. An isolated full gate passed pytest but hit a transient OpenTofu provider-startup failure in the standalone example; the same recipe then passed with a fresh provider data directory and the default temporary-directory setting. The final provenance-controlled full gate is being rerun before review and commit.

Final verification: an isolated sentinel-backed just check passed (1,422 passed, 2 skipped, 6,743 subtests; both OpenTofu roots validated; identifier history and em-dash scans clean). The protected customer-identifier pattern was exercised by exact-head GitHub CI, where the identifier, pytest, OpenTofu, and ci-success jobs all passed. just formatter, JSON dump, recipe list, actionlint, zizmor, named-path diff checks, Makefile absence, script retention, and .venv ignore behavior were also verified. CodeRabbit review found and the migration fixed the tracked nested-virtualenv scan bypass; one non-impactful test-only absolute-git-path minor was deliberately left.

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.

Final Summary

Migrated the repository task surface to a top-level justfile, repointed CI and documentation, preserved all five real shell tools behind recipes, and added safe local-virtualenv handling. Verified by isolated local gate and exact-head required GitHub CI.

View the source file on GitHub