deploy-stack 0.1.0 → 0.2.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/publish.yml +12 -7
- package/README.md +12 -10
- package/package.json +22 -1
- package/src/commands/init.js +62 -21
- package/src/core/telemetry.js +1 -1
- package/src/utils/detector.js +35 -0
|
@@ -7,22 +7,27 @@ on:
|
|
|
7
7
|
jobs:
|
|
8
8
|
build-and-publish:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
10
14
|
steps:
|
|
11
15
|
- uses: actions/checkout@v4
|
|
12
16
|
|
|
13
17
|
- name: Setup Node.js
|
|
14
18
|
uses: actions/setup-node@v4
|
|
15
19
|
with:
|
|
16
|
-
node-version: '
|
|
20
|
+
node-version: '24.x'
|
|
17
21
|
registry-url: 'https://registry.npmjs.org'
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
- name: Upgrade NPM for OIDC
|
|
24
|
+
run: npm install -g npm@latest
|
|
25
|
+
|
|
19
26
|
- name: Inject Telemetry Key
|
|
20
|
-
# This securely swaps the placeholder for the real key right before publishing
|
|
21
27
|
run: sed -i "s/___POSTHOG_INGESTION_KEY___/${{ secrets.POSTHOG_KEY }}/g" src/core/telemetry.js
|
|
22
28
|
|
|
23
|
-
-
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: npm install
|
|
24
31
|
|
|
25
32
|
- name: Publish to NPM
|
|
26
|
-
run: npm publish
|
|
27
|
-
env:
|
|
28
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
33
|
+
run: npm publish --provenance
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## The Problem
|
|
@@ -43,10 +45,9 @@ npx deploy-stack
|
|
|
43
45
|
|
|
44
46
|
The interactive CLI will guide you through the setup:
|
|
45
47
|
1. **Target Directory / Name:** (Type `.` to bootstrap your current directory)
|
|
46
|
-
2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced**
|
|
47
|
-
3. **
|
|
48
|
-
4. **Compute Tier:** (
|
|
49
|
-
5. **Target Port & Framework:** (Generates an optimized multi-stage `Dockerfile`)
|
|
48
|
+
2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced** for custom scaling and branch names)
|
|
49
|
+
3. **Zero-Config Detection:** The CLI automatically scans your `package.json` or `requirements.txt` to detect your framework (Next.js, Express, FastAPI, etc.) and dynamically configures your container port.
|
|
50
|
+
4. **AWS Region & Compute Tier:** (Select your target region and Fargate size with live cost estimates)
|
|
50
51
|
|
|
51
52
|
---
|
|
52
53
|
|
|
@@ -107,7 +108,7 @@ your-project/
|
|
|
107
108
|
|
|
108
109
|
---
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
## 🛡️ Telemetry & Privacy
|
|
111
112
|
By default, `deploy-stack` collects anonymous, hashed usage data to help improve the CLI (e.g., framework presets used, deployment success rates). **No codebase files, AWS credentials, or personal data are ever collected.**
|
|
112
113
|
|
|
113
114
|
To opt out, simply append the flag:
|
|
@@ -119,11 +120,12 @@ npx deploy-stack --no-telemetry
|
|
|
119
120
|
|
|
120
121
|
## 🗺️ Roadmap
|
|
121
122
|
|
|
122
|
-
- [x] **
|
|
123
|
-
- [x] **
|
|
124
|
-
- [
|
|
125
|
-
- [ ] **
|
|
126
|
-
- [ ] **
|
|
123
|
+
- [x] **Core MVP:** Interactive CLI, ECS Fargate + ALB Terraform generation, CI/CD, and Secrets sync.
|
|
124
|
+
- [x] **Production Readiness:** CloudFront distribution, S3 State locking, and OIDC security.
|
|
125
|
+
- [x] **Smart Experience:** Zero-config framework auto-discovery for Next.js, Express, and FastAPI.
|
|
126
|
+
- [ ] **State & Storage:** Automated RDS PostgreSQL provisioning and persistent volumes.
|
|
127
|
+
- [ ] **Advanced Networking:** Custom domains (Route 53), ACM SSL configuration, and environment workspaces (staging/prod).
|
|
128
|
+
- [ ] **Security Hardening:** Private VPC subnets with NAT gateways, and AWS WAF integration.
|
|
127
129
|
|
|
128
130
|
---
|
|
129
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deploy-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,27 @@
|
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git+https://github.com/anton-codes-iac/deploy-stack.git"
|
|
16
16
|
},
|
|
17
|
+
"homepage": "https://github.com/anton-codes-iac/deploy-stack#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/anton-codes-iac/deploy-stack/issues"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"aws",
|
|
23
|
+
"terraform",
|
|
24
|
+
"github-actions",
|
|
25
|
+
"ci-cd",
|
|
26
|
+
"ecs",
|
|
27
|
+
"fargate",
|
|
28
|
+
"nextjs-deployment",
|
|
29
|
+
"infrastructure-as-code",
|
|
30
|
+
"oidc",
|
|
31
|
+
"deploy-stack",
|
|
32
|
+
"expressjs",
|
|
33
|
+
"fastapi",
|
|
34
|
+
"nextjs",
|
|
35
|
+
"static-site",
|
|
36
|
+
"nginx"
|
|
37
|
+
],
|
|
17
38
|
"license": "MIT",
|
|
18
39
|
"dependencies": {
|
|
19
40
|
"@aws-sdk/client-s3": "3.1115.0",
|
package/src/commands/init.js
CHANGED
|
@@ -4,10 +4,11 @@ import fsSync from 'fs';
|
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { intro, outro, group, text, select, spinner, cancel, confirm } from '@clack/prompts';
|
|
7
|
+
import { intro, outro, group, text, select, spinner, cancel, confirm, log } from '@clack/prompts';
|
|
8
8
|
import color from 'picocolors';
|
|
9
9
|
|
|
10
10
|
import { checkDependency } from '../utils/system.js';
|
|
11
|
+
import { detectFramework } from '../utils/detector.js';
|
|
11
12
|
import { trackEvent } from '../core/telemetry.js';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -41,18 +42,58 @@ export async function mainStack() {
|
|
|
41
42
|
process.exit(0);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
const projectName = await text({
|
|
46
|
+
message: 'What is the name of your project? (Type "." to use current directory)',
|
|
47
|
+
placeholder: 'my-web-app',
|
|
48
|
+
validate: (value) => {
|
|
49
|
+
if (!value) return 'Please enter a name.';
|
|
50
|
+
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (typeof projectName === 'symbol') {
|
|
55
|
+
cancel('Operation cancelled.');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const actualProjectName = projectName === '.' ? path.basename(process.cwd()) : projectName;
|
|
60
|
+
const targetDir = projectName === '.' ? process.cwd() : path.join(process.cwd(), projectName);
|
|
61
|
+
|
|
62
|
+
// 2.5 Run the scanner
|
|
63
|
+
const detectedFramework = detectFramework(targetDir);
|
|
64
|
+
if (detectedFramework) {
|
|
65
|
+
log.success(`Auto-detected framework: ${detectedFramework.name}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2.6 Resolve the framework
|
|
69
|
+
let finalFramework = detectedFramework ? detectedFramework.id : null;
|
|
70
|
+
|
|
71
|
+
if (!finalFramework) {
|
|
72
|
+
finalFramework = await select({
|
|
73
|
+
message: 'Which framework preset should we configure?',
|
|
74
|
+
options: [
|
|
75
|
+
{ value: 'node', label: 'Node.js / Express' },
|
|
76
|
+
{ value: 'nextjs', label: 'Next.js (Standalone)' },
|
|
77
|
+
{ value: 'python', label: 'Python FastAPI' },
|
|
78
|
+
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (typeof finalFramework === 'symbol') {
|
|
83
|
+
cancel('Operation cancelled.');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2.7 Set intelligent defaults
|
|
89
|
+
let defaultPort = '3000';
|
|
90
|
+
|
|
91
|
+
if (finalFramework === 'static') defaultPort = '80';
|
|
92
|
+
if (finalFramework === 'python') defaultPort = '8000';
|
|
93
|
+
|
|
44
94
|
// 3. Prompt Configuration Group
|
|
45
95
|
const project = await group(
|
|
46
96
|
{
|
|
47
|
-
name: () =>
|
|
48
|
-
text({
|
|
49
|
-
message: 'What is the name of your project? (Type "." to use current directory)',
|
|
50
|
-
placeholder: 'my-web-app',
|
|
51
|
-
validate: (value) => {
|
|
52
|
-
if (!value) return 'Please enter a name.';
|
|
53
|
-
if (value !== '.' && value.includes(' ')) return 'Name cannot contain spaces.';
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
56
97
|
region: () =>
|
|
57
98
|
select({
|
|
58
99
|
message: 'Which AWS region do you want to deploy to?',
|
|
@@ -67,11 +108,13 @@ export async function mainStack() {
|
|
|
67
108
|
port: () =>
|
|
68
109
|
text({
|
|
69
110
|
message: 'What port does your container expose?',
|
|
70
|
-
placeholder:
|
|
71
|
-
defaultValue:
|
|
111
|
+
placeholder: defaultPort,
|
|
112
|
+
defaultValue: defaultPort,
|
|
72
113
|
}),
|
|
73
|
-
framework: () =>
|
|
74
|
-
|
|
114
|
+
framework: () => {
|
|
115
|
+
if (detectedFramework) return undefined;
|
|
116
|
+
|
|
117
|
+
return select({
|
|
75
118
|
message: 'Which framework preset should we configure?',
|
|
76
119
|
options: [
|
|
77
120
|
{ value: 'node', label: 'Node.js / Express' },
|
|
@@ -79,7 +122,8 @@ export async function mainStack() {
|
|
|
79
122
|
{ value: 'python', label: 'Python FastAPI' },
|
|
80
123
|
{ value: 'static', label: 'Static Site (Gatsby, React, plain HTML via Nginx)' },
|
|
81
124
|
],
|
|
82
|
-
})
|
|
125
|
+
});
|
|
126
|
+
},
|
|
83
127
|
size: () =>
|
|
84
128
|
select({
|
|
85
129
|
message: 'Select your Fargate compute size:',
|
|
@@ -126,9 +170,6 @@ export async function mainStack() {
|
|
|
126
170
|
);
|
|
127
171
|
|
|
128
172
|
// 4. Map the user's choices and update the variables
|
|
129
|
-
const actualProjectName = project.name === '.' ? path.basename(process.cwd()) : project.name;
|
|
130
|
-
const targetDir = project.name === '.' ? process.cwd() : path.join(process.cwd(), project.name);
|
|
131
|
-
|
|
132
173
|
const cpu = project.size === 'small' ? '512' : '256';
|
|
133
174
|
const memory = project.size === 'small' ? '1024' : '512';
|
|
134
175
|
|
|
@@ -212,7 +253,7 @@ export async function mainStack() {
|
|
|
212
253
|
const tfOidcPath = path.join(__dirname, '../../templates', 'terraform', 'oidc.tf');
|
|
213
254
|
const tfBackendPath = path.join(__dirname, '../../templates', 'terraform', 'backend.tf');
|
|
214
255
|
const tfCloudfrontPath = path.join(__dirname, '../../templates', 'terraform', 'cloudfront.tf');
|
|
215
|
-
const dockerTemplatePath = path.join(__dirname, '../../templates', 'docker', `${
|
|
256
|
+
const dockerTemplatePath = path.join(__dirname, '../../templates', 'docker', `${finalFramework}.Dockerfile`);
|
|
216
257
|
const githubActionPath = path.join(__dirname, '../../templates', 'github', 'deploy.yml');
|
|
217
258
|
const readmePath = path.join(__dirname, '../../templates', 'README.md');
|
|
218
259
|
const gitignorePath = path.join(__dirname, '../../templates', '_gitignore');
|
|
@@ -293,7 +334,7 @@ export async function mainStack() {
|
|
|
293
334
|
// 11. Track the event in telemetry
|
|
294
335
|
trackEvent('project_provisioned', {
|
|
295
336
|
projectName: actualProjectName,
|
|
296
|
-
framework:
|
|
337
|
+
framework: finalFramework,
|
|
297
338
|
region: project.region,
|
|
298
339
|
size: project.size,
|
|
299
340
|
setup_mode: setupType,
|
|
@@ -306,7 +347,7 @@ export async function mainStack() {
|
|
|
306
347
|
// 12. Provide the Outro, Framework Warnings and Next Steps
|
|
307
348
|
let frameworkWarnings = '';
|
|
308
349
|
|
|
309
|
-
if (
|
|
350
|
+
if (finalFramework === 'nextjs') {
|
|
310
351
|
frameworkWarnings =
|
|
311
352
|
color.bgYellow(color.black(' ⚠️ IMPORTANT: NEXT.JS SETUP REQUIRED ')) +
|
|
312
353
|
color.yellow('\n You must modify your next.config file and create a health check route before deploying.') +
|
package/src/core/telemetry.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
|
|
3
3
|
const TELEMETRY_ENDPOINT = 'https://app.posthog.com/capture';
|
|
4
4
|
|
|
5
|
-
const POSTHOG_API_KEY = '
|
|
5
|
+
const POSTHOG_API_KEY = 'phc_o2wgA3jVT9rVDiGSDzFAR42zZeiVGhhCY53HXVHUcYGT';
|
|
6
6
|
|
|
7
7
|
export function trackEvent(eventName, properties) {
|
|
8
8
|
// 1. Respect privacy standards
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function detectFramework(targetDir) {
|
|
5
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
6
|
+
const requirementsTxtPath = path.join(targetDir, 'requirements.txt');
|
|
7
|
+
|
|
8
|
+
// 1. Detect Node.js Frameworks
|
|
9
|
+
if (fsSync.existsSync(packageJsonPath)) {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
// Merge dependencies and devDependencies to check both
|
|
13
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
14
|
+
|
|
15
|
+
if (deps['next']) return { id: 'nextjs', name: 'Next.js' };
|
|
16
|
+
if (deps['gatsby'] || deps['react-scripts'] || deps['vue']) return { id: 'static', name: 'Static Site' };
|
|
17
|
+
if (deps['express']) return { id: 'node', name: 'Node.js / Express' };
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Silently fail if package.json is malformed
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. Detect Python Frameworks
|
|
24
|
+
if (fsSync.existsSync(requirementsTxtPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const reqs = fsSync.readFileSync(requirementsTxtPath, 'utf-8').toLowerCase();
|
|
27
|
+
if (reqs.includes('fastapi')) return { id: 'python', name: 'Python FastAPI' };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// Silently fail
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Fallback
|
|
34
|
+
return null;
|
|
35
|
+
}
|