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.
- package/.github/workflows/deploy-docs.yml +37 -0
- package/.github/workflows/iac-validation.yml +67 -6
- package/.github/workflows/publish.yml +5 -0
- package/.muserules +6 -1
- package/README.md +6 -6
- package/apps/docs/.astro/collections/docs.schema.json +644 -0
- package/apps/docs/.astro/content-assets.mjs +4 -0
- package/apps/docs/.astro/content-modules.mjs +4 -0
- package/apps/docs/.astro/content.d.ts +179 -0
- package/apps/docs/.astro/data-store.json +1 -0
- package/apps/docs/.astro/dev.json +14 -0
- package/apps/docs/.astro/settings.json +5 -0
- package/apps/docs/.astro/types.d.ts +2 -0
- package/apps/docs/astro.config.mjs +68 -0
- package/apps/docs/package.json +17 -0
- package/{docs/adr → apps/docs/src/content/docs/adrs}/0001-s3-native-state-locking.md +4 -1
- package/{docs/adr → apps/docs/src/content/docs/adrs}/0002-eject-mechanism-pure-iac.md +4 -1
- package/{docs/adr → apps/docs/src/content/docs/adrs}/0003-sync-ai-context-strategy.md +4 -1
- package/{docs/adr → apps/docs/src/content/docs/adrs}/0004-iac-driven-diagnostic-context.md +4 -1
- package/apps/docs/src/content/docs/cli/apply.md +29 -0
- package/apps/docs/src/content/docs/cli/destroy.md +29 -0
- package/apps/docs/src/content/docs/cli/diagnose.md +28 -0
- package/apps/docs/src/content/docs/cli/doctor.md +28 -0
- package/apps/docs/src/content/docs/cli/eject.md +29 -0
- package/apps/docs/src/content/docs/cli/init.md +42 -0
- package/apps/docs/src/content/docs/cli/secrets.md +36 -0
- package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
- package/apps/docs/src/content/docs/guides/aws-credentials.md +68 -0
- package/apps/docs/src/content/docs/guides/cicd-pipeline.md +46 -0
- package/{docs → apps/docs/src/content/docs}/guides/database-connections.md +4 -1
- package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
- package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
- package/{docs → apps/docs/src/content/docs}/guides/ephemeral-pr-previews.md +4 -1
- package/{docs → apps/docs/src/content/docs/guides}/examples.md +15 -8
- package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
- package/{docs → apps/docs/src/content/docs}/guides/headless.md +4 -1
- package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
- package/{docs → apps/docs/src/content/docs}/guides/secrets-management.md +4 -1
- package/apps/docs/src/content/docs/index.mdx +36 -0
- package/{docs/migration → apps/docs/src/content/docs/migrations}/astro-vercel-to-aws.md +4 -1
- package/{docs/migration → apps/docs/src/content/docs/migrations}/heroku-procfile-to-aws.md +4 -1
- package/{docs/migration → apps/docs/src/content/docs/migrations}/nextjs-vercel-to-aws.md +4 -1
- package/{docs/migration → apps/docs/src/content/docs/migrations}/sveltekit-vercel-to-aws.md +4 -1
- package/{docs/ROADMAP.md → apps/docs/src/content/docs/roadmap.md} +5 -2
- package/{docs → apps/docs/src/content/docs}/testing-strategy.md +13 -9
- package/apps/docs/src/content.config.ts +7 -0
- package/apps/docs/src/custom.css +14 -0
- package/apps/docs/tsconfig.json +6 -0
- package/bin/cli.js +1 -1
- package/package.json +7 -2
- package/src/commands/diagnose.js +21 -7
- package/src/commands/init.js +5 -5
- package/src/commands/secrets.js +23 -8
- package/src/core/telemetry.js +8 -5
- package/src/utils/aws.js +16 -0
- package/src/utils/generator.js +6 -5
- package/src/utils/prompts.js +1 -1
- package/templates/docker/nestjs.Dockerfile +15 -3
- package/templates/docker/svelte.Dockerfile +4 -2
- package/templates/terraform/backend.tf +14 -1
- package/templates/terraform/secrets.tf +0 -6
- package/tests/__snapshots__/generator.test.js.snap +4 -2
- package/tests/diagnose.test.js +2 -2
- package/tests/secrets.test.js +29 -0
- 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 {
|
package/src/utils/generator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 }`;
|
package/src/utils/prompts.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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
|
package/tests/diagnose.test.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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);
|
package/tests/secrets.test.js
CHANGED
|
@@ -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
|
});
|
package/docs/frameworks.md
DELETED
|
@@ -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`.**
|