Justfile
Rama master · réplica de lectura del repositorio original.
# SPDX-License-Identifier: FSL-1.1-Apache-2.0
#
# Top-level command runner. Entry point for contributors.
# Run `just --list` for an overview.
set shell := ["bash", "-cu"]
# Default: show available targets.
default:
@just --list
# Install local Git hooks via lefthook. Idempotent.
bootstrap:
#!/usr/bin/env bash
set -euo pipefail
if command -v lefthook >/dev/null 2>&1; then
lefthook install
echo "==> lefthook hooks installed"
else
echo "lefthook not on PATH; skipping git-hooks install."
echo "Install: nix profile install nixpkgs#lefthook"
fi
# Lint everything: CUE, SPDX, conditionally Rust and TS.
lint:
#!/usr/bin/env bash
set -euo pipefail
./scripts/lint-cue.sh
./scripts/check-spdx-headers.sh
./scripts/check-no-cyrillic.sh
./scripts/check-heredoc-backticks.sh
./scripts/check-dump-diagnostics.sh
./scripts/check-workflows.sh
./scripts/docs-check.sh
./scripts/check-crd-structural.sh
./scripts/check-operator-version-bump.sh
./scripts/check-cli-version-bump.sh
./scripts/check-backup-runner-pin.sh
./scripts/check-argocd-cue-cmp-drift.sh
./scripts/check-cue-cmp-mirror.sh
./scripts/check-argocd-health-lua.sh
./scripts/check-component-enablement.sh
# Pulls each component's upstream chart, so it needs the network (~45s).
./scripts/check-component-claim-templates.sh
# Renders the loader's Argo CD chart and HEADs two registries (network, ~5s).
./scripts/check-ecr-seed.sh
./scripts/check-backup-render.sh
./scripts/check-version-coherence.sh
./scripts/check-plan-checkboxes.sh
# Dependency health — advisories, unmaintained crates, licences, sources.
# A different question from the version watcher's "are we behind?", which
# cannot see an abandoned crate (it sits on its own final release forever)
# or an advisory with no patched version. Also runs in CI (test.yml), since
# this recipe is invoked by no workflow.
./scripts/cargo-deny.sh
# `docs-check.sh` byte-compares the generated CLI reference against a
# fresh render, which cannot see a defect present in BOTH — a doc
# comment that hard-wraps inside a token renders identically twice and
# passes. `docsgen`'s own tests assert the properties instead, and they
# are the only gate that catches that class, so they belong in `lint`
# rather than only in the `test` workflow. ~7s from a warm target dir.
if [ -f cli/Cargo.toml ]; then
echo "==> docsgen property tests (cli)"
( cd cli && cargo test -q -p docsgen --tests )
fi
# cli/ and operator/ are SEPARATE Cargo workspaces (no top-level
# Cargo.toml), so cargo must run from inside each — matching CI
# (.github/workflows/lint.yml runs fmt+clippy per workspace). The
# `-f` guard keeps this safe from a fresh root before subworkspaces exist.
for ws in cli operator; do
if [ -f "$ws/Cargo.toml" ]; then
echo "==> rustfmt + clippy ($ws)"
( cd "$ws" && cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings )
else
echo "==> no $ws/Cargo.toml — skipping rustfmt/clippy"
fi
done
# Per-package JS lint (landing/*, backstage-plugins/*) is owned by CI
# (.github/workflows/lint.yml iterates each package with its own
# bun install). `just lint` only runs a root lint script if one exists;
# there is no root package.json, so this skips cleanly rather than
# erroring with "Script not found lint" from the repo root.
if [ -f package.json ] && grep -q '"lint"' package.json 2>/dev/null; then
bun run lint
else
echo "==> no root package.json lint script — per-package bun lint runs in CI"
fi
# Format all code (in-place).
fmt:
#!/usr/bin/env bash
set -euo pipefail
if command -v cue >/dev/null 2>&1; then
cue fmt ./schemas/... ./examples/...
else
nix run nixpkgs#cue -- fmt ./schemas/... ./examples/...
fi
# Per-workspace (no top-level Cargo.toml) — cargo must run from inside each.
for ws in cli operator; do
if [ -f "$ws/Cargo.toml" ]; then ( cd "$ws" && cargo fmt --all ); fi
done
# Run all tests, conditional on workspace presence.
test:
#!/usr/bin/env bash
set -euo pipefail
# Per-workspace (no top-level Cargo.toml) — cargo must run from inside each.
for ws in cli operator; do
if [ -f "$ws/Cargo.toml" ]; then
echo "==> cargo test ($ws)"
( cd "$ws" && cargo test --all --all-features )
fi
done
if find . -name package.json -not -path '*/node_modules/*' | head -1 | grep -q .; then
bun test
else
echo "==> no package.json — skipping bun test"
fi
# Generate the operator chart CRDs from the v1alpha1 CUE schemas (ADR 0047).
# CUE is the single source of truth; the crd-*.yaml files are GENERATED and
# committed. Runs under `nix develop` so the cue version is flake.lock-pinned
# (the drift gate compares byte-for-byte). Run after any schema change.
gen-crds:
nix develop --command bash -c 'cd operator && cargo run -q -p crdgen -- generate'
# CRD drift gate (ADR 0047): assert the committed crd-*.yaml match what
# crdgen generates from CUE. Local-first — the lefthook pre-commit hook and
# the crd-check CI workflow run the same script.
crd-check:
bash scripts/crd-check.sh
# Validate every CRD against a REAL apiserver (ephemeral kind cluster).
# `helm lint` does NOT catch CRD structural-schema errors — only the
# apiserver does — so run this before any CRD-changing release. Fast
# (~30s) focused CRD gate; `just e2e` is the comprehensive backstop.
# Requires kind + a working docker/podman.
crd-validate:
bash scripts/validate-crds.sh
# Run the GitOps-walk k3d e2e (CMP → Argo CD → operator loop).
# Requires a running Docker daemon and k3d on PATH.
e2e-gitops:
bash e2e/gitops-walk.sh
# Run the local k3d e2e gate (currently just gitops-walk).
# NOTE: e2e/mvp.sh (real Hetzner provision smoke, costs money) is
# intentionally excluded here — it runs via the nightly CI workflow
# only (.github/workflows/nightly.yml, 04:00 UTC).
#
# The platform-migration walk is NOT part of the k3d gate: the
# PlatformController's OCI client is HTTPS-only (oci-distribution
# ClientConfig::default()), while k3d's local registry is plain
# HTTP, so the controller cannot pull fixture compat-docs in-cluster.
# The MigrationPlan gate is covered by operator unit + integration
# tests; a real-infra (HTTPS-registry) migration walk is a tracked
# follow-up for the nightly Hetzner harness. See plan.md §1.81.
e2e: e2e-gitops
# Run the needs.pg ResourceClaim-chain k3d/kind walk (generate -> schedule
# -> provision -> resume + explicit DSN ref -> delete + RetainedClaim ->
# force-GC -> psql DROP proof). Since 2.12 (ADR 0046) the app binds its DSN by
# an explicit `env: {DATABASE_URL: {claim: "pg.url"}}` ref (the 2.4e auto-inject
# + composed connection-Secret key are removed), so while 2.12 is UNRELEASED
# this RUNS WITH APPRAFTER_E2E_LOCAL_OPERATOR=1: it builds + side-loads the
# working-tree operator + admission-webhook and applies the branch CRDs (the
# published image/CRD predate the `env` value node). Deliberately NOT a
# dependency of `e2e`: it boots a Postgres pod (heavier) and runs on its own
# nightly cadence via .github/workflows/e2e-pg-nightly.yml. Requires a container
# runtime (docker->k3d / podman->kind), cargo, kubectl.
e2e-pg:
APPRAFTER_E2E_LOCAL_OPERATOR=1 bash e2e/needs-pg-walk.sh
# Run the needs.redis ResourceClaim-chain k3d walk (generate -> schedule
# -> provision -> resume + DSN -> $N-ACL isolation proof -> delete +
# RetainedClaim -> force-GC -> FLUSHDB/DELUSER proof). Like e2e-pg it is
# NOT a dependency of `e2e`: it boots a Dragonfly pod and runs on its own
# nightly cadence via .github/workflows/e2e-redis-nightly.yml. Requires
# Docker + k3d on PATH.
e2e-redis:
bash e2e/needs-redis-walk.sh
# Run the needs.disk ResourceClaim-chain k3d/kind walk (generate ->
# schedule -> provision (unowned RWO PVC) -> resume + mount -> data
# durability -> delete + RetainedClaim + reattach -> force-GC -> PVC
# dropped). Runs the PUBLISHED operator image (v0.2.21+ ships the 2.6b disk
# feature, CRDs, the disk-local seed, and the PVC RBAC), exactly like
# e2e-pg / e2e-redis. Set APPRAFTER_E2E_LOCAL_OPERATOR=1 to build +
# side-load the working-tree operator instead (pre-release validation of
# disk-code changes). Like e2e-pg/e2e-redis it is NOT a dependency of
# `e2e`: it boots a CNPG pod + provisions PVCs and runs on its own cadence.
# Requires a container runtime (docker→k3d / podman→kind), cargo, kubectl.
e2e-disk:
bash e2e/needs-disk-walk.sh
# Run the env-and-secrets k3d/kind walk — ONE cluster covering BOTH the 2.12
# `Application.env` value-reference chain (ADR 0046) and the CLI-facing secrets
# surface (day-2 D6/D7). `secret seal` into the APP namespace -> `secret list`
# -> `app add` from a git daemon (needs.pg provisions a claim) -> the rendered
# env keeps the literal a literal, points the claim refs at the decomposed
# connection-Secret keys url/user/pass and the external ref at its own
# Secret+key, with EXACTLY ONE DATABASE_URL entry (2.4e auto-inject removed) ->
# the pod's resolved values -> re-seal under another key breaks the binding and
# `app status` explains it (D7) -> recover -> a rotation moves
# status.envConfig.{digest,changedAt} and shows `← old config` WITHOUT rolling
# the pod (D6) -> `secret remove`.
#
# It REPLACES the former `e2e-env-refs` (e2e/needs-env-refs-walk.sh), which
# paid for its own cluster + operator build (~5m20) to assert the operator half
# in isolation via a direct marker-form CR apply. The merged walk reaches the
# operator through cue-cmp + Argo CD instead; the cue-cmp injection stays
# covered on its own by argocd-cue-cmp/test-inject.sh. See the walk header.
#
# The script FORCES APPRAFTER_E2E_LOCAL_OPERATOR=1 itself: it builds +
# side-loads the working-tree operator + admission-webhook and applies the
# branch CRDs/RBAC (the published image/CRD predate 2.22c `status.envConfig`).
# Like e2e-pg/e2e-redis/e2e-disk it is NOT a dependency of `e2e`: it boots a
# CNPG pod + Argo CD + a git daemon and runs on its own nightly cadence via
# .github/workflows/e2e-env-and-secrets-nightly.yml. Requires a container
# runtime (docker→k3d / podman→kind), git, cargo, kubectl, helm, python3.
e2e-env-and-secrets:
bash e2e/env-and-secrets-walk.sh
# Run the per-environment GitOps-walk k3d/kind e2e (2.9, ADR 0044): the
# SAME repo deployed per env (app add --env dev|prod) -> two Argo
# Applications web-dev + web-prod -> the argocd-cue-cmp sidecar injects
# spec.environment -> the operator env-resolves base+environments -> two
# self-contained Deployments in two namespaces (replicas 1 vs 2, TIER dev
# vs prod) -> app status aggregates both -> app remove --env is surgical.
# 2.9 is UNRELEASED, so this RUNS WITH APPRAFTER_E2E_LOCAL_OPERATOR=1: it
# builds + side-loads the working-tree operator + admission-webhook AND the
# working-tree cue-cmp (Approach A — the per-env injection lives only in the
# working-tree entrypoint.sh), and applies the branch CRDs. Like
# e2e-pg/e2e-redis/e2e-disk it is NOT a dependency of `e2e`: it boots Argo CD
# + a git daemon and runs on its own nightly cadence via
# .github/workflows/e2e-per-env-nightly.yml. Requires a container runtime
# (docker→k3d / podman→kind), git, cargo, kubectl.
e2e-per-env:
APPRAFTER_E2E_LOCAL_OPERATOR=1 bash e2e/gitops-walk-per-env.sh
# Run the per-environment `expose` DEEP-MERGE k3d/kind e2e (2.16c): the
# SAME repo deployed per env, where the dev override carries ONLY the diff
# `expose:{network:"internal"}` -> the operator effective_spec DEEP-MERGES it
# onto base.expose so the dev Deployment/Service INHERIT the base port (8080)
# and, being internal, emit NO public HTTPRoute (the inherited hostname is
# inert), while base/prod keeps the public HTTPRoute on x.example.com. Also
# asserts H1: the STORED dev CR's env-override expose stays partial
# ({network:internal} only, no defaulted keys). 2.16c is UNRELEASED, so the
# walk FORCES APPRAFTER_E2E_LOCAL_OPERATOR=1 internally (builds + side-loads
# the working-tree operator + admission-webhook + cue-cmp, applies branch
# CRDs). Uses the DEFAULT CNI (no Cilium → no sandbox-run microVM needed).
# Like the other walk targets it is NOT a dependency of `e2e`; requires a
# container runtime (docker→k3d / podman→kind), git, cargo, kubectl.
e2e-expose-deep-merge:
bash e2e/expose-deep-merge-walk.sh
# Run the needs.networkpolicy egress-enforcement kind+Cilium walk (2.10, ADR
# 0045): the operator derives one per-Application egress CiliumNetworkPolicy
# from declared needs -> a needs.pg app CAN reach pg (Hubble FORWARDED) while
# a needs-less app CANNOT (Hubble DROPPED) -> `apprafter platform egress set
# internal|strict` tightens the baseline (external blocked, then same-namespace
# blocked). UNLIKE the other walks this one needs REAL Cilium: it brings kind
# up with the default CNI + kube-proxy disabled (kind_up_cilium) and bootstraps
# WITH Cilium (bootstrap_with_cilium), then asserts `cilium status --wait`. The
# walk forces the kind runtime + builds the working-tree operator + webhook
# (2.10 UNRELEASED), so it needs cargo, kubectl, a container runtime, AND the
# Cilium + Hubble CLIs (nix develop ships cilium-cli; the lib.sh wrappers fall
# back to `nix run nixpkgs#…`). NOTE: a rootless-podman host with a capped
# memlock ulimit (8 MB) cannot run the cilium-agent — the full enforcement run
# needs a rootful runtime (CI) or a host memlock raise; the nightly is
# .github/workflows/e2e-networkpolicy-nightly.yml. Like the other walk targets
# it is NOT a dependency of `e2e`.
e2e-networkpolicy:
bash e2e/needs-networkpolicy-walk.sh
# Spin up a local k3d cluster suitable for end-to-end work.
e2e-up:
k3d cluster create apprafter-dev \
--servers 1 --agents 0 \
--port "8080:80@loadbalancer" \
--port "8443:443@loadbalancer" \
--k3s-arg "--disable=traefik@server:0" \
--k3s-arg "--disable=servicelb@server:0"
@echo "==> cluster ready. kubectl context: k3d-apprafter-dev"
# Tear down the local k3d cluster.
e2e-down:
k3d cluster delete apprafter-dev
# Live docs preview. Always goes through the flake so the mkdocs
# version, theme and plugins are the flake.lock-pinned ones — the
# old `command -v mkdocs || nix shell …` fallback could not work
# (two python envs; see flake.nix comment).
docs-serve:
nix develop --command mkdocs serve
# Strict docs build. `--strict` turns every mkdocs warning into a
# build failure; `mkdocs.yml` decides which checks warn (dead links,
# dead anchors, pages missing from nav).
docs-build:
nix develop --command mkdocs build --strict
# Documentation drift gate: strict build + generated-reference
# byte-compare. Same script the lefthook hook and the docs.yml
# workflow run.
docs-check:
bash scripts/docs-check.sh
# Live landing content smoke (SYS-3 layer c). Probes the running
# production site — the CMS retags prod with no PR, so an image smoke
# would never witness a content-only regression. Defaults to
# apprafter.dev; pass a base URL to probe a preview/local host.
landing-smoke base='https://apprafter.dev':
bash scripts/landing-site-smoke.sh {{base}}
# Landing fallback-JSON schema gate (SYS-3 layer a). The bun test that
# validates the fallback JSONs baked into the reproducible image against
# the site shapes and the phase registry.
landing-check:
cd landing && bun test landing-content-gate.test.ts
# Regenerate `landing/cms/payload-types.ts` from the Payload collections
# and globals, and commit the result.
#
# MUST be run (and the result committed) after ANY change under
# `landing/cms/src/collections/` or `landing/cms/src/globals/` — a new
# field, a renamed one, a changed type, an edited `admin.description`
# (Payload mirrors those into the file as JSDoc). The generator opens no
# database and needs no configuration values.
#
# Nothing regenerates this for you. There is no pre-commit hook, on
# purpose — `scripts/check-payload-types.sh` explains why a hook cannot
# be correct here — so forgetting it is caught by the `payload-types
# drift` step in `.github/workflows/lint.yml`, on the pull request.
#
# Regenerate the Payload types after editing a collection or global.
payload-types:
cd landing/cms && bun run generate:types
# Byte-compare `landing/cms/payload-types.ts` against a fresh render
# without leaving the regenerated file behind. This is what CI runs; run
# it before pushing if you would rather not wait for the pull request.
# It needs `landing/cms/node_modules`, which is why it is not in
# `just lint` — that is meant to work in a fresh checkout, the same
# boundary CI already draws for the per-package `bun run lint`.
#
# Check the committed Payload types against a fresh render.
payload-types-check:
./scripts/check-payload-types.sh
# Regenerate `docs/reference/cli/**` from the clap definitions.
#
# MUST be run (and the result committed) after ANY change to the CLI
# surface — a new command, a renamed or removed flag, a changed
# default, an edited doc comment on anything under
# `cli/platform-cli/src/`. `docsgen check` byte-compares the committed
# tree against a fresh render, so forgetting this fails `just lint`,
# the pre-commit hook and the docs workflow.
#
# ALSO run it on a release commit: `commands.json` carries
# `cli_version`, so bumping `cli/Cargo.toml` changes a byte-compared
# artefact. That is deliberate — a consumer of the machine-readable
# surface needs to know which CLI it describes.
#
# Goes through the flake for the same pinned toolchain as the docs
# build; `cli/` is its own Cargo workspace, hence the `cd`.
docsgen-generate:
nix develop --command bash -c 'cd cli && cargo run -q -p docsgen -- generate'
# Render the platform-stack umbrella chart from CUE source into
# `platform-stack/dist/platform-stack-<version>/`. Wrapper over
# `make -C platform-stack render`. Used by the publish workflow
# (`.github/workflows/platform-stack-publish.yml`) and by
# contributors editing `platform-stack/cue/`. `dist/` is gitignored.
platform-stack-render:
#!/usr/bin/env bash
set -euo pipefail
if command -v make >/dev/null 2>&1; then
make -C platform-stack render-only
else
nix shell nixpkgs#gnumake -c make -C platform-stack render-only
fi
# Render + helm lint + per-tier helm template sanity check. The
# `make render` target inside `platform-stack/` already invokes
# helm lint; this just bundles the typical developer round-trip
# under one project-level entry point.
platform-stack-check:
#!/usr/bin/env bash
set -euo pipefail
if command -v make >/dev/null 2>&1 && command -v helm >/dev/null 2>&1; then
make -C platform-stack render
else
nix shell nixpkgs#gnumake nixpkgs#kubernetes-helm -c make -C platform-stack render
fi
# Quick repo statistics — lines of code per language.
stats:
@echo "Lines of code (tracked source files):"
@git ls-files \
'*.rs' '*.ts' '*.tsx' '*.cue' '*.sh' '*.nix' \
2>/dev/null | xargs wc -l 2>/dev/null | tail -1 || true