deploy-stack 0.9.14 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  > The zero-lock-in cloud generator. Eject your containerized web app from expensive PaaS platforms to production-ready, highly available AWS infrastructure in 60 seconds.
4
4
 
5
+ [![NPM Version](https://img.shields.io/npm/v/deploy-stack.svg?color=blue&logo=npm)](https://www.npmjs.com/package/deploy-stack)
6
+ [![Node.js Support](https://img.shields.io/node/v/deploy-stack.svg?color=brightgreen)](https://www.npmjs.com/package/deploy-stack)
7
+ [![Security: Trivy](https://img.shields.io/badge/Security-Trivy_Scanned-blue.svg?logo=docker)](https://trivy.dev/)
5
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Security: Trivy](https://img.shields.io/badge/Security-Trivy_Scanned-blue.svg)](https://trivy.dev/)
7
9
 
8
10
  <!-- ![deploy-stack CLI demonstration](./docs/demo.gif) -->
9
11
 
@@ -25,21 +27,26 @@ You retain complete ownership of your infrastructure code without relying on bla
25
27
 
26
28
  ## ✨ Features
27
29
 
28
- * **Zero Vendor Lock-In:** Generates standard, clean `.tf` files. Modify, expand, or decouple them at any time.
29
- * **Automated DevSecOps:** Integrated Trivy vulnerability scanning for your infrastructure (IaC) and Docker containers on every GitHub Actions run.
30
- * **Hardened, Unprivileged Containers:** Built-in Dockerfile generators explicitly drop root privileges and utilize `nginx-unprivileged` for maximum Fargate security compliance.
31
- * **Cost & Observability Baselines:** Prevents runaway AWS bills with explicit 14-day CloudWatch log retention policies and auto-generates 5XX error alerting.
32
- * **Safe Overwrite Flow:** Idempotent CLI safely backs up existing configurations to timestamped `.bak` files and updates `.gitignore` to guarantee zero data loss during rapid iteration.
33
- * **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, and **Zero-Config Static Sites** (React, Vue, SvelteKit, Astro, Vite).
34
- * **Smart Git Integration:** Automatically detects your working branch (`main`, `master`, `develop`) and binds it directly to the generated GitHub Actions pipeline.
35
- * **Production-Grade Defaults:** Automatically provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
36
- * **Global Edge Acceleration:** Includes an integrated AWS CloudFront CDN distribution with SSL termination and optimized caching.
37
- * **Keyless, Zero-Secret CI/CD:** Uses AWS IAM OpenID Connect (OIDC) for automated deployments—no long-lived AWS keys stored in GitHub Secrets.
38
- * **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform (1.7+) native S3 concurrency locking.
39
- * **Built-in Secrets Sync:** Provides a dedicated CLI workflow to securely push local `.env` variables into AWS Secrets Manager and map them directly into containers at runtime.
40
- * **Ecosystem Ready:** Integrated directly with the [deploy-stack GitHub Action](https://github.com/marketplace/actions/deploy-stack-aws-fargate-terraform-deploy) for a secure, boilerplate-free continuous deployment pipeline.
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).
32
+ * **Smart Discovery:** Automatically detects build output directories and generates highly optimized, multi-stage Dockerfiles.
41
33
  * **Database Scaffolding:** Automatically provisions fully isolated, zero-trust AWS RDS PostgreSQL databases for backend monoliths.
42
- * **Safe Teardown:** Completely remove all generated AWS resources and empty S3 state buckets with a single `destroy` command.
34
+
35
+ **🛡️ DevSecOps & Security**
36
+ * **Automated Trivy Scanning:** Integrated IaC and container vulnerability scanning on every GitHub Actions run.
37
+ * **Hardened Containers:** Explicitly drops root privileges using `nginx-unprivileged` and distroless bases for strict Fargate security compliance.
38
+ * **Zero-Secret CI/CD:** Utilizes AWS IAM OpenID Connect (OIDC) for automated deployments—no long-lived AWS keys in GitHub.
39
+ * **Built-in Secrets Manager:** Push local `.env` variables directly into encrypted AWS Secrets Manager vaults with a single CLI command.
40
+
41
+ **☁️ AWS Native Architecture**
42
+ * **Production Defaults:** Provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
43
+ * **Global Edge Acceleration:** Integrated AWS CloudFront CDN distribution with SSL termination and edge caching.
44
+ * **Cost & Observability:** Prevents runaway AWS bills with explicit 14-day CloudWatch log retention and auto-generates 5XX error alerting.
45
+
46
+ **🛠️ Developer Experience**
47
+ * **Zero Vendor Lock-In:** Generates standard, readable Terraform (`.tf`) files. You own the infrastructure.
48
+ * **Native S3 State Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform concurrency locking.
49
+ * **Safe Iteration:** Idempotent CLI safely backs up existing configurations to `.bak` files to guarantee zero data loss.
43
50
 
44
51
  ---
45
52
 
@@ -47,60 +54,44 @@ You retain complete ownership of your infrastructure code without relying on bla
47
54
 
48
55
  Run the CLI directly in your project root:
49
56
 
50
- ```bash
57
+ \`\`\`bash
51
58
  npx deploy-stack
52
- ```
59
+ \`\`\`
53
60
 
54
- The interactive CLI will guide you through the setup:
55
- 1. **Target Directory / Name:** (Type `.` to bootstrap your current directory)
56
- 2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced** for custom scaling and branch names)
57
- 3. **Zero-Config Detection:** The CLI automatically scans your project to detect your framework. For static sites, it intelligently discovers your build output directory (`dist`, `build`, `.output`, etc.) and dynamically configures the hardened Nginx container.
58
- 4. **AWS Region & Compute Tier:** (Select your target region and Fargate size with live cost estimates)
61
+ The interactive wizard will analyze your codebase, detect your framework, estimate your AWS costs, and generate your Terraform and GitHub Actions configurations.
59
62
 
60
63
  ---
61
64
 
62
- ## 🔑 Securely Managing Secrets
65
+ ## 🧰 CLI Command Reference
63
66
 
64
- Avoid committing sensitive environment files to version control. Push local variables directly to AWS:
67
+ `deploy-stack` manages the entire lifecycle of your infrastructure.
65
68
 
66
- ```bash
67
- npx deploy-stack secrets push .env.production
68
- ```
69
- *This command encrypts your values in AWS Secrets Manager and updates `terraform/secret_keys.json` to expose those variables inside your ECS tasks at boot.*
69
+ * **`npx deploy-stack apply`**
70
+ Wraps Terraform execution in a beautiful, terminal-friendly UI. Automatically provisions your AWS infrastructure and outputs your live CDN and Load Balancer URLs.
71
+
72
+ * **`npx deploy-stack secrets push <file>`**
73
+ Securely encrypts your local environment variables (e.g., `.env.production`) into AWS Secrets Manager and maps them to your ECS container at runtime.
70
74
 
71
- ---
75
+ * **`npx deploy-stack doctor`**
76
+ Scans your local environment and generated files to ensure all required dependencies (Docker, Terraform, AWS CLI) are installed and configured correctly.
72
77
 
73
- ## 🛠️ Next Steps After Generation
74
-
75
- 1. **Provision Infrastructure:**
76
- ```bash
77
- cd terraform
78
- terraform init
79
- terraform apply
80
- ```
81
- 2. **Push to GitHub:**
82
- ```bash
83
- git add .
84
- git commit -m "feat: infrastructure and deployment pipeline"
85
- git push origin main
86
- ```
87
- 3. **Automated Deployment:** GitHub Actions securely authenticates via OIDC, builds your container, pushes to Amazon ECR, and executes a zero-downtime rolling deployment to ECS.
78
+ * **`npx deploy-stack destroy`**
79
+ Safely tears down your ECS cluster, Load Balancers, and networking resources to stop AWS billing. Includes an interactive prompt to optionally retain or delete your S3 remote state bucket.
88
80
 
89
- ---
90
-
91
- ## 🗑️ Infrastructure Teardown
92
- To safely completely remove your ECS cluster, load balancers, and empty the remote S3 state bucket, run:
93
- `npx deploy-stack destroy`
81
+ * **`npx deploy-stack eject`**
82
+ Strips all `deploy-stack` metadata and management tags from your project, leaving behind pure, standard Terraform and GitHub Actions files. You retain 100% ownership.
94
83
 
95
84
  ---
96
85
 
97
86
  ## 📁 Generated File Structure
98
87
 
99
- Running the CLI generates a modular architecture tailored to your service:
88
+ Running the CLI seamlessly integrates a modular, DevSecOps-hardened architecture into your repository:
100
89
 
101
- ```text
90
+ \`\`\`text
102
91
  your-project/
103
92
  ├── Dockerfile # Multi-stage container preset
93
+ ├── .dockerignore # Prevents secret leaks into container builds
94
+ ├── .gitignore # Automatically updated to ignore tfstate and .bak files
104
95
  ├── .github/
105
96
  │ └── workflows/
106
97
  │ └── deploy.yml # Keyless OIDC CI/CD deployment pipeline
@@ -112,7 +103,7 @@ your-project/
112
103
  ├── secrets.tf # AWS Secrets Manager integration
113
104
  ├── backend.tf # S3 Remote State backend with native locking
114
105
  └── secret_keys.json # Dynamic key map for injected environment variables
115
- ```
106
+ \`\`\`
116
107
 
117
108
  ---
118
109
 
@@ -125,6 +116,10 @@ your-project/
125
116
  * **[Create React App](https://github.com/anton-codes-iac/deploy-stack-cra-example):** Validates backward compatibility with legacy Webpack pipelines and `build/` auto-detection.
126
117
  * **[Astro Static Site](https://github.com/anton-codes-iac/deploy-stack-astro-example):** Demonstrates modern static site generation (SSG).
127
118
  * **[SvelteKit Application](https://github.com/anton-codes-iac/deploy-stack-svelte-example):** Demonstrates static adapter integration and custom output folder detection.
119
+ * **[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.
120
+ * **[Nuxt 3 (SSR)](https://github.com/anton-codes-iac/deploy-stack-nuxt-example):** Demonstrates a fully server-side rendered Nuxt application using Nitro's optimized Node output.
121
+ * **[Django / Python](https://github.com/anton-codes-iac/deploy-stack-django-example):** A secure Gunicorn/WSGI implementation with PostgreSQL and unprivileged container adapters.
122
+ * **[Go / Fiber](https://github.com/anton-codes-iac/deploy-stack-go-example):** A distroless, compiled Go binary deployment demonstrating ultra-low memory footprints and instant boot times.
128
123
  ---
129
124
 
130
125
  ## 🛡️ Telemetry & Privacy
@@ -139,8 +134,6 @@ npx deploy-stack --no-telemetry
139
134
 
140
135
  ## 🗺️ Roadmap
141
136
 
142
- ## 🗺️ Roadmap
143
-
144
137
  ### Phase 1–3: The Core Engine (Completed)
145
138
  - [x] **Core MVP:** Interactive CLI, ECS Fargate + ALB generation, CI/CD, and Secrets sync.
146
139
  - [x] **Production Readiness:** CloudFront CDN edge distribution, native S3 state locking, and secure OIDC integration.
@@ -153,9 +146,10 @@ npx deploy-stack --no-telemetry
153
146
  - [x] **Zero Vendor Lock-In:** Explicit `npx deploy-stack eject` command to safely strip `ManagedBy` tags and CLI metadata, leaving behind pure IaC.
154
147
  - [x] **Heavy Backend Monoliths:** Hardened, unprivileged container adapters for Go, Nuxt.js, Django, and Rails, complete with automated zero-trust RDS PostgreSQL provisioning.
155
148
 
156
- ### Phase 5: The Activation Engine (Pre-1.0 Growth Loops)
149
+ ### Phase 5: The Activation Engine (v0.10.0 - Current)
150
+ - [x] **Local Execution Wrapper:** Native `deploy-stack apply` command with terminal-optimized streaming to eliminate Terraform context switching.
151
+ - [ ] **Ecosystem Integrations:** Publishing official plugins to the Astro Integrations directory and backend framework ecosystems.
157
152
  - [ ] **Ephemeral PR Previews:** Generating live preview URLs on every GitHub Pull Request, turning single-user tests into team-wide advertisements.
158
- - [ ] **Next.js Image Optimization Handler:** A drop-in AWS Serverless handler (Lambda + CloudFront) to process images affordably, removing a massive Vercel lock-in hurdle.
159
153
  - [ ] **GitHub Deployments UI Sync:** Wiring up the native GitHub "Environments" tab for instant visual validation that the CLI succeeded in the background.
160
154
 
161
155
  ### Phase 6: Workflow Interception (IDE, AI, & Local Bridges)
package/bin/cli.js CHANGED
@@ -5,6 +5,7 @@ import { destroyStack } from '../src/commands/destroy.js';
5
5
  import { runDoctor } from '../src/commands/doctor.js';
6
6
  import { pushSecrets } from '../src/commands/secrets.js';
7
7
  import { ejectStack } from '../src/commands/eject.js';
8
+ import { applyStack } from '../src/commands/apply.js';
8
9
 
9
10
  // 1. Extract the telemetry flag and set the environment variable
10
11
  const rawArgs = process.argv.slice(2);
@@ -22,6 +23,8 @@ if (args[0] === 'secrets' && args[1] === 'push') {
22
23
  const envFile = args[2] || '.env';
23
24
  const projectName = path.basename(process.cwd());
24
25
  pushSecrets(envFile, projectName).catch(console.error);
26
+ } else if (args[0] === 'apply') {
27
+ applyStack().catch(console.error);
25
28
  } else if (args[0] === 'doctor') {
26
29
  runDoctor().catch(console.error);
27
30
  } else if (args[0] === 'destroy') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.9.14",
3
+ "version": "0.10.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,133 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { intro, outro, spinner, log, cancel } from '@clack/prompts';
5
+ import color from 'picocolors';
6
+
7
+ // Helper to run a command while piping the latest stdout line into a @clack spinner
8
+ function runTerraformCommand(args, cwd, spin, loadingPrefix) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn('terraform', args, { cwd });
11
+ let errorOutput = '';
12
+ let isDone = false;
13
+
14
+ child.stdout.on('data', (data) => {
15
+ if (isDone) return;
16
+ const lines = data.toString().split('\n').filter(line => line.trim() !== '');
17
+ if (lines.length > 0) {
18
+ const latestLine = lines[lines.length - 1].trim();
19
+ const display = latestLine.length > 120 ? latestLine.substring(0, 117) + '...' : latestLine;
20
+ spin.message(`${loadingPrefix} - ${color.dim(display)}`);
21
+ }
22
+ });
23
+
24
+ child.stderr.on('data', (data) => {
25
+ errorOutput += data.toString();
26
+ });
27
+
28
+ child.on('close', (code) => {
29
+ isDone = true;
30
+ if (code === 0) {
31
+ resolve();
32
+ } else {
33
+ reject(new Error(errorOutput || `Terraform exited with code ${code}`));
34
+ }
35
+ });
36
+
37
+ child.on('error', (err) => {
38
+ isDone = true;
39
+ reject(err);
40
+ });
41
+ });
42
+ }
43
+
44
+ // Helper to pull JSON outputs from Terraform
45
+ function getTerraformOutputs(cwd) {
46
+ return new Promise((resolve) => {
47
+ const child = spawn('terraform', ['output', '-json'], { cwd });
48
+ let outputData = '';
49
+
50
+ child.stdout.on('data', (data) => {
51
+ outputData += data.toString();
52
+ });
53
+
54
+ child.on('close', (code) => {
55
+ if (code === 0) {
56
+ try {
57
+ resolve(JSON.parse(outputData));
58
+ } catch {
59
+ resolve({});
60
+ }
61
+ } else {
62
+ resolve({});
63
+ }
64
+ });
65
+
66
+ child.on('error', () => {
67
+ resolve({});
68
+ });
69
+ });
70
+ }
71
+
72
+ export async function applyStack() {
73
+ intro(color.bgCyan(color.black(' deploy-stack apply ☁️ ')));
74
+
75
+ const targetDir = process.cwd();
76
+ const tfDir = path.join(targetDir, 'terraform');
77
+
78
+ // 1. Verify that the user is inside a project generated by deploy-stack
79
+ if (!fs.existsSync(tfDir) || !fs.existsSync(path.join(tfDir, 'main.tf'))) {
80
+ log.error(color.red('✖ No terraform directory found in current path.'));
81
+ log.message('Please run "npx deploy-stack" first to generate your infrastructure templates.');
82
+ process.exit(1);
83
+ }
84
+
85
+ const s = spinner();
86
+
87
+ try {
88
+ // 2. Run terraform init
89
+ s.start('Initializing Terraform plugins...');
90
+ await runTerraformCommand(['init', '-upgrade'], tfDir, s, 'Initializing');
91
+ log.success('Terraform initialized.');
92
+
93
+ // 3. Run terraform apply
94
+ s.start('Provisioning AWS infrastructure (this may take 3–5 minutes)...');
95
+ await runTerraformCommand(['apply', '-auto-approve'], tfDir, s, 'Provisioning');
96
+ s.stop('AWS infrastructure provisioned successfully!');
97
+
98
+ // 4. Extract and print the outputs
99
+ const outputs = await getTerraformOutputs(tfDir);
100
+
101
+ const cfUrl = outputs.cloudfront_url?.value;
102
+ const albUrl = outputs.alb_direct_url?.value;
103
+
104
+ let finalMessage = color.green('✅ Infrastructure is live!');
105
+ if (cfUrl) finalMessage += `\n 🌍 CDN: ${color.cyan(cfUrl)}`;
106
+ if (albUrl) finalMessage += `\n 🚦 ALB: ${color.gray(albUrl)}`;
107
+
108
+ outro(`${finalMessage}\n\n ${color.yellow('Push code to deploy your app and clear the 503 error:')}\n ${color.cyan('git add . && git commit -m "ci: infra" && git push origin main')}`);
109
+
110
+ process.exit(0);
111
+
112
+ } catch (error) {
113
+ s.stop(color.red('❌ Terraform apply failed.'));
114
+
115
+ const errString = error.message || "";
116
+
117
+ if (errString.includes('EntityAlreadyExists') && errString.includes('token.actions.githubusercontent.com')) {
118
+ log.error(color.red('GitHub OIDC Provider Conflict Detected'));
119
+ log.message('AWS only allows one GitHub Actions provider per account, and you already have one configured.');
120
+ log.message('');
121
+ log.message(`${color.bold('How to fix this:')}`);
122
+ log.message(`1. Open ${color.cyan('terraform/oidc.tf')}`);
123
+ log.message(`2. Change ${color.green('create_oidc_provider = true')} to ${color.green('create_oidc_provider = false')}`);
124
+ log.message(`3. Run ${color.cyan('npx deploy-stack apply')} again.`);
125
+ } else {
126
+ // Generic fallback for unknown errors
127
+ log.error(color.red(errString));
128
+ log.message(`${color.bold('To debug manually, navigate to your terraform folder:')}`);
129
+ log.message(color.cyan('cd terraform && terraform apply'));
130
+ }
131
+ process.exit(1);
132
+ }
133
+ }
@@ -2,11 +2,45 @@ import fsSync from 'fs';
2
2
  import path from 'path';
3
3
  import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
4
4
  import color from 'picocolors';
5
- import { execSync } from 'child_process';
5
+ import { execSync, spawn } from 'child_process';
6
6
  import { teardownStateBucket } from '../utils/aws.js';
7
7
  import { checkDependency } from '../utils/system.js';
8
8
  import { trackEvent, flushTelemetry } from '../core/telemetry.js';
9
9
 
10
+ // Helper to run a command while piping the latest stdout line into a @clack spinner
11
+ function runTerraformCommand(args, cwd, spin, loadingPrefix) {
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn('terraform', args, { cwd });
14
+ let errorOutput = '';
15
+ let isDone = false;
16
+
17
+ child.stdout.on('data', (data) => {
18
+ if (isDone) return;
19
+ const lines = data.toString().split('\n').filter(line => line.trim() !== '');
20
+ if (lines.length > 0) {
21
+ const latestLine = lines[lines.length - 1].trim();
22
+ const display = latestLine.length > 120 ? latestLine.substring(0, 117) + '...' : latestLine;
23
+ spin.message(`${loadingPrefix} - ${color.dim(display)}`);
24
+ }
25
+ });
26
+
27
+ child.stderr.on('data', (data) => {
28
+ errorOutput += data.toString();
29
+ });
30
+
31
+ child.on('close', (code) => {
32
+ isDone = true;
33
+ if (code === 0) resolve();
34
+ else reject(new Error(errorOutput || `Terraform exited with code ${code}`));
35
+ });
36
+
37
+ child.on('error', (err) => {
38
+ isDone = true;
39
+ reject(err);
40
+ });
41
+ });
42
+ }
43
+
10
44
  export async function destroyStack() {
11
45
  intro(color.bgRed(color.white(' deploy-stack destroy 🗑️ ')));
12
46
 
@@ -46,23 +80,34 @@ export async function destroyStack() {
46
80
  const region = regionMatch ? regionMatch[1] : 'us-east-1';
47
81
 
48
82
  // 2. Execute Terraform Destroy
49
- console.log(color.cyan('\nInitiating Terraform destroy (this may take a few minutes)...\n'));
83
+ s.start('Destroying AWS compute resources (this takes a few minutes)...');
50
84
  try {
51
- execSync('terraform destroy -auto-approve', { cwd: tfDirPath, stdio: 'inherit' });
85
+ await runTerraformCommand(['destroy', '-auto-approve'], tfDirPath, s, 'Destroying');
86
+ s.stop('AWS compute resources destroyed.');
52
87
  } catch (error) {
53
- console.error(color.red('\n✖ Terraform destroy failed. Please check the output above.'));
88
+ s.stop(color.red(' Terraform destroy failed.'));
89
+ console.error(color.red(error.message));
54
90
  process.exit(1);
55
91
  }
56
92
 
57
93
  // 3. Clean up the S3 State Bucket
58
94
  if (bucketName) {
59
- s.start(`Emptying and deleting S3 state bucket: ${bucketName}...`);
60
- try {
61
- await teardownStateBucket(region, bucketName);
62
- s.stop(`S3 bucket ${bucketName} successfully deleted.`);
63
- } catch (error) {
64
- s.stop(`❌ Failed to delete S3 bucket. You may need to delete it manually in the AWS Console.`);
65
- console.error(color.red(`AWS Error: ${error.message}`));
95
+ const deleteS3Bucket = await confirm({
96
+ message: color.yellow(`AWS compute resources destroyed. Do you also want to permanently delete the S3 state bucket?\n (Select 'No' if you plan to run 'deploy-stack apply' later to spin this back up.)`),
97
+ initialValue: false,
98
+ });
99
+
100
+ if (deleteS3Bucket && typeof deleteS3Bucket !== 'symbol') {
101
+ s.start(`Emptying and deleting S3 state bucket: ${bucketName}...`);
102
+ try {
103
+ await teardownStateBucket(region, bucketName);
104
+ s.stop(`S3 bucket ${bucketName} successfully deleted.`);
105
+ } catch (error) {
106
+ s.stop(`❌ Failed to delete S3 bucket. You may need to delete it manually in the AWS Console.`);
107
+ console.error(color.red(`AWS Error: ${error.message}`));
108
+ }
109
+ } else {
110
+ console.log(color.cyan(`\n S3 bucket retained. You can run 'npx deploy-stack apply' anytime to restore your infrastructure.`));
66
111
  }
67
112
  }
68
113
 
@@ -214,6 +214,26 @@ export async function mainStack() {
214
214
  const deployBranch = project.branch || currentGitBranch;
215
215
  const buildDir = detectedFramework?.buildDir || 'dist';
216
216
 
217
+ // 7.4 Check for conflicting CI boilerplate (Rails)
218
+ let disableDefaultCI = false;
219
+ if (finalFramework === 'rails') {
220
+ const ciPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
221
+ const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
222
+
223
+ if (fsSync.existsSync(ciPath) || fsSync.existsSync(dependabotPath)) {
224
+ console.log('');
225
+ const ciContent = await confirm({
226
+ message: color.yellow('We detected default Rails GitHub Actions (ci.yml, dependabot.yml) that usually crash in isolated CI environments without a database. Would you like deploy-stack to safely disable them by renaming them to .bak?'),
227
+ initialValue: true,
228
+ });
229
+ if (typeof ciContent === 'symbol') {
230
+ cancel('Provisioning cancelled.');
231
+ process.exit(0);
232
+ }
233
+ disableDefaultCI = ciContent;
234
+ }
235
+ }
236
+
217
237
  // 7.5. The Pre-Flight Cost Estimator
218
238
  // We explicitly ask for financial consent to eliminate AWS billing anxiety.
219
239
  console.log(''); // Add a blank line for visual pacing
@@ -266,7 +286,8 @@ export async function mainStack() {
266
286
  BUILD_DIR: buildDir,
267
287
  finalFramework: finalFramework,
268
288
  NEEDS_DATABASE: needsDatabase,
269
- DJANGO_WSGI: djangoWsgi
289
+ DJANGO_WSGI: djangoWsgi,
290
+ DISABLE_DEFAULT_CI: disableDefaultCI
270
291
  });
271
292
 
272
293
  // 11. Track the event in telemetry
@@ -292,36 +313,20 @@ export async function mainStack() {
292
313
  const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
293
314
 
294
315
  const cdStep = projectName === '.' ? '' : `1. cd ${projectName}\n `;
295
- const deployStepNum = projectName === '.' ? '1' : '2';
296
- const gitStepNum = projectName === '.' ? '2' : '3';
316
+ const applyStep = `${cdStep}npx deploy-stack apply`;
297
317
 
298
318
  const gitInstructions = isGitInitialized
299
- ? `git add .\n git commit -m "chore: add AWS infrastructure and CI/CD"\n git push`
300
- : `git init\n git add .\n git commit -m "chore: add AWS infrastructure and CI/CD"\n git branch -M ${deployBranch}\n git remote add origin https://github.com/your-username/your-repo.git\n git push -u origin ${deployBranch}`;
301
-
302
- outro(`
303
- ${color.green('✅ Project provisioned successfully!')}
304
-
305
- ${color.blue('🛡️ DevSecOps Enabled:')}
306
- Automated Trivy vulnerability scanning for your Docker container
307
- and Terraform has been added to your CI/CD pipeline.
308
- Check the 'Summary' page of your GitHub Actions runs for reports.
309
-
310
- ${frameworkWarnings}
311
-
312
- Next steps:
313
- ${cdStep}${deployStepNum}. Deploy infrastructure:
314
- cd terraform && terraform init && terraform apply
315
-
316
- ${gitStepNum}. Push to GitHub to trigger CI/CD:
317
- cd ..
318
- ${gitInstructions}
319
+ ? `git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git push`
320
+ : `git init && git add . && git commit -m "chore: add AWS infrastructure and CI/CD" && git branch -M ${deployBranch} && git remote add origin https://github.com/your-username/your-repo.git && git push -u origin ${deployBranch}`;
319
321
 
320
- ${color.cyan('Once deployed, Terraform will output your new https://*.cloudfront.net URL.')}
322
+ const outroMessage = `${color.green(' Templates generated!')} ${color.blue('🛡️ DevSecOps scanning enabled.')}
323
+ ${frameworkWarnings ? `\n ${frameworkWarnings}` : ''}
324
+ ${color.yellow('Next steps:')}
325
+ 1. ${color.cyan(applyStep)}
326
+ 2. ${color.cyan(gitInstructions)}
327
+ ${color.magenta('🚀 Need help?')} ${color.underline('https://calendly.com/anton-codes-iac/15min')}`;
321
328
 
322
- ${color.magenta('🚀 Infrastructure ready! Need help or have feedback? Grab 15 mins with Anton:')}
323
- ${color.underline('https://calendly.com/anton-codes-iac/15min')}
324
- `);
329
+ outro(outroMessage);
325
330
 
326
331
  // 13.Ensure all analytics are sent before the CLI terminates
327
332
  await flushTelemetry();
@@ -152,13 +152,17 @@ Thumbs.db
152
152
  const railsCiPath = path.join(targetDir, '.github', 'workflows', 'ci.yml');
153
153
  const dependabotPath = path.join(targetDir, '.github', 'dependabot.yml');
154
154
 
155
- if (fsSync.existsSync(railsCiPath)) {
156
- fsSync.renameSync(railsCiPath, `${railsCiPath}.bak`);
157
- console.log(' ⚠️ Disabled default Rails ci.yml (renamed to .bak) to prevent database connection crashes in CI.');
158
- }
159
- if (fsSync.existsSync(dependabotPath)) {
160
- fsSync.renameSync(dependabotPath, `${dependabotPath}.bak`);
161
- console.log(' ⚠️ Disabled default dependabot.yml (renamed to .bak) to prevent automated PRs from triggering the broken CI suite.');
155
+ if (config.DISABLE_DEFAULT_CI) {
156
+ if (fsSync.existsSync(railsCiPath)) {
157
+ fsSync.renameSync(railsCiPath, `${railsCiPath}.bak`);
158
+ console.log(' ✅ Safely disabled default Rails ci.yml (renamed to .bak).');
159
+ }
160
+ if (fsSync.existsSync(dependabotPath)) {
161
+ fsSync.renameSync(dependabotPath, `${dependabotPath}.bak`);
162
+ console.log(' ✅ Safely disabled default dependabot.yml (renamed to .bak).');
163
+ }
164
+ } else if (fsSync.existsSync(railsCiPath) || fsSync.existsSync(dependabotPath)) {
165
+ console.log(' ⚠️ You chose to keep your existing GitHub Actions. Please note that default tests may fail until you configure a database service in your CI runner.');
162
166
  }
163
167
  }
164
168
  }
@@ -67,6 +67,10 @@ Make sure your app returns a `200 OK` at your configured path:
67
67
  * **Next.js (App Router):** Create `app/api/health/route.ts` returning a 200 response.
68
68
  * **Express.js:** Add `app.get('/api/health', (req, res) => res.sendStatus(200));`
69
69
  * **FastAPI/Python:** Add `@app.get("/api/health")` returning a 200 status.
70
+ * **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.
71
+ * **Django:** Add a simple view in `urls.py` that returns `HttpResponse("OK", status=200)` at your configured path.
72
+ * **Go:** Add a handler to your mux: `http.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })`
73
+ * **Nuxt 3:** Create a server route at `server/routes/health.ts` returning `200`.
70
74
 
71
75
  ### 2. Enable Standalone Output (Next.js ONLY)
72
76
 
@@ -91,6 +95,10 @@ When running inside a Docker container, your server must bind to all network int
91
95
  Make sure your app is configured correctly:
92
96
  * **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
93
97
  * **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
98
+ * **Ruby on Rails:** Bound automatically by the CLI's Puma command (`-b tcp://0.0.0.0:{{PORT}}`).
99
+ * **Django:** Bound automatically by the CLI's Gunicorn command (`--bind 0.0.0.0:{{PORT}}`).
100
+ * **Go:** Ensure your `ListenAndServe` string looks like this: `http.ListenAndServe(":8080", nil)` or `http.ListenAndServe("0.0.0.0:8080", nil)`.
101
+ * **Nuxt 3:** Bound automatically via the `NITRO_HOST=0.0.0.0` environment variable injected by the CLI Dockerfile.
94
102
 
95
103
  ### 4. Static Sites (Vite, Astro, React, Vue, SvelteKit)
96
104