rastack 0.0.33 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.35](https://github.com/theserverkid/reactapistack/compare/v0.0.34...v0.0.35) (2026-07-11)
6
+
7
+ ### [0.0.34](https://github.com/theserverkid/reactapistack/compare/v0.0.33...v0.0.34) (2026-07-11)
8
+
5
9
  ### [0.0.33](https://github.com/theserverkid/reactapistack/compare/v0.0.32...v0.0.33) (2026-07-11)
6
10
 
7
11
  ### [0.0.32](https://github.com/theserverkid/reactapistack/compare/v0.0.31...v0.0.32) (2026-07-10)
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `rastack ci` — the CI pipeline as a *plan of shell steps*, derived purely from
3
+ * the deploy config.
4
+ *
5
+ * The whole point is to make GitHub Actions cleaner: instead of thirty lines of
6
+ * inline bash (compile the manifest, cross-build the arm64 Lambda, bake the
7
+ * Cognito JWKS, assemble the zip, `update-function-code`, wait, print the URL),
8
+ * a workflow runs one line — `rastack ci deploy` — and this module owns the
9
+ * steps. Because the plan is a pure function of the config, it is fully testable
10
+ * without AWS or a Rust toolchain, and `rastack ci --plan` prints exactly what
11
+ * will run.
12
+ *
13
+ * Values that only exist after the stack is created (the function name, the API
14
+ * URL, the JWKS URL) are either taken from `cfg.outputs` when already resolved,
15
+ * or emitted as a live `$(aws cloudformation describe-stacks …)` substitution so
16
+ * the same plan works before and after they are wired into the config.
17
+ */
18
+ import { DeployConfig } from "./config";
19
+ export type CiPhase = "check" | "build" | "deploy";
20
+ export interface CiStep {
21
+ /** Human label, echoed before the step runs. */
22
+ name: string;
23
+ /** The shell command to execute. */
24
+ run: string;
25
+ /** Working directory relative to the repo root (default: repo root). */
26
+ cwd?: string;
27
+ }
28
+ export interface CiPlanOptions {
29
+ /** How to invoke the rastack CLI for nested steps (default: `rastack`). */
30
+ bin?: string;
31
+ /** Skip the build steps in `deploy` (code already built this job). */
32
+ skipBuild?: boolean;
33
+ /** Override the function name (else from cfg.outputs, else resolved live). */
34
+ functionName?: string;
35
+ /** Override the JWKS URL (else from cfg.outputs, else resolved live). */
36
+ jwksUrl?: string;
37
+ }
38
+ /** `check` — validate the resource graph and compile the manifest. A CI gate. */
39
+ export declare function checkSteps(cfg: DeployConfig, bin: string): CiStep[];
40
+ /**
41
+ * `build` — produce the deployable Lambda bundle: compile the manifest,
42
+ * cross-build the custom-runtime binary, bake the Cognito JWKS in, and zip the
43
+ * binary together with the files it reads at `/var/task`.
44
+ */
45
+ export declare function buildSteps(cfg: DeployConfig, opts?: CiPlanOptions): CiStep[];
46
+ /**
47
+ * `deploy` — the whole CI job: build (unless already built), then ship the code
48
+ * with the single privileged call CI is allowed, `update-function-code`, wait
49
+ * for it to settle, and print the API URL.
50
+ */
51
+ export declare function deploySteps(cfg: DeployConfig, opts?: CiPlanOptions): CiStep[];
52
+ /** Build the ordered step list for a phase. */
53
+ export declare function ciPlan(phase: CiPhase, cfg: DeployConfig, opts?: CiPlanOptions): CiStep[];
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * `rastack ci` — the CI pipeline as a *plan of shell steps*, derived purely from
4
+ * the deploy config.
5
+ *
6
+ * The whole point is to make GitHub Actions cleaner: instead of thirty lines of
7
+ * inline bash (compile the manifest, cross-build the arm64 Lambda, bake the
8
+ * Cognito JWKS, assemble the zip, `update-function-code`, wait, print the URL),
9
+ * a workflow runs one line — `rastack ci deploy` — and this module owns the
10
+ * steps. Because the plan is a pure function of the config, it is fully testable
11
+ * without AWS or a Rust toolchain, and `rastack ci --plan` prints exactly what
12
+ * will run.
13
+ *
14
+ * Values that only exist after the stack is created (the function name, the API
15
+ * URL, the JWKS URL) are either taken from `cfg.outputs` when already resolved,
16
+ * or emitted as a live `$(aws cloudformation describe-stacks …)` substitution so
17
+ * the same plan works before and after they are wired into the config.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.checkSteps = checkSteps;
21
+ exports.buildSteps = buildSteps;
22
+ exports.deploySteps = deploySteps;
23
+ exports.ciPlan = ciPlan;
24
+ /**
25
+ * A stack Output value: the literal from `cfg.outputs` if wired, else a shell
26
+ * command substitution that resolves it live from CloudFormation. Keeping the
27
+ * fallback inline (rather than a separate step) means the plan is a flat list of
28
+ * self-contained commands that read correctly in any order.
29
+ */
30
+ function stackOutput(cfg, outputKey, literal) {
31
+ if (literal)
32
+ return literal;
33
+ return (`$(aws cloudformation describe-stacks --stack-name ${cfg.stackName} ` +
34
+ `--region ${cfg.region} ` +
35
+ `--query "Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue" ` +
36
+ `--output text)`);
37
+ }
38
+ /** The Lambda staging directory `cargo lambda build` writes the binary into. */
39
+ function stageDir(cfg) {
40
+ return `rust/target/lambda/${cfg.lambda.bin}`;
41
+ }
42
+ /** `check` — validate the resource graph and compile the manifest. A CI gate. */
43
+ function checkSteps(cfg, bin) {
44
+ return [
45
+ {
46
+ name: "Validate resource graph",
47
+ run: `${bin} check ${cfg.resourcesDir}`,
48
+ },
49
+ {
50
+ name: "Compile manifest + OpenAPI",
51
+ run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
52
+ },
53
+ ];
54
+ }
55
+ /**
56
+ * `build` — produce the deployable Lambda bundle: compile the manifest,
57
+ * cross-build the custom-runtime binary, bake the Cognito JWKS in, and zip the
58
+ * binary together with the files it reads at `/var/task`.
59
+ */
60
+ function buildSteps(cfg, opts = {}) {
61
+ const bin = opts.bin ?? "rastack";
62
+ const stage = stageDir(cfg);
63
+ const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
64
+ const jwks = stackOutput(cfg, "JwksUrl", opts.jwksUrl ?? cfg.outputs?.jwksUrl);
65
+ return [
66
+ {
67
+ name: "Compile manifest + OpenAPI",
68
+ run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
69
+ },
70
+ {
71
+ name: "Cross-build the Lambda binary",
72
+ run: `cargo lambda build --release ${archFlag} --bin ${cfg.lambda.bin} --features ${cfg.lambda.features}`,
73
+ cwd: "rust",
74
+ },
75
+ {
76
+ name: "Bake the Cognito JWKS into the bundle",
77
+ run: `curl -fsSL "${jwks}" -o ${stage}/jwks.json`,
78
+ },
79
+ {
80
+ name: "Stage the manifest + OpenAPI",
81
+ run: `cp ${cfg.outDir}/schema.rastack.json ${cfg.outDir}/openapi.json ${stage}/`,
82
+ },
83
+ {
84
+ name: "Assemble the deployment bundle",
85
+ run: `zip -j ${stage}/bootstrap.zip ${stage}/${cfg.lambda.bin} ${stage}/schema.rastack.json ${stage}/openapi.json ${stage}/jwks.json`,
86
+ },
87
+ ];
88
+ }
89
+ /**
90
+ * `deploy` — the whole CI job: build (unless already built), then ship the code
91
+ * with the single privileged call CI is allowed, `update-function-code`, wait
92
+ * for it to settle, and print the API URL.
93
+ */
94
+ function deploySteps(cfg, opts = {}) {
95
+ const stage = stageDir(cfg);
96
+ const zip = `${stage}/bootstrap.zip`;
97
+ const fn = stackOutput(cfg, "FunctionName", opts.functionName ?? cfg.outputs?.functionName);
98
+ const apiUrl = stackOutput(cfg, "ApiUrl", cfg.outputs?.apiUrl);
99
+ const steps = opts.skipBuild ? [] : buildSteps(cfg, opts);
100
+ steps.push({
101
+ name: "Ship the code (update-function-code)",
102
+ run: `aws lambda update-function-code --function-name ${fn} ` +
103
+ `--zip-file fileb://${zip} --region ${cfg.region} --publish ` +
104
+ `--output text --query LastModified`,
105
+ }, {
106
+ name: "Wait for the function to update",
107
+ run: `aws lambda wait function-updated --function-name ${fn} --region ${cfg.region}`,
108
+ }, {
109
+ name: "Report the API URL",
110
+ run: `echo "Deployed ${cfg.app} → ${apiUrl}"`,
111
+ });
112
+ return steps;
113
+ }
114
+ /** Build the ordered step list for a phase. */
115
+ function ciPlan(phase, cfg, opts = {}) {
116
+ const bin = opts.bin ?? "rastack";
117
+ switch (phase) {
118
+ case "check":
119
+ return checkSteps(cfg, bin);
120
+ case "build":
121
+ return buildSteps(cfg, opts);
122
+ case "deploy":
123
+ return deploySteps(cfg, opts);
124
+ }
125
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `rastack deploy` config — the single source of truth for a repo's serverless
3
+ * deployment, written to `.rastack/deploy.json`.
4
+ *
5
+ * The deploy is split by *what changes* (see docs/serverless-deploy.md): the
6
+ * CloudFormation stack owns all durable infrastructure and is created once by a
7
+ * human, while CI does exactly one thing on every push —
8
+ * `aws lambda update-function-code`. This module models the small set of
9
+ * *authoring inputs* a human decides up front (app slug, region, the GitHub repo
10
+ * whose CI ships the code, the Lambda build shape) plus the *stack outputs* that
11
+ * only exist after the stack is created (function name, API URL, JWKS URL, …).
12
+ * CI resolves any missing output live from the stack, so the config stays a
13
+ * short, human-owned file.
14
+ *
15
+ * Everything here is pure and side-effect-free — the same discipline as the
16
+ * resource compiler and the token compiler. `rastack-{init,deploy,ci}.ts` are the
17
+ * thin IO wrappers that read/write the file and shell out.
18
+ */
19
+ /** Values CI resolves from the CloudFormation stack Outputs (see app.yaml). */
20
+ export interface DeployOutputs {
21
+ /** Lambda function name — CI targets this with `update-function-code`. */
22
+ functionName?: string;
23
+ /** Base URL of the deployed HTTP API. */
24
+ apiUrl?: string;
25
+ /** OIDC role CI assumes — wire as the `AWS_DEPLOY_ROLE_ARN` repo Variable. */
26
+ deployRoleArn?: string;
27
+ /** Cognito User Pool id (identity / JWTs). */
28
+ userPoolId?: string;
29
+ /** App client id — the token audience `rastack-server` pins. */
30
+ userPoolClientId?: string;
31
+ /** JWKS URL baked into the Lambda bundle at build time. */
32
+ jwksUrl?: string;
33
+ /** S3 Iceberg warehouse bucket — the system of record. */
34
+ warehouseBucket?: string;
35
+ }
36
+ export interface LambdaBuild {
37
+ /** Cargo bin built for the custom runtime (`provided.al2023`). */
38
+ bin: string;
39
+ /** Target architecture — arm64 (Graviton, the default) or x86_64. */
40
+ arch: "arm64" | "x86_64";
41
+ /** Cargo features the Lambda build enables. */
42
+ features: string;
43
+ }
44
+ export interface DeployConfig {
45
+ /** App slug — namespaces buckets/roles/SSM. Cognito-safe: `^[a-z0-9]{1,20}$`. */
46
+ app: string;
47
+ /** Resource-name prefix shared across a deployment. `^[a-z0-9]{1,20}$`. */
48
+ namePrefix: string;
49
+ /** AWS region the stack (and CI) target. */
50
+ region: string;
51
+ /** CloudFormation stack name — defaults to `${namePrefix}-${app}`. */
52
+ stackName: string;
53
+ /** The GitHub repo whose CI is trusted to ship the Lambda code. */
54
+ github: {
55
+ owner: string;
56
+ repo: string;
57
+ branch: string;
58
+ };
59
+ /** Iceberg namespace the warehouse writes under. */
60
+ namespace: string;
61
+ /** How the Rust Lambda bundle is built. */
62
+ lambda: LambdaBuild;
63
+ /** Where the TypeScript resource definitions live. */
64
+ resourcesDir: string;
65
+ /** Where `rastack compile` writes the manifest + OpenAPI. */
66
+ outDir: string;
67
+ /** Bootstrap templates bucket that hosts `app.yaml` (for the launch command). */
68
+ templatesBucket?: string;
69
+ /** Bootstrap artifacts bucket holding the Lambda placeholder bundle. */
70
+ artifactsBucket?: string;
71
+ /** Whether the function verifies the `access` or `id` token. */
72
+ tokenUse: "access" | "id";
73
+ /** Values resolved from the stack after it is created (optional). */
74
+ outputs?: DeployOutputs;
75
+ }
76
+ export declare const DEFAULT_LAMBDA: LambdaBuild;
77
+ /** The out-of-the-box config — everything a fresh repo can default sensibly. */
78
+ export declare const DEFAULTS: Omit<DeployConfig, "app" | "github">;
79
+ /** A recursively-optional view of the config, as read from disk or CLI flags. */
80
+ export type PartialDeployConfig = {
81
+ [K in keyof DeployConfig]?: DeployConfig[K] extends object ? Partial<DeployConfig[K]> : DeployConfig[K];
82
+ };
83
+ /**
84
+ * Fill a partial config with defaults into a complete `DeployConfig`. Never
85
+ * throws — `validateConfig` reports what is still missing or malformed.
86
+ */
87
+ export declare function normalizeConfig(partial?: PartialDeployConfig): DeployConfig;
88
+ /**
89
+ * Return a list of human-readable problems, empty when the config is deployable.
90
+ * The slug/prefix patterns mirror app.yaml's `AllowedPattern`s exactly, so a
91
+ * config that passes here also passes CloudFormation's own validation.
92
+ */
93
+ export declare function validateConfig(cfg: DeployConfig): string[];
94
+ /** Deep-merge a patch over a base config (used by `deploy`/`init` reconfigure). */
95
+ export declare function mergeConfig(base: DeployConfig, patch: PartialDeployConfig): DeployConfig;
96
+ /**
97
+ * Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
98
+ * (`git@github.com:owner/repo.git`) and HTTPS
99
+ * (`https://github.com/owner/repo(.git)`) forms — so `init` can default the
100
+ * GitHub coordinates from `git remote get-url origin`. Returns null for any
101
+ * non-GitHub or unparseable URL.
102
+ */
103
+ export declare function parseGitHubRemote(url: string): {
104
+ owner: string;
105
+ repo: string;
106
+ } | null;
107
+ /** A `Key=Value` CloudFormation parameter override. */
108
+ export interface StackParameter {
109
+ key: string;
110
+ value: string;
111
+ }
112
+ /**
113
+ * The `--parameter-overrides` for `aws cloudformation deploy` of app.yaml,
114
+ * derived from the config. Order and keys match the template's `Parameters`.
115
+ * `ArtifactsBucket` (which has no template default) is included only when set.
116
+ */
117
+ export declare function stackParameters(cfg: DeployConfig): StackParameter[];
118
+ /** The S3 URL app.yaml is launched from, when the templates bucket is known. */
119
+ export declare function templateUrl(cfg: DeployConfig): string | null;
120
+ /**
121
+ * The one-time `aws cloudformation deploy` command that creates the app's
122
+ * infrastructure. Launched from the hosted template URL when the templates
123
+ * bucket is known, else from the in-repo `deploy/cloudformation/app.yaml`.
124
+ */
125
+ export declare function deployStackCommand(cfg: DeployConfig): string;
126
+ /** GitHub repo Variable to wire (name + resolved value, if known from outputs). */
127
+ export interface RepoVariable {
128
+ name: string;
129
+ value: string;
130
+ from: string;
131
+ }
132
+ /**
133
+ * The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
134
+ * `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
135
+ * Outputs when already resolved, else an empty string to fill in from the
136
+ * Console.
137
+ */
138
+ export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
139
+ /**
140
+ * The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
141
+ * The IaC sets these on the function; surfaced here for `rastack deploy` to
142
+ * print so the mapping between config and runtime env is inspectable.
143
+ */
144
+ export declare function lambdaEnv(cfg: DeployConfig): StackParameter[];
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ /**
3
+ * `rastack deploy` config — the single source of truth for a repo's serverless
4
+ * deployment, written to `.rastack/deploy.json`.
5
+ *
6
+ * The deploy is split by *what changes* (see docs/serverless-deploy.md): the
7
+ * CloudFormation stack owns all durable infrastructure and is created once by a
8
+ * human, while CI does exactly one thing on every push —
9
+ * `aws lambda update-function-code`. This module models the small set of
10
+ * *authoring inputs* a human decides up front (app slug, region, the GitHub repo
11
+ * whose CI ships the code, the Lambda build shape) plus the *stack outputs* that
12
+ * only exist after the stack is created (function name, API URL, JWKS URL, …).
13
+ * CI resolves any missing output live from the stack, so the config stays a
14
+ * short, human-owned file.
15
+ *
16
+ * Everything here is pure and side-effect-free — the same discipline as the
17
+ * resource compiler and the token compiler. `rastack-{init,deploy,ci}.ts` are the
18
+ * thin IO wrappers that read/write the file and shell out.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEFAULTS = exports.DEFAULT_LAMBDA = void 0;
22
+ exports.normalizeConfig = normalizeConfig;
23
+ exports.validateConfig = validateConfig;
24
+ exports.mergeConfig = mergeConfig;
25
+ exports.parseGitHubRemote = parseGitHubRemote;
26
+ exports.stackParameters = stackParameters;
27
+ exports.templateUrl = templateUrl;
28
+ exports.deployStackCommand = deployStackCommand;
29
+ exports.repoVariables = repoVariables;
30
+ exports.lambdaEnv = lambdaEnv;
31
+ exports.DEFAULT_LAMBDA = {
32
+ bin: "bootstrap",
33
+ arch: "arm64",
34
+ features: "lambda",
35
+ };
36
+ /** The out-of-the-box config — everything a fresh repo can default sensibly. */
37
+ exports.DEFAULTS = {
38
+ namePrefix: "rastack",
39
+ region: "af-south-1",
40
+ stackName: "",
41
+ namespace: "production",
42
+ lambda: exports.DEFAULT_LAMBDA,
43
+ resourcesDir: "resources",
44
+ outDir: ".rastack",
45
+ tokenUse: "access",
46
+ };
47
+ /**
48
+ * Fill a partial config with defaults into a complete `DeployConfig`. Never
49
+ * throws — `validateConfig` reports what is still missing or malformed.
50
+ */
51
+ function normalizeConfig(partial = {}) {
52
+ const app = (partial.app ?? "").trim();
53
+ const namePrefix = (partial.namePrefix ?? exports.DEFAULTS.namePrefix).trim();
54
+ const github = {
55
+ owner: (partial.github?.owner ?? "").trim(),
56
+ repo: (partial.github?.repo ?? "").trim(),
57
+ branch: (partial.github?.branch ?? "master").trim(),
58
+ };
59
+ const stackName = (partial.stackName ?? "").trim() || (app ? `${namePrefix}-${app}` : "");
60
+ return {
61
+ app,
62
+ namePrefix,
63
+ region: (partial.region ?? exports.DEFAULTS.region).trim(),
64
+ stackName,
65
+ github,
66
+ namespace: (partial.namespace ?? exports.DEFAULTS.namespace).trim(),
67
+ lambda: {
68
+ bin: (partial.lambda?.bin ?? exports.DEFAULT_LAMBDA.bin).trim(),
69
+ arch: partial.lambda?.arch ?? exports.DEFAULT_LAMBDA.arch,
70
+ features: (partial.lambda?.features ?? exports.DEFAULT_LAMBDA.features).trim(),
71
+ },
72
+ resourcesDir: (partial.resourcesDir ?? exports.DEFAULTS.resourcesDir).trim(),
73
+ outDir: (partial.outDir ?? exports.DEFAULTS.outDir).trim(),
74
+ templatesBucket: partial.templatesBucket?.trim() || undefined,
75
+ artifactsBucket: partial.artifactsBucket?.trim() || undefined,
76
+ tokenUse: partial.tokenUse ?? exports.DEFAULTS.tokenUse,
77
+ outputs: partial.outputs ? { ...partial.outputs } : undefined,
78
+ };
79
+ }
80
+ const SLUG = /^[a-z0-9]{1,20}$/;
81
+ const REGION = /^[a-z]{2}-[a-z]+-\d+$/;
82
+ const GH_NAME = /^[A-Za-z0-9_.-]+$/;
83
+ const NAMESPACE = /^[a-z0-9_]+$/;
84
+ /**
85
+ * Return a list of human-readable problems, empty when the config is deployable.
86
+ * The slug/prefix patterns mirror app.yaml's `AllowedPattern`s exactly, so a
87
+ * config that passes here also passes CloudFormation's own validation.
88
+ */
89
+ function validateConfig(cfg) {
90
+ const errors = [];
91
+ if (!cfg.app)
92
+ errors.push("app is required (a short slug, e.g. `airline`).");
93
+ else if (!SLUG.test(cfg.app))
94
+ errors.push(`app "${cfg.app}" must match ${SLUG} (lowercase letters/digits, 1–20).`);
95
+ if (!SLUG.test(cfg.namePrefix))
96
+ errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
97
+ if (!cfg.region)
98
+ errors.push("region is required (e.g. `af-south-1`).");
99
+ else if (!REGION.test(cfg.region))
100
+ errors.push(`region "${cfg.region}" does not look like an AWS region (e.g. af-south-1).`);
101
+ if (!cfg.github.owner)
102
+ errors.push("github.owner is required.");
103
+ else if (!GH_NAME.test(cfg.github.owner))
104
+ errors.push(`github.owner "${cfg.github.owner}" is not a valid GitHub name.`);
105
+ if (!cfg.github.repo)
106
+ errors.push("github.repo is required.");
107
+ else if (!GH_NAME.test(cfg.github.repo))
108
+ errors.push(`github.repo "${cfg.github.repo}" is not a valid GitHub name.`);
109
+ if (!cfg.github.branch)
110
+ errors.push("github.branch is required.");
111
+ if (!NAMESPACE.test(cfg.namespace))
112
+ errors.push(`namespace "${cfg.namespace}" must match ${NAMESPACE}.`);
113
+ if (cfg.lambda.arch !== "arm64" && cfg.lambda.arch !== "x86_64")
114
+ errors.push(`lambda.arch must be "arm64" or "x86_64" (got "${cfg.lambda.arch}").`);
115
+ if (!cfg.lambda.bin)
116
+ errors.push("lambda.bin is required.");
117
+ return errors;
118
+ }
119
+ /** Deep-merge a patch over a base config (used by `deploy`/`init` reconfigure). */
120
+ function mergeConfig(base, patch) {
121
+ return normalizeConfig({
122
+ ...base,
123
+ ...patch,
124
+ github: { ...base.github, ...patch.github },
125
+ lambda: { ...base.lambda, ...patch.lambda },
126
+ outputs: { ...base.outputs, ...patch.outputs },
127
+ });
128
+ }
129
+ /**
130
+ * Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
131
+ * (`git@github.com:owner/repo.git`) and HTTPS
132
+ * (`https://github.com/owner/repo(.git)`) forms — so `init` can default the
133
+ * GitHub coordinates from `git remote get-url origin`. Returns null for any
134
+ * non-GitHub or unparseable URL.
135
+ */
136
+ function parseGitHubRemote(url) {
137
+ const trimmed = url.trim();
138
+ const m = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/.exec(trimmed) ||
139
+ /^(?:https?:\/\/|ssh:\/\/git@)github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(trimmed);
140
+ if (!m)
141
+ return null;
142
+ const owner = m[1];
143
+ const repo = m[2];
144
+ if (!GH_NAME.test(owner) || !GH_NAME.test(repo))
145
+ return null;
146
+ return { owner, repo };
147
+ }
148
+ /**
149
+ * The `--parameter-overrides` for `aws cloudformation deploy` of app.yaml,
150
+ * derived from the config. Order and keys match the template's `Parameters`.
151
+ * `ArtifactsBucket` (which has no template default) is included only when set.
152
+ */
153
+ function stackParameters(cfg) {
154
+ const params = [
155
+ { key: "AppName", value: cfg.app },
156
+ { key: "NamePrefix", value: cfg.namePrefix },
157
+ { key: "GitHubOwner", value: cfg.github.owner },
158
+ { key: "GitHubRepo", value: cfg.github.repo },
159
+ { key: "GitHubBranch", value: cfg.github.branch },
160
+ { key: "IcebergNamespace", value: cfg.namespace },
161
+ ];
162
+ if (cfg.artifactsBucket)
163
+ params.push({ key: "ArtifactsBucket", value: cfg.artifactsBucket });
164
+ return params;
165
+ }
166
+ /** The S3 URL app.yaml is launched from, when the templates bucket is known. */
167
+ function templateUrl(cfg) {
168
+ if (!cfg.templatesBucket)
169
+ return null;
170
+ return `https://${cfg.templatesBucket}.s3.${cfg.region}.amazonaws.com/app.yaml`;
171
+ }
172
+ /**
173
+ * The one-time `aws cloudformation deploy` command that creates the app's
174
+ * infrastructure. Launched from the hosted template URL when the templates
175
+ * bucket is known, else from the in-repo `deploy/cloudformation/app.yaml`.
176
+ */
177
+ function deployStackCommand(cfg) {
178
+ const src = templateUrl(cfg);
179
+ const templateArg = src
180
+ ? `--template-url ${src}`
181
+ : "--template-file deploy/cloudformation/app.yaml";
182
+ const overrides = stackParameters(cfg)
183
+ .map((p) => `${p.key}=${p.value}`)
184
+ .join(" ");
185
+ return [
186
+ "aws cloudformation deploy",
187
+ templateArg,
188
+ `--stack-name ${cfg.stackName}`,
189
+ "--capabilities CAPABILITY_NAMED_IAM",
190
+ `--region ${cfg.region}`,
191
+ `--parameter-overrides ${overrides}`,
192
+ ].join(" \\\n ");
193
+ }
194
+ /**
195
+ * The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
196
+ * `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
197
+ * Outputs when already resolved, else an empty string to fill in from the
198
+ * Console.
199
+ */
200
+ function repoVariables(cfg) {
201
+ return [
202
+ {
203
+ name: "AWS_DEPLOY_ROLE_ARN",
204
+ value: cfg.outputs?.deployRoleArn ?? "",
205
+ from: "DeployRoleArn stack output",
206
+ },
207
+ { name: "AWS_REGION", value: cfg.region, from: "Region stack output" },
208
+ ];
209
+ }
210
+ /**
211
+ * The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
212
+ * The IaC sets these on the function; surfaced here for `rastack deploy` to
213
+ * print so the mapping between config and runtime env is inspectable.
214
+ */
215
+ function lambdaEnv(cfg) {
216
+ const warehouse = cfg.outputs?.warehouseBucket
217
+ ? `s3://${cfg.outputs.warehouseBucket}/warehouse`
218
+ : "s3://{warehouse-bucket}/warehouse";
219
+ const manifest = `/var/task/schema.rastack.json`;
220
+ return [
221
+ { key: "RASTACK_WAREHOUSE", value: warehouse },
222
+ { key: "RASTACK_MANIFEST", value: manifest },
223
+ { key: "RASTACK_OPENAPI", value: "/var/task/openapi.json" },
224
+ { key: "RASTACK_JWKS", value: "/var/task/jwks.json" },
225
+ {
226
+ key: "RASTACK_ISSUER",
227
+ value: cfg.outputs?.userPoolId
228
+ ? `https://cognito-idp.${cfg.region}.amazonaws.com/${cfg.outputs.userPoolId}`
229
+ : `https://cognito-idp.${cfg.region}.amazonaws.com/{userPoolId}`,
230
+ },
231
+ {
232
+ key: "RASTACK_AUDIENCE",
233
+ value: cfg.outputs?.userPoolClientId ?? "{appClientId}",
234
+ },
235
+ { key: "RASTACK_TOKEN_USE", value: cfg.tokenUse },
236
+ ];
237
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `rastack deploy | ci | init` core — the DevOps surface of React API Stack.
3
+ *
4
+ * Pure, testable building blocks (config, CI plan, workflow templates); the
5
+ * `rastack-{deploy,ci,init}.ts` entrypoints are the thin IO wrappers over them.
6
+ */
7
+ export * from "./config";
8
+ export * from "./ci";
9
+ export * from "./workflows";
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * The `rastack deploy | ci | init` core — the DevOps surface of React API Stack.
4
+ *
5
+ * Pure, testable building blocks (config, CI plan, workflow templates); the
6
+ * `rastack-{deploy,ci,init}.ts` entrypoints are the thin IO wrappers over them.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ __exportStar(require("./config"), exports);
24
+ __exportStar(require("./ci"), exports);
25
+ __exportStar(require("./workflows"), exports);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Filesystem + prompt helpers shared by the `rastack deploy | ci | init`
3
+ * entrypoints. Everything pure lives in the sibling modules; this is the small
4
+ * IO boundary they wrap.
5
+ */
6
+ import * as readline from "readline";
7
+ import { DeployConfig } from "./config";
8
+ /** Absolute path to the deploy config for a project root. */
9
+ export declare function configPath(cwd?: string): string;
10
+ /** Read + normalize the deploy config, or null when it does not exist yet. */
11
+ export declare function loadConfig(cwd?: string): DeployConfig | null;
12
+ /** Write the deploy config, creating `.rastack/` if needed. */
13
+ export declare function saveConfig(cfg: DeployConfig, cwd?: string): string;
14
+ /**
15
+ * Best-effort GitHub `origin` remote URL, for defaulting the repo coordinates.
16
+ * Returns null when not a git repo or no origin.
17
+ */
18
+ export declare function gitRemoteUrl(cwd?: string): string | null;
19
+ /** True when we can prompt the user (a TTY, and not explicitly opted out). */
20
+ export declare function canPrompt(nonInteractive: boolean): boolean;
21
+ /**
22
+ * Ask a single question with a default, echoed as `label [default]: `. Returns
23
+ * the trimmed answer, or the default on an empty line.
24
+ */
25
+ export declare function ask(rl: readline.Interface, label: string, def: string): Promise<string>;
26
+ /** Run a callback with a readline interface, always closing it. */
27
+ export declare function withPrompt<T>(fn: (rl: readline.Interface) => Promise<T>): Promise<T>;