rastack 0.0.62 → 0.0.64
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 +14 -0
- package/admin/AdminApp.tsx +56 -3
- package/admin/index.tsx +8 -2
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/auth/provider.ts +71 -8
- package/dist/admin.js +17 -14
- package/dist/compile/analyze.js +11 -1
- package/dist/define/index.d.ts +8 -1
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- 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 +57 -0
- package/dist/deploy/io.js +141 -0
- package/dist/deploy/workflows.d.ts +30 -0
- package/dist/deploy/workflows.js +117 -12
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-ci.js +19 -1
- package/dist/rastack-dev.js +48 -2
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +231 -92
- package/dist/rastack.js +1 -0
- package/dist/templates/cloudformation/app.yaml +133 -2
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/provider.tsx +23 -0
- package/provider/types.ts +9 -0
- package/src/compile/analyze.ts +11 -1
- package/src/define/index.ts +5 -1
- package/src/define/manifest.ts +7 -1
- package/src/deploy/ci.ts +50 -16
- package/src/deploy/config.ts +340 -7
- package/src/deploy/io.ts +179 -2
- package/src/deploy/workflows.ts +122 -12
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-ci.ts +20 -2
- package/src/rastack-dev.ts +61 -6
- package/src/rastack-init.ts +283 -115
- package/src/rastack.ts +1 -0
|
@@ -34,7 +34,44 @@ Parameters:
|
|
|
34
34
|
GitHubBranch:
|
|
35
35
|
Type: String
|
|
36
36
|
Default: master
|
|
37
|
-
Description: Branch of GitHubRepo allowed to deploy.
|
|
37
|
+
Description: Branch of GitHubRepo allowed to deploy (used when GitHubEnvironment is blank).
|
|
38
|
+
|
|
39
|
+
GitHubEnvironment:
|
|
40
|
+
Type: String
|
|
41
|
+
Default: ""
|
|
42
|
+
Description: >
|
|
43
|
+
GitHub Actions **Environment** (e.g. development / production) this stack's
|
|
44
|
+
deploy role trusts. When set, the OIDC trust is scoped to
|
|
45
|
+
`…:environment:<name>` — the subject a job that declares `environment:` gets
|
|
46
|
+
— instead of a branch ref, so the dev and prod deploys can share a branch
|
|
47
|
+
set yet only assume their own environment's role. Blank keeps the legacy
|
|
48
|
+
branch-ref trust (`…:ref:refs/heads/<GitHubBranch>`). `rastack init` sets it
|
|
49
|
+
to development / production for the dev / prod stacks.
|
|
50
|
+
|
|
51
|
+
CallbackUrls:
|
|
52
|
+
Type: CommaDelimitedList
|
|
53
|
+
Default: "http://localhost:4321"
|
|
54
|
+
Description: >
|
|
55
|
+
Allowed OAuth redirect (callback) URLs for the Hosted UI app client. The
|
|
56
|
+
local `rastack dev` origin is the default so a developer can sign in against
|
|
57
|
+
this environment's real Cognito pool while developing; add your deployed
|
|
58
|
+
frontend origin(s) too.
|
|
59
|
+
|
|
60
|
+
LogoutUrls:
|
|
61
|
+
Type: CommaDelimitedList
|
|
62
|
+
Default: "http://localhost:4321"
|
|
63
|
+
Description: Allowed sign-out redirect URLs for the Hosted UI app client.
|
|
64
|
+
|
|
65
|
+
ExternalApiUpstream:
|
|
66
|
+
Type: String
|
|
67
|
+
Default: ""
|
|
68
|
+
Description: >
|
|
69
|
+
Base URL (no trailing slash) the **external-API proxy** forwards to —
|
|
70
|
+
typically a UAT or prod backend. When set, the stack adds a
|
|
71
|
+
Cognito-authorized `ANY /proxy/{proxy+}` route to the HTTP API that forwards
|
|
72
|
+
to `<upstream>/{proxy}`, so a locally-running app (signed in against this
|
|
73
|
+
pool) reaches external APIs only through the proxy, never directly. Blank
|
|
74
|
+
omits the proxy route + authorizer entirely.
|
|
38
75
|
|
|
39
76
|
ArtifactsBucket:
|
|
40
77
|
Type: String
|
|
@@ -87,6 +124,10 @@ Conditions:
|
|
|
87
124
|
DeriveOIDCArn: !Equals [!Ref OIDCProviderArn, ""]
|
|
88
125
|
ShouldTrailDataEvents: !Equals [!Ref EnableDataEventTrail, "true"]
|
|
89
126
|
UseAdvancedSecurity: !Not [!Equals [!Ref AdvancedSecurity, "OFF"]]
|
|
127
|
+
# Scope the deploy-role trust to a GitHub Actions Environment when one is given.
|
|
128
|
+
TrustEnvironment: !Not [!Equals [!Ref GitHubEnvironment, ""]]
|
|
129
|
+
# Provision the external-API proxy route only when an upstream is configured.
|
|
130
|
+
HasProxy: !Not [!Equals [!Ref ExternalApiUpstream, ""]]
|
|
90
131
|
|
|
91
132
|
Resources:
|
|
92
133
|
|
|
@@ -290,6 +331,20 @@ Resources:
|
|
|
290
331
|
- ALLOW_USER_SRP_AUTH
|
|
291
332
|
- ALLOW_REFRESH_TOKEN_AUTH
|
|
292
333
|
PreventUserExistenceErrors: ENABLED
|
|
334
|
+
# Hosted UI (OAuth 2.0 Authorization Code + PKCE) — the flow rastack/auth's
|
|
335
|
+
# <CognitoAuthProvider> drives. Enabling it here lets a locally-running app
|
|
336
|
+
# (`rastack dev`) sign in against this environment's real Cognito pool.
|
|
337
|
+
SupportedIdentityProviders:
|
|
338
|
+
- COGNITO
|
|
339
|
+
AllowedOAuthFlowsUserPoolClient: true
|
|
340
|
+
AllowedOAuthFlows:
|
|
341
|
+
- code
|
|
342
|
+
AllowedOAuthScopes:
|
|
343
|
+
- openid
|
|
344
|
+
- email
|
|
345
|
+
- profile
|
|
346
|
+
CallbackURLs: !Ref CallbackUrls
|
|
347
|
+
LogoutURLs: !Ref LogoutUrls
|
|
293
348
|
TokenValidityUnits:
|
|
294
349
|
AccessToken: minutes
|
|
295
350
|
IdToken: minutes
|
|
@@ -299,6 +354,15 @@ Resources:
|
|
|
299
354
|
RefreshTokenValidity: 30
|
|
300
355
|
EnableTokenRevocation: true
|
|
301
356
|
|
|
357
|
+
# Hosted UI domain — the browser sign-in surface. Prefix is globally unique via
|
|
358
|
+
# the account id. Both the deployed frontend and local `rastack dev` redirect
|
|
359
|
+
# here to authenticate against this environment's pool.
|
|
360
|
+
UserPoolDomain:
|
|
361
|
+
Type: AWS::Cognito::UserPoolDomain
|
|
362
|
+
Properties:
|
|
363
|
+
Domain: !Sub "${NamePrefix}-${AppName}-${AWS::AccountId}"
|
|
364
|
+
UserPoolId: !Ref UserPool
|
|
365
|
+
|
|
302
366
|
IdentityPool:
|
|
303
367
|
Type: AWS::Cognito::IdentityPool
|
|
304
368
|
Properties:
|
|
@@ -494,6 +558,49 @@ Resources:
|
|
|
494
558
|
Principal: apigateway.amazonaws.com
|
|
495
559
|
SourceArn: !Sub "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*/*"
|
|
496
560
|
|
|
561
|
+
# -- External-API proxy (optional) --------------------------------------------
|
|
562
|
+
# When ExternalApiUpstream is set, a Cognito-authorized `ANY /proxy/{proxy+}`
|
|
563
|
+
# route forwards to `<upstream>/{proxy}`. HTTP API prefers the more specific
|
|
564
|
+
# route, so `/proxy/*` goes here and everything else still hits the Rust Lambda
|
|
565
|
+
# via `$default`. The JWT authorizer means only callers holding a valid token
|
|
566
|
+
# from THIS environment's pool can use the proxy — so a local dev app signs in
|
|
567
|
+
# against the dev pool, then reaches UAT/prod externals only through here.
|
|
568
|
+
ProxyJwtAuthorizer:
|
|
569
|
+
Type: AWS::ApiGatewayV2::Authorizer
|
|
570
|
+
Condition: HasProxy
|
|
571
|
+
Properties:
|
|
572
|
+
ApiId: !Ref HttpApi
|
|
573
|
+
AuthorizerType: JWT
|
|
574
|
+
Name: !Sub "${NamePrefix}-${AppName}-cognito"
|
|
575
|
+
IdentitySource:
|
|
576
|
+
- "$request.header.Authorization"
|
|
577
|
+
JwtConfiguration:
|
|
578
|
+
Audience:
|
|
579
|
+
- !Ref UserPoolClient
|
|
580
|
+
Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
|
|
581
|
+
|
|
582
|
+
ProxyIntegration:
|
|
583
|
+
Type: AWS::ApiGatewayV2::Integration
|
|
584
|
+
Condition: HasProxy
|
|
585
|
+
Properties:
|
|
586
|
+
ApiId: !Ref HttpApi
|
|
587
|
+
IntegrationType: HTTP_PROXY
|
|
588
|
+
IntegrationMethod: ANY
|
|
589
|
+
# `{proxy}` is the greedy path variable captured by the route; the caller's
|
|
590
|
+
# Authorization header is forwarded so the upstream can authorize too.
|
|
591
|
+
IntegrationUri: !Sub "${ExternalApiUpstream}/{proxy}"
|
|
592
|
+
PayloadFormatVersion: "1.0"
|
|
593
|
+
|
|
594
|
+
ProxyRoute:
|
|
595
|
+
Type: AWS::ApiGatewayV2::Route
|
|
596
|
+
Condition: HasProxy
|
|
597
|
+
Properties:
|
|
598
|
+
ApiId: !Ref HttpApi
|
|
599
|
+
RouteKey: "ANY /proxy/{proxy+}"
|
|
600
|
+
Target: !Sub "integrations/${ProxyIntegration}"
|
|
601
|
+
AuthorizationType: JWT
|
|
602
|
+
AuthorizerId: !Ref ProxyJwtAuthorizer
|
|
603
|
+
|
|
497
604
|
# -- Deploy role (GitHub OIDC) — code only ------------------------------------
|
|
498
605
|
# CI assumes this to ship compiled code. It can UpdateFunctionCode on THIS one
|
|
499
606
|
# function and read this app's SSM params — nothing else — with an explicit
|
|
@@ -518,7 +625,13 @@ Resources:
|
|
|
518
625
|
StringEquals:
|
|
519
626
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
520
627
|
StringLike:
|
|
521
|
-
|
|
628
|
+
# An environment-gated GitHub job's OIDC subject is
|
|
629
|
+
# `repo:O/R:environment:<env>`, not `…:ref:refs/heads/<branch>`,
|
|
630
|
+
# so the trust must match the environment when one is set.
|
|
631
|
+
token.actions.githubusercontent.com:sub: !If
|
|
632
|
+
- TrustEnvironment
|
|
633
|
+
- !Sub "repo:${GitHubOwner}/${GitHubRepo}:environment:${GitHubEnvironment}"
|
|
634
|
+
- !Sub "repo:${GitHubOwner}/${GitHubRepo}:ref:refs/heads/${GitHubBranch}"
|
|
522
635
|
Policies:
|
|
523
636
|
- PolicyName: update-function-code-only
|
|
524
637
|
PolicyDocument:
|
|
@@ -602,6 +715,14 @@ Resources:
|
|
|
602
715
|
Type: String
|
|
603
716
|
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json"
|
|
604
717
|
|
|
718
|
+
ParamHostedUiUrl:
|
|
719
|
+
Type: AWS::SSM::Parameter
|
|
720
|
+
DependsOn: UserPoolDomain
|
|
721
|
+
Properties:
|
|
722
|
+
Name: !Sub "/rastack/${AppName}/hosted-ui-url"
|
|
723
|
+
Type: String
|
|
724
|
+
Value: !Sub "https://${NamePrefix}-${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com"
|
|
725
|
+
|
|
605
726
|
Outputs:
|
|
606
727
|
ApiUrl:
|
|
607
728
|
Description: Base URL of the deployed API (set as the frontend's remote base).
|
|
@@ -627,9 +748,19 @@ Outputs:
|
|
|
627
748
|
JwksUrl:
|
|
628
749
|
Description: JWKS to bake into the Lambda bundle at build time.
|
|
629
750
|
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json"
|
|
751
|
+
UserPoolDomain:
|
|
752
|
+
Description: Cognito Hosted UI domain prefix (scheme-less).
|
|
753
|
+
Value: !Ref UserPoolDomain
|
|
754
|
+
HostedUiUrl:
|
|
755
|
+
Description: Cognito Hosted UI base URL — browser sign-in / `rastack dev` auth.
|
|
756
|
+
Value: !Sub "https://${NamePrefix}-${AppName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com"
|
|
630
757
|
WarehouseBucketName:
|
|
631
758
|
Description: S3 Iceberg warehouse — the system of record.
|
|
632
759
|
Value: !Ref WarehouseBucket
|
|
633
760
|
AuditLogBucketName:
|
|
634
761
|
Description: S3 access logs + CloudTrail data events for the warehouse.
|
|
635
762
|
Value: !Ref AuditLogBucket
|
|
763
|
+
ProxyUrl:
|
|
764
|
+
Condition: HasProxy
|
|
765
|
+
Description: Base URL of the Cognito-authorized external-API proxy (when enabled).
|
|
766
|
+
Value: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/proxy"
|
|
@@ -375,7 +375,7 @@ function __wbg_get_imports() {
|
|
|
375
375
|
return ret;
|
|
376
376
|
},
|
|
377
377
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
378
|
-
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx:
|
|
378
|
+
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 90, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
379
379
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_);
|
|
380
380
|
return ret;
|
|
381
381
|
},
|
|
Binary file
|
package/package.json
CHANGED
package/provider/provider.tsx
CHANGED
|
@@ -170,6 +170,11 @@ export function RAStackProvider({
|
|
|
170
170
|
const existing = await persistence.load();
|
|
171
171
|
if (existing) {
|
|
172
172
|
await engine.importWarehouse(existing);
|
|
173
|
+
} else if (config.seedWarehouse) {
|
|
174
|
+
// Seed from a bundled warehouse blob (exportWarehouse format) rather
|
|
175
|
+
// than loose Iceberg files.
|
|
176
|
+
const seed = await config.seedWarehouse();
|
|
177
|
+
if (seed) await engine.importWarehouse(seed);
|
|
173
178
|
} else {
|
|
174
179
|
const base = config.warehouseBaseUrl ?? "/data/warehouse/";
|
|
175
180
|
const resources = manifestResources(manifest);
|
|
@@ -208,6 +213,24 @@ export function RAStackProvider({
|
|
|
208
213
|
}, 400);
|
|
209
214
|
persistRef.current = persist;
|
|
210
215
|
|
|
216
|
+
// Persist at the *engine* level, not just in the axios adapter. Consumers
|
|
217
|
+
// that talk to the engine directly (the admin console calls
|
|
218
|
+
// `engine.handle("DELETE", …)` straight, bypassing the generated hooks /
|
|
219
|
+
// axios seam) must still have their writes saved to IndexedDB — otherwise
|
|
220
|
+
// every reload re-seeds from the committed warehouse and silently
|
|
221
|
+
// resurrects deleted rows. Wrapping `handle` here makes persistence fire
|
|
222
|
+
// for both paths (the adapter's `onWrite` below then coalesces into the
|
|
223
|
+
// same debounce, so an app-hook write persists exactly once).
|
|
224
|
+
const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
225
|
+
const rawHandle = engine.handle.bind(engine);
|
|
226
|
+
engine.handle = ((method: string, path: string, query?: any, body?: any) =>
|
|
227
|
+
Promise.resolve(rawHandle(method, path, query, body)).then((res: any) => {
|
|
228
|
+
if (MUTATING.has(String(method).toUpperCase()) && res && res.status < 300) {
|
|
229
|
+
persist();
|
|
230
|
+
}
|
|
231
|
+
return res;
|
|
232
|
+
})) as typeof engine.handle;
|
|
233
|
+
|
|
211
234
|
engineRef.current = engine;
|
|
212
235
|
configureApi({
|
|
213
236
|
baseURL: "",
|
package/provider/types.ts
CHANGED
|
@@ -152,6 +152,15 @@ export interface RAStackProviderConfig {
|
|
|
152
152
|
fileSource?: import("../cache").WarehouseFileSource;
|
|
153
153
|
/** `s3` mode: how to load/save the warehouse blob from object storage. */
|
|
154
154
|
warehouse?: WarehouseSource;
|
|
155
|
+
/**
|
|
156
|
+
* Seed `local` mode from a **bundled warehouse blob** — the format
|
|
157
|
+
* `exportWarehouse()` produces and `importWarehouse()` consumes — instead of
|
|
158
|
+
* walking loose Iceberg files under `warehouseBaseUrl`. Use this when the app
|
|
159
|
+
* commits a single warehouse blob asset (e.g. bundled JSON) rather than a
|
|
160
|
+
* directory of parquet files. Takes precedence over `warehouseBaseUrl` /
|
|
161
|
+
* `fileSource` seeding, and is ignored once a persisted local copy exists.
|
|
162
|
+
*/
|
|
163
|
+
seedWarehouse?: () => Uint8Array | null | Promise<Uint8Array | null>;
|
|
155
164
|
/**
|
|
156
165
|
* Override how the WASM module is loaded. Supply this from app code (where
|
|
157
166
|
* your bundler can resolve the generated bundle) if the default loader can't
|
package/src/compile/analyze.ts
CHANGED
|
@@ -179,7 +179,17 @@ function analyzeField(
|
|
|
179
179
|
const app = literalTypeProp(fieldType, "__app", checker, initializer);
|
|
180
180
|
const model = literalTypeProp(fieldType, "__model", checker, initializer);
|
|
181
181
|
if (app && model) {
|
|
182
|
-
|
|
182
|
+
const fk: FieldModel = { name, type: "fk", relation: { app, model } };
|
|
183
|
+
// `s.ref(() => X, { null: true })` — a nullable relation (a shared secret
|
|
184
|
+
// with no project, an unassigned task). The options object is the ref
|
|
185
|
+
// call's second argument, read the same way scalar constraints are.
|
|
186
|
+
if (ts.isCallExpression(initializer)) {
|
|
187
|
+
const opts = literalToValue(initializer.arguments[1]);
|
|
188
|
+
if (opts && typeof opts === "object" && opts.null === true) {
|
|
189
|
+
fk.null = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return fk;
|
|
183
193
|
}
|
|
184
194
|
return undefined;
|
|
185
195
|
}
|
package/src/define/index.ts
CHANGED
|
@@ -66,6 +66,9 @@ export interface Ref<App extends string, Model extends string> {
|
|
|
66
66
|
readonly __model: Model;
|
|
67
67
|
/** The authored `() => Resource` thunk — carried at runtime. */
|
|
68
68
|
readonly target?: () => ResourceType<App, Model, any>;
|
|
69
|
+
/** Optional FK constraints — `null: true` makes the relation nullable
|
|
70
|
+
* (e.g. a shared secret with no project, or an unassigned task). */
|
|
71
|
+
readonly options?: { null?: boolean };
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface StringOptions {
|
|
@@ -153,8 +156,9 @@ export const s = {
|
|
|
153
156
|
*/
|
|
154
157
|
ref<A extends string, M extends string>(
|
|
155
158
|
thunk: () => ResourceType<A, M, any>,
|
|
159
|
+
options?: { null?: boolean },
|
|
156
160
|
): Ref<A, M> {
|
|
157
|
-
return { __rastackRef: true, target: thunk } as Ref<A, M>;
|
|
161
|
+
return { __rastackRef: true, target: thunk, options } as Ref<A, M>;
|
|
158
162
|
},
|
|
159
163
|
};
|
|
160
164
|
|
package/src/define/manifest.ts
CHANGED
|
@@ -121,11 +121,17 @@ function runtimeField(
|
|
|
121
121
|
`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`,
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
const fk: FieldModel = {
|
|
125
125
|
name,
|
|
126
126
|
type: "fk",
|
|
127
127
|
relation: { app: target.app, model: target.model },
|
|
128
128
|
};
|
|
129
|
+
// A nullable FK (`s.ref(() => X, { null: true })`) — e.g. a shared secret
|
|
130
|
+
// with no project — so the engine allows and stores a null relation.
|
|
131
|
+
if ((field.options as { null?: boolean } | undefined)?.null === true) {
|
|
132
|
+
fk.null = true;
|
|
133
|
+
}
|
|
134
|
+
return fk;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
if (typeof field.__rastackScalar !== "string") return undefined;
|
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;
|