chore(scripts): rewrite the CI guards and operator scripts in Nushell #492

Merged
Claude-Run merged 1 commit from chore/BUNYIP-490-nushell-ci-guards into main 2026-08-06 13:01:08 +02:00
Member

Every script under scripts/ is now a #!/usr/bin/env nu script with the executable bit set, replacing the eight Bash/POSIX-sh scripts. The seven guards check.yml invokes keep their exit-code contract exactly (0 pass, 1 violation, 2 cannot-run) and their failure messages stay at least as specific; grep/sed/awk pipelines became structured Nushell (glob, parse --regex, enumerate, complete) so the file:line reporting is data rather than string surgery.

Each guard was proven to still fail on a deliberately introduced violation of the invariant it protects, against throwaway fixtures: duplicate and malformed migration versions and a missing dir; an edited and a deleted committed migration plus an unreachable base ref (and a migrations README edit still exempt, BUNYIP-458); a pull_request trigger on e2e.yml, an off-allowlist secrets.* reference, and an npm ci without --ignore-scripts; check.yml off the dev label, a run-time C-toolchain install, a run-time Playwright-library install, and an unannotated runs-on (with a commented-out install still passing); each of BUNYIP-426 F4/F6/F8/F10 including a tag-only base image and an undefined build stage; a reintroduced STRIPE_* env read and a reintroduced TOTP_ENCRYPTION_KEY_PREV, with the migrations/, e2e/ and self-exclusion paths still honoured.

The three non-CI scripts converted too, so no .sh remains under scripts/. init-secrets was exercised end to end: .env quote stripping, dev-placeholder rejection, byte-exact secrets with mode 600, the derived database_url, the empty-file defensive fill, the legacy flat-to-oidc/ key migration (byte-identical), and a clean idempotent re-run. refresh-ip2-datasets was exercised against a local archive: install is byte-identical and atomic, and the missing-token, unknown-id, download-failure and invalid-zip branches all exit non-zero with the temp dir cleaned up (a try replaces the shell EXIT trap). purge-staging-test-accounts was exercised against a throwaway postgres: dry-run by default, refusal without --i-know-this-is-not-prod, refusal on a wrong phrase, and the committed hard delete removing exactly the matched non-admin rows and their non-cascade dependents.

Two behaviour fixes fell out of that testing rather than being ports. The purge script's preview and count queries used psql -c, which does not expand :'pattern', so both always failed with a syntax error before any row was listed; they now go in on stdin like the DELETE block already did. And input reads the terminal rather than stdin, so a non-interactive --commit now refuses with a clear message and exit 3 instead of an I/O error.

scripts/check-no-bash.nu is new and gates the removal: it fails on any tracked .sh file or POSIX-shell shebang under scripts/, and runs from both check.yml and just check. Every in-repo reference to an old .sh path moved with it (check.yml, justfile, CLAUDE.md, .env.example, compose.yml, migrations/README.md, docs/, and the Rust doc comments in admin.rs, dashboard.rs and stripe.rs), and the Nushell prerequisite in docs/getting-started.md now names scripts/ alongside the just recipes.

#BUNYIP-490

