From 45cafe8b0010097b2ec43e1def9d6760785d6c6a Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 21:52:09 +0200 Subject: [PATCH 01/19] docs(superpowers): implementation plan for PR 2 of the scripts restructure ops/ and docs/ move under a new gate that resolves every script path on an executed .tf/.tm.hcl line: 87 today, 30 of them to scripts this PR moves. Every expected count was measured by simulating the move. --- ...2026-09-21-scripts-restructure-pr2-plan.md | 888 ++++++++++++++++++ 1 file changed, 888 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md diff --git a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md new file mode 100644 index 000000000..24d8fb718 --- /dev/null +++ b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md @@ -0,0 +1,888 @@ +# scripts/ restructure — PR 2 (`ops/`, `docs/`, the terramate-reference gate) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move the day-2 operations scripts into `scripts/ops//` and the docs-site generators +into `scripts/docs/`, delete the two dead scripts the design names, and move `check-rebased.sh` to +`scripts/ci/`. All of it happens under a new gate that proves every script path opentofu and +terramate execute still resolves. + +**Architecture:** This PR has the same shape as PR 1: the gate ships first, on the unmoved tree, +and is provably green before anything relocates. Then the files move, one audience per commit, and +each move re-runs the gate. The gate is needed because 30 executed references to these scripts +live in `.tf` and `.tm.hcl` files. They run at apply and destroy time, no CI job executes +them, and neither existing gate reads them. + +**Tech Stack:** bash, go-task v3.53.1 (via `mise.toml`), terramate, Python 3 (`diagram-icons.py`, +untouched apart from its root depth). + +**Spec:** [`docs/superpowers/specs/2026-09-17-scripts-restructure-design.md`](../specs/2026-09-17-scripts-restructure-design.md) +— the target layout (~lines 55–100) and *Risk and sequencing*. PR 1's plan, +[`2026-09-17-scripts-restructure-pr1-plan.md`](2026-09-17-scripts-restructure-pr1-plan.md), holds +the reference-rewrite recipe this plan reuses. + +## Global Constraints + +- **Branch base.** Work happens in worktree `.claude/worktrees/scripts-pr2` on branch + `worktree-scripts-pr2`, which is cut from PR 1's head `7ec01df5` (#2061, unmerged). **PR 2 does + not open until #2061 merges**. The `check-rebased` pre-push hook refuses the push anyway. +- **Never execute an `ops/` script to test a move.** They act on live clouds: + `eks-prepare-destroy.sh` deletes every PVC. Prove paths statically, with `bash -n`, `test -e` on + the resolved path, and the gates. +- **Never anchor a path rewrite on the bare token `scripts/`.** `opentofu/{aws,gcp}/openbao/cluster/scripts/` + are module-local directories. Anchor on `scripts/`. +- **`sed -i --follow-symlinks`, always.** `sed -i` replaces a symlink with a regular file even on + a no-match run. The repo has 13 tracked symlinks, and ten are `CLAUDE.md` → `AGENTS.md`. +- **Select rewrite targets with `git grep -l`, never a file glob.** +- **The dated archive is not rewritten:** `docs/superpowers/plans/`, `docs/superpowers/specs/`, + `docs/specs/`. +- **The prefix drops only when it repeats the directory name.** `aws-sweep-orphaned-volumes.sh` + becomes `ops/aws/sweep-orphaned-volumes.sh`. `eks-`, `cnpg-` and `destroy-` are not directory + names and stay. +- **Do not move:** `openbao-snapshot.sh` (a symlink whose home is unsettled), or anything bound + for `provision/` in PR 3: `helm-release-present.sh`, `openbao-adopt-jwt-mount.sh`, + `openbao-config.sh`, `secret-store.sh`, `tm-provisioner.sh`, `zitadel-actions/`, + `zitadel-idp.sh`, `zitadel-oidc-clients.sh`. +- **Job names in `.github/workflows/*.y*ml` must not change.** They are required-check contexts. + No PR 2 script is referenced from a workflow (0 hits), so no workflow should change at all. +- Repository metadata is English. Never co-author commits, and add no generated-with lines. + +--- + +## File Structure + +**Created:** + +| Path | Responsibility | +|---|---| +| `scripts/ci/tests/test-terramate-script-refs.sh` | the gate: every script path on an executed opentofu/terramate line exists | +| `scripts/ops/tasks.yaml` | `ops:*` tasks, each a one-line call with `{{.CLI_ARGS}}` passthrough | +| `scripts/docs/tasks.yaml` | `docs:*` tasks | + +**Moved** (`git mv`; depth fixes go in the same commit): + +| From `scripts/` | To `scripts/` | Task | +|---|---|---| +| `export-diagrams.sh`, `diagram-icons.py`, `build-og-card.html` | `docs/` (names unchanged) | 3 | +| `aws-sweep-orphaned-volumes.sh` | `ops/aws/sweep-orphaned-volumes.sh` | 4 | +| `aws-sweep-teardown-blockers.sh` | `ops/aws/sweep-teardown-blockers.sh` | 4 | +| `eks-prepare-destroy.sh`, `eks-recycle-bootstrap-nodes.sh` | `ops/aws/` (names unchanged) | 4 | +| `gcp-adopt-workforce-pool.sh` | `ops/gcp/adopt-workforce-pool.sh` | 4 | +| `gcp-purge-dns-records.sh` | `ops/gcp/purge-dns-records.sh` | 4 | +| `gcp-sweep-orphaned-disks.sh` | `ops/gcp/sweep-orphaned-disks.sh` | 4 | +| `k8s-reclaim-csi-volumes.sh` | `ops/k8s/reclaim-csi-volumes.sh` | 4 | +| `cnpg-prepare-restore.sh`, `cnpg-promote-seed.sh` | `ops/k8s/` (names unchanged) | 4 | +| `demo-load.sh` | `ops/demo/load.sh` | 4 | +| `cleanup-benchmark-images.sh` | `ops/demo/` (name unchanged) | 4 | +| `destroy-stage2.sh`, `tofu-destroy-contained.sh`, `terramate-destroy-confirm.sh` | `ops/teardown/` (names unchanged) | 5 | +| `check-rebased.sh` | `ci/check-rebased.sh` | 6 | + +**Deleted:** `scripts/teardown.sh`, `scripts/aws-sweep-controller-orphans.sh` (Task 2). The design's +Deletions table covers both; neither has an external caller. + +**Modified:** `taskfile.yaml`, `.pre-commit-config.yaml`, `scripts/README.md`, `scripts/AGENTS.md`, +`scripts/ci/tests/test-cnpg-promote-seed.sh`, plus every file `git grep -l` selects in Tasks 3–6. + +--- + +### Task 1: The `test-terramate-script-refs.sh` gate + +Measured on this branch: **87** script references on executed (non-comment) lines under +`opentofu/`, in five shapes. `${terramate.root.path.fs.absolute}/scripts/X` ×70, `$${ROOT}/scripts/X` +×7, bare `scripts/X` in `echo` hints ×4, `${path.module}/scripts/X` ×4 (module-local), and +`${path.module}/../../../../scripts/X` ×2. + +Two rules resolve all five. A `${path.module}/` prefix resolves beside the `.tf` file. Everything +else resolves from the repo root. + +**Files:** +- Create: `scripts/ci/tests/test-terramate-script-refs.sh` +- Modify: `scripts/AGENTS.md` (one row in `## The rest`) + +**Interfaces:** +- Produces: the suite. `run.sh` discovers it with no wiring. Two env overrides exist for fixtures: + `TM_REFS_ROOT` (repo root to scan) and `TM_REFS_FLOOR` (coverage floor, default 80). Tasks 3–6 + re-run it after every move. + +- [ ] **Step 1: Write the gate** + +```bash +#!/usr/bin/env bash +# Every script path that opentofu and terramate name on an executed line must exist. +# +# These run at apply and destroy time, and no CI job executes them. A move that +# misses one fails mid-destroy with a bare "No such file". test-script-paths.sh +# checks paths computed *inside* scripts; verify-doc-paths.sh reads only the docs +# site. Neither reads *.tm.hcl or *.tf, which is where these live. +# +# Comment lines are skipped: a stale comment misleads, but it cannot break a run. +# `echo` hints are checked — an operator copies them during a failed destroy. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="${TM_REFS_ROOT:-$(cd "$HERE/../../.." && pwd)}" +# Measured at the commit that added this gate. A count below it means the +# extraction broke, not that references went away; fail rather than pass over less. +FLOOR="${TM_REFS_FLOOR:-80}" + +checked=0 failed=0 +while IFS= read -r hit; do + file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; text="${rest#*:}" + [[ "$text" =~ ^[[:space:]]*(#|//) ]] && continue + while IFS= read -r ref; do + [ -n "$ref" ] || continue + if [[ "$ref" == '${path.module}/'* ]]; then + target="$ROOT/$(dirname "$file")/${ref#'${path.module}/'}" + else + target="$ROOT/scripts/${ref#*scripts/}" + fi + checked=$((checked + 1)) + if [ ! -e "$target" ]; then + printf 'FAIL %s:%s %s\n resolved to %s\n' "$file" "$line" "$ref" "${target#"$ROOT"/}" + failed=$((failed + 1)) + fi + done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' <<<"$text" \ + | sed -E 's/^[[:space:]"(]//') +done < <(cd "$ROOT" && grep -rnE --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ + 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' opentofu 2>/dev/null) + +if [ "$checked" -lt "$FLOOR" ]; then + echo "FAIL checked $checked script reference(s), floor is $FLOOR: the extraction broke, not the references" + exit 1 +fi +echo "$checked script reference(s) on executed opentofu/terramate lines checked; $failed failed" +[ "$failed" -eq 0 ] +``` + +`chmod +x` it. The `'${path.module}/'` single quotes are deliberate: they are a literal match, not +an expansion. ShellCheck's SC2016 is not raised at `-S warning`, but confirm that in Step 6. + +- [ ] **Step 2: Run it against the real tree** + +Run: `bash scripts/ci/tests/test-terramate-script-refs.sh; echo "exit=$?"` +Expected: `87 script reference(s) on executed opentofu/terramate lines checked; 0 failed`, `exit=0`. + +- [ ] **Step 3: Prove it catches a moved script** + +It must fail on exactly the move it exists for. Run it against a copy with +`terramate-destroy-confirm.sh` moved and nothing rewritten: + +```bash +T=$(mktemp -d); cp -r opentofu scripts "$T"/ +mkdir -p "$T/scripts/ops/teardown" +mv "$T/scripts/terramate-destroy-confirm.sh" "$T/scripts/ops/teardown/" +TM_REFS_ROOT="$T" bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1; echo "exit=${PIPESTATUS[0]}" +``` +Expected: `87 script reference(s) … checked; 15 failed`, `exit=1`. + +- [ ] **Step 4: Prove it resolves `${path.module}` climbs** + +```bash +mv "$T/scripts/ops/teardown/terramate-destroy-confirm.sh" "$T/scripts/" +mv "$T/scripts/helm-release-present.sh" "$T/scripts/helm-release-present.sh.bak" +TM_REFS_ROOT="$T" bash scripts/ci/tests/test-terramate-script-refs.sh | grep -c '^FAIL' +``` +Expected: `2`, meaning the two `${path.module}/../../../../scripts/helm-release-present.sh` lines +in `opentofu/{aws/eks,gcp/gke}/configure/main.tf`. + +- [ ] **Step 5: Prove it refuses to pass over nothing** + +```bash +E=$(mktemp -d); mkdir -p "$E/opentofu" +TM_REFS_ROOT="$E" bash scripts/ci/tests/test-terramate-script-refs.sh; echo "exit=$?" +rm -rf "$T" "$E" +``` +Expected: `FAIL checked 0 script reference(s), floor is 80: …`, `exit=1`. + +- [ ] **Step 6: Lint, and prove discovery** + +```bash +shellcheck -x -S warning scripts/ci/tests/test-terramate-script-refs.sh; echo "shellcheck=$?" +task ci:test | grep -E 'terramate-script-refs|passed,' +``` +Expected: `shellcheck=0`, a `PASS test-terramate-script-refs` line, and +`23 passed, 1 skipped, 0 failed` (PR 1 had 22 passed). + +- [ ] **Step 7: Document it** + +In `scripts/AGENTS.md`, in the `| Script | Checks |` table under `## The rest`, add this row +directly after the `validate-idp-topology.sh` row: + +```markdown +| `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl` line exists — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | +``` + +- [ ] **Step 8: Commit** + +```bash +git add scripts/ci/tests/test-terramate-script-refs.sh scripts/AGENTS.md +git commit -m "test(ci): gate every script path opentofu and terramate execute + +87 references on executed lines, 30 of them to scripts this PR moves. +They run at apply and destroy time and no CI job executes them; neither +test-script-paths.sh nor verify-doc-paths.sh reads a .tf or .tm.hcl." +``` + +--- + +### Task 2: Delete the two dead scripts + +`teardown.sh` calls itself "the supported way to tear the platform down", but nothing calls it. The +supported path is `terramate script run --reverse destroy`, whose confirmation step is +`terramate-destroy-confirm.sh`. `aws-sweep-controller-orphans.sh` has exactly one caller, and that +caller is `teardown.sh`. + +**Files:** +- Delete: `scripts/teardown.sh`, `scripts/aws-sweep-controller-orphans.sh` +- Modify: `scripts/eks-prepare-destroy.sh` (the comment block around line 30 that names + `aws-sweep-controller-orphans.sh`) + +**Interfaces:** +- Consumes: nothing. Produces: a smaller rename surface for Tasks 4–5. + +- [ ] **Step 1: Confirm there is no caller** + +```bash +git grep -n -E 'teardown\.sh|aws-sweep-controller-orphans' \ + -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' +``` +Expected: only lines inside the two files themselves, plus `scripts/eks-prepare-destroy.sh:30`, a +comment that mentions the sweep but does not invoke it. Any other hit is a caller: **stop and report +it** rather than deleting. + +- [ ] **Step 2: Confirm nothing is lost that the destroy path needs** + +Read the header of `scripts/aws-sweep-controller-orphans.sh` and list what it sweeps. Then read +`scripts/aws-sweep-teardown-blockers.sh` and `scripts/eks-prepare-destroy.sh`, which the terramate +destroy path does call, and say in the report whether they cover each item. + +This step decides nothing on its own. The deletion is the design's call. Its job is to put in +front of the reviewer any capability that disappears with the file. If an item is not covered, say +so plainly in the report and the commit body. + +- [ ] **Step 3: Delete, and fix the comment that names a deleted file** + +```bash +git rm scripts/teardown.sh scripts/aws-sweep-controller-orphans.sh +``` + +Rewrite the `eks-prepare-destroy.sh` comment near line 30 ("WHY NOT IN eks-prepare-destroy.sh") so +it no longer names a script that does not exist. Keep the *why* if it still holds. If it only made +sense next to the deleted file, delete the comment too. + +- [ ] **Step 4: Verify** + +```bash +bash -n scripts/eks-prepare-destroy.sh; echo "syntax=$?" +bash scripts/ci/tests/test-no-secret-argv.sh >/dev/null; echo "argv=$?" +task ci:test | tail -1 +``` +Expected: `syntax=0`, `argv=0`, `23 passed, 1 skipped, 0 failed`. + +- [ ] **Step 5: Commit** + +```bash +git add -A scripts/ +git commit -m "chore(scripts): delete teardown.sh and the sweep only it called + +Neither has a caller: terramate script run --reverse destroy is the +teardown path. Named in the design's Deletions table." +``` + +If Step 2 found an uncovered capability, add one line to the commit body naming it. + +--- + +### Task 3: Move the docs-site generators into `scripts/docs/` + +Three files, 14 reference lines across 9 files. All three compute a path to the repo root from +where they sit, and each lands one level deeper. + +**Files:** +- Move: `scripts/{export-diagrams.sh,diagram-icons.py,build-og-card.html}` → `scripts/docs/` +- Modify: the three files' root depths, plus every file Step 4 selects + +**Interfaces:** +- Produces: `scripts/docs/export-diagrams.sh`, `scripts/docs/diagram-icons.py`, + `scripts/docs/build-og-card.html`. Task 7 indexes the first two. + +- [ ] **Step 1: Record the audit baseline** + +`diagram-icons.py audit` exits 0 whatever it finds. Run from the wrong root, it prints nothing and +still exits 0, so only its counts prove it read the diagrams: + +```bash +python3 scripts/diagram-icons.py audit 2>&1 | grep -cE '^[a-z0-9]' +python3 scripts/diagram-icons.py audit 2>&1 | grep -E '^ +[0-9]+ (local|none)' +``` +Expected, as measured on this branch: `10`, then ` 18 local …` and ` 8 none …`. + +- [ ] **Step 2: Move** + +```bash +mkdir -p scripts/docs +git mv scripts/export-diagrams.sh scripts/diagram-icons.py scripts/build-og-card.html scripts/docs/ +``` + +- [ ] **Step 3: Correct the three depths** + +| File:line | From | To | +|---|---|---| +| `scripts/docs/export-diagrams.sh:22` | `cd "$(dirname "$0")/.."` | `cd "$(dirname "$0")/../.."` | +| `scripts/docs/diagram-icons.py:36` | `ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))` | `ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))` | +| `scripts/docs/build-og-card.html:85` | `src="../website/static/images/ogenki-mark-white.svg"` | `src="../../website/static/images/ogenki-mark-white.svg"` | + +Verify each line number before editing. Then grep all three files for any other `..`, `dirname` +or `scripts/` path, and fix it the same way. + +- [ ] **Step 4: Rewrite the references** + +```bash +cat > /tmp/moved-pr2-docs.txt <<'EOF' +export-diagrams.sh|docs/export-diagrams.sh +diagram-icons.py|docs/diagram-icons.py +build-og-card.html|docs/build-og-card.html +EOF +TARGETS=$(git grep -l -E 'scripts/(export-diagrams\.sh|diagram-icons\.py|build-og-card\.html)' \ + -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') +printf '%s\n' "$TARGETS" | wc -l # expect 9 +while IFS='|' read -r old new; do + # shellcheck disable=SC2086 + sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS +done < /tmp/moved-pr2-docs.txt +``` + +The `scripts/docs/…` targets contain no `scripts/` substring, so the loop is idempotent. Also +update the two rows in `scripts/AGENTS.md`'s `## The rest` table that name `diagram-icons.py +audit` and `export-diagrams.sh` by bare name: prefix them `docs/`. + +- [ ] **Step 5: Verify** + +```bash +python3 scripts/docs/diagram-icons.py audit 2>&1 | grep -cE '^[a-z0-9]' +python3 scripts/docs/diagram-icons.py audit 2>&1 | grep -E '^ +[0-9]+ (local|none)' +test -e "scripts/docs/../../website/static/images/ogenki-mark-white.svg" && echo "og-card img ok" +bash -n scripts/docs/export-diagrams.sh; echo "syntax=$?" +bash scripts/ci/tests/test-script-paths.sh | tail -1 +task ci:links; task ci:doc-paths +for f in $(git ls-files -s | awk '$1=="120000"{print $4}'); do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done +``` +Expected: +- the same `10` / `18 local` / `8 none` as Step 1; +- `og-card img ok`; +- `syntax=0`; +- the paths gate: `0 failed`. It checks `export-diagrams.sh`'s `cd … /../..` root; +- both doc gates pass; +- no `NOT A SYMLINK` line. + +Do not run `export-diagrams.sh` for real. It needs the pinned drawio, and it rewrites every SVG. + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "refactor(scripts): move the docs-site generators to scripts/docs/" +``` + +--- + +### Task 4: Move the day-2 operations scripts into `scripts/ops/{aws,gcp,k8s,demo}/` + +12 files, 43 reference lines across 18 files. 11 of those lines are in `.tf`/`.tm.hcl`, and 8 of +those 11 are executed; the other 3 are comments. Seven +files also change name, so bare-name mentions go stale as well as paths. + +**Files:** +- Move: the 12 files in the File Structure table marked Task 4 +- Modify: four internal paths (Step 3), `scripts/ci/tests/test-cnpg-promote-seed.sh:25-26`, and + every file Step 4 selects + +**Interfaces:** +- Consumes: Task 1's gate. +- Produces: the `ops/aws|gcp|k8s|demo/` paths Task 7 indexes. + +- [ ] **Step 1: Move** + +```bash +mkdir -p scripts/ops/{aws,gcp,k8s,demo} +git mv scripts/aws-sweep-orphaned-volumes.sh scripts/ops/aws/sweep-orphaned-volumes.sh +git mv scripts/aws-sweep-teardown-blockers.sh scripts/ops/aws/sweep-teardown-blockers.sh +git mv scripts/eks-prepare-destroy.sh scripts/ops/aws/eks-prepare-destroy.sh +git mv scripts/eks-recycle-bootstrap-nodes.sh scripts/ops/aws/eks-recycle-bootstrap-nodes.sh +git mv scripts/gcp-adopt-workforce-pool.sh scripts/ops/gcp/adopt-workforce-pool.sh +git mv scripts/gcp-purge-dns-records.sh scripts/ops/gcp/purge-dns-records.sh +git mv scripts/gcp-sweep-orphaned-disks.sh scripts/ops/gcp/sweep-orphaned-disks.sh +git mv scripts/k8s-reclaim-csi-volumes.sh scripts/ops/k8s/reclaim-csi-volumes.sh +git mv scripts/cnpg-prepare-restore.sh scripts/ops/k8s/cnpg-prepare-restore.sh +git mv scripts/cnpg-promote-seed.sh scripts/ops/k8s/cnpg-promote-seed.sh +git mv scripts/demo-load.sh scripts/ops/demo/load.sh +git mv scripts/cleanup-benchmark-images.sh scripts/ops/demo/cleanup-benchmark-images.sh +``` + +- [ ] **Step 2: Watch the gate fail** + +```bash +bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 +bash scripts/ci/tests/test-script-paths.sh | tail -1 +``` +Expected: `87 … checked; 8 failed` (the executed references, which Step 4 fixes), then +`11 roots, 21 sources, 16 subjects checked; 4 failed`: the three `lib/` sources and +`test-cnpg-promote-seed.sh`'s subject, all fixed in Step 3. Both counts were measured by simulating +this move. A different number means the measurement is stale: report it. + +- [ ] **Step 3: Correct the internal paths** + +| File:line | From | To | +|---|---|---| +| `scripts/ops/gcp/sweep-orphaned-disks.sh:53` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | +| `scripts/ops/gcp/purge-dns-records.sh:38` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | +| `scripts/ops/k8s/cnpg-prepare-restore.sh:60` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | +| `scripts/ops/aws/eks-prepare-destroy.sh:115` | `"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" \|\| true` | `"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" \|\| true` | + +The last row is the one no gate sees. Both files move into *different* directories, and the target +is renamed. It is a path passed to `exec`, not a `source`, so `test-script-paths.sh` cannot check +it. Update any `# shellcheck source=` directive next to the three `lib/` lines in the same way. + +In `scripts/ci/tests/test-cnpg-promote-seed.sh`, delete the revisit comment at line 25 ("The +subject is still at scripts/ root. When it moves, this path moves with it.") and change line 26: + +```bash +SCRIPT="$HERE/../../ops/k8s/cnpg-promote-seed.sh" +``` + +- [ ] **Step 4: Rewrite the references** + +```bash +cat > /tmp/moved-pr2-ops.txt <<'EOF' +aws-sweep-orphaned-volumes.sh|ops/aws/sweep-orphaned-volumes.sh +aws-sweep-teardown-blockers.sh|ops/aws/sweep-teardown-blockers.sh +eks-prepare-destroy.sh|ops/aws/eks-prepare-destroy.sh +eks-recycle-bootstrap-nodes.sh|ops/aws/eks-recycle-bootstrap-nodes.sh +gcp-adopt-workforce-pool.sh|ops/gcp/adopt-workforce-pool.sh +gcp-purge-dns-records.sh|ops/gcp/purge-dns-records.sh +gcp-sweep-orphaned-disks.sh|ops/gcp/sweep-orphaned-disks.sh +k8s-reclaim-csi-volumes.sh|ops/k8s/reclaim-csi-volumes.sh +cnpg-prepare-restore.sh|ops/k8s/cnpg-prepare-restore.sh +cnpg-promote-seed.sh|ops/k8s/cnpg-promote-seed.sh +demo-load.sh|ops/demo/load.sh +cleanup-benchmark-images.sh|ops/demo/cleanup-benchmark-images.sh +EOF +RE='scripts/(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|eks-prepare-destroy|eks-recycle-bootstrap-nodes|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|cnpg-prepare-restore|cnpg-promote-seed|demo-load|cleanup-benchmark-images)\.sh' +TARGETS=$(git grep -l -E "$RE" -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') +printf '%s\n' "$TARGETS" | wc -l # expect 18 +while IFS='|' read -r old new; do + # shellcheck disable=SC2086 + sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS +done < /tmp/moved-pr2-ops.txt +``` + +No new path contains an old `scripts/` substring, so the loop is idempotent. + +- [ ] **Step 5: Fix the bare-name mentions the path rewrite cannot reach** + +Seven files were renamed, and 12 mentions name them without a `scripts/` prefix. Prose, usage +strings and error messages would name a file that no longer exists: + +```bash +git grep -n -E '(^|[^/a-z-])(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|demo-load)\.sh' \ + -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' +``` + +Before Step 4 this returned 12 lines. Change each remaining hit to the new name, or to the new +path where the text names a location. A script's own `Usage:` line should give its new path from +the repo root, e.g. `scripts/ops/gcp/purge-dns-records.sh`. Re-run the grep; expected: no output. + +Also update the `scripts/AGENTS.md` `## The rest` row naming `eks-prepare-destroy.sh` to +`ops/aws/eks-prepare-destroy.sh`. + +- [ ] **Step 6: Verify** + +```bash +bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 +bash scripts/ci/tests/test-script-paths.sh | tail -1 +for f in scripts/ops/*/*.sh; do bash -n "$f" || echo "SYNTAX: $f"; done +test -x scripts/ops/aws/../k8s/reclaim-csi-volumes.sh && echo "eks->k8s call resolves" +task ci:test | tail -1 +task ci:links; task ci:doc-paths +git diff --name-only HEAD -- opentofu | grep -E '\.(tf|tm\.hcl|tfvars)$' +(cd opentofu && terramate fmt --check && terramate list >/dev/null && echo "terramate ok") +for f in $(git ls-files -s | awk '$1=="120000"{print $4}'); do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done +``` +Expected: +- the terramate gate: `87 … checked; 0 failed`; +- the paths gate: `0 failed`. It checks the three `lib/` sources; +- no `SYNTAX:` line; +- `eks->k8s call resolves`; +- `23 passed, 1 skipped, 0 failed`. `test-cnpg-promote-seed` passes against its new subject; +- both doc gates pass; +- the `git diff` lists only `.tm.hcl`/`.tf` files whose diff is a `scripts/` → `scripts/` + substitution. Inspect each with `git diff -- ` and confirm no `${path.module}` line changed; +- `terramate ok`; +- no `NOT A SYMLINK` line. + +- [ ] **Step 7: Commit** + +```bash +git add -A +git commit -m "refactor(scripts): move the day-2 operations scripts to scripts/ops/ + +aws/, gcp/, k8s/, demo/. The cloud prefix drops where it repeats the +directory. eks-prepare-destroy.sh's call to reclaim-csi-volumes.sh +crosses directories now and is corrected by hand: no gate sees a path +passed to exec." +``` + +--- + +### Task 5: Move the teardown scripts into `scripts/ops/teardown/` + +Three files, 30 reference lines across 21 files, and **22 of those lines are in `.tm.hcl`**. This +is the destroy path: `terramate script run --reverse destroy` calls these from every stack. The task +is kept separate from Task 4 because a reviewer can reject one while approving the other. + +**Files:** +- Move: `scripts/{destroy-stage2.sh,tofu-destroy-contained.sh,terramate-destroy-confirm.sh}` → + `scripts/ops/teardown/` +- Modify: every file Step 3 selects + +**Interfaces:** +- Consumes: Task 1's gate. Produces: `scripts/ops/teardown/`, which Task 7 documents but does not + index. + +- [ ] **Step 1: Move, and watch the gate fail** + +```bash +mkdir -p scripts/ops/teardown +git mv scripts/destroy-stage2.sh scripts/tofu-destroy-contained.sh \ + scripts/terramate-destroy-confirm.sh scripts/ops/teardown/ +bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 +``` +Expected: `87 … checked; 22 failed`. + +- [ ] **Step 2: Check for internal paths** + +```bash +grep -nE 'BASH_SOURCE|dirname|source |^\s*\. |\.\./|scripts/' scripts/ops/teardown/*.sh +``` +The inventory found no self-resolved path in these three. If this grep shows one, correct its depth +as in Task 4 Step 3, and say so in the report. + +- [ ] **Step 3: Rewrite the references** + +```bash +cat > /tmp/moved-pr2-teardown.txt <<'EOF' +destroy-stage2.sh|ops/teardown/destroy-stage2.sh +tofu-destroy-contained.sh|ops/teardown/tofu-destroy-contained.sh +terramate-destroy-confirm.sh|ops/teardown/terramate-destroy-confirm.sh +EOF +TARGETS=$(git grep -l -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm)\.sh' \ + -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') +printf '%s\n' "$TARGETS" | wc -l # expect 21 +while IFS='|' read -r old new; do + # shellcheck disable=SC2086 + sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS +done < /tmp/moved-pr2-teardown.txt +``` + +- [ ] **Step 4: Verify** + +Run the same block as Task 4 Step 6, plus: + +```bash +for f in scripts/ops/teardown/*.sh; do bash -n "$f" || echo "SYNTAX: $f"; done +git grep -n -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm)\.sh' \ + -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' +``` +Expected: +- the terramate gate: `87 … checked; 0 failed`; +- `terramate ok`; +- no `SYNTAX:` line; +- the final `git grep`: no output. + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "refactor(scripts): move the teardown scripts to scripts/ops/teardown/ + +22 of their 30 references are terramate destroy calls. The terramate +reference gate went from 22 failed to 0." +``` + +--- + +### Task 6: Move `check-rebased.sh` into `scripts/ci/` + +It is the pre-push hook that refuses a branch behind `origin/main`. `scripts/README.md` already +describes `ci/` as "the gates CI runs, and you before pushing", and this hook is the second half +of that sentence. The owner chose `ci/` on 2026-09-21. + +**Files:** +- Move: `scripts/check-rebased.sh` → `scripts/ci/check-rebased.sh` +- Modify: `.pre-commit-config.yaml:76` + +- [ ] **Step 1: Move and repoint** + +```bash +git mv scripts/check-rebased.sh scripts/ci/check-rebased.sh +sed -i --follow-symlinks 's|entry: scripts/check-rebased.sh|entry: scripts/ci/check-rebased.sh|' .pre-commit-config.yaml +git grep -n 'check-rebased\.sh' -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' +``` +Expected: every hit names `scripts/ci/check-rebased.sh`. Fix any that does not. The inventory found +no self-resolved path in the script. Confirm with +`grep -nE 'BASH_SOURCE|dirname|source ' scripts/ci/check-rebased.sh`. + +- [ ] **Step 2: Verify the hook still finds it** + +```bash +pre-commit run check-rebased --hook-stage pre-push --all-files; echo "exit=$?" +``` + +While this branch is based on unmerged PR 1, the hook correctly **refuses**: it reports the branch +behind `origin/main` and exits nonzero. Both outcomes prove the path resolves. The only failure is +pre-commit reporting the executable as not found. + +- [ ] **Step 3: Commit** + +```bash +git add -A +git commit -m "refactor(scripts): move check-rebased.sh to scripts/ci/" +``` + +--- + +### Task 7: Index `ops/` and `docs/` in `task --list`, and document the layout + +Spec criterion 2: `task --list` names every entry point with a one-line description. The +`ops/teardown/` scripts are not entry points: terramate destroy scripts call them. They are +documented but not indexed. + +**Files:** +- Create: `scripts/ops/tasks.yaml`, `scripts/docs/tasks.yaml` +- Modify: `taskfile.yaml`, `scripts/README.md` + +**Interfaces:** +- Consumes: every path from Tasks 3–5. +- Produces: `task ops::` and `task docs:`. Arguments pass through with `--`, e.g. + `task ops:gcp:purge-dns-records -- --help`. + +Both files keep PR 1's two properties. They are **not** named `taskfile.yaml`, because go-task stops +at the first taskfile it finds walking up, so a nested `taskfile.yaml` would shadow the root one. +And every command is `{{.TASKFILE_DIR}}`-relative, so tasks work from any directory. + +- [ ] **Step 1: Write `scripts/ops/tasks.yaml`** + +```yaml +version: "3" + +# ops/teardown/ is not indexed: terramate destroy scripts call it. Run +# `terramate script run --reverse destroy` instead. +tasks: + aws:sweep-orphaned-volumes: + desc: Delete EBS volumes the CSI driver created that nothing uses, after a cluster destroy + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-orphaned-volumes.sh {{.CLI_ARGS}}"] + aws:sweep-teardown-blockers: + desc: Clear the two things that reliably block tofu destroy on AWS + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-teardown-blockers.sh {{.CLI_ARGS}}"] + aws:eks-prepare-destroy: + desc: Prepare an EKS cluster for destruction — suspends Flux, deletes every PVC + cmds: ["{{.TASKFILE_DIR}}/aws/eks-prepare-destroy.sh {{.CLI_ARGS}}"] + aws:eks-recycle-bootstrap-nodes: + desc: Recycle EKS nodes whose ENIs predate Cilium + cmds: ["{{.TASKFILE_DIR}}/aws/eks-recycle-bootstrap-nodes.sh {{.CLI_ARGS}}"] + gcp:adopt-workforce-pool: + desc: Make the workforce pool survive a teardown and rebuild + cmds: ["{{.TASKFILE_DIR}}/gcp/adopt-workforce-pool.sh {{.CLI_ARGS}}"] + gcp:purge-dns-records: + desc: Empty a Cloud DNS managed zone so the zone can be destroyed + cmds: ["{{.TASKFILE_DIR}}/gcp/purge-dns-records.sh {{.CLI_ARGS}}"] + gcp:sweep-orphaned-disks: + desc: Delete Persistent Disks GKE's CSI driver created that nothing uses + cmds: ["{{.TASKFILE_DIR}}/gcp/sweep-orphaned-disks.sh {{.CLI_ARGS}}"] + k8s:reclaim-csi-volumes: + desc: Reclaim CSI-provisioned volumes before a cluster destroy (cloud-neutral) + cmds: ["{{.TASKFILE_DIR}}/k8s/reclaim-csi-volumes.sh {{.CLI_ARGS}}"] + k8s:cnpg-prepare-restore: + desc: Clear a CNPG cluster's live WAL archive so a new cluster can start (off the normal path) + cmds: ["{{.TASKFILE_DIR}}/k8s/cnpg-prepare-restore.sh {{.CLI_ARGS}}"] + k8s:cnpg-promote-seed: + desc: Promote a live CNPG archive to a frozen restore seed, or verify one + cmds: ["{{.TASKFILE_DIR}}/k8s/cnpg-promote-seed.sh {{.CLI_ARGS}}"] + demo:load: + desc: Run an image-gallery load-generator scenario in-cluster + cmds: ["{{.TASKFILE_DIR}}/demo/load.sh {{.CLI_ARGS}}"] + demo:cleanup-benchmark-images: + desc: Remove benchmark-generated images from the image-gallery database and bucket + cmds: ["{{.TASKFILE_DIR}}/demo/cleanup-benchmark-images.sh {{.CLI_ARGS}}"] +``` + +Check each `desc` against its script's header comment. Where the header says something the desc +contradicts, the header wins: fix the desc. + +- [ ] **Step 2: Write `scripts/docs/tasks.yaml`** + +```yaml +version: "3" + +# build-og-card.html is not a task: open it in a browser and screenshot it. +tasks: + export-diagrams: + desc: Regenerate every diagram SVG the site embeds from its .drawio source (needs the pinned drawio) + cmds: ["{{.TASKFILE_DIR}}/export-diagrams.sh {{.CLI_ARGS}}"] + diagram-icons: + desc: The icon library behind docs/architecture/*.drawio — `task docs:diagram-icons -- audit` + cmds: ["python3 {{.TASKFILE_DIR}}/diagram-icons.py {{.CLI_ARGS}}"] +``` + +- [ ] **Step 3: Include both from the root taskfile** + +In `taskfile.yaml`, add two entries beside the existing `ci:` include, in the same shape: + +```yaml + ops: + taskfile: scripts/ops/tasks.yaml + docs: + taskfile: scripts/docs/tasks.yaml +``` + +Leave `check` unchanged. No `ops:` or `docs:` task is a CI gate. + +- [ ] **Step 4: Update `scripts/README.md`** + +Replace the table and the closing sentence with: + +```markdown +| Directory | Audience | +|---|---| +| `ci/` | the gates CI runs, and you before pushing. `task check` runs every one CI runs | +| `ci/tests/` | suites `run.sh` discovers: `test-*.sh` and `test-*.py` here, `*/test-*.py` one level down. A `# requires:` tool that is absent, or an exit 77, reports `SKIP` | +| `ops/aws/`, `ops/gcp/`, `ops/k8s/` | day-2 operations, run by a human. Some are also called from terramate destroy scripts | +| `ops/teardown/` | called by `terramate script run --reverse destroy`, not run by hand | +| `ops/demo/` | demo load generation and cleanup | +| `docs/` | docs-site generators, run by hand. `build-og-card.html` opens in a browser | +| `lib/` | sourced by the others, never run directly | + +Apply-time provisioning scripts move to `provision/` in the next phase; until then they remain at +the root of `scripts/`. +``` + +- [ ] **Step 5: Verify** + +```bash +task --list | grep -cE '^\* (ops|docs):' +(cd scripts/ops/aws && task ops:gcp:purge-dns-records -- --help >/dev/null 2>&1; echo "from-subdir exit=$?") +task docs:diagram-icons -- audit 2>&1 | grep -E '^ +[0-9]+ (local|none)' +task ci:links +``` +Expected: +- `14`; +- the `--help` call: exit 0, or the script's own usage exit code. Confirm by reading its argument + parsing that `--help` only prints usage. If it does anything else, use a different read-only + proof and say which; +- `18 local` / `8 none`; +- links pass. + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "feat(task): index ops/ and docs/ in task --list + +Spec criterion 2: every entry point has a one-line description. +ops/teardown/ is documented but not indexed; terramate calls it." +``` + +--- + +### Task 8: Rebase onto `main`, full verification, and the PR + +The controller runs this inline. It only starts **after #2061 merges**. + +- [ ] **Step 1: Drop PR 1's commits and rebase** + +PR 1 squash-merges, so its 30 commits on this branch become one commit on `main` with a different +SHA. Rebase only this PR's own commits: + +```bash +git fetch origin +git rebase --onto origin/main 7ec01df5 +git log --oneline origin/main..HEAD # only this plan's commits +``` + +`7ec01df5` is PR 1's head when this branch was cut. If #2061 gained commits after 2026-09-21, use +its final head instead: `gh pr view 2061 --json headRefOid`. + +- [ ] **Step 2: Run the whole gate set, once, alone in this checkout** + +```bash +task check +bash scripts/ci/tests/test-terramate-script-refs.sh +``` +Expected: +- `task check` exits 0, with the rendered resource count, `23 passed, 1 skipped, 0 failed`, and all + six gates passing; +- the terramate gate: `87 … checked; 0 failed`. + +Cite both verbatim. + +- [ ] **Step 3: Prove the layout** + +```bash +ls scripts/ +``` +Expected: +- directories: `ci docs lib ops zitadel-actions`; +- files: `AGENTS.md CLAUDE.md README.md tasks.yaml`; +- eight files still at root: seven that PR 3 moves (`helm-release-present.sh openbao-adopt-jwt-mount.sh + openbao-config.sh secret-store.sh tm-provisioner.sh zitadel-idp.sh zitadel-oidc-clients.sh`), plus + the `openbao-snapshot.sh` symlink, whose home is unsettled. + +Criterion 1 ("no loose executables") is met only after PR 3. Say so in the PR body. + +- [ ] **Step 4: The rest of the evidence** + +```bash +./scripts/ci/validate-vmrules.sh +(cd opentofu && terramate fmt --check && terramate list | wc -l) +shellcheck -x -S warning $(find ./scripts \( -type f -o -type l \) -name "*.sh") +for f in $(git ls-files -s | awk '$1=="120000"{print $4}'); do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done +``` +Expected: each exits 0, and no `NOT A SYMLINK` line. + +- [ ] **Step 5: The owner-run preview (a merge gate, not a CI gate)** + +`terramate script run preview` needs a live cluster, because Helm reads the endpoint at plan time. +The owner decided on 2026-09-21 that the PR may open on the static gate, and that the preview runs +before merge, whenever a cluster is next up: + +```bash +cd opentofu +TM_CLOUD=aws terramate script run preview +TM_CLOUD=gcp terramate script run preview +``` + +The PR body carries this as an unchecked merge checkbox. **Do not merge with it unchecked.** + +- [ ] **Step 6: Open the PR** + +Follow `.agents/skills/create-pr/SKILL.md`. The body must carry: +- links to the design, PR 1's plan, and this plan; +- a mermaid diagram of before/after; +- the cited output from Steps 2–4; +- the preview merge checkbox from Step 5; +- a statement that PR 3 (`provision/`) is still pending; +- Task 2's finding about any capability that left with `aws-sweep-controller-orphans.sh`. + +--- + +## Self-Review + +- **Spec coverage.** The design's PR 2 row names `ops/` and `docs/`: Tasks 3–5. Its evidence column + names a preview on both clouds: Task 8 Step 5, as a merge gate with the owner's decision, backed + in CI by Task 1. Its Deletions table: Task 2. Criterion 2: Task 7. `check-rebased.sh` post-dates + the design; Task 6 carries the owner's placement. +- **Measured, not copied.** The design says PR 2 has 77 live references. Measured: 147 matches + across the 18 moving files, 123 excluding a script's mention of itself. Each task's rewrite + expects its own measured count (9, 18, 21 files), not the design's. +- **Exact values are consistent across tasks:** 87 references checked, floor 80; failure counts + 8 (Task 4) and 22 (Task 5), both measured by simulating the move; suite count 23. From 408853b24caa7abf4a199501e31aeb64efcf30f2 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 21:55:27 +0200 Subject: [PATCH 02/19] docs(superpowers): correct two PR 2 plan counts for the Task 2 deletion --- .../plans/2026-09-21-scripts-restructure-pr2-plan.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md index 24d8fb718..64dbdadca 100644 --- a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md +++ b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md @@ -426,7 +426,8 @@ bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 bash scripts/ci/tests/test-script-paths.sh | tail -1 ``` Expected: `87 … checked; 8 failed` (the executed references, which Step 4 fixes), then -`11 roots, 21 sources, 16 subjects checked; 4 failed`: the three `lib/` sources and +`10 roots, 21 sources, 16 subjects checked; 4 failed` (10, not 11: Task 2 deleted `teardown.sh`, +which was one of the roots): the three `lib/` sources and `test-cnpg-promote-seed.sh`'s subject, all fixed in Step 3. Both counts were measured by simulating this move. A different number means the measurement is stale: report it. @@ -437,7 +438,7 @@ this move. A different number means the measurement is stale: report it. | `scripts/ops/gcp/sweep-orphaned-disks.sh:53` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | | `scripts/ops/gcp/purge-dns-records.sh:38` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | | `scripts/ops/k8s/cnpg-prepare-restore.sh:60` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | -| `scripts/ops/aws/eks-prepare-destroy.sh:115` | `"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" \|\| true` | `"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" \|\| true` | +| `scripts/ops/aws/eks-prepare-destroy.sh` (~`:115`; Task 2's comment edit shifts it, so find it by content) | `"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" \|\| true` | `"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" \|\| true` | The last row is the one no gate sees. Both files move into *different* directories, and the target is renamed. It is a path passed to `exec`, not a `source`, so `test-script-paths.sh` cannot check @@ -469,7 +470,7 @@ cleanup-benchmark-images.sh|ops/demo/cleanup-benchmark-images.sh EOF RE='scripts/(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|eks-prepare-destroy|eks-recycle-bootstrap-nodes|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|cnpg-prepare-restore|cnpg-promote-seed|demo-load|cleanup-benchmark-images)\.sh' TARGETS=$(git grep -l -E "$RE" -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') -printf '%s\n' "$TARGETS" | wc -l # expect 18 +printf '%s\n' "$TARGETS" | wc -l # expect 17 (18 before Task 2 deleted teardown.sh) while IFS='|' read -r old new; do # shellcheck disable=SC2086 sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS From c9a14d63995889eacc1e2a6801d81d08f1a73779 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 21:58:45 +0200 Subject: [PATCH 03/19] test(ci): gate every script path opentofu and terramate execute 87 references on executed lines, 30 of them to scripts this PR moves. They run at apply and destroy time and no CI job executes them; neither test-script-paths.sh nor verify-doc-paths.sh reads a .tf or .tm.hcl. --- scripts/AGENTS.md | 1 + .../ci/tests/test-terramate-script-refs.sh | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 scripts/ci/tests/test-terramate-script-refs.sh diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 3e9d980ba..aa6b6da34 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -96,6 +96,7 @@ suite**; if a change stops that being true, it has regressed to testing nothing. | `verify-doc-paths.sh` | every **backticked** repository path in `website/content/` still exists. Runs in the website workflow, not in `validate-manifests.sh`, and catches what `validate-links.sh` cannot: a path in prose, or one linked by absolute GitHub URL. No allowlist by design — fix the path or drop the reference | | `validate-doc-claims.sh` | docs still agree with config, per `.doc-claims.yaml` | | `validate-idp-topology.sh` | exactly one cloud hosts ZITADEL (ADR-0027) | +| `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl` line exists — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | | `diagram-icons.py audit` | boxes naming a product that render without an icon. Advisory, never a CI gate | | `export-diagrams.sh` | regenerates every SVG the site embeds. Pins the drawio version on purpose | | `eks-prepare-destroy.sh` | **deletes every PVC.** Must carry a cloud gate | diff --git a/scripts/ci/tests/test-terramate-script-refs.sh b/scripts/ci/tests/test-terramate-script-refs.sh new file mode 100755 index 000000000..7d411dab3 --- /dev/null +++ b/scripts/ci/tests/test-terramate-script-refs.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Every script path that opentofu and terramate name on an executed line must exist. +# +# These run at apply and destroy time, and no CI job executes them. A move that +# misses one fails mid-destroy with a bare "No such file". test-script-paths.sh +# checks paths computed *inside* scripts; verify-doc-paths.sh reads only the docs +# site. Neither reads *.tm.hcl or *.tf, which is where these live. +# +# Comment lines are skipped: a stale comment misleads, but it cannot break a run. +# `echo` hints are checked — an operator copies them during a failed destroy. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="${TM_REFS_ROOT:-$(cd "$HERE/../../.." && pwd)}" +# Measured at the commit that added this gate. A count below it means the +# extraction broke, not that references went away; fail rather than pass over less. +FLOOR="${TM_REFS_FLOOR:-80}" + +checked=0 failed=0 +while IFS= read -r hit; do + file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; text="${rest#*:}" + [[ "$text" =~ ^[[:space:]]*(#|//) ]] && continue + while IFS= read -r ref; do + [ -n "$ref" ] || continue + if [[ "$ref" == '${path.module}/'* ]]; then + target="$ROOT/$(dirname "$file")/${ref#'${path.module}/'}" + else + target="$ROOT/scripts/${ref#*scripts/}" + fi + checked=$((checked + 1)) + if [ ! -e "$target" ]; then + printf 'FAIL %s:%s %s\n resolved to %s\n' "$file" "$line" "$ref" "${target#"$ROOT"/}" + failed=$((failed + 1)) + fi + done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' <<<"$text" \ + | sed -E 's/^[[:space:]"(]//') +done < <(cd "$ROOT" && grep -rnE --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ + 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' opentofu 2>/dev/null) + +if [ "$checked" -lt "$FLOOR" ]; then + echo "FAIL checked $checked script reference(s), floor is $FLOOR: the extraction broke, not the references" + exit 1 +fi +echo "$checked script reference(s) on executed opentofu/terramate lines checked; $failed failed" +[ "$failed" -eq 0 ] From d4d710c1d5f61eb60c965fcf9f0a338d54269aa9 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:01:30 +0200 Subject: [PATCH 04/19] docs(superpowers): keep teardown.sh and its orphan sweep, move them to ops/ The design listed both for deletion as having no caller. A human is the caller: teardown.sh is the supported teardown path (#1970, #1976) and the sweep is its recovery step. Owner decision, 2026-09-21. --- ...2026-09-21-scripts-restructure-pr2-plan.md | 182 ++++++++---------- 1 file changed, 83 insertions(+), 99 deletions(-) diff --git a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md index 64dbdadca..83e5cf074 100644 --- a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md +++ b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md @@ -3,8 +3,8 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Move the day-2 operations scripts into `scripts/ops//` and the docs-site generators -into `scripts/docs/`, delete the two dead scripts the design names, and move `check-rebased.sh` to -`scripts/ci/`. All of it happens under a new gate that proves every script path opentofu and +into `scripts/docs/`, and move `check-rebased.sh` to `scripts/ci/`. `teardown.sh` and its orphan sweep +move too: the design listed them for deletion, but they are live human tools (see Task 2). All of it happens under a new gate that proves every script path opentofu and terramate execute still resolves. **Architecture:** This PR has the same shape as PR 1: the gate ships first, on the unmoved tree, @@ -66,6 +66,7 @@ the reference-rewrite recipe this plan reuses. | `export-diagrams.sh`, `diagram-icons.py`, `build-og-card.html` | `docs/` (names unchanged) | 3 | | `aws-sweep-orphaned-volumes.sh` | `ops/aws/sweep-orphaned-volumes.sh` | 4 | | `aws-sweep-teardown-blockers.sh` | `ops/aws/sweep-teardown-blockers.sh` | 4 | +| `aws-sweep-controller-orphans.sh` | `ops/aws/sweep-controller-orphans.sh` | 4 | | `eks-prepare-destroy.sh`, `eks-recycle-bootstrap-nodes.sh` | `ops/aws/` (names unchanged) | 4 | | `gcp-adopt-workforce-pool.sh` | `ops/gcp/adopt-workforce-pool.sh` | 4 | | `gcp-purge-dns-records.sh` | `ops/gcp/purge-dns-records.sh` | 4 | @@ -75,10 +76,11 @@ the reference-rewrite recipe this plan reuses. | `demo-load.sh` | `ops/demo/load.sh` | 4 | | `cleanup-benchmark-images.sh` | `ops/demo/` (name unchanged) | 4 | | `destroy-stage2.sh`, `tofu-destroy-contained.sh`, `terramate-destroy-confirm.sh` | `ops/teardown/` (names unchanged) | 5 | +| `teardown.sh` | `ops/teardown/teardown.sh` | 5 | | `check-rebased.sh` | `ci/check-rebased.sh` | 6 | -**Deleted:** `scripts/teardown.sh`, `scripts/aws-sweep-controller-orphans.sh` (Task 2). The design's -Deletions table covers both; neither has an external caller. +**Deleted:** nothing. The design's Deletions table listed `teardown.sh` and +`aws-sweep-controller-orphans.sh`; the owner overruled it on 2026-09-21 (Task 2). **Modified:** `taskfile.yaml`, `.pre-commit-config.yaml`, `scripts/README.md`, `scripts/AGENTS.md`, `scripts/ci/tests/test-cnpg-promote-seed.sh`, plus every file `git grep -l` selects in Tasks 3–6. @@ -225,71 +227,21 @@ test-script-paths.sh nor verify-doc-paths.sh reads a .tf or .tm.hcl." --- -### Task 2: Delete the two dead scripts +### Task 2: Dropped — `teardown.sh` and its orphan sweep are kept -`teardown.sh` calls itself "the supported way to tear the platform down", but nothing calls it. The -supported path is `terramate script run --reverse destroy`, whose confirmation step is -`terramate-destroy-confirm.sh`. `aws-sweep-controller-orphans.sh` has exactly one caller, and that -caller is `teardown.sh`. +The design's Deletions table listed both as "no caller in CI, opentofu, manifests or docs". That is +true, and it misses the point: **a human is the caller.** -**Files:** -- Delete: `scripts/teardown.sh`, `scripts/aws-sweep-controller-orphans.sh` -- Modify: `scripts/eks-prepare-destroy.sh` (the comment block around line 30 that names - `aws-sweep-controller-orphans.sh`) - -**Interfaces:** -- Consumes: nothing. Produces: a smaller rename surface for Tasks 4–5. - -- [ ] **Step 1: Confirm there is no caller** - -```bash -git grep -n -E 'teardown\.sh|aws-sweep-controller-orphans' \ - -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' -``` -Expected: only lines inside the two files themselves, plus `scripts/eks-prepare-destroy.sh:30`, a -comment that mentions the sweep but does not invoke it. Any other hit is a caller: **stop and report -it** rather than deleting. - -- [ ] **Step 2: Confirm nothing is lost that the destroy path needs** +- `teardown.sh` calls itself "the supported way to tear the platform down" (#1970, #1976). It exists + because bare `terramate script run --reverse destroy` stops at the first failing stack and can + report success having destroyed nothing. It continues past failures, sweeps what controllers left, + retries, then verifies against the cloud. +- `aws-sweep-controller-orphans.sh` is that sweep. It exists for the teardown that fails partway, + when `eks/init`'s own destroy-time sweeps can no longer run. -Read the header of `scripts/aws-sweep-controller-orphans.sh` and list what it sweeps. Then read -`scripts/aws-sweep-teardown-blockers.sh` and `scripts/eks-prepare-destroy.sh`, which the terramate -destroy path does call, and say in the report whether they cover each item. - -This step decides nothing on its own. The deletion is the design's call. Its job is to put in -front of the reviewer any capability that disappears with the file. If an item is not covered, say -so plainly in the report and the commit body. - -- [ ] **Step 3: Delete, and fix the comment that names a deleted file** - -```bash -git rm scripts/teardown.sh scripts/aws-sweep-controller-orphans.sh -``` - -Rewrite the `eks-prepare-destroy.sh` comment near line 30 ("WHY NOT IN eks-prepare-destroy.sh") so -it no longer names a script that does not exist. Keep the *why* if it still holds. If it only made -sense next to the deleted file, delete the comment too. - -- [ ] **Step 4: Verify** - -```bash -bash -n scripts/eks-prepare-destroy.sh; echo "syntax=$?" -bash scripts/ci/tests/test-no-secret-argv.sh >/dev/null; echo "argv=$?" -task ci:test | tail -1 -``` -Expected: `syntax=0`, `argv=0`, `23 passed, 1 skipped, 0 failed`. - -- [ ] **Step 5: Commit** - -```bash -git add -A scripts/ -git commit -m "chore(scripts): delete teardown.sh and the sweep only it called - -Neither has a caller: terramate script run --reverse destroy is the -teardown path. Named in the design's Deletions table." -``` - -If Step 2 found an uncovered capability, add one line to the commit body naming it. +The owner decided on 2026-09-21 to keep both. They move with their audience instead: +`aws-sweep-controller-orphans.sh` in Task 4, `teardown.sh` in Task 5. Both are indexed in Task 7. +Nothing is deleted in this PR. --- @@ -388,12 +340,12 @@ git commit -m "refactor(scripts): move the docs-site generators to scripts/docs/ ### Task 4: Move the day-2 operations scripts into `scripts/ops/{aws,gcp,k8s,demo}/` -12 files, 43 reference lines across 18 files. 11 of those lines are in `.tf`/`.tm.hcl`, and 8 of +13 files, 44 reference lines across 18 files. 11 of those lines are in `.tf`/`.tm.hcl`, and 8 of those 11 are executed; the other 3 are comments. Seven files also change name, so bare-name mentions go stale as well as paths. **Files:** -- Move: the 12 files in the File Structure table marked Task 4 +- Move: the 13 files in the File Structure table marked Task 4 - Modify: four internal paths (Step 3), `scripts/ci/tests/test-cnpg-promote-seed.sh:25-26`, and every file Step 4 selects @@ -407,6 +359,7 @@ files also change name, so bare-name mentions go stale as well as paths. mkdir -p scripts/ops/{aws,gcp,k8s,demo} git mv scripts/aws-sweep-orphaned-volumes.sh scripts/ops/aws/sweep-orphaned-volumes.sh git mv scripts/aws-sweep-teardown-blockers.sh scripts/ops/aws/sweep-teardown-blockers.sh +git mv scripts/aws-sweep-controller-orphans.sh scripts/ops/aws/sweep-controller-orphans.sh git mv scripts/eks-prepare-destroy.sh scripts/ops/aws/eks-prepare-destroy.sh git mv scripts/eks-recycle-bootstrap-nodes.sh scripts/ops/aws/eks-recycle-bootstrap-nodes.sh git mv scripts/gcp-adopt-workforce-pool.sh scripts/ops/gcp/adopt-workforce-pool.sh @@ -426,8 +379,7 @@ bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 bash scripts/ci/tests/test-script-paths.sh | tail -1 ``` Expected: `87 … checked; 8 failed` (the executed references, which Step 4 fixes), then -`10 roots, 21 sources, 16 subjects checked; 4 failed` (10, not 11: Task 2 deleted `teardown.sh`, -which was one of the roots): the three `lib/` sources and +`11 roots, 21 sources, 16 subjects checked; 4 failed`: the three `lib/` sources and `test-cnpg-promote-seed.sh`'s subject, all fixed in Step 3. Both counts were measured by simulating this move. A different number means the measurement is stale: report it. @@ -438,7 +390,7 @@ this move. A different number means the measurement is stale: report it. | `scripts/ops/gcp/sweep-orphaned-disks.sh:53` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | | `scripts/ops/gcp/purge-dns-records.sh:38` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | | `scripts/ops/k8s/cnpg-prepare-restore.sh:60` | `. "$(dirname "$0")/lib/gcloud-adc.sh"` | `. "$(dirname "$0")/../../lib/gcloud-adc.sh"` | -| `scripts/ops/aws/eks-prepare-destroy.sh` (~`:115`; Task 2's comment edit shifts it, so find it by content) | `"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" \|\| true` | `"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" \|\| true` | +| `scripts/ops/aws/eks-prepare-destroy.sh` (~`:115`; find it by content) | `"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" \|\| true` | `"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" \|\| true` | The last row is the one no gate sees. Both files move into *different* directories, and the target is renamed. It is a path passed to `exec`, not a `source`, so `test-script-paths.sh` cannot check @@ -457,6 +409,7 @@ SCRIPT="$HERE/../../ops/k8s/cnpg-promote-seed.sh" cat > /tmp/moved-pr2-ops.txt <<'EOF' aws-sweep-orphaned-volumes.sh|ops/aws/sweep-orphaned-volumes.sh aws-sweep-teardown-blockers.sh|ops/aws/sweep-teardown-blockers.sh +aws-sweep-controller-orphans.sh|ops/aws/sweep-controller-orphans.sh eks-prepare-destroy.sh|ops/aws/eks-prepare-destroy.sh eks-recycle-bootstrap-nodes.sh|ops/aws/eks-recycle-bootstrap-nodes.sh gcp-adopt-workforce-pool.sh|ops/gcp/adopt-workforce-pool.sh @@ -468,9 +421,9 @@ cnpg-promote-seed.sh|ops/k8s/cnpg-promote-seed.sh demo-load.sh|ops/demo/load.sh cleanup-benchmark-images.sh|ops/demo/cleanup-benchmark-images.sh EOF -RE='scripts/(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|eks-prepare-destroy|eks-recycle-bootstrap-nodes|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|cnpg-prepare-restore|cnpg-promote-seed|demo-load|cleanup-benchmark-images)\.sh' +RE='scripts/(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|aws-sweep-controller-orphans|eks-prepare-destroy|eks-recycle-bootstrap-nodes|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|cnpg-prepare-restore|cnpg-promote-seed|demo-load|cleanup-benchmark-images)\.sh' TARGETS=$(git grep -l -E "$RE" -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') -printf '%s\n' "$TARGETS" | wc -l # expect 17 (18 before Task 2 deleted teardown.sh) +printf '%s\n' "$TARGETS" | wc -l # expect 18 while IFS='|' read -r old new; do # shellcheck disable=SC2086 sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS @@ -481,15 +434,15 @@ No new path contains an old `scripts/` substring, so the loop is idempoten - [ ] **Step 5: Fix the bare-name mentions the path rewrite cannot reach** -Seven files were renamed, and 12 mentions name them without a `scripts/` prefix. Prose, usage +Eight files were renamed, and 13 mentions name them without a `scripts/` prefix. Prose, usage strings and error messages would name a file that no longer exists: ```bash -git grep -n -E '(^|[^/a-z-])(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|demo-load)\.sh' \ +git grep -n -E '(^|[^/a-z-])(aws-sweep-orphaned-volumes|aws-sweep-teardown-blockers|aws-sweep-controller-orphans|gcp-adopt-workforce-pool|gcp-purge-dns-records|gcp-sweep-orphaned-disks|k8s-reclaim-csi-volumes|demo-load)\.sh' \ -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' ``` -Before Step 4 this returned 12 lines. Change each remaining hit to the new name, or to the new +Before Step 4 this returned 13 lines. Change each remaining hit to the new name, or to the new path where the text names a location. A script's own `Usage:` line should give its new path from the repo root, e.g. `scripts/ops/gcp/purge-dns-records.sh`. Re-run the grep; expected: no output. @@ -537,18 +490,19 @@ passed to exec." ### Task 5: Move the teardown scripts into `scripts/ops/teardown/` -Three files, 30 reference lines across 21 files, and **22 of those lines are in `.tm.hcl`**. This +Four files, 34 reference lines across 22 files, and **22 of those lines are in `.tm.hcl`**. This is the destroy path: `terramate script run --reverse destroy` calls these from every stack. The task is kept separate from Task 4 because a reviewer can reject one while approving the other. **Files:** - Move: `scripts/{destroy-stage2.sh,tofu-destroy-contained.sh,terramate-destroy-confirm.sh}` → - `scripts/ops/teardown/` + `scripts/ops/teardown/`, and `scripts/teardown.sh` → `scripts/ops/teardown/teardown.sh` +- Modify: `teardown.sh`'s root depth (line 38) - Modify: every file Step 3 selects **Interfaces:** -- Consumes: Task 1's gate. Produces: `scripts/ops/teardown/`, which Task 7 documents but does not - index. +- Consumes: Task 1's gate. Produces: `scripts/ops/teardown/`. Task 7 indexes `teardown.sh` and + documents the other three. - [ ] **Step 1: Move, and watch the gate fail** @@ -556,16 +510,28 @@ is kept separate from Task 4 because a reviewer can reject one while approving t mkdir -p scripts/ops/teardown git mv scripts/destroy-stage2.sh scripts/tofu-destroy-contained.sh \ scripts/terramate-destroy-confirm.sh scripts/ops/teardown/ +git mv scripts/teardown.sh scripts/ops/teardown/teardown.sh bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 +bash scripts/ci/tests/test-script-paths.sh | tail -1 ``` -Expected: `87 … checked; 22 failed`. +Expected: `87 … checked; 22 failed`, then `11 roots, 21 sources, 16 subjects checked; 1 failed`. That +one is `teardown.sh:38`, which resolves to `scripts/ops` instead of the repo root. Both counts were +measured by simulating this move. + +- [ ] **Step 2: Correct `teardown.sh`'s depth, and check the other three** + +| File:line | From | To | +|---|---|---| +| `scripts/ops/teardown/teardown.sh:38` | `ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` | `ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"` | -- [ ] **Step 2: Check for internal paths** +Its `${ROOT}/scripts/aws-sweep-*.sh` calls were already rewritten to `${ROOT}/scripts/ops/aws/…` by +Task 4, since the file was still at `scripts/` root then. With `ROOT` corrected they resolve +unchanged. ```bash -grep -nE 'BASH_SOURCE|dirname|source |^\s*\. |\.\./|scripts/' scripts/ops/teardown/*.sh +grep -nE 'BASH_SOURCE|dirname|source |^\s*\. |\.\./' scripts/ops/teardown/{destroy-stage2,tofu-destroy-contained,terramate-destroy-confirm}.sh ``` -The inventory found no self-resolved path in these three. If this grep shows one, correct its depth +The inventory found no self-resolved path in those three. If this grep shows one, correct its depth as in Task 4 Step 3, and say so in the report. - [ ] **Step 3: Rewrite the references** @@ -575,10 +541,11 @@ cat > /tmp/moved-pr2-teardown.txt <<'EOF' destroy-stage2.sh|ops/teardown/destroy-stage2.sh tofu-destroy-contained.sh|ops/teardown/tofu-destroy-contained.sh terramate-destroy-confirm.sh|ops/teardown/terramate-destroy-confirm.sh +teardown.sh|ops/teardown/teardown.sh EOF -TARGETS=$(git grep -l -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm)\.sh' \ +TARGETS=$(git grep -l -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm|teardown)\.sh' \ -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs') -printf '%s\n' "$TARGETS" | wc -l # expect 21 +printf '%s\n' "$TARGETS" | wc -l # expect 22 while IFS='|' read -r old new; do # shellcheck disable=SC2086 sed -i --follow-symlinks "s|scripts/${old}|scripts/${new}|g" $TARGETS @@ -591,22 +558,31 @@ Run the same block as Task 4 Step 6, plus: ```bash for f in scripts/ops/teardown/*.sh; do bash -n "$f" || echo "SYNTAX: $f"; done -git grep -n -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm)\.sh' \ +bash scripts/ci/tests/test-script-paths.sh | tail -1 +for s in sweep-teardown-blockers sweep-controller-orphans sweep-orphaned-volumes; do + test -e "scripts/ops/teardown/../../../scripts/ops/aws/$s.sh" && echo "teardown -> $s ok"; done +git grep -n -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy-confirm|teardown)\.sh' \ -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' ``` Expected: - the terramate gate: `87 … checked; 0 failed`; - `terramate ok`; - no `SYNTAX:` line; +- the paths gate: `11 roots, … 0 failed`; +- three `teardown -> … ok` lines: `teardown.sh`'s corrected `ROOT` still reaches the sweeps; - the final `git grep`: no output. +The `scripts/teardown.sh` → `scripts/ops/teardown/teardown.sh` substitution is idempotent: the new +path does not contain the substring `scripts/teardown.sh`. + - [ ] **Step 5: Commit** ```bash git add -A git commit -m "refactor(scripts): move the teardown scripts to scripts/ops/teardown/ -22 of their 30 references are terramate destroy calls. The terramate +teardown.sh is the supported teardown entry point; the other three are +what terramate destroy calls (22 of the 34 references). The terramate reference gate went from 22 failed to 0." ``` @@ -654,9 +630,9 @@ git commit -m "refactor(scripts): move check-rebased.sh to scripts/ci/" ### Task 7: Index `ops/` and `docs/` in `task --list`, and document the layout -Spec criterion 2: `task --list` names every entry point with a one-line description. The -`ops/teardown/` scripts are not entry points: terramate destroy scripts call them. They are -documented but not indexed. +Spec criterion 2: `task --list` names every entry point with a one-line description. In +`ops/teardown/`, only `teardown.sh` is an entry point. Terramate destroy scripts call the other three, +so they are documented but not indexed. **Files:** - Create: `scripts/ops/tasks.yaml`, `scripts/docs/tasks.yaml` @@ -676,15 +652,21 @@ And every command is `{{.TASKFILE_DIR}}`-relative, so tasks work from any direct ```yaml version: "3" -# ops/teardown/ is not indexed: terramate destroy scripts call it. Run -# `terramate script run --reverse destroy` instead. +# The three helpers beside teardown.sh are not indexed: terramate destroy +# scripts call them. teardown.sh is the entry point. tasks: + teardown: + desc: Tear the platform down — destroy past failures, sweep controller orphans, retry, verify against the cloud + cmds: ["{{.TASKFILE_DIR}}/teardown/teardown.sh {{.CLI_ARGS}}"] aws:sweep-orphaned-volumes: desc: Delete EBS volumes the CSI driver created that nothing uses, after a cluster destroy cmds: ["{{.TASKFILE_DIR}}/aws/sweep-orphaned-volumes.sh {{.CLI_ARGS}}"] aws:sweep-teardown-blockers: desc: Clear the two things that reliably block tofu destroy on AWS cmds: ["{{.TASKFILE_DIR}}/aws/sweep-teardown-blockers.sh {{.CLI_ARGS}}"] + aws:sweep-controller-orphans: + desc: Sweep what in-cluster controllers left in AWS after a partial teardown (refuses while the cluster exists) + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-controller-orphans.sh {{.CLI_ARGS}}"] aws:eks-prepare-destroy: desc: Prepare an EKS cluster for destruction — suspends Flux, deletes every PVC cmds: ["{{.TASKFILE_DIR}}/aws/eks-prepare-destroy.sh {{.CLI_ARGS}}"] @@ -758,7 +740,7 @@ Replace the table and the closing sentence with: | `ci/` | the gates CI runs, and you before pushing. `task check` runs every one CI runs | | `ci/tests/` | suites `run.sh` discovers: `test-*.sh` and `test-*.py` here, `*/test-*.py` one level down. A `# requires:` tool that is absent, or an exit 77, reports `SKIP` | | `ops/aws/`, `ops/gcp/`, `ops/k8s/` | day-2 operations, run by a human. Some are also called from terramate destroy scripts | -| `ops/teardown/` | called by `terramate script run --reverse destroy`, not run by hand | +| `ops/teardown/` | `teardown.sh` is the supported way to tear the platform down (`task ops:teardown`). The other three are called by terramate destroy scripts | | `ops/demo/` | demo load generation and cleanup | | `docs/` | docs-site generators, run by hand. `build-og-card.html` opens in a browser | | `lib/` | sourced by the others, never run directly | @@ -776,7 +758,7 @@ task docs:diagram-icons -- audit 2>&1 | grep -E '^ +[0-9]+ (local|none)' task ci:links ``` Expected: -- `14`; +- `16`; - the `--help` call: exit 0, or the script's own usage exit code. Confirm by reading its argument parsing that `--help` only prints usage. If it does anything else, use a different read-only proof and say which; @@ -790,7 +772,7 @@ git add -A git commit -m "feat(task): index ops/ and docs/ in task --list Spec criterion 2: every entry point has a one-line description. -ops/teardown/ is documented but not indexed; terramate calls it." +Only teardown.sh is indexed in ops/teardown/; terramate calls the rest." ``` --- @@ -872,7 +854,8 @@ Follow `.agents/skills/create-pr/SKILL.md`. The body must carry: - the cited output from Steps 2–4; - the preview merge checkbox from Step 5; - a statement that PR 3 (`provision/`) is still pending; -- Task 2's finding about any capability that left with `aws-sweep-controller-orphans.sh`. +- a note that the design's Deletions table was overruled for `teardown.sh` and + `aws-sweep-controller-orphans.sh`: they moved instead (owner decision, 2026-09-21). --- @@ -880,10 +863,11 @@ Follow `.agents/skills/create-pr/SKILL.md`. The body must carry: - **Spec coverage.** The design's PR 2 row names `ops/` and `docs/`: Tasks 3–5. Its evidence column names a preview on both clouds: Task 8 Step 5, as a merge gate with the owner's decision, backed - in CI by Task 1. Its Deletions table: Task 2. Criterion 2: Task 7. `check-rebased.sh` post-dates + in CI by Task 1. Its Deletions table: overruled for two live tools (owner, 2026-09-21, Task 2); its other two + entries are already absent. Criterion 2: Task 7. `check-rebased.sh` post-dates the design; Task 6 carries the owner's placement. - **Measured, not copied.** The design says PR 2 has 77 live references. Measured: 147 matches across the 18 moving files, 123 excluding a script's mention of itself. Each task's rewrite - expects its own measured count (9, 18, 21 files), not the design's. + expects its own measured count (9, 18, 22 files), not the design's. - **Exact values are consistent across tasks:** 87 references checked, floor 80; failure counts 8 (Task 4) and 22 (Task 5), both measured by simulating the move; suite count 23. From 1203f789ec87d72305409eac22b5c95125db4874 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:07:50 +0200 Subject: [PATCH 05/19] docs(superpowers): PR 2 plan counts 88 references once the gate reads yaml --- ...2026-09-21-scripts-restructure-pr2-plan.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md index 83e5cf074..1e2433624 100644 --- a/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md +++ b/docs/superpowers/plans/2026-09-21-scripts-restructure-pr2-plan.md @@ -89,9 +89,9 @@ the reference-rewrite recipe this plan reuses. ### Task 1: The `test-terramate-script-refs.sh` gate -Measured on this branch: **87** script references on executed (non-comment) lines under +Measured on this branch: **88** script references on executed (non-comment) lines under `opentofu/`, in five shapes. `${terramate.root.path.fs.absolute}/scripts/X` ×70, `$${ROOT}/scripts/X` -×7, bare `scripts/X` in `echo` hints ×4, `${path.module}/scripts/X` ×4 (module-local), and +×7, bare `scripts/X` in `echo` hints ×4, `${path.module}/scripts/X` ×5 (module-local, one of them a `.yaml` read by `file()`), and `${path.module}/../../../../scripts/X` ×2. Two rules resolve all five. A `${path.module}/` prefix resolves beside the `.tf` file. Everything @@ -143,10 +143,10 @@ while IFS= read -r hit; do printf 'FAIL %s:%s %s\n resolved to %s\n' "$file" "$line" "$ref" "${target#"$ROOT"/}" failed=$((failed + 1)) fi - done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' <<<"$text" \ + done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' <<<"$text" \ | sed -E 's/^[[:space:]"(]//') done < <(cd "$ROOT" && grep -rnE --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ - 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' opentofu 2>/dev/null) + 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' opentofu 2>/dev/null) if [ "$checked" -lt "$FLOOR" ]; then echo "FAIL checked $checked script reference(s), floor is $FLOOR: the extraction broke, not the references" @@ -162,7 +162,7 @@ an expansion. ShellCheck's SC2016 is not raised at `-S warning`, but confirm tha - [ ] **Step 2: Run it against the real tree** Run: `bash scripts/ci/tests/test-terramate-script-refs.sh; echo "exit=$?"` -Expected: `87 script reference(s) on executed opentofu/terramate lines checked; 0 failed`, `exit=0`. +Expected: `88 script reference(s) on executed opentofu/terramate lines checked; 0 failed`, `exit=0`. - [ ] **Step 3: Prove it catches a moved script** @@ -175,7 +175,7 @@ mkdir -p "$T/scripts/ops/teardown" mv "$T/scripts/terramate-destroy-confirm.sh" "$T/scripts/ops/teardown/" TM_REFS_ROOT="$T" bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1; echo "exit=${PIPESTATUS[0]}" ``` -Expected: `87 script reference(s) … checked; 15 failed`, `exit=1`. +Expected: `88 script reference(s) … checked; 15 failed`, `exit=1`. - [ ] **Step 4: Prove it resolves `${path.module}` climbs** @@ -220,7 +220,7 @@ directly after the `validate-idp-topology.sh` row: git add scripts/ci/tests/test-terramate-script-refs.sh scripts/AGENTS.md git commit -m "test(ci): gate every script path opentofu and terramate execute -87 references on executed lines, 30 of them to scripts this PR moves. +88 references on executed lines, 30 of them to scripts this PR moves. They run at apply and destroy time and no CI job executes them; neither test-script-paths.sh nor verify-doc-paths.sh reads a .tf or .tm.hcl." ``` @@ -378,7 +378,7 @@ git mv scripts/cleanup-benchmark-images.sh scripts/ops/demo/cleanup-benchmar bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 bash scripts/ci/tests/test-script-paths.sh | tail -1 ``` -Expected: `87 … checked; 8 failed` (the executed references, which Step 4 fixes), then +Expected: `88 … checked; 8 failed` (the executed references, which Step 4 fixes), then `11 roots, 21 sources, 16 subjects checked; 4 failed`: the three `lib/` sources and `test-cnpg-promote-seed.sh`'s subject, all fixed in Step 3. Both counts were measured by simulating this move. A different number means the measurement is stale: report it. @@ -463,7 +463,7 @@ git diff --name-only HEAD -- opentofu | grep -E '\.(tf|tm\.hcl|tfvars)$' for f in $(git ls-files -s | awk '$1=="120000"{print $4}'); do [ -L "$f" ] || echo "NOT A SYMLINK: $f"; done ``` Expected: -- the terramate gate: `87 … checked; 0 failed`; +- the terramate gate: `88 … checked; 0 failed`; - the paths gate: `0 failed`. It checks the three `lib/` sources; - no `SYNTAX:` line; - `eks->k8s call resolves`; @@ -514,7 +514,7 @@ git mv scripts/teardown.sh scripts/ops/teardown/teardown.sh bash scripts/ci/tests/test-terramate-script-refs.sh | tail -1 bash scripts/ci/tests/test-script-paths.sh | tail -1 ``` -Expected: `87 … checked; 22 failed`, then `11 roots, 21 sources, 16 subjects checked; 1 failed`. That +Expected: `88 … checked; 22 failed`, then `11 roots, 21 sources, 16 subjects checked; 1 failed`. That one is `teardown.sh:38`, which resolves to `scripts/ops` instead of the repo root. Both counts were measured by simulating this move. @@ -565,7 +565,7 @@ git grep -n -E 'scripts/(destroy-stage2|tofu-destroy-contained|terramate-destroy -- ':!docs/superpowers/plans' ':!docs/superpowers/specs' ':!docs/specs' ``` Expected: -- the terramate gate: `87 … checked; 0 failed`; +- the terramate gate: `88 … checked; 0 failed`; - `terramate ok`; - no `SYNTAX:` line; - the paths gate: `11 roots, … 0 failed`; @@ -804,7 +804,7 @@ bash scripts/ci/tests/test-terramate-script-refs.sh Expected: - `task check` exits 0, with the rendered resource count, `23 passed, 1 skipped, 0 failed`, and all six gates passing; -- the terramate gate: `87 … checked; 0 failed`. +- the terramate gate: `88 … checked; 0 failed`. Cite both verbatim. @@ -869,5 +869,5 @@ Follow `.agents/skills/create-pr/SKILL.md`. The body must carry: - **Measured, not copied.** The design says PR 2 has 77 live references. Measured: 147 matches across the 18 moving files, 123 excluding a script's mention of itself. Each task's rewrite expects its own measured count (9, 18, 22 files), not the design's. -- **Exact values are consistent across tasks:** 87 references checked, floor 80; failure counts +- **Exact values are consistent across tasks:** 88 references checked, floor 80; failure counts 8 (Task 4) and 22 (Task 5), both measured by simulating the move; suite count 23. From 4e406e4522973251b2be634c2a91949aeaabb574 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:08:02 +0200 Subject: [PATCH 06/19] test(ci): check yaml assets the terramate-reference gate skipped --- scripts/ci/tests/test-terramate-script-refs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci/tests/test-terramate-script-refs.sh b/scripts/ci/tests/test-terramate-script-refs.sh index 7d411dab3..d647a6853 100755 --- a/scripts/ci/tests/test-terramate-script-refs.sh +++ b/scripts/ci/tests/test-terramate-script-refs.sh @@ -32,10 +32,10 @@ while IFS= read -r hit; do printf 'FAIL %s:%s %s\n resolved to %s\n' "$file" "$line" "$ref" "${target#"$ROOT"/}" failed=$((failed + 1)) fi - done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' <<<"$text" \ + done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' <<<"$text" \ | sed -E 's/^[[:space:]"(]//') done < <(cd "$ROOT" && grep -rnE --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ - 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js)' opentofu 2>/dev/null) + 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' opentofu 2>/dev/null) if [ "$checked" -lt "$FLOOR" ]; then echo "FAIL checked $checked script reference(s), floor is $FLOOR: the extraction broke, not the references" From 301af40f7b486137e0e349e6d239002a743f5c49 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:12:11 +0200 Subject: [PATCH 07/19] refactor(scripts): move the docs-site generators to scripts/docs/ --- docs/architecture/AGENTS.md | 6 +++--- docs/architecture/README.md | 6 +++--- docs/architecture/icons/manifest.json | 2 +- mise.toml | 4 ++-- scripts/AGENTS.md | 4 ++-- scripts/{ => docs}/build-og-card.html | 2 +- scripts/{ => docs}/diagram-icons.py | 4 ++-- scripts/{ => docs}/export-diagrams.sh | 2 +- website/assets/screenshots/app-wizard/README.md | 2 +- website/content/docs/reference/ci-workflows.md | 2 +- website/hugo.yaml | 2 +- website/layouts/_markup/render-image.html | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) rename scripts/{ => docs}/build-og-card.html (97%) rename scripts/{ => docs}/diagram-icons.py (98%) rename scripts/{ => docs}/export-diagrams.sh (99%) diff --git a/docs/architecture/AGENTS.md b/docs/architecture/AGENTS.md index 41795d4e5..8ae26f116 100644 --- a/docs/architecture/AGENTS.md +++ b/docs/architecture/AGENTS.md @@ -23,14 +23,14 @@ Stop at the first hit: | # | Source | How | |---|---|---| -| 1 | `icons/` | `./scripts/diagram-icons.py style ` — 15 logos already rasterised | +| 1 | `icons/` | `./scripts/docs/diagram-icons.py style ` — 15 logos already rasterised | | 2 | mxgraph stencil | `shapesearch.py ""`, then rasterise into `icons/` | | 3 | CNCF Artwork | **list** `projects//icon/color/` and pick the SVG — filenames are not derivable | | 4 | Project brand | `aiicons.py "" --embed`, then rasterise | | 5 | — | a clean ogenki box, no icon | Reaching step 5 is fine. Skipping to it without trying 1–4 is the failure this rule exists to stop. -`./scripts/diagram-icons.py audit` lists boxes that name a product and render without one, +`./scripts/docs/diagram-icons.py audit` lists boxes that name a product and render without one, grouped by which source would supply it. **Advisory, deliberately not a CI gate** — whether a box wants a logo is a judgment call, and a gate that can go red on a judgment call gets switched off. @@ -100,7 +100,7 @@ A logo used by a second diagram belongs in `icons/`, not embedded twice. Check ` ## After any diagram change ```bash -./scripts/export-diagrams.sh # regenerates every SVG the site embeds +./scripts/docs/export-diagrams.sh # regenerates every SVG the site embeds ./scripts/ci/validate-links.sh # after any file move ./scripts/ci/validate-doc-claims.sh # the pages that describe the diagram ``` diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 6c04e1934..cb1391d5b 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -6,7 +6,7 @@ Drawio (`.drawio`) sources. Open with the [drawio desktop app](https://www.drawi **One command regenerates every export:** ```bash -./scripts/export-diagrams.sh +./scripts/docs/export-diagrams.sh ``` It writes SVG to `website/static/images/diagrams/`, writes the one PNG GitHub still needs, and @@ -76,13 +76,13 @@ re-rasterising its own. | # | Source | How | |---|--------|-----| -| 1 | `icons/` | `./scripts/diagram-icons.py style ` — paste-ready, ogenki palette applied | +| 1 | `icons/` | `./scripts/docs/diagram-icons.py style ` — paste-ready, ogenki palette applied | | 2 | mxgraph stencil | `shapesearch.py ""` to find it, then **rasterise it into `icons/`** — see below | | 3 | [CNCF Artwork](https://github.com/cncf/artwork) | **list** `projects//icon/color/` and pick the SVG — `external-secrets-operator` ships `eso-icon-color.svg`, so filenames are not slugs | | 4 | Project brand | `aiicons.py "" --embed` | | 5 | none | a clean ogenki box — the honest fallback, only after 1–4 miss | -`./scripts/diagram-icons.py audit` lists every box that names a product and renders without one, +`./scripts/docs/diagram-icons.py audit` lists every box that names a product and renders without one, grouped by which source would supply it. It is advisory, not a CI gate: whether a box wants a logo is a judgment call, and a gate that can go red on a judgment call gets switched off. diff --git a/docs/architecture/icons/manifest.json b/docs/architecture/icons/manifest.json index 70d94ea00..ec145be21 100644 --- a/docs/architecture/icons/manifest.json +++ b/docs/architecture/icons/manifest.json @@ -1,5 +1,5 @@ { - "_note": "Logos rasterised for platform-overview.drawio and extracted here so every diagram references one asset instead of re-fetching and re-rasterising its own. PNG, not SVG: headless drawio export does not render SVG data URIs. Embed with image=data:image/png, -- a comma, not ';base64,', which terminates the style value. Prefer ./scripts/diagram-icons.py style over hand-rolling one.", + "_note": "Logos rasterised for platform-overview.drawio and extracted here so every diagram references one asset instead of re-fetching and re-rasterising its own. PNG, not SVG: headless drawio export does not render SVG data URIs. Embed with image=data:image/png, -- a comma, not ';base64,', which terminates the style value. Prefer ./scripts/docs/diagram-icons.py style over hand-rolling one.", "_aliases_note": "victorialogs and victoriatraces currently render the VictoriaMetrics mark -- platform-overview used one logo for all three, and the three extracted files were byte-identical. Both projects have their own upstream logo; fetch the real marks, add them as files, and drop the alias.", "aliases": { "envoy": "envoy-ai-gateway", diff --git a/mise.toml b/mise.toml index 67f5f7b1f..53f118889 100644 --- a/mise.toml +++ b/mise.toml @@ -84,7 +84,7 @@ task = "3.53.1" # gcloud components install gke-gcloud-auth-plugin "asdf:mise-plugins/mise-gcloud" = "581.0.0" -# Diagram exporter (scripts/export-diagrams.sh) — PINNED, BUT NOT HERE. +# Diagram exporter (scripts/docs/export-diagrams.sh) — PINNED, BUT NOT HERE. # # The SVGs under website/static/images/diagrams/ are committed artifacts, so the # tool that generates them should be pinned like any other. mise cannot be the @@ -104,7 +104,7 @@ task = "3.53.1" # reverted. Even made to install, it would pull ~170 MB into every job for a tool # none of them run -- no CI job exports diagrams. # -# The pin therefore lives in scripts/export-diagrams.sh as +# The pin therefore lives in scripts/docs/export-diagrams.sh as # DRAWIO_PINNED_VERSION, which refuses to run against a different build. That is # where the version actually matters, and it costs CI nothing. # diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index aa6b6da34..71265568f 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -97,8 +97,8 @@ suite**; if a change stops that being true, it has regressed to testing nothing. | `validate-doc-claims.sh` | docs still agree with config, per `.doc-claims.yaml` | | `validate-idp-topology.sh` | exactly one cloud hosts ZITADEL (ADR-0027) | | `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl` line exists — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | -| `diagram-icons.py audit` | boxes naming a product that render without an icon. Advisory, never a CI gate | -| `export-diagrams.sh` | regenerates every SVG the site embeds. Pins the drawio version on purpose | +| `docs/diagram-icons.py audit` | boxes naming a product that render without an icon. Advisory, never a CI gate | +| `docs/export-diagrams.sh` | regenerates every SVG the site embeds. Pins the drawio version on purpose | | `eks-prepare-destroy.sh` | **deletes every PVC.** Must carry a cloud gate | | `tm-provisioner.sh` | the `TM_CLOUD` lane enforcement every stack routes through | diff --git a/scripts/build-og-card.html b/scripts/docs/build-og-card.html similarity index 97% rename from scripts/build-og-card.html rename to scripts/docs/build-og-card.html index 0c56e4e71..2d0c2af8c 100644 --- a/scripts/build-og-card.html +++ b/scripts/docs/build-og-card.html @@ -82,7 +82,7 @@
- + Cloud Native Reference
diff --git a/scripts/diagram-icons.py b/scripts/docs/diagram-icons.py similarity index 98% rename from scripts/diagram-icons.py rename to scripts/docs/diagram-icons.py index 9b7297205..06f12ef84 100755 --- a/scripts/diagram-icons.py +++ b/scripts/docs/diagram-icons.py @@ -33,7 +33,7 @@ import sys from collections import defaultdict -ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ICONS = os.path.join(ROOT, 'docs', 'architecture', 'icons') SRC = os.path.join(ROOT, 'docs', 'architecture') @@ -162,7 +162,7 @@ def cmd_audit(args): for src in ('local', 'vendor', 'cncf', 'brand', 'none'): if by_source[src]: how = { - 'local': "docs/architecture/icons/ — ./scripts/diagram-icons.py style ", + 'local': "docs/architecture/icons/ — ./scripts/docs/diagram-icons.py style ", 'vendor': "an mxgraph stencil — shapesearch.py, no embedding needed", 'cncf': "github.com/cncf/artwork — fetch, then rsvg-convert -w 64 -h 64", 'brand': "the project's brand page — aiicons.py --embed, then rasterise", diff --git a/scripts/export-diagrams.sh b/scripts/docs/export-diagrams.sh similarity index 99% rename from scripts/export-diagrams.sh rename to scripts/docs/export-diagrams.sh index e2816d275..7221a412e 100755 --- a/scripts/export-diagrams.sh +++ b/scripts/docs/export-diagrams.sh @@ -19,7 +19,7 @@ # Requires the draw.io desktop app on PATH. set -euo pipefail -cd "$(dirname "$0")/.." +cd "$(dirname "$0")/../.." command -v drawio >/dev/null || { echo "drawio not on PATH — install the desktop app"; exit 1; } diff --git a/website/assets/screenshots/app-wizard/README.md b/website/assets/screenshots/app-wizard/README.md index 4be01b903..6584cc6f7 100644 --- a/website/assets/screenshots/app-wizard/README.md +++ b/website/assets/screenshots/app-wizard/README.md @@ -22,4 +22,4 @@ width ~1400px, light theme, matching the ogenki branding. `website/assets/` is Hugo's asset pipeline, reachable from a template through `resources.Get`. Images embedded directly from Markdown with a site-root path belong in `website/static/` instead — see the note at the top of -`scripts/export-diagrams.sh`, which records the same distinction for diagrams. +`scripts/docs/export-diagrams.sh`, which records the same distinction for diagrams. diff --git a/website/content/docs/reference/ci-workflows.md b/website/content/docs/reference/ci-workflows.md index da4dd87b7..e855f4728 100644 --- a/website/content/docs/reference/ci-workflows.md +++ b/website/content/docs/reference/ci-workflows.md @@ -242,7 +242,7 @@ pre-commit run --all-files | Secrets | `detect-secrets` (baseline: `.secrets.baseline`) | `check-added-large-files` caps a file at 1000 KB, which is the constraint the -diagram export budget in `scripts/export-diagrams.sh` is set below. +diagram export budget in `scripts/docs/export-diagrams.sh` is set below. ## Self-hosted GitHub runners diff --git a/website/hugo.yaml b/website/hugo.yaml index 5850fdf8a..f55356875 100644 --- a/website/hugo.yaml +++ b/website/hugo.yaml @@ -70,7 +70,7 @@ params: # Social card. Consumed by layouts/_partials/opengraph.html and # layouts/_partials/twitter_cards.html, both of which absolute-ise it against # baseURL — so it has to stay a site-root path, not a relative one. - # Regenerate from scripts/build-og-card.html; see that file's header. + # Regenerate from scripts/docs/build-og-card.html; see that file's header. images: - /images/og-card.png navbar: diff --git a/website/layouts/_markup/render-image.html b/website/layouts/_markup/render-image.html index 9316aa3c2..db2e45572 100644 --- a/website/layouts/_markup/render-image.html +++ b/website/layouts/_markup/render-image.html @@ -14,7 +14,7 @@ The numbers are read from the SVG rather than written down here, because drawio changes a diagram's dimensions whenever its content changes. A hardcoded - size would silently stop matching on the next ./scripts/export-diagrams.sh run + size would silently stop matching on the next ./scripts/docs/export-diagrams.sh run and reserve the wrong box, which is worse than reserving none. Markdown attributes (`![alt](x.svg){width="1343"}`) do not work here: Goldmark From 667629de74b6fdc6281b3a6f73873d613ec48d37 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:31:58 +0200 Subject: [PATCH 08/19] refactor(scripts): move the day-2 operations scripts to scripts/ops/ aws/, gcp/, k8s/, demo/. The cloud prefix drops where it repeats the directory. eks-prepare-destroy.sh's call to reclaim-csi-volumes.sh crosses directories now and is corrected by hand: no gate sees a path passed to exec. --- apps/platform/image-gallery/loadgen.yaml | 2 +- infrastructure/gcp-0/cloudnative-pg/gcs-bucket.yaml | 2 +- opentofu/aws/eks/init/helm_values/cilium.yaml | 2 +- opentofu/aws/eks/init/main.tf | 2 +- opentofu/aws/eks/init/workflows.tm.hcl | 10 +++++----- opentofu/gcp/gke/init/workflows.tm.hcl | 6 +++--- opentofu/gcp/network/workflows.tm.hcl | 2 +- opentofu/gcp/workforce-identity/workflows.tm.hcl | 4 ++-- scripts/AGENTS.md | 2 +- scripts/ci/tests/test-cnpg-promote-seed.sh | 3 +-- scripts/{ => ops/aws}/eks-prepare-destroy.sh | 2 +- scripts/{ => ops/aws}/eks-recycle-bootstrap-nodes.sh | 0 .../aws/sweep-controller-orphans.sh} | 2 +- .../aws/sweep-orphaned-volumes.sh} | 4 ++-- .../aws/sweep-teardown-blockers.sh} | 2 +- scripts/{ => ops/demo}/cleanup-benchmark-images.sh | 0 scripts/{demo-load.sh => ops/demo/load.sh} | 6 +++--- .../gcp/adopt-workforce-pool.sh} | 2 +- .../gcp/purge-dns-records.sh} | 4 ++-- .../gcp/sweep-orphaned-disks.sh} | 6 +++--- scripts/{ => ops/k8s}/cnpg-prepare-restore.sh | 2 +- scripts/{ => ops/k8s}/cnpg-promote-seed.sh | 0 .../k8s/reclaim-csi-volumes.sh} | 8 ++++---- scripts/teardown.sh | 6 +++--- security/base/zitadel/sqlinstance.yaml | 8 ++++---- security/gcp-0/zitadel/kustomization.yaml | 2 +- website/content/docs/get-started/aws/teardown.md | 10 +++++----- website/content/docs/get-started/gcp/teardown.md | 10 +++++----- .../docs/guides/migrate-the-identity-provider.md | 4 ++-- website/content/docs/guides/restore-a-database.md | 6 +++--- website/content/docs/reference/commands.md | 8 ++++---- 31 files changed, 63 insertions(+), 64 deletions(-) rename scripts/{ => ops/aws}/eks-prepare-destroy.sh (99%) rename scripts/{ => ops/aws}/eks-recycle-bootstrap-nodes.sh (100%) rename scripts/{aws-sweep-controller-orphans.sh => ops/aws/sweep-controller-orphans.sh} (98%) rename scripts/{aws-sweep-orphaned-volumes.sh => ops/aws/sweep-orphaned-volumes.sh} (95%) rename scripts/{aws-sweep-teardown-blockers.sh => ops/aws/sweep-teardown-blockers.sh} (99%) rename scripts/{ => ops/demo}/cleanup-benchmark-images.sh (100%) rename scripts/{demo-load.sh => ops/demo/load.sh} (77%) rename scripts/{gcp-adopt-workforce-pool.sh => ops/gcp/adopt-workforce-pool.sh} (98%) rename scripts/{gcp-purge-dns-records.sh => ops/gcp/purge-dns-records.sh} (96%) rename scripts/{gcp-sweep-orphaned-disks.sh => ops/gcp/sweep-orphaned-disks.sh} (95%) rename scripts/{ => ops/k8s}/cnpg-prepare-restore.sh (99%) rename scripts/{ => ops/k8s}/cnpg-promote-seed.sh (100%) rename scripts/{k8s-reclaim-csi-volumes.sh => ops/k8s/reclaim-csi-volumes.sh} (95%) diff --git a/apps/platform/image-gallery/loadgen.yaml b/apps/platform/image-gallery/loadgen.yaml index 33b652939..efab70f37 100644 --- a/apps/platform/image-gallery/loadgen.yaml +++ b/apps/platform/image-gallery/loadgen.yaml @@ -2,7 +2,7 @@ # # SUSPENDED by default. When resumed it runs `steady` (1 req/s, the mixed API # mix) for 14 minutes every 15 minutes, so dashboards always have data. -# On-demand runs of any scenario: scripts/demo-load.sh . +# On-demand runs of any scenario: scripts/ops/demo/load.sh . # # The image tag moves with the App's (apps/base/complete/app.yaml): same release, same API. apiVersion: batch/v1 diff --git a/infrastructure/gcp-0/cloudnative-pg/gcs-bucket.yaml b/infrastructure/gcp-0/cloudnative-pg/gcs-bucket.yaml index 005de93d1..bef0da161 100644 --- a/infrastructure/gcp-0/cloudnative-pg/gcs-bucket.yaml +++ b/infrastructure/gcp-0/cloudnative-pg/gcs-bucket.yaml @@ -32,7 +32,7 @@ # # Freezing a recovery seed is likewise a script now, not a `gcloud storage mv`: # -# ./scripts/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ +# ./scripts/ops/k8s/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ # --cloud gcp --bucket ${project_id}-ogenki-cnpg-backups --apply # # It discovers the uid-suffixed live prefix from the Cluster, forces the diff --git a/opentofu/aws/eks/init/helm_values/cilium.yaml b/opentofu/aws/eks/init/helm_values/cilium.yaml index 35eb634e8..71308b471 100644 --- a/opentofu/aws/eks/init/helm_values/cilium.yaml +++ b/opentofu/aws/eks/init/helm_values/cilium.yaml @@ -90,7 +90,7 @@ prometheus: # every node would cost real schedulable capacity to guard a transient. # # WHY THIS WENT UNNOTICED FOR SO LONG: the OOM kills were always happening, but -# scripts/eks-recycle-bootstrap-nodes.sh used to read Cilium's ConfigMap BEFORE +# scripts/ops/aws/eks-recycle-bootstrap-nodes.sh used to read Cilium's ConfigMap BEFORE # waiting for Cilium, found the key unset, and exited 0. The wait never ran, so # a slow, crash-looping rollout looked like a healthy one and the damage # surfaced an hour later as IP exhaustion in an unrelated component. Fixing that diff --git a/opentofu/aws/eks/init/main.tf b/opentofu/aws/eks/init/main.tf index 595fe10e4..aac617bcf 100644 --- a/opentofu/aws/eks/init/main.tf +++ b/opentofu/aws/eks/init/main.tf @@ -184,7 +184,7 @@ module "eks" { # showed prefixes=0 while every later Karpenter node had prefixes. # # That assumption no longer holds. The deploy script now recycles node-group - # nodes after Cilium is healthy (scripts/eks-recycle-bootstrap-nodes.sh, + # nodes after Cilium is healthy (scripts/ops/aws/eks-recycle-bootstrap-nodes.sh, # stage 3), so their replacements come up with Cilium already running and DO # get prefixes. Verified on aws-0: both node-group nodes now report # prefixes=3 after being replaced, alongside every Karpenter node. diff --git a/opentofu/aws/eks/init/workflows.tm.hcl b/opentofu/aws/eks/init/workflows.tm.hcl index 4a2ef6bed..8a875c6f3 100644 --- a/opentofu/aws/eks/init/workflows.tm.hcl +++ b/opentofu/aws/eks/init/workflows.tm.hcl @@ -77,7 +77,7 @@ script "deploy" { name = "stage3-recycle-bootstrap-nodes" description = "Recycle node-group nodes whose ENIs predate Cilium (no-op once they use prefix delegation)" commands = [ - ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "-c", "${terramate.root.path.fs.absolute}/scripts/eks-recycle-bootstrap-nodes.sh --cluster-name ${global.eks_cluster_name} --region ${global.region}"], + ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "-c", "${terramate.root.path.fs.absolute}/scripts/ops/aws/eks-recycle-bootstrap-nodes.sh --cluster-name ${global.eks_cluster_name} --region ${global.region}"], ] } @@ -259,7 +259,7 @@ script "destroy" { "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", - "${terramate.root.path.fs.absolute}/scripts/eks-prepare-destroy.sh", + "${terramate.root.path.fs.absolute}/scripts/ops/aws/eks-prepare-destroy.sh", "--cluster-name", global.eks_cluster_name, "--region", @@ -357,7 +357,7 @@ script "destroy" { # stacks entirely untouched and a GKE cluster running. # # Idempotent and dry-run-safe; on a healthy teardown it finds nothing and says - # so. See scripts/aws-sweep-teardown-blockers.sh for what it will not touch. + # so. See scripts/ops/aws/sweep-teardown-blockers.sh for what it will not touch. job { name = "stage0-sweep-teardown-blockers" description = "Clear ExternalDNS records and the EKS-managed SG that block DeleteHostedZone / DeleteVpc" @@ -367,7 +367,7 @@ script "destroy" { "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", - "${terramate.root.path.fs.absolute}/scripts/aws-sweep-teardown-blockers.sh", + "${terramate.root.path.fs.absolute}/scripts/ops/aws/sweep-teardown-blockers.sh", "--cluster-name", global.eks_cluster_name, "--region", @@ -388,7 +388,7 @@ script "destroy" { "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", - "${terramate.root.path.fs.absolute}/scripts/aws-sweep-orphaned-volumes.sh", + "${terramate.root.path.fs.absolute}/scripts/ops/aws/sweep-orphaned-volumes.sh", "--cluster-name", global.eks_cluster_name, "--region", diff --git a/opentofu/gcp/gke/init/workflows.tm.hcl b/opentofu/gcp/gke/init/workflows.tm.hcl index 532e9d13e..429b17916 100644 --- a/opentofu/gcp/gke/init/workflows.tm.hcl +++ b/opentofu/gcp/gke/init/workflows.tm.hcl @@ -414,7 +414,7 @@ script "destroy" { if gcloud container clusters get-credentials "$${name}" \ --location "$${location}" --project "$${project}" 2>/dev/null; then - bash "${terramate.root.path.fs.absolute}/scripts/k8s-reclaim-csi-volumes.sh" || true + bash "${terramate.root.path.fs.absolute}/scripts/ops/k8s/reclaim-csi-volumes.sh" || true else echo "[warn] could not fetch credentials for $${name}; skipping the in-cluster" echo " reclaim. Any orphaned disks are swept by the network stack destroy." @@ -482,7 +482,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - # The backstop k8s-reclaim-csi-volumes.sh has always CLAIMED to have. + # The backstop reclaim-csi-volumes.sh has always CLAIMED to have. # # That script reclaims PVs while the cluster still exists -- the only # moment the CSI controller can -- and when it runs out of time it warns @@ -498,7 +498,7 @@ script "destroy" { # after it, everything that leaked is unattached and visible. # # Never fails the teardown -- see the script's closing comment. - bash "${terramate.root.path.fs.absolute}/scripts/gcp-sweep-orphaned-disks.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/gcp/sweep-orphaned-disks.sh" \ --project "$(cd "${terramate.root.path.fs.absolute}/opentofu/gcp/gke/init" && \ awk -F'=' '/^[[:space:]]*project_id/{gsub(/[[:space:]"]/,"",$2); print $2}' variables.tfvars)" \ --apply diff --git a/opentofu/gcp/network/workflows.tm.hcl b/opentofu/gcp/network/workflows.tm.hcl index 568145c5c..6804376e4 100644 --- a/opentofu/gcp/network/workflows.tm.hcl +++ b/opentofu/gcp/network/workflows.tm.hcl @@ -120,7 +120,7 @@ script "destroy" { # `set -e` killed the destroy before tofu ran. The stack then looked # torn down (terramate exits non-zero at the very end) while the VPC, # the NAT and the Tailscale router were all still billing. - bash "${terramate.root.path.fs.absolute}/scripts/gcp-purge-dns-records.sh" "$${zone}" "$${project}" + bash "${terramate.root.path.fs.absolute}/scripts/ops/gcp/purge-dns-records.sh" "$${zone}" "$${project}" else echo "[warn] no DNS zone in state; skipping the record purge." fi diff --git a/opentofu/gcp/workforce-identity/workflows.tm.hcl b/opentofu/gcp/workforce-identity/workflows.tm.hcl index 83cfbb791..22fa9ece4 100644 --- a/opentofu/gcp/workforce-identity/workflows.tm.hcl +++ b/opentofu/gcp/workforce-identity/workflows.tm.hcl @@ -44,8 +44,8 @@ script "deploy" { # that window fails 409 on the very first resource -- while every check # reports the org clean, because `list` hides deleted pools. This # undeletes and imports whatever is already there; it is a no-op on a - # genuinely fresh org. See scripts/gcp-adopt-workforce-pool.sh. - bash "${terramate.root.path.fs.absolute}/scripts/gcp-adopt-workforce-pool.sh" \ + # genuinely fresh org. See scripts/ops/gcp/adopt-workforce-pool.sh. + bash "${terramate.root.path.fs.absolute}/scripts/ops/gcp/adopt-workforce-pool.sh" \ --pool "$(awk -F= '/workforce_pool_id/{gsub(/[ "]/,"",$2); print $2}' variables.tfvars)" \ --apply ${global.provisioner} apply -auto-approve -var-file=variables.tfvars -var='deploy_identity_provider=${global.deploy_identity_provider_gcp}' diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 71265568f..ca3e33974 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -99,7 +99,7 @@ suite**; if a change stops that being true, it has regressed to testing nothing. | `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl` line exists — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | | `docs/diagram-icons.py audit` | boxes naming a product that render without an icon. Advisory, never a CI gate | | `docs/export-diagrams.sh` | regenerates every SVG the site embeds. Pins the drawio version on purpose | -| `eks-prepare-destroy.sh` | **deletes every PVC.** Must carry a cloud gate | +| `ops/aws/eks-prepare-destroy.sh` | **deletes every PVC.** Must carry a cloud gate | | `tm-provisioner.sh` | the `TM_CLOUD` lane enforcement every stack routes through | **`validate-idp-topology.sh` only checks the repo.** `gcp-0` runs its own ZITADEL while the diff --git a/scripts/ci/tests/test-cnpg-promote-seed.sh b/scripts/ci/tests/test-cnpg-promote-seed.sh index f69aa02b4..23cd3dd40 100755 --- a/scripts/ci/tests/test-cnpg-promote-seed.sh +++ b/scripts/ci/tests/test-cnpg-promote-seed.sh @@ -22,8 +22,7 @@ # for why that run could not happen in this session, and what it still owes. set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" -# The subject is still at scripts/ root. When it moves, this path moves with it. -SCRIPT="$HERE/../../cnpg-promote-seed.sh" +SCRIPT="$HERE/../../ops/k8s/cnpg-promote-seed.sh" STUB="$(mktemp -d)"; FIX="$(mktemp -d)" trap 'rm -rf "$STUB" "$FIX"' EXIT fail=0 diff --git a/scripts/eks-prepare-destroy.sh b/scripts/ops/aws/eks-prepare-destroy.sh similarity index 99% rename from scripts/eks-prepare-destroy.sh rename to scripts/ops/aws/eks-prepare-destroy.sh index 588477b90..5532792f6 100755 --- a/scripts/eks-prepare-destroy.sh +++ b/scripts/ops/aws/eks-prepare-destroy.sh @@ -112,7 +112,7 @@ kubectl delete validatingadmissionpolicybinding --all --wait=false 2>/dev/null | # 2026-08-27 gcp-0 teardown for want of exactly this, and a second copy would # have been a second thing to forget. The EBS sweep below is the AWS-specific # half, and stays here. -"$(dirname "$0")/k8s-reclaim-csi-volumes.sh" || true +"$(dirname "$0")/../k8s/reclaim-csi-volumes.sh" || true # Sweep EBS volumes this cluster orphaned in EARLIER runs. The reclaim above # only covers PVs that still exist; anything left behind by a previous destroy diff --git a/scripts/eks-recycle-bootstrap-nodes.sh b/scripts/ops/aws/eks-recycle-bootstrap-nodes.sh similarity index 100% rename from scripts/eks-recycle-bootstrap-nodes.sh rename to scripts/ops/aws/eks-recycle-bootstrap-nodes.sh diff --git a/scripts/aws-sweep-controller-orphans.sh b/scripts/ops/aws/sweep-controller-orphans.sh similarity index 98% rename from scripts/aws-sweep-controller-orphans.sh rename to scripts/ops/aws/sweep-controller-orphans.sh index 908886670..7eccd2067 100755 --- a/scripts/aws-sweep-controller-orphans.sh +++ b/scripts/ops/aws/sweep-controller-orphans.sh @@ -49,7 +49,7 @@ # Dry-run unless --apply. # # Usage: -# aws-sweep-controller-orphans.sh --cluster-name aws-0 --region eu-west-3 [--vpc-id V] [--apply] +# scripts/ops/aws/sweep-controller-orphans.sh --cluster-name aws-0 --region eu-west-3 [--vpc-id V] [--apply] set -o nounset set -o pipefail diff --git a/scripts/aws-sweep-orphaned-volumes.sh b/scripts/ops/aws/sweep-orphaned-volumes.sh similarity index 95% rename from scripts/aws-sweep-orphaned-volumes.sh rename to scripts/ops/aws/sweep-orphaned-volumes.sh index 38f83f4a4..00a51c18c 100755 --- a/scripts/aws-sweep-orphaned-volumes.sh +++ b/scripts/ops/aws/sweep-orphaned-volumes.sh @@ -20,7 +20,7 @@ # This sweep runs after `tofu destroy` returns, when every node is terminated # and so every volume of this cluster is unambiguously detached. There is no # in-flight state left to race, which is exactly what makes the late moment the -# reliable one. GCP's equivalent (gcp-sweep-orphaned-disks.sh) had to be written +# reliable one. GCP's equivalent (sweep-orphaned-disks.sh) had to be written # from scratch because GKE had no sweep at all; here the moment is the fix. # # WHAT IT MATCHES, AND WHY IT IS SAFE @@ -39,7 +39,7 @@ # live cluster's volume during a reschedule. Keep all three. # # Usage: -# aws-sweep-orphaned-volumes.sh --cluster-name N --region R [--profile P] [--apply] +# scripts/ops/aws/sweep-orphaned-volumes.sh --cluster-name N --region R [--profile P] [--apply] # # Dry-run unless --apply, and it lists exactly what it would delete. diff --git a/scripts/aws-sweep-teardown-blockers.sh b/scripts/ops/aws/sweep-teardown-blockers.sh similarity index 99% rename from scripts/aws-sweep-teardown-blockers.sh rename to scripts/ops/aws/sweep-teardown-blockers.sh index 5b21b9aa0..8499d4556 100755 --- a/scripts/aws-sweep-teardown-blockers.sh +++ b/scripts/ops/aws/sweep-teardown-blockers.sh @@ -48,7 +48,7 @@ # actually gone. # # Usage: -# aws-sweep-teardown-blockers.sh --cluster-name N --region R \ +# scripts/ops/aws/sweep-teardown-blockers.sh --cluster-name N --region R \ # [--zone-name Z] [--profile P] [--apply] # # Dry-run unless --apply, and it lists exactly what it would delete. diff --git a/scripts/cleanup-benchmark-images.sh b/scripts/ops/demo/cleanup-benchmark-images.sh similarity index 100% rename from scripts/cleanup-benchmark-images.sh rename to scripts/ops/demo/cleanup-benchmark-images.sh diff --git a/scripts/demo-load.sh b/scripts/ops/demo/load.sh similarity index 77% rename from scripts/demo-load.sh rename to scripts/ops/demo/load.sh index 738aaf578..50643f9c9 100755 --- a/scripts/demo-load.sh +++ b/scripts/ops/demo/load.sh @@ -3,9 +3,9 @@ # image-gallery-loadgen CronJob's template. The Job exports its traces and # metrics like the app does, so each trace starts at the load generator. # -# scripts/demo-load.sh [duration] [rate] -# scripts/demo-load.sh incident # ~10 min scripted incident; resets the demo controls on exit -# scripts/demo-load.sh mixed 15m 25 # the soak: the 25 req/s cap for 15 minutes +# scripts/ops/demo/load.sh [duration] [rate] +# scripts/ops/demo/load.sh incident # ~10 min scripted incident; resets the demo controls on exit +# scripts/ops/demo/load.sh mixed 15m 25 # the soak: the 25 req/s cap for 15 minutes set -euo pipefail usage() { sed -n '2,8p' "$0"; } diff --git a/scripts/gcp-adopt-workforce-pool.sh b/scripts/ops/gcp/adopt-workforce-pool.sh similarity index 98% rename from scripts/gcp-adopt-workforce-pool.sh rename to scripts/ops/gcp/adopt-workforce-pool.sh index 5e49d289e..88459177a 100755 --- a/scripts/gcp-adopt-workforce-pool.sh +++ b/scripts/ops/gcp/adopt-workforce-pool.sh @@ -43,7 +43,7 @@ # property worth having; this script is what buys it. # # Usage (from the workforce-identity stack directory): -# gcp-adopt-workforce-pool.sh --pool NAME [--provider NAME] [--location L] [--apply] +# scripts/ops/gcp/adopt-workforce-pool.sh --pool NAME [--provider NAME] [--location L] [--apply] # # Dry-run unless --apply. diff --git a/scripts/gcp-purge-dns-records.sh b/scripts/ops/gcp/purge-dns-records.sh similarity index 96% rename from scripts/gcp-purge-dns-records.sh rename to scripts/ops/gcp/purge-dns-records.sh index aa0c72702..cb80e3c3b 100755 --- a/scripts/gcp-purge-dns-records.sh +++ b/scripts/ops/gcp/purge-dns-records.sh @@ -24,7 +24,7 @@ # when the zone goes -- so they never block the destroy. # # Usage: -# gcp-purge-dns-records.sh +# scripts/ops/gcp/purge-dns-records.sh # # Safe to run when the zone is already gone or already empty: it reports and # exits 0 either way, so a destroy that is re-run does not fail here. @@ -35,7 +35,7 @@ set -o pipefail # gcloud must run as the identity OpenTofu uses, not the CLI account. # shellcheck source=scripts/lib/gcloud-adc.sh -. "$(dirname "$0")/lib/gcloud-adc.sh" +. "$(dirname "$0")/../../lib/gcloud-adc.sh" ZONE="${1:-}" PROJECT="${2:-}" diff --git a/scripts/gcp-sweep-orphaned-disks.sh b/scripts/ops/gcp/sweep-orphaned-disks.sh similarity index 95% rename from scripts/gcp-sweep-orphaned-disks.sh rename to scripts/ops/gcp/sweep-orphaned-disks.sh index a01225436..f640eacd7 100755 --- a/scripts/gcp-sweep-orphaned-disks.sh +++ b/scripts/ops/gcp/sweep-orphaned-disks.sh @@ -4,7 +4,7 @@ # # WHY THIS EXISTS # -# scripts/k8s-reclaim-csi-volumes.sh tries to reclaim every PV *before* the +# scripts/ops/k8s/reclaim-csi-volumes.sh tries to reclaim every PV *before* the # cluster is deleted, which is the only moment the CSI controller can do it. When # it cannot finish in time it prints a warning and exits 0, ending with: # @@ -40,7 +40,7 @@ # named that way. # # Usage: -# gcp-sweep-orphaned-disks.sh --project ID [--apply] +# scripts/ops/gcp/sweep-orphaned-disks.sh --project ID [--apply] # # Dry-run unless --apply, and it lists exactly what it would delete. @@ -50,7 +50,7 @@ set -o pipefail # gcloud must run as the identity OpenTofu uses, not the CLI account. # shellcheck source=scripts/lib/gcloud-adc.sh -. "$(dirname "$0")/lib/gcloud-adc.sh" +. "$(dirname "$0")/../../lib/gcloud-adc.sh" PROJECT="" APPLY="false" diff --git a/scripts/cnpg-prepare-restore.sh b/scripts/ops/k8s/cnpg-prepare-restore.sh similarity index 99% rename from scripts/cnpg-prepare-restore.sh rename to scripts/ops/k8s/cnpg-prepare-restore.sh index 1e65c573a..b7eadd132 100755 --- a/scripts/cnpg-prepare-restore.sh +++ b/scripts/ops/k8s/cnpg-prepare-restore.sh @@ -57,7 +57,7 @@ set -o pipefail # scripts/lib/gcloud-adc.sh. Without it this script reports "could not list the # live archive" for a bucket the deploy writes to happily. # shellcheck source=scripts/lib/gcloud-adc.sh -. "$(dirname "$0")/lib/gcloud-adc.sh" +. "$(dirname "$0")/../../lib/gcloud-adc.sh" CLOUD="" BUCKET="" CLUSTER="" SEED="" PROJECT="" PROFILE="" REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}" diff --git a/scripts/cnpg-promote-seed.sh b/scripts/ops/k8s/cnpg-promote-seed.sh similarity index 100% rename from scripts/cnpg-promote-seed.sh rename to scripts/ops/k8s/cnpg-promote-seed.sh diff --git a/scripts/k8s-reclaim-csi-volumes.sh b/scripts/ops/k8s/reclaim-csi-volumes.sh similarity index 95% rename from scripts/k8s-reclaim-csi-volumes.sh rename to scripts/ops/k8s/reclaim-csi-volumes.sh index ffe3e258c..886db498b 100755 --- a/scripts/k8s-reclaim-csi-volumes.sh +++ b/scripts/ops/k8s/reclaim-csi-volumes.sh @@ -16,20 +16,20 @@ # because that is where it first hurt. # # The cloud-side sweep this script defers to now runs on both clouds AFTER the -# cluster is destroyed -- scripts/gcp-sweep-orphaned-disks.sh and -# scripts/aws-sweep-orphaned-volumes.sh, each wired into its own `destroy`. +# cluster is destroyed -- scripts/ops/gcp/sweep-orphaned-disks.sh and +# scripts/ops/aws/sweep-orphaned-volumes.sh, each wired into its own `destroy`. # GCP had no sweep at all before that. AWS had one, but only in prepare-destroy, # before the destroy -- so it missed whatever was still detaching, and left it # for a teardown a rebuild away. # -# Extracted from scripts/eks-prepare-destroy.sh rather than copied: every step +# Extracted from scripts/ops/aws/eks-prepare-destroy.sh rather than copied: every step # below is plain Kubernetes, and a second copy is a second thing to forget. # # MUST run while the CSI controller is still schedulable -- before any node # draining starts. # # Usage: -# k8s-reclaim-csi-volumes.sh [kube-context] +# scripts/ops/k8s/reclaim-csi-volumes.sh [kube-context] # # With no argument the current context is used. Never fails the caller: a # cluster that is already gone, or unreachable, leaves nothing to reclaim and diff --git a/scripts/teardown.sh b/scripts/teardown.sh index a349ca7f8..6c6c0de44 100755 --- a/scripts/teardown.sh +++ b/scripts/teardown.sh @@ -87,11 +87,11 @@ if [ "$VERIFY_ONLY" -eq 0 ]; then --query 'Vpcs[?IsDefault==`false`].VpcId' --output text 2>/dev/null)" if [ -n "${_left//[[:space:]]/}" ]; then echo "=== destroy left resources behind — sweeping controller-created orphans ===" - bash "${ROOT}/scripts/aws-sweep-teardown-blockers.sh" \ + bash "${ROOT}/scripts/ops/aws/sweep-teardown-blockers.sh" \ --cluster-name "${EKS_CLUSTER_NAME:-aws-0}" --region "$_region" --apply || true - bash "${ROOT}/scripts/aws-sweep-controller-orphans.sh" \ + bash "${ROOT}/scripts/ops/aws/sweep-controller-orphans.sh" \ --cluster-name "${EKS_CLUSTER_NAME:-aws-0}" --region "$_region" --apply || true - bash "${ROOT}/scripts/aws-sweep-orphaned-volumes.sh" \ + bash "${ROOT}/scripts/ops/aws/sweep-orphaned-volumes.sh" \ --cluster-name "${EKS_CLUSTER_NAME:-aws-0}" --region "$_region" --apply || true echo diff --git a/security/base/zitadel/sqlinstance.yaml b/security/base/zitadel/sqlinstance.yaml index d390483d3..a18e81d76 100644 --- a/security/base/zitadel/sqlinstance.yaml +++ b/security/base/zitadel/sqlinstance.yaml @@ -55,7 +55,7 @@ spec: # loss versus -2's intent: only the final ~50 seconds, which contain # nothing but the backup itself. # - # LESSON, now encoded in scripts/cnpg-promote-seed.sh (see the rotation + # LESSON, now encoded in scripts/ops/k8s/cnpg-promote-seed.sh (see the rotation # command at the end of this block): after the one-shot Backup completes, # force the segment out before copying — # kubectl exec -n security xplane-zitadel-cnpg-cluster-1 -c postgres -- \ @@ -64,7 +64,7 @@ spec: # prefix, and only then take the copy. # # NOTE: this was a COPY, so the live prefix is NOT empty. A restore - # therefore still needs scripts/cnpg-prepare-restore.sh first (see the + # therefore still needs scripts/ops/k8s/cnpg-prepare-restore.sh first (see the # expected-empty-archive note below). Earlier rotations MOVED the # prefix and got that clearing for free. # [SUPERSEDED by #1963 — kept as a record of what was true on @@ -115,12 +115,12 @@ spec: # uid), so a new cluster's destination is empty because it never existed. # # Rotate this seed with: - # scripts/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ + # scripts/ops/k8s/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ # --cloud aws --bucket -ogenki-cnpg-backups --apply # It discovers the live prefix, forces the final WAL segment out before # copying, and verifies the result is restorable rather than counting objects. # ── zitadel-20260904: the first seed cut by the script, not by hand ───────── - # Created 2026-09-04 by scripts/cnpg-promote-seed.sh --apply against the + # Created 2026-09-04 by scripts/ops/k8s/cnpg-promote-seed.sh --apply against the # rebuilt aws-0, on the live prefix xplane-zitadel-cnpg-cluster-87c1dcbf -- # the first per-generation prefix, and the first promotion that discovered its # source rather than assuming it. diff --git a/security/gcp-0/zitadel/kustomization.yaml b/security/gcp-0/zitadel/kustomization.yaml index 564a8b8a1..4b4f5131b 100644 --- a/security/gcp-0/zitadel/kustomization.yaml +++ b/security/gcp-0/zitadel/kustomization.yaml @@ -33,7 +33,7 @@ patches: # 2026-08-28 and is not any more: gcp-0 now writes real backups, so a seed # is frozen the same way aws-0's is: # - # ./scripts/cnpg-promote-seed.sh --cluster xplane-zitadel \ + # ./scripts/ops/k8s/cnpg-promote-seed.sh --cluster xplane-zitadel \ # --namespace security --cloud gcp \ # --bucket ${project_id}-ogenki-cnpg-backups --apply # diff --git a/website/content/docs/get-started/aws/teardown.md b/website/content/docs/get-started/aws/teardown.md index 56a3c51ef..0f1dfd9fe 100644 --- a/website/content/docs/get-started/aws/teardown.md +++ b/website/content/docs/get-started/aws/teardown.md @@ -20,7 +20,7 @@ terramate script run destroy Five steps, defined in `opentofu/aws/eks/init/workflows.tm.hcl`: -1. **`prepare-destroy`** — runs `scripts/eks-prepare-destroy.sh` (see below). +1. **`prepare-destroy`** — runs `scripts/ops/aws/eks-prepare-destroy.sh` (see below). 2. **`stage2-destroy-addons`** — *attempts* to destroy the `eks/configure` stack (Cilium, Flux) via `scripts/destroy-stage2.sh` in its `attempt` mode. Never fatal: everything that stack manages lives inside the cluster step 3 deletes anyway, so a failure @@ -70,7 +70,7 @@ Before OpenTofu deletes anything, the script: once their pods are evicted with the nodes, every subsequent delete would otherwise fail against a webhook with no live endpoint. - Reclaims CSI-provisioned EBS volumes, by calling - `scripts/k8s-reclaim-csi-volumes.sh` — the same script the GKE teardown + `scripts/ops/k8s/reclaim-csi-volumes.sh` — the same script the GKE teardown calls, since every step of it is plain Kubernetes. It patches **every** PV's `persistentVolumeReclaimPolicy` to `Delete` — including PVs deliberately set to `Retain` — deletes CloudNativePG `Cluster` resources so the operator @@ -129,7 +129,7 @@ emptied for resources that still exist. ## The sweep before the destroy `terramate script run destroy` opens with `stage0-sweep-teardown-blockers`, which -runs `scripts/aws-sweep-teardown-blockers.sh`. It clears the two things that make +runs `scripts/ops/aws/sweep-teardown-blockers.sh`. It clears the two things that make `tofu destroy` **fail**, neither of which Terraform owns: - **ExternalDNS records.** Route53 refuses `DeleteHostedZone` while any record @@ -164,7 +164,7 @@ actually gone. ## The sweep after the destroy `terramate script run destroy` ends with `stage3-sweep-orphaned-volumes`, which -runs `scripts/aws-sweep-orphaned-volumes.sh` once the cluster is gone. +runs `scripts/ops/aws/sweep-orphaned-volumes.sh` once the cluster is gone. It exists because the pre-destroy sweep above runs at the wrong moment to be complete. It fires moments after the PVCs are deleted, so a volume still @@ -186,7 +186,7 @@ Run it by hand if you tore the cluster down some other way. It is a dry run unless you pass `--apply`: ```bash -./scripts/aws-sweep-orphaned-volumes.sh --cluster-name aws-0 --region eu-west-3 +./scripts/ops/aws/sweep-orphaned-volumes.sh --cluster-name aws-0 --region eu-west-3 ``` GCP has the same step as `stage2-sweep-orphaned-disks` — see the diff --git a/website/content/docs/get-started/gcp/teardown.md b/website/content/docs/get-started/gcp/teardown.md index 27519b642..b839a4ed8 100644 --- a/website/content/docs/get-started/gcp/teardown.md +++ b/website/content/docs/get-started/gcp/teardown.md @@ -61,7 +61,7 @@ destroy` says "Destroy complete" and the disks keep billing. Three survived the 2026-08-27 teardown (20/10/5 GB), the GCP replay of an EBS leak EKS already had a step for. -`stage2-reclaim-volumes` calls `scripts/k8s-reclaim-csi-volumes.sh`, shared with +`stage2-reclaim-volumes` calls `scripts/ops/k8s/reclaim-csi-volumes.sh`, shared with the AWS teardown because every step of it is plain Kubernetes — what it does, step by step, is on the [AWS teardown]({{< relref "/docs/get-started/aws/teardown.md#what-eks-prepare-destroysh-does-first" >}}). @@ -88,8 +88,8 @@ from, and never fails the teardown. Run it by hand against any project at any time: ```bash -./scripts/gcp-sweep-orphaned-disks.sh --project # dry run -./scripts/gcp-sweep-orphaned-disks.sh --project --apply +./scripts/ops/gcp/sweep-orphaned-disks.sh --project # dry run +./scripts/ops/gcp/sweep-orphaned-disks.sh --project --apply ``` {{< callout type="warning" >}} @@ -129,7 +129,7 @@ which lands at the *end* of the network destroy, after the rest of the VPC is already gone, leaving the stack half torn down. On 2026-08-27 that meant deleting twelve records by hand. -The network stack's destroy now runs `scripts/gcp-purge-dns-records.sh` first, +The network stack's destroy now runs `scripts/ops/gcp/purge-dns-records.sh` first, reading the zone name from state rather than re-deriving it. Apex NS and SOA are left alone: Cloud DNS will not delete them separately and removes them with the zone. Safe to re-run — it exits 0 when the zone is already gone or already empty. @@ -229,7 +229,7 @@ rather than as an error during the rebuild. {{< /callout >}} You should not normally have to do any of this: the workforce-identity stack's -deploy runs `scripts/gcp-adopt-workforce-pool.sh` first, which undeletes the +deploy runs `scripts/ops/gcp/adopt-workforce-pool.sh` first, which undeletes the pool *and* its provider and imports both into state, so a rebuild inside the 30-day window just works. It is a no-op on a genuinely fresh org. The manual commands are here for when you are diagnosing rather than deploying. diff --git a/website/content/docs/guides/migrate-the-identity-provider.md b/website/content/docs/guides/migrate-the-identity-provider.md index 3aef6bd72..ab128cfcf 100644 --- a/website/content/docs/guides/migrate-the-identity-provider.md +++ b/website/content/docs/guides/migrate-the-identity-provider.md @@ -45,7 +45,7 @@ Examples are AWS → GCP; reverse the cloud arguments to go the other way. [Restore a database]({{< relref "/docs/guides/restore-a-database.md" >}}): ```bash -./scripts/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ +./scripts/ops/k8s/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ --cloud aws --bucket eu-west-3-ogenki-cnpg-backups --apply ``` @@ -56,7 +56,7 @@ aws s3 sync s3://eu-west-3-ogenki-cnpg-backups/zitadel-20260902/ /tmp/zitadel-se gcloud storage rsync --recursive /tmp/zitadel-seed/ \ gs://-ogenki-cnpg-backups/zitadel-20260902/ -./scripts/cnpg-promote-seed.sh --verify-seed zitadel-20260902 \ +./scripts/ops/k8s/cnpg-promote-seed.sh --verify-seed zitadel-20260902 \ --cloud gcp --bucket -ogenki-cnpg-backups ``` diff --git a/website/content/docs/guides/restore-a-database.md b/website/content/docs/guides/restore-a-database.md index 03e8eda59..729f3e585 100644 --- a/website/content/docs/guides/restore-a-database.md +++ b/website/content/docs/guides/restore-a-database.md @@ -36,7 +36,7 @@ archive — a live prefix keeps changing under you, and the whole point is a kno state you can return to. ```bash -./scripts/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ +./scripts/ops/k8s/cnpg-promote-seed.sh --cluster xplane-zitadel --namespace security \ --cloud gcp --bucket -ogenki-cnpg-backups --apply ``` @@ -84,7 +84,7 @@ bootstraps empty. Recovery is unaffected by any of this: it reads `spec.objectStoreRecovery.path` explicitly and never touches a live archive. {{< callout type="info" >}} -`scripts/cnpg-prepare-restore.sh` still exists as an escape hatch for the cases +`scripts/ops/k8s/cnpg-prepare-restore.sh` still exists as an escape hatch for the cases that still collide: a cluster pinned to an explicit `serverName`, an archive left behind by a pre-#1963 generation, or a deliberate reuse of a prefix. It refuses to clear a live archive unless the named seed actually holds a base @@ -138,7 +138,7 @@ identity providers. All five matched exactly: ## Rotating the seed Refresh it when the database changes meaningfully — new OAuth apps, a schema -migration, significant user growth. Re-run `scripts/cnpg-promote-seed.sh` (if +migration, significant user growth. Re-run `scripts/ops/k8s/cnpg-promote-seed.sh` (if `--seed` is omitted it defaults to the claim name with any `xplane-` prefix removed, plus today's date) and update `path`. The old prefix costs a few tens of megabytes; keep it until the new one has been restored from at least once. diff --git a/website/content/docs/reference/commands.md b/website/content/docs/reference/commands.md index 90de47d40..15db3a2c4 100644 --- a/website/content/docs/reference/commands.md +++ b/website/content/docs/reference/commands.md @@ -235,13 +235,13 @@ each gate actually checks. | `openbao-snapshot.sh` | OpenBao Raft snapshot automation | | `secret-store.sh` | Inspects and seeds the cloud secret store backing External Secrets (`check`, `seed`, `migrate-aws`) | | `terramate-destroy-confirm.sh` | Single y/n prompt every stack's destroy script calls first, cached so `--reverse destroy` asks once | -| `eks-prepare-destroy.sh` | Pre-destroy EKS cleanup — suspends Flux, disables blocking webhooks, sweeps orphaned EBS volumes; the CSI volume reclaim itself moved to `k8s-reclaim-csi-volumes.sh` | +| `eks-prepare-destroy.sh` | Pre-destroy EKS cleanup — suspends Flux, disables blocking webhooks, sweeps orphaned EBS volumes; the CSI volume reclaim itself moved to `reclaim-csi-volumes.sh` | | `eks-recycle-bootstrap-nodes.sh` | Recycles Stage 1 node-group nodes so they pick up Cilium prefix delegation | -| `k8s-reclaim-csi-volumes.sh` | Reclaims CSI-provisioned volumes before a cluster destroy — cloud-neutral, called by both teardown paths | +| `reclaim-csi-volumes.sh` | Reclaims CSI-provisioned volumes before a cluster destroy — cloud-neutral, called by both teardown paths | | `destroy-stage2.sh` | Graceful-then-reconcile teardown of either cloud's `configure` stack, never gating the cluster delete | -| `gcp-purge-dns-records.sh` | Empties a Cloud DNS zone of external-dns leftovers so `tofu destroy` can delete it | +| `purge-dns-records.sh` | Empties a Cloud DNS zone of external-dns leftovers so `tofu destroy` can delete it | | `export-diagrams.sh` | Exports `.drawio` architecture diagrams to PNG | | `cleanup-benchmark-images.sh` | Cleans up images left behind by the image-gallery/benchmark scripts | -| `demo-load.sh` | Runs an image-gallery load-generator scenario in-cluster (`browse`, `upload`, `mixed`, `steady`, `incident`) from the suspended `image-gallery-loadgen` CronJob | +| `load.sh` | Runs an image-gallery load-generator scenario in-cluster (`browse`, `upload`, `mixed`, `steady`, `incident`) from the suspended `image-gallery-loadgen` CronJob | | `test-flux-schema.sh` | Quarantined, never run: it asserts bundle filenames the render has outgrown. See `scripts/ci/tests/quarantine/README.md` | | `test-vector-vrl.sh` / `validate-vector-vrl.sh` / `vector-vrl-tests/` | Validate the Vector log-parsing configuration | From 10cfecef8eb00346c277c6d7433ffd681316fb92 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:34:32 +0200 Subject: [PATCH 09/19] refactor(scripts): move the teardown scripts to scripts/ops/teardown/ teardown.sh is the supported teardown entry point; the other three are what terramate destroy calls (22 of the 34 references). The terramate reference gate went from 22 failed to 0. --- opentofu/aws/eks/init/workflows.tm.hcl | 6 +++--- opentofu/aws/llm-platform/workflows.tm.hcl | 2 +- opentofu/aws/openbao/cluster/workflows.tm.hcl | 2 +- opentofu/aws/openbao/lineage/workflows.tm.hcl | 2 +- opentofu/aws/openbao/management/workflows.tm.hcl | 4 ++-- opentofu/gcp/gke/configure/workflows.tm.hcl | 4 ++-- opentofu/gcp/gke/init/workflows.tm.hcl | 6 +++--- opentofu/gcp/network/workflows.tm.hcl | 2 +- opentofu/gcp/openbao/cluster/workflows.tm.hcl | 2 +- opentofu/gcp/openbao/lineage/workflows.tm.hcl | 2 +- opentofu/gcp/openbao/management/workflows.tm.hcl | 4 ++-- opentofu/gcp/workforce-identity/workflows.tm.hcl | 2 +- opentofu/shared/aws-gcp-federation/workflows.tm.hcl | 2 +- opentofu/shared/tailscale/workflows.tm.hcl | 2 +- opentofu/workflows.tm.hcl | 2 +- scripts/ops/aws/eks-prepare-destroy.sh | 2 +- scripts/{ => ops/teardown}/destroy-stage2.sh | 2 +- scripts/{ => ops/teardown}/teardown.sh | 10 +++++----- .../{ => ops/teardown}/terramate-destroy-confirm.sh | 2 +- scripts/{ => ops/teardown}/tofu-destroy-contained.sh | 0 website/content/docs/get-started/aws/teardown.md | 6 +++--- website/content/docs/get-started/gcp/teardown.md | 2 +- website/content/docs/reference/commands.md | 2 +- 23 files changed, 35 insertions(+), 35 deletions(-) rename scripts/{ => ops/teardown}/destroy-stage2.sh (98%) rename scripts/{ => ops/teardown}/teardown.sh (95%) rename scripts/{ => ops/teardown}/terramate-destroy-confirm.sh (96%) rename scripts/{ => ops/teardown}/tofu-destroy-contained.sh (100%) diff --git a/opentofu/aws/eks/init/workflows.tm.hcl b/opentofu/aws/eks/init/workflows.tm.hcl index 8a875c6f3..6b36ff9a2 100644 --- a/opentofu/aws/eks/init/workflows.tm.hcl +++ b/opentofu/aws/eks/init/workflows.tm.hcl @@ -249,7 +249,7 @@ script "destroy" { commands = [ # Single y/n prompt; cached for 10 min so `--reverse destroy` asks once. # Bypass with TM_DESTROY_CONFIRMED=true for CI. - ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh"], + ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh"], # Init before anything is torn down: a lock file predating a new provider # must fail here, not after Flux has been suspended. Same stack dir as the # stage1-destroy-cluster job below, so that job inherits this init. @@ -322,7 +322,7 @@ script "destroy" { ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "-c", "if ! (cd ../configure && bash '${terramate.root.path.fs.absolute}/scripts/openbao-config.sh' ca --root-ca-secret-name '${global.ca_chain_secret_name}' --ca-output-file .tls/ca.pem --region '${global.region}' --profile '${global.profile}'); then echo '[warn] CA chain fetch failed -- continuing anyway.'; echo ' The vault provider will fail to configure and destroy-stage2.sh will'; echo ' fall through to its tolerant path. Failing here instead would strand'; echo ' the live EKS cluster stage 1 is about to delete.'; fi"], ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "-c", - "bash '${terramate.root.path.fs.absolute}/scripts/destroy-stage2.sh' attempt '${terramate.root.path.fs.absolute}/opentofu/aws/eks/configure' -var='cilium_version=${global.cilium_version}' -var='gateway_api_version=${global.gateway_api_version}' -var='flux_operator_version=${global.flux_operator_version}' -var='flux_instance_version=${global.flux_instance_version}'"], + "bash '${terramate.root.path.fs.absolute}/scripts/ops/teardown/destroy-stage2.sh' attempt '${terramate.root.path.fs.absolute}/opentofu/aws/eks/configure' -var='cilium_version=${global.cilium_version}' -var='gateway_api_version=${global.gateway_api_version}' -var='flux_operator_version=${global.flux_operator_version}' -var='flux_instance_version=${global.flux_instance_version}'"], ] } @@ -413,7 +413,7 @@ script "destroy" { description = "Drop stage-2 state entries whose cluster no longer exists" commands = [ ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "-c", - "bash '${terramate.root.path.fs.absolute}/scripts/destroy-stage2.sh' reconcile '${terramate.root.path.fs.absolute}/opentofu/aws/eks/configure'"], + "bash '${terramate.root.path.fs.absolute}/scripts/ops/teardown/destroy-stage2.sh' reconcile '${terramate.root.path.fs.absolute}/opentofu/aws/eks/configure'"], ] } } diff --git a/opentofu/aws/llm-platform/workflows.tm.hcl b/opentofu/aws/llm-platform/workflows.tm.hcl index 8cfb65ded..fa806b4ee 100644 --- a/opentofu/aws/llm-platform/workflows.tm.hcl +++ b/opentofu/aws/llm-platform/workflows.tm.hcl @@ -128,7 +128,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m ${global.provisioner} destroy -auto-approve -var-file=variables.tfvars SCRIPT diff --git a/opentofu/aws/openbao/cluster/workflows.tm.hcl b/opentofu/aws/openbao/cluster/workflows.tm.hcl index 697cf816c..8446974bd 100644 --- a/opentofu/aws/openbao/cluster/workflows.tm.hcl +++ b/opentofu/aws/openbao/cluster/workflows.tm.hcl @@ -17,7 +17,7 @@ script "destroy" { name = "destroy" description = "Confirm, snapshot, destroy" commands = [ - ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh"], + ["bash", "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh", "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh"], # The CA fetch and the snapshot share ONE gate, in one bash step. # # They were two separate ungated steps, and that made diff --git a/opentofu/aws/openbao/lineage/workflows.tm.hcl b/opentofu/aws/openbao/lineage/workflows.tm.hcl index 409c9033a..2cb33b3b5 100644 --- a/opentofu/aws/openbao/lineage/workflows.tm.hcl +++ b/opentofu/aws/openbao/lineage/workflows.tm.hcl @@ -29,7 +29,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m ${global.provisioner} destroy -auto-approve -var-file=variables.tfvars BASH diff --git a/opentofu/aws/openbao/management/workflows.tm.hcl b/opentofu/aws/openbao/management/workflows.tm.hcl index e7038cf59..ed1b8875f 100644 --- a/opentofu/aws/openbao/management/workflows.tm.hcl +++ b/opentofu/aws/openbao/management/workflows.tm.hcl @@ -89,7 +89,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh" --tm-run bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh" --tm-run bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" # CA fetch BEFORE `tofu init`, as in this file's `deploy` and in both # openbao/cluster stacks. The fetch is the step that can fail -- an # unreadable or missing ca-chain secret -- and `tofu init` is a backend @@ -116,7 +116,7 @@ script "destroy" { # random_password in this same state are real resources, do not match # the prefix, and tofu still has to delete them. bash "${terramate.root.path.fs.absolute}/scripts/tm-provisioner.sh" --tm-run \ - bash "${terramate.root.path.fs.absolute}/scripts/tofu-destroy-contained.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/tofu-destroy-contained.sh" \ --contained-prefix vault_ -- \ -auto-approve -parallelism=1 -var-file=variables.tfvars BASH diff --git a/opentofu/gcp/gke/configure/workflows.tm.hcl b/opentofu/gcp/gke/configure/workflows.tm.hcl index 77e8fde02..bcc981df6 100644 --- a/opentofu/gcp/gke/configure/workflows.tm.hcl +++ b/opentofu/gcp/gke/configure/workflows.tm.hcl @@ -149,7 +149,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" # The CA fetch is BEST-EFFORT here, and only here -- deploy and preview # above keep it strict, because there the vault provider must configure # for the apply to mean anything. @@ -179,7 +179,7 @@ script "destroy" { echo " will fall through to its tolerant path. Failing here instead" echo " would strand the live GKE cluster gke/init is about to delete." fi - bash "${terramate.root.path.fs.absolute}/scripts/destroy-stage2.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/destroy-stage2.sh" \ attempt "${terramate.root.path.fs.absolute}/opentofu/gcp/gke/configure" \ -var='cilium_version=${global.cilium_version}' \ -var='gateway_api_version=${global.gateway_api_version}' \ diff --git a/opentofu/gcp/gke/init/workflows.tm.hcl b/opentofu/gcp/gke/init/workflows.tm.hcl index 429b17916..14f9a2f48 100644 --- a/opentofu/gcp/gke/init/workflows.tm.hcl +++ b/opentofu/gcp/gke/init/workflows.tm.hcl @@ -366,7 +366,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" # Init before anything is torn down: a lock file predating a new provider # must fail here, not after resources have started disappearing. Same stack # dir as stage1-destroy-cluster, so that job inherits this init. @@ -431,7 +431,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/destroy-stage2.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/destroy-stage2.sh" \ attempt "${terramate.root.path.fs.absolute}/opentofu/gcp/gke/configure" \ -var='cilium_version=${global.cilium_version}' \ -var='gateway_api_version=${global.gateway_api_version}' \ @@ -514,7 +514,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/destroy-stage2.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/destroy-stage2.sh" \ reconcile "${terramate.root.path.fs.absolute}/opentofu/gcp/gke/configure" BASH ], diff --git a/opentofu/gcp/network/workflows.tm.hcl b/opentofu/gcp/network/workflows.tm.hcl index 6804376e4..610b33566 100644 --- a/opentofu/gcp/network/workflows.tm.hcl +++ b/opentofu/gcp/network/workflows.tm.hcl @@ -93,7 +93,7 @@ script "destroy" { ["bash", "-c", <<-BASH ${global.cloud_gate} set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m # Empty the private Cloud DNS zone first. external-dns wrote a record for diff --git a/opentofu/gcp/openbao/cluster/workflows.tm.hcl b/opentofu/gcp/openbao/cluster/workflows.tm.hcl index 4b588ffb6..cc8c74ca5 100644 --- a/opentofu/gcp/openbao/cluster/workflows.tm.hcl +++ b/opentofu/gcp/openbao/cluster/workflows.tm.hcl @@ -127,7 +127,7 @@ script "destroy" { commands = [ # The confirmation prompt is its own step because it must run whether or # not the snapshot is skipped. - ["bash", global.provisioner, "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh"], + ["bash", global.provisioner, "--tm-run", "bash", "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh"], # One last snapshot into the lineage bucket before the node goes -- the # in-cluster CronJob is already gone at this point of a reverse destroy. # Fails hard when OpenBao is unreachable; TM_OPENBAO_SKIP_SNAPSHOT=true diff --git a/opentofu/gcp/openbao/lineage/workflows.tm.hcl b/opentofu/gcp/openbao/lineage/workflows.tm.hcl index 9bed1fb83..05f1d4267 100644 --- a/opentofu/gcp/openbao/lineage/workflows.tm.hcl +++ b/opentofu/gcp/openbao/lineage/workflows.tm.hcl @@ -104,7 +104,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m ${global.provisioner} destroy -auto-approve -var-file=variables.tfvars BASH diff --git a/opentofu/gcp/openbao/management/workflows.tm.hcl b/opentofu/gcp/openbao/management/workflows.tm.hcl index 7ddd095f7..33cfc3d0c 100644 --- a/opentofu/gcp/openbao/management/workflows.tm.hcl +++ b/opentofu/gcp/openbao/management/workflows.tm.hcl @@ -198,7 +198,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m ${global.openbao_ca_fetch} # Contained destroy, matching aws/openbao/management. Every `vault_*` @@ -222,7 +222,7 @@ script "destroy" { # Only `vault_*` is dropped from state. The google_secret_manager_* and # random_password resources here are real and do not match the prefix, # so tofu still deletes them. - bash "${terramate.root.path.fs.absolute}/scripts/tofu-destroy-contained.sh" \ + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/tofu-destroy-contained.sh" \ --contained-prefix vault_ -- \ -auto-approve -parallelism=1 -var-file=variables.tfvars BASH diff --git a/opentofu/gcp/workforce-identity/workflows.tm.hcl b/opentofu/gcp/workforce-identity/workflows.tm.hcl index 22fa9ece4..25d6d0a57 100644 --- a/opentofu/gcp/workforce-identity/workflows.tm.hcl +++ b/opentofu/gcp/workforce-identity/workflows.tm.hcl @@ -119,7 +119,7 @@ script "destroy" { [warn] can undo by re-running deploy. [warn] ───────────────────────────────────────────────────────────────── WARN - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" ${global.provisioner} init -lock-timeout=5m ${global.provisioner} destroy -auto-approve -var-file=variables.tfvars -var='deploy_identity_provider=${global.deploy_identity_provider_gcp}' BASH diff --git a/opentofu/shared/aws-gcp-federation/workflows.tm.hcl b/opentofu/shared/aws-gcp-federation/workflows.tm.hcl index 00fdb8365..17b7ee409 100644 --- a/opentofu/shared/aws-gcp-federation/workflows.tm.hcl +++ b/opentofu/shared/aws-gcp-federation/workflows.tm.hcl @@ -47,7 +47,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" # `destroy` is a standalone entrypoint: unlike `deploy` it can be the first # tofu command run in a stack, so it has to init itself. ${global.provisioner} init -lock-timeout=5m diff --git a/opentofu/shared/tailscale/workflows.tm.hcl b/opentofu/shared/tailscale/workflows.tm.hcl index 8b63a619f..ecb2370a6 100644 --- a/opentofu/shared/tailscale/workflows.tm.hcl +++ b/opentofu/shared/tailscale/workflows.tm.hcl @@ -45,7 +45,7 @@ script "destroy" { exit 0 fi set -euo pipefail - bash "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh" + bash "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh" # `destroy` is a standalone entrypoint: unlike `deploy` it can be the first # tofu command run in a stack, so it has to init itself. ${global.provisioner} init -lock-timeout=5m diff --git a/opentofu/workflows.tm.hcl b/opentofu/workflows.tm.hcl index 098e33e24..009e97302 100644 --- a/opentofu/workflows.tm.hcl +++ b/opentofu/workflows.tm.hcl @@ -98,7 +98,7 @@ script "destroy" { commands = [ # Single y/n prompt; cached for 10 min so `--reverse destroy` asks once. # Bypass with TM_DESTROY_CONFIRMED=true for CI. - ["bash", "${terramate.root.path.fs.absolute}/scripts/terramate-destroy-confirm.sh"], + ["bash", "${terramate.root.path.fs.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh"], # `destroy` is a standalone entrypoint: unlike `deploy` it can be the first # tofu command run in a stack, so it has to init itself. Without this a lock # file predating a new provider fails the whole `--reverse destroy` sweep. diff --git a/scripts/ops/aws/eks-prepare-destroy.sh b/scripts/ops/aws/eks-prepare-destroy.sh index 5532792f6..8f295b78b 100755 --- a/scripts/ops/aws/eks-prepare-destroy.sh +++ b/scripts/ops/aws/eks-prepare-destroy.sh @@ -40,7 +40,7 @@ fi echo "This script will delete the EKS cluster ${CLUSTER_NAME} in region ${REGION}" echo "This action is irreversible and will delete all resources in the cluster" echo "Please ensure you have backed up any important data before proceeding" -# Same env-var bypass as scripts/terramate-destroy-confirm.sh — lets a +# Same env-var bypass as scripts/ops/teardown/terramate-destroy-confirm.sh — lets a # `terramate script run --reverse destroy` orchestrate this prep step # without a second human prompt after the user already consented once. if [ "${TM_DESTROY_CONFIRMED:-false}" = "true" ]; then diff --git a/scripts/destroy-stage2.sh b/scripts/ops/teardown/destroy-stage2.sh similarity index 98% rename from scripts/destroy-stage2.sh rename to scripts/ops/teardown/destroy-stage2.sh index bc2a5c885..cbd298a7f 100755 --- a/scripts/destroy-stage2.sh +++ b/scripts/ops/teardown/destroy-stage2.sh @@ -128,7 +128,7 @@ reconcile) echo "[error] could not read the stage-2 state." >&2 echo "[error] Refusing to assume it is empty: that would leave real drift" >&2 echo "[error] behind and report success. Fix the backend, then re-run:" >&2 - echo "[error] bash scripts/destroy-stage2.sh reconcile ${dir}" >&2 + echo "[error] bash scripts/ops/teardown/destroy-stage2.sh reconcile ${dir}" >&2 exit 1 fi diff --git a/scripts/teardown.sh b/scripts/ops/teardown/teardown.sh similarity index 95% rename from scripts/teardown.sh rename to scripts/ops/teardown/teardown.sh index 6c6c0de44..a7ca04b68 100755 --- a/scripts/teardown.sh +++ b/scripts/ops/teardown/teardown.sh @@ -26,16 +26,16 @@ # finished when the provider says there is nothing left. # # Usage: -# scripts/teardown.sh # aws (the TM_CLOUD default) -# TM_CLOUD=gcp scripts/teardown.sh -# TM_CLOUD=all scripts/teardown.sh -# scripts/teardown.sh --verify-only # skip the destroy, just report what is left +# scripts/ops/teardown/teardown.sh # aws (the TM_CLOUD default) +# TM_CLOUD=gcp scripts/ops/teardown/teardown.sh +# TM_CLOUD=all scripts/ops/teardown/teardown.sh +# scripts/ops/teardown/teardown.sh --verify-only # skip the destroy, just report what is left # # TM_DESTROY_CONFIRMED=true skips the interactive prompt, for unattended runs. set -o nounset set -o pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" CLOUDS="${TM_CLOUD:-aws}" VERIFY_ONLY=0 [ "${1:-}" = "--verify-only" ] && VERIFY_ONLY=1 diff --git a/scripts/terramate-destroy-confirm.sh b/scripts/ops/teardown/terramate-destroy-confirm.sh similarity index 96% rename from scripts/terramate-destroy-confirm.sh rename to scripts/ops/teardown/terramate-destroy-confirm.sh index f8cc673ae..a614a5508 100755 --- a/scripts/terramate-destroy-confirm.sh +++ b/scripts/ops/teardown/terramate-destroy-confirm.sh @@ -9,7 +9,7 @@ # # Usage in workflows.tm.hcl: # commands = [ -# ["bash", "${terramate.root.path.absolute}/scripts/terramate-destroy-confirm.sh"], +# ["bash", "${terramate.root.path.absolute}/scripts/ops/teardown/terramate-destroy-confirm.sh"], # [global.provisioner, "destroy", ...], # ] # diff --git a/scripts/tofu-destroy-contained.sh b/scripts/ops/teardown/tofu-destroy-contained.sh similarity index 100% rename from scripts/tofu-destroy-contained.sh rename to scripts/ops/teardown/tofu-destroy-contained.sh diff --git a/website/content/docs/get-started/aws/teardown.md b/website/content/docs/get-started/aws/teardown.md index 0f1dfd9fe..9b58601d6 100644 --- a/website/content/docs/get-started/aws/teardown.md +++ b/website/content/docs/get-started/aws/teardown.md @@ -22,7 +22,7 @@ Five steps, defined in `opentofu/aws/eks/init/workflows.tm.hcl`: 1. **`prepare-destroy`** — runs `scripts/ops/aws/eks-prepare-destroy.sh` (see below). 2. **`stage2-destroy-addons`** — *attempts* to destroy the `eks/configure` stack - (Cilium, Flux) via `scripts/destroy-stage2.sh` in its `attempt` mode. Never fatal: everything + (Cilium, Flux) via `scripts/ops/teardown/destroy-stage2.sh` in its `attempt` mode. Never fatal: everything that stack manages lives inside the cluster step 3 deletes anyway, so a failure here must not strand the one billable resource ([why](#stage-2-never-gates-the-cluster)). 3. **`stage1-destroy-cluster`** — destroys the `eks/init` stack (the cluster itself). @@ -42,7 +42,7 @@ TM_CLOUD=all terramate script run --reverse destroy # both clouds ``` Reverse dependency order, with a single confirmation prompt -(`scripts/terramate-destroy-confirm.sh`) cached for 10 minutes so the whole +(`scripts/ops/teardown/terramate-destroy-confirm.sh`) cached for 10 minutes so the whole sweep only asks once. `TM_DESTROY_CONFIRMED=true` skips it for CI. {{< callout type="info" >}} @@ -102,7 +102,7 @@ Before OpenTofu deletes anything, the script: The `eks/configure` stack manages Cilium, the Flux Operator and the Flux Instance — all of them objects *inside* the cluster that stage 1 deletes moments later. Its -teardown is therefore tidiness, never a prerequisite, and `scripts/destroy-stage2.sh` +teardown is therefore tidiness, never a prerequisite, and `scripts/ops/teardown/destroy-stage2.sh` enforces that: `attempt` reports a failure and exits 0. Both clouds proved why the hard version is wrong: diff --git a/website/content/docs/get-started/gcp/teardown.md b/website/content/docs/get-started/gcp/teardown.md index b839a4ed8..988400c4b 100644 --- a/website/content/docs/get-started/gcp/teardown.md +++ b/website/content/docs/get-started/gcp/teardown.md @@ -25,7 +25,7 @@ TM_CLOUD=gcp terramate script run destroy Six jobs, defined in `opentofu/gcp/gke/init/workflows.tm.hcl`: -1. **confirm + init** — `scripts/terramate-destroy-confirm.sh`, then `tofu init`. +1. **confirm + init** — `scripts/ops/teardown/terramate-destroy-confirm.sh`, then `tofu init`. Init runs *before* anything is destroyed on purpose: a lock file predating a new provider must fail here, not once resources have started disappearing. 2. **`stage2-reclaim-volumes`** — reclaims CSI-provisioned PD disks while the diff --git a/website/content/docs/reference/commands.md b/website/content/docs/reference/commands.md index 15db3a2c4..b4b2d8da6 100644 --- a/website/content/docs/reference/commands.md +++ b/website/content/docs/reference/commands.md @@ -55,7 +55,7 @@ default behaviour". |---|---|---|---| | `TM_CLOUD` | *not a boolean* — a comma list of lanes, or `all` | `aws`: AWS stacks run, GCP stacks echo `[skip]` and exit 0, `opentofu/shared/**` runs under every value | Every `tofu` call, via `scripts/tm-provisioner.sh` behind `global.provisioner`; plus the non-tofu jobs that carry `--tm-run` or `${global.cloud_gate}` | | `TM_LLM_PLATFORM_ENABLED` | the stack's `deploy`/`preview`/`drift detect`/`destroy` run | `[skip]`, exit 0 — the platform is never built by a bare `terramate script run deploy` | `opentofu/aws/llm-platform/workflows.tm.hcl` | -| `TM_DESTROY_CONFIRMED` | the y/n prompt is bypassed (this is the CI escape hatch) | prompts once on `/dev/tty`, cached 10 min so `--reverse destroy` asks once; **exits 1** when there is no tty | `scripts/terramate-destroy-confirm.sh`, called first by every stack's `destroy` | +| `TM_DESTROY_CONFIRMED` | the y/n prompt is bypassed (this is the CI escape hatch) | prompts once on `/dev/tty`, cached 10 min so `--reverse destroy` asks once; **exits 1** when there is no tty | `scripts/ops/teardown/terramate-destroy-confirm.sh`, called first by every stack's `destroy` | | `TM_LINEAGE_DESTROY` | the four lineage-bearing stacks are destroyed | `[skip]`, exit 0 — a `--reverse destroy` sweep leaves the seal key, both snapshot buckets, the PKI mount and the JWT auth mounts standing | `destroy` in `opentofu/aws/openbao/{lineage,management}` and `opentofu/gcp/openbao/{lineage,management}` | | `TM_OPENBAO_SKIP_SNAPSHOT` | **inverted** — the CA fetch and the pre-destroy raft snapshot are skipped, and everything written since the last scheduled snapshot is lost | the snapshot is taken, and a failure aborts the destroy rather than stranding a node's data | `destroy` in `opentofu/{aws,gcp}/openbao/cluster`, and the `pre-destroy-snapshot` subcommand of `scripts/openbao-config.sh` | | `TM_TAILNET_DESTROY` | the tailnet-wide singletons are destroyed | `[skip]`, exit 0 — tearing down one cloud does not remove tailnet access for the other | `destroy` in `opentofu/shared/tailscale/workflows.tm.hcl` | From 5b93dc9e2a60cb88605e61c7ff739c7eaeaf874f Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:35:01 +0200 Subject: [PATCH 10/19] refactor(scripts): move check-rebased.sh to scripts/ci/ --- .pre-commit-config.yaml | 2 +- scripts/{ => ci}/check-rebased.sh | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{ => ci}/check-rebased.sh (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7ab0be3c..5d52aacf6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,7 +73,7 @@ repos: hooks: - id: check-rebased name: Branch is rebased on origin/main - entry: scripts/check-rebased.sh + entry: scripts/ci/check-rebased.sh language: script stages: [pre-push] always_run: true diff --git a/scripts/check-rebased.sh b/scripts/ci/check-rebased.sh similarity index 100% rename from scripts/check-rebased.sh rename to scripts/ci/check-rebased.sh From c41ecdd2f86dfe2541b318f697ff6b2ee17911ff Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:37:00 +0200 Subject: [PATCH 11/19] feat(task): index ops/ and docs/ in task --list Spec criterion 2: every entry point has a one-line description. Only teardown.sh is indexed in ops/teardown/; terramate calls the rest. --- scripts/README.md | 8 +++++-- scripts/docs/tasks.yaml | 10 +++++++++ scripts/ops/tasks.yaml | 47 +++++++++++++++++++++++++++++++++++++++++ taskfile.yaml | 4 ++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 scripts/docs/tasks.yaml create mode 100644 scripts/ops/tasks.yaml diff --git a/scripts/README.md b/scripts/README.md index 5f25180b1..f9a66cd1e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,7 +8,11 @@ directly — none of them depend on the task runner. |---|---| | `ci/` | the gates CI runs, and you before pushing. `task check` runs every one CI runs | | `ci/tests/` | suites `run.sh` discovers: `test-*.sh` and `test-*.py` here, `*/test-*.py` one level down. A `# requires:` tool that is absent, or an exit 77, reports `SKIP` | +| `ops/aws/`, `ops/gcp/`, `ops/k8s/` | day-2 operations, run by a human. Some are also called from terramate destroy scripts | +| `ops/teardown/` | `teardown.sh` is the supported way to tear the platform down (`task ops:teardown`). The other three are called by terramate destroy scripts | +| `ops/demo/` | demo load generation and cleanup | +| `docs/` | docs-site generators, run by hand. `build-og-card.html` opens in a browser | | `lib/` | sourced by the others, never run directly | -Directories for day-2 operations and apply-time provisioning land in later phases of this -restructure; until then those scripts remain at the root of `scripts/`. +Apply-time provisioning scripts move to `provision/` in the next phase; until then they remain at +the root of `scripts/`. diff --git a/scripts/docs/tasks.yaml b/scripts/docs/tasks.yaml new file mode 100644 index 000000000..d21a62f90 --- /dev/null +++ b/scripts/docs/tasks.yaml @@ -0,0 +1,10 @@ +version: "3" + +# build-og-card.html is not a task: open it in a browser and screenshot it. +tasks: + export-diagrams: + desc: Regenerate every diagram SVG the site embeds from its .drawio source (needs the pinned drawio) + cmds: ["{{.TASKFILE_DIR}}/export-diagrams.sh {{.CLI_ARGS}}"] + diagram-icons: + desc: The icon library behind docs/architecture/*.drawio — `task docs:diagram-icons -- audit` + cmds: ["python3 {{.TASKFILE_DIR}}/diagram-icons.py {{.CLI_ARGS}}"] diff --git a/scripts/ops/tasks.yaml b/scripts/ops/tasks.yaml new file mode 100644 index 000000000..5c3fa69d9 --- /dev/null +++ b/scripts/ops/tasks.yaml @@ -0,0 +1,47 @@ +version: "3" + +# The three helpers beside teardown.sh are not indexed: terramate destroy +# scripts call them. teardown.sh is the entry point. +tasks: + teardown: + desc: Tear the platform down — destroy past failures, sweep controller orphans, retry, verify against the cloud + cmds: ["{{.TASKFILE_DIR}}/teardown/teardown.sh {{.CLI_ARGS}}"] + aws:sweep-orphaned-volumes: + desc: Delete EBS volumes the CSI driver created that nothing uses, after a cluster destroy + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-orphaned-volumes.sh {{.CLI_ARGS}}"] + aws:sweep-teardown-blockers: + desc: Clear the two things that reliably block tofu destroy on AWS + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-teardown-blockers.sh {{.CLI_ARGS}}"] + aws:sweep-controller-orphans: + desc: Sweep what in-cluster controllers left in AWS after a partial teardown (refuses while the cluster exists) + cmds: ["{{.TASKFILE_DIR}}/aws/sweep-controller-orphans.sh {{.CLI_ARGS}}"] + aws:eks-prepare-destroy: + desc: Prepare an EKS cluster for destruction — suspends Flux, deletes every PVC + cmds: ["{{.TASKFILE_DIR}}/aws/eks-prepare-destroy.sh {{.CLI_ARGS}}"] + aws:eks-recycle-bootstrap-nodes: + desc: Recycle EKS nodes whose ENIs predate Cilium + cmds: ["{{.TASKFILE_DIR}}/aws/eks-recycle-bootstrap-nodes.sh {{.CLI_ARGS}}"] + gcp:adopt-workforce-pool: + desc: Make the workforce pool survive a teardown and rebuild + cmds: ["{{.TASKFILE_DIR}}/gcp/adopt-workforce-pool.sh {{.CLI_ARGS}}"] + gcp:purge-dns-records: + desc: Empty a Cloud DNS managed zone so the zone can be destroyed + cmds: ["{{.TASKFILE_DIR}}/gcp/purge-dns-records.sh {{.CLI_ARGS}}"] + gcp:sweep-orphaned-disks: + desc: Delete Persistent Disks GKE's CSI driver created that nothing uses + cmds: ["{{.TASKFILE_DIR}}/gcp/sweep-orphaned-disks.sh {{.CLI_ARGS}}"] + k8s:reclaim-csi-volumes: + desc: Reclaim CSI-provisioned volumes before a cluster destroy (cloud-neutral) + cmds: ["{{.TASKFILE_DIR}}/k8s/reclaim-csi-volumes.sh {{.CLI_ARGS}}"] + k8s:cnpg-prepare-restore: + desc: Clear a CNPG cluster's live WAL archive so a new cluster can start (off the normal path) + cmds: ["{{.TASKFILE_DIR}}/k8s/cnpg-prepare-restore.sh {{.CLI_ARGS}}"] + k8s:cnpg-promote-seed: + desc: Promote a live CNPG archive to a frozen restore seed, or verify one + cmds: ["{{.TASKFILE_DIR}}/k8s/cnpg-promote-seed.sh {{.CLI_ARGS}}"] + demo:load: + desc: Run an image-gallery load-generator scenario in-cluster + cmds: ["{{.TASKFILE_DIR}}/demo/load.sh {{.CLI_ARGS}}"] + demo:cleanup-benchmark-images: + desc: Remove benchmark-generated images from the image-gallery database and bucket + cmds: ["{{.TASKFILE_DIR}}/demo/cleanup-benchmark-images.sh {{.CLI_ARGS}}"] diff --git a/taskfile.yaml b/taskfile.yaml index 9a57faa98..7821e8ec4 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -6,6 +6,10 @@ includes: # file and no ci:* task would exist. ci: taskfile: scripts/tasks.yaml + ops: + taskfile: scripts/ops/tasks.yaml + docs: + taskfile: scripts/docs/tasks.yaml tasks: default: From 443ea0ad6705138a3dd94aa986ece643d5342ada Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:54:55 +0200 Subject: [PATCH 12/19] fix(ci): tell a sibling path from a root climb in the paths gate Check 1 flagged any dirname+.. idiom that didn't resolve to the repo root, which made every correct Task 4/5 fix false-positive: climbing to a sibling directory (lib/, k8s/) isn't a root climb. Classify by what follows the climb -- a named path segment means "sibling", checked for existence; anything else (the climb ends the path, or a variable follows it) means "root", checked against the two markers as before. This also means eks-prepare-destroy.sh's exec of ../k8s/reclaim-csi-volumes.sh is now gated -- 169b2493 said no gate saw it; it does now, via the new "relative paths" count. 11 roots, 4 relative paths, 21 sources, 16 subjects checked; 0 failed. --- scripts/ci/tests/test-script-paths.sh | 38 ++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/scripts/ci/tests/test-script-paths.sh b/scripts/ci/tests/test-script-paths.sh index ed93eb4ae..1b0be2324 100755 --- a/scripts/ci/tests/test-script-paths.sh +++ b/scripts/ci/tests/test-script-paths.sh @@ -2,11 +2,14 @@ # requires: # # Two things break silently when a script moves, and neither is an external -# reference. The "/.." depth it uses to reach the repo root: a wrong count makes -# `cd` SUCCEED at the wrong directory, so every relative path after it is quietly -# wrong. And the relative path it sources a library, or reaches its test subject, -# through. Six of the nine lib/-sourcing scripts run during a terramate apply, so -# that failure lands mid-deploy with no CI gate in front of it. +# reference. The "/.." depth it uses: to reach the repo root, or to reach a +# sibling path (an exec target, a sourced file). A wrong count makes `cd` +# SUCCEED at the wrong directory, or makes an exec/source target resolve to +# nothing -- either way quietly. Root climbs are checked against two markers; +# sibling climbs are checked for existence. And the relative path it sources a +# library, or reaches its test subject, through. Six of the nine +# lib/-sourcing scripts run during a terramate apply, so that failure lands +# mid-deploy with no CI gate in front of it. # # SCRIPT_PATHS_ROOT exists so this suite can be aimed at a fixture tree and # proved to fail. Without a negative case a green gate means nothing. @@ -20,7 +23,7 @@ REPO_ROOT="$(cd "$HERE/../../.." && pwd)" SCRIPTS="${SCRIPT_PATHS_ROOT:-$REPO_ROOT/scripts}" MARKER_ROOT="$(cd "$SCRIPTS/.." && pwd)" -fails=0 n_roots=0 n_sources=0 n_subjects=0 +fails=0 n_roots=0 n_rels=0 n_sources=0 n_subjects=0 fail() { printf 'FAIL %s\n' "$*" >&2; fails=$((fails + 1)); } rel() { printf '%s' "${1#"$MARKER_ROOT"/}"; } @@ -49,10 +52,27 @@ while IFS= read -r script; do dir="$(cd "$(dirname "$script")" && pwd)" - # 1. Self-resolved repo roots. + # 1. Self-resolved paths: a repo-root climb, or a climb into a sibling path. + # + # The "/.." run alone does not say which: `cd "$(dirname "$0")/.."` climbs to + # the root, but `"$(dirname "$0")/../k8s/x.sh"` climbs to a SIBLING directory + # and was never meant to land on the root at all. Classify by what follows + # the climb: a named path segment means "sibling" (checked for existence, + # since it is not the repo root by design); anything else -- the climb ends + # the path, or a variable follows it -- means "root" (checked against the two + # markers, which fails loudly on a variable it cannot resolve). while IFS=: read -r lineno line; do ups="$(printf '%s' "$line" | grep -oE '(/\.\.)+' | head -1)" [ -n "$ups" ] || continue + after="${line#*"$ups"}" + case "$after" in + /[A-Za-z0-9_]*) + seg="$(printf '%s' "$after" | grep -oE '^(/[A-Za-z0-9._-]+)+')" + n_rels=$((n_rels + 1)) + [ -e "$dir$ups$seg" ] \ + || fail "$(rel "$script"):$lineno — reaches a missing path: $dir$ups$seg" + continue ;; + esac n_roots=$((n_roots + 1)) if ! resolved="$(cd "$dir$ups" 2>/dev/null && pwd)"; then fail "$(rel "$script"):$lineno — '$dir$ups' resolves nowhere" @@ -97,8 +117,8 @@ while IFS= read -r script; do done < <(find "$SCRIPTS" \( -type f -o -type l \) -name '*.sh' | sort) -printf '%d roots, %d sources, %d subjects checked; %d failed\n' \ - "$n_roots" "$n_sources" "$n_subjects" "$fails" +printf '%d roots, %d relative paths, %d sources, %d subjects checked; %d failed\n' \ + "$n_roots" "$n_rels" "$n_sources" "$n_subjects" "$fails" # A gate that checked nothing has not passed. 20 sources is the floor measured # when this gate was written (21 at the time). Below it, suspect a broken From 00303967db5c97d9370006dd4e824ea9a8b95621 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 22:55:10 +0200 Subject: [PATCH 13/19] docs(scripts): name the ops scripts terramate runs on deploy The ops/{aws,gcp,k8s} row said "also called from terramate destroy scripts", but eks-recycle-bootstrap-nodes.sh and adopt-workforce-pool.sh run on every deploy, not just destroy. Named both, and corrected the closing note: those two already moved to ops/, ahead of the rest of the apply-time scripts still waiting on provision/. --- scripts/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/README.md b/scripts/README.md index f9a66cd1e..a76300d1d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,11 +8,12 @@ directly — none of them depend on the task runner. |---|---| | `ci/` | the gates CI runs, and you before pushing. `task check` runs every one CI runs | | `ci/tests/` | suites `run.sh` discovers: `test-*.sh` and `test-*.py` here, `*/test-*.py` one level down. A `# requires:` tool that is absent, or an exit 77, reports `SKIP` | -| `ops/aws/`, `ops/gcp/`, `ops/k8s/` | day-2 operations, run by a human. Some are also called from terramate destroy scripts | +| `ops/aws/`, `ops/gcp/`, `ops/k8s/` | day-2 operations, run by a human. Some are also called from terramate deploy or destroy scripts — `eks-recycle-bootstrap-nodes.sh` and `adopt-workforce-pool.sh` run on every deploy | | `ops/teardown/` | `teardown.sh` is the supported way to tear the platform down (`task ops:teardown`). The other three are called by terramate destroy scripts | | `ops/demo/` | demo load generation and cleanup | | `docs/` | docs-site generators, run by hand. `build-og-card.html` opens in a browser | | `lib/` | sourced by the others, never run directly | -Apply-time provisioning scripts move to `provision/` in the next phase; until then they remain at -the root of `scripts/`. +Apply-time provisioning scripts move to `provision/` in a later phase. Most still sit at the root +of `scripts/`; `eks-recycle-bootstrap-nodes.sh` and `adopt-workforce-pool.sh` already moved to +`ops/`, since they also run on every deploy. From 733ceeb5c8338e8759ff89cf1327e58be048aa94 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:09:55 +0200 Subject: [PATCH 14/19] fix(ci): fail a climb that stops short on a name the repo root also has Round 1's sibling branch trusted plain existence, but AGENTS.md and README.md both nest at several depths: a climb that stops one level short of the root can land on a decoy of the same name and pass by accident, exactly like the wrong-depth case it's meant to catch. Before the existence check, look at the climb's first path segment. If it also exists at the repo root and the climb itself didn't land there, fail loudly instead of trusting the coincidence. 11 roots, 4 relative paths, 21 sources, 16 subjects checked; 0 failed. --- scripts/ci/tests/test-script-paths.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/ci/tests/test-script-paths.sh b/scripts/ci/tests/test-script-paths.sh index 1b0be2324..dce55e20b 100755 --- a/scripts/ci/tests/test-script-paths.sh +++ b/scripts/ci/tests/test-script-paths.sh @@ -6,7 +6,10 @@ # sibling path (an exec target, a sourced file). A wrong count makes `cd` # SUCCEED at the wrong directory, or makes an exec/source target resolve to # nothing -- either way quietly. Root climbs are checked against two markers; -# sibling climbs are checked for existence. And the relative path it sources a +# sibling climbs are checked for existence, EXCEPT when the first segment also +# exists at the repo root and the climb itself isn't the root: AGENTS.md and +# README.md both nest at several depths, so "exists" alone would pass a climb +# that stopped one level short by accident. And the relative path it sources a # library, or reaches its test subject, through. Six of the nine # lib/-sourcing scripts run during a terramate apply, so that failure lands # mid-deploy with no CI gate in front of it. @@ -69,8 +72,19 @@ while IFS= read -r script; do /[A-Za-z0-9_]*) seg="$(printf '%s' "$after" | grep -oE '^(/[A-Za-z0-9._-]+)+')" n_rels=$((n_rels + 1)) - [ -e "$dir$ups$seg" ] \ - || fail "$(rel "$script"):$lineno — reaches a missing path: $dir$ups$seg" + # A climb that stops one level short of the root, on a directory that + # HAPPENS to have a same-named entry (AGENTS.md and README.md both + # nest), passes the plain existence check by accident -- indistinguish- + # able from a genuine wrong depth. Fail loudly instead when the first + # segment also exists at the true root and the climb itself is not it. + first_seg="${seg#/}"; first_seg="${first_seg%%/*}" + climbed="$(cd "$dir$ups" 2>/dev/null && pwd)" + if [ -e "$MARKER_ROOT/$first_seg" ] && { [ -z "$climbed" ] || ! is_repo_root "$climbed"; }; then + fail "$(rel "$script"):$lineno — '$first_seg' also exists at the repo root; this climb stops at ${climbed:-$dir$ups}, not the root. If you meant the root, climb to it; if you meant this sibling, compute it from a REPO_ROOT/SCRIPTS variable instead" + else + [ -e "$dir$ups$seg" ] \ + || fail "$(rel "$script"):$lineno — reaches a missing path: $dir$ups$seg" + fi continue ;; esac n_roots=$((n_roots + 1)) From 19d0fa64e4887a6342198b5efc818f3cebb78fab Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:37:30 +0200 Subject: [PATCH 15/19] fix(ci): fail loudly on a script-path shape the terramate-refs gate can't resolve The gate only ever extracted what its inner regex matched, so an unbraced variable, a single-quoted path, a bare "../.." climb, or a $(git rev-parse ...) prefix silently checked 0 refs and passed. Count the outer "scripts/..." mentions per line against what the inner regex extracted; fewer extracted fails as an unresolvable shape. Also close the trust gap on the other side: a ${...}/$${...} prefix now resolves only as ${path.module}, ${terramate.root.path.fs.absolute} or $${ROOT} -- the three this codebase actually uses. Anything else (${path.root}, ${terramate.stack.path.absolute}) failed silently resolved against $ROOT/scripts/ before; it now fails as an unrecognised prefix instead of guessing. --exclude-dir=.terraform on the outer grep: vendored modules there are gitignored and inflated the count locally (94 vs 88 in CI). 88 checked; 0 failed on the tree, unchanged. --- .../ci/tests/test-terramate-script-refs.sh | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/scripts/ci/tests/test-terramate-script-refs.sh b/scripts/ci/tests/test-terramate-script-refs.sh index d647a6853..c09aafe4f 100755 --- a/scripts/ci/tests/test-terramate-script-refs.sh +++ b/scripts/ci/tests/test-terramate-script-refs.sh @@ -8,6 +8,14 @@ # # Comment lines are skipped: a stale comment misleads, but it cannot break a run. # `echo` hints are checked — an operator copies them during a failed destroy. +# +# Every mention is either resolved or fails loudly. A line where fewer refs were +# extracted than "scripts/..." mentions counted holds a shape the inner regex +# cannot parse -- these used to pass silently at 0 checked. A ${...}/$${...} +# prefix resolves only as ${path.module} (beside the file), +# ${terramate.root.path.fs.absolute} or $${ROOT} (the repo root) -- the three +# this codebase actually uses; anything else is an unrecognised prefix, never a +# guess at what it might mean. set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -20,13 +28,32 @@ checked=0 failed=0 while IFS= read -r hit; do file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; text="${rest#*:}" [[ "$text" =~ ^[[:space:]]*(#|//) ]] && continue + + # The outer count has no prefix opinion: it is the floor every extracted ref + # must meet. Fewer extracted than mentioned means a shape the inner regex + # cannot parse -- e.g. an unbraced variable, a single-quoted path, a bare + # "../.." climb -- slipped through unresolved. + want="$(grep -oE 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' <<<"$text" | wc -l)" + got=0 while IFS= read -r ref; do [ -n "$ref" ] || continue - if [[ "$ref" == '${path.module}/'* ]]; then - target="$ROOT/$(dirname "$file")/${ref#'${path.module}/'}" - else - target="$ROOT/scripts/${ref#*scripts/}" - fi + got=$((got + 1)) + case "$ref" in + '${path.module}/'*) + target="$ROOT/$(dirname "$file")/${ref#'${path.module}/'}" ;; + '${terramate.root.path.fs.absolute}/'*|'$${ROOT}/'*|'scripts/'*) + target="$ROOT/scripts/${ref#*scripts/}" ;; + '${'*|'$${'*) + # Any other brace prefix -- ${path.root}, ${terramate.stack.path.absolute} + # -- is NOT the repo root and NOT beside the file. Guessing it is one + # of those is exactly the silent-wrong-resolution this gate exists to + # catch, so it fails instead of resolving against $ROOT/scripts/. + checked=$((checked + 1)); failed=$((failed + 1)) + printf 'FAIL %s:%s unrecognised prefix: %s\n' "$file" "$line" "$ref" + continue ;; + *) + target="$ROOT/scripts/${ref#*scripts/}" ;; + esac checked=$((checked + 1)) if [ ! -e "$target" ]; then printf 'FAIL %s:%s %s\n resolved to %s\n' "$file" "$line" "$ref" "${target#"$ROOT"/}" @@ -34,7 +61,11 @@ while IFS= read -r hit; do fi done < <(grep -oE '[$]?[$][{][^}]+[}](/\.\.)*/scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)|(^|[[:space:]"(])scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' <<<"$text" \ | sed -E 's/^[[:space:]"(]//') -done < <(cd "$ROOT" && grep -rnE --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ + if [ "$got" -lt "$want" ]; then + printf 'FAIL %s:%s a script path in a shape this gate cannot resolve\n' "$file" "$line" + failed=$((failed + 1)) + fi +done < <(cd "$ROOT" && grep -rnE --exclude-dir=.terraform --include='*.tf' --include='*.tm.hcl' --include='*.tfvars' \ 'scripts/[A-Za-z0-9_./-]+\.(sh|py|js|ya?ml)' opentofu 2>/dev/null) if [ "$checked" -lt "$FLOOR" ]; then From c4b1da35f779e40b784936a5534ae6aa1b429391 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:38:11 +0200 Subject: [PATCH 16/19] fix(task): drop ops:k8s:reclaim-csi-volumes from the ops index It is a destroy-time helper, not a day-2 entry point: gke destroy and eks-prepare-destroy.sh call it directly, with no prompt, and it deletes every PVC and CNPG cluster in the current kube context. The file's own rule already excludes destroy-time helpers -- this one was missed. task --list now shows 15 ops/docs tasks, not 16. --- scripts/ops/tasks.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/ops/tasks.yaml b/scripts/ops/tasks.yaml index 5c3fa69d9..aa5d7c46a 100644 --- a/scripts/ops/tasks.yaml +++ b/scripts/ops/tasks.yaml @@ -1,7 +1,10 @@ version: "3" # The three helpers beside teardown.sh are not indexed: terramate destroy -# scripts call them. teardown.sh is the entry point. +# scripts call them. teardown.sh is the entry point. k8s/reclaim-csi-volumes.sh +# is the same kind of destroy-time helper -- gke destroy and +# eks-prepare-destroy.sh call it -- and is excluded for the same reason: no +# prompt, and it deletes every PVC and CNPG cluster in the current context. tasks: teardown: desc: Tear the platform down — destroy past failures, sweep controller orphans, retry, verify against the cloud @@ -30,9 +33,6 @@ tasks: gcp:sweep-orphaned-disks: desc: Delete Persistent Disks GKE's CSI driver created that nothing uses cmds: ["{{.TASKFILE_DIR}}/gcp/sweep-orphaned-disks.sh {{.CLI_ARGS}}"] - k8s:reclaim-csi-volumes: - desc: Reclaim CSI-provisioned volumes before a cluster destroy (cloud-neutral) - cmds: ["{{.TASKFILE_DIR}}/k8s/reclaim-csi-volumes.sh {{.CLI_ARGS}}"] k8s:cnpg-prepare-restore: desc: Clear a CNPG cluster's live WAL archive so a new cluster can start (off the normal path) cmds: ["{{.TASKFILE_DIR}}/k8s/cnpg-prepare-restore.sh {{.CLI_ARGS}}"] From 858478f172e31f79f1c513b08fcd0f267021977e Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:39:06 +0200 Subject: [PATCH 17/19] fix(ci): correct the R5 fail message's suggested fix "Compute it from a REPO_ROOT/SCRIPTS variable" was itself wrong: a SCRIPTS=.../../.. variable climbs the same ambiguous way and fails the root check it's supposed to satisfy. The only fix that actually resolves is climbing to REPO_ROOT and naming the path from there. --- scripts/ci/tests/test-script-paths.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/tests/test-script-paths.sh b/scripts/ci/tests/test-script-paths.sh index dce55e20b..ef0186b36 100755 --- a/scripts/ci/tests/test-script-paths.sh +++ b/scripts/ci/tests/test-script-paths.sh @@ -80,7 +80,7 @@ while IFS= read -r script; do first_seg="${seg#/}"; first_seg="${first_seg%%/*}" climbed="$(cd "$dir$ups" 2>/dev/null && pwd)" if [ -e "$MARKER_ROOT/$first_seg" ] && { [ -z "$climbed" ] || ! is_repo_root "$climbed"; }; then - fail "$(rel "$script"):$lineno — '$first_seg' also exists at the repo root; this climb stops at ${climbed:-$dir$ups}, not the root. If you meant the root, climb to it; if you meant this sibling, compute it from a REPO_ROOT/SCRIPTS variable instead" + fail "$(rel "$script"):$lineno — '$first_seg' also exists at the repo root; this climb stops at ${climbed:-$dir$ups}, not the root. Either way, climb to the repo root (REPO_ROOT) and name the path from there" else [ -e "$dir$ups$seg" ] \ || fail "$(rel "$script"):$lineno — reaches a missing path: $dir$ups$seg" From 86c10ce78d93d589708aab302ab8ee7925988438 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:44:03 +0200 Subject: [PATCH 18/19] docs(scripts): correct two review findings in the README and AGENTS tables M5: the closing paragraph repeated the ops row and reversed the causality (named the two deploy-time scripts as if they were the exception rather than the rule). Replaced with the plain fact: apply-time provisioning scripts still sit at the root of scripts/, moving to provision/ later. M6: the terramate-refs row described the gate's pre-I1 behavior (exists or is silently skipped). After I1 every mention on an executed line is resolved or fails loudly, and .tfvars is one of the three extensions it reads. --- scripts/AGENTS.md | 2 +- scripts/README.md | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index ca3e33974..5e70eeb01 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -96,7 +96,7 @@ suite**; if a change stops that being true, it has regressed to testing nothing. | `verify-doc-paths.sh` | every **backticked** repository path in `website/content/` still exists. Runs in the website workflow, not in `validate-manifests.sh`, and catches what `validate-links.sh` cannot: a path in prose, or one linked by absolute GitHub URL. No allowlist by design — fix the path or drop the reference | | `validate-doc-claims.sh` | docs still agree with config, per `.doc-claims.yaml` | | `validate-idp-topology.sh` | exactly one cloud hosts ZITADEL (ADR-0027) | -| `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl` line exists — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | +| `ci/tests/test-terramate-script-refs.sh` | every script path on an **executed** `.tf`/`.tm.hcl`/`.tfvars` line is resolved or fails loudly — the apply- and destroy-time calls no CI job runs. Comments are skipped; `echo` hints are not | | `docs/diagram-icons.py audit` | boxes naming a product that render without an icon. Advisory, never a CI gate | | `docs/export-diagrams.sh` | regenerates every SVG the site embeds. Pins the drawio version on purpose | | `ops/aws/eks-prepare-destroy.sh` | **deletes every PVC.** Must carry a cloud gate | diff --git a/scripts/README.md b/scripts/README.md index a76300d1d..e86dba197 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -14,6 +14,5 @@ directly — none of them depend on the task runner. | `docs/` | docs-site generators, run by hand. `build-og-card.html` opens in a browser | | `lib/` | sourced by the others, never run directly | -Apply-time provisioning scripts move to `provision/` in a later phase. Most still sit at the root -of `scripts/`; `eks-recycle-bootstrap-nodes.sh` and `adopt-workforce-pool.sh` already moved to -`ops/`, since they also run on every deploy. +Apply-time provisioning scripts still sit at the root of `scripts/`; they move to `provision/` in +a later phase. From 5a64e1ee8f1ce3484049ad67c6dd789708751b85 Mon Sep 17 00:00:00 2001 From: Smana Date: Mon, 21 Sep 2026 23:44:21 +0200 Subject: [PATCH 19/19] fix(scripts): four reviewer-triaged deferred fixes - ops:gcp:adopt-workforce-pool needs an initialised stack directory as cwd for its tofu-import half; without dir:, that half always skipped. Set dir: to the workforce-identity stack. - adopt-workforce-pool.sh's usage said "from the stack directory" but gave a repo-root path; gave both the from-stack-dir and from-anywhere forms instead. - commands.md named the renamed scripts by bare name, which verify-doc-paths.sh cannot check and which means nothing on their own (load.sh, reclaim-csi-volumes.sh, ...). Full scripts/ops/... and scripts/docs/... paths. Also: export-diagrams.sh writes SVG, not PNG. - docs/architecture/AGENTS.md's "after any diagram change" block lost its column alignment when export-diagrams.sh moved into scripts/docs/. --- docs/architecture/AGENTS.md | 2 +- scripts/ops/gcp/adopt-workforce-pool.sh | 7 +++++-- scripts/ops/tasks.yaml | 1 + website/content/docs/reference/commands.md | 12 ++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/architecture/AGENTS.md b/docs/architecture/AGENTS.md index 8ae26f116..6d236cef2 100644 --- a/docs/architecture/AGENTS.md +++ b/docs/architecture/AGENTS.md @@ -100,7 +100,7 @@ A logo used by a second diagram belongs in `icons/`, not embedded twice. Check ` ## After any diagram change ```bash -./scripts/docs/export-diagrams.sh # regenerates every SVG the site embeds +./scripts/docs/export-diagrams.sh # regenerates every SVG the site embeds ./scripts/ci/validate-links.sh # after any file move ./scripts/ci/validate-doc-claims.sh # the pages that describe the diagram ``` diff --git a/scripts/ops/gcp/adopt-workforce-pool.sh b/scripts/ops/gcp/adopt-workforce-pool.sh index 88459177a..b3b290dcf 100755 --- a/scripts/ops/gcp/adopt-workforce-pool.sh +++ b/scripts/ops/gcp/adopt-workforce-pool.sh @@ -42,8 +42,11 @@ # 30-day tombstones accumulate. A stable name that survives rebuilds is the # property worth having; this script is what buys it. # -# Usage (from the workforce-identity stack directory): -# scripts/ops/gcp/adopt-workforce-pool.sh --pool NAME [--provider NAME] [--location L] [--apply] +# Usage: the state adoption half needs an initialised stack directory as cwd. +# from the stack directory: +# bash ../../../scripts/ops/gcp/adopt-workforce-pool.sh --pool NAME [--provider NAME] [--location L] [--apply] +# from anywhere: +# task ops:gcp:adopt-workforce-pool -- --pool NAME [--provider NAME] [--location L] [--apply] # # Dry-run unless --apply. diff --git a/scripts/ops/tasks.yaml b/scripts/ops/tasks.yaml index aa5d7c46a..52161fe10 100644 --- a/scripts/ops/tasks.yaml +++ b/scripts/ops/tasks.yaml @@ -26,6 +26,7 @@ tasks: cmds: ["{{.TASKFILE_DIR}}/aws/eks-recycle-bootstrap-nodes.sh {{.CLI_ARGS}}"] gcp:adopt-workforce-pool: desc: Make the workforce pool survive a teardown and rebuild + dir: "{{.ROOT_DIR}}/opentofu/gcp/workforce-identity" cmds: ["{{.TASKFILE_DIR}}/gcp/adopt-workforce-pool.sh {{.CLI_ARGS}}"] gcp:purge-dns-records: desc: Empty a Cloud DNS managed zone so the zone can be destroyed diff --git a/website/content/docs/reference/commands.md b/website/content/docs/reference/commands.md index b4b2d8da6..cf2a19fad 100644 --- a/website/content/docs/reference/commands.md +++ b/website/content/docs/reference/commands.md @@ -237,11 +237,11 @@ each gate actually checks. | `terramate-destroy-confirm.sh` | Single y/n prompt every stack's destroy script calls first, cached so `--reverse destroy` asks once | | `eks-prepare-destroy.sh` | Pre-destroy EKS cleanup — suspends Flux, disables blocking webhooks, sweeps orphaned EBS volumes; the CSI volume reclaim itself moved to `reclaim-csi-volumes.sh` | | `eks-recycle-bootstrap-nodes.sh` | Recycles Stage 1 node-group nodes so they pick up Cilium prefix delegation | -| `reclaim-csi-volumes.sh` | Reclaims CSI-provisioned volumes before a cluster destroy — cloud-neutral, called by both teardown paths | -| `destroy-stage2.sh` | Graceful-then-reconcile teardown of either cloud's `configure` stack, never gating the cluster delete | -| `purge-dns-records.sh` | Empties a Cloud DNS zone of external-dns leftovers so `tofu destroy` can delete it | -| `export-diagrams.sh` | Exports `.drawio` architecture diagrams to PNG | -| `cleanup-benchmark-images.sh` | Cleans up images left behind by the image-gallery/benchmark scripts | -| `load.sh` | Runs an image-gallery load-generator scenario in-cluster (`browse`, `upload`, `mixed`, `steady`, `incident`) from the suspended `image-gallery-loadgen` CronJob | +| `scripts/ops/k8s/reclaim-csi-volumes.sh` | Reclaims CSI-provisioned volumes before a cluster destroy — cloud-neutral, called by both teardown paths | +| `scripts/ops/teardown/destroy-stage2.sh` | Graceful-then-reconcile teardown of either cloud's `configure` stack, never gating the cluster delete | +| `scripts/ops/gcp/purge-dns-records.sh` | Empties a Cloud DNS zone of external-dns leftovers so `tofu destroy` can delete it | +| `scripts/docs/export-diagrams.sh` | Exports `.drawio` architecture diagrams to SVG | +| `scripts/ops/demo/cleanup-benchmark-images.sh` | Cleans up images left behind by the image-gallery/benchmark scripts | +| `scripts/ops/demo/load.sh` | Runs an image-gallery load-generator scenario in-cluster (`browse`, `upload`, `mixed`, `steady`, `incident`) from the suspended `image-gallery-loadgen` CronJob | | `test-flux-schema.sh` | Quarantined, never run: it asserts bundle filenames the render has outgrown. See `scripts/ci/tests/quarantine/README.md` | | `test-vector-vrl.sh` / `validate-vector-vrl.sh` / `vector-vrl-tests/` | Validate the Vector log-parsing configuration |