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.
- package/.github/workflows/publish.yml +25 -0
- package/.github/workflows/test.yml +43 -0
- package/README.md +3 -3
- package/bin/cli.js +12 -42
- package/docs/ROADMAP.md +2 -2
- package/docs/adr/0001-s3-native-state-locking.md +35 -0
- package/docs/adr/0002-eject-mechanism-pure-iac.md +37 -0
- package/docs/adr/0003-sync-ai-context-strategy.md +46 -0
- package/docs/examples.md +4 -1
- package/docs/frameworks.md +34 -0
- package/docs/testing-strategy.md +23 -0
- package/package.json +7 -2
- package/src/commands/init.js +10 -2
- package/src/core/parser.js +43 -0
- package/src/utils/detector.js +22 -0
- package/src/utils/generator.js +1 -0
- package/src/utils/prompts.js +2 -1
- package/src/utils/warnings.js +7 -0
- package/templates/README.md +2 -0
- package/templates/docker/nestjs.Dockerfile +21 -0
- package/templates/docker/python.Dockerfile +1 -1
- package/tests/__snapshots__/generator.test.js.snap +7194 -0
- package/tests/ai.test.js +60 -0
- package/tests/cli.test.js +21 -0
- package/tests/generator.test.js +130 -0
- package/tests/parser.test.js +32 -0
- package/tests/secrets.test.js +93 -0
|
@@ -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
|
-
- [
|
|
174
|
-
- [
|
|
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
|
|
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
|
-
|
|
20
|
-
const args = rawArgs.filter((arg) => arg !== '--no-telemetry' && !arg.startsWith('--no-telemetry='));
|
|
20
|
+
const { positionalArgs, isHeadless, isDryRun, headlessOptions } = parsed;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
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 (
|
|
26
|
+
} else if (positionalArgs[0] === 'apply') {
|
|
57
27
|
applyStack({ isDryRun }).catch(e => { console.error(e); process.exit(1); });
|
|
58
|
-
} else if (
|
|
28
|
+
} else if (positionalArgs[0] === 'doctor') {
|
|
59
29
|
runDoctor().catch(e => { console.error(e); process.exit(1); });
|
|
60
|
-
} else if (
|
|
30
|
+
} else if (positionalArgs[0] === 'destroy') {
|
|
61
31
|
destroyStack().catch(e => { console.error(e); process.exit(1); });
|
|
62
|
-
} else if (
|
|
32
|
+
} else if (positionalArgs[0] === 'eject') {
|
|
63
33
|
ejectStack().catch(e => { console.error(e); process.exit(1); });
|
|
64
|
-
} else if (
|
|
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
|
-
- [
|
|
34
|
-
- [
|
|
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
|
-
* **[
|
|
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.
|
|
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": "
|
|
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
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -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 === '
|
|
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
|
+
}
|
package/src/utils/detector.js
CHANGED
|
@@ -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
|
}
|
package/src/utils/generator.js
CHANGED
|
@@ -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',
|
package/src/utils/prompts.js
CHANGED
|
@@ -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({
|
package/src/utils/warnings.js
CHANGED
|
@@ -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 ')) +
|
package/templates/README.md
CHANGED
|
@@ -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
|
|