Every script under scripts/ is now a `#!/usr/bin/env nu` script with the executable bit set, replacing the eight Bash/POSIX-sh scripts. The seven guards check.yml invokes keep their exit-code contract exactly (0 pass, 1 violation, 2 cannot-run) and their failure messages stay at least as specific; grep/sed/awk pipelines became structured Nushell (`glob`, `parse --regex`, `enumerate`, `complete`) so the file:line reporting is data rather than string surgery. Each guard was proven to still fail on a deliberately introduced violation of the invariant it protects, against throwaway fixtures: duplicate and malformed migration versions and a missing dir; an edited and a deleted committed migration plus an unreachable base ref (and a migrations README edit still exempt, BUNYIP-458); a pull_request trigger on e2e.yml, an off-allowlist `secrets.*` reference, and an `npm ci` without --ignore-scripts; check.yml off the dev label, a run-time C-toolchain install, a run-time Playwright-library install, and an unannotated `runs-on` (with a commented-out install still passing); each of BUNYIP-426 F4/F6/F8/F10 including a tag-only base image and an undefined build stage; a reintroduced STRIPE_* env read and a reintroduced TOTP_ENCRYPTION_KEY_PREV, with the migrations/, e2e/ and self-exclusion paths still honoured. The three non-CI scripts converted too, so no .sh remains under scripts/. init-secrets was exercised end to end: .env quote stripping, dev-placeholder rejection, byte-exact secrets with mode 600, the derived database_url, the empty-file defensive fill, the legacy flat-to-oidc/ key migration (byte-identical), and a clean idempotent re-run. refresh-ip2-datasets was exercised against a local archive: install is byte-identical and atomic, and the missing-token, unknown-id, download-failure and invalid-zip branches all exit non-zero with the temp dir cleaned up (a `try` replaces the shell EXIT trap). purge-staging-test-accounts was exercised against a throwaway postgres: dry-run by default, refusal without --i-know-this-is-not-prod, refusal on a wrong phrase, and the committed hard delete removing exactly the matched non-admin rows and their non-cascade dependents. Two behaviour fixes fell out of that testing rather than being ports. The purge script's preview and count queries used `psql -c`, which does not expand `:'pattern'`, so both always failed with a syntax error before any row was listed; they now go in on stdin like the DELETE block already did. And `input` reads the terminal rather than stdin, so a non-interactive --commit now refuses with a clear message and exit 3 instead of an I/O error. scripts/check-no-bash.nu is new and gates the removal: it fails on any tracked .sh file or POSIX-shell shebang under scripts/, and runs from both check.yml and `just check`. Every in-repo reference to an old .sh path moved with it (check.yml, justfile, CLAUDE.md, .env.example, compose.yml, migrations/README.md, docs/, and the Rust doc comments in admin.rs, dashboard.rs and stripe.rs), and the Nushell prerequisite in docs/getting-started.md now names scripts/ alongside the just recipes. #BUNYIP-490
chore(scripts): rewrite the CI guards and operator scripts in Nushell
All checks were successful
E2E PR gate / E2E / PR gate (pull_request) Successful in 29s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m53s
Create release / Create release from merged PR (pull_request) Has been skipped
e41d6e3988
Every script under scripts/ is now a `#!/usr/bin/env nu` script with the executable bit set, replacing the eight Bash/POSIX-sh scripts. The seven guards check.yml invokes keep their exit-code contract exactly (0 pass, 1 violation, 2 cannot-run) and their failure messages stay at least as specific; grep/sed/awk pipelines became structured Nushell (`glob`, `parse --regex`, `enumerate`, `complete`) so the file:line reporting is data rather than string surgery.

Each guard was proven to still fail on a deliberately introduced violation of the invariant it protects, against throwaway fixtures: duplicate and malformed migration versions and a missing dir; an edited and a deleted committed migration plus an unreachable base ref (and a migrations README edit still exempt, BUNYIP-458); a pull_request trigger on e2e.yml, an off-allowlist `secrets.*` reference, and an `npm ci` without --ignore-scripts; check.yml off the dev label, a run-time C-toolchain install, a run-time Playwright-library install, and an unannotated `runs-on` (with a commented-out install still passing); each of BUNYIP-426 F4/F6/F8/F10 including a tag-only base image and an undefined build stage; a reintroduced STRIPE_* env read and a reintroduced TOTP_ENCRYPTION_KEY_PREV, with the migrations/, e2e/ and self-exclusion paths still honoured.

The three non-CI scripts converted too, so no .sh remains under scripts/. init-secrets was exercised end to end: .env quote stripping, dev-placeholder rejection, byte-exact secrets with mode 600, the derived database_url, the empty-file defensive fill, the legacy flat-to-oidc/ key migration (byte-identical), and a clean idempotent re-run. refresh-ip2-datasets was exercised against a local archive: install is byte-identical and atomic, and the missing-token, unknown-id, download-failure and invalid-zip branches all exit non-zero with the temp dir cleaned up (a `try` replaces the shell EXIT trap). purge-staging-test-accounts was exercised against a throwaway postgres: dry-run by default, refusal without --i-know-this-is-not-prod, refusal on a wrong phrase, and the committed hard delete removing exactly the matched non-admin rows and their non-cascade dependents.

Two behaviour fixes fell out of that testing rather than being ports. The purge script's preview and count queries used `psql -c`, which does not expand `:'pattern'`, so both always failed with a syntax error before any row was listed; they now go in on stdin like the DELETE block already did. And `input` reads the terminal rather than stdin, so a non-interactive --commit now refuses with a clear message and exit 3 instead of an I/O error.

scripts/check-no-bash.nu is new and gates the removal: it fails on any tracked .sh file or POSIX-shell shebang under scripts/, and runs from both check.yml and `just check`. Every in-repo reference to an old .sh path moved with it (check.yml, justfile, CLAUDE.md, .env.example, compose.yml, migrations/README.md, docs/, and the Rust doc comments in admin.rs, dashboard.rs and stripe.rs), and the Nushell prerequisite in docs/getting-started.md now names scripts/ alongside the just recipes.

#BUNYIP-490
Claude-Run deleted branch chore/BUNYIP-490-nushell-ci-guards 2026-08-06 13:01:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!492
No description provided.