deploy-stack 0.4.0 → 0.5.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
@@ -33,6 +33,8 @@ You retain complete ownership of your infrastructure code without relying on bla
33
33
  * **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern native S3 concurrency locking.
34
34
  * **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.
35
35
  * **Non-Destructive:** Safely analyzes existing directories and prompts for confirmation before updating any files.
36
+ * **Zero-Config Detection:** Automatically resolves output directories for Vite, Astro, SvelteKit, CRA, and more.
37
+ * **Safe Teardown:** Completely remove all generated AWS resources and empty S3 state buckets with a single `destroy` command.
36
38
 
37
39
  ---
38
40
 
@@ -81,6 +83,12 @@ npx deploy-stack secrets push .env.production
81
83
 
82
84
  ---
83
85
 
86
+ ## 🗑️ Infrastructure Teardown
87
+ To safely completely remove your ECS cluster, load balancers, and empty the remote S3 state bucket, run:
88
+ `npx deploy-stack destroy`
89
+
90
+ ---
91
+
84
92
  ## 📁 Generated File Structure
85
93
 
86
94
  Running the CLI generates a modular architecture tailored to your service:
package/bin/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'path';
3
+ import { mainStack } from '../src/commands/init.js';
4
+ import { destroyStack } from '../src/commands/destroy.js';
3
5
  import { runDoctor } from '../src/commands/doctor.js';
4
6
  import { pushSecrets } from '../src/commands/secrets.js';
5
- import { mainStack } from '../src/commands/init.js';
6
7
 
7
8
  // 1. Extract the telemetry flag and set the environment variable
8
9
  const rawArgs = process.argv.slice(2);
