deploy-stack 0.17.7 → 0.17.9

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.
@@ -28,6 +28,31 @@ jobs:
28
28
 
29
29
  - name: Install dependencies
30
30
  run: npm install
31
+
32
+ # --- Phase 1: Static Generation Validation ---
33
+ - name: Run Unit & Snapshot Tests
34
+ run: npm test
35
+
36
+ # --- Phase 2: Execution & Security Validation ---
37
+ - name: Setup TFLint
38
+ uses: terraform-linters/setup-tflint@v4
39
+ with:
40
+ github_token: ${{ secrets.GITHUB_TOKEN }}
41
+
42
+ - name: Init TFLint
43
+ run: tflint --init
44
+
45
+ - name: Run TFLint across Terraform files
46
+ run: tflint --recursive || true
47
+
48
+ - name: Run Trivy IaC Config Scanner
49
+ uses: aquasecurity/trivy-action@master
50
+ with:
51
+ scan-type: 'config'
52
+ scan-dir: '.'
53
+ format: 'table'
54
+ exit-code: '0'
31
55
 
56
+ # --- Phase 3: Publish ---
32
57
  - name: Publish to NPM
33
58
  run: npm publish --provenance
@@ -0,0 +1,43 @@
1
+ name: Test Suite
2
+
3
+ on:
4
+ push:
5
+ branches: [ "**" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Setup Node.js
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '24.x'
19
+
20
+ - name: Install dependencies
21
+ run: npm install
22
+
23
+ - name: Run Vitest Suite
24
+ run: npm test
25
+
26
+ - name: Setup TFLint
27
+ uses: terraform-linters/setup-tflint@v4
28
+ with:
29
+ github_token: ${{ secrets.GITHUB_TOKEN }}
30
+
31
+ - name: Init TFLint
32
+ run: tflint --init
33
+
34
+ - name: Run TFLint across Terraform files
35
+ run: tflint --recursive || true
36
+
37
+ - name: Run Trivy IaC Config Scanner
38
+ uses: aquasecurity/trivy-action@master
39
+ with:
40
+ scan-type: 'config'
41
+ scan-dir: '.'
42
+ format: 'table'
43
+ exit-code: '0' # Set to '1' to block PRs on Trivy findings once baselined
package/README.md CHANGED
@@ -28,7 +28,7 @@ You retain complete ownership of your infrastructure code without relying on bla
28
28
  ## ✨ Features
29
29
 
30
30
  **🚀 Zero-Config Deployments**
31
- * **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, Go, Django, Rails, Nuxt 3, and Static Sites (React, Vue, SvelteKit, Astro).
31
+ * **Framework Agnostic:** Tailored container presets for Next.js, Express.js, NestJS, FastAPI, Go, Django, Rails, Nuxt 3, and Static Sites (React, Vue, SvelteKit, Astro).
32
32
  * **Smart Discovery:** Automatically detects build output directories and generates highly optimized, multi-stage Dockerfiles.
33
33
  * **Migration Engines:** Natively parses Heroku `Procfile` configurations, `vercel.json` routing rules, and `docker-compose.yml` sidecar architectures to automatically translate them into standard AWS Fargate and Application Load Balancer topologies.
34
34
  * **Database Scaffolding:** Automatically provisions fully isolated, zero-trust AWS RDS PostgreSQL databases for backend monoliths.
@@ -170,8 +170,8 @@ npx deploy-stack --no-telemetry
170
170
  - [x] `vite-plugin-deploy-stack` (Vite / React / Vue SPA ecosystem)
171
171
  - [x] `svelte-adapter-deploy-stack` (SvelteKit adapter integration)
172
172
  - [x] `cookiecutter-django-deploy-stack` (Listed on Django Packages)
173
- - [ ] `cookiecutter-fastapi-deploy-stack` (Cookiecutter for modern async Python)
174
- - [ ] `nest-deploy-stack` (Native `nest add` schematic for NestJS)
173
+ - [x] `cookiecutter-fastapi-deploy-stack` (Cookiecutter for modern async Python)
174
+ - [x] `nest-deploy-stack` (Native `nest add` schematic for NestJS)
175
175
  - [ ] `rails-template-deploy-stack` (Zero-click Ruby on Rails application template)
176
176
  - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` (alias: `wtf`) to automatically analyze and troubleshoot common day-2 AWS operational issues (e.g., Fargate OOM kills, ALB 502s) directly from the terminal.
177
177
 
package/bin/cli.js CHANGED
@@ -7,61 +7,31 @@ import { pushSecrets } from '../src/commands/secrets.js';
7
7
  import { ejectStack } from '../src/commands/eject.js';
8
8
  import { applyStack } from '../src/commands/apply.js';
9
9
  import { syncAi } from '../src/commands/sync-ai.js';
10
+ import { parseCliArgs } from '../src/core/parser.js';
10
11
 
11
- // 1. Extract the telemetry flag and set the environment variable
12
12
  const rawArgs = process.argv.slice(2);
13
- const hasNoTelemetry = rawArgs.some(arg => arg === '--no-telemetry' || arg.startsWith('--no-telemetry='));
13
+ const parsed = parseCliArgs(rawArgs);
14
14
 
15
- if (hasNoTelemetry) {
15
+ if (parsed.hasNoTelemetry) {
16
16
  process.env.DO_NOT_TRACK = '1';
17
17
  }
18
+ process.env.CLI_COMMAND = parsed.baseCommand;
18
19
 
19
- // 2. Filter out the telemetry flag from the args so the subcommands don't see it
20
- const args = rawArgs.filter((arg) => arg !== '--no-telemetry' && !arg.startsWith('--no-telemetry='));
20
+ const { positionalArgs, isHeadless, isDryRun, headlessOptions } = parsed;
21
21
 
22
- // 3. Isolate positional commands from flags
23
- // This ensures flags (e.g., --dry-run) don't accidentally become file paths
24
- const positionalArgs = args.filter(arg => !arg.startsWith('--'));
25
-
26
- // Safely capture the base command for telemetry (ignoring file paths)
27
- const baseCommand = positionalArgs.length > 0 ? positionalArgs.slice(0, 2).join(' ') : 'init';
28
- process.env.CLI_COMMAND = baseCommand;
29
-
30
- // 4. Parse headless flags
31
- const isHeadless = args.includes('--headless');
32
- const isDryRun = args.includes('--dry-run');
33
- const getFlag = (flagName) => {
34
- const match = args.find(a => a === `--${flagName}` || a.startsWith(`--${flagName}=`));
35
- if (match === `--${flagName}`) return true;
36
- return match ? match.split('=')[1] : undefined;
37
- };
38
- const headlessOptions = isHeadless ? {
39
- dir: getFlag('dir'),
40
- framework: getFlag('framework'),
41
- region: getFlag('region'),
42
- port: getFlag('port'),
43
- size: getFlag('size'),
44
- healthCheckPath: getFlag('healthCheckPath'),
45
- desiredCount: getFlag('desiredCount'),
46
- branch: getFlag('branch'),
47
- needsDatabase: getFlag('needsDatabase'),
48
- enablePrPreviews: getFlag('enablePrPreviews')
49
- } : {};
50
-
51
- // 5. Handle commands
52
- if (args[0] === 'secrets' && args[1] === 'push') {
53
- const envFile = args[2] || '.env';
22
+ if (positionalArgs[0] === 'secrets' && positionalArgs[1] === 'push') {
23
+ const envFile = positionalArgs[2] || '.env';
54
24
  const projectName = path.basename(process.cwd());
55
25
  pushSecrets(envFile, projectName).catch(e => { console.error(e); process.exit(1); });
56
- } else if (args[0] === 'apply') {
26
+ } else if (positionalArgs[0] === 'apply') {
57
27
  applyStack({ isDryRun }).catch(e => { console.error(e); process.exit(1); });
58
- } else if (args[0] === 'doctor') {
28
+ } else if (positionalArgs[0] === 'doctor') {
59
29
  runDoctor().catch(e => { console.error(e); process.exit(1); });
60
- } else if (args[0] === 'destroy') {
30
+ } else if (positionalArgs[0] === 'destroy') {
61
31
  destroyStack().catch(e => { console.error(e); process.exit(1); });
62
- } else if (args[0] === 'eject') {
32
+ } else if (positionalArgs[0] === 'eject') {
63
33
  ejectStack().catch(e => { console.error(e); process.exit(1); });
64
- } else if (args[0] === 'sync-ai') {
34
+ } else if (positionalArgs[0] === 'sync-ai') {
65
35
  syncAi().catch(e => { console.error(e); process.exit(1); });
66
36
  } else {
67
37
  mainStack({ isHeadless, headlessOptions }).catch(e => { console.error(e); process.exit(1); });
package/docs/ROADMAP.md CHANGED
@@ -30,7 +30,7 @@
30
30
  - [x] `vite-plugin-deploy-stack` (Live on NPM)
31
31
  - [x] `svelte-adapter-deploy-stack` (SvelteKit adapter integration)
32
32
  - [x] `cookiecutter-django-deploy-stack` (Listed on Django Packages)
33
- - [ ] `cookiecutter-fastapi-deploy-stack` (Cookiecutter for modern async Python)
34
- - [ ] `nest-deploy-stack` (Native `nest add` schematic for NestJS)
33
+ - [x] `cookiecutter-fastapi-deploy-stack` (Cookiecutter for modern async Python)
34
+ - [x] `nest-deploy-stack` (Native `nest add` schematic for NestJS)
35
35
  - [ ] `rails-template-deploy-stack` (Zero-click Ruby on Rails application template)
36
36
  - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` (alias: `wtf`) to automatically analyze and troubleshoot common day-2 AWS operational issues (e.g., Fargate OOM kills, ALB 502s) directly from the terminal.
@@ -0,0 +1,35 @@
1
+ # 0001. S3 Native State Locking
2
+
3
+ * **Status:** Accepted
4
+ * **Date:** 2026-08-15 (Retroactive)
5
+ * **Deciders:** Core Engineering Team
6
+
7
+ ## Context and Problem Statement
8
+
9
+ When deploying infrastructure via Terraform across local developer workstations and automated CI/CD pipelines, remote state management is required to prevent race conditions, state drift, and concurrent apply corruption.
10
+
11
+ Traditionally, managing Terraform remote state on AWS required provisioning both an S3 bucket (for storage) and a dedicated DynamoDB table (for state locking). This added operational overhead, increased the baseline AWS resource footprint, and required developers to manage extra IAM permissions solely for locking metadata.
12
+
13
+ ## Decision Drivers
14
+
15
+ * **Simplicity:** Minimize the number of AWS resources a user has to provision and manage day-to-day.
16
+ * **Cost Efficiency:** Eliminate unnecessary idle infrastructure costs (e.g., DynamoDB provisioned capacity).
17
+ * **Reliability:** Guarantee that concurrent CI/CD pipeline runs and local CLI executions cannot corrupt Terraform state files.
18
+
19
+ ## Considered Options
20
+
21
+ 1. **S3 + DynamoDB Table:** The traditional HashiCorp recommendation for remote state locking.
22
+ 2. **S3 Native State Locking:** Utilizing S3's native conditional write support for state locking directly within the S3 bucket backend.
23
+ 3. **Third-Party State Backends:** (e.g., Terraform Cloud) — rejected to preserve zero-vendor-lock-in and keep execution local to the user's AWS account.
24
+
25
+ ## Decision Outcome
26
+
27
+ **Chosen Option:** Use an encrypted Amazon S3 bucket as the remote state backend leveraging Terraform's native S3 state locking capabilities.
28
+
29
+ ### Positive Consequences
30
+ * **Zero Maintenance:** Users do not have to monitor, manage, or pay for an extra DynamoDB table.
31
+ * **Tighter Security:** Simplifies the IAM policy scope required for the `deploy-stack` state bucket helper, adhering strictly to least privilege.
32
+ * **Frictionless Onboarding:** Streamlines the bootstrapping experience during the initial `npx deploy-stack` run.
33
+
34
+ ### Negative Consequences
35
+ * Relies on modern Terraform backend behavior that supports S3 native locks. Edge cases involving highly outdated, legacy Terraform CLI versions are not supported.
@@ -0,0 +1,37 @@
1
+ # 0002. Eject Mechanism for Pure IaC
2
+
3
+ * **Status:** Accepted
4
+ * **Date:** 2026-08-20 (Retroactive)
5
+ * **Deciders:** Core Engineering Team
6
+
7
+ ## Context and Problem Statement
8
+
9
+ `deploy-stack` abstracts away the complexity of writing raw Terraform for ECS Fargate, ALBs, CloudFront, OIDC, and Secrets Manager. However, a primary reason senior platform teams hesitate to adopt deployment generators is the fear of **tool lock-in**. Teams need a guarantee that if their architecture outgrows the CLI, or if they wish to take 100% manual control of the codebase, they can do so without starting from scratch.
10
+
11
+ ## Decision Drivers
12
+
13
+ * **Zero Vendor Lock-In:** Uphold the foundational promise that developers permanently own their infrastructure code.
14
+ * **Auditability & Freedom:** Provide teams with an unambiguous "escape hatch" to sever ties with `deploy-stack` management metadata while maintaining a perfectly functioning infrastructure pipeline.
15
+
16
+ ## Considered Options
17
+
18
+ 1. **No Eject Command:** Require users to manually delete `ManagedBy` tags and untangle state/workflows by hand.
19
+ 2. **Submodule / Framework Wrapper:** Keep the Terraform code hidden inside a remote module (rejected, as it violates the core premise of transparent, readable IaC).
20
+ 3. **Explicit `eject` Command:** Build a dedicated `npx deploy-stack eject` utility that strips all CLI metadata and tracking tags, leaving behind clean, standard Terraform and GitHub Actions files.
21
+
22
+ ## Decision Outcome
23
+
24
+ **Chosen Option:** Implement an explicit `npx deploy-stack eject` command as a core feature.
25
+
26
+ ### Technical Implementation Details
27
+ When invoked, `eject`:
28
+ * Removes or sanitizes internal `ManagedBy = "deploy-stack"` default tags across all generated files.
29
+ * Preserves all generated `.tf`, `Dockerfile`, and `.github/workflows/` files safely in place.
30
+ * Detaches the project from the CLI entirely, leaving valid Terraform code that can be managed directly via the `terraform` or `opentofu` binaries.
31
+
32
+ ### Positive Consequences
33
+ * Builds trust with engineers and security teams who refuse black-box wrappers.
34
+ * Eliminates friction during adoption; users know they can safely leave at any time.
35
+
36
+ ### Negative Consequences
37
+ * Ejected repositories permanently lose access to automated security patches, template updates, or CLI-driven drift synchronization.
@@ -0,0 +1,46 @@
1
+ # 0003. AI Context Synchronization Strategy
2
+
3
+ * **Status:** Accepted
4
+ * **Date:** 2026-09-02 (Retroactive)
5
+ * **Deciders:** Core Engineering Team
6
+
7
+ ## Context and Problem Statement
8
+
9
+ Modern engineering teams heavily utilize AI coding assistants (Cursor, GitHub Copilot, Windsurf, Claude Code, etc.) in their local IDEs. However, when dealing with Infrastructure-as-Code (IaC), AI models frequently hallucinate invalid Terraform syntax, recommend destructive manual AWS CLI commands, or ignore critical project-specific constraints like unprivileged container ports and OIDC auth flows.
10
+
11
+ Furthermore, automatically writing instruction files into user repositories carries a high risk of clobbering a team's existing, carefully crafted agent prompts.
12
+
13
+ ## Decision Drivers
14
+
15
+ * **Hallucination Mitigation:** Provide structured, deterministic instructions to IDE AI assistants to ensure they generate valid Terraform and safe workflows.
16
+ * **Non-Destructive Integration:** Guarantee that existing `.cursorrules`, `CLAUDE.md`, or shared workspace instruction files are never accidentally overwritten or destroyed.
17
+ * **Multi-Tool Support:** Support the highly fragmented landscape of AI coding tools without forcing users into a specific IDE.
18
+
19
+ ## Considered Options
20
+
21
+ 1. **Single Global Instruction File:** Only support `.cursorrules` (rejected as too narrow for modern multi-tool teams).
22
+ 2. **Blind Overwrite of Agent Files:** Replace existing AI rule files with `deploy-stack` defaults (rejected due to the unacceptable risk of destroying user configuration).
23
+ 3. **Isolated Rule Files + Delimited Block Injection (`sync-ai`):** Create dedicated files where supported (e.g., `deploy-stack.mdc`), and safely inject delimited, managed markdown blocks into existing shared instruction files where necessary.
24
+
25
+ ## Decision Outcome
26
+
27
+ **Chosen Option:** Build a dedicated `npx deploy-stack sync-ai` command and a non-destructive auto-injection engine.
28
+
29
+ ### Supported Targets
30
+ The engine intelligently maps instructions to the following environments:
31
+ * **Cursor:** `.cursor/rules/deploy-stack.mdc`
32
+ * **Roo Code / Roo-Cline:** `.roo/rules/deploy-stack.md`
33
+ * **Trae:** `.trae/rules/project_rules.md` (managed block injection)
34
+ * **Continue:** `.prompts/deploy-stack.prompt`
35
+ * **Windsurf:** `.windsurfrules` (managed block injection)
36
+ * **GitHub Copilot:** `.github/copilot-instructions.md` (managed block injection)
37
+ * **Claude Code:** `CLAUDE.md` (managed block injection)
38
+ * **Goose:** `.goosehints`
39
+ * **Aider:** `.aider.conf.yml` / `.aider.model.settings.yml`
40
+
41
+ ### Positive Consequences
42
+ * Dramatically reduces AI-induced infrastructure errors and dangerous AWS CLI recommendations.
43
+ * Safe, idempotent execution allows teams to run `npx deploy-stack sync-ai` whenever their architecture parameters (like AWS region or ports) change, without fear of losing their own prompts.
44
+
45
+ ### Negative Consequences
46
+ * Requires ongoing maintenance of parser logic and block delimiters as AI coding assistant vendors rapidly change their configuration file specifications.
package/docs/examples.md CHANGED
@@ -20,6 +20,7 @@ These repositories demonstrate how `deploy-stack` handles various frameworks and
20
20
 
21
21
  ### Backend APIs & Monoliths
22
22
  * **[Express.js API](https://github.com/anton-codes-iac/deploy-stack-express-example):** A standard Node.js backend setup.
23
+ * **[NestJS API](https://github.com/anton-codes-iac/deploy-stack-nest-example):** A robust NestJS architecture utilizing AST code-patching and highly optimized multi-stage TypeScript builds.
23
24
  * **[Python FastAPI](https://github.com/anton-codes-iac/deploy-stack-fastapi-example):** A Python API demonstrating unprivileged port mapping.
24
25
  * **[Ruby on Rails](https://github.com/anton-codes-iac/deploy-stack-rails-example):** A production Rails 7+ setup featuring an auto-provisioned PostgreSQL database and secure `.auto.tfvars` Master Key injection.
25
26
  * **[Django / Python](https://github.com/anton-codes-iac/deploy-stack-django-example):** A secure Gunicorn/WSGI implementation with PostgreSQL and unprivileged container adapters.
@@ -35,4 +36,6 @@ In addition to standalone reference repositories, `deploy-stack` provides native
35
36
  * **[nuxt-deploy-stack](https://www.npmjs.com/package/nuxt-deploy-stack):** Nitro-optimized deployment integration for Nuxt 3 applications.
36
37
  * **[vite-plugin-deploy-stack](https://www.npmjs.com/package/vite-plugin-deploy-stack):** Zero-config Vite build plugin for single-page applications.
37
38
  * **[svelte-adapter-deploy-stack](https://www.npmjs.com/package/svelte-adapter-deploy-stack):** Native SvelteKit adapter producing optimized Fargate container builds.
38
- * **[cookiecutter-django-deploy-stack](https://github.com/anton-codes-iac/cookiecutter-django-deploy-stack):** Community Django starter listed on [Django Packages](https://djangopackages.org/packages/p/cookiecutter-django-deploy-stack/) with built-in Fargate and managed RDS scaffolding.
39
+ * **[nest-deploy-stack](https://www.npmjs.com/package/nest-deploy-stack):** Native Angular DevKit schematic for NestJS, installable via `nest add`.
40
+ * **[cookiecutter-django-deploy-stack](https://github.com/anton-codes-iac/cookiecutter-django-deploy-stack):** Community Django starter listed on Django Packages.
41
+ * **[cookiecutter-fastapi-deploy-stack](https://github.com/anton-codes-iac/cookiecutter-fastapi-deploy-stack):** Instant scaffolding for modern, async FastAPI deployments.
@@ -0,0 +1,34 @@
1
+ # 🧩 Framework Support & Quirks
2
+
3
+ `deploy-stack` is designed to be as "zero-config" as possible. However, because different frameworks have unique internal architectures (especially around network binding and build outputs), a few frameworks require minor application-level tweaks to run securely in a Dockerized AWS Fargate environment.
4
+
5
+ ## The 3-Tier Support Philosophy
6
+
7
+ We handle framework requirements using a 3-tier strategy so you are never left guessing why a deployment failed:
8
+
9
+ 1. **Zero-Touch Plugins (Tier 1):** If you use one of our ecosystem plugins (e.g., `nest add nest-deploy-stack` or `cookiecutter-django-deploy-stack`), your code is automatically patched and configured. Zero manual intervention required.
10
+ 2. **Intelligent CLI Pre-flight (Tier 2):** If you run the standalone `deploy-stack` CLI against a raw repository, the CLI statically analyzes your code. If it detects a missing production requirement (like a localhost binding), it will flag it inline in your terminal with the exact copy-paste fix.
11
+ 3. **In-Repo Docs (Tier 3):** The generated `DEPLOYMENT.md` file always contains a framework-specific checklist before you push to CI/CD.
12
+
13
+ ---
14
+
15
+ ## 🛠️ Framework Requirements Cheat Sheet
16
+
17
+ | Framework | What `deploy-stack` Automates | Application Code Requirement | Zero-Click Starter / Plugin |
18
+ |---|---|---|---|
19
+ | **Next.js** | Multi-stage Dockerfile, CloudFront edge routing, `vercel.json` parsing | `output: 'standalone'` must be set in `next.config.js` | Built-in CLI detection |
20
+ | **NestJS** | Multi-stage TypeScript build (`dist/`), unprivileged Node runtime | `await app.listen(port, '0.0.0.0')` in `src/main.ts` | `nest-deploy-stack` (`nest add`) |
21
+ | **FastAPI** | Alpine Python container, Uvicorn CLI args, unprivileged port mapping | None (0.0.0.0 set via Docker CMD) | `cookiecutter-fastapi-deploy-stack` |
22
+ | **Django** | Gunicorn WSGI adapter, Celery worker topologies, RDS bindings | None (0.0.0.0 set via Docker CMD) | `cookiecutter-django-deploy-stack` |
23
+ | **Ruby on Rails** | Puma multi-threading adapter, `.auto.tfvars` Master Key injection | None (0.0.0.0 set via Docker CMD) | *Coming soon* |
24
+ | **Nuxt 3** | Nitro-optimized Node output | None (`NITRO_HOST=0.0.0.0` injected automatically) | `nuxt-deploy-stack` |
25
+ | **SvelteKit** | Node adapter conversion | None (`HOST=0.0.0.0` injected automatically) | `svelte-adapter-deploy-stack` |
26
+ | **Static Sites** *(Vite, Astro, React)* | Output folder detection (`dist/`, `build/`), Nginx routing | None | `vite-plugin-deploy-stack` |
27
+
28
+ ## ⚠️ The Golden Rule: 0.0.0.0 vs Localhost
29
+
30
+ The most common reason a newly deployed container fails its ALB health check is network binding.
31
+
32
+ In local development, frameworks bind to `localhost` (or `127.0.0.1`) for security. However, inside a Docker container on AWS ECS, binding to `localhost` means the web server only listens to internal container traffic. The AWS Application Load Balancer (ALB) trying to route traffic from the outside world will hit a closed port, resulting in a `502 Bad Gateway` or `503 Service Temporarily Unavailable`.
33
+
34
+ **Always ensure your application explicitly binds to `0.0.0.0`.**
@@ -0,0 +1,23 @@
1
+ # deploy-stack Testing Strategy
2
+
3
+ To ensure zero regressions in infrastructure generation and safe local execution, `deploy-stack` relies on a multi-layered testing strategy split between fast local snapshots and rigid CI/CD validation.
4
+
5
+ ## 1. Unit & Argument Testing
6
+ We use pure Node.js unit tests (via Vitest) to validate the CLI argument parser (`src/core/parser.js`). This ensures that flags (like `--headless` or `--no-telemetry`) are routed correctly and never hijack positional arguments like file paths.
7
+
8
+ ## 2. Infrastructure Snapshot Harness (The Static Contract)
9
+ Because `deploy-stack` generates highly dynamic Terraform (`.tf`), GitHub Actions (`.yml`), and `Dockerfile` configurations, we use **Vitest Snapshots** to lock in the expected text outputs.
10
+ * **The Matrix:** The test suite generates dummy projects across 11 architectural topologies (including Django, Rails, Go, Nuxt, Next.js, SvelteKit, and Vercel/Heroku migrations).
11
+ * **Negative Testing:** The suite explicitly checks for the *absence* of files (e.g., ensuring `database.tf` or `worker.tf` are not generated for static sites).
12
+ * **Updating Snapshots:** If a template change is intentional, developers must run `npm run test:update` to overwrite the baseline `__snapshots__`.
13
+
14
+ ## 3. External API Mocking
15
+ To ensure tests run sub-second and deterministically without requiring real AWS credentials, we intercept network boundaries:
16
+ * **AWS Secrets Manager:** `tests/secrets.test.js` uses Vitest's `vi.hoisted()` and `vi.mock()` to intercept `@aws-sdk/client-secrets-manager`. This verifies the CLI correctly formats payloads and handles network exceptions (like `ResourceNotFoundException`) completely offline.
17
+ * **Telemetry:** PostHog tracking is mocked to prevent test executions from polluting production analytics.
18
+
19
+ ## 4. Continuous Integration & Execution Validation (CI)
20
+ While Vitest proves the CLI generates the *correct* files, GitHub Actions proves those files *actually work*. All tests are strictly gated via `.github/workflows/test.yml`.
21
+ * **Phase 1 (Generation):** Vitest runs unit and snapshot tests to verify the CLI contract.
22
+ * **Phase 2 (Static Application Security Testing - SAST):** The CI pipeline runs `trivy config` against the generated `Dockerfile` and Terraform snapshots to guarantee they remain compliant with strict security policies.
23
+ * **Phase 3 (IaC Validation):** The CI pipeline runs `tflint` and `terraform validate` against the snapshots to ensure HashiCorp's compiler accepts the generated syntax before a release is cut.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.17.7",
3
+ "version": "0.17.9",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -10,7 +10,9 @@
10
10
  "deploy-stack": "bin/cli.js"
11
11
  },
12
12
  "scripts": {
13
- "test": "echo \"Error: no test specified\" && exit 1"
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:update": "vitest run -u"
14
16
  },
15
17
  "author": "anton-codes-iac",
16
18
  "repository": {
@@ -48,5 +50,8 @@
48
50
  "dotenv": "17.4.2",
49
51
  "js-yaml": "5.4.1",
50
52
  "picocolors": "1.1.1"
53
+ },
54
+ "devDependencies": {
55
+ "vitest": "5.0.0"
51
56
  }
52
57
  }
@@ -10,7 +10,8 @@ import {
10
10
  parseVercelConfig,
11
11
  analyzeNextConfig,
12
12
  analyzeSvelteConfig,
13
- analyzeAstroConfig
13
+ analyzeAstroConfig,
14
+ analyzeNestApp
14
15
  } from '../utils/detector.js';
15
16
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
16
17
  import { getFrameworkWarning } from '../utils/warnings.js';
@@ -74,7 +75,14 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
74
75
  }
75
76
 
76
77
  // 4. Framework Migration Checks (Vercel Escape Hatch)
77
- if (config.framework === 'nextjs') {
78
+ if (config.framework === 'nestjs') {
79
+ const nestAnalysis = analyzeNestApp(dirConfig.targetDir);
80
+ if (nestAnalysis.hasMain && !nestAnalysis.listensOnAllInterfaces) {
81
+ log.warn(color.yellow(`⚠️ NestJS must listen on 0.0.0.0 to receive traffic in AWS Fargate.`));
82
+ console.log(color.cyan(` In ${nestAnalysis.filePath}, update your bootstrap:`));
83
+ console.log(color.green(` await app.listen(process.env.PORT ?? 3000, '0.0.0.0');\n`));
84
+ }
85
+ } else if (config.framework === 'nextjs') {
78
86
  const nextConfig = analyzeNextConfig(dirConfig.targetDir);
79
87
  if (nextConfig.hasConfig && !nextConfig.isStandalone) {
80
88
  log.warn(color.yellow('⚠️ Next.js config is missing "output: \'standalone\'".'));
@@ -0,0 +1,43 @@
1
+ export function parseCliArgs(processArgs) {
2
+ // 1. Extract telemetry flag safely
3
+ const hasNoTelemetry = processArgs.some(arg => arg === '--no-telemetry' || arg.startsWith('--no-telemetry='));
4
+
5
+ // 2. Filter out telemetry flag
6
+ const args = processArgs.filter(arg => arg !== '--no-telemetry' && !arg.startsWith('--no-telemetry='));
7
+
8
+ // 3. Isolate positional commands
9
+ const positionalArgs = args.filter(arg => !arg.startsWith('--'));
10
+ const baseCommand = positionalArgs.length > 0 ? positionalArgs.slice(0, 2).join(' ') : 'init';
11
+
12
+ // 4. Parse execution flags
13
+ const isHeadless = args.includes('--headless');
14
+ const isDryRun = args.includes('--dry-run');
15
+
16
+ const getFlag = (flagName) => {
17
+ const match = args.find(a => a === `--${flagName}` || a.startsWith(`--${flagName}=`));
18
+ if (match === `--${flagName}`) return true;
19
+ return match ? match.split('=')[1] : undefined;
20
+ };
21
+
22
+ const headlessOptions = isHeadless ? {
23
+ dir: getFlag('dir'),
24
+ framework: getFlag('framework'),
25
+ region: getFlag('region'),
26
+ port: getFlag('port'),
27
+ size: getFlag('size'),
28
+ healthCheckPath: getFlag('healthCheckPath'),
29
+ desiredCount: getFlag('desiredCount'),
30
+ branch: getFlag('branch'),
31
+ needsDatabase: getFlag('needsDatabase'),
32
+ enablePrPreviews: getFlag('enablePrPreviews')
33
+ } : {};
34
+
35
+ return {
36
+ hasNoTelemetry,
37
+ positionalArgs,
38
+ baseCommand,
39
+ isHeadless,
40
+ isDryRun,
41
+ headlessOptions
42
+ };
43
+ }
@@ -17,6 +17,7 @@ export function detectFramework(targetDir) {
17
17
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
18
18
 
19
19
  // Fullstack / API
20
+ if (deps['@nestjs/core']) return { id: 'nestjs', name: 'NestJS' };
20
21
  if (deps['next']) return { id: 'nextjs', name: 'Next.js' };
21
22
  if (deps['nuxt']) return { id: 'nuxt', name: 'Nuxt 3 (SSR)' };
22
23
  if (deps['express']) return { id: 'node', name: 'Node.js / Express' };
@@ -192,4 +193,25 @@ export function analyzeAstroConfig(targetDir) {
192
193
  else if (content.includes('@astrojs/node')) adapter = 'node';
193
194
 
194
195
  return { hasConfig: true, adapter };
196
+ }
197
+
198
+ // Checks if a NestJS app explicitly listens on 0.0.0.0 for Docker networking
199
+ export function analyzeNestApp(targetDir) {
200
+ const mainTsPath = path.join(targetDir, 'src', 'main.ts');
201
+ const mainJsPath = path.join(targetDir, 'src', 'main.js');
202
+
203
+ const filePath = fsSync.existsSync(mainTsPath) ? mainTsPath : (fsSync.existsSync(mainJsPath) ? mainJsPath : null);
204
+ if (!filePath) return { hasMain: false, listensOnAllInterfaces: true };
205
+
206
+ try {
207
+ const content = fsSync.readFileSync(filePath, 'utf-8');
208
+ const hasHostBinding = content.includes('0.0.0.0');
209
+ return {
210
+ hasMain: true,
211
+ listensOnAllInterfaces: hasHostBinding,
212
+ filePath: path.relative(targetDir, filePath)
213
+ };
214
+ } catch {
215
+ return { hasMain: false, listensOnAllInterfaces: true };
216
+ }
195
217
  }
@@ -266,6 +266,7 @@ Thumbs.db
266
266
 
267
267
  const presets = {
268
268
  node: '\n# Node.js\nnode_modules/\nnpm-debug.log\nyarn-error.log\n',
269
+ nestjs: '\n# NestJS\nnode_modules/\ndist/\nnpm-debug.log\n',
269
270
  nextjs: '\n# Next.js\nnode_modules/\n.next/\nout/\nbuild/\nnext-env.d.ts\n',
270
271
  nuxt: '\n# Nuxt 3\nnode_modules/\n.nuxt/\n.output/\ndist/\n',
271
272
  python: '\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\nvenv/\nenv/\n.venv/\n.pytest_cache/\n',
@@ -58,6 +58,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
58
58
  message: 'Which framework preset should we configure?',
59
59
  options: [
60
60
  { value: 'node', label: 'Node.js / Express' },
61
+ { value: 'nestjs', label: 'NestJS' },
61
62
  { value: 'nextjs', label: 'Next.js (Standalone)' },
62
63
  { value: 'nuxt', label: 'Nuxt 3 (SSR)' },
63
64
  { value: 'svelte', label: 'SvelteKit (SSR)' },
@@ -90,7 +91,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
90
91
  } catch (e) { }
91
92
 
92
93
  let needsDatabase = false;
93
- const isBackendFramework = ['node', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
94
+ const isBackendFramework = ['node', 'nestjs', 'nextjs', 'nuxt', 'python', 'django', 'rails', 'go'].includes(finalFramework);
94
95
 
95
96
  if (isBackendFramework) {
96
97
  const dbChoice = await confirm({
@@ -8,6 +8,13 @@ export function getFrameworkWarning(frameworkId) {
8
8
  color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
9
9
  color.yellow('\n See the "Critical Application Prerequisites" section in your README.md for copy-paste code.\n\n')
10
10
  );
11
+ case 'nestjs':
12
+ return (
13
+ color.bgYellow(color.black(' ⚠️ IMPORTANT: NESTJS SETUP REQUIRED ')) +
14
+ color.yellow('\n You must ensure your app binds to 0.0.0.0 to receive traffic in AWS Fargate.') +
15
+ color.yellow('\n In src/main.ts, update your bootstrap function to:') +
16
+ color.green('\n await app.listen(process.env.PORT ?? 3000, \'0.0.0.0\');\n\n')
17
+ );
11
18
  case 'node':
12
19
  return (
13
20
  color.bgYellow(color.black(' ⚠️ IMPORTANT: NODE.JS SETUP REQUIRED ')) +
@@ -73,6 +73,7 @@ AWS constantly pings your container to ensure it is alive. If you configured a c
73
73
  Make sure your app returns a `200 OK` at your configured path:
74
74
 
75
75
  * **Next.js (App Router):** Create `app/api/health/route.ts` returning a 200 response.
76
+ * **NestJS:** No action required if your health check path is `/` (the default `AppController` handles this). If you configured a custom path, create a specific controller for it returning a 200 response.
76
77
  * **Express.js:** Add `app.get('/api/health', (req, res) => res.sendStatus(200));`
77
78
  * **FastAPI/Python:** Add `@app.get("/api/health")` returning a 200 status.
78
79
  * **Ruby on Rails:** Rails 7.1+ includes a default `/up` health check. Ensure `Rails.application.config.force_ssl = true` isn't blocking HTTP health checks from the ALB.
@@ -101,6 +102,7 @@ export default nextConfig;
101
102
  When running inside a Docker container, your server must bind to all network interfaces (`0.0.0.0`), not just `localhost` or `127.0.0.1`. If you bind to localhost, the AWS Load Balancer will not be able to route traffic to your application.
102
103
 
103
104
  Make sure your app is configured correctly:
105
+ * **NestJS:** Update `src/main.ts` to `await app.listen(process.env.PORT ?? 3000, '0.0.0.0');`
104
106
  * **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
105
107
  * **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
106
108
  * **Ruby on Rails:** Bound automatically by the CLI's Puma command (`-b tcp://0.0.0.0:{{PORT}}`).
@@ -0,0 +1,21 @@
1
+ # --- Stage 1: Build ---
2
+ FROM node:22-alpine AS builder
3
+ WORKDIR /app
4
+ COPY package*.json ./
5
+ RUN npm ci
6
+ COPY . .
7
+ RUN npm run build
8
+
9
+ # --- Stage 2: Production ---
10
+ FROM node:22-alpine
11
+ # DevSecOps: Patch Alpine OS and update npm
12
+ RUN apk update && apk upgrade --no-cache && npm install -g npm@latest
13
+ ENV NODE_ENV=production
14
+ WORKDIR /app
15
+ COPY --chown=node:node package*.json ./
16
+ RUN npm ci --omit=dev
17
+ COPY --chown=node:node --from=builder /app/dist ./dist
18
+ USER node
19
+ # Expose the port defined by Terraform
20
+ EXPOSE {{PORT}}
21
+ CMD ["node", "dist/main.js"]
@@ -11,7 +11,7 @@ RUN addgroup -S appuser && \
11
11
  adduser -S appuser -G appuser -D -s /bin/sh
12
12
 
13
13
  # Install runtime libraries and temporary build tools
14
- RUN apk update && \
14
+ RUN apk update && apk upgrade --no-cache && \
15
15
  apk add --no-cache libpq && \
16
16
  apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev
17
17