rastack 0.0.61 → 0.0.63

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/dist/deploy/io.js CHANGED
@@ -53,6 +53,7 @@ exports.ghAvailable = ghAvailable;
53
53
  exports.ghAuthenticated = ghAuthenticated;
54
54
  exports.ghAuthLogin = ghAuthLogin;
55
55
  exports.setRepoVariable = setRepoVariable;
56
+ exports.ghEnvironmentCreate = ghEnvironmentCreate;
56
57
  exports.ask = ask;
57
58
  exports.withPrompt = withPrompt;
58
59
  const child_process_1 = require("child_process");
@@ -278,6 +279,22 @@ function setRepoVariable(args, cwd = process.cwd()) {
278
279
  return false;
279
280
  }
280
281
  }
282
+ /**
283
+ * Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
284
+ * so its scoped Variables can be set. Returns true on success. Best-effort — a
285
+ * missing/unauthenticated `gh` or an API error degrades to the printed
286
+ * instructions, exactly like `setRepoVariable`.
287
+ */
288
+ function ghEnvironmentCreate(args, cwd = process.cwd()) {
289
+ try {
290
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
291
+ (0, child_process_1.execFileSync)("gh", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
292
+ return true;
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
281
298
  /**
282
299
  * Ask a single question with a default, echoed as `label [default]: `. Returns
283
300
  * the trimmed answer, or the default on an empty line.
@@ -34,6 +34,36 @@ export declare function renderConfigFile(cfg: DeployConfig): string;
34
34
  export declare const CONFIG_PATH = ".rastack/deploy.json";
35
35
  export declare const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
36
36
  export declare const DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
37
+ export declare const ENV_PATH = ".env";
38
+ export declare const GITIGNORE_PATH = ".gitignore";
39
+ /**
40
+ * The generated `.env` — the app's **local** environment. Every value is
41
+ * non-secret (client ids, pool ids, public URLs from the stack Outputs), but the
42
+ * file is git-ignored by convention (`gitignoreWith`) so a developer can add
43
+ * their own secrets beside these without ever committing them. Values are blank
44
+ * until the matching stack is deployed; `rastack init` rewrites the file with the
45
+ * resolved Outputs after standing the backends up.
46
+ *
47
+ * Keys are `RASTACK_<ENV>_*` (a neutral prefix). For a browser bundle, re-export
48
+ * the ones you need under your bundler's public prefix (EXPO_PUBLIC_/VITE_/…);
49
+ * `rastack dev` also serves the dev pool config live at `/__rastack/cognito.json`.
50
+ */
51
+ export declare function renderEnvFile(cfg: DeployConfig): string;
52
+ /**
53
+ * Upsert the generated `RASTACK_*` keys from `renderEnvFile(cfg)` into an
54
+ * existing `.env`, preserving every other line (a developer's own secrets,
55
+ * comments, extra keys). A matching `RASTACK_*=` line is updated in place;
56
+ * generated keys not yet present are appended. When there is no existing file,
57
+ * this is exactly `renderEnvFile(cfg)`. Pure — the IO wrapper reads/writes.
58
+ */
59
+ export declare function applyEnvValues(existing: string, cfg: DeployConfig): string;
60
+ /**
61
+ * Return `.gitignore` content with a `.env` entry present (idempotent). Appends
62
+ * under a labelled block when missing, leaves the file untouched when `.env` is
63
+ * already ignored — so `rastack init` never duplicates the entry or clobbers a
64
+ * developer's existing ignore rules.
65
+ */
66
+ export declare function gitignoreWith(existing: string, entry?: string): string;
37
67
  export interface ScaffoldOptions {
38
68
  /** Omit the GitHub Actions workflows (config only). */
39
69
  noWorkflows?: boolean;
@@ -12,11 +12,15 @@
12
12
  * Pure string builders — `rastack-init.ts` is the thin wrapper that writes them.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.DEPLOY_WORKFLOW_PATH = exports.CI_WORKFLOW_PATH = exports.CONFIG_PATH = void 0;
15
+ exports.GITIGNORE_PATH = exports.ENV_PATH = exports.DEPLOY_WORKFLOW_PATH = exports.CI_WORKFLOW_PATH = exports.CONFIG_PATH = void 0;
16
16
  exports.renderCiWorkflow = renderCiWorkflow;
17
17
  exports.renderDeployWorkflow = renderDeployWorkflow;
18
18
  exports.renderConfigFile = renderConfigFile;
19
+ exports.renderEnvFile = renderEnvFile;
20
+ exports.applyEnvValues = applyEnvValues;
21
+ exports.gitignoreWith = gitignoreWith;
19
22
  exports.scaffoldFiles = scaffoldFiles;
23
+ const config_1 = require("./config");
20
24
  /** Node version the templated workflows pin (LTS — safe for a fresh repo). */
21
25
  const NODE_VERSION = "22";
22
26
  /**
@@ -24,6 +28,8 @@ const NODE_VERSION = "22";
24
28
  * every push/PR. The clean replacement for hand-written compile/check steps.
25
29
  */
26
30
  function renderCiWorkflow(cfg) {
31
+ const prod = (0, config_1.resolveEnvironment)(cfg, "prod");
32
+ const dev = (0, config_1.resolveEnvironment)(cfg, "dev");
27
33
  return `name: CI
28
34
 
29
35
  # Validate the resource graph and compile the manifest on every change.
@@ -32,7 +38,7 @@ function renderCiWorkflow(cfg) {
32
38
 
33
39
  on:
34
40
  push:
35
- branches: [${cfg.github.branch}]
41
+ branches: [${prod.branch}, ${dev.branch}]
36
42
  pull_request:
37
43
 
38
44
  permissions:
@@ -61,19 +67,28 @@ jobs:
61
67
  */
62
68
  function renderDeployWorkflow(cfg) {
63
69
  const arch = cfg.lambda.arch === "arm64" ? "arm64" : "x86_64";
70
+ const prod = (0, config_1.resolveEnvironment)(cfg, "prod");
71
+ const dev = (0, config_1.resolveEnvironment)(cfg, "dev");
72
+ // Pick the environment (and rastack --env flag) from the pushed branch.
73
+ const ghEnv = `\${{ github.ref_name == '${prod.branch}' && '${prod.ghEnvironment}' || '${dev.ghEnvironment}' }}`;
74
+ const envFlag = `\${{ github.ref_name == '${prod.branch}' && 'prod' || 'dev' }}`;
64
75
  return `name: Deploy backend
65
76
 
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
77
+ # Ship the Rust API (rastack-server) to AWS Lambda — dev and prod. Infrastructure
78
+ # is created once by \`rastack init\` (docs/serverless-deploy.md); this workflow
68
79
  # only ever runs \`aws lambda update-function-code\`, wrapped by \`rastack ci\`.
69
80
  #
70
- # Wire these from the CloudFormation stack Outputs as GitHub repo Variables:
81
+ # The environment is chosen from the branch:
82
+ # ${dev.branch} → ${dev.ghEnvironment} (dev stack) ${prod.branch} → ${prod.ghEnvironment} (prod stack)
83
+ #
84
+ # Each GitHub Actions Environment holds its own AWS Variables (wired by
85
+ # \`rastack init\` from that environment's stack Outputs):
71
86
  # AWS_DEPLOY_ROLE_ARN DeployRoleArn (the OIDC role this job assumes)
72
87
  # AWS_REGION Region (${cfg.region})
73
88
 
74
89
  on:
75
90
  push:
76
- branches: [${cfg.github.branch}]
91
+ branches: [${prod.branch}, ${dev.branch}]
77
92
 
78
93
  # Least privilege: read the repo + mint an OIDC token to assume the deploy role.
79
94
  permissions:
@@ -81,15 +96,20 @@ permissions:
81
96
  id-token: write
82
97
 
83
98
  concurrency:
84
- group: deploy-backend
99
+ # One in-flight deploy per branch, so dev and prod never cancel each other.
100
+ group: deploy-backend-\${{ github.ref_name }}
85
101
  cancel-in-progress: true
86
102
 
87
103
  jobs:
88
104
  deploy:
89
105
  name: Build & deploy the Lambda (${arch})
90
106
  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.
107
+ # Select the GitHub Environment from the branch. Its scoped Variables
108
+ # (AWS_DEPLOY_ROLE_ARN / AWS_REGION) then resolve to the right stack, and
109
+ # prod protection rules (if any) gate the run.
110
+ environment: ${ghEnv}
111
+ # Skip on forks / environments that never wired the deploy role, so the job
112
+ # does not fail red for contributors who cannot (and should not) deploy.
93
113
  if: vars.AWS_DEPLOY_ROLE_ARN != ''
94
114
  steps:
95
115
  - uses: actions/checkout@v4
@@ -116,10 +136,10 @@ jobs:
116
136
  role-to-assume: \${{ vars.AWS_DEPLOY_ROLE_ARN }}
117
137
  aws-region: \${{ vars.AWS_REGION }}
118
138
 
119
- # One line: compile the manifest, cross-build the bundle, bake the JWKS,
120
- # and update-function-code. See \`rastack ci --plan deploy\`.
139
+ # One line: compile the manifest, cross-build the bundle, bake the env's
140
+ # Cognito JWKS, and update-function-code for that env's function.
121
141
  - name: Build & deploy
122
- run: rastack ci deploy
142
+ run: rastack ci deploy --env ${envFlag}
123
143
  `;
124
144
  }
125
145
  /** The deploy config written to `.rastack/deploy.json`, pretty-printed. */
@@ -129,6 +149,91 @@ function renderConfigFile(cfg) {
129
149
  exports.CONFIG_PATH = ".rastack/deploy.json";
130
150
  exports.CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
131
151
  exports.DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
152
+ exports.ENV_PATH = ".env";
153
+ exports.GITIGNORE_PATH = ".gitignore";
154
+ /**
155
+ * The generated `.env` — the app's **local** environment. Every value is
156
+ * non-secret (client ids, pool ids, public URLs from the stack Outputs), but the
157
+ * file is git-ignored by convention (`gitignoreWith`) so a developer can add
158
+ * their own secrets beside these without ever committing them. Values are blank
159
+ * until the matching stack is deployed; `rastack init` rewrites the file with the
160
+ * resolved Outputs after standing the backends up.
161
+ *
162
+ * Keys are `RASTACK_<ENV>_*` (a neutral prefix). For a browser bundle, re-export
163
+ * the ones you need under your bundler's public prefix (EXPO_PUBLIC_/VITE_/…);
164
+ * `rastack dev` also serves the dev pool config live at `/__rastack/cognito.json`.
165
+ */
166
+ function renderEnvFile(cfg) {
167
+ const lines = [
168
+ "# Generated by `rastack init` — local app environment (git-ignored).",
169
+ "# Non-secret values from the CloudFormation stack Outputs; add your own",
170
+ "# secrets below. Re-run `rastack init` to refresh after a deploy.",
171
+ "",
172
+ `RASTACK_APP=${cfg.app}`,
173
+ `RASTACK_REGION=${cfg.region}`,
174
+ "",
175
+ ];
176
+ const envKeys = [
177
+ ["dev", "DEV"],
178
+ ["prod", "PROD"],
179
+ ];
180
+ for (const [env, prefix] of envKeys) {
181
+ const e = (0, config_1.resolveEnvironment)(cfg, env);
182
+ const o = e.outputs ?? {};
183
+ lines.push(`# ${env} (${e.branch} → ${e.ghEnvironment})`, `RASTACK_${prefix}_API_URL=${o.apiUrl ?? ""}`, `RASTACK_${prefix}_USER_POOL_ID=${o.userPoolId ?? ""}`, `RASTACK_${prefix}_USER_POOL_CLIENT_ID=${o.userPoolClientId ?? ""}`, `RASTACK_${prefix}_HOSTED_UI_URL=${o.hostedUiUrl ?? ""}`, `RASTACK_${prefix}_PROXY_URL=${o.proxyUrl ?? ""}`, "");
184
+ }
185
+ return lines.join("\n");
186
+ }
187
+ /**
188
+ * Upsert the generated `RASTACK_*` keys from `renderEnvFile(cfg)` into an
189
+ * existing `.env`, preserving every other line (a developer's own secrets,
190
+ * comments, extra keys). A matching `RASTACK_*=` line is updated in place;
191
+ * generated keys not yet present are appended. When there is no existing file,
192
+ * this is exactly `renderEnvFile(cfg)`. Pure — the IO wrapper reads/writes.
193
+ */
194
+ function applyEnvValues(existing, cfg) {
195
+ const rendered = renderEnvFile(cfg);
196
+ if (!existing.trim())
197
+ return rendered;
198
+ const generated = new Map();
199
+ for (const line of rendered.split("\n")) {
200
+ const m = /^(RASTACK_[A-Z0-9_]+)=(.*)$/.exec(line);
201
+ if (m)
202
+ generated.set(m[1], m[2]);
203
+ }
204
+ const seen = new Set();
205
+ const out = existing.split("\n").map((line) => {
206
+ const m = /^(RASTACK_[A-Z0-9_]+)=/.exec(line);
207
+ if (m && generated.has(m[1])) {
208
+ seen.add(m[1]);
209
+ return `${m[1]}=${generated.get(m[1])}`;
210
+ }
211
+ return line;
212
+ });
213
+ const missing = [...generated].filter(([k]) => !seen.has(k));
214
+ if (missing.length) {
215
+ if (out.length && out[out.length - 1].trim() !== "")
216
+ out.push("");
217
+ for (const [k, v] of missing)
218
+ out.push(`${k}=${v}`);
219
+ out.push("");
220
+ }
221
+ return out.join("\n");
222
+ }
223
+ /**
224
+ * Return `.gitignore` content with a `.env` entry present (idempotent). Appends
225
+ * under a labelled block when missing, leaves the file untouched when `.env` is
226
+ * already ignored — so `rastack init` never duplicates the entry or clobbers a
227
+ * developer's existing ignore rules.
228
+ */
229
+ function gitignoreWith(existing, entry = ".env") {
230
+ const lines = existing.split("\n").map((l) => l.trim());
231
+ if (lines.includes(entry))
232
+ return existing;
233
+ const base = existing.replace(/\n+$/, "");
234
+ const block = `# rastack — local environment (secrets)\n${entry}\n`;
235
+ return base ? `${base}\n\n${block}` : block;
236
+ }
132
237
  /**
133
238
  * The full set of files `rastack init` templates: the deploy config plus the two
134
239
  * base workflows. Returned as data so the caller decides what to write, skip, or
@@ -19,9 +19,17 @@
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  const child_process_1 = require("child_process");
21
21
  const compile_1 = require("./compile");
22
+ const config_1 = require("./deploy/config");
22
23
  const ci_1 = require("./deploy/ci");
23
24
  const io_1 = require("./deploy/io");
24
25
  const PHASES = ["check", "build", "deploy"];
26
+ /** Coerce a string to an `EnvName`, or undefined when unset/unrecognised. */
27
+ function parseEnv(value) {
28
+ const v = (value ?? "").trim();
29
+ return config_1.ENV_NAMES.includes(v)
30
+ ? v
31
+ : undefined;
32
+ }
25
33
  function parseFlags(argv) {
26
34
  let phase = "";
27
35
  let plan = false;
@@ -29,6 +37,7 @@ function parseFlags(argv) {
29
37
  bin: process.env.RASTACK_BIN || "rastack",
30
38
  functionName: process.env.RASTACK_FUNCTION || undefined,
31
39
  jwksUrl: process.env.RASTACK_JWKS_URL || undefined,
40
+ env: parseEnv(process.env.RASTACK_ENV),
32
41
  };
33
42
  for (let i = 0; i < argv.length; i++) {
34
43
  const a = argv[i];
@@ -47,6 +56,15 @@ function parseFlags(argv) {
47
56
  case "--jwks-url":
48
57
  opts.jwksUrl = next();
49
58
  break;
59
+ case "--env": {
60
+ const env = parseEnv(next());
61
+ if (!env) {
62
+ console.error(`Unknown --env: expected ${config_1.ENV_NAMES.join(" | ")}.`);
63
+ process.exit(1);
64
+ }
65
+ opts.env = env;
66
+ break;
67
+ }
50
68
  case "--bin":
51
69
  opts.bin = next();
52
70
  break;
@@ -65,7 +83,7 @@ function parseFlags(argv) {
65
83
  }
66
84
  }
67
85
  if (!phase) {
68
- console.error(`Usage: rastack ci <${PHASES.join(" | ")}> [--plan] [--skip-build]\n`);
86
+ console.error(`Usage: rastack ci <${PHASES.join(" | ")}> [--env dev|prod] [--plan] [--skip-build]\n`);
69
87
  process.exit(1);
70
88
  }
71
89
  return { phase, plan, opts };
@@ -60,6 +60,8 @@ const http = __importStar(require("http"));
60
60
  const net = __importStar(require("net"));
61
61
  const path = __importStar(require("path"));
62
62
  const compile_1 = require("./compile");
63
+ const config_1 = require("./deploy/config");
64
+ const io_1 = require("./deploy/io");
63
65
  const harness_1 = require("./dev/harness");
64
66
  const HERE = __dirname; // dist/ when installed
65
67
  /** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
@@ -131,6 +133,38 @@ function serveFile(res, file) {
131
133
  res.writeHead(200, { "Content-Type": (0, harness_1.contentType)(file) }).end(data);
132
134
  });
133
135
  }
136
+ /**
137
+ * Serve the dev environment's Cognito coordinates (`/__rastack/cognito.json`) so
138
+ * a locally-running app can sign in against the *real* dev user pool. Read from
139
+ * `.rastack/deploy.json`'s dev outputs; `{}` when the dev stack isn't deployed
140
+ * yet (the app then falls back to `mode:"local"`). Returns whether a real config
141
+ * was found, for the startup hint.
142
+ */
143
+ function serveDevCognito(res, cwd) {
144
+ let body = "{}";
145
+ try {
146
+ const cfg = (0, io_1.loadConfig)(cwd);
147
+ const runtime = cfg ? (0, config_1.cognitoRuntimeConfig)(cfg, "dev") : null;
148
+ if (runtime)
149
+ body = JSON.stringify(runtime);
150
+ }
151
+ catch {
152
+ /* no/invalid config → empty object, app uses the local shim */
153
+ }
154
+ res
155
+ .writeHead(200, { "Content-Type": "application/json; charset=utf-8" })
156
+ .end(body);
157
+ }
158
+ /** Whether a real dev Cognito config is available to advertise on startup. */
159
+ function hasDevCognito(cwd) {
160
+ try {
161
+ const cfg = (0, io_1.loadConfig)(cwd);
162
+ return Boolean(cfg && (0, config_1.cognitoRuntimeConfig)(cfg, "dev"));
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
134
168
  /** Best-effort open the default browser at `url`. */
135
169
  function openBrowser(url) {
136
170
  const cmd = process.platform === "darwin"
@@ -190,7 +224,7 @@ function getFreePort() {
190
224
  srv.listen(0, "127.0.0.1", () => {
191
225
  const addr = srv.address();
192
226
  const port = addr && typeof addr === "object" ? addr.port : 0;
193
- srv.close(() => (port ? resolve(port) : reject(new Error("no free port"))));
227
+ srv.close(() => port ? resolve(port) : reject(new Error("no free port")));
194
228
  });
195
229
  });
196
230
  }
@@ -240,7 +274,13 @@ function waitForMetro(port, timeoutMs = 120000) {
240
274
  }
241
275
  /** Reverse-proxy one HTTP request/response pair to Metro. */
242
276
  function proxyToMetro(req, res, port) {
243
- const proxyReq = http.request({ host: "127.0.0.1", port, method: req.method, path: req.url, headers: req.headers }, (proxyRes) => {
277
+ const proxyReq = http.request({
278
+ host: "127.0.0.1",
279
+ port,
280
+ method: req.method,
281
+ path: req.url,
282
+ headers: req.headers,
283
+ }, (proxyRes) => {
244
284
  res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
245
285
  proxyRes.pipe(res);
246
286
  });
@@ -310,6 +350,8 @@ async function runMetroMode(args, cwd) {
310
350
  return serveUnder(res, wasmDir, url.slice("/wasm/".length));
311
351
  if (url === "/schema.rastack.json" || url === "/openapi.json")
312
352
  return serveUnder(res, args.outDir, path.basename(url));
353
+ if (url === "/__rastack/cognito.json")
354
+ return serveDevCognito(res, cwd);
313
355
  if (url.startsWith("/data/warehouse/"))
314
356
  return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
315
357
  // Everything else → the live Metro bundle (HTML, JS, assets, HMR polls).
@@ -372,6 +414,8 @@ async function main(argv) {
372
414
  return serveUnder(res, wasmDir, url.slice("/wasm/".length));
373
415
  if (url === "/schema.rastack.json" || url === "/openapi.json")
374
416
  return serveUnder(res, args.outDir, path.basename(url));
417
+ if (url === "/__rastack/cognito.json")
418
+ return serveDevCognito(res, cwd);
375
419
  if (url.startsWith("/data/warehouse/"))
376
420
  return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
377
421
  // No app → the Django-style welcome page (root only; else 404).
@@ -404,6 +448,8 @@ async function main(argv) {
404
448
  console.log(` ✓ app: ${path.relative(process.cwd(), appRoot) || "."}`);
405
449
  else
406
450
  console.log(` ○ no app found — serving the welcome page`);
451
+ if (hasDevCognito(cwd))
452
+ console.log(` ✓ dev Cognito: /__rastack/cognito.json (sign in against the real dev pool)`);
407
453
  console.log(`\n ▸ ${url}\n`);
408
454
  console.log(" Need the database console? Run `rastack admin`. Ctrl+C to stop.\n");
409
455
  if (args.open)