@@ -22,6 +23,8 @@ if (args[0] === 'secrets' && args[1] === 'push') {
22
23
  pushSecrets(envFile, projectName).catch(console.error);
23
24
  } else if (args[0] === 'doctor') {
24
25
  runDoctor().catch(console.error);
26
+ } else if (args[0] === 'destroy') {
27
+ destroyStack().catch(console.error);
25
28
  } else {
26
29
  mainStack().catch(console.error);
27
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,77 @@
1
+ import fsSync from 'fs';
2
+ import path from 'path';
3
+ import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
4
+ import color from 'picocolors';
5
+ import { execSync } from 'child_process';
6
+ import { teardownStateBucket } from '../utils/aws.js';
7
+ import { checkDependency } from '../utils/system.js';
8
+ import { trackEvent, flushTelemetry } from '../core/telemetry.js';
9
+
10
+ export async function destroyStack() {
11
+ intro(color.bgRed(color.white(' deploy-stack destroy 🗑️ ')));
12
+
13
+ const tfDirPath = path.join(process.cwd(), 'terraform');
14
+ const backendFilePath = path.join(tfDirPath, 'backend.tf');
15
+
16
+ if (!fsSync.existsSync(backendFilePath)) {
17
+ console.error(color.red('✖ No terraform/backend.tf found in the current directory.'));
18
+ console.log(color.yellow('Are you in the root of a deploy-stack project?'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const hasTerraform = await checkDependency('terraform');
23
+ if (!hasTerraform) {
24
+ console.error(color.red('✖ Terraform is not installed.'));
25
+ process.exit(1);
26
+ }
27
+
28
+ const proceed = await confirm({
29
+ message: color.red('⚠️ WARNING: This will permanently destroy all AWS resources associated with this project. Are you absolutely sure?'),
30
+ initialValue: false,
31
+ });
32
+
33
+ if (!proceed) {
34
+ cancel('Destruction cancelled. Your infrastructure is safe.');
35
+ process.exit(0);
36
+ }
37
+
38
+ const s = spinner();
39
+
40
+ // 1. Extract Bucket and Region from backend.tf
41
+ const backendContent = fsSync.readFileSync(backendFilePath, 'utf-8');
42
+ const bucketMatch = backendContent.match(/bucket\s*=\s*"([^"]+)"/);
43
+ const regionMatch = backendContent.match(/region\s*=\s*"([^"]+)"/);
44
+
45
+ const bucketName = bucketMatch ? bucketMatch[1] : null;
46
+ const region = regionMatch ? regionMatch[1] : 'us-east-1';
47
+
48
+ // 2. Execute Terraform Destroy
49
+ console.log(color.cyan('\nInitiating Terraform destroy (this may take a few minutes)...\n'));
50
+ try {
51
+ execSync('terraform destroy -auto-approve', { cwd: tfDirPath, stdio: 'inherit' });
52
+ } catch (error) {
53
+ console.error(color.red('\n✖ Terraform destroy failed. Please check the output above.'));
54
+ process.exit(1);
55
+ }
56
+
57
+ // 3. Clean up the S3 State Bucket
58
+ 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}`));
66
+ }
67
+ }
68
+
69
+ trackEvent('project_destroyed', {
70
+ region,
71
+ bucket: bucketName,
72
+ success: true
73
+ });
74
+ await flushTelemetry();
75
+
76
+ outro(color.green('✅ Infrastructure successfully destroyed. Your AWS bill is safe.'));
77
+ }
@@ -232,6 +232,7 @@ export async function mainStack() {
232
232
  trackEvent('project_provisioned', {
233
233
  projectName: actualProjectName,
234
234
  framework: finalFramework,
235
+ specific_framework: detectedFramework?.name || finalFramework,
235
236
  region: project.region,
236
237
  size: project.size,
237
238
  setup_mode: setupType,
package/src/utils/aws.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
2
- import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
2
+ import { S3Client, CreateBucketCommand, PutBucketVersioningCommand, PutBucketTaggingCommand } from '@aws-sdk/client-s3';
3
+ import { DeleteBucketCommand, ListObjectVersionsCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
3
4
 
4
5
  export async function provisionStateBucket(region, projectName) {
5
6
  const stsClient = new STSClient({ region });
@@ -23,6 +24,15 @@ export async function provisionStateBucket(region, projectName) {
23
24
  CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
24
25
  }));
25
26
 
27
+ await s3Client.send(new PutBucketTaggingCommand({
28
+ Bucket: stateBucketName,
29
+ Tagging: {
30
+ TagSet: [
31
+ { Key: "ManagedBy", Value: "deploy-stack" }
32
+ ]
33
+ }
34
+ }));
35
+
26
36
  await s3Client.send(new PutBucketVersioningCommand({
27
37
  Bucket: stateBucketName,
28
38
  VersioningConfiguration: { Status: 'Enabled' }
@@ -35,4 +45,36 @@ export async function provisionStateBucket(region, projectName) {
35
45
  }
36
46
 
37
47
  return { awsAccountId, stateBucketName };
48
+ }
49
+
50
+ export async function teardownStateBucket(region, bucketName) {
51
+ const client = new S3Client({ region });
52
+
53
+ try {
54
+ // 1. Fetch all object versions and delete markers
55
+ const listCommand = new ListObjectVersionsCommand({ Bucket: bucketName });
56
+ const { Versions, DeleteMarkers } = await client.send(listCommand);
57
+
58
+ const objectsToDelete = [];
59
+ if (Versions) objectsToDelete.push(...Versions.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
60
+ if (DeleteMarkers) objectsToDelete.push(...DeleteMarkers.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
61
+
62
+ // 2. Delete all contents if any exist
63
+ if (objectsToDelete.length > 0) {
64
+ const deleteCommand = new DeleteObjectsCommand({
65
+ Bucket: bucketName,
66
+ Delete: { Objects: objectsToDelete }
67
+ });
68
+ await client.send(deleteCommand);
69
+ }
70
+
71
+ // 3. Delete the now-empty bucket
72
+ const deleteBucketCommand = new DeleteBucketCommand({ Bucket: bucketName });
73
+ await client.send(deleteBucketCommand);
74
+
75
+ return true;
76
+ } catch (error) {
77
+ if (error.name === 'NoSuchBucket') return true; // Already deleted
78
+ throw error;
79
+ }
38
80
  }
@@ -29,7 +29,7 @@ This infrastructure provisions a highly available Application Load Balancer (ALB
29
29
  ```
30
30
 
31
31
  3. **Automated CI/CD (Keyless via OIDC):**
32
- Push this repository to GitHub. Your deployment pipeline uses AWS IAM OpenID Connect (OIDC) to authenticate securely with temporary credentials—**no long-lived AWS secret keys are required in GitHub Secrets**. Every push to `main` will automatically build, package, and deploy your application.
32
+ Push this repository to GitHub. Your deployment pipeline uses AWS IAM OpenID Connect (OIDC) to authenticate securely with temporary credentials—**no long-lived AWS secret keys are required in GitHub Secrets**. Every push to `{{DEPLOY_BRANCH}}` will automatically build, package, and deploy your application.
33
33
 
34
34
  ### ⚠️ Troubleshooting: OIDC Provider Already Exists
35
35
  AWS only permits one GitHub Actions OIDC provider per AWS account. If `terraform apply` fails with an `EntityAlreadyExists` error regarding the OIDC provider, it indicates GitHub Actions was previously configured in this account.
@@ -48,13 +48,11 @@ Re-run `terraform apply` to link directly to your existing provider.
48
48
 
49
49
  If you are done testing and want to stop all AWS billing, you must destroy the infrastructure.
50
50
 
51
- Because our Terraform configuration is set to force-delete the ECR image repository (even if images are present), teardown is a single, clean command:
52
-
51
+ Run the automated teardown command from the root of your project:
53
52
  ```bash
54
- cd terraform
55
- terraform destroy
53
+ npx deploy-stack destroy
56
54
  ```
57
- *Type `yes` when prompted. This will permanently delete the Load Balancer, ECS cluster, log groups, and associated networking components.*
55
+ *Type `yes` when prompted. This will execute a safe Terraform teardown of your Load Balancer, ECS cluster, and networking components, followed by automatically emptying and deleting your remote S3 state bucket.*
58
56
 
59
57
  ## ⚠️ Critical Application Prerequisites
60
58
 
@@ -94,8 +92,8 @@ Make sure your app is configured correctly:
94
92
  * **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
95
93
  * **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
96
94
 
97
- ### 4. Static Sites (Vite, Astro, React, Vue)
95
+ ### 4. Static Sites (Vite, Astro, React, Vue, SvelteKit)
98
96
 
99
97
  If you are deploying a static site, your application is served via a highly optimized, unprivileged Nginx container.
100
- 1. **Build Folder:** Different frameworks output compiled assets to different folders. Open your `Dockerfile` and ensure the `COPY --from=builder` command points to the correct folder (`dist`, `build`, or `out`).
101
- 2. **Health Checks:** You do not need to configure a custom `/health` route. Nginx will automatically return a `200 OK` when AWS pings the root `/` index page.
98
+ * **Zero-Config Build:** The CLI automatically detected your framework's output folder (`dist`, `build`, etc.) and pre-configured your Dockerfile.
99
+ * **Health Checks:** You do not need to configure a custom `/health` route. Nginx will automatically return a `200 OK` when AWS pings the root `/` index page.