grada-run 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.github/workflows/e2e.yml +73 -0
  2. package/apps/docs/src/content/docs/cli/add.md +2 -1
  3. package/apps/docs/src/content/docs/cli/apply.md +5 -2
  4. package/apps/docs/src/content/docs/cli/destroy.md +7 -3
  5. package/apps/docs/src/content/docs/cli/eject.md +6 -2
  6. package/apps/docs/src/content/docs/guides/headless.md +12 -0
  7. package/apps/docs/src/content/docs/roadmap.md +5 -3
  8. package/apps/docs/src/content/docs/testing-strategy.md +6 -1
  9. package/bin/cli.js +7 -7
  10. package/package.json +3 -1
  11. package/specs/e2e-testing.md +50 -0
  12. package/specs/test-suite-deduplication.md +42 -0
  13. package/src/commands/apply.js +4 -4
  14. package/src/commands/destroy.js +3 -3
  15. package/src/commands/eject.js +5 -2
  16. package/src/core/parser.js +2 -0
  17. package/src/utils/command.js +13 -0
  18. package/tests/add.test.js +12 -31
  19. package/tests/ai.test.js +9 -12
  20. package/tests/apply.test.js +6 -48
  21. package/tests/capabilities.test.js +5 -8
  22. package/tests/command.test.js +9 -10
  23. package/tests/db.test.js +6 -37
  24. package/tests/destroy.test.js +6 -32
  25. package/tests/diagnose.test.js +7 -21
  26. package/tests/doctor.test.js +6 -13
  27. package/tests/domain.test.js +12 -29
  28. package/tests/drift.test.js +12 -27
  29. package/tests/e2e/helpers.js +122 -0
  30. package/tests/e2e/tier0.e2e.test.js +142 -0
  31. package/tests/e2e/tier1.live.e2e.test.js +118 -0
  32. package/tests/exec.test.js +6 -20
  33. package/tests/gc.test.js +6 -36
  34. package/tests/headless.test.js +37 -108
  35. package/tests/helpers/clack.js +103 -0
  36. package/tests/helpers/console.js +41 -0
  37. package/tests/helpers/telemetry.js +37 -0
  38. package/tests/helpers/tmpdir.js +36 -0
  39. package/tests/logs.test.js +4 -15
  40. package/tests/resolvers.test.js +5 -8
  41. package/tests/rollback.test.js +6 -28
  42. package/tests/secrets.test.js +6 -29
  43. package/tests/sleep-wake.test.js +11 -29
  44. package/tests/status.test.js +6 -20
  45. package/tests/visualizer.test.js +12 -28
  46. package/vitest.config.js +2 -1
  47. package/vitest.e2e.tier0.config.js +8 -0
  48. package/vitest.e2e.tier1.config.js +8 -0
  49. package/specs/deploy-stack-to-grada-run-rebrand.md +0 -45
