deploy-stack 0.18.0 → 0.18.2

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 (65) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/iac-validation.yml +67 -6
  3. package/.github/workflows/publish.yml +5 -0
  4. package/.muserules +6 -1
  5. package/README.md +6 -6
  6. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  7. package/apps/docs/.astro/content-assets.mjs +4 -0
  8. package/apps/docs/.astro/content-modules.mjs +4 -0
  9. package/apps/docs/.astro/content.d.ts +179 -0
  10. package/apps/docs/.astro/data-store.json +1 -0
  11. package/apps/docs/.astro/dev.json +14 -0
  12. package/apps/docs/.astro/settings.json +5 -0
  13. package/apps/docs/.astro/types.d.ts +2 -0
  14. package/apps/docs/astro.config.mjs +68 -0
  15. package/apps/docs/package.json +17 -0
  16. package/{docs/adr → apps/docs/src/content/docs/adrs}/0001-s3-native-state-locking.md +4 -1
  17. package/{docs/adr → apps/docs/src/content/docs/adrs}/0002-eject-mechanism-pure-iac.md +4 -1
  18. package/{docs/adr → apps/docs/src/content/docs/adrs}/0003-sync-ai-context-strategy.md +4 -1
  19. package/{docs/adr → apps/docs/src/content/docs/adrs}/0004-iac-driven-diagnostic-context.md +4 -1
  20. package/apps/docs/src/content/docs/cli/apply.md +29 -0
  21. package/apps/docs/src/content/docs/cli/destroy.md +29 -0
  22. package/apps/docs/src/content/docs/cli/diagnose.md +28 -0
  23. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  24. package/apps/docs/src/content/docs/cli/eject.md +29 -0
  25. package/apps/docs/src/content/docs/cli/init.md +42 -0
  26. package/apps/docs/src/content/docs/cli/secrets.md +36 -0
  27. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  28. package/apps/docs/src/content/docs/guides/aws-credentials.md +68 -0
  29. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +46 -0
  30. package/{docs → apps/docs/src/content/docs}/guides/database-connections.md +4 -1
  31. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  32. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  33. package/{docs → apps/docs/src/content/docs}/guides/ephemeral-pr-previews.md +4 -1
  34. package/{docs → apps/docs/src/content/docs/guides}/examples.md +15 -8
  35. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  36. package/{docs → apps/docs/src/content/docs}/guides/headless.md +4 -1
  37. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  38. package/{docs → apps/docs/src/content/docs}/guides/secrets-management.md +4 -1
  39. package/apps/docs/src/content/docs/index.mdx +36 -0
  40. package/{docs/migration → apps/docs/src/content/docs/migrations}/astro-vercel-to-aws.md +4 -1
  41. package/{docs/migration → apps/docs/src/content/docs/migrations}/heroku-procfile-to-aws.md +4 -1
  42. package/{docs/migration → apps/docs/src/content/docs/migrations}/nextjs-vercel-to-aws.md +4 -1
  43. package/{docs/migration → apps/docs/src/content/docs/migrations}/sveltekit-vercel-to-aws.md +4 -1
  44. package/{docs/ROADMAP.md → apps/docs/src/content/docs/roadmap.md} +5 -2
  45. package/{docs → apps/docs/src/content/docs}/testing-strategy.md +13 -9
  46. package/apps/docs/src/content.config.ts +7 -0
  47. package/apps/docs/src/custom.css +14 -0
  48. package/apps/docs/tsconfig.json +6 -0
  49. package/bin/cli.js +1 -1
  50. package/package.json +7 -2
  51. package/src/commands/diagnose.js +21 -7
  52. package/src/commands/init.js +5 -5
  53. package/src/commands/secrets.js +23 -8
  54. package/src/core/telemetry.js +8 -5
  55. package/src/utils/aws.js +16 -0
  56. package/src/utils/generator.js +6 -5
  57. package/src/utils/prompts.js +1 -1
  58. package/templates/docker/nestjs.Dockerfile +15 -3
  59. package/templates/docker/svelte.Dockerfile +4 -2
  60. package/templates/terraform/backend.tf +14 -1
  61. package/templates/terraform/secrets.tf +0 -6
  62. package/tests/__snapshots__/generator.test.js.snap +4 -2
  63. package/tests/diagnose.test.js +2 -2
  64. package/tests/secrets.test.js +29 -0
  65. package/docs/frameworks.md +0 -34
package/src/utils/aws.js CHANGED
@@ -2,7 +2,20 @@ import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
2
2
  import { S3Client, CreateBucketCommand, PutBucketVersioningCommand, PutBucketTaggingCommand } from '@aws-sdk/client-s3';
