@m-kopa/launchpad-cli 0.64.0 → 0.66.0
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 +32 -1
- package/dist/cli.js +483 -78
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/manifest/container-runtime-warnings.d.ts +13 -0
- package/dist/manifest/container-runtime-warnings.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +60 -4
- package/skills/launchpad-deploy-status/SKILL.md +4 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +12 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
|
|
7
7
|
pre-1.0 minor bumps may carry breaking changes per ADR 0005.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 0.66.0 - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Container apps can sign in through the Entra auth gateway with `auth: gateway` (AIO-811, ADR 0057). `validate` and `plan` accept it, and a gateway container app uses no Cloudflare Access app or Zero Trust seat.
|
|
14
|
+
- The platform now operates a one-minute, platform-owned health sweep against live container apps, default-off behind a kill-switch and shipped with no manifest, CLI, or `launchpad.yaml` changes (AIO-269). The sweep authenticates as a dedicated Cloudflare Access service token the platform team provisions and rotates — see `docs/runbooks/container-health-monitoring.md`. App authors take no action; this note exists only so the new, platform-operated probe traffic to the generated Worker's `/__platform/health/container` route and the platform-level Access policy addition on each container app's custom-hostname application are documented, not undocumented infrastructure. See `docs/concepts/containers.md` for a short pointer.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `launchpad init` now writes `auth: gateway` for container apps by default. Pass `--auth access` to keep the old Access sign-in (AIO-811).
|
|
19
|
+
|
|
20
|
+
## 0.65.0 - 2026-10-03
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `validate` and `plan` print a non-blocking warning for any container manifest: the platform's runtime switch may still be `off` for the app, in which case `launchpad deploy` returns HTTP 503 `container_runtime_off` (AIO-786).
|
|
25
|
+
A `runtime: aca` manifest gets a second warning — that value is reserved and currently ignored. Exit codes are unchanged; see `docs/concepts/containers.md` for the runtime gate, the `lite`-only container size, and the cold-start behaviour after idle.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Raise the production minimum CLI version to 0.64.0 through the governed policy workflow.
|
|
30
|
+
- The `launchpad-deploy`, `launchpad-status` and `launchpad-deploy-status` skills keep a refused dispatch build on dispatch (AIO-756).
|
|
31
|
+
They name the `hidden build output is not supported` error and its fix, and they say that the Cloudflare account allows only 100 Pages projects.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- A first deploy with several hundred files no longer times out at `content_seeded` (AIO-738).
|
|
36
|
+
The platform now sends text files inside the git tree call, in chunks of at most 500 files or 8 MB, and checks every file's git SHA after the commit tree is built; if any SHA differs, it falls back to one blob per file.
|
|
37
|
+
Binary files still get their own blobs, created 8 at a time. A rate-limited blob or tree call retries, and the commit step has 10 minutes.
|
|
38
|
+
- `launchpad destroy` now tears down an app whose provisioning failed at stage `pending` when main has no per-app Terraform.
|
|
39
|
+
It refuses with `tf_pr_open` while the app's Terraform PR is open, and with `dispatch_delivery_present` when a pre-Terraform app already has a dispatch delivery.
|
|
40
|
+
- `launchpad deploy`'s apply-status polling (the automatic reconcile path for an access or origin change, e.g. switching `origin: dispatch` → `pages`) no longer goes quiet after a real `terraform apply` failure. Previously, once the bot recorded that failure and queued an automatic retry, every poll *before* the retry's PR opened fell through to a stale lookup and 404'd `no_apply_found` — which the CLI treats as "still settling" and retries. If the retry's regenerated Terraform happened to be byte-identical to what the failed attempt already merged (because the failed apply's file content lands on `main` even though the real infrastructure was never created), its PR carried an empty diff and never re-ran `terraform apply` at all, so the original failure could go unreported for as long as it took that empty retry to resolve (AIO-756). The bot now reports the already-known `failed` verdict on every poll in that window, so `launchpad deploy` exits non-zero with the failing stage named, instead of appearing to hang or (in the worst case) eventually reading as if nothing was wrong. Run `launchpad status <slug>` to confirm the current state after a reported failure.
|
|
10
41
|
|
|
11
42
|
## 0.64.0 - 2026-09-25
|
|
12
43
|
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var CLI_VERSION = "0.
|
|
22
|
+
var CLI_VERSION = "0.66.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import { readFileSync } from "node:fs";
|
|
@@ -2744,13 +2744,6 @@ var ManifestSchema = z2.object({
|
|
|
2744
2744
|
});
|
|
2745
2745
|
}
|
|
2746
2746
|
}
|
|
2747
|
-
if (isContainer && m.auth === "gateway") {
|
|
2748
|
-
ctx.addIssue({
|
|
2749
|
-
code: z2.ZodIssueCode.custom,
|
|
2750
|
-
path: ["auth"],
|
|
2751
|
-
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
2752
|
-
});
|
|
2753
|
-
}
|
|
2754
2747
|
if (isContainer && m.containers === undefined) {
|
|
2755
2748
|
ctx.addIssue({
|
|
2756
2749
|
code: z2.ZodIssueCode.custom,
|
|
@@ -3472,13 +3465,6 @@ var SpecSchema = z3.object({
|
|
|
3472
3465
|
message: "build is not allowed when appType is 'container' (container build lives in the Dockerfile)"
|
|
3473
3466
|
});
|
|
3474
3467
|
}
|
|
3475
|
-
if (isContainer && s.auth === "gateway") {
|
|
3476
|
-
ctx.addIssue({
|
|
3477
|
-
code: z3.ZodIssueCode.custom,
|
|
3478
|
-
path: ["auth"],
|
|
3479
|
-
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
3468
|
if (!isContainer && s.fronted_by_concierge === true) {
|
|
3483
3469
|
ctx.addIssue({
|
|
3484
3470
|
code: z3.ZodIssueCode.custom,
|
|
@@ -3754,10 +3740,17 @@ function validateDeploymentCapability(manifest) {
|
|
|
3754
3740
|
className: containerClassName(view.appName),
|
|
3755
3741
|
bindingName,
|
|
3756
3742
|
dockerfilePathFromContainerDirectory: `../${container.image.slice(2)}`,
|
|
3757
|
-
environmentNames
|
|
3743
|
+
environmentNames,
|
|
3744
|
+
authMode: deploymentAuthMode(manifest)
|
|
3758
3745
|
}
|
|
3759
3746
|
};
|
|
3760
3747
|
}
|
|
3748
|
+
function deploymentAuthMode(manifest) {
|
|
3749
|
+
if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
|
|
3750
|
+
return manifest.spec.auth === "gateway" ? "gateway" : "access";
|
|
3751
|
+
}
|
|
3752
|
+
return manifest.auth === "gateway" ? "gateway" : "access";
|
|
3753
|
+
}
|
|
3761
3754
|
function containerClassName(containerName) {
|
|
3762
3755
|
return `AppContainer${containerName.split("-").map(capitalise).join("")}`;
|
|
3763
3756
|
}
|
|
@@ -3832,8 +3825,12 @@ function emit(capability) {
|
|
|
3832
3825
|
const app = JSON.stringify(capability.appName);
|
|
3833
3826
|
const className = capability.className;
|
|
3834
3827
|
const bindingName = capability.bindingName;
|
|
3828
|
+
const healthcheckPath = JSON.stringify(capability.container.healthcheck);
|
|
3829
|
+
const isGateway = capability.authMode === "gateway";
|
|
3835
3830
|
const environmentFields = capability.environmentNames.map((name) => ` ${name}: string;`);
|
|
3836
3831
|
const environmentValues = capability.environmentNames.map((name) => ` ${name}: this.env.${name} ?? "",`);
|
|
3832
|
+
const envAuthFields = isGateway ? [" GATEWAY_ISSUER: string;", " GATEWAY_AUD: string;", " GATEWAY_JWKS_URL: string;"] : [" CF_ACCESS_AUD: string;", " CF_ACCESS_TEAM_DOMAIN: string;"];
|
|
3833
|
+
const monitorEnvField = isGateway ? [" MONITOR_SHARED_SECRET: string;"] : [];
|
|
3837
3834
|
const lines = [
|
|
3838
3835
|
"/**",
|
|
3839
3836
|
" * Auto-generated by @m-kopa/launchpad-engine.",
|
|
@@ -3843,17 +3840,19 @@ function emit(capability) {
|
|
|
3843
3840
|
"import {",
|
|
3844
3841
|
" AccessJwtVerificationError,",
|
|
3845
3842
|
" createJwksCache,",
|
|
3846
|
-
" parseAccessJwtFromHeaders,",
|
|
3843
|
+
isGateway ? " parseGatewayAssertionFromHeaders," : " parseAccessJwtFromHeaders,",
|
|
3847
3844
|
" verifyAccessJwt,",
|
|
3845
|
+
...isGateway ? [] : [" verifyJwt,"],
|
|
3848
3846
|
" type User,",
|
|
3847
|
+
...isGateway ? [] : [" type VerifiedPrincipal,"],
|
|
3849
3848
|
'} from "@m-kopa/platform-auth";',
|
|
3850
3849
|
"",
|
|
3851
3850
|
"type Env = {",
|
|
3852
3851
|
` ${bindingName}: DurableObjectNamespace<${className}>;`,
|
|
3853
3852
|
" BINDINGS_PROXY_SECRET: string;",
|
|
3854
|
-
|
|
3855
|
-
" CF_ACCESS_TEAM_DOMAIN: string;",
|
|
3853
|
+
...envAuthFields,
|
|
3856
3854
|
" LAUNCHPAD_DEPLOYMENT_SHA: string;",
|
|
3855
|
+
...isGateway ? monitorEnvField : [" MONITOR_CLIENT_ID: string;"],
|
|
3857
3856
|
...environmentFields,
|
|
3858
3857
|
"};",
|
|
3859
3858
|
"",
|
|
@@ -3868,7 +3867,7 @@ function emit(capability) {
|
|
|
3868
3867
|
"function jwks(env: Env): ReturnType<typeof createJwksCache> {",
|
|
3869
3868
|
" if (jwksCache === null) {",
|
|
3870
3869
|
" jwksCache = createJwksCache(",
|
|
3871
|
-
" `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
|
|
3870
|
+
isGateway ? " env.GATEWAY_JWKS_URL," : " `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
|
|
3872
3871
|
" );",
|
|
3873
3872
|
" }",
|
|
3874
3873
|
" return jwksCache;",
|
|
@@ -3896,29 +3895,59 @@ function emit(capability) {
|
|
|
3896
3895
|
" return difference === 0;",
|
|
3897
3896
|
"}",
|
|
3898
3897
|
"",
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
"
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
"
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
"
|
|
3921
|
-
|
|
3898
|
+
...isGateway ? [
|
|
3899
|
+
"async function authenticate(request: Request, env: Env): Promise<User | Response> {",
|
|
3900
|
+
" if (!env.GATEWAY_ISSUER || !env.GATEWAY_AUD || !env.GATEWAY_JWKS_URL) {",
|
|
3901
|
+
' return unauthenticated("auth_unconfigured");',
|
|
3902
|
+
" }",
|
|
3903
|
+
" const token = parseGatewayAssertionFromHeaders(request.headers);",
|
|
3904
|
+
" if (token === null) {",
|
|
3905
|
+
' return unauthenticated("gateway_assertion_missing");',
|
|
3906
|
+
" }",
|
|
3907
|
+
" try {",
|
|
3908
|
+
" // verifyAccessJwt is a generic RS256-against-issuer/audience/JWKS",
|
|
3909
|
+
" // check; a gateway-fronted container verifies the Entra-OIDC",
|
|
3910
|
+
" // gateway's assertion (ADR 0057) with the SAME verifier used for",
|
|
3911
|
+
" // Cf Access JWTs, against the GATEWAY_* values instead.",
|
|
3912
|
+
" return await verifyAccessJwt(token, {",
|
|
3913
|
+
" teamDomain: env.GATEWAY_ISSUER,",
|
|
3914
|
+
" audience: env.GATEWAY_AUD,",
|
|
3915
|
+
" jwks: jwks(env),",
|
|
3916
|
+
" clockTolerance: 30,",
|
|
3917
|
+
" });",
|
|
3918
|
+
" } catch (error) {",
|
|
3919
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
3920
|
+
" return unauthenticated(error.code);",
|
|
3921
|
+
" }",
|
|
3922
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "gateway_assertion_verification_failed" }));',
|
|
3923
|
+
' return unauthenticated("gateway_assertion_verification_failed");',
|
|
3924
|
+
" }",
|
|
3925
|
+
"}"
|
|
3926
|
+
] : [
|
|
3927
|
+
"async function authenticate(request: Request, env: Env): Promise<User | Response> {",
|
|
3928
|
+
" if (!env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
|
|
3929
|
+
' return unauthenticated("auth_unconfigured");',
|
|
3930
|
+
" }",
|
|
3931
|
+
" const token = parseAccessJwtFromHeaders(request.headers);",
|
|
3932
|
+
" if (token === null) {",
|
|
3933
|
+
' return unauthenticated("access_jwt_missing");',
|
|
3934
|
+
" }",
|
|
3935
|
+
" try {",
|
|
3936
|
+
" return await verifyAccessJwt(token, {",
|
|
3937
|
+
" teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
|
|
3938
|
+
" audience: env.CF_ACCESS_AUD,",
|
|
3939
|
+
" jwks: jwks(env),",
|
|
3940
|
+
" clockTolerance: 30,",
|
|
3941
|
+
" });",
|
|
3942
|
+
" } catch (error) {",
|
|
3943
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
3944
|
+
" return unauthenticated(error.code);",
|
|
3945
|
+
" }",
|
|
3946
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "access_jwt_verification_failed" }));',
|
|
3947
|
+
' return unauthenticated("access_jwt_verification_failed");',
|
|
3948
|
+
" }",
|
|
3949
|
+
"}"
|
|
3950
|
+
],
|
|
3922
3951
|
"",
|
|
3923
3952
|
"function forwardedRequest(",
|
|
3924
3953
|
" request: Request,",
|
|
@@ -3955,6 +3984,173 @@ function emit(capability) {
|
|
|
3955
3984
|
" });",
|
|
3956
3985
|
"}",
|
|
3957
3986
|
"",
|
|
3987
|
+
"/**",
|
|
3988
|
+
" * Build the machine-only health-probe request dispatched to the",
|
|
3989
|
+
" * container DO. Deliberately NOT `forwardedRequest`: it carries no",
|
|
3990
|
+
" * inbound header (no auth, no user identity, no bindings-proxy",
|
|
3991
|
+
" * secret/url) and targets the manifest-declared healthcheck path,",
|
|
3992
|
+
" * never the Worker's own reserved `GET /health` liveness route.",
|
|
3993
|
+
" */",
|
|
3994
|
+
"function buildHealthDispatchRequest(",
|
|
3995
|
+
" origin: string,",
|
|
3996
|
+
" requestId: string,",
|
|
3997
|
+
"): Request {",
|
|
3998
|
+
" const headers = new Headers();",
|
|
3999
|
+
' headers.set("X-Request-ID", requestId);',
|
|
4000
|
+
` return new Request(origin + ${healthcheckPath}, {`,
|
|
4001
|
+
' method: "GET",',
|
|
4002
|
+
" headers,",
|
|
4003
|
+
' redirect: "manual",',
|
|
4004
|
+
" });",
|
|
4005
|
+
"}",
|
|
4006
|
+
"",
|
|
4007
|
+
"function healthDenied(code: string): Response {",
|
|
4008
|
+
" return Response.json(",
|
|
4009
|
+
' { error: "health_unavailable", code },',
|
|
4010
|
+
' { status: 404, headers: { "cache-control": "no-store" } },',
|
|
4011
|
+
" );",
|
|
4012
|
+
"}",
|
|
4013
|
+
"",
|
|
4014
|
+
...isGateway ? [
|
|
4015
|
+
"/**",
|
|
4016
|
+
" * `GET /__platform/health/container` — AIO-269 T8. A gateway-",
|
|
4017
|
+
" * fronted app has no Access app and no public hostname (ADR",
|
|
4018
|
+
" * 0057), so there is no Access service-token surface for the",
|
|
4019
|
+
" * monitor to present a credential to. Instead this trusts a",
|
|
4020
|
+
" * shared secret (`MONITOR_SHARED_SECRET`) carried in",
|
|
4021
|
+
" * `X-Launchpad-Monitor-Secret` — safe ONLY because this Worker",
|
|
4022
|
+
" * is reachable exclusively via launchpad-container-router's",
|
|
4023
|
+
" * per-app service binding (never a public hostname), and the",
|
|
4024
|
+
" * monitor is the only caller with a binding to that router.",
|
|
4025
|
+
" * Missing configuration, a missing header, and a mismatched",
|
|
4026
|
+
" * secret all fail closed with no container fetch, same as the",
|
|
4027
|
+
" * Access-mode path below. The container's response body is",
|
|
4028
|
+
" * never forwarded; the reply is a fixed, redacted, `no-store`",
|
|
4029
|
+
" * JSON body.",
|
|
4030
|
+
" */",
|
|
4031
|
+
"async function handleContainerHealth(",
|
|
4032
|
+
" request: Request,",
|
|
4033
|
+
" env: Env,",
|
|
4034
|
+
" requestId: string,",
|
|
4035
|
+
"): Promise<Response> {",
|
|
4036
|
+
' if (request.method !== "GET") {',
|
|
4037
|
+
' return healthDenied("method_not_allowed");',
|
|
4038
|
+
" }",
|
|
4039
|
+
" if (!env.MONITOR_SHARED_SECRET) {",
|
|
4040
|
+
' return healthDenied("monitor_unconfigured");',
|
|
4041
|
+
" }",
|
|
4042
|
+
' const presented = request.headers.get("x-launchpad-monitor-secret");',
|
|
4043
|
+
" if (!presented || !(await secretsEqual(presented, env.MONITOR_SHARED_SECRET))) {",
|
|
4044
|
+
' return healthDenied("monitor_secret_mismatch");',
|
|
4045
|
+
" }",
|
|
4046
|
+
" const url = new URL(request.url);",
|
|
4047
|
+
" const dispatchRequest = buildHealthDispatchRequest(url.origin, requestId);",
|
|
4048
|
+
" const start = Date.now();",
|
|
4049
|
+
" let containerResponse: Response;",
|
|
4050
|
+
" try {",
|
|
4051
|
+
` const id = env.${bindingName}.idFromName("singleton");`,
|
|
4052
|
+
` containerResponse = await env.${bindingName}.get(id).fetch(dispatchRequest);`,
|
|
4053
|
+
" } catch {",
|
|
4054
|
+
" return Response.json(",
|
|
4055
|
+
" {",
|
|
4056
|
+
' outcome: "error",',
|
|
4057
|
+
" workload_status: null,",
|
|
4058
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4059
|
+
" request_id: requestId,",
|
|
4060
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4061
|
+
" },",
|
|
4062
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4063
|
+
" );",
|
|
4064
|
+
" }",
|
|
4065
|
+
" return Response.json(",
|
|
4066
|
+
" {",
|
|
4067
|
+
' outcome: containerResponse.ok ? "healthy" : "unhealthy",',
|
|
4068
|
+
" workload_status: containerResponse.status,",
|
|
4069
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4070
|
+
" request_id: requestId,",
|
|
4071
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4072
|
+
" },",
|
|
4073
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4074
|
+
" );",
|
|
4075
|
+
"}"
|
|
4076
|
+
] : [
|
|
4077
|
+
"/**",
|
|
4078
|
+
" * `GET /__platform/health/container` — machine-only. Gated on",
|
|
4079
|
+
" * `verifyJwt` returning a service principal whose `commonName`",
|
|
4080
|
+
" * matches the configured monitor client ID. Anonymous callers,",
|
|
4081
|
+
" * human JWTs, wrong service principals, non-GET methods, and a",
|
|
4082
|
+
" * missing monitor-client-ID configuration all fail closed with no",
|
|
4083
|
+
" * container fetch. The container's response body is never",
|
|
4084
|
+
" * forwarded; the reply is a fixed, redacted, `no-store` JSON body.",
|
|
4085
|
+
" */",
|
|
4086
|
+
"async function handleContainerHealth(",
|
|
4087
|
+
" request: Request,",
|
|
4088
|
+
" env: Env,",
|
|
4089
|
+
" requestId: string,",
|
|
4090
|
+
"): Promise<Response> {",
|
|
4091
|
+
' if (request.method !== "GET") {',
|
|
4092
|
+
' return healthDenied("method_not_allowed");',
|
|
4093
|
+
" }",
|
|
4094
|
+
" if (!env.MONITOR_CLIENT_ID || !env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
|
|
4095
|
+
' return healthDenied("monitor_unconfigured");',
|
|
4096
|
+
" }",
|
|
4097
|
+
" const token = parseAccessJwtFromHeaders(request.headers);",
|
|
4098
|
+
" if (token === null) {",
|
|
4099
|
+
' return healthDenied("access_jwt_missing");',
|
|
4100
|
+
" }",
|
|
4101
|
+
" let principal: VerifiedPrincipal;",
|
|
4102
|
+
" try {",
|
|
4103
|
+
" principal = await verifyJwt(token, {",
|
|
4104
|
+
" teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
|
|
4105
|
+
" audience: env.CF_ACCESS_AUD,",
|
|
4106
|
+
" jwks: jwks(env),",
|
|
4107
|
+
" clockTolerance: 30,",
|
|
4108
|
+
" });",
|
|
4109
|
+
" } catch (error) {",
|
|
4110
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
4111
|
+
" return healthDenied(error.code);",
|
|
4112
|
+
" }",
|
|
4113
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "monitor_jwt_verification_failed" }));',
|
|
4114
|
+
' return healthDenied("monitor_jwt_verification_failed");',
|
|
4115
|
+
" }",
|
|
4116
|
+
' if (principal.kind !== "service") {',
|
|
4117
|
+
' return healthDenied("human_identity_not_allowed");',
|
|
4118
|
+
" }",
|
|
4119
|
+
" if (principal.principal.commonName !== env.MONITOR_CLIENT_ID) {",
|
|
4120
|
+
' return healthDenied("monitor_identity_mismatch");',
|
|
4121
|
+
" }",
|
|
4122
|
+
" const url = new URL(request.url);",
|
|
4123
|
+
" const dispatchRequest = buildHealthDispatchRequest(url.origin, requestId);",
|
|
4124
|
+
" const start = Date.now();",
|
|
4125
|
+
" let containerResponse: Response;",
|
|
4126
|
+
" try {",
|
|
4127
|
+
` const id = env.${bindingName}.idFromName("singleton");`,
|
|
4128
|
+
` containerResponse = await env.${bindingName}.get(id).fetch(dispatchRequest);`,
|
|
4129
|
+
" } catch {",
|
|
4130
|
+
" return Response.json(",
|
|
4131
|
+
" {",
|
|
4132
|
+
' outcome: "error",',
|
|
4133
|
+
" workload_status: null,",
|
|
4134
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4135
|
+
" request_id: requestId,",
|
|
4136
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4137
|
+
" },",
|
|
4138
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4139
|
+
" );",
|
|
4140
|
+
" }",
|
|
4141
|
+
" return Response.json(",
|
|
4142
|
+
" {",
|
|
4143
|
+
' outcome: containerResponse.ok ? "healthy" : "unhealthy",',
|
|
4144
|
+
" workload_status: containerResponse.status,",
|
|
4145
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
4146
|
+
" request_id: requestId,",
|
|
4147
|
+
" duration_ms: Math.min(Date.now() - start, 30_000),",
|
|
4148
|
+
" },",
|
|
4149
|
+
' { status: 200, headers: { "cache-control": "no-store" } },',
|
|
4150
|
+
" );",
|
|
4151
|
+
"}"
|
|
4152
|
+
],
|
|
4153
|
+
"",
|
|
3958
4154
|
"function runtimeEvent(",
|
|
3959
4155
|
" env: Env,",
|
|
3960
4156
|
" requestId: string,",
|
|
@@ -3993,6 +4189,11 @@ function emit(capability) {
|
|
|
3993
4189
|
' runtimeEvent(env, requestId, response.status, "bindings_unknown");',
|
|
3994
4190
|
" return response;",
|
|
3995
4191
|
" }",
|
|
4192
|
+
' if (url.pathname === "/__platform/health/container") {',
|
|
4193
|
+
" const response = await handleContainerHealth(request, env, requestId);",
|
|
4194
|
+
' runtimeEvent(env, requestId, response.status, "container_health_probe");',
|
|
4195
|
+
" return response;",
|
|
4196
|
+
" }",
|
|
3996
4197
|
" const identity = await authenticate(request, env);",
|
|
3997
4198
|
" if (identity instanceof Response) {",
|
|
3998
4199
|
' runtimeEvent(env, requestId, identity.status, "authentication_failed");',
|
|
@@ -4033,6 +4234,8 @@ function generateWranglerToml(manifest) {
|
|
|
4033
4234
|
}
|
|
4034
4235
|
function emit2(capability) {
|
|
4035
4236
|
const maxInstances = capability.container.max_instances ?? 1;
|
|
4237
|
+
const isGateway = capability.authMode === "gateway";
|
|
4238
|
+
const authVars = isGateway ? ['GATEWAY_ISSUER = ""', 'GATEWAY_AUD = ""', 'GATEWAY_JWKS_URL = ""'] : ['CF_ACCESS_AUD = ""', 'CF_ACCESS_TEAM_DOMAIN = ""'];
|
|
4036
4239
|
return [
|
|
4037
4240
|
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
4038
4241
|
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
@@ -4042,14 +4245,14 @@ function emit2(capability) {
|
|
|
4042
4245
|
'main = "worker/index.ts"',
|
|
4043
4246
|
`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`,
|
|
4044
4247
|
`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => `"${flag}"`).join(", ")}]`,
|
|
4248
|
+
...isGateway ? ["workers_dev = false"] : [],
|
|
4045
4249
|
"",
|
|
4046
4250
|
"[observability]",
|
|
4047
4251
|
"enabled = true",
|
|
4048
4252
|
"head_sampling_rate = 1.0",
|
|
4049
4253
|
"",
|
|
4050
4254
|
"[vars]",
|
|
4051
|
-
|
|
4052
|
-
'CF_ACCESS_TEAM_DOMAIN = ""',
|
|
4255
|
+
...authVars,
|
|
4053
4256
|
'LAUNCHPAD_DEPLOYMENT_SHA = ""',
|
|
4054
4257
|
"",
|
|
4055
4258
|
"[[containers]]",
|
|
@@ -4093,6 +4296,10 @@ function generateGithubDeployWorkflow(manifest) {
|
|
|
4093
4296
|
return { kind: "ok", content: emit3(result.capability), warnings: [] };
|
|
4094
4297
|
}
|
|
4095
4298
|
function emit3(capability) {
|
|
4299
|
+
const isGateway = capability.authMode === "gateway";
|
|
4300
|
+
const authVarNames = isGateway ? ["GATEWAY_ISSUER", "GATEWAY_AUD", "GATEWAY_JWKS_URL"] : ["CF_ACCESS_AUD", "CF_ACCESS_TEAM_DOMAIN"];
|
|
4301
|
+
const authVarEnvLines = authVarNames.map((name) => ` ${name}: \${{ vars.${name} }}`);
|
|
4302
|
+
const authVarWranglerFlags = authVarNames.map((name) => ` --var "${name}:\${${name}}" \\`);
|
|
4096
4303
|
const lines = [
|
|
4097
4304
|
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
4098
4305
|
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
@@ -4181,29 +4388,22 @@ function emit3(capability) {
|
|
|
4181
4388
|
" cache: npm",
|
|
4182
4389
|
" cache-dependency-path: |",
|
|
4183
4390
|
" container/package-lock.json",
|
|
4184
|
-
" container/backend/package-lock.json",
|
|
4185
4391
|
"",
|
|
4186
4392
|
" - name: Install frozen Worker dependencies",
|
|
4187
4393
|
" working-directory: container",
|
|
4188
4394
|
" run: npm ci --ignore-scripts",
|
|
4189
4395
|
"",
|
|
4190
|
-
" - name: Install frozen workload dependencies",
|
|
4191
|
-
" working-directory: container/backend",
|
|
4192
|
-
" run: npm ci --ignore-scripts",
|
|
4193
|
-
"",
|
|
4194
4396
|
" - name: Verify required deployment inputs",
|
|
4195
4397
|
" env:",
|
|
4196
4398
|
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4197
4399
|
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4198
|
-
|
|
4199
|
-
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4400
|
+
...authVarEnvLines,
|
|
4200
4401
|
" BINDINGS_PROXY_SECRET: ${{ secrets.BINDINGS_PROXY_SECRET }}",
|
|
4201
|
-
"
|
|
4202
|
-
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4402
|
+
...isGateway ? [" MONITOR_SHARED_SECRET: ${{ secrets.MONITOR_SHARED_SECRET }}"] : [],
|
|
4203
4403
|
...capability.environmentNames.map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
4204
4404
|
" run: |",
|
|
4205
4405
|
" set -euo pipefail",
|
|
4206
|
-
|
|
4406
|
+
` for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID BINDINGS_PROXY_SECRET${isGateway ? " MONITOR_SHARED_SECRET" : ""} ${authVarNames.join(" ")}; do`,
|
|
4207
4407
|
' if [ -z "${!name:-}" ]; then',
|
|
4208
4408
|
' echo "Required deployment input $name is empty" >&2',
|
|
4209
4409
|
" exit 1",
|
|
@@ -4216,37 +4416,207 @@ function emit3(capability) {
|
|
|
4216
4416
|
" fi"
|
|
4217
4417
|
]),
|
|
4218
4418
|
"",
|
|
4219
|
-
" - name: Deploy Worker
|
|
4419
|
+
" - name: Deploy Worker, container image and secrets",
|
|
4220
4420
|
" working-directory: container",
|
|
4221
4421
|
" env:",
|
|
4222
4422
|
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4223
4423
|
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4424
|
+
...authVarEnvLines,
|
|
4425
|
+
...secretNames(capability).map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
4224
4426
|
" run: |",
|
|
4427
|
+
" set -euo pipefail",
|
|
4428
|
+
' secrets_file="$(mktemp "${RUNNER_TEMP}/launchpad-secrets.XXXXXX")"',
|
|
4429
|
+
` trap 'rm -f "$secrets_file"' EXIT`,
|
|
4430
|
+
` jq -n '${secretsJqFilter(capability)}' > "$secrets_file"`,
|
|
4225
4431
|
" npx --no-install wrangler deploy \\",
|
|
4226
4432
|
' --message "launchpad:${GITHUB_SHA}" \\',
|
|
4227
4433
|
' --tag "launchpad-deployment-sha:${GITHUB_SHA}" \\',
|
|
4228
|
-
|
|
4229
|
-
' --var "
|
|
4230
|
-
' --
|
|
4231
|
-
""
|
|
4232
|
-
...secretSteps(capability)
|
|
4434
|
+
...authVarWranglerFlags,
|
|
4435
|
+
' --var "LAUNCHPAD_DEPLOYMENT_SHA:${GITHUB_SHA}" \\',
|
|
4436
|
+
' --secrets-file "$secrets_file"',
|
|
4437
|
+
""
|
|
4233
4438
|
];
|
|
4234
4439
|
return lines.join(`
|
|
4235
4440
|
`);
|
|
4236
4441
|
}
|
|
4237
|
-
function
|
|
4238
|
-
return ["BINDINGS_PROXY_SECRET", ...capability.environmentNames]
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4442
|
+
function secretNames(capability) {
|
|
4443
|
+
return capability.authMode === "gateway" ? ["BINDINGS_PROXY_SECRET", "MONITOR_SHARED_SECRET", ...capability.environmentNames] : ["BINDINGS_PROXY_SECRET", ...capability.environmentNames];
|
|
4444
|
+
}
|
|
4445
|
+
function secretsJqFilter(capability) {
|
|
4446
|
+
return `{${secretNames(capability).map((name) => `${name}: env.${name}`).join(", ")}}`;
|
|
4447
|
+
}
|
|
4448
|
+
// ../launchpad-engine/dist/per-app-lock.js
|
|
4449
|
+
var PER_APP_LOCK_HCL = `# This file is maintained automatically by "terraform init".
|
|
4450
|
+
# Manual edits may be lost in future updates.
|
|
4451
|
+
|
|
4452
|
+
provider "registry.terraform.io/cloudflare/cloudflare" {
|
|
4453
|
+
version = "5.19.1"
|
|
4454
|
+
constraints = "~> 5.19"
|
|
4455
|
+
hashes = [
|
|
4456
|
+
"h1:HkKPMZ/n+QiExkRUSLjGMTGnuIaph+k932LiTp7CKZM=",
|
|
4457
|
+
"h1:mopYISyLkUVUwMjJbuPenxIUVDrna1/7ACB1hfMfciU=",
|
|
4458
|
+
"zh:0651618000db705564dab5a25322b9d76ea54b7dd78931ed3565497b559babeb",
|
|
4459
|
+
"zh:1a7847e9479fb6d21a65ef933ffae1416b1e4b44ca940c0d6c50fc4248cc4a0d",
|
|
4460
|
+
"zh:5597cee5854131045eb9f201ae3a70b59c51955d31a647d9616863c746d902cb",
|
|
4461
|
+
"zh:580786830d93e35b957754fd4c62d4681a3b19abc28b757e41acba26455663b1",
|
|
4462
|
+
"zh:83c4bdfb0e74fd50e56fff3c461d76c1c1ec61af3f679e4de1aa70b5ed05a09f",
|
|
4463
|
+
"zh:abb4d1052cee61d80f9cb51e5421e3c118312403afb7104b98bd7e310ac736ee",
|
|
4464
|
+
"zh:b0aeeb3d66ea4d719989875e778c477065ba941e3a76e9a8caacc3be08208dd9",
|
|
4465
|
+
"zh:e43b4b2dfcec1ce2115f5a5c86042d432deb49bee8eae103eb56d97ea02e2e3b",
|
|
4466
|
+
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
|
4467
|
+
]
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
provider "registry.terraform.io/hashicorp/azurerm" {
|
|
4471
|
+
version = "4.76.0"
|
|
4472
|
+
constraints = "~> 4.0"
|
|
4473
|
+
hashes = [
|
|
4474
|
+
"h1:4VD01UBYjsa3w7zeEBKnDtwClwpsPw6Dk+V9evhiKqU=",
|
|
4475
|
+
"h1:bOzo55MQgsvQ+KRwrWM5q+waapuMZ3AGIkK5ZIOe+JU=",
|
|
4476
|
+
"zh:07bb3a087abca8bd14cc28e3d3d4abb71e0ed711ecec65e7c0a2f1a97b948cf4",
|
|
4477
|
+
"zh:0fa6640b5d6c0b0fe0b7fa25cebc0091b7dda2efd83ec0347c5a50dfce0957bc",
|
|
4478
|
+
"zh:3c8618c8a5ab07fadfcb53ecf614f7b99e25bb6c407cf0af703a6b0647d4461d",
|
|
4479
|
+
"zh:5f23fdda6ff290bc99b25b68f612bf67fbb503b04b1e38664e66d3204a51e99e",
|
|
4480
|
+
"zh:68e6794cbe5e3b021657035c2465767b8296beed6c9acc8a9e032c4b90ba5746",
|
|
4481
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4482
|
+
"zh:8a51dad112248d3fdfb19cdd65a182bd6a37e7fb7f1af801ab72a962813b5188",
|
|
4483
|
+
"zh:945ff59c36de8c2128487f4ae0838655feef7c869112430ffa24ea69ff483b76",
|
|
4484
|
+
"zh:b7306af0ecc8ad78771c17e0e911dd7a561251e4255ffe80516f0654a53f42bb",
|
|
4485
|
+
"zh:c584de4858fc4ece330f1986f371acd235a5444e9b14ecea177779b1214539e0",
|
|
4486
|
+
"zh:daa3675af21f8cdf330532af0b420287388f62ac57eaf0bd4c48e732d1052f35",
|
|
4487
|
+
"zh:e76d972712b48a689f8deae41a3ad796c31d8b3798074d66c6f68e2b3b0aaa1f",
|
|
4488
|
+
]
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
provider "registry.terraform.io/hashicorp/time" {
|
|
4492
|
+
version = "0.14.0"
|
|
4493
|
+
constraints = "~> 0.13"
|
|
4494
|
+
hashes = [
|
|
4495
|
+
"h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=",
|
|
4496
|
+
"h1:4EThC3ocCFiFPMZQSUvSGSxoJqBcGWxMcFYmL67uS7Y=",
|
|
4497
|
+
"zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937",
|
|
4498
|
+
"zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1",
|
|
4499
|
+
"zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10",
|
|
4500
|
+
"zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341",
|
|
4501
|
+
"zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95",
|
|
4502
|
+
"zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3",
|
|
4503
|
+
"zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2",
|
|
4504
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4505
|
+
"zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7",
|
|
4506
|
+
"zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb",
|
|
4507
|
+
"zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed",
|
|
4508
|
+
"zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b",
|
|
4509
|
+
"zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79",
|
|
4510
|
+
]
|
|
4511
|
+
}
|
|
4512
|
+
`;
|
|
4513
|
+
|
|
4514
|
+
// ../launchpad-engine/dist/per-app-container-lock.js
|
|
4515
|
+
var PER_APP_CONTAINER_LOCK_HCL = `# This file is maintained automatically by "terraform init".
|
|
4516
|
+
# Manual edits may be lost in future updates.
|
|
4517
|
+
|
|
4518
|
+
provider "registry.terraform.io/cloudflare/cloudflare" {
|
|
4519
|
+
version = "5.19.1"
|
|
4520
|
+
constraints = "~> 5.19"
|
|
4521
|
+
hashes = [
|
|
4522
|
+
"h1:HkKPMZ/n+QiExkRUSLjGMTGnuIaph+k932LiTp7CKZM=",
|
|
4523
|
+
"h1:mopYISyLkUVUwMjJbuPenxIUVDrna1/7ACB1hfMfciU=",
|
|
4524
|
+
"zh:0651618000db705564dab5a25322b9d76ea54b7dd78931ed3565497b559babeb",
|
|
4525
|
+
"zh:1a7847e9479fb6d21a65ef933ffae1416b1e4b44ca940c0d6c50fc4248cc4a0d",
|
|
4526
|
+
"zh:5597cee5854131045eb9f201ae3a70b59c51955d31a647d9616863c746d902cb",
|
|
4527
|
+
"zh:580786830d93e35b957754fd4c62d4681a3b19abc28b757e41acba26455663b1",
|
|
4528
|
+
"zh:83c4bdfb0e74fd50e56fff3c461d76c1c1ec61af3f679e4de1aa70b5ed05a09f",
|
|
4529
|
+
"zh:abb4d1052cee61d80f9cb51e5421e3c118312403afb7104b98bd7e310ac736ee",
|
|
4530
|
+
"zh:b0aeeb3d66ea4d719989875e778c477065ba941e3a76e9a8caacc3be08208dd9",
|
|
4531
|
+
"zh:e43b4b2dfcec1ce2115f5a5c86042d432deb49bee8eae103eb56d97ea02e2e3b",
|
|
4532
|
+
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
|
4533
|
+
]
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
provider "registry.terraform.io/hashicorp/random" {
|
|
4537
|
+
version = "3.9.0"
|
|
4538
|
+
constraints = "~> 3.6"
|
|
4539
|
+
hashes = [
|
|
4540
|
+
"h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
|
|
4541
|
+
"h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=",
|
|
4542
|
+
"zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
|
|
4543
|
+
"zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
|
|
4544
|
+
"zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
|
|
4545
|
+
"zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
|
|
4546
|
+
"zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
|
|
4547
|
+
"zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
|
|
4548
|
+
"zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
|
|
4549
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4550
|
+
"zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
|
|
4551
|
+
"zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
|
|
4552
|
+
"zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
|
|
4553
|
+
"zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
|
|
4554
|
+
"zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
|
|
4555
|
+
]
|
|
4249
4556
|
}
|
|
4557
|
+
|
|
4558
|
+
provider "registry.terraform.io/hashicorp/time" {
|
|
4559
|
+
version = "0.14.0"
|
|
4560
|
+
constraints = "~> 0.13"
|
|
4561
|
+
hashes = [
|
|
4562
|
+
"h1:/hlxsUpuN/lvPTNL9+NyVGsOyRsK5NsxwFMsj5CdOp4=",
|
|
4563
|
+
"h1:4EThC3ocCFiFPMZQSUvSGSxoJqBcGWxMcFYmL67uS7Y=",
|
|
4564
|
+
"zh:12abfd6b800e4d7fa6db7310dec8ffd440b31993861ef188c7ed5260b3073937",
|
|
4565
|
+
"zh:23005521e800bb19e1597bf755c5f70d675d30b685d4255001ed5fa47d9df3f1",
|
|
4566
|
+
"zh:2fea249b582ae97cd1cc10385187ea50993bb47c28cc5df0305e57ceaabf0a10",
|
|
4567
|
+
"zh:322018d3b987b7aad08697178029a2bb667bed699e88328f0c89c52a2fd41341",
|
|
4568
|
+
"zh:32a08e98fce2d273cb9b2c89d6c54727cc9f0a32e15bfd896be4e02cc6b48f95",
|
|
4569
|
+
"zh:3db89aabd0e619616bd4b0f8b373a7586dfe60feffcea12a84a0bdbc445714b3",
|
|
4570
|
+
"zh:7488f56c81d742dc020f29063626c8f07ca188aa97be61e7307e8d62397020a2",
|
|
4571
|
+
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
|
4572
|
+
"zh:7cb4067f2e7559b13f7562ef722f948950901eb37834873e98360ab28f66e9d7",
|
|
4573
|
+
"zh:9d552c8345f61e1b7db8e725144981345f18ac1014d58d6f5ddf0928a195fffb",
|
|
4574
|
+
"zh:a8e69fb6b97fc9d86fb19a9f4d42abe33c4a68e700b15387ce2e17d2b9934bed",
|
|
4575
|
+
"zh:aeeb900eb8dd0f790c60ea5c0e0c8d42bd6e4a54f391681d4decca15b544394b",
|
|
4576
|
+
"zh:c239c619101a8c95e1f14061eb973c57a8d15fa0e68878ced5bbd76858ee5b79",
|
|
4577
|
+
]
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
provider "registry.terraform.io/integrations/github" {
|
|
4581
|
+
version = "6.12.1"
|
|
4582
|
+
constraints = "~> 6.0"
|
|
4583
|
+
hashes = [
|
|
4584
|
+
"h1:bGz4LIep/7PVrqy6P8cTYbAJpdxXGrupUJjkCczlzIs=",
|
|
4585
|
+
"h1:hHOwf554tYL9pQS2uRPbaAGSXRbbBJ/+HtQAoT9mtuA=",
|
|
4586
|
+
"zh:3e1a4081ecb9518fdf0074db83c16ad00dc81ffe8249a6e3cf1894e947e28df6",
|
|
4587
|
+
"zh:4cb8224b7f530795b674ac044675f6b22a7c9154f55eb9f76c5af6c7534056a4",
|
|
4588
|
+
"zh:560bc08637926191f6871a89e986022ca67c70afda5bebca34b5216e6fac69c9",
|
|
4589
|
+
"zh:5a70b5d2ac650c5c9819a1875411ebda229d0fcc6c9f57f9d751852ca3cd77ac",
|
|
4590
|
+
"zh:8668d93bd4dc2ffa2545e1473af600a925d479b16033a71a4498a16f3b683c0c",
|
|
4591
|
+
"zh:86eacc6059fd057948e178b665ba5cce74bd5488a9e1035734e60ff5ef1b6f8f",
|
|
4592
|
+
"zh:a329fac98881d8dfc211a9bdc0ec6f2948f0b0c2704d1b6cbe5307403c7ad1b2",
|
|
4593
|
+
"zh:dadd44abab3c52b9d572955afaef1658790e17ea355ee22b58996d81d28e02d8",
|
|
4594
|
+
"zh:de9f455ef342cc38fb76bce844bfcd376fb81a4b9f9bc2fae023ff99efdf1338",
|
|
4595
|
+
"zh:f8c6d2e8351b334491790358574e0a30a7c6d7f5b80f7daf32a7c0f3e9b1ab19",
|
|
4596
|
+
"zh:fab41971a3edee04ab6eceaeab4eeb9a2b2f38a2af3b06eda93e2117b64994be",
|
|
4597
|
+
"zh:fb1279b566dd9c8c117b2e4e0cc8344413b8fc8f2a3e24be22a9b2610551777b",
|
|
4598
|
+
"zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25",
|
|
4599
|
+
"zh:fe79d2a861fb9af420fa5bd7f02c031b2a0a3edf5dbc46022c8ecc7a33cf2b6d",
|
|
4600
|
+
]
|
|
4601
|
+
}
|
|
4602
|
+
`;
|
|
4603
|
+
var PER_APP_CONTAINER_GATEWAY_LOCK_HCL = (() => {
|
|
4604
|
+
const marker = 'provider "registry.terraform.io/hashicorp/azurerm" {';
|
|
4605
|
+
const start = PER_APP_LOCK_HCL.indexOf(marker);
|
|
4606
|
+
const end = PER_APP_LOCK_HCL.indexOf(`
|
|
4607
|
+
}
|
|
4608
|
+
`, start) + 3;
|
|
4609
|
+
if (start < 0 || end < 3)
|
|
4610
|
+
throw new Error("azurerm block missing from the pages lock");
|
|
4611
|
+
const azurerm = PER_APP_LOCK_HCL.slice(start, end);
|
|
4612
|
+
const before = 'provider "registry.terraform.io/hashicorp/random" {';
|
|
4613
|
+
const at = PER_APP_CONTAINER_LOCK_HCL.indexOf(before);
|
|
4614
|
+
if (at < 0)
|
|
4615
|
+
throw new Error("random block missing from the container lock");
|
|
4616
|
+
return PER_APP_CONTAINER_LOCK_HCL.slice(0, at) + azurerm + `
|
|
4617
|
+
` + PER_APP_CONTAINER_LOCK_HCL.slice(at);
|
|
4618
|
+
})();
|
|
4619
|
+
|
|
4250
4620
|
// ../launchpad-engine/dist/per-app-workspace.js
|
|
4251
4621
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
4252
4622
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
@@ -11789,7 +12159,7 @@ function buildManifest(inputs, detected) {
|
|
|
11789
12159
|
catalogue: { listed: false },
|
|
11790
12160
|
access
|
|
11791
12161
|
};
|
|
11792
|
-
const resolvedAuth = inputs.auth ??
|
|
12162
|
+
const resolvedAuth = inputs.auth ?? "gateway";
|
|
11793
12163
|
if (resolvedAuth === "gateway") {
|
|
11794
12164
|
manifest.auth = "gateway";
|
|
11795
12165
|
}
|
|
@@ -12402,6 +12772,20 @@ function describe25(e) {
|
|
|
12402
12772
|
|
|
12403
12773
|
// src/commands/plan.ts
|
|
12404
12774
|
import { resolve as resolve12 } from "node:path";
|
|
12775
|
+
|
|
12776
|
+
// src/manifest/container-runtime-warnings.ts
|
|
12777
|
+
var CONTAINER_RUNTIME_GATE_WARNING = "Container apps are gated by the platform runtime switch; `launchpad deploy` may return " + "503 container_runtime_off until the platform team enables your app. See docs/concepts/containers.md.";
|
|
12778
|
+
var RUNTIME_ACA_RESERVED_WARNING = "deployment.runtime: aca is reserved and currently ignored — the app runs on cloudflare-containers " + "regardless. See docs/concepts/containers.md.";
|
|
12779
|
+
function containerRuntimeWarnings(view) {
|
|
12780
|
+
if (view.deploymentType !== "container")
|
|
12781
|
+
return [];
|
|
12782
|
+
const warnings = [CONTAINER_RUNTIME_GATE_WARNING];
|
|
12783
|
+
if (view.runtime === "aca")
|
|
12784
|
+
warnings.push(RUNTIME_ACA_RESERVED_WARNING);
|
|
12785
|
+
return warnings;
|
|
12786
|
+
}
|
|
12787
|
+
|
|
12788
|
+
// src/commands/plan.ts
|
|
12405
12789
|
var planCommand = {
|
|
12406
12790
|
name: "plan",
|
|
12407
12791
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -12433,7 +12817,15 @@ function renderJson(result, io) {
|
|
|
12433
12817
|
switch (result.kind) {
|
|
12434
12818
|
case "ok": {
|
|
12435
12819
|
const plan = derivePlan(result.manifest);
|
|
12436
|
-
|
|
12820
|
+
const warnings = containerRuntimeWarnings({
|
|
12821
|
+
deploymentType: plan.desired.app.deploymentType,
|
|
12822
|
+
runtime: plan.desired.app.runtime
|
|
12823
|
+
});
|
|
12824
|
+
io.out(JSON.stringify({
|
|
12825
|
+
ok: true,
|
|
12826
|
+
...plan,
|
|
12827
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
12828
|
+
}));
|
|
12437
12829
|
return 0;
|
|
12438
12830
|
}
|
|
12439
12831
|
case "file-not-found":
|
|
@@ -12543,6 +12935,12 @@ function renderPlan(m, io) {
|
|
|
12543
12935
|
}
|
|
12544
12936
|
io.out("");
|
|
12545
12937
|
}
|
|
12938
|
+
for (const w of containerRuntimeWarnings({
|
|
12939
|
+
deploymentType: desired.app.deploymentType,
|
|
12940
|
+
runtime: desired.app.runtime
|
|
12941
|
+
})) {
|
|
12942
|
+
io.out(`⚠ ${w}`);
|
|
12943
|
+
}
|
|
12546
12944
|
const totalBindings = desired.secrets.envFile.length + desired.secrets.platformManaged.length;
|
|
12547
12945
|
io.out(`Secrets (${totalBindings})`);
|
|
12548
12946
|
if (totalBindings === 0) {
|
|
@@ -16106,6 +16504,9 @@ function renderHumanOk(result, group, boundary, io) {
|
|
|
16106
16504
|
if (!boundary.declared) {
|
|
16107
16505
|
io.out(`⚠ ${DECLARE_APP_HINT}`);
|
|
16108
16506
|
}
|
|
16507
|
+
for (const w of containerRuntimeWarnings(view)) {
|
|
16508
|
+
io.out(`⚠ ${w}`);
|
|
16509
|
+
}
|
|
16109
16510
|
switch (group.kind) {
|
|
16110
16511
|
case "skipped":
|
|
16111
16512
|
return 0;
|
|
@@ -16172,6 +16573,7 @@ function renderHumanError(result, io) {
|
|
|
16172
16573
|
}
|
|
16173
16574
|
function renderJsonOk(result, group, boundary, io) {
|
|
16174
16575
|
const view = manifestView(result.manifest);
|
|
16576
|
+
const runtimeWarnings = containerRuntimeWarnings(view);
|
|
16175
16577
|
const base = {
|
|
16176
16578
|
manifest_valid: true,
|
|
16177
16579
|
runtime_supported: true,
|
|
@@ -16181,7 +16583,8 @@ function renderJsonOk(result, group, boundary, io) {
|
|
|
16181
16583
|
declared: boundary.declared,
|
|
16182
16584
|
errors: boundary.errors,
|
|
16183
16585
|
warnings: boundary.warnings
|
|
16184
|
-
}
|
|
16586
|
+
},
|
|
16587
|
+
...runtimeWarnings.length > 0 ? { warnings: runtimeWarnings } : {}
|
|
16185
16588
|
};
|
|
16186
16589
|
if (boundary.errors.length > 0) {
|
|
16187
16590
|
io.out(JSON.stringify({ ok: false, ...base }));
|
|
@@ -16270,7 +16673,8 @@ function manifestView(manifest) {
|
|
|
16270
16673
|
access: manifest.spec.access,
|
|
16271
16674
|
buildCommand: manifest.spec.build?.command,
|
|
16272
16675
|
destinationDir: manifest.spec.build?.destination_dir,
|
|
16273
|
-
appDeclared: manifest.spec.app !== undefined
|
|
16676
|
+
appDeclared: manifest.spec.app !== undefined,
|
|
16677
|
+
runtime: undefined
|
|
16274
16678
|
};
|
|
16275
16679
|
}
|
|
16276
16680
|
return {
|
|
@@ -16279,7 +16683,8 @@ function manifestView(manifest) {
|
|
|
16279
16683
|
access: manifest.access,
|
|
16280
16684
|
buildCommand: manifest.build?.command,
|
|
16281
16685
|
destinationDir: manifest.build?.destination_dir,
|
|
16282
|
-
appDeclared: manifest.app !== undefined
|
|
16686
|
+
appDeclared: manifest.app !== undefined,
|
|
16687
|
+
runtime: manifest.deployment.runtime
|
|
16283
16688
|
};
|
|
16284
16689
|
}
|
|
16285
16690
|
function parseArgs13(args) {
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface InitInputs {
|
|
|
10
10
|
type: AppType | undefined;
|
|
11
11
|
/**
|
|
12
12
|
* Edge gating mode (sp-gwdflt). Supplied via `--auth`. Undefined ⇒
|
|
13
|
-
* the buildManifest default: `gateway` for
|
|
14
|
-
*
|
|
13
|
+
* the buildManifest default: `gateway` for every shape, containers
|
|
14
|
+
* included (AIO-811).
|
|
15
15
|
*/
|
|
16
16
|
auth: AuthMode | undefined;
|
|
17
17
|
/** New apps choose dispatch explicitly; existing manifests keep their origin. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAMjE,eAAO,MAAM,WAAW,EAAE,OAIzB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AASjE,eAAO,MAAM,eAAe,EAAE,OAI7B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const CONTAINER_RUNTIME_GATE_WARNING: string;
|
|
2
|
+
export declare const RUNTIME_ACA_RESERVED_WARNING: string;
|
|
3
|
+
/**
|
|
4
|
+
* Non-blocking advisories for a manifest, derived from its deployment
|
|
5
|
+
* type and (v1alpha1-only) runtime field. Empty for any non-container
|
|
6
|
+
* manifest, so callers can spread/append it without a type-specific
|
|
7
|
+
* branch.
|
|
8
|
+
*/
|
|
9
|
+
export declare function containerRuntimeWarnings(view: {
|
|
10
|
+
readonly deploymentType: string;
|
|
11
|
+
readonly runtime: string | undefined;
|
|
12
|
+
}): readonly string[];
|
|
13
|
+
//# sourceMappingURL=container-runtime-warnings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-runtime-warnings.d.ts","sourceRoot":"","sources":["../../src/manifest/container-runtime-warnings.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,8BAA8B,QAE6D,CAAC;AAOzG,eAAO,MAAM,4BAA4B,QAEO,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,GAAG,SAAS,MAAM,EAAE,CAKpB"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.66.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m-kopa/launchpad-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"description": "Launchpad CLI — clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "22.18.10",
|
|
40
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
41
|
-
"@typescript-eslint/parser": "8.
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "8.71.0",
|
|
41
|
+
"@typescript-eslint/parser": "8.71.0",
|
|
42
42
|
"@vitest/coverage-istanbul": "4.1.5",
|
|
43
43
|
"eslint": "9.39.4",
|
|
44
44
|
"typescript": "5.9.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-content-pr
|
|
3
3
|
description: Push a content change to a Launchpad app via `launchpad deploy` and verify it shipped via `launchpad status`. Covers the post-first-deploy iteration loop (edit → deploy → verify) — subsequent deploys commit directly to the app repo's main and the Pages build runs asynchronously, so verification is its own step. Use when someone says "push a content change", "ship an update", "/launchpad-content-pr", "verify my deploy", or after `/launchpad-deploy` reports `done` and they want to follow up with an edit.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-deploy
|
|
3
3
|
description: Walk a Launchpad user through deploying an app from their local working directory (Model A — `launchpad init` + `launchpad deploy`). Wraps the CLI verbs end-to-end: detects the app shape, scaffolds `launchpad.yaml`, resolves the allowed Entra group via `launchpad groups`, bundles the CWD via `launchpad deploy`, and watches the rollout via `launchpad status`. Use when someone says "deploy a new app", "ship my app to Launchpad", "/launchpad-deploy", "I have an app locally — get it on Launchpad", or any variant. Resume/abandon for legacy in-flight provisioning is at the bottom.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -165,12 +165,20 @@ For a React API app, `functions/` converts as-is and the dispatch build compiles
|
|
|
165
165
|
When deploy says secrets carried over, tell the author to run `launchpad secrets push` once `launchpad status` shows the first delivery; pushing earlier fails with `dispatch_script_not_delivered`.
|
|
166
166
|
Still refused on dispatch, with the reason printed: wrangler settings beyond name, compatibility date/flags, `pages_build_output_dir` and one D1 (so KV, R2, services, vars, Durable Objects, queues), a root `functions/_middleware` or root-level catch-all that would claim `/*`, a hand-written `_worker.js`, and functions that compile to more than one module.
|
|
167
167
|
A function that calls `next()` to fall through to a static asset does not work on dispatch, because the Worker has no asset fallback.
|
|
168
|
+
A dispatch build also refuses hidden files or directories inside the build output (`destination_dir`) with `hidden build output is not supported: <path>`.
|
|
169
|
+
`launchpad status <slug>` shows this as `v2 delivery: state refused` followed by the reason.
|
|
170
|
+
The usual cause in a React API app is three files copied from the starter template: `static/.github/workflows/launchpad-review.yml`, `static/.github/scripts/parse-html-validate.mjs` and `static/public/.gitkeep`.
|
|
171
|
+
Fix it on dispatch: show the author the paths the error names, delete them with the author's agreement, then run `launchpad deploy` again.
|
|
172
|
+
Delete only the paths the error names, and never the repository-root `.github/` directory.
|
|
173
|
+
If `launchpad status` shows the same refusal after that deploy, the paths are outside the manifest's `app.include`, so the deploy cannot remove them; offer `/launchpad-report` instead of editing `app.include` to force it.
|
|
174
|
+
A refused dispatch build is not a reason to switch to Pages: do not rewrite the manifest as v1, set `origin: pages`, or pass `--origin pages` to get past it.
|
|
175
|
+
The Cloudflare account has a fixed limit of 100 Pages projects, so a new Pages project can fail at Terraform apply even after the CLI accepts the deploy.
|
|
168
176
|
Changing `appType` to `react` would not convert the API; do not suggest it.
|
|
169
177
|
Do not use this command to migrate a registered app or delete its Pages resources.
|
|
170
178
|
If the pool is full or inventory cannot be read, report the capacity error; do not fall back to Pages or change authentication to bypass it.
|
|
171
179
|
Do not replace that manifest with a v1 example or add `--origin pages` just because an older app uses Pages.
|
|
172
180
|
Use `--origin pages` only when Pages is deliberately required.
|
|
173
|
-
Containers keep their existing deployment path.
|
|
181
|
+
Containers keep their existing deployment path, with one change: use `auth: gateway` for them (see "Container apps" below).
|
|
174
182
|
Dispatch supports plain-text environment variables and `secret_text` env vars; after the first deploy is delivered, `launchpad secrets push` sets the secret values, live immediately with no redeploy.
|
|
175
183
|
|
|
176
184
|
This is interactive by default. It will ask for:
|
|
@@ -222,7 +230,7 @@ commit to a slug:
|
|
|
222
230
|
| `static` | n/a | n/a | n/a | No bundler, no TS, no React. |
|
|
223
231
|
| `react` | n/a | client-side or remote-fetch only | n/a | SPA only — no server-side anything. |
|
|
224
232
|
| `react+api` | `hono` (exclusive) | Cloudflare D1 / Neon (HTTP) / KV / R2 | **Sibling cron Worker** (Pages has no `scheduled` handler — see "Two-tier apps" below) | Requires `compatibility_flags = ["nodejs_compat"]` in `wrangler.toml` (ADR-0011 carve-out). |
|
|
225
|
-
| `container` | any HTTP server | any (container-local or HTTP backend) | container-local | Single HTTP port.
|
|
233
|
+
| `container` | any HTTP server | any (container-local or HTTP backend) | container-local | Single HTTP port. Use `auth: gateway` (default for containers; see "Container apps" below). The rest of the guided flow in this skill covers `static`/`react`/`react+api`. |
|
|
226
234
|
|
|
227
235
|
**Will not run on the Workers runtime** (advisory — representative,
|
|
228
236
|
not exhaustive): `fastify` / `@fastify/*` / `express` / `koa` /
|
|
@@ -346,6 +354,39 @@ endpoint. Files outside the manifest's app boundary (`app.root` /
|
|
|
346
354
|
`app.include`) and never-shippable files (private keys, `.env`
|
|
347
355
|
material) are stripped with a warning before upload.
|
|
348
356
|
|
|
357
|
+
### Container apps — use `auth: gateway` (AIO-811)
|
|
358
|
+
|
|
359
|
+
Container apps (`type: container`) sign in through the same Entra auth gateway as Pages apps. `launchpad init` scaffolds new container apps with `auth: gateway` by default; keep it. Pass `--auth access` only for a legacy Access app.
|
|
360
|
+
|
|
361
|
+
Request path: user → Entra auth gateway → `launchpad-container-router` (one service binding per app) → the app's container Worker.
|
|
362
|
+
The container Worker has no public hostname and no Cloudflare Access application.
|
|
363
|
+
It verifies the gateway's `X-Launchpad-User-Assertion` and fails closed with 401 when the assertion is missing or invalid.
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
apiVersion: launchpad.m-kopa.us/v1alpha1
|
|
367
|
+
kind: App
|
|
368
|
+
metadata:
|
|
369
|
+
name: my-container-app
|
|
370
|
+
team: my-team
|
|
371
|
+
owner: owner@m-kopa.com
|
|
372
|
+
deployment:
|
|
373
|
+
type: container
|
|
374
|
+
auth: gateway
|
|
375
|
+
catalogue:
|
|
376
|
+
listed: false
|
|
377
|
+
access:
|
|
378
|
+
allowed_entra_group: G_my_group # or omit and use a private audience
|
|
379
|
+
containers:
|
|
380
|
+
- name: my-container-app
|
|
381
|
+
image: ghcr.io/m-kopa/my-container-app:1.0.0
|
|
382
|
+
port: 8080
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`auth: access` for containers is **legacy**. Every Access-gated app uses a Cloudflare Zero Trust seat and the account has a fixed seat cap (about 50 seats; AIO-811 was prompted by hitting it), so new Access apps can be refused once the cap is reached.
|
|
386
|
+
Only keep `auth: access` on an existing container app that has not been migrated.
|
|
387
|
+
Do not switch an existing container between modes on your own; offer `/launchpad-report` so the platform team can migrate it.
|
|
388
|
+
Reading the signed-in user inside the container is covered in `/launchpad-identity`.
|
|
389
|
+
|
|
349
390
|
**First deploy vs subsequent deploys** (M-1234). Under Model A there
|
|
350
391
|
is no separate "create the app" step — `launchpad deploy` against a
|
|
351
392
|
fresh slug auto-provisions:
|
|
@@ -355,7 +396,7 @@ fresh slug auto-provisions:
|
|
|
355
396
|
workflow (TF apply, cert issuance, edge auth, app-repo scaffold).
|
|
356
397
|
Pages manifests must explicitly set `auth: gateway` to use the platform Entra-OIDC gateway.
|
|
357
398
|
`launchpad init` writes this field. Pages apps cannot opt into Cloudflare Access; only
|
|
358
|
-
containers retain that login mode. The CLI prints `✓ First-time deploy — provisioning
|
|
399
|
+
containers retain that legacy login mode, and container apps should use `auth: gateway` too (AIO-811). The CLI prints `✓ First-time deploy — provisioning
|
|
359
400
|
workflow started` and waits for delivery. If its wait expires, check
|
|
360
401
|
`launchpad status <slug>` before reporting success. Provisioning typically takes
|
|
361
402
|
5–10 minutes. **Your bundle ships with the provisioning run** — when
|
|
@@ -437,6 +478,17 @@ Flags — there is nothing useful to pass on the Model A path:
|
|
|
437
478
|
symlinks, secret-pattern hits, build-command violations) are
|
|
438
479
|
rejected at upload and self-describing in the CLI's stderr; surface
|
|
439
480
|
them verbatim and let the user fix and re-`launchpad deploy`.
|
|
481
|
+
- **Dispatch delivery refused** (`launchpad status` shows `v2 delivery:
|
|
482
|
+
state refused`) → surface the `failed at` reason verbatim. For
|
|
483
|
+
`hidden build output is not supported`, follow the fix in §A.2 and
|
|
484
|
+
re-`launchpad deploy` on dispatch. Do not move the app to Pages.
|
|
485
|
+
- **`tf_apply_failed` naming the Pages project limit** (`You have
|
|
486
|
+
reached the limit of projects`, Cloudflare code 8000027) → this is
|
|
487
|
+
a platform capacity fault, not the author's. Do not retry on Pages,
|
|
488
|
+
and do not create a new slug to get round it. A registered app that
|
|
489
|
+
was moved from dispatch to Pages needs the platform team to return
|
|
490
|
+
it to dispatch (`launchpad migrate` refuses registered apps), so
|
|
491
|
+
offer `/launchpad-report` with the slug and the error.
|
|
440
492
|
- **Terminal-failed but the app is actually serving** → `launchpad
|
|
441
493
|
recover <slug>`. The bot re-derives reality from live Cloudflare
|
|
442
494
|
state and repairs the record to `live` only when the app is
|
|
@@ -612,6 +664,10 @@ the in-flight case.
|
|
|
612
664
|
TF emits them as `secret_text`; trust it.
|
|
613
665
|
- Do **not** re-enable `confine_origin` on a gateway `react+api` app — it
|
|
614
666
|
strips the user JWT and 401s every user (see "Gateway auth").
|
|
667
|
+
- Do **not** switch a dispatch app to Pages (`origin: pages`, a v1
|
|
668
|
+
manifest or `--origin pages`) to get past a refused dispatch build.
|
|
669
|
+
Fix the reason the build names. New Pages projects are limited by
|
|
670
|
+
the account's Pages project limit; dispatch apps are not.
|
|
615
671
|
- Do **not** `wrangler secret put` for a two-tier app — `launchpad secrets
|
|
616
672
|
push` with `targets: all` seeds both tiers via the bot.
|
|
617
673
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-deploy-status
|
|
3
3
|
description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket), or watch provisioning live with `launchpad watch`. Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "watch my deploy go live", "watch provisioning", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -382,3 +382,6 @@ A missing Pages project is expected for `origin: dispatch`; do not treat it as a
|
|
|
382
382
|
Use `launchpad status <slug> --json` to compare `deployedSha`, `headSha` and `deployment.liveDeployment.buildStatus`.
|
|
383
383
|
A `live_drift_unknown` state can include a successful dispatch deployment while infrastructure drift is unavailable.
|
|
384
384
|
If the CLI's first-deploy wait expires, keep checking the recorded submission instead of creating another app or claiming it is live.
|
|
385
|
+
A `v2 delivery: state refused` line means the dispatch build refused the commit; surface its `failed at` reason verbatim.
|
|
386
|
+
For `hidden build output is not supported: <path>`, the author deletes the named paths inside the build output (usually `static/.github/` and `static/public/.gitkeep` copied from the starter) and runs `launchpad deploy` again.
|
|
387
|
+
Do not move a refused dispatch app to Pages: the Cloudflare account has a fixed limit of 100 Pages projects, and a `tf_apply_failed` reason that names the project limit is a platform fault to report with `/launchpad-report`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-destroy
|
|
3
3
|
description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-identity
|
|
3
3
|
description: Teach an app author how to use the signed-in user's identity inside a Launchpad app — read the gateway-forwarded X-Launchpad-User-Assertion in a Pages Function, VERIFY it with @m-kopa/platform-auth (fail-closed), and show who's logged in (sub/email/name). Use when someone says "who is logged in", "show the current user", "get the user's email in my app", "auth in my launchpad app", "read the user identity", "/launchpad-identity", or is wiring up an /api/me for a gateway-fronted app.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -198,6 +198,16 @@ if (me.authenticated) {
|
|
|
198
198
|
}
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
## Container apps (`type: container`, `auth: gateway`)
|
|
202
|
+
|
|
203
|
+
Container apps behind the gateway (AIO-811) get the same signed-in identity. The path is gateway → `launchpad-container-router` → your app's container Worker, and the gateway still sends `X-Launchpad-User-Assertion`.
|
|
204
|
+
|
|
205
|
+
- **The container Worker verifies the assertion for you.** The generated `container-worker.ts` reads `X-Launchpad-User-Assertion`, verifies it with `@m-kopa/platform-auth` against `GATEWAY_ISSUER`, `GATEWAY_AUD` and `GATEWAY_JWKS_URL`, and returns 401 if it is missing or invalid. It is the only gate, because the container has no public hostname and no Cloudflare Access app.
|
|
206
|
+
- **Your container process receives plain headers after that check:** `X-User-Sub` (Entra `oid`) and `X-User-Email` (tenant UPN), plus `X-User-Groups` when groups are present. Inbound `X-User-*` headers are stripped before the Worker sets its own, so a client cannot forge them.
|
|
207
|
+
- **Do not re-verify in the container unless you need to.** The raw assertion is checked at the container Worker and your process sees only the `X-User-*` headers.
|
|
208
|
+
- Same rules as Pages: identity is for attribution, not authorization. Enforce who may open the app through `access.allowed_entra_group`.
|
|
209
|
+
- The legacy `auth: access` container mode (Cloudflare Access JWT, `CF_ACCESS_*`) still works for existing apps but is limited by the Zero Trust seat cap. New container apps should use `auth: gateway`.
|
|
210
|
+
|
|
201
211
|
## The two app shapes
|
|
202
212
|
|
|
203
213
|
### (a) build-less `type: static` — vendored verifier (INTERIM)
|
|
@@ -323,6 +333,7 @@ An unverified payload can be forged by anyone who can reach your origin
|
|
|
323
333
|
- Do **not** confuse `launchpad whoami` (your CLI session) with in-app
|
|
324
334
|
identity. This skill is about the **end-user signed into your deployed
|
|
325
335
|
app**, read server-side from the forwarded token.
|
|
336
|
+
- Do **not** read identity from `X-User-*` headers in a container unless `auth: gateway` or `auth: access` is set on the app; they are only trustworthy because the container Worker verified the token first.
|
|
326
337
|
- Do **not** trust the header without verifying the signature, and do **not**
|
|
327
338
|
omit the audience check — both open you to forgery / cross-app replay.
|
|
328
339
|
- Do **not** gate access on `/api/me` claims — they are attribution, not
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-onboard
|
|
3
3
|
description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-report
|
|
3
3
|
description: File a bug report or feature request to the Launchpad team's tracker from the CLI. Use when someone reports something broken, hits an error in a launchpad command, or wishes a feature existed — e.g. "this is broken", "report a bug", "can you file that", "I wish launchpad could…", "/launchpad-bug", "/launchpad-feature". Always confirm and show exactly what you'll send before filing; never file silently.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-status
|
|
3
3
|
description: Safely refresh a Launchpad clone, show manifest drift, or read the deployed manifest. Wraps bare `launchpad pull` for source sync, `launchpad pull --manifest` for deployed YAML, and `launchpad status` for drift. Use for "sync my clone", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or post-deploy verification.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.66.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -388,3 +388,6 @@ A missing Pages project is expected for `origin: dispatch`; do not treat it as a
|
|
|
388
388
|
Use `launchpad status <slug> --json` to compare `deployedSha`, `headSha` and `deployment.liveDeployment.buildStatus`.
|
|
389
389
|
A `live_drift_unknown` state can include a successful dispatch deployment while infrastructure drift is unavailable.
|
|
390
390
|
If the CLI's first-deploy wait expires, keep checking the recorded submission instead of creating another app or claiming it is live.
|
|
391
|
+
A `v2 delivery: state refused` line means the dispatch build refused the commit; surface its `failed at` reason verbatim.
|
|
392
|
+
For `hidden build output is not supported: <path>`, the author deletes the named paths inside the build output (usually `static/.github/` and `static/public/.gitkeep` copied from the starter) and runs `launchpad deploy` again.
|
|
393
|
+
Do not move a refused dispatch app to Pages: the Cloudflare account has a fixed limit of 100 Pages projects, and a `tf_apply_failed` reason that names the project limit is a platform fault to report with `/launchpad-report`.
|