rastack 0.0.33 → 0.0.35
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/CHANGELOG.md +4 -0
- package/dist/deploy/ci.d.ts +53 -0
- package/dist/deploy/ci.js +125 -0
- package/dist/deploy/config.d.ts +144 -0
- package/dist/deploy/config.js +237 -0
- package/dist/deploy/index.d.ts +9 -0
- package/dist/deploy/index.js +25 -0
- package/dist/deploy/io.d.ts +27 -0
- package/dist/deploy/io.js +121 -0
- package/dist/deploy/workflows.d.ts +46 -0
- package/dist/deploy/workflows.js +145 -0
- package/dist/rastack-ci.d.ts +18 -0
- package/dist/rastack-ci.js +118 -0
- package/dist/rastack-deploy.d.ts +20 -0
- package/dist/rastack-deploy.js +196 -0
- package/dist/rastack-init.d.ts +14 -0
- package/dist/rastack-init.js +183 -0
- package/dist/rastack.d.ts +3 -0
- package/dist/rastack.js +50 -9
- package/package.json +1 -1
- package/src/deploy/ci.ts +177 -0
- package/src/deploy/config.ts +329 -0
- package/src/deploy/index.ts +10 -0
- package/src/deploy/io.ts +90 -0
- package/src/deploy/workflows.ts +163 -0
- package/src/rastack-ci.ts +131 -0
- package/src/rastack-deploy.ts +257 -0
- package/src/rastack-init.ts +187 -0
- package/src/rastack.ts +51 -9
- package/test/deploy.spec.ts +355 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rastack ci` — run the CI pipeline for a phase, so GitHub Actions stays a
|
|
5
|
+
* one-liner.
|
|
6
|
+
*
|
|
7
|
+
* rastack ci <check|build|deploy> [--plan] [--skip-build]
|
|
8
|
+
* [--function <name>] [--jwks-url <url>] [--bin <cmd>]
|
|
9
|
+
*
|
|
10
|
+
* check validate the resource graph + compile the manifest (a CI gate)
|
|
11
|
+
* build cross-build the Lambda bundle and bake the Cognito JWKS in
|
|
12
|
+
* deploy build (unless --skip-build) then `update-function-code`
|
|
13
|
+
*
|
|
14
|
+
* The steps are a pure function of `.rastack/deploy.json` (see
|
|
15
|
+
* tools/src/deploy/ci.ts); `--plan` prints them without running anything.
|
|
16
|
+
* Function name / JWKS URL come from the config's stack outputs when wired, else
|
|
17
|
+
* are resolved live from CloudFormation at run time.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execSync } from "child_process";
|
|
21
|
+
import { CiPhase, CiPlanOptions, CiStep, ciPlan } from "./deploy/ci";
|
|
22
|
+
import { loadConfig } from "./deploy/io";
|
|
23
|
+
|
|
24
|
+
const PHASES: CiPhase[] = ["check", "build", "deploy"];
|
|
25
|
+
|
|
26
|
+
interface Flags {
|
|
27
|
+
phase: CiPhase;
|
|
28
|
+
plan: boolean;
|
|
29
|
+
opts: CiPlanOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseFlags(argv: string[]): Flags {
|
|
33
|
+
let phase: CiPhase | "" = "";
|
|
34
|
+
let plan = false;
|
|
35
|
+
const opts: CiPlanOptions = {
|
|
36
|
+
bin: process.env.RASTACK_BIN || "rastack",
|
|
37
|
+
functionName: process.env.RASTACK_FUNCTION || undefined,
|
|
38
|
+
jwksUrl: process.env.RASTACK_JWKS_URL || undefined,
|
|
39
|
+
};
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i];
|
|
42
|
+
const next = () => argv[++i] ?? "";
|
|
43
|
+
switch (a) {
|
|
44
|
+
case "--plan":
|
|
45
|
+
case "--dry-run":
|
|
46
|
+
plan = true;
|
|
47
|
+
break;
|
|
48
|
+
case "--skip-build":
|
|
49
|
+
opts.skipBuild = true;
|
|
50
|
+
break;
|
|
51
|
+
case "--function":
|
|
52
|
+
opts.functionName = next();
|
|
53
|
+
break;
|
|
54
|
+
case "--jwks-url":
|
|
55
|
+
opts.jwksUrl = next();
|
|
56
|
+
break;
|
|
57
|
+
case "--bin":
|
|
58
|
+
opts.bin = next();
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
if (a.startsWith("-")) {
|
|
62
|
+
console.error(`Unknown flag: ${a}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
} else if (!phase && PHASES.includes(a as CiPhase)) {
|
|
65
|
+
phase = a as CiPhase;
|
|
66
|
+
} else if (!phase) {
|
|
67
|
+
console.error(
|
|
68
|
+
`Unknown ci phase: "${a}" (expected ${PHASES.join(" | ")}).`,
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!phase) {
|
|
75
|
+
console.error(
|
|
76
|
+
`Usage: rastack ci <${PHASES.join(" | ")}> [--plan] [--skip-build]\n`,
|
|
77
|
+
);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return { phase, plan, opts };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printPlan(phase: CiPhase, steps: CiStep[]): void {
|
|
84
|
+
console.log(`\n rastack ci ${phase} — ${steps.length} step(s):\n`);
|
|
85
|
+
steps.forEach((s, i) => {
|
|
86
|
+
const at = s.cwd ? ` (in ${s.cwd}/)` : "";
|
|
87
|
+
console.log(` ${i + 1}. ${s.name}${at}`);
|
|
88
|
+
console.log(` $ ${s.run}\n`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runSteps(phase: CiPhase, steps: CiStep[]): void {
|
|
93
|
+
for (const step of steps) {
|
|
94
|
+
console.log(`\n▸ ${step.name}${step.cwd ? ` (in ${step.cwd}/)` : ""}`);
|
|
95
|
+
console.log(` $ ${step.run}`);
|
|
96
|
+
try {
|
|
97
|
+
// Runs a CI pipeline step derived from the developer's own deploy config
|
|
98
|
+
// — no untrusted input. A shell is required for the `$(aws …)` output
|
|
99
|
+
// substitutions the plan uses to resolve stack outputs at run time.
|
|
100
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
101
|
+
execSync(step.run, {
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
cwd: step.cwd,
|
|
104
|
+
env: process.env,
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
console.error(`\n ✗ step failed: ${step.name}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log(`\n ✓ rastack ci ${phase} complete.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function main(argv: string[]): void {
|
|
115
|
+
const { phase, plan, opts } = parseFlags(argv);
|
|
116
|
+
const cfg = loadConfig();
|
|
117
|
+
if (!cfg) {
|
|
118
|
+
console.error(
|
|
119
|
+
" ✗ no .rastack/deploy.json found. Run `rastack init` (or `rastack deploy`) first.",
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const steps = ciPlan(phase, cfg, opts);
|
|
125
|
+
if (plan) printPlan(phase, steps);
|
|
126
|
+
else runSteps(phase, steps);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (require.main === module) {
|
|
130
|
+
main(process.argv.slice(2));
|
|
131
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rastack deploy` — configure a repo's serverless deployment.
|
|
5
|
+
*
|
|
6
|
+
* rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
|
|
7
|
+
* [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
|
|
8
|
+
* [--artifacts-bucket <b>] [--arch arm64|x86_64]
|
|
9
|
+
* [--print] [--run] [--yes]
|
|
10
|
+
*
|
|
11
|
+
* Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
|
|
12
|
+
* run) and prints exactly how to stand the app up: the one-time
|
|
13
|
+
* `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
|
|
14
|
+
* Variables CI reads, and the Lambda's runtime env. Infra is created once by a
|
|
15
|
+
* human — this command *configures* that step, it does not silently mutate the
|
|
16
|
+
* cloud. `--run` executes the launch command for you; `--print` is
|
|
17
|
+
* non-interactive (config as-is).
|
|
18
|
+
*
|
|
19
|
+
* The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from "child_process";
|
|
23
|
+
import {
|
|
24
|
+
DeployConfig,
|
|
25
|
+
PartialDeployConfig,
|
|
26
|
+
deployStackCommand,
|
|
27
|
+
lambdaEnv,
|
|
28
|
+
mergeConfig,
|
|
29
|
+
normalizeConfig,
|
|
30
|
+
repoVariables,
|
|
31
|
+
stackParameters,
|
|
32
|
+
templateUrl,
|
|
33
|
+
validateConfig,
|
|
34
|
+
} from "./deploy/config";
|
|
35
|
+
import {
|
|
36
|
+
ask,
|
|
37
|
+
canPrompt,
|
|
38
|
+
loadConfig,
|
|
39
|
+
saveConfig,
|
|
40
|
+
withPrompt,
|
|
41
|
+
} from "./deploy/io";
|
|
42
|
+
|
|
43
|
+
interface Flags {
|
|
44
|
+
patch: PartialDeployConfig;
|
|
45
|
+
print: boolean;
|
|
46
|
+
run: boolean;
|
|
47
|
+
nonInteractive: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseFlags(argv: string[]): Flags {
|
|
51
|
+
const patch: PartialDeployConfig = { github: {}, lambda: {} };
|
|
52
|
+
let print = false;
|
|
53
|
+
let run = false;
|
|
54
|
+
let nonInteractive = false;
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
const next = () => argv[++i] ?? "";
|
|
58
|
+
switch (a) {
|
|
59
|
+
case "--app":
|
|
60
|
+
patch.app = next();
|
|
61
|
+
break;
|
|
62
|
+
case "--region":
|
|
63
|
+
patch.region = next();
|
|
64
|
+
break;
|
|
65
|
+
case "--owner":
|
|
66
|
+
patch.github!.owner = next();
|
|
67
|
+
break;
|
|
68
|
+
case "--repo":
|
|
69
|
+
patch.github!.repo = next();
|
|
70
|
+
break;
|
|
71
|
+
case "--branch":
|
|
72
|
+
patch.github!.branch = next();
|
|
73
|
+
break;
|
|
74
|
+
case "--prefix":
|
|
75
|
+
patch.namePrefix = next();
|
|
76
|
+
break;
|
|
77
|
+
case "--namespace":
|
|
78
|
+
patch.namespace = next();
|
|
79
|
+
break;
|
|
80
|
+
case "--stack-name":
|
|
81
|
+
patch.stackName = next();
|
|
82
|
+
break;
|
|
83
|
+
case "--templates-bucket":
|
|
84
|
+
patch.templatesBucket = next();
|
|
85
|
+
break;
|
|
86
|
+
case "--artifacts-bucket":
|
|
87
|
+
patch.artifactsBucket = next();
|
|
88
|
+
break;
|
|
89
|
+
case "--arch":
|
|
90
|
+
patch.lambda!.arch = next() as DeployConfig["lambda"]["arch"];
|
|
91
|
+
break;
|
|
92
|
+
case "--print":
|
|
93
|
+
print = true;
|
|
94
|
+
nonInteractive = true;
|
|
95
|
+
break;
|
|
96
|
+
case "--run":
|
|
97
|
+
run = true;
|
|
98
|
+
break;
|
|
99
|
+
case "--yes":
|
|
100
|
+
case "-y":
|
|
101
|
+
case "--non-interactive":
|
|
102
|
+
nonInteractive = true;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
if (a.startsWith("-")) {
|
|
106
|
+
console.error(`Unknown flag: ${a}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { patch, print, run, nonInteractive };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function seedConfig(patch: PartialDeployConfig): DeployConfig {
|
|
115
|
+
const existing = loadConfig();
|
|
116
|
+
return existing ? mergeConfig(existing, patch) : normalizeConfig(patch);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function promptMissing(cfg: DeployConfig): Promise<DeployConfig> {
|
|
120
|
+
return withPrompt(async (rl) => {
|
|
121
|
+
const app = await ask(rl, "App slug", cfg.app);
|
|
122
|
+
const owner = await ask(rl, "GitHub owner", cfg.github.owner);
|
|
123
|
+
const repo = await ask(rl, "GitHub repo", cfg.github.repo);
|
|
124
|
+
const branch = await ask(rl, "Deploy branch", cfg.github.branch);
|
|
125
|
+
const region = await ask(rl, "AWS region", cfg.region);
|
|
126
|
+
const namespace = await ask(rl, "Iceberg namespace", cfg.namespace);
|
|
127
|
+
const artifactsBucket = await ask(
|
|
128
|
+
rl,
|
|
129
|
+
"Artifacts bucket (from bootstrap stack)",
|
|
130
|
+
cfg.artifactsBucket ?? "",
|
|
131
|
+
);
|
|
132
|
+
const templatesBucket = await ask(
|
|
133
|
+
rl,
|
|
134
|
+
"Templates bucket (from bootstrap stack, optional)",
|
|
135
|
+
cfg.templatesBucket ?? "",
|
|
136
|
+
);
|
|
137
|
+
return mergeConfig(cfg, {
|
|
138
|
+
app,
|
|
139
|
+
region,
|
|
140
|
+
namespace,
|
|
141
|
+
artifactsBucket,
|
|
142
|
+
templatesBucket,
|
|
143
|
+
github: { owner, repo, branch },
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Pad a list of `Key=Value` rows into an aligned table. */
|
|
149
|
+
function table(rows: { key: string; value: string }[]): string {
|
|
150
|
+
const width = Math.max(0, ...rows.map((r) => r.key.length));
|
|
151
|
+
return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printSummary(cfg: DeployConfig): void {
|
|
155
|
+
const src = templateUrl(cfg);
|
|
156
|
+
console.log(`\n Deployment config (${cfg.stackName} @ ${cfg.region})\n`);
|
|
157
|
+
console.log(" ── 1. Create the infrastructure (once, by a human) ──");
|
|
158
|
+
if (!cfg.artifactsBucket) {
|
|
159
|
+
console.log(
|
|
160
|
+
" ⚠ artifactsBucket is unset — app.yaml requires it. Run the bootstrap\n" +
|
|
161
|
+
" stack first, then set --artifacts-bucket (its ArtifactsBucketName output).",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (!src) {
|
|
165
|
+
console.log(
|
|
166
|
+
" (launching from the in-repo template; set --templates-bucket to launch\n" +
|
|
167
|
+
" the hosted app.yaml by URL instead)",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
console.log("\n" + deployStackCommand(cfg) + "\n");
|
|
171
|
+
|
|
172
|
+
console.log(
|
|
173
|
+
" ── 2. Wire the GitHub repo Variables (from the stack Outputs) ──",
|
|
174
|
+
);
|
|
175
|
+
console.log(
|
|
176
|
+
table(
|
|
177
|
+
repoVariables(cfg).map((v) => ({
|
|
178
|
+
key: v.name,
|
|
179
|
+
value: v.value || `<${v.from}>`,
|
|
180
|
+
})),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
console.log("\n ── 3. The Lambda runtime env (set by the stack) ──");
|
|
185
|
+
console.log(table(lambdaEnv(cfg)));
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
`\n ── 4. Then every push to ${cfg.github.branch} runs \`rastack ci deploy\`. ──\n`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runLaunch(cfg: DeployConfig): void {
|
|
193
|
+
const params = stackParameters(cfg).map((p) => `${p.key}=${p.value}`);
|
|
194
|
+
const src = templateUrl(cfg);
|
|
195
|
+
const args = [
|
|
196
|
+
"cloudformation",
|
|
197
|
+
"deploy",
|
|
198
|
+
...(src
|
|
199
|
+
? ["--template-url", src]
|
|
200
|
+
: ["--template-file", "deploy/cloudformation/app.yaml"]),
|
|
201
|
+
"--stack-name",
|
|
202
|
+
cfg.stackName,
|
|
203
|
+
"--capabilities",
|
|
204
|
+
"CAPABILITY_NAMED_IAM",
|
|
205
|
+
"--region",
|
|
206
|
+
cfg.region,
|
|
207
|
+
"--parameter-overrides",
|
|
208
|
+
...params,
|
|
209
|
+
];
|
|
210
|
+
console.log(`\n ▸ aws ${args.join(" ")}\n`);
|
|
211
|
+
try {
|
|
212
|
+
// Runs the developer's own AWS CLI with args built from their config — no
|
|
213
|
+
// untrusted input, and `execFileSync` (no shell) avoids any injection.
|
|
214
|
+
execFileSync("aws", args, { stdio: "inherit" });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(
|
|
217
|
+
`\n ✗ aws cloudformation deploy failed. Is the AWS CLI installed and are` +
|
|
218
|
+
` your credentials configured?\n ${(err as Error).message}`,
|
|
219
|
+
);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function main(argv: string[]): Promise<void> {
|
|
225
|
+
const flags = parseFlags(argv);
|
|
226
|
+
let cfg = seedConfig(flags.patch);
|
|
227
|
+
|
|
228
|
+
if (canPrompt(flags.nonInteractive)) {
|
|
229
|
+
console.log("\n rastack deploy — configure the serverless deployment\n");
|
|
230
|
+
cfg = await promptMissing(cfg);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const errors = validateConfig(cfg);
|
|
234
|
+
if (errors.length) {
|
|
235
|
+
for (const e of errors) console.error(` ✗ ${e}`);
|
|
236
|
+
console.error(
|
|
237
|
+
`\n ${errors.length} problem(s) — fix the config and re-run.`,
|
|
238
|
+
);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!flags.print) {
|
|
243
|
+
const file = saveConfig(cfg);
|
|
244
|
+
console.log(` ✓ saved ${file}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
printSummary(cfg);
|
|
248
|
+
|
|
249
|
+
if (flags.run) runLaunch(cfg);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (require.main === module) {
|
|
253
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
254
|
+
console.error(` ✗ ${(err as Error).message}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rastack init` — template the base GitHub Actions + deploy config into a repo.
|
|
5
|
+
*
|
|
6
|
+
* rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
|
|
7
|
+
* [--region <r>] [--force] [--no-workflows] [--yes]
|
|
8
|
+
*
|
|
9
|
+
* Scaffolds a new project (or reconfigures an existing one) so it is ready to
|
|
10
|
+
* deploy: `.rastack/deploy.json` plus `.github/workflows/{ci,deploy-backend}.yml`,
|
|
11
|
+
* both of which run `rastack ci`. GitHub coordinates default from the repo's
|
|
12
|
+
* `origin` remote; missing fields are prompted for on a TTY. Existing files are
|
|
13
|
+
* left untouched unless `--force`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import {
|
|
19
|
+
DeployConfig,
|
|
20
|
+
PartialDeployConfig,
|
|
21
|
+
mergeConfig,
|
|
22
|
+
normalizeConfig,
|
|
23
|
+
parseGitHubRemote,
|
|
24
|
+
validateConfig,
|
|
25
|
+
} from "./deploy/config";
|
|
26
|
+
import { scaffoldFiles } from "./deploy/workflows";
|
|
27
|
+
import {
|
|
28
|
+
ask,
|
|
29
|
+
canPrompt,
|
|
30
|
+
gitRemoteUrl,
|
|
31
|
+
loadConfig,
|
|
32
|
+
withPrompt,
|
|
33
|
+
} from "./deploy/io";
|
|
34
|
+
|
|
35
|
+
interface Flags {
|
|
36
|
+
patch: PartialDeployConfig;
|
|
37
|
+
force: boolean;
|
|
38
|
+
noWorkflows: boolean;
|
|
39
|
+
nonInteractive: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseFlags(argv: string[]): Flags {
|
|
43
|
+
const patch: PartialDeployConfig = { github: {} };
|
|
44
|
+
let force = false;
|
|
45
|
+
let noWorkflows = false;
|
|
46
|
+
let nonInteractive = false;
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const a = argv[i];
|
|
49
|
+
const next = () => argv[++i] ?? "";
|
|
50
|
+
switch (a) {
|
|
51
|
+
case "--app":
|
|
52
|
+
patch.app = next();
|
|
53
|
+
break;
|
|
54
|
+
case "--owner":
|
|
55
|
+
patch.github!.owner = next();
|
|
56
|
+
break;
|
|
57
|
+
case "--repo":
|
|
58
|
+
patch.github!.repo = next();
|
|
59
|
+
break;
|
|
60
|
+
case "--branch":
|
|
61
|
+
patch.github!.branch = next();
|
|
62
|
+
break;
|
|
63
|
+
case "--region":
|
|
64
|
+
patch.region = next();
|
|
65
|
+
break;
|
|
66
|
+
case "--prefix":
|
|
67
|
+
patch.namePrefix = next();
|
|
68
|
+
break;
|
|
69
|
+
case "--force":
|
|
70
|
+
force = true;
|
|
71
|
+
break;
|
|
72
|
+
case "--no-workflows":
|
|
73
|
+
noWorkflows = true;
|
|
74
|
+
break;
|
|
75
|
+
case "--yes":
|
|
76
|
+
case "-y":
|
|
77
|
+
case "--non-interactive":
|
|
78
|
+
nonInteractive = true;
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
if (a.startsWith("-")) {
|
|
82
|
+
console.error(`Unknown flag: ${a}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { patch, force, noWorkflows, nonInteractive };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
91
|
+
function seedConfig(flags: Flags): DeployConfig {
|
|
92
|
+
const existing = loadConfig();
|
|
93
|
+
let cfg = existing
|
|
94
|
+
? mergeConfig(existing, flags.patch)
|
|
95
|
+
: normalizeConfig(flags.patch);
|
|
96
|
+
// Default the GitHub coordinates from `origin` when not already known.
|
|
97
|
+
if (!cfg.github.owner || !cfg.github.repo) {
|
|
98
|
+
const url = gitRemoteUrl();
|
|
99
|
+
const remote = url ? parseGitHubRemote(url) : null;
|
|
100
|
+
if (remote) {
|
|
101
|
+
cfg = mergeConfig(cfg, {
|
|
102
|
+
github: {
|
|
103
|
+
owner: cfg.github.owner || remote.owner,
|
|
104
|
+
repo: cfg.github.repo || remote.repo,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return cfg;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function promptMissing(cfg: DeployConfig): Promise<DeployConfig> {
|
|
113
|
+
return withPrompt(async (rl) => {
|
|
114
|
+
const app = await ask(
|
|
115
|
+
rl,
|
|
116
|
+
"App slug",
|
|
117
|
+
cfg.app || path.basename(process.cwd()),
|
|
118
|
+
);
|
|
119
|
+
const owner = await ask(rl, "GitHub owner", cfg.github.owner);
|
|
120
|
+
const repo = await ask(rl, "GitHub repo", cfg.github.repo);
|
|
121
|
+
const branch = await ask(rl, "Deploy branch", cfg.github.branch);
|
|
122
|
+
const region = await ask(rl, "AWS region", cfg.region);
|
|
123
|
+
return mergeConfig(cfg, {
|
|
124
|
+
app,
|
|
125
|
+
region,
|
|
126
|
+
github: { owner, repo, branch },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main(argv: string[]): Promise<void> {
|
|
132
|
+
const flags = parseFlags(argv);
|
|
133
|
+
let cfg = seedConfig(flags);
|
|
134
|
+
|
|
135
|
+
if (canPrompt(flags.nonInteractive)) {
|
|
136
|
+
console.log(
|
|
137
|
+
"\n rastack init — scaffold the deploy config + GitHub Actions\n",
|
|
138
|
+
);
|
|
139
|
+
cfg = await promptMissing(cfg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const errors = validateConfig(cfg);
|
|
143
|
+
// In non-interactive mode a still-incomplete config is fatal; interactively we
|
|
144
|
+
// still write what we have so the user can finish it in the file, but warn.
|
|
145
|
+
if (errors.length) {
|
|
146
|
+
const label = flags.nonInteractive ? "✗" : "⚠";
|
|
147
|
+
for (const e of errors) console.error(` ${label} ${e}`);
|
|
148
|
+
if (flags.nonInteractive) {
|
|
149
|
+
console.error(
|
|
150
|
+
`\n ${errors.length} problem(s). Pass the missing values as flags.`,
|
|
151
|
+
);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const files = scaffoldFiles(cfg, { noWorkflows: flags.noWorkflows });
|
|
157
|
+
const written: string[] = [];
|
|
158
|
+
const skipped: string[] = [];
|
|
159
|
+
for (const f of files) {
|
|
160
|
+
const abs = path.join(process.cwd(), f.path);
|
|
161
|
+
if (fs.existsSync(abs) && !flags.force) {
|
|
162
|
+
skipped.push(f.path);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
166
|
+
fs.writeFileSync(abs, f.content);
|
|
167
|
+
written.push(f.path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log("");
|
|
171
|
+
for (const w of written) console.log(` ✓ wrote ${w}`);
|
|
172
|
+
for (const s of skipped)
|
|
173
|
+
console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
|
|
174
|
+
console.log(
|
|
175
|
+
`\n Next:\n` +
|
|
176
|
+
` 1. rastack deploy — review the config and print the stack launch command\n` +
|
|
177
|
+
` 2. create the CloudFormation stack (once), then wire the repo Variables it prints\n` +
|
|
178
|
+
` 3. push to ${cfg.github.branch} — CI runs \`rastack ci deploy\`\n`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (require.main === module) {
|
|
183
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
184
|
+
console.error(` ✗ ${(err as Error).message}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
}
|
package/src/rastack.ts
CHANGED
|
@@ -16,22 +16,44 @@
|
|
|
16
16
|
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
17
17
|
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
18
18
|
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
19
|
+
* rastack init [--app slug] [--force] Template the base GitHub Actions + deploy config
|
|
20
|
+
* rastack deploy [--print] [--run] Configure the serverless deployment
|
|
21
|
+
* rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
import { execSync, spawnSync } from "child_process";
|
|
25
|
+
import { readFileSync } from "fs";
|
|
22
26
|
import path from "path";
|
|
23
27
|
|
|
24
28
|
const [command, ...rest] = process.argv.slice(2);
|
|
25
29
|
|
|
30
|
+
/** Print the installed package version (used by the installer to verify). */
|
|
31
|
+
function printVersion(): void {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(
|
|
34
|
+
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
|
|
35
|
+
);
|
|
36
|
+
console.log(pkg.version);
|
|
37
|
+
} catch {
|
|
38
|
+
console.log("unknown");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function run(script: string, args: string[] = rest) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
try {
|
|
44
|
+
// `rastack` is a local developer CLI: `script` is a fixed internal dist filename
|
|
45
|
+
// and `args` come from the developer's own shell (process.argv), not from an
|
|
46
|
+
// untrusted network request — there is no command-injection surface here.
|
|
47
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
48
|
+
execSync(`node ${path.join(__dirname, script)} ${args.join(" ")}`, {
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// The subcommand already printed its own error over inherited stdio; just
|
|
54
|
+
// propagate its exit code without dumping node's child_process stack trace.
|
|
55
|
+
process.exit((err as { status?: number }).status ?? 1);
|
|
56
|
+
}
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/** Invoke the compiled Rust server binary (built by `cargo build` in rust/). */
|
|
@@ -59,6 +81,11 @@ function runRust(subcommand: string) {
|
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
switch (command) {
|
|
84
|
+
case "--version":
|
|
85
|
+
case "-v":
|
|
86
|
+
case "version":
|
|
87
|
+
printVersion();
|
|
88
|
+
break;
|
|
62
89
|
case "compile":
|
|
63
90
|
case "check":
|
|
64
91
|
case "list":
|
|
@@ -90,6 +117,18 @@ switch (command) {
|
|
|
90
117
|
case "seed":
|
|
91
118
|
run("seed.js");
|
|
92
119
|
break;
|
|
120
|
+
case "init":
|
|
121
|
+
// Template the base GitHub Actions + deploy config into a repo.
|
|
122
|
+
run("rastack-init.js", rest);
|
|
123
|
+
break;
|
|
124
|
+
case "deploy":
|
|
125
|
+
// Configure the serverless deployment (writes .rastack/deploy.json).
|
|
126
|
+
run("rastack-deploy.js", rest);
|
|
127
|
+
break;
|
|
128
|
+
case "ci":
|
|
129
|
+
// Run a CI pipeline phase (check | build | deploy).
|
|
130
|
+
run("rastack-ci.js", rest);
|
|
131
|
+
break;
|
|
93
132
|
case "generate":
|
|
94
133
|
case "gen":
|
|
95
134
|
// Proxy to existing rastack codegen pipeline
|
|
@@ -107,7 +146,10 @@ switch (command) {
|
|
|
107
146
|
` rastack serve | admin [--warehouse dir]\n` +
|
|
108
147
|
` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
109
148
|
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
110
|
-
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n
|
|
149
|
+
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
|
|
150
|
+
` rastack init [--app slug] [--owner o] [--repo r] [--force] Template GitHub Actions + deploy config\n` +
|
|
151
|
+
` rastack deploy [--app slug] [--print] [--run] Configure the serverless deployment\n` +
|
|
152
|
+
` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n`,
|
|
111
153
|
);
|
|
112
154
|
process.exit(1);
|
|
113
155
|
}
|