@@ -0,0 +1,73 @@
1
+ name: E2E
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ schedule:
8
+ - cron: '0 2 * * *'
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ tier0:
13
+ if: github.event_name == 'pull_request' || github.event_name == 'push'
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Setup Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '24.x'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Setup Terraform
27
+ uses: hashicorp/setup-terraform@v3
28
+
29
+ - name: Run Tier 0 E2E
30
+ run: npm run test:e2e:tier0
31
+
32
+ - name: Upload workspace on failure
33
+ if: failure()
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: e2e-tier0-workspace
37
+ path: tests/e2e/.tmp/
38
+ if-no-files-found: ignore
39
+
40
+ tier1:
41
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+
46
+ - name: Setup Node.js
47
+ uses: actions/setup-node@v4
48
+ with:
49
+ node-version: '24.x'
50
+
51
+ - name: Install dependencies
52
+ run: npm ci
53
+
54
+ - name: Setup Terraform
55
+ uses: hashicorp/setup-terraform@v3
56
+
57
+ - name: Configure AWS credentials
58
+ uses: aws-actions/configure-aws-credentials@v4
59
+ with:
60
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
61
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
62
+ aws-region: us-east-2
63
+
64
+ - name: Run Tier 1 live E2E
65
+ run: npm run test:e2e:tier1
66
+
67
+ - name: Upload workspace on failure
68
+ if: failure()
69
+ uses: actions/upload-artifact@v4
70
+ with:
71
+ name: e2e-tier1-workspace
72
+ path: tests/e2e/.tmp/
73
+ if-no-files-found: ignore
@@ -54,7 +54,7 @@ To switch Bedrock models later, just run `grada add ai:bedrock` again (interacti
54
54
  | `--model <id>` | Bedrock model or inference profile ID (default `us.anthropic.claude-sonnet-4-6`). Only applies to `ai:bedrock`. |
55
55
  | `--list-models` | Print the Bedrock model catalog (works offline, no project required). Only applies to `ai:bedrock`. |
56
56
  | `--refresh` | Refresh the Bedrock model catalog from live AWS data before listing or provisioning. Only applies to `ai:bedrock`. |
57
- | `--domain <domain>` | Domain for the SES identity (defaults to your `domain add` domain, or prompts interactively). Only applies to `email:ses`. |
57
+ | `--domain <domain>` | Domain for the SES identity (defaults to your `domain add` domain, or prompts interactively; required in `--headless` mode without a `domain.tf`). Only applies to `email:ses`. |
58
58
  | `--from-email <email>` | Default sender address (default `noreply@<domain>`). Must belong to the SES domain. Only applies to `email:ses`. |
59
59
  | `--zone-id <id>` | Route 53 hosted zone ID for automatic DKIM/SPF/DMARC records. Only applies to `email:ses`. |
60
60
  | `--schedule <expr>` | EventBridge Scheduler expression, e.g. `cron(0 2 * * ? *)` or `rate(1 hour)` (default `cron(0 0 * * ? *)`). Only applies to `cron`. |
@@ -62,6 +62,7 @@ To switch Bedrock models later, just run `grada add ai:bedrock` again (interacti
62
62
  | `--name <job>` | Job slug customizing the schedule name (default `daily-job`). Only applies to `cron`. |
63
63
  | `--timezone <tz>` | IANA timezone for the schedule expression (default `UTC`). Only applies to `cron`. |
64
64
  | `--force` | Overwrite the existing addon file (also accepts `--force=false`). Without it, re-adding refuses to clobber your edits. |
65
+ | `--headless` | Scaffold without interactive prompts, using flag values and built-in defaults (Bedrock default model, daily cron schedule). |
65
66
 
66
67
  Requires a project initialized with `grada` (`terraform/main.tf` must exist).
67
68
 
@@ -11,8 +11,8 @@ Run the Terraform plan/apply flow against the generated configuration.
11
11
  - Renders an infrastructure preview from your Terraform config and framework detection: a `Fixed Baseline` monthly figure with per-service breakdown, one topology entry per provisioned [`add`](/grada/cli/add/) addon (collapsing to a single `Addons (N)` line when three or more are active), and a one-line `Usage-based (N addons)` summary of metered billing drivers (shown only when usage-billed addons are present). With `--dry-run` it stops there and provisions nothing.
12
12
  - Otherwise runs `terraform init -upgrade` followed by `terraform apply -auto-approve` in `terraform/`, streaming progress, then prints the live URLs from the Terraform outputs (`cloudfront_url` and `alb_direct_url`, or `api_gateway_url` on `--target lambda`) plus the `git push` command that deploys your app and clears the initial 503.
13
13
  - On `--target lambda` projects, ensures the ECR repository exists first and seeds a minimal placeholder image under `:latest` when nothing has been pushed yet (Lambda rejects empty repositories), so Day-0 provisioning succeeds before the first code push.
14
- - Asks for confirmation after the preview; declining aborts without provisioning anything.
15
- - If the S3 state bucket is missing (e.g. deleted manually), offers to recreate it and resume automatically instead of failing.
14
+ - Asks for confirmation after the preview; declining aborts without provisioning anything. With `--auto-approve` (or the global `--headless` flag), the preview still renders but provisioning proceeds without prompting.
15
+ - If the S3 state bucket is missing (e.g. deleted manually), offers to recreate it and resume automatically instead of failing; `--auto-approve` and `--headless` accept the recovery without prompting.
16
16
  - If the environment is asleep (a `.grada/sleep-state.json` entry exists), warns you to run [`wake`](/grada/cli/sleep/) first — applying would start tasks against a stopped database.
17
17
  - On the known GitHub OIDC provider conflict (`EntityAlreadyExists` for `token.actions.githubusercontent.com`), tells you to set `create_oidc_provider = false` in `terraform/oidc.tf` and re-run; other failures print the Terraform error and the manual `cd terraform && terraform apply` fallback.
18
18
 
@@ -21,6 +21,7 @@ Run the Terraform plan/apply flow against the generated configuration.
21
21
  ```bash
22
22
  npx grada-run apply
23
23
  npx grada-run apply --dry-run
24
+ npx grada-run apply --auto-approve
24
25
  ```
25
26
 
26
27
  ## Flags
@@ -28,5 +29,7 @@ npx grada-run apply --dry-run
28
29
  | Flag | Description |
29
30
  | ---- | ----------- |
30
31
  | `--dry-run` | Render a preview of the planned changes without applying them. |
32
+ | `--auto-approve` | Skip the post-preview confirmation (and the state-bucket recovery prompt) for non-interactive runs. |
33
+ | `--headless` | Global automation flag; implies `--auto-approve` for this command. |
31
34
 
32
35
  `apply` shells out to the `terraform` binary in your generated `terraform/` directory and streams progress while it runs.
@@ -8,21 +8,25 @@ Permanently delete the AWS infrastructure created by `apply` when a project is r
8
8
  ## What it does
9
9
 
10
10
  - Verifies you are in a grada project (`terraform/backend.tf` must exist) and that the `terraform` binary is installed, exiting otherwise.
11
- - Asks for explicit confirmation before doing anything destructive; declining cancels with no changes.
11
+ - Asks for explicit confirmation before doing anything destructive; declining cancels with no changes. With `--yes` (or the global `--headless` flag), destruction proceeds without prompting.
12
12
  - Before destroying, wakes a stopped or transitional-state database back to `available` (RDS refuses to delete databases that aren't available), so tearing down an asleep environment succeeds instead of failing mid-destroy; aborts with a retry message if the database never becomes ready.
13
13
  - Runs `terraform destroy -auto-approve` in `terraform/`, streaming progress, so all compute resources (ECS or Lambda, ALB or API Gateway, database, and related resources) are removed.
14
- - Parses the state bucket name and region out of `terraform/backend.tf` (region defaults to `us-east-2` when not found), then optionally asks whether to also empty and delete the S3 state bucket via `teardownStateBucket`. Answering "No" keeps the bucket so `apply` can restore the infrastructure later.
14
+ - Parses the state bucket name and region out of `terraform/backend.tf` (region defaults to `us-east-2` when not found), then optionally asks whether to also empty and delete the S3 state bucket via `teardownStateBucket`. Answering "No" keeps the bucket so `apply` can restore the infrastructure later. With `--yes` (or `--headless`), the bucket is deleted without asking.
15
15
  - Emits an `infrastructure_destroyed` telemetry event recording success and whether the state bucket was retained.
16
16
 
17
17
  ## Usage
18
18
 
19
19
  ```bash
20
20
  npx grada-run destroy
21
+ npx grada-run destroy --yes
21
22
  ```
22
23
 
23
24
  ## Flags
24
25
 
25
- This command accepts no CLI flags. Both confirmation prompts are interactive.
26
+ | Flag | Description |
27
+ | ---- | ----------- |
28
+ | `--yes` | Answer "Yes" to both the destruction and state-bucket confirmations for non-interactive runs. |
29
+ | `--headless` | Global automation flag; implies `--yes` for this command. |
26
30
 
27
31
  ## See also
28
32
 
@@ -7,7 +7,7 @@ Take permanent, sole ownership of your infrastructure files when you no longer w
7
7
 
8
8
  ## What it does
9
9
 
10
- - Asks for explicit confirmation (defaulting to "No"); declining cancels with no changes.
10
+ - Asks for explicit confirmation (defaulting to "No"); declining cancels with no changes. With `--yes` (or the global `--headless` flag), ejection proceeds without prompting.
11
11
  - Strips grada metadata from your local files: removes the `# grada generated infrastructure` header and the `default_tags { tags = { ManagedBy = "grada" } }` block from `terraform/main.tf`, and removes the `# grada backups` block from `.gitignore`.
12
12
  - Recursively deletes every `*.bak.*` backup file in the project (skipping `node_modules` and `.git`).
13
13
  - Leaves your infrastructure fully operational as raw, standalone Terraform. As a final step, run `terraform apply` inside `terraform/` so AWS syncs state and removes the live `ManagedBy` tags.
@@ -17,11 +17,15 @@ Take permanent, sole ownership of your infrastructure files when you no longer w
17
17
 
18
18
  ```bash
19
19
  npx grada-run eject
20
+ npx grada-run eject --yes
20
21
  ```
21
22
 
22
23
  ## Flags
23
24
 
24
- This command accepts no CLI flags. The confirmation prompt is interactive.
25
+ | Flag | Description |
26
+ | ---- | ----------- |
27
+ | `--yes` | Skip the confirmation prompt for non-interactive runs. |
28
+ | `--headless` | Global automation flag; implies `--yes` for this command. |
25
29
 
26
30
  ## See also
27
31
 
@@ -68,6 +68,18 @@ npx grada-run --headless --framework=nestjs --needsDatabase \
68
68
  --with db:redis,ai:bedrock,email:ses --domain example.com --setup-ci-migrate
69
69
  ```
70
70
 
71
+ ## Automating lifecycle commands
72
+
73
+ `--headless` also suppresses confirmations in the Day-2 lifecycle commands, so scripted pipelines can provision, tear down, and decouple without hanging on a prompt:
74
+
75
+ ```bash
76
+ npx grada-run apply --auto-approve # provision without the preview confirmation
77
+ npx grada-run destroy --yes # tear down compute and delete the state bucket
78
+ npx grada-run eject --yes # strip CLI metadata without confirming
79
+ ```
80
+
81
+ Each command also accepts `--headless` directly (implying the approval flag). Without an approval flag, a non-interactive invocation cancels with no changes rather than destroying anything.
82
+
71
83
  ## See also
72
84
 
73
85
  - [`npx grada-run`](/grada/cli/init/) for the full flag table.
@@ -80,11 +80,13 @@ description: Where grada has been and what comes next — completed phases and t
80
80
  ### Phase 11: The `grada.run` Rebrand, Daily Observability & Agentic Ecosystem (Current)
81
81
  **Goal:** Transition the platform identity to **Grada (`grada.run`)**, close the daily observability gap with zero-cost CloudWatch Golden Signals, eliminate cross-command state-transition bugs, and launch the native MCP and AI Agent Plugin ecosystem.
82
82
 
83
- - [ ] **Unified Brand & Binary Transition (`grada`):** Transition the primary CLI binary and npm packages to `grada` / `@grada-run/grada` (while publishing a forwarding wrapper on `grada`) with transparent dual-read fallbacks for AWS tags (`ManagedBy: grada` + `grada`), local state ledgers (`.grada/` + `.grada/`), doc ownership markers (`GRADA.md`), and environment variables.
83
+ - [x] **Unified Brand & Binary Transition (`grada`):** Ship the `grada` binary alongside `grada-run` (plus a deprecated `deploy-stack` alias) and publish the `@grada-run/grada` scoped alias on release, keeping existing deployments working through transparent dual-read fallbacks for AWS tags, local state ledgers (`.grada/` + `.deploy-stack/`), machine markers, doc ownership markers (`GRADA.md` + `DEPLOY-STACK.md`), and environment variables. (Verified live.)
84
84
  - [ ] **AI-Driven Edge-Case & State Transition Audit:** Run a systematic codebase audit tracing multi-step lifecycle mutations across both `--target ecs` and `--target lambda` (e.g., scaffold with `--db-engine aurora-postgresql` → `add queue:sqs` → `add cron` → `db enable-vector` → `sleep` → `wake` → `drift` → `rollback` → `eject` → `destroy`), patching race conditions, partial Terraform state locks, and UX dead ends.
85
- - [ ] **Deterministic E2E & Flaky Test Elimination:** Harden the Vitest suite by isolating network/loopback and SDK mocks, eliminating timing-dependent test flakiness, and running automated LocalStack E2E lifecycle tests (`init` → `apply` → `status` → `destroy`) in GitHub Actions.
85
+ - [x] **Two-Tier E2E Harness & Automation Bypasses:** Ship the black-box harness (`tests/e2e/`, Tier 0 mock-AWS suite on every PR, Tier 1 live `init` → `apply` → `status` → `destroy` lifecycle on a nightly schedule) plus `--auto-approve`/`--yes`/`--headless` bypasses for `apply`, `destroy`, and `eject`, replacing the planned LocalStack approach.
86
+ - [ ] **Deterministic Suite & Flaky Test Elimination:** Isolate network/loopback and SDK mocks in the Vitest suite, eliminate the timing-dependent loopback failure, and record first-green Tier 0 (PR) and Tier 1 (nightly) runs.
87
+ - [ ] **Live Service Acceptance (Tier 2 E2E):** Provision each add-on capability on real AWS (nightly) and verify runtime behavior — SQS send/receive + DLQ, DynamoDB put/get, Redis connectivity, S3 presigned-URL flow, SES send, Bedrock invoke, cron scheduling — with per-service setup/assert/teardown and spend caps.
86
88
  - [ ] **Multi-Stage Dockerfile Hardening:** Refactor generated Dockerfiles to use multi-stage builds (e.g., `node:22-alpine AS builder` → `distroless/nodejs`), explicitly stripping package managers (`npm`, `yarn`) from the final runtime image to mathematically eliminate `HIGH`/`CRITICAL` vulnerability scanner noise on Day-0.
87
- - [ ] **Test Suite Deduplication & Hygiene:** Extract the hand-rolled `@clack/prompts` and `telemetry` mocks currently duplicated across 15+ test files into a centralized `tests/helpers/` directory to shrink maintenance surface area without dropping the 1,000+ test coverage count.
89
+ - [x] **Test Suite Deduplication & Hygiene:** Extract the hand-rolled `@clack/prompts` and `telemetry` mocks currently duplicated across 15+ test files into a centralized `tests/helpers/` directory to shrink maintenance surface area without dropping the 1,000+ test coverage count.
88
90
  - [ ] **Golden Signals Live Telemetry & Alert Webhooks:** Upgrade `grada status` (and `grada status --watch`) to surface real-time CloudWatch Golden Signals (ECS CPU/Memory %, ALB requests/min, p95 latency, 5xx count, and RDS connections / Aurora ACUs) at $0 extra AWS cost, and add `grada alerts` to wire CloudWatch 5xx alarms and container crash events directly to Slack, Discord, or email.
89
91
  - [ ] **Architecture Decision Records (ADRs) & Docs Audit:** Review and standardize all ADRs and Astro Starlight documentation under the `grada` brand to ensure every Phase 9–11 command, flag, compute target (`ecs` and `lambda`), and IAM security boundary is accurately documented with zero stale references.
90
92
  - [ ] **Zero-Compute Static Target (`--target static`):** Provide a dedicated target for Vite SPAs, Astro SSG, and Next.js static exports that bypasses compute entirely, deploying pre-built assets directly to an S3 bucket fronted by CloudFront.
@@ -19,7 +19,7 @@ Because `grada` generates highly dynamic Terraform (`.tf`), GitHub Actions (`.ym
19
19
  * **Updating Snapshots:** If a template change is intentional, developers must run `npm run test:update` to overwrite the baseline `__snapshots__`.
20
20
 
21
21
  ## 3. External API mocking
22
- To ensure tests run sub-second and deterministically without requiring real AWS credentials, we intercept network boundaries:
22
+ To ensure tests run sub-second and deterministically without requiring real AWS credentials, we intercept network boundaries. Shared mock factories live in `tests/helpers/` (`clack.js` for `@clack/prompts`, `telemetry.js` for PostHog tracking, `console.js` for process/console spies, `tmpdir.js` for fixture directories) so new suites reuse one-liner `vi.mock` delegations instead of hand-rolling mocks:
23
23
  * **AWS Secrets Manager:** `tests/secrets.test.js` uses Vitest's `vi.hoisted()` and `vi.mock()` to intercept `@aws-sdk/client-secrets-manager` (plus an injected ECS client for the restart path). This verifies push/pull/audit payload handling, key-change detection, and network exceptions (like `ResourceNotFoundException`) completely offline.
24
24
  * **ECS & CloudWatch Logs:** `tests/diagnose.test.js` injects mock ECS/CloudWatch clients to verify failure analysis (stopped reasons, exit codes, log extraction) and behavior contracts — e.g., expired sessions (`UnrecognizedClientException`) exit gracefully with code 1, and unrecognized `secrets push` filenames fall back to `.env` with a warning.
25
25
  * **Telemetry:** PostHog tracking is mocked to prevent test executions from polluting production analytics.
@@ -30,3 +30,8 @@ While Vitest proves the CLI generates the *correct* files, GitHub Actions proves
30
30
  * **Phase 2 (Static Application Security Testing - SAST):** CI runs a pinned Trivy filesystem scan (`aquasecurity/trivy-action` by SHA) against each generated project directory, writing advisory `trivy-fs-results.txt` reports (`HIGH,CRITICAL`, `exit-code: 0`) instead of failing the build.
31
31
  * **Phase 3 (IaC Validation):** The `iac-validation` matrix workflow scaffolds all 10 supported frameworks headlessly (`--headless --preconfigured`), then runs `terraform init -backend=false` + `terraform validate`, `tflint`, the advisory filesystem scan, a stripped-Dockerfile `docker build`, and an advisory container-image scan (`trivy-image-results.txt`).
32
32
  * **Phase 4 (Release gate):** `.github/workflows/publish.yml` reuses `iac-validation.yml` via `workflow_call` as a `validate` job; `build-and-publish` has `needs: [validate]`, so NPM publishing on release is blocked until the full matrix passes.
33
+
34
+ ## 5. End-to-end lifecycle testing
35
+ Black-box suites under `tests/e2e/` execute the real `bin/cli.js` via `child_process` with stdin closed (a prompt crashes loudly instead of hanging) and `DO_NOT_TRACK=1`. They are excluded from the default `npm test` run and have dedicated configs:
36
+ * **Tier 0 (`npm run test:e2e:tier0`, every PR):** mock-AWS scaffold checks (`init` for ECS and Lambda targets, the full 7-capability `add` matrix plus an `init --with` composition, `terraform validate`), local checks (`doctor`, `eject`), and failure-path contracts (clean exit-1 shapes, no stack traces). Runs in `.github/workflows/e2e.yml` alongside Tier 1.
37
+ * **Tier 1 (`npm run test:e2e:tier1`, nightly/manual only):** the full live lifecycle (`init` → `apply --auto-approve` → `status` with a retry-until-healthy loop → `destroy --yes`) against real AWS via OIDC, skipping gracefully without credentials.
package/bin/cli.js CHANGED
@@ -29,8 +29,8 @@ const HELP_TEXT = [
29
29
  '',
30
30
  'Commands:',
31
31
  ' init Provision infrastructure and CI/CD pipelines',
32
- ' apply Apply infrastructure changes',
33
- ' destroy Tear down infrastructure',
32
+ ' apply Apply infrastructure changes (--auto-approve)',
33
+ ' destroy Tear down infrastructure (--yes)',
34
34
  ' doctor Run pre-flight dependency checks',
35
35
  ' logs [service] Stream CloudWatch logs (--tail, -f/--follow, --error, --since, --region)',
36
36
  ' status Service health dashboard (--region, --json)',
@@ -52,7 +52,7 @@ const HELP_TEXT = [
52
52
  ' secrets push Push environment secrets',
53
53
  ' secrets pull Pull environment secrets',
54
54
  ' secrets audit Audit local vs remote secrets drift',
55
- ' eject Eject to self-managed configs',
55
+ ' eject Eject to self-managed configs (--yes)',
56
56
  ' sync-ai Sync AI assistant rules',
57
57
  '',
58
58
  'Init options:',
@@ -67,7 +67,7 @@ if (parsed.hasNoTelemetry) {
67
67
  }
68
68
  process.env.CLI_COMMAND = parsed.baseCommand;
69
69
 
70
- const { positionalArgs, isHeadless, isDryRun, isPreconfigured, headlessOptions, initOptions } = parsed;
70
+ const { positionalArgs, isHeadless, isDryRun, isPreconfigured, autoApprove, yes: confirmYes, headlessOptions, initOptions } = parsed;
71
71
 
72
72
  function parseRegionFlag(args) {
73
73
  for (let i = 0; i < args.length; i++) {
@@ -100,13 +100,13 @@ if (positionalArgs[0] === 'secrets' && positionalArgs[1] === 'push') {
100
100
  const projectName = path.basename(process.cwd());
101
101
  runCommand(auditSecrets(envFile, projectName, { region: parseRegionFlag(rawArgs) }));
102
102
  } else if (positionalArgs[0] === 'apply') {
103
- runCommand(applyStack({ isDryRun }));
103
+ runCommand(applyStack({ isDryRun, ...(autoApprove ? { autoApprove: true } : {}), ...(isHeadless ? { isHeadless: true } : {}) }));
104
104
  } else if (positionalArgs[0] === 'doctor') {
105
105
  runCommand(runDoctor());
106
106
  } else if (positionalArgs[0] === 'destroy') {
107
- runCommand(destroyStack());
107
+ runCommand(destroyStack({ ...(confirmYes ? { yes: true } : {}), ...(isHeadless ? { isHeadless: true } : {}) }));
108
108
  } else if (positionalArgs[0] === 'eject') {
109
- runCommand(ejectStack());
109
+ runCommand(ejectStack({ ...(confirmYes ? { yes: true } : {}), ...(isHeadless ? { isHeadless: true } : {}) }));
110
110
  } else if (positionalArgs[0] === 'sync-ai') {
111
111
  runCommand(syncAi());
112
112
  } else if (positionalArgs[0] === 'diagnose' || positionalArgs[0] === 'wtf') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grada-run",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -19,6 +19,8 @@
19
19
  "test:watch": "vitest",
20
20
  "test:update": "vitest run -u",
21
21
  "test:iac": "node scripts/test-iac.js",
22
+ "test:e2e:tier0": "vitest run -c vitest.e2e.tier0.config.js",
23
+ "test:e2e:tier1": "vitest run -c vitest.e2e.tier1.config.js",
22
24
  "sync:bedrock-models": "node scripts/sync-bedrock-models.js",
23
25
  "docs:dev": "npm run dev --workspace=apps/docs",
24
26
  "docs:build": "npm run build --workspace=apps/docs"
@@ -0,0 +1,50 @@
1
+ # Specification: E2E Lifecycle Testing Harness
2
+
3
+ ## 1. Overview
4
+ Implement a deterministic, two-tiered End-to-End (E2E) testing harness that executes the actual `bin/cli.js` entrypoint via `child_process`.
5
+ This spec includes a "Phase 0" to wire minimal headless bypasses into the CLI so automated lifecycle commands do not hang on interactive prompts.
6
+
7
+ ## 2. Boundaries & Constraints
8
+ - **ISOLATION:** The main unit test suite must remain blazing fast. You MUST add `exclude: ['tests/e2e/**']` to the root `vitest.config.js`.
9
+ - **NO UNIT MOCKS:** The E2E harness must execute the real binary against the real filesystem.
10
+ - **HEADLESS PURITY:** All E2E `execFileSync` calls must use `stdio: ['ignore', 'inherit', 'inherit']` (closing stdin). If a command attempts to prompt, it must crash loudly rather than hanging the test.
11
+ - **ENVIRONMENT:** All E2E tests must run with `DO_NOT_TRACK=1` and `TF_PLUGIN_CACHE_DIR`. Tier 0 explicitly sets `CI_MOCK_AWS=true`. Tier 1 explicitly requires real AWS credentials and does NOT set the mock flag.
12
+
13
+ ## 3. Phase 0: Minimal Headless Wiring (Production)
14
+ Do not write new flag parsers; `--headless` is already parsed globally as `isHeadless` in `bin/cli.js`. Implement a shared bypass helper (e.g., using `resolveHeadless()`) to handle the `--headless`, `--auto-approve`, and `--yes` precedence seamlessly:
15
+ - **Apply:** Bypass the `renderDryRunPreview` confirm prompt AND the `NoSuchBucket` recovery prompt if `--auto-approve` or `--headless` is true.
16
+ - **Destroy:** Bypass both confirm prompts if `--yes` or `--headless` is true.
17
+ - **Eject:** Bypass the confirm prompt if `--yes` or `--headless` is true.
18
+
19
+ ## 4. Phase 1: Tier 0 E2E (Fast, PR-Friendly, Mock AWS)
20
+ Create `tests/e2e/tier0.e2e.test.js` and `vitest.e2e.tier0.config.js` (with a `300s` test timeout). Add `"test:e2e:tier0": "vitest run -c vitest.e2e.tier0.config.js"` to `package.json`. Ensure `CI_MOCK_AWS=true` is set.
21
+ **Targets:**
22
+ 1. **ECS Scaffold:** In a single shared tmpdir, run `node bin/cli.js init --target ecs --headless` -> `node bin/cli.js add queue:sqs --headless`.
23
+ - *Assertions:* Exit 0. Verify `README.md` (the primary doc destination), `main.tf`, and addon files exist. Run `terraform init -backend=false && terraform validate`.
24
+ 1b. **Addon Matrix:** For each remaining capability (`storage:s3`, `db:dynamodb`, `db:redis`, `ai:bedrock`, `email:ses` with `--domain example.com`, `cron`): fresh tmpdir, `init --target ecs --headless`, `add <capability> --headless`, assert exit 0 and the capability `.tf` file exists, then init + validate. Plus one `init --target ecs --with queue:sqs,db:redis --headless` composition asserting both `.tf` files exist, then init + validate.
25
+ 2. **Lambda Scaffold:** `node bin/cli.js init --target lambda --headless`.
26
+ - *Assertions:* Exit 0. Validate Terraform.
27
+ 3. **Local Checks:**
28
+ - `eject --headless`: In a *fresh* isolated tmpdir, run `init` first, then run `eject`. Behaviorally verify metadata is stripped from files.
29
+ - `doctor`: Assert exit 0.
30
+ 4. **Failure Paths (Contract Pinning):**
31
+ - `apply --headless` outside a project directory.
32
+ - `init --target fake-target`.
33
+ - *Assertions:* Must exit 1 cleanly with expected error shape (no stack traces).
34
+
35
+ ## 5. Phase 2: Tier 1 E2E (Real AWS, Nightly/Manual)
36
+ Create `tests/e2e/tier1.live.e2e.test.js` and `vitest.e2e.tier1.config.js` (with a `15+` minute timeout). Add `"test:e2e:tier1": "vitest run -c vitest.e2e.tier1.config.js"`.
37
+ - **Skip Logic:** Use `test.skipIf(!process.env.AWS_ACCESS_KEY_ID)` so this suite gracefully skips if run locally without credentials.
38
+ - **The Lifecycle:** Run `init --target ecs --headless` -> `apply --auto-approve` -> `status` -> `destroy --yes` in sequence.
39
+ - **Assertions:** Exit code 0 for all steps. *Critical:* For `status`, implement a retry-until-healthy loop (e.g., check every 15s for up to 3 mins) since ECS tasks take time to boot after `apply`. Post-destroy, assert the state bucket was successfully deleted.
40
+
41
+ ## 6. GitHub Actions Integration
42
+ Create `.github/workflows/e2e.yml`:
43
+ - **Job 1 (Tier 0):** Runs on `pull_request` and `push`. Steps: checkout, setup-node (v24), `npm ci`, `hashicorp/setup-terraform`, and `npm run test:e2e:tier0`. Upload the workspace as an artifact on failure.
44
+ - **Job 2 (Tier 1 Live):** Runs on `workflow_dispatch` and a nightly `schedule` (`cron: '0 2 * * *'`). Steps: checkout, setup-node, `npm ci`, setup-terraform. Configure AWS credentials using `aws-actions/configure-aws-credentials@v4` with `role-to-assume: ${{ vars.AWS_ROLE_ARN }}` (the role must have broad deploy permissions). Execute `npm run test:e2e:tier1`. Upload the workspace as an artifact on failure.
45
+
46
+ ## 7. Acceptance Criteria
47
+ 1. The standard unit test suite has the same count and status as the baseline (1,068 total; only the known loopback failure), with E2E files correctly excluded from the default run.
48
+ 2. Tier 0 runs green locally and in PRs.
49
+ 3. Tier 1 passes against real AWS (or correctly skips if `AWS_ACCESS_KEY_ID` is absent).
50
+ 4. The Phase 0 bypass flags (`--headless`, `--auto-approve`, `--yes`) function correctly when invoked from a real terminal.
@@ -0,0 +1,42 @@
1
+ # Specification: Test Suite Deduplication & Hygiene
2
+
3
+ ## 1. Overview
4
+ The current Vitest test suite contains significant boilerplate. Mocks for terminal interactions (`@clack/prompts`), telemetry (`src/core/telemetry.js`), and basic console/tmpdir helpers are hand-rolled and duplicated across 17+ test files.
5
+
6
+ This task extracts these shared mocks into a centralized `tests/helpers/` directory to shrink the maintenance surface area.
7
+
8
+ ## 2. Boundaries & Constraints
9
+ - **NO PRODUCTION CHANGES:** Do not modify any files in `src/`, `bin/`, or `templates/`. Do not modify `vitest.config.js`.
10
+ - **OUT OF SCOPE FILES:** Do not migrate `tests/telemetry.test.js` or `tests/commands-import.test.js` (leave their bespoke mocks intact to prevent import/circular breakages).
11
+ - **OUT OF SCOPE MOCKS:** Do NOT attempt to unify AWS SDK or `child_process` mocks in this pass.
12
+ - **EXPLICIT IMPORTS:** Use explicit `vi.hoisted` imports per test file. Do not use global `setupFiles` auto-mocking.
13
+
14
+ ## 3. Extraction Targets
15
+ Create the `tests/helpers/` directory and extract the following:
16
+
17
+ 1. **`@clack/prompts` Mock:**
18
+ - Centralize the mock implementations for all used exports: `intro`, `outro`, `text`, `select`, `confirm`, `spinner`, `isCancel`, `cancel`, `log`, `multiselect`, `password`, `note`, and `group`.
19
+ - **Bare Defaults:** Shared factories must ship with bare `vi.fn()`s. Any preset return values (like `mockResolvedValue(['claude'])`) must remain as test-specific overrides in the individual test files.
20
+ - **`isCancel` Semantics:** Must preserve the real behavior: `(value) => typeof value === 'symbol'`, not a bare `vi.fn()`.
21
+ - **`log` Union:** The helper's `log` object must include all 5 sub-methods used across the suite: `{ info, warn, message, success, error }`.
22
+ - **Spinner Shape:** Standardize the spinner mock shape to support `.message()` calls and track created spinners.
23
+
24
+ 2. **Telemetry Mock:**
25
+ - Centralize the `vi.mock('../src/core/telemetry.js', ...)` implementations.
26
+ - Mock `trackEvent`, `flushTelemetry` (with `mockResolvedValue()`), `trackSuccess`, and `trackFailure`. Ensure the success/failure mocks delegate to `trackEvent` correctly so existing success-path assertions don't break.
27
+ - Pass through `isActiveEnvValue` using `importOriginal`.
28
+
29
+ 3. **Other Duplication:**
30
+ - Extract the recurring `process.exit`, `console.log`, and `console.error` spy trio.
31
+ - Extract `tmpdir` fixture generation helpers.
32
+
33
+ ## 4. Implementation Strategy
34
+ - Create clean, exportable mock factories in `tests/helpers/`.
35
+ - Iteratively migrate the applicable test files to use these new helpers via `vi.hoisted()`.
36
+ - Replace the duplicated inline `vi.mock()` blocks with one-liners delegating to the helper.
37
+ - **Reset Semantics:** Each file should maintain its own `vi.clearAllMocks()` or `vi.resetAllMocks()` in its `beforeEach` hook.
38
+
39
+ ## 5. Acceptance Criteria
40
+ - `tests/helpers/` exists and contains the centralized mocks.
41
+ - No test file (except the explicitly excluded ones) retains a hand-rolled `@clack/prompts` or telemetry mock block.
42
+ - **Coverage Preservation:** ≥1,067 passing tests, with zero failures other than the known pre-existing `db.test.js` loopback port failure.
@@ -5,7 +5,7 @@ import color from 'picocolors';
5
5
  import { renderDryRunPreview, parseTerraformConfig, buildCostTelemetryProps } from '../utils/visualizer.js';
6
6
  import { detectFramework } from '../utils/detector.js';
7
7
  import { trackEvent, flushTelemetry, trackSuccess, trackFailure } from '../core/telemetry.js';
8
- import { failCommand } from '../utils/command.js';
8
+ import { failCommand, shouldAutoApprove } from '../utils/command.js';
9
9
  import { normalizeOptions } from '../utils/args.js';
10
10
  import { spawnSync } from 'child_process';
11
11
  import { provisionStateBucket } from '../utils/aws.js';
@@ -58,9 +58,9 @@ export async function applyStack(input = {}) {
58
58
  ...costProps,
59
59
  });
60
60
  process.exit(0);
61
- } else if (options.autoApprove) {
61
+ } else if (shouldAutoApprove(options)) {
62
62
  await renderDryRunPreview(detectedConfig, true);
63
- } else if (!options.autoApprove) {
63
+ } else {
64
64
  const confirmed = await renderDryRunPreview(detectedConfig, false);
65
65
  if (!confirmed) {
66
66
  cancel('Apply aborted.');
@@ -140,7 +140,7 @@ export async function applyStack(input = {}) {
140
140
 
141
141
  trackEvent('recovery_prompted', { type: 'state_bucket_missing' });
142
142
 
143
- const shouldRecreate = await confirm({
143
+ const shouldRecreate = shouldAutoApprove(options) ? true : await confirm({
144
144
  message: 'Do you want to automatically recreate the state bucket and resume provisioning?',
145
145
  initialValue: true
146
146
  });
@@ -6,7 +6,7 @@ import { RDSClient, DescribeDBInstancesCommand, DescribeDBClustersCommand, Start
6
6
  import { teardownStateBucket, resolveClient } from '../utils/aws.js';
7
7
  import { checkDependency, pollUntil } from '../utils/system.js';
8
8
  import { trackEvent, flushTelemetry, trackSuccess } from '../core/telemetry.js';
9
- import { failCommand } from '../utils/command.js';
9
+ import { failCommand, shouldAutoApprove } from '../utils/command.js';
10
10
  import { runTerraformCommand } from '../utils/terraform.js';
11
11
  import { findDbTarget, resolveDbIdentifier, resolveDbClusterIdentifier } from '../utils/rds.js';
12
12
  import { normalizeOptions } from '../utils/args.js';
@@ -35,7 +35,7 @@ export async function destroyStack(input = {}) {
35
35
  return failCommand({ message: '✖ Terraform is not installed.', useErrorStream: true });
36
36
  }
37
37
 
38
- const proceed = await confirm({
38
+ const proceed = shouldAutoApprove(options) ? true : await confirm({
39
39
  message: color.red('⚠️ WARNING: This will permanently destroy all AWS resources associated with this project. Are you absolutely sure?'),
40
40
  initialValue: false,
41
41
  });
@@ -187,7 +187,7 @@ export async function destroyStack(input = {}) {
187
187
  let deleteS3Bucket = false;
188
188
 
189
189
  if (bucketName) {
190
- deleteS3Bucket = await confirm({
190
+ deleteS3Bucket = shouldAutoApprove(options) ? true : await confirm({
191
191
  message: color.yellow(`AWS compute resources destroyed. Do you also want to permanently delete the S3 state bucket?\n (Select 'No' if you plan to run 'grada apply' later to spin this back up.)`),
192
192
  initialValue: false,
193
193
  });
@@ -3,6 +3,8 @@ import path from 'path';
3
3
  import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
4
4
  import color from 'picocolors';
5
5
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
6
+ import { shouldAutoApprove } from '../utils/command.js';
7
+ import { normalizeOptions } from '../utils/args.js';
6
8
 
7
9
  // Matches the `# <brand> generated infrastructure...` header line in
8
10
  // either brand variant (Lambda targets append a suffix in parentheses).
@@ -47,14 +49,15 @@ function collectTerraformFiles(dir, out = []) {
47
49
  return out;
48
50
  }
49
51
 
50
- export async function ejectStack() {
52
+ export async function ejectStack(input = {}) {
53
+ const options = normalizeOptions(input);
51
54
  intro(color.bgRed(color.white(' grada eject ⏏️ ')));
52
55
 
53
56
  console.log(color.yellow('This will permanently decouple your infrastructure from the grada CLI.'));
54
57
  console.log(color.gray('It removes all ManagedBy tags and tool-specific metadata from your local files.'));
55
58
  console.log(color.gray('Your infrastructure will remain fully operational as raw, standalone Terraform.'));
56
59
 
57
- const shouldEject = await confirm({
60
+ const shouldEject = shouldAutoApprove(options) ? true : await confirm({
58
61
  message: 'Are you sure you want to eject? (This cannot be undone)',
59
62
  initialValue: false,
60
63
  });
@@ -120,6 +120,8 @@ export function parseCliArgs(processArgs) {
120
120
  isHeadless,
121
121
  isDryRun,
122
122
  isPreconfigured,
123
+ autoApprove: getBoolFlag('auto-approve') === true,
124
+ yes: getBoolFlag('yes') === true,
123
125
  headlessOptions,
124
126
  initOptions
125
127
  };
@@ -1,5 +1,18 @@
1
1
  import color from 'picocolors';
2
2
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
3
+ import { normalizeOptions } from './args.js';
4
+
5
+ // True when a destructive confirm must be skipped: explicit approval flags
6
+ // (--auto-approve, --yes) or explicit headless mode. Deliberately checks
7
+ // only explicit options — never CI/non-TTY inference — so a bare command
8
+ // in automation still prompts (and fails loudly) instead of silently
9
+ // approving destruction.
10
+ export function shouldAutoApprove(options = {}) {
11
+ const opts = normalizeOptions(options);
12
+ return [opts.autoApprove, opts.yes, opts.isHeadless, opts.headless].some(
13
+ (flag) => flag === true || flag === 'true'
14
+ );
15
+ }
3
16
 
4
17
  function paint(tone, fallback) {
5
18
  return typeof color[tone] === 'function' ? color[tone] : fallback;
package/tests/add.test.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ // NOTE: helper imports must stay above src imports: vi.mock factories run
3
+ // during module evaluation and need the factories initialized.
4
+ import { clackPromptsMockFactory } from './helpers/clack.js';
5
+ import { telemetryMockFactory } from './helpers/telemetry.js';
6
+ import { createTmpDirTracker } from './helpers/tmpdir.js';
2
7
  import fs from 'fs';
3
8
  import os from 'os';
4
9
  import path from 'path';
@@ -46,37 +51,15 @@ const S3_ENV = [
46
51
  { name: 'S3_CDN_URL', value: 'https://${aws_cloudfront_distribution.storage_cdn.domain_name}' },
47
52
  ];
48
53
 
49
- vi.mock('@clack/prompts', () => ({
50
- intro: vi.fn(),
51
- outro: vi.fn(),
52
- select: vi.fn(),
53
- text: vi.fn(),
54
- spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
55
- log: { info: vi.fn(), warn: vi.fn(), message: vi.fn(), success: vi.fn(), error: vi.fn() },
56
- cancel: vi.fn(),
57
- isCancel: (value) => typeof value === 'symbol',
58
- }));
59
-
60
- vi.mock('../src/core/telemetry.js', async (importOriginal) => {
61
- const actual = await importOriginal();
62
- const trackEvent = vi.fn();
63
- const flushTelemetry = vi.fn().mockResolvedValue();
64
- // Mirrors the real trackSuccess delegation so success-path assertions
65
- // keep observing trackEvent (the real helper is unit-tested separately).
66
- const trackSuccess = vi.fn(async (event, properties) => {
67
- trackEvent(event, { ...properties, success: true });
68
- await flushTelemetry();
69
- });
70
- return { trackEvent, flushTelemetry, trackSuccess, isActiveEnvValue: actual.isActiveEnvValue };
71
- });
54
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
55
+
56
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
72
57
 
73
- let tmpDirs = [];
58
+ const tmp = createTmpDirTracker();
74
59
  let exitSpy;
75
60
 
76
61
  function makeTmp() {
77
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'add-test-'));
78
- tmpDirs.push(dir);
79
- return dir;
62
+ return tmp.makeTmp('add-test-');
80
63
  }
81
64
 
82
65
  function writeWorkerTf(dir, { lifecycle = false } = {}) {
@@ -145,16 +128,14 @@ function writeMainTf(dir, environment = '') {
145
128
  }
146
129
 
147
130
  beforeEach(() => {
148
- tmpDirs = [];
131
+ tmp.reset();
149
132
  vi.clearAllMocks();
150
133
  exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
151
134
  });
152
135
 
153
136
  afterEach(() => {
154
137
  exitSpy.mockRestore();
155
- for (const dir of tmpDirs) {
156
- fs.rmSync(dir, { recursive: true, force: true });
157
- }
138
+ tmp.cleanup();
158
139
  });
159
140
 
160
141
  describe('parseAddArgs', () => {
package/tests/ai.test.js CHANGED
@@ -1,24 +1,21 @@
1
1
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ // NOTE: helper imports must stay above src imports: vi.mock factories run
3
+ // during module evaluation and need the factories initialized.
4
+ import { clackPromptsMockFactory, mockMultiselect } from './helpers/clack.js';
5
+ import { telemetryMockFactory } from './helpers/telemetry.js';
2
6
  import fs from 'fs/promises';
3
7
  import path from 'path';
4
8
  import { syncAi } from '../src/commands/sync-ai.js';
5
9
  import { injectManagedBlock } from '../src/utils/ai-rules.js';
6
10
 
7
11
  // 1. Mock the interactive prompts to simulate user input
8
- vi.mock('@clack/prompts', () => ({
9
- intro: vi.fn(),
10
- outro: vi.fn(),
11
- // Simulate the user selecting 'claude' from the list and hitting Enter
12
- multiselect: vi.fn().mockResolvedValue(['claude']),
13
- spinner: () => ({ start: vi.fn(), stop: vi.fn(), message: vi.fn() }),
14
- log: { success: vi.fn(), warn: vi.fn(), error: vi.fn(), message: vi.fn() }
15
- }));
12
+ vi.mock('@clack/prompts', () => clackPromptsMockFactory());
13
+
14
+ // Simulate the user selecting 'claude' from the list and hitting Enter
15
+ mockMultiselect.mockResolvedValue(['claude']);
16
16
 
17
17
  // 2. Mock telemetry to prevent real network calls
18
- vi.mock('../src/core/telemetry.js', () => ({
19
- trackEvent: vi.fn(),
20
- flushTelemetry: vi.fn().mockResolvedValue(),
21
- }));
18
+ vi.mock('../src/core/telemetry.js', (importOriginal) => telemetryMockFactory(importOriginal));
22
19
 
23
20
  describe('AI Context Synchronization', () => {
24
21
  const originalCwd = process.cwd();