rastack 0.0.62 → 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/CHANGELOG.md +7 -0
- package/auth/provider.ts +71 -8
- package/dist/admin.js +9 -9
- package/dist/deploy/ci.d.ts +10 -3
- package/dist/deploy/ci.js +22 -11
- package/dist/deploy/config.d.ts +155 -5
- package/dist/deploy/config.js +216 -6
- package/dist/deploy/io.d.ts +7 -0
- package/dist/deploy/io.js +17 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.js +190 -64
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +17 -1
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +235 -76
- package/src/rastack.ts +1 -0
package/src/deploy/ci.ts
CHANGED
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
* the same plan works before and after they are wired into the config.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DeployConfig,
|
|
21
|
+
DeployOutputs,
|
|
22
|
+
EnvName,
|
|
23
|
+
resolveEnvironment,
|
|
24
|
+
} from "./config";
|
|
20
25
|
|
|
21
26
|
export type CiPhase = "check" | "build" | "deploy";
|
|
22
27
|
|
|
@@ -55,27 +60,47 @@ export interface CiPlanOptions {
|
|
|
55
60
|
bin?: string;
|
|
56
61
|
/** Skip the build steps in `deploy` (code already built this job). */
|
|
57
62
|
skipBuild?: boolean;
|
|
58
|
-
/** Override the function name (else from
|
|
63
|
+
/** Override the function name (else from the env's outputs, else resolved live). */
|
|
59
64
|
functionName?: string;
|
|
60
|
-
/** Override the JWKS URL (else from
|
|
65
|
+
/** Override the JWKS URL (else from the env's outputs, else resolved live). */
|
|
61
66
|
jwksUrl?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Which environment (dev/prod) to build+ship. When set, the stack name,
|
|
69
|
+
* outputs, and app label come from that environment — so `rastack ci deploy
|
|
70
|
+
* --env dev` targets the dev stack (dev's Cognito JWKS, dev's function). When
|
|
71
|
+
* unset the legacy single-stack fields (`cfg.stackName` / `cfg.outputs`) apply.
|
|
72
|
+
*/
|
|
73
|
+
env?: EnvName;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The stack name + outputs + app label the plan targets, honouring `opts.env`. */
|
|
77
|
+
function ciTarget(
|
|
78
|
+
cfg: DeployConfig,
|
|
79
|
+
opts: CiPlanOptions,
|
|
80
|
+
): { stackName: string; outputs?: DeployOutputs; app: string } {
|
|
81
|
+
if (opts.env) {
|
|
82
|
+
const e = resolveEnvironment(cfg, opts.env);
|
|
83
|
+
return { stackName: e.stackName, outputs: e.outputs, app: e.appName };
|
|
84
|
+
}
|
|
85
|
+
return { stackName: cfg.stackName, outputs: cfg.outputs, app: cfg.app };
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
/**
|
|
65
|
-
* A stack Output value: the literal
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
89
|
+
* A stack Output value: the literal if wired, else a shell command substitution
|
|
90
|
+
* that resolves it live from CloudFormation. Keeping the fallback inline (rather
|
|
91
|
+
* than a separate step) means the plan is a flat list of self-contained commands
|
|
92
|
+
* that read correctly in any order.
|
|
69
93
|
*/
|
|
70
94
|
function stackOutput(
|
|
71
|
-
|
|
95
|
+
stackName: string,
|
|
96
|
+
region: string,
|
|
72
97
|
outputKey: string,
|
|
73
98
|
literal?: string,
|
|
74
99
|
): string {
|
|
75
100
|
if (literal) return literal;
|
|
76
101
|
return (
|
|
77
|
-
`$(aws cloudformation describe-stacks --stack-name ${
|
|
78
|
-
`--region ${
|
|
102
|
+
`$(aws cloudformation describe-stacks --stack-name ${stackName} ` +
|
|
103
|
+
`--region ${region} ` +
|
|
79
104
|
`--query "Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue" ` +
|
|
80
105
|
`--output text)`
|
|
81
106
|
);
|
|
@@ -107,11 +132,13 @@ export function buildSteps(
|
|
|
107
132
|
opts: CiPlanOptions = {},
|
|
108
133
|
): CiStep[] {
|
|
109
134
|
const stage = stageDir(cfg);
|
|
135
|
+
const target = ciTarget(cfg, opts);
|
|
110
136
|
const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
|
|
111
137
|
const jwks = stackOutput(
|
|
112
|
-
|
|
138
|
+
target.stackName,
|
|
139
|
+
cfg.region,
|
|
113
140
|
"JwksUrl",
|
|
114
|
-
opts.jwksUrl ??
|
|
141
|
+
opts.jwksUrl ?? target.outputs?.jwksUrl,
|
|
115
142
|
);
|
|
116
143
|
return [
|
|
117
144
|
compileStep(cfg),
|
|
@@ -146,12 +173,19 @@ export function deploySteps(
|
|
|
146
173
|
): CiStep[] {
|
|
147
174
|
const stage = stageDir(cfg);
|
|
148
175
|
const zip = `${stage}/bootstrap.zip`;
|
|
176
|
+
const target = ciTarget(cfg, opts);
|
|
149
177
|
const fn = stackOutput(
|
|
150
|
-
|
|
178
|
+
target.stackName,
|
|
179
|
+
cfg.region,
|
|
151
180
|
"FunctionName",
|
|
152
|
-
opts.functionName ??
|
|
181
|
+
opts.functionName ?? target.outputs?.functionName,
|
|
182
|
+
);
|
|
183
|
+
const apiUrl = stackOutput(
|
|
184
|
+
target.stackName,
|
|
185
|
+
cfg.region,
|
|
186
|
+
"ApiUrl",
|
|
187
|
+
target.outputs?.apiUrl,
|
|
153
188
|
);
|
|
154
|
-
const apiUrl = stackOutput(cfg, "ApiUrl", cfg.outputs?.apiUrl);
|
|
155
189
|
const steps: CiStep[] = opts.skipBuild ? [] : buildSteps(cfg, opts);
|
|
156
190
|
steps.push(
|
|
157
191
|
{
|
|
@@ -167,7 +201,7 @@ export function deploySteps(
|
|
|
167
201
|
},
|
|
168
202
|
{
|
|
169
203
|
name: "Report the API URL",
|
|
170
|
-
run: `echo "Deployed ${
|
|
204
|
+
run: `echo "Deployed ${target.app} → ${apiUrl}"`,
|
|
171
205
|
},
|
|
172
206
|
);
|
|
173
207
|
return steps;
|
package/src/deploy/config.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface DeployOutputs {
|
|
|
33
33
|
jwksUrl?: string;
|
|
34
34
|
/** S3 Iceberg warehouse bucket — the system of record. */
|
|
35
35
|
warehouseBucket?: string;
|
|
36
|
+
/** Cognito Hosted UI domain (scheme-less), for browser sign-in. */
|
|
37
|
+
userPoolDomain?: string;
|
|
38
|
+
/** Full Cognito Hosted UI base URL (`https://…`). */
|
|
39
|
+
hostedUiUrl?: string;
|
|
40
|
+
/** Base URL of the external-API proxy route, when the env provisions one. */
|
|
41
|
+
proxyUrl?: string;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export interface LambdaBuild {
|
|
@@ -44,6 +50,36 @@ export interface LambdaBuild {
|
|
|
44
50
|
features: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Per-environment overrides. The **prod** environment defaults its values from
|
|
55
|
+
* the top-level config (its branch is `github.branch`, its stack is `stackName`,
|
|
56
|
+
* its outputs are the top-level `outputs`); the **dev** environment defaults to
|
|
57
|
+
* the `development` branch and a `-dev` stack. Anything set here wins over those
|
|
58
|
+
* defaults. Only the fields that actually differ per environment live here —
|
|
59
|
+
* `app`/`namePrefix`/`region`/`github.owner`/`github.repo` are shared.
|
|
60
|
+
*/
|
|
61
|
+
export interface EnvOverride {
|
|
62
|
+
/** GitHub branch whose push deploys this environment. */
|
|
63
|
+
branch?: string;
|
|
64
|
+
/** GitHub Actions **Environment** name (scoped Variables + protection rules). */
|
|
65
|
+
ghEnvironment?: string;
|
|
66
|
+
/** CloudFormation stack name for this environment. */
|
|
67
|
+
stackName?: string;
|
|
68
|
+
/** Iceberg namespace this environment's warehouse writes under. */
|
|
69
|
+
namespace?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Base URL of the upstream this environment's **external-API proxy** forwards
|
|
72
|
+
* to (typically a UAT or prod backend). When set, the stack provisions a
|
|
73
|
+
* Cognito-authorized `ANY /proxy/{proxy+}` route on the HTTP API that forwards
|
|
74
|
+
* to `${proxyUpstream}/{proxy}` — so a locally-running app (signed in against
|
|
75
|
+
* this environment's Cognito pool) reaches external APIs *only* through the
|
|
76
|
+
* proxy, never directly, and credentials stay server-side. Empty = no proxy.
|
|
77
|
+
*/
|
|
78
|
+
proxyUpstream?: string;
|
|
79
|
+
/** Values resolved from this environment's stack after it is created. */
|
|
80
|
+
outputs?: DeployOutputs;
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
export interface DeployConfig {
|
|
48
84
|
/** App slug — namespaces buckets/roles/SSM. Cognito-safe: `^[a-z0-9]{1,20}$`. */
|
|
49
85
|
app: string;
|
|
@@ -51,9 +87,16 @@ export interface DeployConfig {
|
|
|
51
87
|
namePrefix: string;
|
|
52
88
|
/** AWS region the stack (and CI) target. */
|
|
53
89
|
region: string;
|
|
54
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* CloudFormation stack name of the **prod** environment — defaults to
|
|
92
|
+
* `${namePrefix}-${app}`. Dev's stack defaults to this + `-dev`.
|
|
93
|
+
*/
|
|
55
94
|
stackName: string;
|
|
56
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* The GitHub repo whose CI is trusted to ship the Lambda code. `branch` is the
|
|
97
|
+
* **prod** deploy branch (default `master`); the dev branch defaults to
|
|
98
|
+
* `development` and is overridable under `environments.dev.branch`.
|
|
99
|
+
*/
|
|
57
100
|
github: { owner: string; repo: string; branch: string };
|
|
58
101
|
/** Iceberg namespace the warehouse writes under. */
|
|
59
102
|
namespace: string;
|
|
@@ -69,10 +112,130 @@ export interface DeployConfig {
|
|
|
69
112
|
artifactsBucket?: string;
|
|
70
113
|
/** Whether the function verifies the `access` or `id` token. */
|
|
71
114
|
tokenUse: "access" | "id";
|
|
72
|
-
/** Values resolved from the stack after it is created (optional). */
|
|
115
|
+
/** Values resolved from the (prod) stack after it is created (optional). */
|
|
116
|
+
outputs?: DeployOutputs;
|
|
117
|
+
/**
|
|
118
|
+
* Per-environment overrides. A `dev` + `prod` split is the default even with
|
|
119
|
+
* this omitted (see `resolveEnvironment`); this only records what a user
|
|
120
|
+
* changed (a custom dev branch, dev stack outputs, …).
|
|
121
|
+
*/
|
|
122
|
+
environments?: Partial<Record<EnvName, EnvOverride>>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The two deployment environments — dev (development branch) and prod (master). */
|
|
126
|
+
export type EnvName = "prod" | "dev";
|
|
127
|
+
export const ENV_NAMES: readonly EnvName[] = ["prod", "dev"] as const;
|
|
128
|
+
|
|
129
|
+
/** The fully-resolved shape of one environment (top-level + defaults + overrides). */
|
|
130
|
+
export interface EnvDef {
|
|
131
|
+
/** Which environment this is. */
|
|
132
|
+
name: EnvName;
|
|
133
|
+
/** Branch whose push deploys it (`master` for prod, `development` for dev). */
|
|
134
|
+
branch: string;
|
|
135
|
+
/** GitHub Actions Environment name (`production` / `development`). */
|
|
136
|
+
ghEnvironment: string;
|
|
137
|
+
/**
|
|
138
|
+
* The app.yaml `AppName` parameter — the infra namespace. Prod uses the app
|
|
139
|
+
* slug; dev uses `${app}dev` so its buckets/roles/pools/SSM never collide with
|
|
140
|
+
* prod's (AppName is Cognito-safe `^[a-z0-9]{1,20}$`, so the suffix is glued,
|
|
141
|
+
* not hyphenated).
|
|
142
|
+
*/
|
|
143
|
+
appName: string;
|
|
144
|
+
/** CloudFormation stack name for this environment. */
|
|
145
|
+
stackName: string;
|
|
146
|
+
/** Iceberg namespace this environment writes under. */
|
|
147
|
+
namespace: string;
|
|
148
|
+
/** Upstream the external-API proxy forwards to (empty when no proxy). */
|
|
149
|
+
proxyUpstream: string;
|
|
150
|
+
/** Values resolved from this environment's stack, when known. */
|
|
73
151
|
outputs?: DeployOutputs;
|
|
74
152
|
}
|
|
75
153
|
|
|
154
|
+
/** Defaults for the dev environment when `environments.dev` is unset. */
|
|
155
|
+
export const DEV_DEFAULTS = {
|
|
156
|
+
branch: "development",
|
|
157
|
+
ghEnvironment: "development",
|
|
158
|
+
namespace: "development",
|
|
159
|
+
} as const;
|
|
160
|
+
|
|
161
|
+
/** The GitHub Actions Environment name the prod deploy uses. */
|
|
162
|
+
export const PROD_GH_ENVIRONMENT = "production";
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
|
|
166
|
+
* `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
|
|
167
|
+
* from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
|
|
168
|
+
*/
|
|
169
|
+
export function envAppName(cfg: DeployConfig, env: EnvName): string {
|
|
170
|
+
return env === "dev" ? `${cfg.app}dev` : cfg.app;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve one environment to its full definition: the prod environment inherits
|
|
175
|
+
* the top-level branch/stack/namespace/outputs, the dev environment defaults to
|
|
176
|
+
* the `development` branch and a `-dev` stack, and anything under
|
|
177
|
+
* `cfg.environments[env]` overrides those.
|
|
178
|
+
*/
|
|
179
|
+
export function resolveEnvironment(cfg: DeployConfig, env: EnvName): EnvDef {
|
|
180
|
+
const o = cfg.environments?.[env] ?? {};
|
|
181
|
+
if (env === "prod") {
|
|
182
|
+
return {
|
|
183
|
+
name: "prod",
|
|
184
|
+
branch: o.branch ?? cfg.github.branch,
|
|
185
|
+
ghEnvironment: o.ghEnvironment ?? PROD_GH_ENVIRONMENT,
|
|
186
|
+
appName: cfg.app,
|
|
187
|
+
stackName: o.stackName ?? cfg.stackName,
|
|
188
|
+
namespace: o.namespace ?? cfg.namespace,
|
|
189
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
190
|
+
outputs: o.outputs ?? cfg.outputs,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
name: "dev",
|
|
195
|
+
branch: o.branch ?? DEV_DEFAULTS.branch,
|
|
196
|
+
ghEnvironment: o.ghEnvironment ?? DEV_DEFAULTS.ghEnvironment,
|
|
197
|
+
appName: envAppName(cfg, "dev"),
|
|
198
|
+
stackName: o.stackName ?? (cfg.stackName ? `${cfg.stackName}-dev` : ""),
|
|
199
|
+
namespace: o.namespace ?? DEV_DEFAULTS.namespace,
|
|
200
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
201
|
+
outputs: o.outputs,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Both environments, prod first, fully resolved. */
|
|
206
|
+
export function resolveEnvironments(cfg: DeployConfig): EnvDef[] {
|
|
207
|
+
return ENV_NAMES.map((e) => resolveEnvironment(cfg, e));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The browser-facing Cognito config for one environment — what `rastack dev`
|
|
212
|
+
* serves at `/__rastack/cognito.json` so a locally-running app can sign in
|
|
213
|
+
* against the real (dev) Cognito pool via the Hosted UI. Shaped to feed
|
|
214
|
+
* `cognitoRuntimeToConfig` in `rastack/auth`. Returns null until the
|
|
215
|
+
* environment's stack Outputs (client id + Hosted UI url) are known.
|
|
216
|
+
*/
|
|
217
|
+
export interface CognitoRuntimeConfig {
|
|
218
|
+
userPoolId?: string;
|
|
219
|
+
clientId: string;
|
|
220
|
+
/** Hosted UI base URL (`https://…amazoncognito.com`). */
|
|
221
|
+
hostedUiUrl: string;
|
|
222
|
+
region: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function cognitoRuntimeConfig(
|
|
226
|
+
cfg: DeployConfig,
|
|
227
|
+
env: EnvName,
|
|
228
|
+
): CognitoRuntimeConfig | null {
|
|
229
|
+
const o = resolveEnvironment(cfg, env).outputs;
|
|
230
|
+
if (!o?.userPoolClientId || !o?.hostedUiUrl) return null;
|
|
231
|
+
return {
|
|
232
|
+
userPoolId: o.userPoolId,
|
|
233
|
+
clientId: o.userPoolClientId,
|
|
234
|
+
hostedUiUrl: o.hostedUiUrl,
|
|
235
|
+
region: cfg.region,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
76
239
|
export const DEFAULT_LAMBDA: LambdaBuild = {
|
|
77
240
|
bin: "bootstrap",
|
|
78
241
|
arch: "arm64",
|
|
@@ -132,9 +295,31 @@ export function normalizeConfig(
|
|
|
132
295
|
artifactsBucket: partial.artifactsBucket?.trim() || undefined,
|
|
133
296
|
tokenUse: partial.tokenUse ?? DEFAULTS.tokenUse,
|
|
134
297
|
outputs: partial.outputs ? { ...partial.outputs } : undefined,
|
|
298
|
+
environments: normalizeEnvironments(partial.environments),
|
|
135
299
|
};
|
|
136
300
|
}
|
|
137
301
|
|
|
302
|
+
/** Copy through per-environment overrides, dropping an empty map to undefined. */
|
|
303
|
+
function normalizeEnvironments(
|
|
304
|
+
envs: PartialDeployConfig["environments"],
|
|
305
|
+
): DeployConfig["environments"] {
|
|
306
|
+
if (!envs) return undefined;
|
|
307
|
+
const out: Partial<Record<EnvName, EnvOverride>> = {};
|
|
308
|
+
for (const name of ENV_NAMES) {
|
|
309
|
+
const o = envs[name];
|
|
310
|
+
if (!o) continue;
|
|
311
|
+
const entry: EnvOverride = {};
|
|
312
|
+
if (o.branch?.trim()) entry.branch = o.branch.trim();
|
|
313
|
+
if (o.ghEnvironment?.trim()) entry.ghEnvironment = o.ghEnvironment.trim();
|
|
314
|
+
if (o.stackName?.trim()) entry.stackName = o.stackName.trim();
|
|
315
|
+
if (o.namespace?.trim()) entry.namespace = o.namespace.trim();
|
|
316
|
+
if (o.proxyUpstream?.trim()) entry.proxyUpstream = o.proxyUpstream.trim();
|
|
317
|
+
if (o.outputs) entry.outputs = { ...o.outputs };
|
|
318
|
+
if (Object.keys(entry).length) out[name] = entry;
|
|
319
|
+
}
|
|
320
|
+
return Object.keys(out).length ? out : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
138
323
|
const SLUG = /^[a-z0-9]{1,20}$/;
|
|
139
324
|
const REGION = /^[a-z]{2}-[a-z]+-\d+$/;
|
|
140
325
|
const GH_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -152,6 +337,14 @@ export function validateConfig(cfg: DeployConfig): string[] {
|
|
|
152
337
|
errors.push(
|
|
153
338
|
`app "${cfg.app}" must match ${SLUG} (lowercase letters/digits, 1–20).`,
|
|
154
339
|
);
|
|
340
|
+
// The dev environment appends "dev" to the app slug for its infra namespace
|
|
341
|
+
// (buckets/roles/pools/SSM). That derived name must still be a valid slug, so
|
|
342
|
+
// the app slug is effectively capped at 17 chars once a dev env is in play.
|
|
343
|
+
else if (!SLUG.test(envAppName(cfg, "dev")))
|
|
344
|
+
errors.push(
|
|
345
|
+
`app "${cfg.app}" is too long: the dev environment uses "${envAppName(cfg, "dev")}" ` +
|
|
346
|
+
`for its infra names, which must still match ${SLUG} (so app ≤ 17 chars).`,
|
|
347
|
+
);
|
|
155
348
|
|
|
156
349
|
if (!SLUG.test(cfg.namePrefix))
|
|
157
350
|
errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
|
|
@@ -226,9 +419,31 @@ export function mergeConfig(
|
|
|
226
419
|
github: { ...base.github, ...patch.github },
|
|
227
420
|
lambda: { ...base.lambda, ...patch.lambda },
|
|
228
421
|
outputs: { ...base.outputs, ...patch.outputs },
|
|
422
|
+
environments: mergeEnvironments(base.environments, patch.environments),
|
|
229
423
|
});
|
|
230
424
|
}
|
|
231
425
|
|
|
426
|
+
/** Deep-merge per-environment overrides (per env, and per env's outputs). */
|
|
427
|
+
function mergeEnvironments(
|
|
428
|
+
base: DeployConfig["environments"],
|
|
429
|
+
patch: PartialDeployConfig["environments"],
|
|
430
|
+
): DeployConfig["environments"] {
|
|
431
|
+
if (!base && !patch) return undefined;
|
|
432
|
+
const out: Partial<Record<EnvName, EnvOverride>> = {};
|
|
433
|
+
for (const name of ENV_NAMES) {
|
|
434
|
+
const b = base?.[name];
|
|
435
|
+
const p = patch?.[name];
|
|
436
|
+
if (!b && !p) continue;
|
|
437
|
+
out[name] = {
|
|
438
|
+
...b,
|
|
439
|
+
...p,
|
|
440
|
+
outputs:
|
|
441
|
+
b?.outputs || p?.outputs ? { ...b?.outputs, ...p?.outputs } : undefined,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return Object.keys(out).length ? out : undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
232
447
|
/**
|
|
233
448
|
* Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
|
|
234
449
|
* (`git@github.com:owner/repo.git`) and HTTPS
|
|
@@ -314,6 +529,40 @@ export function stackParameters(cfg: DeployConfig): StackParameter[] {
|
|
|
314
529
|
return params;
|
|
315
530
|
}
|
|
316
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The app.yaml `--parameter-overrides` for a **specific environment** (dev or
|
|
534
|
+
* prod). Same as `stackParameters` but the AppName/branch/namespace come from the
|
|
535
|
+
* resolved environment, and it also passes `GitHubEnvironment` so the stack's
|
|
536
|
+
* deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
|
|
537
|
+
* for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
|
|
538
|
+
* AppName is the bare slug, so prod's resource names are byte-for-byte what the
|
|
539
|
+
* pre-environment template produced.
|
|
540
|
+
*/
|
|
541
|
+
export function stackParametersForEnv(
|
|
542
|
+
cfg: DeployConfig,
|
|
543
|
+
env: EnvName,
|
|
544
|
+
): StackParameter[] {
|
|
545
|
+
const e = resolveEnvironment(cfg, env);
|
|
546
|
+
const params: StackParameter[] = [
|
|
547
|
+
{ key: "AppName", value: e.appName },
|
|
548
|
+
{ key: "NamePrefix", value: cfg.namePrefix },
|
|
549
|
+
{ key: "GitHubOwner", value: cfg.github.owner },
|
|
550
|
+
{ key: "GitHubRepo", value: cfg.github.repo },
|
|
551
|
+
{ key: "GitHubBranch", value: e.branch },
|
|
552
|
+
{ key: "GitHubEnvironment", value: e.ghEnvironment },
|
|
553
|
+
{ key: "IcebergNamespace", value: e.namespace },
|
|
554
|
+
];
|
|
555
|
+
if (cfg.artifactsBucket)
|
|
556
|
+
params.push({ key: "ArtifactsBucket", value: cfg.artifactsBucket });
|
|
557
|
+
// The external-API proxy is provisioned only when an upstream is configured
|
|
558
|
+
// (typically dev → a UAT/prod backend). Empty leaves the route/authorizer out.
|
|
559
|
+
if (e.proxyUpstream)
|
|
560
|
+
params.push({ key: "ExternalApiUpstream", value: e.proxyUpstream });
|
|
561
|
+
if (!cognitoAdvancedSecuritySupported(cfg.region))
|
|
562
|
+
params.push({ key: "AdvancedSecurity", value: "OFF" });
|
|
563
|
+
return params;
|
|
564
|
+
}
|
|
565
|
+
|
|
317
566
|
/**
|
|
318
567
|
* The canonical repo-relative CloudFormation template paths. These are the paths
|
|
319
568
|
* in the *framework* repo (theserverkid/reactapistack) and the default the argv
|
|
@@ -399,6 +648,29 @@ export function deployStackCommand(
|
|
|
399
648
|
return awsCommand(deployStackArgs(cfg, templateFile));
|
|
400
649
|
}
|
|
401
650
|
|
|
651
|
+
/** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
|
|
652
|
+
export function deployStackArgsForEnv(
|
|
653
|
+
cfg: DeployConfig,
|
|
654
|
+
env: EnvName,
|
|
655
|
+
templateFile: string = APP_TEMPLATE_FILE,
|
|
656
|
+
): string[] {
|
|
657
|
+
return cfnDeployArgs(
|
|
658
|
+
templateFile,
|
|
659
|
+
resolveEnvironment(cfg, env).stackName,
|
|
660
|
+
cfg.region,
|
|
661
|
+
stackParametersForEnv(cfg, env),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** The single-line `aws cloudformation deploy` command for one environment. */
|
|
666
|
+
export function deployStackCommandForEnv(
|
|
667
|
+
cfg: DeployConfig,
|
|
668
|
+
env: EnvName,
|
|
669
|
+
templateFile: string = APP_TEMPLATE_FILE,
|
|
670
|
+
): string {
|
|
671
|
+
return awsCommand(deployStackArgsForEnv(cfg, env, templateFile));
|
|
672
|
+
}
|
|
673
|
+
|
|
402
674
|
/**
|
|
403
675
|
* The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
|
|
404
676
|
* Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
|
|
@@ -470,6 +742,9 @@ export const APP_STACK_OUTPUTS: { key: string; field: keyof DeployOutputs }[] =
|
|
|
470
742
|
{ key: "UserPoolClientId", field: "userPoolClientId" },
|
|
471
743
|
{ key: "JwksUrl", field: "jwksUrl" },
|
|
472
744
|
{ key: "WarehouseBucketName", field: "warehouseBucket" },
|
|
745
|
+
{ key: "UserPoolDomain", field: "userPoolDomain" },
|
|
746
|
+
{ key: "HostedUiUrl", field: "hostedUiUrl" },
|
|
747
|
+
{ key: "ProxyUrl", field: "proxyUrl" },
|
|
473
748
|
];
|
|
474
749
|
|
|
475
750
|
/** GitHub repo Variable to wire (name + resolved value, if known from outputs). */
|
|
@@ -477,13 +752,20 @@ export interface RepoVariable {
|
|
|
477
752
|
name: string;
|
|
478
753
|
value: string;
|
|
479
754
|
from: string;
|
|
755
|
+
/**
|
|
756
|
+
* The GitHub Actions **Environment** this Variable is scoped to (`development`
|
|
757
|
+
* / `production`). When set, `init` wires it under that environment so the
|
|
758
|
+
* dev and prod deploys each read their own role/region.
|
|
759
|
+
*/
|
|
760
|
+
ghEnvironment?: string;
|
|
480
761
|
}
|
|
481
762
|
|
|
482
763
|
/**
|
|
483
764
|
* The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
|
|
484
765
|
* `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
|
|
485
766
|
* Outputs when already resolved, else an empty string to fill in from the
|
|
486
|
-
* Console.
|
|
767
|
+
* Console. This is the legacy single-environment view (the prod stack); the
|
|
768
|
+
* dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
|
|
487
769
|
*/
|
|
488
770
|
export function repoVariables(cfg: DeployConfig): RepoVariable[] {
|
|
489
771
|
return [
|
|
@@ -496,6 +778,33 @@ export function repoVariables(cfg: DeployConfig): RepoVariable[] {
|
|
|
496
778
|
];
|
|
497
779
|
}
|
|
498
780
|
|
|
781
|
+
/**
|
|
782
|
+
* The same two Variables, scoped to one environment's GitHub Actions
|
|
783
|
+
* Environment. The dev deploy reads `development`'s copies, the prod deploy
|
|
784
|
+
* `production`'s — so the two never cross-assume each other's role. Values come
|
|
785
|
+
* from the environment's own stack Outputs.
|
|
786
|
+
*/
|
|
787
|
+
export function repoVariablesForEnv(
|
|
788
|
+
cfg: DeployConfig,
|
|
789
|
+
env: EnvName,
|
|
790
|
+
): RepoVariable[] {
|
|
791
|
+
const e = resolveEnvironment(cfg, env);
|
|
792
|
+
return [
|
|
793
|
+
{
|
|
794
|
+
name: "AWS_DEPLOY_ROLE_ARN",
|
|
795
|
+
value: e.outputs?.deployRoleArn ?? "",
|
|
796
|
+
from: "DeployRoleArn stack output",
|
|
797
|
+
ghEnvironment: e.ghEnvironment,
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: "AWS_REGION",
|
|
801
|
+
value: cfg.region,
|
|
802
|
+
from: "Region stack output",
|
|
803
|
+
ghEnvironment: e.ghEnvironment,
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
}
|
|
807
|
+
|
|
499
808
|
/**
|
|
500
809
|
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
501
810
|
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
@@ -509,15 +818,39 @@ export function ghVariableSetArgs(
|
|
|
509
818
|
cfg: DeployConfig,
|
|
510
819
|
name: string,
|
|
511
820
|
value: string,
|
|
821
|
+
ghEnvironment?: string,
|
|
512
822
|
): string[] {
|
|
513
|
-
|
|
823
|
+
const args = [
|
|
514
824
|
"variable",
|
|
515
825
|
"set",
|
|
516
826
|
name,
|
|
517
827
|
"--repo",
|
|
518
828
|
`${cfg.github.owner}/${cfg.github.repo}`,
|
|
519
|
-
|
|
520
|
-
|
|
829
|
+
];
|
|
830
|
+
// Scope the Variable to a GitHub Actions Environment when one is given, so the
|
|
831
|
+
// dev and prod deploys each read their own role/region.
|
|
832
|
+
if (ghEnvironment) args.push("--env", ghEnvironment);
|
|
833
|
+
args.push("--body", value);
|
|
834
|
+
return args;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* The argv (after `gh`) that creates (idempotently) a GitHub Actions
|
|
839
|
+
* **Environment** on the config's repo — `gh api --method PUT
|
|
840
|
+
* repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
|
|
841
|
+
* and `production` before wiring their scoped Variables, since a Variable can
|
|
842
|
+
* only be set on an environment that already exists. PUT is idempotent, so
|
|
843
|
+
* re-running `init` is safe.
|
|
844
|
+
*/
|
|
845
|
+
export function ghEnvironmentCreateArgs(
|
|
846
|
+
cfg: DeployConfig,
|
|
847
|
+
ghEnvironment: string,
|
|
848
|
+
): string[] {
|
|
849
|
+
return [
|
|
850
|
+
"api",
|
|
851
|
+
"--method",
|
|
852
|
+
"PUT",
|
|
853
|
+
`repos/${cfg.github.owner}/${cfg.github.repo}/environments/${ghEnvironment}`,
|
|
521
854
|
];
|
|
522
855
|
}
|
|
523
856
|
|
package/src/deploy/io.ts
CHANGED
|
@@ -236,7 +236,23 @@ export function ghAuthLogin(cwd = process.cwd()): boolean {
|
|
|
236
236
|
* `execFileSync` (no shell) so the ARN/region values are never interpolated
|
|
237
237
|
* into a command line.
|
|
238
238
|
*/
|
|
239
|
-
export function setRepoVariable(
|
|
239
|
+
export function setRepoVariable(args: string[], cwd = process.cwd()): boolean {
|
|
240
|
+
try {
|
|
241
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
242
|
+
execFileSync("gh", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
|
|
251
|
+
* so its scoped Variables can be set. Returns true on success. Best-effort — a
|
|
252
|
+
* missing/unauthenticated `gh` or an API error degrades to the printed
|
|
253
|
+
* instructions, exactly like `setRepoVariable`.
|
|
254
|
+
*/
|
|
255
|
+
export function ghEnvironmentCreate(
|
|
240
256
|
args: string[],
|
|
241
257
|
cwd = process.cwd(),
|
|
242
258
|
): boolean {
|