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/dist/deploy/ci.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* or emitted as a live `$(aws cloudformation describe-stacks …)` substitution so
|
|
16
16
|
* the same plan works before and after they are wired into the config.
|
|
17
17
|
*/
|
|
18
|
-
import { DeployConfig } from "./config";
|
|
18
|
+
import { DeployConfig, EnvName } from "./config";
|
|
19
19
|
export type CiPhase = "check" | "build" | "deploy";
|
|
20
20
|
export interface CiStep {
|
|
21
21
|
/** Human label, echoed before the step runs. */
|
|
@@ -37,10 +37,17 @@ export interface CiPlanOptions {
|
|
|
37
37
|
bin?: string;
|
|
38
38
|
/** Skip the build steps in `deploy` (code already built this job). */
|
|
39
39
|
skipBuild?: boolean;
|
|
40
|
-
/** Override the function name (else from
|
|
40
|
+
/** Override the function name (else from the env's outputs, else resolved live). */
|
|
41
41
|
functionName?: string;
|
|
42
|
-
/** Override the JWKS URL (else from
|
|
42
|
+
/** Override the JWKS URL (else from the env's outputs, else resolved live). */
|
|
43
43
|
jwksUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Which environment (dev/prod) to build+ship. When set, the stack name,
|
|
46
|
+
* outputs, and app label come from that environment — so `rastack ci deploy
|
|
47
|
+
* --env dev` targets the dev stack (dev's Cognito JWKS, dev's function). When
|
|
48
|
+
* unset the legacy single-stack fields (`cfg.stackName` / `cfg.outputs`) apply.
|
|
49
|
+
*/
|
|
50
|
+
env?: EnvName;
|
|
44
51
|
}
|
|
45
52
|
/** `check` — validate the resource graph and compile the manifest. A CI gate. */
|
|
46
53
|
export declare function checkSteps(cfg: DeployConfig, bin: string): CiStep[];
|
package/dist/deploy/ci.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.checkSteps = checkSteps;
|
|
|
21
21
|
exports.buildSteps = buildSteps;
|
|
22
22
|
exports.deploySteps = deploySteps;
|
|
23
23
|
exports.ciPlan = ciPlan;
|
|
24
|
+
const config_1 = require("./config");
|
|
24
25
|
/**
|
|
25
26
|
* The in-process compile step shared by `check` and `build`: it produces
|
|
26
27
|
* `schema.rastack.json` + `openapi.json` in `cfg.outDir` without a subprocess,
|
|
@@ -34,17 +35,25 @@ function compileStep(cfg) {
|
|
|
34
35
|
action: "compile",
|
|
35
36
|
};
|
|
36
37
|
}
|
|
38
|
+
/** The stack name + outputs + app label the plan targets, honouring `opts.env`. */
|
|
39
|
+
function ciTarget(cfg, opts) {
|
|
40
|
+
if (opts.env) {
|
|
41
|
+
const e = (0, config_1.resolveEnvironment)(cfg, opts.env);
|
|
42
|
+
return { stackName: e.stackName, outputs: e.outputs, app: e.appName };
|
|
43
|
+
}
|
|
44
|
+
return { stackName: cfg.stackName, outputs: cfg.outputs, app: cfg.app };
|
|
45
|
+
}
|
|
37
46
|
/**
|
|
38
|
-
* A stack Output value: the literal
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
47
|
+
* A stack Output value: the literal if wired, else a shell command substitution
|
|
48
|
+
* that resolves it live from CloudFormation. Keeping the fallback inline (rather
|
|
49
|
+
* than a separate step) means the plan is a flat list of self-contained commands
|
|
50
|
+
* that read correctly in any order.
|
|
42
51
|
*/
|
|
43
|
-
function stackOutput(
|
|
52
|
+
function stackOutput(stackName, region, outputKey, literal) {
|
|
44
53
|
if (literal)
|
|
45
54
|
return literal;
|
|
46
|
-
return (`$(aws cloudformation describe-stacks --stack-name ${
|
|
47
|
-
`--region ${
|
|
55
|
+
return (`$(aws cloudformation describe-stacks --stack-name ${stackName} ` +
|
|
56
|
+
`--region ${region} ` +
|
|
48
57
|
`--query "Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue" ` +
|
|
49
58
|
`--output text)`);
|
|
50
59
|
}
|
|
@@ -69,8 +78,9 @@ function checkSteps(cfg, bin) {
|
|
|
69
78
|
*/
|
|
70
79
|
function buildSteps(cfg, opts = {}) {
|
|
71
80
|
const stage = stageDir(cfg);
|
|
81
|
+
const target = ciTarget(cfg, opts);
|
|
72
82
|
const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
|
|
73
|
-
const jwks = stackOutput(cfg, "JwksUrl", opts.jwksUrl ??
|
|
83
|
+
const jwks = stackOutput(target.stackName, cfg.region, "JwksUrl", opts.jwksUrl ?? target.outputs?.jwksUrl);
|
|
74
84
|
return [
|
|
75
85
|
compileStep(cfg),
|
|
76
86
|
{
|
|
@@ -100,8 +110,9 @@ function buildSteps(cfg, opts = {}) {
|
|
|
100
110
|
function deploySteps(cfg, opts = {}) {
|
|
101
111
|
const stage = stageDir(cfg);
|
|
102
112
|
const zip = `${stage}/bootstrap.zip`;
|
|
103
|
-
const
|
|
104
|
-
const
|
|
113
|
+
const target = ciTarget(cfg, opts);
|
|
114
|
+
const fn = stackOutput(target.stackName, cfg.region, "FunctionName", opts.functionName ?? target.outputs?.functionName);
|
|
115
|
+
const apiUrl = stackOutput(target.stackName, cfg.region, "ApiUrl", target.outputs?.apiUrl);
|
|
105
116
|
const steps = opts.skipBuild ? [] : buildSteps(cfg, opts);
|
|
106
117
|
steps.push({
|
|
107
118
|
name: "Ship the code (update-function-code)",
|
|
@@ -113,7 +124,7 @@ function deploySteps(cfg, opts = {}) {
|
|
|
113
124
|
run: `aws lambda wait function-updated --function-name ${fn} --region ${cfg.region}`,
|
|
114
125
|
}, {
|
|
115
126
|
name: "Report the API URL",
|
|
116
|
-
run: `echo "Deployed ${
|
|
127
|
+
run: `echo "Deployed ${target.app} → ${apiUrl}"`,
|
|
117
128
|
});
|
|
118
129
|
return steps;
|
|
119
130
|
}
|
package/dist/deploy/config.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export interface DeployOutputs {
|
|
|
32
32
|
jwksUrl?: string;
|
|
33
33
|
/** S3 Iceberg warehouse bucket — the system of record. */
|
|
34
34
|
warehouseBucket?: string;
|
|
35
|
+
/** Cognito Hosted UI domain (scheme-less), for browser sign-in. */
|
|
36
|
+
userPoolDomain?: string;
|
|
37
|
+
/** Full Cognito Hosted UI base URL (`https://…`). */
|
|
38
|
+
hostedUiUrl?: string;
|
|
39
|
+
/** Base URL of the external-API proxy route, when the env provisions one. */
|
|
40
|
+
proxyUrl?: string;
|
|
35
41
|
}
|
|
36
42
|
export interface LambdaBuild {
|
|
37
43
|
/** Cargo bin built for the custom runtime (`provided.al2023`). */
|
|
@@ -41,6 +47,35 @@ export interface LambdaBuild {
|
|
|
41
47
|
/** Cargo features the Lambda build enables. */
|
|
42
48
|
features: string;
|
|
43
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-environment overrides. The **prod** environment defaults its values from
|
|
52
|
+
* the top-level config (its branch is `github.branch`, its stack is `stackName`,
|
|
53
|
+
* its outputs are the top-level `outputs`); the **dev** environment defaults to
|
|
54
|
+
* the `development` branch and a `-dev` stack. Anything set here wins over those
|
|
55
|
+
* defaults. Only the fields that actually differ per environment live here —
|
|
56
|
+
* `app`/`namePrefix`/`region`/`github.owner`/`github.repo` are shared.
|
|
57
|
+
*/
|
|
58
|
+
export interface EnvOverride {
|
|
59
|
+
/** GitHub branch whose push deploys this environment. */
|
|
60
|
+
branch?: string;
|
|
61
|
+
/** GitHub Actions **Environment** name (scoped Variables + protection rules). */
|
|
62
|
+
ghEnvironment?: string;
|
|
63
|
+
/** CloudFormation stack name for this environment. */
|
|
64
|
+
stackName?: string;
|
|
65
|
+
/** Iceberg namespace this environment's warehouse writes under. */
|
|
66
|
+
namespace?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Base URL of the upstream this environment's **external-API proxy** forwards
|
|
69
|
+
* to (typically a UAT or prod backend). When set, the stack provisions a
|
|
70
|
+
* Cognito-authorized `ANY /proxy/{proxy+}` route on the HTTP API that forwards
|
|
71
|
+
* to `${proxyUpstream}/{proxy}` — so a locally-running app (signed in against
|
|
72
|
+
* this environment's Cognito pool) reaches external APIs *only* through the
|
|
73
|
+
* proxy, never directly, and credentials stay server-side. Empty = no proxy.
|
|
74
|
+
*/
|
|
75
|
+
proxyUpstream?: string;
|
|
76
|
+
/** Values resolved from this environment's stack after it is created. */
|
|
77
|
+
outputs?: DeployOutputs;
|
|
78
|
+
}
|
|
44
79
|
export interface DeployConfig {
|
|
45
80
|
/** App slug — namespaces buckets/roles/SSM. Cognito-safe: `^[a-z0-9]{1,20}$`. */
|
|
46
81
|
app: string;
|
|
@@ -48,9 +83,16 @@ export interface DeployConfig {
|
|
|
48
83
|
namePrefix: string;
|
|
49
84
|
/** AWS region the stack (and CI) target. */
|
|
50
85
|
region: string;
|
|
51
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* CloudFormation stack name of the **prod** environment — defaults to
|
|
88
|
+
* `${namePrefix}-${app}`. Dev's stack defaults to this + `-dev`.
|
|
89
|
+
*/
|
|
52
90
|
stackName: string;
|
|
53
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The GitHub repo whose CI is trusted to ship the Lambda code. `branch` is the
|
|
93
|
+
* **prod** deploy branch (default `master`); the dev branch defaults to
|
|
94
|
+
* `development` and is overridable under `environments.dev.branch`.
|
|
95
|
+
*/
|
|
54
96
|
github: {
|
|
55
97
|
owner: string;
|
|
56
98
|
repo: string;
|
|
@@ -70,9 +112,80 @@ export interface DeployConfig {
|
|
|
70
112
|
artifactsBucket?: string;
|
|
71
113
|
/** Whether the function verifies the `access` or `id` token. */
|
|
72
114
|
tokenUse: "access" | "id";
|
|
73
|
-
/** 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
|
+
/** The two deployment environments — dev (development branch) and prod (master). */
|
|
125
|
+
export type EnvName = "prod" | "dev";
|
|
126
|
+
export declare const ENV_NAMES: readonly EnvName[];
|
|
127
|
+
/** The fully-resolved shape of one environment (top-level + defaults + overrides). */
|
|
128
|
+
export interface EnvDef {
|
|
129
|
+
/** Which environment this is. */
|
|
130
|
+
name: EnvName;
|
|
131
|
+
/** Branch whose push deploys it (`master` for prod, `development` for dev). */
|
|
132
|
+
branch: string;
|
|
133
|
+
/** GitHub Actions Environment name (`production` / `development`). */
|
|
134
|
+
ghEnvironment: string;
|
|
135
|
+
/**
|
|
136
|
+
* The app.yaml `AppName` parameter — the infra namespace. Prod uses the app
|
|
137
|
+
* slug; dev uses `${app}dev` so its buckets/roles/pools/SSM never collide with
|
|
138
|
+
* prod's (AppName is Cognito-safe `^[a-z0-9]{1,20}$`, so the suffix is glued,
|
|
139
|
+
* not hyphenated).
|
|
140
|
+
*/
|
|
141
|
+
appName: string;
|
|
142
|
+
/** CloudFormation stack name for this environment. */
|
|
143
|
+
stackName: string;
|
|
144
|
+
/** Iceberg namespace this environment writes under. */
|
|
145
|
+
namespace: string;
|
|
146
|
+
/** Upstream the external-API proxy forwards to (empty when no proxy). */
|
|
147
|
+
proxyUpstream: string;
|
|
148
|
+
/** Values resolved from this environment's stack, when known. */
|
|
74
149
|
outputs?: DeployOutputs;
|
|
75
150
|
}
|
|
151
|
+
/** Defaults for the dev environment when `environments.dev` is unset. */
|
|
152
|
+
export declare const DEV_DEFAULTS: {
|
|
153
|
+
readonly branch: "development";
|
|
154
|
+
readonly ghEnvironment: "development";
|
|
155
|
+
readonly namespace: "development";
|
|
156
|
+
};
|
|
157
|
+
/** The GitHub Actions Environment name the prod deploy uses. */
|
|
158
|
+
export declare const PROD_GH_ENVIRONMENT = "production";
|
|
159
|
+
/**
|
|
160
|
+
* The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
|
|
161
|
+
* `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
|
|
162
|
+
* from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
|
|
163
|
+
*/
|
|
164
|
+
export declare function envAppName(cfg: DeployConfig, env: EnvName): string;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve one environment to its full definition: the prod environment inherits
|
|
167
|
+
* the top-level branch/stack/namespace/outputs, the dev environment defaults to
|
|
168
|
+
* the `development` branch and a `-dev` stack, and anything under
|
|
169
|
+
* `cfg.environments[env]` overrides those.
|
|
170
|
+
*/
|
|
171
|
+
export declare function resolveEnvironment(cfg: DeployConfig, env: EnvName): EnvDef;
|
|
172
|
+
/** Both environments, prod first, fully resolved. */
|
|
173
|
+
export declare function resolveEnvironments(cfg: DeployConfig): EnvDef[];
|
|
174
|
+
/**
|
|
175
|
+
* The browser-facing Cognito config for one environment — what `rastack dev`
|
|
176
|
+
* serves at `/__rastack/cognito.json` so a locally-running app can sign in
|
|
177
|
+
* against the real (dev) Cognito pool via the Hosted UI. Shaped to feed
|
|
178
|
+
* `cognitoRuntimeToConfig` in `rastack/auth`. Returns null until the
|
|
179
|
+
* environment's stack Outputs (client id + Hosted UI url) are known.
|
|
180
|
+
*/
|
|
181
|
+
export interface CognitoRuntimeConfig {
|
|
182
|
+
userPoolId?: string;
|
|
183
|
+
clientId: string;
|
|
184
|
+
/** Hosted UI base URL (`https://…amazoncognito.com`). */
|
|
185
|
+
hostedUiUrl: string;
|
|
186
|
+
region: string;
|
|
187
|
+
}
|
|
188
|
+
export declare function cognitoRuntimeConfig(cfg: DeployConfig, env: EnvName): CognitoRuntimeConfig | null;
|
|
76
189
|
export declare const DEFAULT_LAMBDA: LambdaBuild;
|
|
77
190
|
/** The out-of-the-box config — everything a fresh repo can default sensibly. */
|
|
78
191
|
export declare const DEFAULTS: Omit<DeployConfig, "app" | "github">;
|
|
@@ -136,6 +249,16 @@ export declare function cognitoAdvancedSecuritySupported(region: string): boolea
|
|
|
136
249
|
* the template's ENFORCED default stands).
|
|
137
250
|
*/
|
|
138
251
|
export declare function stackParameters(cfg: DeployConfig): StackParameter[];
|
|
252
|
+
/**
|
|
253
|
+
* The app.yaml `--parameter-overrides` for a **specific environment** (dev or
|
|
254
|
+
* prod). Same as `stackParameters` but the AppName/branch/namespace come from the
|
|
255
|
+
* resolved environment, and it also passes `GitHubEnvironment` so the stack's
|
|
256
|
+
* deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
|
|
257
|
+
* for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
|
|
258
|
+
* AppName is the bare slug, so prod's resource names are byte-for-byte what the
|
|
259
|
+
* pre-environment template produced.
|
|
260
|
+
*/
|
|
261
|
+
export declare function stackParametersForEnv(cfg: DeployConfig, env: EnvName): StackParameter[];
|
|
139
262
|
/**
|
|
140
263
|
* The canonical repo-relative CloudFormation template paths. These are the paths
|
|
141
264
|
* in the *framework* repo (theserverkid/reactapistack) and the default the argv
|
|
@@ -158,6 +281,10 @@ export declare function deployStackArgs(cfg: DeployConfig, templateFile?: string
|
|
|
158
281
|
* for you; it is also printed by `--print` so it can be run by hand.
|
|
159
282
|
*/
|
|
160
283
|
export declare function deployStackCommand(cfg: DeployConfig, templateFile?: string): string;
|
|
284
|
+
/** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
|
|
285
|
+
export declare function deployStackArgsForEnv(cfg: DeployConfig, env: EnvName, templateFile?: string): string[];
|
|
286
|
+
/** The single-line `aws cloudformation deploy` command for one environment. */
|
|
287
|
+
export declare function deployStackCommandForEnv(cfg: DeployConfig, env: EnvName, templateFile?: string): string;
|
|
161
288
|
/**
|
|
162
289
|
* The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
|
|
163
290
|
* Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
|
|
@@ -193,14 +320,28 @@ export interface RepoVariable {
|
|
|
193
320
|
name: string;
|
|
194
321
|
value: string;
|
|
195
322
|
from: string;
|
|
323
|
+
/**
|
|
324
|
+
* The GitHub Actions **Environment** this Variable is scoped to (`development`
|
|
325
|
+
* / `production`). When set, `init` wires it under that environment so the
|
|
326
|
+
* dev and prod deploys each read their own role/region.
|
|
327
|
+
*/
|
|
328
|
+
ghEnvironment?: string;
|
|
196
329
|
}
|
|
197
330
|
/**
|
|
198
331
|
* The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
|
|
199
332
|
* `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
|
|
200
333
|
* Outputs when already resolved, else an empty string to fill in from the
|
|
201
|
-
* Console.
|
|
334
|
+
* Console. This is the legacy single-environment view (the prod stack); the
|
|
335
|
+
* dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
|
|
202
336
|
*/
|
|
203
337
|
export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
|
|
338
|
+
/**
|
|
339
|
+
* The same two Variables, scoped to one environment's GitHub Actions
|
|
340
|
+
* Environment. The dev deploy reads `development`'s copies, the prod deploy
|
|
341
|
+
* `production`'s — so the two never cross-assume each other's role. Values come
|
|
342
|
+
* from the environment's own stack Outputs.
|
|
343
|
+
*/
|
|
344
|
+
export declare function repoVariablesForEnv(cfg: DeployConfig, env: EnvName): RepoVariable[];
|
|
204
345
|
/**
|
|
205
346
|
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
206
347
|
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
@@ -210,7 +351,16 @@ export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
|
|
|
210
351
|
* user's GitHub) rather than an API token; a pure argv so it is inspectable and
|
|
211
352
|
* shell-free (`execFileSync`).
|
|
212
353
|
*/
|
|
213
|
-
export declare function ghVariableSetArgs(cfg: DeployConfig, name: string, value: string): string[];
|
|
354
|
+
export declare function ghVariableSetArgs(cfg: DeployConfig, name: string, value: string, ghEnvironment?: string): string[];
|
|
355
|
+
/**
|
|
356
|
+
* The argv (after `gh`) that creates (idempotently) a GitHub Actions
|
|
357
|
+
* **Environment** on the config's repo — `gh api --method PUT
|
|
358
|
+
* repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
|
|
359
|
+
* and `production` before wiring their scoped Variables, since a Variable can
|
|
360
|
+
* only be set on an environment that already exists. PUT is idempotent, so
|
|
361
|
+
* re-running `init` is safe.
|
|
362
|
+
*/
|
|
363
|
+
export declare function ghEnvironmentCreateArgs(cfg: DeployConfig, ghEnvironment: string): string[];
|
|
214
364
|
/**
|
|
215
365
|
* The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
|
|
216
366
|
* The IaC sets these on the function; surfaced here for `rastack deploy` to
|
package/dist/deploy/config.js
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* thin IO wrappers that read/write the file and shell out.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = void 0;
|
|
21
|
+
exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = exports.PROD_GH_ENVIRONMENT = exports.DEV_DEFAULTS = exports.ENV_NAMES = void 0;
|
|
22
|
+
exports.envAppName = envAppName;
|
|
23
|
+
exports.resolveEnvironment = resolveEnvironment;
|
|
24
|
+
exports.resolveEnvironments = resolveEnvironments;
|
|
25
|
+
exports.cognitoRuntimeConfig = cognitoRuntimeConfig;
|
|
22
26
|
exports.normalizeConfig = normalizeConfig;
|
|
23
27
|
exports.validateConfig = validateConfig;
|
|
24
28
|
exports.validateBootstrapConfig = validateBootstrapConfig;
|
|
@@ -26,15 +30,83 @@ exports.mergeConfig = mergeConfig;
|
|
|
26
30
|
exports.parseGitHubRemote = parseGitHubRemote;
|
|
27
31
|
exports.cognitoAdvancedSecuritySupported = cognitoAdvancedSecuritySupported;
|
|
28
32
|
exports.stackParameters = stackParameters;
|
|
33
|
+
exports.stackParametersForEnv = stackParametersForEnv;
|
|
29
34
|
exports.deployStackArgs = deployStackArgs;
|
|
30
35
|
exports.deployStackCommand = deployStackCommand;
|
|
36
|
+
exports.deployStackArgsForEnv = deployStackArgsForEnv;
|
|
37
|
+
exports.deployStackCommandForEnv = deployStackCommandForEnv;
|
|
31
38
|
exports.bootstrapStackName = bootstrapStackName;
|
|
32
39
|
exports.bootstrapStackParameters = bootstrapStackParameters;
|
|
33
40
|
exports.bootstrapStackArgs = bootstrapStackArgs;
|
|
34
41
|
exports.bootstrapStackCommand = bootstrapStackCommand;
|
|
35
42
|
exports.repoVariables = repoVariables;
|
|
43
|
+
exports.repoVariablesForEnv = repoVariablesForEnv;
|
|
36
44
|
exports.ghVariableSetArgs = ghVariableSetArgs;
|
|
45
|
+
exports.ghEnvironmentCreateArgs = ghEnvironmentCreateArgs;
|
|
37
46
|
exports.lambdaEnv = lambdaEnv;
|
|
47
|
+
exports.ENV_NAMES = ["prod", "dev"];
|
|
48
|
+
/** Defaults for the dev environment when `environments.dev` is unset. */
|
|
49
|
+
exports.DEV_DEFAULTS = {
|
|
50
|
+
branch: "development",
|
|
51
|
+
ghEnvironment: "development",
|
|
52
|
+
namespace: "development",
|
|
53
|
+
};
|
|
54
|
+
/** The GitHub Actions Environment name the prod deploy uses. */
|
|
55
|
+
exports.PROD_GH_ENVIRONMENT = "production";
|
|
56
|
+
/**
|
|
57
|
+
* The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
|
|
58
|
+
* `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
|
|
59
|
+
* from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
|
|
60
|
+
*/
|
|
61
|
+
function envAppName(cfg, env) {
|
|
62
|
+
return env === "dev" ? `${cfg.app}dev` : cfg.app;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve one environment to its full definition: the prod environment inherits
|
|
66
|
+
* the top-level branch/stack/namespace/outputs, the dev environment defaults to
|
|
67
|
+
* the `development` branch and a `-dev` stack, and anything under
|
|
68
|
+
* `cfg.environments[env]` overrides those.
|
|
69
|
+
*/
|
|
70
|
+
function resolveEnvironment(cfg, env) {
|
|
71
|
+
const o = cfg.environments?.[env] ?? {};
|
|
72
|
+
if (env === "prod") {
|
|
73
|
+
return {
|
|
74
|
+
name: "prod",
|
|
75
|
+
branch: o.branch ?? cfg.github.branch,
|
|
76
|
+
ghEnvironment: o.ghEnvironment ?? exports.PROD_GH_ENVIRONMENT,
|
|
77
|
+
appName: cfg.app,
|
|
78
|
+
stackName: o.stackName ?? cfg.stackName,
|
|
79
|
+
namespace: o.namespace ?? cfg.namespace,
|
|
80
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
81
|
+
outputs: o.outputs ?? cfg.outputs,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: "dev",
|
|
86
|
+
branch: o.branch ?? exports.DEV_DEFAULTS.branch,
|
|
87
|
+
ghEnvironment: o.ghEnvironment ?? exports.DEV_DEFAULTS.ghEnvironment,
|
|
88
|
+
appName: envAppName(cfg, "dev"),
|
|
89
|
+
stackName: o.stackName ?? (cfg.stackName ? `${cfg.stackName}-dev` : ""),
|
|
90
|
+
namespace: o.namespace ?? exports.DEV_DEFAULTS.namespace,
|
|
91
|
+
proxyUpstream: o.proxyUpstream ?? "",
|
|
92
|
+
outputs: o.outputs,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Both environments, prod first, fully resolved. */
|
|
96
|
+
function resolveEnvironments(cfg) {
|
|
97
|
+
return exports.ENV_NAMES.map((e) => resolveEnvironment(cfg, e));
|
|
98
|
+
}
|
|
99
|
+
function cognitoRuntimeConfig(cfg, env) {
|
|
100
|
+
const o = resolveEnvironment(cfg, env).outputs;
|
|
101
|
+
if (!o?.userPoolClientId || !o?.hostedUiUrl)
|
|
102
|
+
return null;
|
|
103
|
+
return {
|
|
104
|
+
userPoolId: o.userPoolId,
|
|
105
|
+
clientId: o.userPoolClientId,
|
|
106
|
+
hostedUiUrl: o.hostedUiUrl,
|
|
107
|
+
region: cfg.region,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
38
110
|
exports.DEFAULT_LAMBDA = {
|
|
39
111
|
bin: "bootstrap",
|
|
40
112
|
arch: "arm64",
|
|
@@ -82,8 +154,36 @@ function normalizeConfig(partial = {}) {
|
|
|
82
154
|
artifactsBucket: partial.artifactsBucket?.trim() || undefined,
|
|
83
155
|
tokenUse: partial.tokenUse ?? exports.DEFAULTS.tokenUse,
|
|
84
156
|
outputs: partial.outputs ? { ...partial.outputs } : undefined,
|
|
157
|
+
environments: normalizeEnvironments(partial.environments),
|
|
85
158
|
};
|
|
86
159
|
}
|
|
160
|
+
/** Copy through per-environment overrides, dropping an empty map to undefined. */
|
|
161
|
+
function normalizeEnvironments(envs) {
|
|
162
|
+
if (!envs)
|
|
163
|
+
return undefined;
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const name of exports.ENV_NAMES) {
|
|
166
|
+
const o = envs[name];
|
|
167
|
+
if (!o)
|
|
168
|
+
continue;
|
|
169
|
+
const entry = {};
|
|
170
|
+
if (o.branch?.trim())
|
|
171
|
+
entry.branch = o.branch.trim();
|
|
172
|
+
if (o.ghEnvironment?.trim())
|
|
173
|
+
entry.ghEnvironment = o.ghEnvironment.trim();
|
|
174
|
+
if (o.stackName?.trim())
|
|
175
|
+
entry.stackName = o.stackName.trim();
|
|
176
|
+
if (o.namespace?.trim())
|
|
177
|
+
entry.namespace = o.namespace.trim();
|
|
178
|
+
if (o.proxyUpstream?.trim())
|
|
179
|
+
entry.proxyUpstream = o.proxyUpstream.trim();
|
|
180
|
+
if (o.outputs)
|
|
181
|
+
entry.outputs = { ...o.outputs };
|
|
182
|
+
if (Object.keys(entry).length)
|
|
183
|
+
out[name] = entry;
|
|
184
|
+
}
|
|
185
|
+
return Object.keys(out).length ? out : undefined;
|
|
186
|
+
}
|
|
87
187
|
const SLUG = /^[a-z0-9]{1,20}$/;
|
|
88
188
|
const REGION = /^[a-z]{2}-[a-z]+-\d+$/;
|
|
89
189
|
const GH_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
@@ -99,6 +199,12 @@ function validateConfig(cfg) {
|
|
|
99
199
|
errors.push("app is required (a short slug, e.g. `airline`).");
|
|
100
200
|
else if (!SLUG.test(cfg.app))
|
|
101
201
|
errors.push(`app "${cfg.app}" must match ${SLUG} (lowercase letters/digits, 1–20).`);
|
|
202
|
+
// The dev environment appends "dev" to the app slug for its infra namespace
|
|
203
|
+
// (buckets/roles/pools/SSM). That derived name must still be a valid slug, so
|
|
204
|
+
// the app slug is effectively capped at 17 chars once a dev env is in play.
|
|
205
|
+
else if (!SLUG.test(envAppName(cfg, "dev")))
|
|
206
|
+
errors.push(`app "${cfg.app}" is too long: the dev environment uses "${envAppName(cfg, "dev")}" ` +
|
|
207
|
+
`for its infra names, which must still match ${SLUG} (so app ≤ 17 chars).`);
|
|
102
208
|
if (!SLUG.test(cfg.namePrefix))
|
|
103
209
|
errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
|
|
104
210
|
if (!cfg.region)
|
|
@@ -158,8 +264,27 @@ function mergeConfig(base, patch) {
|
|
|
158
264
|
github: { ...base.github, ...patch.github },
|
|
159
265
|
lambda: { ...base.lambda, ...patch.lambda },
|
|
160
266
|
outputs: { ...base.outputs, ...patch.outputs },
|
|
267
|
+
environments: mergeEnvironments(base.environments, patch.environments),
|
|
161
268
|
});
|
|
162
269
|
}
|
|
270
|
+
/** Deep-merge per-environment overrides (per env, and per env's outputs). */
|
|
271
|
+
function mergeEnvironments(base, patch) {
|
|
272
|
+
if (!base && !patch)
|
|
273
|
+
return undefined;
|
|
274
|
+
const out = {};
|
|
275
|
+
for (const name of exports.ENV_NAMES) {
|
|
276
|
+
const b = base?.[name];
|
|
277
|
+
const p = patch?.[name];
|
|
278
|
+
if (!b && !p)
|
|
279
|
+
continue;
|
|
280
|
+
out[name] = {
|
|
281
|
+
...b,
|
|
282
|
+
...p,
|
|
283
|
+
outputs: b?.outputs || p?.outputs ? { ...b?.outputs, ...p?.outputs } : undefined,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return Object.keys(out).length ? out : undefined;
|
|
287
|
+
}
|
|
163
288
|
/**
|
|
164
289
|
* Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
|
|
165
290
|
* (`git@github.com:owner/repo.git`) and HTTPS
|
|
@@ -232,6 +357,36 @@ function stackParameters(cfg) {
|
|
|
232
357
|
params.push({ key: "AdvancedSecurity", value: "OFF" });
|
|
233
358
|
return params;
|
|
234
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* The app.yaml `--parameter-overrides` for a **specific environment** (dev or
|
|
362
|
+
* prod). Same as `stackParameters` but the AppName/branch/namespace come from the
|
|
363
|
+
* resolved environment, and it also passes `GitHubEnvironment` so the stack's
|
|
364
|
+
* deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
|
|
365
|
+
* for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
|
|
366
|
+
* AppName is the bare slug, so prod's resource names are byte-for-byte what the
|
|
367
|
+
* pre-environment template produced.
|
|
368
|
+
*/
|
|
369
|
+
function stackParametersForEnv(cfg, env) {
|
|
370
|
+
const e = resolveEnvironment(cfg, env);
|
|
371
|
+
const params = [
|
|
372
|
+
{ key: "AppName", value: e.appName },
|
|
373
|
+
{ key: "NamePrefix", value: cfg.namePrefix },
|
|
374
|
+
{ key: "GitHubOwner", value: cfg.github.owner },
|
|
375
|
+
{ key: "GitHubRepo", value: cfg.github.repo },
|
|
376
|
+
{ key: "GitHubBranch", value: e.branch },
|
|
377
|
+
{ key: "GitHubEnvironment", value: e.ghEnvironment },
|
|
378
|
+
{ key: "IcebergNamespace", value: e.namespace },
|
|
379
|
+
];
|
|
380
|
+
if (cfg.artifactsBucket)
|
|
381
|
+
params.push({ key: "ArtifactsBucket", value: cfg.artifactsBucket });
|
|
382
|
+
// The external-API proxy is provisioned only when an upstream is configured
|
|
383
|
+
// (typically dev → a UAT/prod backend). Empty leaves the route/authorizer out.
|
|
384
|
+
if (e.proxyUpstream)
|
|
385
|
+
params.push({ key: "ExternalApiUpstream", value: e.proxyUpstream });
|
|
386
|
+
if (!cognitoAdvancedSecuritySupported(cfg.region))
|
|
387
|
+
params.push({ key: "AdvancedSecurity", value: "OFF" });
|
|
388
|
+
return params;
|
|
389
|
+
}
|
|
235
390
|
/**
|
|
236
391
|
* The canonical repo-relative CloudFormation template paths. These are the paths
|
|
237
392
|
* in the *framework* repo (theserverkid/reactapistack) and the default the argv
|
|
@@ -296,6 +451,14 @@ function deployStackArgs(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
|
296
451
|
function deployStackCommand(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
297
452
|
return awsCommand(deployStackArgs(cfg, templateFile));
|
|
298
453
|
}
|
|
454
|
+
/** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
|
|
455
|
+
function deployStackArgsForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
456
|
+
return cfnDeployArgs(templateFile, resolveEnvironment(cfg, env).stackName, cfg.region, stackParametersForEnv(cfg, env));
|
|
457
|
+
}
|
|
458
|
+
/** The single-line `aws cloudformation deploy` command for one environment. */
|
|
459
|
+
function deployStackCommandForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
|
|
460
|
+
return awsCommand(deployStackArgsForEnv(cfg, env, templateFile));
|
|
461
|
+
}
|
|
299
462
|
/**
|
|
300
463
|
* The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
|
|
301
464
|
* Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
|
|
@@ -346,12 +509,16 @@ exports.APP_STACK_OUTPUTS = [
|
|
|
346
509
|
{ key: "UserPoolClientId", field: "userPoolClientId" },
|
|
347
510
|
{ key: "JwksUrl", field: "jwksUrl" },
|
|
348
511
|
{ key: "WarehouseBucketName", field: "warehouseBucket" },
|
|
512
|
+
{ key: "UserPoolDomain", field: "userPoolDomain" },
|
|
513
|
+
{ key: "HostedUiUrl", field: "hostedUiUrl" },
|
|
514
|
+
{ key: "ProxyUrl", field: "proxyUrl" },
|
|
349
515
|
];
|
|
350
516
|
/**
|
|
351
517
|
* The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
|
|
352
518
|
* `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
|
|
353
519
|
* Outputs when already resolved, else an empty string to fill in from the
|
|
354
|
-
* Console.
|
|
520
|
+
* Console. This is the legacy single-environment view (the prod stack); the
|
|
521
|
+
* dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
|
|
355
522
|
*/
|
|
356
523
|
function repoVariables(cfg) {
|
|
357
524
|
return [
|
|
@@ -363,6 +530,29 @@ function repoVariables(cfg) {
|
|
|
363
530
|
{ name: "AWS_REGION", value: cfg.region, from: "Region stack output" },
|
|
364
531
|
];
|
|
365
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* The same two Variables, scoped to one environment's GitHub Actions
|
|
535
|
+
* Environment. The dev deploy reads `development`'s copies, the prod deploy
|
|
536
|
+
* `production`'s — so the two never cross-assume each other's role. Values come
|
|
537
|
+
* from the environment's own stack Outputs.
|
|
538
|
+
*/
|
|
539
|
+
function repoVariablesForEnv(cfg, env) {
|
|
540
|
+
const e = resolveEnvironment(cfg, env);
|
|
541
|
+
return [
|
|
542
|
+
{
|
|
543
|
+
name: "AWS_DEPLOY_ROLE_ARN",
|
|
544
|
+
value: e.outputs?.deployRoleArn ?? "",
|
|
545
|
+
from: "DeployRoleArn stack output",
|
|
546
|
+
ghEnvironment: e.ghEnvironment,
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "AWS_REGION",
|
|
550
|
+
value: cfg.region,
|
|
551
|
+
from: "Region stack output",
|
|
552
|
+
ghEnvironment: e.ghEnvironment,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
}
|
|
366
556
|
/**
|
|
367
557
|
* The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
|
|
368
558
|
* repo. `init` runs this for each resolved `repoVariables` entry so the
|
|
@@ -372,15 +562,35 @@ function repoVariables(cfg) {
|
|
|
372
562
|
* user's GitHub) rather than an API token; a pure argv so it is inspectable and
|
|
373
563
|
* shell-free (`execFileSync`).
|
|
374
564
|
*/
|
|
375
|
-
function ghVariableSetArgs(cfg, name, value) {
|
|
376
|
-
|
|
565
|
+
function ghVariableSetArgs(cfg, name, value, ghEnvironment) {
|
|
566
|
+
const args = [
|
|
377
567
|
"variable",
|
|
378
568
|
"set",
|
|
379
569
|
name,
|
|
380
570
|
"--repo",
|
|
381
571
|
`${cfg.github.owner}/${cfg.github.repo}`,
|
|
382
|
-
|
|
383
|
-
|
|
572
|
+
];
|
|
573
|
+
// Scope the Variable to a GitHub Actions Environment when one is given, so the
|
|
574
|
+
// dev and prod deploys each read their own role/region.
|
|
575
|
+
if (ghEnvironment)
|
|
576
|
+
args.push("--env", ghEnvironment);
|
|
577
|
+
args.push("--body", value);
|
|
578
|
+
return args;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The argv (after `gh`) that creates (idempotently) a GitHub Actions
|
|
582
|
+
* **Environment** on the config's repo — `gh api --method PUT
|
|
583
|
+
* repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
|
|
584
|
+
* and `production` before wiring their scoped Variables, since a Variable can
|
|
585
|
+
* only be set on an environment that already exists. PUT is idempotent, so
|
|
586
|
+
* re-running `init` is safe.
|
|
587
|
+
*/
|
|
588
|
+
function ghEnvironmentCreateArgs(cfg, ghEnvironment) {
|
|
589
|
+
return [
|
|
590
|
+
"api",
|
|
591
|
+
"--method",
|
|
592
|
+
"PUT",
|
|
593
|
+
`repos/${cfg.github.owner}/${cfg.github.repo}/environments/${ghEnvironment}`,
|
|
384
594
|
];
|
|
385
595
|
}
|
|
386
596
|
/**
|
package/dist/deploy/io.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare function ghAuthLogin(cwd?: string): boolean;
|
|
|
75
75
|
* into a command line.
|
|
76
76
|
*/
|
|
77
77
|
export declare function setRepoVariable(args: string[], cwd?: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
|
|
80
|
+
* so its scoped Variables can be set. Returns true on success. Best-effort — a
|
|
81
|
+
* missing/unauthenticated `gh` or an API error degrades to the printed
|
|
82
|
+
* instructions, exactly like `setRepoVariable`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function ghEnvironmentCreate(args: string[], cwd?: string): boolean;
|
|
78
85
|
/**
|
|
79
86
|
* Ask a single question with a default, echoed as `label [default]: `. Returns
|
|
80
87
|
* the trimmed answer, or the default on an empty line.
|