3
3
  import { DeleteBucketCommand, ListObjectVersionsCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
4
4
 
5
+ export async function checkAwsCredentials(region) {
6
+ const resolvedRegion = region || process.env.AWS_REGION || 'us-east-1';
7
+ if (process.env.CI_MOCK_AWS === 'true') {
8
+ return { accountId: '123456789012', awsAccountId: '123456789012', region: resolvedRegion };
9
+ }
10
+ const stsClient = new STSClient({ region: resolvedRegion });
11
+ const { Account } = await stsClient.send(new GetCallerIdentityCommand({}));
12
+ return { accountId: Account, awsAccountId: Account, region: resolvedRegion };
13
+ }
14
+
5
15
  export async function provisionStateBucket(region, projectName) {
16
+ if (process.env.CI_MOCK_AWS === 'true') {
17
+ return { awsAccountId: '123456789012', stateBucketName: 'mock-tf-state-bucket' };
18
+ }
6
19
  const stsClient = new STSClient({ region });
7
20
  let awsAccountId;
8
21
 
@@ -48,6 +61,9 @@ export async function provisionStateBucket(region, projectName) {
48
61
  }
49
62
 
50
63
  export async function teardownStateBucket(region, bucketName) {
64
+ if (process.env.CI_MOCK_AWS === 'true') {
65
+ return true;
66
+ }
51
67
  const client = new S3Client({ region });
52
68
 
53
69
  try {
@@ -138,14 +138,15 @@ export async function generateTemplates(targetDir, config) {
138
138
  if (config.finalFramework === 'rails') {
139
139
  secretsArray.push(`{ "name": "RAILS_MASTER_KEY", "valueFrom": "\${local.secret_arn}:RAILS_MASTER_KEY::" }`);
140
140
 
141
- initialSecretMap += `,\n RAILS_MASTER_KEY = var.rails_master_key`;
142
-
141
+ let initialMasterKey = "1234567890abcdef1234567890abcdef"; // Dummy fallback for CI/CD
143
142
  const masterKeyPath = path.join(targetDir, 'config', 'master.key');
143
+
144
144
  if (fsSync.existsSync(masterKeyPath)) {
145
- const realKey = fsSync.readFileSync(masterKeyPath, 'utf-8').trim();
146
- const tfvarsPath = path.join(targetDir, 'terraform', 'secrets.auto.tfvars');
147
- fsSync.writeFileSync(tfvarsPath, `rails_master_key = "${realKey}"\n`);
145
+ initialMasterKey = fsSync.readFileSync(masterKeyPath, 'utf-8').trim();
148
146
  }
147
+
148
+ // Inject the string literal directly, avoiding Terraform variables entirely
149
+ initialSecretMap += `,\n RAILS_MASTER_KEY = "${initialMasterKey}"`;
149
150
  }
150
151
 
151
152
  initialSecretMap += `\n }`;
@@ -106,7 +106,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
106
106
  let aiAssistants = [];
107
107
  if (setupType === 'advanced') {
108
108
  const prChoice = await confirm({
109
- message: `Enable Ephemeral PR Previews? (Spins up isolated, temporary AWS environments for PRs)\n ${color.gray('📖 Learn more: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/guides/ephemeral-pr-previews.md')}`,
109
+ message: `Enable Ephemeral PR Previews? (Spins up isolated, temporary AWS environments for PRs)\n ${color.gray('📖 Learn more: https://github.com/anton-codes-iac/deploy-stack/blob/main/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md')}`,
110
110
  initialValue: false,
111
111
  });
112
112
  if (typeof prChoice === 'symbol') process.exit(0);
@@ -8,13 +8,25 @@ RUN npm run build
8
8
 
9
9
  # --- Stage 2: Production ---
10
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
11
+
12
+ # 1. DevSecOps: Patch Alpine OS
13
+ RUN apk update && apk upgrade --no-cache
14
+
13
15
  ENV NODE_ENV=production
14
16
  WORKDIR /app
17
+
18
+ # 2. Install ONLY production dependencies
15
19
  COPY --chown=node:node package*.json ./
16
- RUN npm ci --omit=dev
20
+ RUN npm ci --omit=dev && npm cache clean --force
21
+
22
+ # 3. DevSecOps: Nuke all package managers to eliminate base-image CVEs
23
+ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
24
+ /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \
25
+ /usr/local/lib/node_modules/corepack /usr/local/bin/corepack
26
+
27
+ # 4. Copy the compiled application
17
28
  COPY --chown=node:node --from=builder /app/dist ./dist
29
+
18
30
  USER node
19
31
  # Expose the port defined by Terraform
20
32
  EXPOSE {{PORT}}
@@ -40,8 +40,10 @@ RUN npm ci --omit=dev && npm cache clean --force
40
40
  # 4. Copy the compiled SvelteKit server from the builder stage
41
41
  COPY --from=builder --chown=node:node /app/build/ ./build/
42
42
 
43
- # 5. DevSecOps: Nuke NPM completely to eliminate Trivy vulnerabilities
44
- RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
43
+ # 5. DevSecOps: Nuke all package managers to eliminate base-image CVEs
44
+ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \
45
+ /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \
46
+ /usr/local/lib/node_modules/corepack /usr/local/bin/corepack
45
47
 
46
48
  USER node
47
49
  EXPOSE {{PORT}}
@@ -1,4 +1,17 @@
1
1
  terraform {
2
+ required_version = ">= 1.5.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 5.0"
8
+ }
9
+ tls = {
10
+ source = "hashicorp/tls"
11
+ version = "~> 4.0"
12
+ }
13
+ }
14
+
2
15
  backend "s3" {
3
16
  bucket = "{{STATE_BUCKET}}"
4
17
  key = "state/terraform.tfstate"
@@ -6,4 +19,4 @@ terraform {
6
19
  encrypt = true
7
20
  use_lockfile = true
8
21
  }
9
- }
22
+ }
@@ -19,12 +19,6 @@ locals {
19
19
  secret_arn = terraform.workspace == "default" ? aws_secretsmanager_secret.app_secrets[0].arn : data.aws_secretsmanager_secret.existing_secrets[0].arn
20
20
  }
21
21
 
22
- # Fallback dummy key for CI/CD environments where the real key isn't present
23
- variable "rails_master_key" {
24
- type = string
25
- default = "1234567890abcdef1234567890abcdef"
26
- }
27
-
28
22
  # Initial placeholder secret so the ECS task doesn't fail on first boot
29
23
  resource "aws_secretsmanager_secret_version" "app_secrets_initial" {
30
24
  count = terraform.workspace == "default" ? 1 : 0
@@ -5356,8 +5356,10 @@ RUN npm ci --omit=dev && npm cache clean --force
5356
5356
  # 4. Copy the compiled SvelteKit server from the builder stage
5357
5357
  COPY --from=builder --chown=node:node /app/build/ ./build/
5358
5358
 
5359
- # 5. DevSecOps: Nuke NPM completely to eliminate Trivy vulnerabilities
5360
- RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
5359
+ # 5. DevSecOps: Nuke all package managers to eliminate base-image CVEs
5360
+ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx \\
5361
+ /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \\
5362
+ /usr/local/lib/node_modules/corepack /usr/local/bin/corepack
5361
5363
 
5362
5364
  USER node
5363
5365
  EXPOSE 8000
@@ -104,7 +104,7 @@ describe('Command: diagnose', () => {
104
104
  try {
105
105
  const result = await runDiagnose({
106
106
  cluster: 'test-cluster',
107
- region: 'us-east-1',
107
+ region: 'us-east-2',
108
108
  logGroup: '/ecs/test'
109
109
  });
110
110
 
@@ -132,7 +132,7 @@ describe('Command: diagnose', () => {
132
132
  });
133
133
 
134
134
  try {
135
- const result = await runDiagnose({ cluster: 'test-cluster', region: 'us-east-1' });
135
+ const result = await runDiagnose({ cluster: 'test-cluster', region: 'us-east-2' });
136
136
  expect(result.healthy).toBe(true);
137
137
  expect(mockLogsSend).not.toHaveBeenCalled();
138
138
  expect(output.join('\n')).toMatch(/healthy|No stopped tasks/i);
@@ -90,4 +90,33 @@ describe('Secrets Push Command', () => {
90
90
  exitSpy.mockRestore();
91
91
  consoleSpy.mockRestore();
92
92
  });
93
+
94
+ it('gracefully falls back to .env when envFilePath is undefined or omitted', async () => {
95
+ // Create default .env file
96
+ await fs.writeFile('.env', 'DATABASE_URL=postgres://localhost:5432/db');
97
+ await fs.writeFile(path.join('terraform', 'main.tf'), 'region = "us-east-1"');
98
+
99
+ // Call pushSecrets with undefined/omitted argument
100
+ await pushSecrets(undefined, 'my-project');
101
+
102
+ // Verify it resolved .env properly and sent secrets
103
+ expect(MockUpdateSecretCommand).toHaveBeenCalledWith({
104
+ SecretId: 'my-project-secrets',
105
+ SecretString: JSON.stringify({ DATABASE_URL: 'postgres://localhost:5432/db' })
106
+ });
107
+ });
108
+
109
+ it('gracefully falls back to .env when envFilePath is passed as an object or invalid type', async () => {
110
+ // Simulates Commander passing an options object as the first parameter
111
+ await fs.writeFile('.env', 'STRIPE_KEY=sk_test_12345');
112
+ await fs.writeFile(path.join('terraform', 'main.tf'), 'region = "us-east-1"');
113
+
114
+ // Call pushSecrets with an object
115
+ await pushSecrets({}, 'my-project');
116
+
117
+ expect(MockUpdateSecretCommand).toHaveBeenCalledWith({
118
+ SecretId: 'my-project-secrets',
119
+ SecretString: JSON.stringify({ STRIPE_KEY: 'sk_test_12345' })
120
+ });
121
+ });
93
122
  });
@@ -1,34 +0,0 @@
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 adapter, `.auto.tfvars` Master Key injection, Kamal Dockerfile replaced with 0-CVE Alpine build | None (0.0.0.0 set via Docker CMD) | `rails-template-deploy-stack` |
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`.**