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,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
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
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const config_1 = require("./deploy/config");
|
|
52
|
+
const workflows_1 = require("./deploy/workflows");
|
|
53
|
+
const io_1 = require("./deploy/io");
|
|
54
|
+
function parseFlags(argv) {
|
|
55
|
+
const patch = { github: {} };
|
|
56
|
+
let force = false;
|
|
57
|
+
let noWorkflows = false;
|
|
58
|
+
let nonInteractive = false;
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
const next = () => argv[++i] ?? "";
|
|
62
|
+
switch (a) {
|
|
63
|
+
case "--app":
|
|
64
|
+
patch.app = next();
|
|
65
|
+
break;
|
|
66
|
+
case "--owner":
|
|
67
|
+
patch.github.owner = next();
|
|
68
|
+
break;
|
|
69
|
+
case "--repo":
|
|
70
|
+
patch.github.repo = next();
|
|
71
|
+
break;
|
|
72
|
+
case "--branch":
|
|
73
|
+
patch.github.branch = next();
|
|
74
|
+
break;
|
|
75
|
+
case "--region":
|
|
76
|
+
patch.region = next();
|
|
77
|
+
break;
|
|
78
|
+
case "--prefix":
|
|
79
|
+
patch.namePrefix = next();
|
|
80
|
+
break;
|
|
81
|
+
case "--force":
|
|
82
|
+
force = true;
|
|
83
|
+
break;
|
|
84
|
+
case "--no-workflows":
|
|
85
|
+
noWorkflows = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--yes":
|
|
88
|
+
case "-y":
|
|
89
|
+
case "--non-interactive":
|
|
90
|
+
nonInteractive = true;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
if (a.startsWith("-")) {
|
|
94
|
+
console.error(`Unknown flag: ${a}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { patch, force, noWorkflows, nonInteractive };
|
|
100
|
+
}
|
|
101
|
+
/** Seed a config: existing file → flags → git remote → defaults. */
|
|
102
|
+
function seedConfig(flags) {
|
|
103
|
+
const existing = (0, io_1.loadConfig)();
|
|
104
|
+
let cfg = existing
|
|
105
|
+
? (0, config_1.mergeConfig)(existing, flags.patch)
|
|
106
|
+
: (0, config_1.normalizeConfig)(flags.patch);
|
|
107
|
+
// Default the GitHub coordinates from `origin` when not already known.
|
|
108
|
+
if (!cfg.github.owner || !cfg.github.repo) {
|
|
109
|
+
const url = (0, io_1.gitRemoteUrl)();
|
|
110
|
+
const remote = url ? (0, config_1.parseGitHubRemote)(url) : null;
|
|
111
|
+
if (remote) {
|
|
112
|
+
cfg = (0, config_1.mergeConfig)(cfg, {
|
|
113
|
+
github: {
|
|
114
|
+
owner: cfg.github.owner || remote.owner,
|
|
115
|
+
repo: cfg.github.repo || remote.repo,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return cfg;
|
|
121
|
+
}
|
|
122
|
+
async function promptMissing(cfg) {
|
|
123
|
+
return (0, io_1.withPrompt)(async (rl) => {
|
|
124
|
+
const app = await (0, io_1.ask)(rl, "App slug", cfg.app || path.basename(process.cwd()));
|
|
125
|
+
const owner = await (0, io_1.ask)(rl, "GitHub owner", cfg.github.owner);
|
|
126
|
+
const repo = await (0, io_1.ask)(rl, "GitHub repo", cfg.github.repo);
|
|
127
|
+
const branch = await (0, io_1.ask)(rl, "Deploy branch", cfg.github.branch);
|
|
128
|
+
const region = await (0, io_1.ask)(rl, "AWS region", cfg.region);
|
|
129
|
+
return (0, config_1.mergeConfig)(cfg, {
|
|
130
|
+
app,
|
|
131
|
+
region,
|
|
132
|
+
github: { owner, repo, branch },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function main(argv) {
|
|
137
|
+
const flags = parseFlags(argv);
|
|
138
|
+
let cfg = seedConfig(flags);
|
|
139
|
+
if ((0, io_1.canPrompt)(flags.nonInteractive)) {
|
|
140
|
+
console.log("\n rastack init — scaffold the deploy config + GitHub Actions\n");
|
|
141
|
+
cfg = await promptMissing(cfg);
|
|
142
|
+
}
|
|
143
|
+
const errors = (0, config_1.validateConfig)(cfg);
|
|
144
|
+
// In non-interactive mode a still-incomplete config is fatal; interactively we
|
|
145
|
+
// still write what we have so the user can finish it in the file, but warn.
|
|
146
|
+
if (errors.length) {
|
|
147
|
+
const label = flags.nonInteractive ? "✗" : "⚠";
|
|
148
|
+
for (const e of errors)
|
|
149
|
+
console.error(` ${label} ${e}`);
|
|
150
|
+
if (flags.nonInteractive) {
|
|
151
|
+
console.error(`\n ${errors.length} problem(s). Pass the missing values as flags.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const files = (0, workflows_1.scaffoldFiles)(cfg, { noWorkflows: flags.noWorkflows });
|
|
156
|
+
const written = [];
|
|
157
|
+
const skipped = [];
|
|
158
|
+
for (const f of files) {
|
|
159
|
+
const abs = path.join(process.cwd(), f.path);
|
|
160
|
+
if (fs.existsSync(abs) && !flags.force) {
|
|
161
|
+
skipped.push(f.path);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
165
|
+
fs.writeFileSync(abs, f.content);
|
|
166
|
+
written.push(f.path);
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
for (const w of written)
|
|
170
|
+
console.log(` ✓ wrote ${w}`);
|
|
171
|
+
for (const s of skipped)
|
|
172
|
+
console.log(` • skipped ${s} (exists — pass --force to overwrite)`);
|
|
173
|
+
console.log(`\n Next:\n` +
|
|
174
|
+
` 1. rastack deploy — review the config and print the stack launch command\n` +
|
|
175
|
+
` 2. create the CloudFormation stack (once), then wire the repo Variables it prints\n` +
|
|
176
|
+
` 3. push to ${cfg.github.branch} — CI runs \`rastack ci deploy\`\n`);
|
|
177
|
+
}
|
|
178
|
+
if (require.main === module) {
|
|
179
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
180
|
+
console.error(` ✗ ${err.message}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
}
|
package/dist/rastack.d.ts
CHANGED
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
16
16
|
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
17
17
|
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
18
|
+
* rastack init [--app slug] [--force] Template the base GitHub Actions + deploy config
|
|
19
|
+
* rastack deploy [--print] [--run] Configure the serverless deployment
|
|
20
|
+
* rastack ci <check|build|deploy> [--plan] Run the CI pipeline (keeps Actions a one-liner)
|
|
18
21
|
*/
|
|
19
22
|
export {};
|
package/dist/rastack.js
CHANGED
|
@@ -16,23 +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
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
24
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
25
|
};
|
|
23
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
27
|
const child_process_1 = require("child_process");
|
|
28
|
+
const fs_1 = require("fs");
|
|
25
29
|
const path_1 = __importDefault(require("path"));
|
|
26
30
|
const [command, ...rest] = process.argv.slice(2);
|
|
31
|
+
/** Print the installed package version (used by the installer to verify). */
|
|
32
|
+
function printVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)(path_1.default.join(__dirname, "..", "package.json"), "utf-8"));
|
|
35
|
+
console.log(pkg.version);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.log("unknown");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
function run(script, args = rest) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
try {
|
|
43
|
+
// `rastack` is a local developer CLI: `script` is a fixed internal dist filename
|
|
44
|
+
// and `args` come from the developer's own shell (process.argv), not from an
|
|
45
|
+
// untrusted network request — there is no command-injection surface here.
|
|
46
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
47
|
+
(0, child_process_1.execSync)(`node ${path_1.default.join(__dirname, script)} ${args.join(" ")}`, {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
env: process.env,
|
|
50
|
+
});
|
|
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.status ?? 1);
|
|
56
|
+
}
|
|
36
57
|
}
|
|
37
58
|
/** Invoke the compiled Rust server binary (built by `cargo build` in rust/). */
|
|
38
59
|
function runRust(subcommand) {
|
|
@@ -55,6 +76,11 @@ function runRust(subcommand) {
|
|
|
55
76
|
process.exit(result.status ?? 0);
|
|
56
77
|
}
|
|
57
78
|
switch (command) {
|
|
79
|
+
case "--version":
|
|
80
|
+
case "-v":
|
|
81
|
+
case "version":
|
|
82
|
+
printVersion();
|
|
83
|
+
break;
|
|
58
84
|
case "compile":
|
|
59
85
|
case "check":
|
|
60
86
|
case "list":
|
|
@@ -86,6 +112,18 @@ switch (command) {
|
|
|
86
112
|
case "seed":
|
|
87
113
|
run("seed.js");
|
|
88
114
|
break;
|
|
115
|
+
case "init":
|
|
116
|
+
// Template the base GitHub Actions + deploy config into a repo.
|
|
117
|
+
run("rastack-init.js", rest);
|
|
118
|
+
break;
|
|
119
|
+
case "deploy":
|
|
120
|
+
// Configure the serverless deployment (writes .rastack/deploy.json).
|
|
121
|
+
run("rastack-deploy.js", rest);
|
|
122
|
+
break;
|
|
123
|
+
case "ci":
|
|
124
|
+
// Run a CI pipeline phase (check | build | deploy).
|
|
125
|
+
run("rastack-ci.js", rest);
|
|
126
|
+
break;
|
|
89
127
|
case "generate":
|
|
90
128
|
case "gen":
|
|
91
129
|
// Proxy to existing rastack codegen pipeline
|
|
@@ -102,6 +140,9 @@ switch (command) {
|
|
|
102
140
|
` rastack serve | admin [--warehouse dir]\n` +
|
|
103
141
|
` rastack wasm Compile the API to WebAssembly (browser local dev)\n` +
|
|
104
142
|
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
105
|
-
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n`
|
|
143
|
+
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
|
|
144
|
+
` rastack init [--app slug] [--owner o] [--repo r] [--force] Template GitHub Actions + deploy config\n` +
|
|
145
|
+
` rastack deploy [--app slug] [--print] [--run] Configure the serverless deployment\n` +
|
|
146
|
+
` rastack ci <check|build|deploy> [--plan] Run the CI pipeline (cleaner Actions)\n`);
|
|
106
147
|
process.exit(1);
|
|
107
148
|
}
|
package/package.json
CHANGED
package/src/deploy/ci.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rastack ci` — the CI pipeline as a *plan of shell steps*, derived purely from
|
|
3
|
+
* the deploy config.
|
|
4
|
+
*
|
|
5
|
+
* The whole point is to make GitHub Actions cleaner: instead of thirty lines of
|
|
6
|
+
* inline bash (compile the manifest, cross-build the arm64 Lambda, bake the
|
|
7
|
+
* Cognito JWKS, assemble the zip, `update-function-code`, wait, print the URL),
|
|
8
|
+
* a workflow runs one line — `rastack ci deploy` — and this module owns the
|
|
9
|
+
* steps. Because the plan is a pure function of the config, it is fully testable
|
|
10
|
+
* without AWS or a Rust toolchain, and `rastack ci --plan` prints exactly what
|
|
11
|
+
* will run.
|
|
12
|
+
*
|
|
13
|
+
* Values that only exist after the stack is created (the function name, the API
|
|
14
|
+
* URL, the JWKS URL) are either taken from `cfg.outputs` when already resolved,
|
|
15
|
+
* or emitted as a live `$(aws cloudformation describe-stacks …)` substitution so
|
|
16
|
+
* the same plan works before and after they are wired into the config.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { DeployConfig } from "./config";
|
|
20
|
+
|
|
21
|
+
export type CiPhase = "check" | "build" | "deploy";
|
|
22
|
+
|
|
23
|
+
export interface CiStep {
|
|
24
|
+
/** Human label, echoed before the step runs. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** The shell command to execute. */
|
|
27
|
+
run: string;
|
|
28
|
+
/** Working directory relative to the repo root (default: repo root). */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CiPlanOptions {
|
|
33
|
+
/** How to invoke the rastack CLI for nested steps (default: `rastack`). */
|
|
34
|
+
bin?: string;
|
|
35
|
+
/** Skip the build steps in `deploy` (code already built this job). */
|
|
36
|
+
skipBuild?: boolean;
|
|
37
|
+
/** Override the function name (else from cfg.outputs, else resolved live). */
|
|
38
|
+
functionName?: string;
|
|
39
|
+
/** Override the JWKS URL (else from cfg.outputs, else resolved live). */
|
|
40
|
+
jwksUrl?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A stack Output value: the literal from `cfg.outputs` if wired, else a shell
|
|
45
|
+
* command substitution that resolves it live from CloudFormation. Keeping the
|
|
46
|
+
* fallback inline (rather than a separate step) means the plan is a flat list of
|
|
47
|
+
* self-contained commands that read correctly in any order.
|
|
48
|
+
*/
|
|
49
|
+
function stackOutput(
|
|
50
|
+
cfg: DeployConfig,
|
|
51
|
+
outputKey: string,
|
|
52
|
+
literal?: string,
|
|
53
|
+
): string {
|
|
54
|
+
if (literal) return literal;
|
|
55
|
+
return (
|
|
56
|
+
`$(aws cloudformation describe-stacks --stack-name ${cfg.stackName} ` +
|
|
57
|
+
`--region ${cfg.region} ` +
|
|
58
|
+
`--query "Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue" ` +
|
|
59
|
+
`--output text)`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The Lambda staging directory `cargo lambda build` writes the binary into. */
|
|
64
|
+
function stageDir(cfg: DeployConfig): string {
|
|
65
|
+
return `rust/target/lambda/${cfg.lambda.bin}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `check` — validate the resource graph and compile the manifest. A CI gate. */
|
|
69
|
+
export function checkSteps(cfg: DeployConfig, bin: string): CiStep[] {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
name: "Validate resource graph",
|
|
73
|
+
run: `${bin} check ${cfg.resourcesDir}`,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "Compile manifest + OpenAPI",
|
|
77
|
+
run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `build` — produce the deployable Lambda bundle: compile the manifest,
|
|
84
|
+
* cross-build the custom-runtime binary, bake the Cognito JWKS in, and zip the
|
|
85
|
+
* binary together with the files it reads at `/var/task`.
|
|
86
|
+
*/
|
|
87
|
+
export function buildSteps(
|
|
88
|
+
cfg: DeployConfig,
|
|
89
|
+
opts: CiPlanOptions = {},
|
|
90
|
+
): CiStep[] {
|
|
91
|
+
const bin = opts.bin ?? "rastack";
|
|
92
|
+
const stage = stageDir(cfg);
|
|
93
|
+
const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
|
|
94
|
+
const jwks = stackOutput(
|
|
95
|
+
cfg,
|
|
96
|
+
"JwksUrl",
|
|
97
|
+
opts.jwksUrl ?? cfg.outputs?.jwksUrl,
|
|
98
|
+
);
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
name: "Compile manifest + OpenAPI",
|
|
102
|
+
run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "Cross-build the Lambda binary",
|
|
106
|
+
run: `cargo lambda build --release ${archFlag} --bin ${cfg.lambda.bin} --features ${cfg.lambda.features}`,
|
|
107
|
+
cwd: "rust",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Bake the Cognito JWKS into the bundle",
|
|
111
|
+
run: `curl -fsSL "${jwks}" -o ${stage}/jwks.json`,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "Stage the manifest + OpenAPI",
|
|
115
|
+
run: `cp ${cfg.outDir}/schema.rastack.json ${cfg.outDir}/openapi.json ${stage}/`,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "Assemble the deployment bundle",
|
|
119
|
+
run: `zip -j ${stage}/bootstrap.zip ${stage}/${cfg.lambda.bin} ${stage}/schema.rastack.json ${stage}/openapi.json ${stage}/jwks.json`,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `deploy` — the whole CI job: build (unless already built), then ship the code
|
|
126
|
+
* with the single privileged call CI is allowed, `update-function-code`, wait
|
|
127
|
+
* for it to settle, and print the API URL.
|
|
128
|
+
*/
|
|
129
|
+
export function deploySteps(
|
|
130
|
+
cfg: DeployConfig,
|
|
131
|
+
opts: CiPlanOptions = {},
|
|
132
|
+
): CiStep[] {
|
|
133
|
+
const stage = stageDir(cfg);
|
|
134
|
+
const zip = `${stage}/bootstrap.zip`;
|
|
135
|
+
const fn = stackOutput(
|
|
136
|
+
cfg,
|
|
137
|
+
"FunctionName",
|
|
138
|
+
opts.functionName ?? cfg.outputs?.functionName,
|
|
139
|
+
);
|
|
140
|
+
const apiUrl = stackOutput(cfg, "ApiUrl", cfg.outputs?.apiUrl);
|
|
141
|
+
const steps: CiStep[] = opts.skipBuild ? [] : buildSteps(cfg, opts);
|
|
142
|
+
steps.push(
|
|
143
|
+
{
|
|
144
|
+
name: "Ship the code (update-function-code)",
|
|
145
|
+
run:
|
|
146
|
+
`aws lambda update-function-code --function-name ${fn} ` +
|
|
147
|
+
`--zip-file fileb://${zip} --region ${cfg.region} --publish ` +
|
|
148
|
+
`--output text --query LastModified`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "Wait for the function to update",
|
|
152
|
+
run: `aws lambda wait function-updated --function-name ${fn} --region ${cfg.region}`,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "Report the API URL",
|
|
156
|
+
run: `echo "Deployed ${cfg.app} → ${apiUrl}"`,
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
return steps;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build the ordered step list for a phase. */
|
|
163
|
+
export function ciPlan(
|
|
164
|
+
phase: CiPhase,
|
|
165
|
+
cfg: DeployConfig,
|
|
166
|
+
opts: CiPlanOptions = {},
|
|
167
|
+
): CiStep[] {
|
|
168
|
+
const bin = opts.bin ?? "rastack";
|
|
169
|
+
switch (phase) {
|
|
170
|
+
case "check":
|
|
171
|
+
return checkSteps(cfg, bin);
|
|
172
|
+
case "build":
|
|
173
|
+
return buildSteps(cfg, opts);
|
|
174
|
+
case "deploy":
|
|
175
|
+
return deploySteps(cfg, opts);
|
|
176
|
+
}
|
|
177
|
+
}
|