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,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Filesystem + prompt helpers shared by the `rastack deploy | ci | init`
|
|
4
|
+
* entrypoints. Everything pure lives in the sibling modules; this is the small
|
|
5
|
+
* IO boundary they wrap.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.configPath = configPath;
|
|
42
|
+
exports.loadConfig = loadConfig;
|
|
43
|
+
exports.saveConfig = saveConfig;
|
|
44
|
+
exports.gitRemoteUrl = gitRemoteUrl;
|
|
45
|
+
exports.canPrompt = canPrompt;
|
|
46
|
+
exports.ask = ask;
|
|
47
|
+
exports.withPrompt = withPrompt;
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const readline = __importStar(require("readline"));
|
|
52
|
+
const workflows_1 = require("./workflows");
|
|
53
|
+
const config_1 = require("./config");
|
|
54
|
+
/** Absolute path to the deploy config for a project root. */
|
|
55
|
+
function configPath(cwd = process.cwd()) {
|
|
56
|
+
return path.join(cwd, workflows_1.CONFIG_PATH);
|
|
57
|
+
}
|
|
58
|
+
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
59
|
+
function loadConfig(cwd = process.cwd()) {
|
|
60
|
+
const file = configPath(cwd);
|
|
61
|
+
if (!fs.existsSync(file))
|
|
62
|
+
return null;
|
|
63
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
64
|
+
return (0, config_1.normalizeConfig)(raw);
|
|
65
|
+
}
|
|
66
|
+
/** Write the deploy config, creating `.rastack/` if needed. */
|
|
67
|
+
function saveConfig(cfg, cwd = process.cwd()) {
|
|
68
|
+
const file = configPath(cwd);
|
|
69
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
71
|
+
return file;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort GitHub `origin` remote URL, for defaulting the repo coordinates.
|
|
75
|
+
* Returns null when not a git repo or no origin.
|
|
76
|
+
*/
|
|
77
|
+
function gitRemoteUrl(cwd = process.cwd()) {
|
|
78
|
+
try {
|
|
79
|
+
// Reads the developer's own git config with a fixed command; no untrusted
|
|
80
|
+
// input, so there is no command-injection surface.
|
|
81
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
82
|
+
const out = (0, child_process_1.execSync)("git remote get-url origin", {
|
|
83
|
+
cwd,
|
|
84
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
85
|
+
});
|
|
86
|
+
const url = out.toString().trim();
|
|
87
|
+
return url || null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** True when we can prompt the user (a TTY, and not explicitly opted out). */
|
|
94
|
+
function canPrompt(nonInteractive) {
|
|
95
|
+
return !nonInteractive && Boolean(process.stdin.isTTY);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
99
|
+
* the trimmed answer, or the default on an empty line.
|
|
100
|
+
*/
|
|
101
|
+
function ask(rl, label, def) {
|
|
102
|
+
const suffix = def ? ` [${def}]` : "";
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
rl.question(` ${label}${suffix}: `, (answer) => {
|
|
105
|
+
resolve(answer.trim() || def);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Run a callback with a readline interface, always closing it. */
|
|
110
|
+
async function withPrompt(fn) {
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout,
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
return await fn(rl);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rl.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files `rastack init` templates into a repo — the base GitHub Actions and
|
|
3
|
+
* the deploy config — so starting (or reconfiguring) a project is one command.
|
|
4
|
+
*
|
|
5
|
+
* The generated workflows lean on `rastack ci`: rather than inlining the build
|
|
6
|
+
* and deploy shell, each job runs one `rastack ci …` line, and the CLI owns the
|
|
7
|
+
* steps (tools/src/deploy/ci.ts). That keeps the YAML short and keeps the
|
|
8
|
+
* pipeline logic versioned with the CLI, testable, and inspectable with
|
|
9
|
+
* `rastack ci --plan`.
|
|
10
|
+
*
|
|
11
|
+
* Pure string builders — `rastack-init.ts` is the thin wrapper that writes them.
|
|
12
|
+
*/
|
|
13
|
+
import { DeployConfig } from "./config";
|
|
14
|
+
/** A templated file: repo-relative path + its contents. */
|
|
15
|
+
export interface ScaffoldFile {
|
|
16
|
+
path: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `.github/workflows/ci.yml` — validate resources and compile the manifest on
|
|
21
|
+
* every push/PR. The clean replacement for hand-written compile/check steps.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderCiWorkflow(cfg: DeployConfig): string;
|
|
24
|
+
/**
|
|
25
|
+
* `.github/workflows/deploy-backend.yml` — ship the Rust Lambda on every push to
|
|
26
|
+
* the deploy branch. Authenticates with GitHub OIDC (no stored keys) and then
|
|
27
|
+
* runs one line, `rastack ci deploy`, which cross-builds the bundle, bakes the
|
|
28
|
+
* JWKS, and calls `update-function-code`. That single privileged call is the
|
|
29
|
+
* whole of CI's blast radius (docs/serverless-deploy.md §2).
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderDeployWorkflow(cfg: DeployConfig): string;
|
|
32
|
+
/** The deploy config written to `.rastack/deploy.json`, pretty-printed. */
|
|
33
|
+
export declare function renderConfigFile(cfg: DeployConfig): string;
|
|
34
|
+
export declare const CONFIG_PATH = ".rastack/deploy.json";
|
|
35
|
+
export declare const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
36
|
+
export declare const DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
|
|
37
|
+
export interface ScaffoldOptions {
|
|
38
|
+
/** Omit the GitHub Actions workflows (config only). */
|
|
39
|
+
noWorkflows?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The full set of files `rastack init` templates: the deploy config plus the two
|
|
43
|
+
* base workflows. Returned as data so the caller decides what to write, skip, or
|
|
44
|
+
* overwrite.
|
|
45
|
+
*/
|
|
46
|
+
export declare function scaffoldFiles(cfg: DeployConfig, opts?: ScaffoldOptions): ScaffoldFile[];
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The files `rastack init` templates into a repo — the base GitHub Actions and
|
|
4
|
+
* the deploy config — so starting (or reconfiguring) a project is one command.
|
|
5
|
+
*
|
|
6
|
+
* The generated workflows lean on `rastack ci`: rather than inlining the build
|
|
7
|
+
* and deploy shell, each job runs one `rastack ci …` line, and the CLI owns the
|
|
8
|
+
* steps (tools/src/deploy/ci.ts). That keeps the YAML short and keeps the
|
|
9
|
+
* pipeline logic versioned with the CLI, testable, and inspectable with
|
|
10
|
+
* `rastack ci --plan`.
|
|
11
|
+
*
|
|
12
|
+
* Pure string builders — `rastack-init.ts` is the thin wrapper that writes them.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DEPLOY_WORKFLOW_PATH = exports.CI_WORKFLOW_PATH = exports.CONFIG_PATH = void 0;
|
|
16
|
+
exports.renderCiWorkflow = renderCiWorkflow;
|
|
17
|
+
exports.renderDeployWorkflow = renderDeployWorkflow;
|
|
18
|
+
exports.renderConfigFile = renderConfigFile;
|
|
19
|
+
exports.scaffoldFiles = scaffoldFiles;
|
|
20
|
+
/** Node version the templated workflows pin (LTS — safe for a fresh repo). */
|
|
21
|
+
const NODE_VERSION = "22";
|
|
22
|
+
/**
|
|
23
|
+
* `.github/workflows/ci.yml` — validate resources and compile the manifest on
|
|
24
|
+
* every push/PR. The clean replacement for hand-written compile/check steps.
|
|
25
|
+
*/
|
|
26
|
+
function renderCiWorkflow(cfg) {
|
|
27
|
+
return `name: CI
|
|
28
|
+
|
|
29
|
+
# Validate the resource graph and compile the manifest on every change.
|
|
30
|
+
# The heavy lifting lives in \`rastack ci check\` (see tools/src/deploy/ci.ts),
|
|
31
|
+
# so this workflow stays a single command.
|
|
32
|
+
|
|
33
|
+
on:
|
|
34
|
+
push:
|
|
35
|
+
branches: [${cfg.github.branch}]
|
|
36
|
+
pull_request:
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
validate:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: actions/setup-node@v4
|
|
47
|
+
with:
|
|
48
|
+
node-version: '${NODE_VERSION}'
|
|
49
|
+
- name: Install rastack
|
|
50
|
+
run: npm install -g rastack
|
|
51
|
+
- name: Validate resources & compile manifest
|
|
52
|
+
run: rastack ci check
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `.github/workflows/deploy-backend.yml` — ship the Rust Lambda on every push to
|
|
57
|
+
* the deploy branch. Authenticates with GitHub OIDC (no stored keys) and then
|
|
58
|
+
* runs one line, `rastack ci deploy`, which cross-builds the bundle, bakes the
|
|
59
|
+
* JWKS, and calls `update-function-code`. That single privileged call is the
|
|
60
|
+
* whole of CI's blast radius (docs/serverless-deploy.md §2).
|
|
61
|
+
*/
|
|
62
|
+
function renderDeployWorkflow(cfg) {
|
|
63
|
+
const arch = cfg.lambda.arch === "arm64" ? "arm64" : "x86_64";
|
|
64
|
+
return `name: Deploy backend
|
|
65
|
+
|
|
66
|
+
# Ship the Rust API (rastack-server) to AWS Lambda. Infrastructure is created
|
|
67
|
+
# once by a human via CloudFormation (docs/serverless-deploy.md); this workflow
|
|
68
|
+
# only ever runs \`aws lambda update-function-code\`, wrapped by \`rastack ci\`.
|
|
69
|
+
#
|
|
70
|
+
# Wire these from the CloudFormation stack Outputs as GitHub repo Variables:
|
|
71
|
+
# AWS_DEPLOY_ROLE_ARN DeployRoleArn (the OIDC role this job assumes)
|
|
72
|
+
# AWS_REGION Region (${cfg.region})
|
|
73
|
+
|
|
74
|
+
on:
|
|
75
|
+
push:
|
|
76
|
+
branches: [${cfg.github.branch}]
|
|
77
|
+
|
|
78
|
+
# Least privilege: read the repo + mint an OIDC token to assume the deploy role.
|
|
79
|
+
permissions:
|
|
80
|
+
contents: read
|
|
81
|
+
id-token: write
|
|
82
|
+
|
|
83
|
+
concurrency:
|
|
84
|
+
group: deploy-backend
|
|
85
|
+
cancel-in-progress: true
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
deploy:
|
|
89
|
+
name: Build & deploy the Lambda (${arch})
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
# Skip on forks / repos that never wired the deploy role, so the job does
|
|
92
|
+
# not fail red for contributors who cannot (and should not) deploy.
|
|
93
|
+
if: vars.AWS_DEPLOY_ROLE_ARN != ''
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/checkout@v4
|
|
96
|
+
|
|
97
|
+
- uses: actions/setup-node@v4
|
|
98
|
+
with:
|
|
99
|
+
node-version: '${NODE_VERSION}'
|
|
100
|
+
- name: Install rastack
|
|
101
|
+
run: npm install -g rastack
|
|
102
|
+
|
|
103
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
104
|
+
- name: Cache cargo
|
|
105
|
+
uses: Swatinem/rust-cache@v2
|
|
106
|
+
with:
|
|
107
|
+
workspaces: rust
|
|
108
|
+
# cargo-lambda cross-builds the custom-runtime binary; it auto-installs
|
|
109
|
+
# zig (via pip) for the cross-compile when needed.
|
|
110
|
+
- name: Install cargo-lambda
|
|
111
|
+
run: pip3 install cargo-lambda
|
|
112
|
+
|
|
113
|
+
- name: Configure AWS credentials (OIDC — no stored keys)
|
|
114
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
115
|
+
with:
|
|
116
|
+
role-to-assume: \${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
|
117
|
+
aws-region: \${{ vars.AWS_REGION }}
|
|
118
|
+
|
|
119
|
+
# One line: compile the manifest, cross-build the bundle, bake the JWKS,
|
|
120
|
+
# and update-function-code. See \`rastack ci --plan deploy\`.
|
|
121
|
+
- name: Build & deploy
|
|
122
|
+
run: rastack ci deploy
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
/** The deploy config written to `.rastack/deploy.json`, pretty-printed. */
|
|
126
|
+
function renderConfigFile(cfg) {
|
|
127
|
+
return JSON.stringify(cfg, null, 2) + "\n";
|
|
128
|
+
}
|
|
129
|
+
exports.CONFIG_PATH = ".rastack/deploy.json";
|
|
130
|
+
exports.CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
131
|
+
exports.DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
|
|
132
|
+
/**
|
|
133
|
+
* The full set of files `rastack init` templates: the deploy config plus the two
|
|
134
|
+
* base workflows. Returned as data so the caller decides what to write, skip, or
|
|
135
|
+
* overwrite.
|
|
136
|
+
*/
|
|
137
|
+
function scaffoldFiles(cfg, opts = {}) {
|
|
138
|
+
const files = [
|
|
139
|
+
{ path: exports.CONFIG_PATH, content: renderConfigFile(cfg) },
|
|
140
|
+
];
|
|
141
|
+
if (!opts.noWorkflows) {
|
|
142
|
+
files.push({ path: exports.CI_WORKFLOW_PATH, content: renderCiWorkflow(cfg) }, { path: exports.DEPLOY_WORKFLOW_PATH, content: renderDeployWorkflow(cfg) });
|
|
143
|
+
}
|
|
144
|
+
return files;
|
|
145
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `rastack ci` — run the CI pipeline for a phase, so GitHub Actions stays a
|
|
4
|
+
* one-liner.
|
|
5
|
+
*
|
|
6
|
+
* rastack ci <check|build|deploy> [--plan] [--skip-build]
|
|
7
|
+
* [--function <name>] [--jwks-url <url>] [--bin <cmd>]
|
|
8
|
+
*
|
|
9
|
+
* check validate the resource graph + compile the manifest (a CI gate)
|
|
10
|
+
* build cross-build the Lambda bundle and bake the Cognito JWKS in
|
|
11
|
+
* deploy build (unless --skip-build) then `update-function-code`
|
|
12
|
+
*
|
|
13
|
+
* The steps are a pure function of `.rastack/deploy.json` (see
|
|
14
|
+
* tools/src/deploy/ci.ts); `--plan` prints them without running anything.
|
|
15
|
+
* Function name / JWKS URL come from the config's stack outputs when wired, else
|
|
16
|
+
* are resolved live from CloudFormation at run time.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const ci_1 = require("./deploy/ci");
|
|
22
|
+
const io_1 = require("./deploy/io");
|
|
23
|
+
const PHASES = ["check", "build", "deploy"];
|
|
24
|
+
function parseFlags(argv) {
|
|
25
|
+
let phase = "";
|
|
26
|
+
let plan = false;
|
|
27
|
+
const opts = {
|
|
28
|
+
bin: process.env.RASTACK_BIN || "rastack",
|
|
29
|
+
functionName: process.env.RASTACK_FUNCTION || undefined,
|
|
30
|
+
jwksUrl: process.env.RASTACK_JWKS_URL || undefined,
|
|
31
|
+
};
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
const next = () => argv[++i] ?? "";
|
|
35
|
+
switch (a) {
|
|
36
|
+
case "--plan":
|
|
37
|
+
case "--dry-run":
|
|
38
|
+
plan = true;
|
|
39
|
+
break;
|
|
40
|
+
case "--skip-build":
|
|
41
|
+
opts.skipBuild = true;
|
|
42
|
+
break;
|
|
43
|
+
case "--function":
|
|
44
|
+
opts.functionName = next();
|
|
45
|
+
break;
|
|
46
|
+
case "--jwks-url":
|
|
47
|
+
opts.jwksUrl = next();
|
|
48
|
+
break;
|
|
49
|
+
case "--bin":
|
|
50
|
+
opts.bin = next();
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
if (a.startsWith("-")) {
|
|
54
|
+
console.error(`Unknown flag: ${a}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
else if (!phase && PHASES.includes(a)) {
|
|
58
|
+
phase = a;
|
|
59
|
+
}
|
|
60
|
+
else if (!phase) {
|
|
61
|
+
console.error(`Unknown ci phase: "${a}" (expected ${PHASES.join(" | ")}).`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!phase) {
|
|
67
|
+
console.error(`Usage: rastack ci <${PHASES.join(" | ")}> [--plan] [--skip-build]\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return { phase, plan, opts };
|
|
71
|
+
}
|
|
72
|
+
function printPlan(phase, steps) {
|
|
73
|
+
console.log(`\n rastack ci ${phase} — ${steps.length} step(s):\n`);
|
|
74
|
+
steps.forEach((s, i) => {
|
|
75
|
+
const at = s.cwd ? ` (in ${s.cwd}/)` : "";
|
|
76
|
+
console.log(` ${i + 1}. ${s.name}${at}`);
|
|
77
|
+
console.log(` $ ${s.run}\n`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function runSteps(phase, steps) {
|
|
81
|
+
for (const step of steps) {
|
|
82
|
+
console.log(`\n▸ ${step.name}${step.cwd ? ` (in ${step.cwd}/)` : ""}`);
|
|
83
|
+
console.log(` $ ${step.run}`);
|
|
84
|
+
try {
|
|
85
|
+
// Runs a CI pipeline step derived from the developer's own deploy config
|
|
86
|
+
// — no untrusted input. A shell is required for the `$(aws …)` output
|
|
87
|
+
// substitutions the plan uses to resolve stack outputs at run time.
|
|
88
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
89
|
+
(0, child_process_1.execSync)(step.run, {
|
|
90
|
+
stdio: "inherit",
|
|
91
|
+
cwd: step.cwd,
|
|
92
|
+
env: process.env,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
console.error(`\n ✗ step failed: ${step.name}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`\n ✓ rastack ci ${phase} complete.`);
|
|
101
|
+
}
|
|
102
|
+
function main(argv) {
|
|
103
|
+
const { phase, plan, opts } = parseFlags(argv);
|
|
104
|
+
const cfg = (0, io_1.loadConfig)();
|
|
105
|
+
if (!cfg) {
|
|
106
|
+
console.error(" ✗ no .rastack/deploy.json found. Run `rastack init` (or `rastack deploy`) first.");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const steps = (0, ci_1.ciPlan)(phase, cfg, opts);
|
|
111
|
+
if (plan)
|
|
112
|
+
printPlan(phase, steps);
|
|
113
|
+
else
|
|
114
|
+
runSteps(phase, steps);
|
|
115
|
+
}
|
|
116
|
+
if (require.main === module) {
|
|
117
|
+
main(process.argv.slice(2));
|
|
118
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `rastack deploy` — configure a repo's serverless deployment.
|
|
4
|
+
*
|
|
5
|
+
* rastack deploy [--app <slug>] [--region <r>] [--owner <o>] [--repo <r>]
|
|
6
|
+
* [--branch <b>] [--namespace <n>] [--templates-bucket <b>]
|
|
7
|
+
* [--artifacts-bucket <b>] [--arch arm64|x86_64]
|
|
8
|
+
* [--print] [--run] [--yes]
|
|
9
|
+
*
|
|
10
|
+
* Writes/updates `.rastack/deploy.json` (creating it if `rastack init` has not
|
|
11
|
+
* run) and prints exactly how to stand the app up: the one-time
|
|
12
|
+
* `aws cloudformation deploy` that creates all infrastructure, the GitHub repo
|
|
13
|
+
* Variables CI reads, and the Lambda's runtime env. Infra is created once by a
|
|
14
|
+
* human — this command *configures* that step, it does not silently mutate the
|
|
15
|
+
* cloud. `--run` executes the launch command for you; `--print` is
|
|
16
|
+
* non-interactive (config as-is).
|
|
17
|
+
*
|
|
18
|
+
* The recurring code deploy is CI's job (`rastack ci deploy`), not this command.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
const child_process_1 = require("child_process");
|
|
23
|
+
const config_1 = require("./deploy/config");
|
|
24
|
+
const io_1 = require("./deploy/io");
|
|
25
|
+
function parseFlags(argv) {
|
|
26
|
+
const patch = { github: {}, lambda: {} };
|
|
27
|
+
let print = false;
|
|
28
|
+
let run = false;
|
|
29
|
+
let nonInteractive = false;
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
const next = () => argv[++i] ?? "";
|
|
33
|
+
switch (a) {
|
|
34
|
+
case "--app":
|
|
35
|
+
patch.app = next();
|
|
36
|
+
break;
|
|
37
|
+
case "--region":
|
|
38
|
+
patch.region = next();
|
|
39
|
+
break;
|
|
40
|
+
case "--owner":
|
|
41
|
+
patch.github.owner = next();
|
|
42
|
+
break;
|
|
43
|
+
case "--repo":
|
|
44
|
+
patch.github.repo = next();
|
|
45
|
+
break;
|
|
46
|
+
case "--branch":
|
|
47
|
+
patch.github.branch = next();
|
|
48
|
+
break;
|
|
49
|
+
case "--prefix":
|
|
50
|
+
patch.namePrefix = next();
|
|
51
|
+
break;
|
|
52
|
+
case "--namespace":
|
|
53
|
+
patch.namespace = next();
|
|
54
|
+
break;
|
|
55
|
+
case "--stack-name":
|
|
56
|
+
patch.stackName = next();
|
|
57
|
+
break;
|
|
58
|
+
case "--templates-bucket":
|
|
59
|
+
patch.templatesBucket = next();
|
|
60
|
+
break;
|
|
61
|
+
case "--artifacts-bucket":
|
|
62
|
+
patch.artifactsBucket = next();
|
|
63
|
+
break;
|
|
64
|
+
case "--arch":
|
|
65
|
+
patch.lambda.arch = next();
|
|
66
|
+
break;
|
|
67
|
+
case "--print":
|
|
68
|
+
print = true;
|
|
69
|
+
nonInteractive = true;
|
|
70
|
+
break;
|
|
71
|
+
case "--run":
|
|
72
|
+
run = true;
|
|
73
|
+
break;
|
|
74
|
+
case "--yes":
|
|
75
|
+
case "-y":
|
|
76
|
+
case "--non-interactive":
|
|
77
|
+
nonInteractive = true;
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
if (a.startsWith("-")) {
|
|
81
|
+
console.error(`Unknown flag: ${a}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { patch, print, run, nonInteractive };
|
|
87
|
+
}
|
|
88
|
+
function seedConfig(patch) {
|
|
89
|
+
const existing = (0, io_1.loadConfig)();
|
|
90
|
+
return existing ? (0, config_1.mergeConfig)(existing, patch) : (0, config_1.normalizeConfig)(patch);
|
|
91
|
+
}
|
|
92
|
+
async function promptMissing(cfg) {
|
|
93
|
+
return (0, io_1.withPrompt)(async (rl) => {
|
|
94
|
+
const app = await (0, io_1.ask)(rl, "App slug", cfg.app);
|
|
95
|
+
const owner = await (0, io_1.ask)(rl, "GitHub owner", cfg.github.owner);
|
|
96
|
+
const repo = await (0, io_1.ask)(rl, "GitHub repo", cfg.github.repo);
|
|
97
|
+
const branch = await (0, io_1.ask)(rl, "Deploy branch", cfg.github.branch);
|
|
98
|
+
const region = await (0, io_1.ask)(rl, "AWS region", cfg.region);
|
|
99
|
+
const namespace = await (0, io_1.ask)(rl, "Iceberg namespace", cfg.namespace);
|
|
100
|
+
const artifactsBucket = await (0, io_1.ask)(rl, "Artifacts bucket (from bootstrap stack)", cfg.artifactsBucket ?? "");
|
|
101
|
+
const templatesBucket = await (0, io_1.ask)(rl, "Templates bucket (from bootstrap stack, optional)", cfg.templatesBucket ?? "");
|
|
102
|
+
return (0, config_1.mergeConfig)(cfg, {
|
|
103
|
+
app,
|
|
104
|
+
region,
|
|
105
|
+
namespace,
|
|
106
|
+
artifactsBucket,
|
|
107
|
+
templatesBucket,
|
|
108
|
+
github: { owner, repo, branch },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Pad a list of `Key=Value` rows into an aligned table. */
|
|
113
|
+
function table(rows) {
|
|
114
|
+
const width = Math.max(0, ...rows.map((r) => r.key.length));
|
|
115
|
+
return rows.map((r) => ` ${r.key.padEnd(width)} ${r.value}`).join("\n");
|
|
116
|
+
}
|
|
117
|
+
function printSummary(cfg) {
|
|
118
|
+
const src = (0, config_1.templateUrl)(cfg);
|
|
119
|
+
console.log(`\n Deployment config (${cfg.stackName} @ ${cfg.region})\n`);
|
|
120
|
+
console.log(" ── 1. Create the infrastructure (once, by a human) ──");
|
|
121
|
+
if (!cfg.artifactsBucket) {
|
|
122
|
+
console.log(" ⚠ artifactsBucket is unset — app.yaml requires it. Run the bootstrap\n" +
|
|
123
|
+
" stack first, then set --artifacts-bucket (its ArtifactsBucketName output).");
|
|
124
|
+
}
|
|
125
|
+
if (!src) {
|
|
126
|
+
console.log(" (launching from the in-repo template; set --templates-bucket to launch\n" +
|
|
127
|
+
" the hosted app.yaml by URL instead)");
|
|
128
|
+
}
|
|
129
|
+
console.log("\n" + (0, config_1.deployStackCommand)(cfg) + "\n");
|
|
130
|
+
console.log(" ── 2. Wire the GitHub repo Variables (from the stack Outputs) ──");
|
|
131
|
+
console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
|
|
132
|
+
key: v.name,
|
|
133
|
+
value: v.value || `<${v.from}>`,
|
|
134
|
+
}))));
|
|
135
|
+
console.log("\n ── 3. The Lambda runtime env (set by the stack) ──");
|
|
136
|
+
console.log(table((0, config_1.lambdaEnv)(cfg)));
|
|
137
|
+
console.log(`\n ── 4. Then every push to ${cfg.github.branch} runs \`rastack ci deploy\`. ──\n`);
|
|
138
|
+
}
|
|
139
|
+
function runLaunch(cfg) {
|
|
140
|
+
const params = (0, config_1.stackParameters)(cfg).map((p) => `${p.key}=${p.value}`);
|
|
141
|
+
const src = (0, config_1.templateUrl)(cfg);
|
|
142
|
+
const args = [
|
|
143
|
+
"cloudformation",
|
|
144
|
+
"deploy",
|
|
145
|
+
...(src
|
|
146
|
+
? ["--template-url", src]
|
|
147
|
+
: ["--template-file", "deploy/cloudformation/app.yaml"]),
|
|
148
|
+
"--stack-name",
|
|
149
|
+
cfg.stackName,
|
|
150
|
+
"--capabilities",
|
|
151
|
+
"CAPABILITY_NAMED_IAM",
|
|
152
|
+
"--region",
|
|
153
|
+
cfg.region,
|
|
154
|
+
"--parameter-overrides",
|
|
155
|
+
...params,
|
|
156
|
+
];
|
|
157
|
+
console.log(`\n ▸ aws ${args.join(" ")}\n`);
|
|
158
|
+
try {
|
|
159
|
+
// Runs the developer's own AWS CLI with args built from their config — no
|
|
160
|
+
// untrusted input, and `execFileSync` (no shell) avoids any injection.
|
|
161
|
+
(0, child_process_1.execFileSync)("aws", args, { stdio: "inherit" });
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.error(`\n ✗ aws cloudformation deploy failed. Is the AWS CLI installed and are` +
|
|
165
|
+
` your credentials configured?\n ${err.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function main(argv) {
|
|
170
|
+
const flags = parseFlags(argv);
|
|
171
|
+
let cfg = seedConfig(flags.patch);
|
|
172
|
+
if ((0, io_1.canPrompt)(flags.nonInteractive)) {
|
|
173
|
+
console.log("\n rastack deploy — configure the serverless deployment\n");
|
|
174
|
+
cfg = await promptMissing(cfg);
|
|
175
|
+
}
|
|
176
|
+
const errors = (0, config_1.validateConfig)(cfg);
|
|
177
|
+
if (errors.length) {
|
|
178
|
+
for (const e of errors)
|
|
179
|
+
console.error(` ✗ ${e}`);
|
|
180
|
+
console.error(`\n ${errors.length} problem(s) — fix the config and re-run.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
if (!flags.print) {
|
|
184
|
+
const file = (0, io_1.saveConfig)(cfg);
|
|
185
|
+
console.log(` ✓ saved ${file}`);
|
|
186
|
+
}
|
|
187
|
+
printSummary(cfg);
|
|
188
|
+
if (flags.run)
|
|
189
|
+
runLaunch(cfg);
|
|
190
|
+
}
|
|
191
|
+
if (require.main === module) {
|
|
192
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
193
|
+
console.error(` ✗ ${err.message}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `rastack init` — template the base GitHub Actions + deploy config into a repo.
|
|
4
|
+
*
|
|
5
|
+
* rastack init [--app <slug>] [--owner <o>] [--repo <r>] [--branch <b>]
|
|
6
|
+
* [--region <r>] [--force] [--no-workflows] [--yes]
|
|
7
|
+
*
|
|
8
|
+
* Scaffolds a new project (or reconfigures an existing one) so it is ready to
|
|
9
|
+
* deploy: `.rastack/deploy.json` plus `.github/workflows/{ci,deploy-backend}.yml`,
|
|
10
|
+
* both of which run `rastack ci`. GitHub coordinates default from the repo's
|
|
11
|
+
* `origin` remote; missing fields are prompted for on a TTY. Existing files are
|
|
12
|
+
* left untouched unless `--force`.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|