@m-kopa/launchpad-cli 0.57.0 → 0.61.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 +89 -0
- package/README.md +10 -0
- package/dist/bundle/upload.d.ts +2 -0
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +845 -326
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/status.d.ts +14 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/deploy/apply.d.ts +6 -0
- package/dist/deploy/apply.d.ts.map +1 -1
- package/dist/deploy/commit-match-settle.d.ts +14 -1
- package/dist/deploy/commit-match-settle.d.ts.map +1 -1
- package/dist/deploy/deployment-status.d.ts +19 -0
- package/dist/deploy/deployment-status.d.ts.map +1 -1
- package/dist/deploy/manifest-diff.d.ts +2 -0
- package/dist/deploy/manifest-diff.d.ts.map +1 -1
- package/dist/deploy/verify-deploy.d.ts +2 -0
- package/dist/deploy/verify-deploy.d.ts.map +1 -1
- package/dist/manifest/load.d.ts +27 -0
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/secrets/push.d.ts +8 -0
- package/dist/secrets/push.d.ts.map +1 -1
- package/dist/secrets/status.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +13 -1
- package/skills/launchpad-deploy/SKILL.md +50 -23
- package/skills/launchpad-deploy-status/SKILL.md +10 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +13 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +10 -1
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.61.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import { readFileSync } from "node:fs";
|
|
@@ -2138,6 +2138,9 @@ function parse202(body, fallbackSlug) {
|
|
|
2138
2138
|
slug: typeof body.slug === "string" ? body.slug : fallbackSlug,
|
|
2139
2139
|
message: typeof body.message === "string" ? body.message : "bundle committed; deployment is pending"
|
|
2140
2140
|
};
|
|
2141
|
+
if (body.v2Delivery !== undefined && body.v2Delivery !== "pending" && body.v2Delivery !== "deferred") {
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2141
2144
|
return reconcile === undefined ? accepted : { ...accepted, reconcile };
|
|
2142
2145
|
}
|
|
2143
2146
|
if (body.status === "provisioning_started") {
|
|
@@ -2567,11 +2570,23 @@ import { join as join6 } from "node:path";
|
|
|
2567
2570
|
import { parse as parseYaml2 } from "yaml";
|
|
2568
2571
|
|
|
2569
2572
|
// ../launchpad-engine/dist/schema.js
|
|
2573
|
+
import { z as z2 } from "zod";
|
|
2574
|
+
|
|
2575
|
+
// ../launchpad-engine/dist/audience.js
|
|
2570
2576
|
import { z } from "zod";
|
|
2577
|
+
var PrivateOwnerSchema = z.object({ oid: z.uuid(), email: z.email() }).strict();
|
|
2578
|
+
function validateAudience(access, ctx) {
|
|
2579
|
+
const groups = access.allowed_entra_groups ?? (access.allowed_entra_group ? [access.allowed_entra_group] : []);
|
|
2580
|
+
const valid = access.audience === "private" ? access.private_owner !== undefined && access.allowed_entra_groups !== undefined && groups.length === 0 && !access.identity_types?.includes("dsr") : access.private_owner === undefined && groups.length > 0;
|
|
2581
|
+
if (!valid)
|
|
2582
|
+
ctx.addIssue({ code: "custom", path: ["audience"], message: "Private access requires a verified private_owner, no groups, and employee identity; group access requires at least one group and no private_owner." });
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// ../launchpad-engine/dist/schema.js
|
|
2571
2586
|
var APP_TYPES2 = ["static", "react", "react+api", "container"];
|
|
2572
2587
|
var AUTH_MODES = ["access", "gateway"];
|
|
2573
2588
|
var IDENTITY_TYPES = ["employee", "dsr"];
|
|
2574
|
-
var IdentityTypesSchema =
|
|
2589
|
+
var IdentityTypesSchema = z2.array(z2.enum(IDENTITY_TYPES)).min(1).refine((values) => new Set(values).size === values.length, {
|
|
2575
2590
|
message: "identity_types must not contain duplicates"
|
|
2576
2591
|
}).refine((values) => values.includes("employee"), {
|
|
2577
2592
|
message: "identity_types must include employee"
|
|
@@ -2583,111 +2598,115 @@ var PRIMARY_TARGET = "primary";
|
|
|
2583
2598
|
var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
2584
2599
|
var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
2585
2600
|
var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
|
|
2586
|
-
var MetadataSchema =
|
|
2587
|
-
name:
|
|
2588
|
-
team:
|
|
2589
|
-
owner:
|
|
2590
|
-
description:
|
|
2601
|
+
var MetadataSchema = z2.object({
|
|
2602
|
+
name: z2.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
|
|
2603
|
+
team: z2.string().min(1),
|
|
2604
|
+
owner: z2.string().email(),
|
|
2605
|
+
description: z2.string().max(280).optional()
|
|
2591
2606
|
}).strict();
|
|
2592
|
-
var DeploymentSchema =
|
|
2593
|
-
type:
|
|
2594
|
-
runtime:
|
|
2607
|
+
var DeploymentSchema = z2.object({
|
|
2608
|
+
type: z2.enum(APP_TYPES2),
|
|
2609
|
+
runtime: z2.enum(RUNTIMES).optional()
|
|
2595
2610
|
}).strict();
|
|
2596
|
-
var AccessSchema =
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2611
|
+
var AccessSchema = z2.object({
|
|
2612
|
+
audience: z2.literal("private").optional(),
|
|
2613
|
+
private_owner: PrivateOwnerSchema.optional(),
|
|
2614
|
+
allowed_entra_group: z2.string().min(1).optional(),
|
|
2615
|
+
allowed_entra_groups: z2.array(z2.string().min(1)).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
|
|
2616
|
+
session_duration: z2.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional(),
|
|
2600
2617
|
identity_types: IdentityTypesSchema.optional()
|
|
2601
2618
|
}).strict().superRefine((access, ctx) => {
|
|
2602
2619
|
const hasSingular = access.allowed_entra_group !== undefined;
|
|
2603
2620
|
const hasPlural = access.allowed_entra_groups !== undefined;
|
|
2604
|
-
if (!hasSingular && !hasPlural) {
|
|
2621
|
+
if (!hasSingular && !hasPlural && access.audience !== "private") {
|
|
2605
2622
|
ctx.addIssue({
|
|
2606
|
-
code:
|
|
2623
|
+
code: z2.ZodIssueCode.custom,
|
|
2607
2624
|
message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
|
|
2608
2625
|
path: ["allowed_entra_groups"]
|
|
2609
2626
|
});
|
|
2610
2627
|
}
|
|
2611
2628
|
if (hasSingular && hasPlural) {
|
|
2612
2629
|
ctx.addIssue({
|
|
2613
|
-
code:
|
|
2630
|
+
code: z2.ZodIssueCode.custom,
|
|
2614
2631
|
message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
|
|
2615
2632
|
path: ["allowed_entra_groups"]
|
|
2616
2633
|
});
|
|
2617
2634
|
}
|
|
2618
|
-
});
|
|
2635
|
+
}).superRefine(validateAudience);
|
|
2619
2636
|
function allowedEntraGroups(access) {
|
|
2637
|
+
if (access.audience === "private")
|
|
2638
|
+
return [];
|
|
2620
2639
|
if (access.allowed_entra_groups !== undefined) {
|
|
2621
2640
|
return access.allowed_entra_groups;
|
|
2622
2641
|
}
|
|
2623
2642
|
return [access.allowed_entra_group];
|
|
2624
2643
|
}
|
|
2625
|
-
var ContainerSchema =
|
|
2626
|
-
name:
|
|
2627
|
-
image:
|
|
2628
|
-
port:
|
|
2629
|
-
ingress:
|
|
2630
|
-
healthcheck:
|
|
2631
|
-
env:
|
|
2632
|
-
max_instances:
|
|
2644
|
+
var ContainerSchema = z2.object({
|
|
2645
|
+
name: z2.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
|
|
2646
|
+
image: z2.string().min(1),
|
|
2647
|
+
port: z2.number().int().min(1).max(65535),
|
|
2648
|
+
ingress: z2.boolean().optional(),
|
|
2649
|
+
healthcheck: z2.string().default("/health"),
|
|
2650
|
+
env: z2.array(z2.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
|
|
2651
|
+
max_instances: z2.number().int().min(1).max(100).optional()
|
|
2633
2652
|
}).strict();
|
|
2634
|
-
var TargetSchema =
|
|
2635
|
-
kind:
|
|
2636
|
-
script:
|
|
2637
|
-
schedule:
|
|
2638
|
-
d1_binding:
|
|
2653
|
+
var TargetSchema = z2.object({
|
|
2654
|
+
kind: z2.enum(TARGET_KINDS),
|
|
2655
|
+
script: z2.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
|
|
2656
|
+
schedule: z2.array(z2.string().min(1).max(120)).min(1).max(20).optional(),
|
|
2657
|
+
d1_binding: z2.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
|
|
2639
2658
|
}).strict();
|
|
2640
|
-
var SecretBindingSchema =
|
|
2641
|
-
name:
|
|
2642
|
-
source:
|
|
2643
|
-
description:
|
|
2644
|
-
targets:
|
|
2659
|
+
var SecretBindingSchema = z2.object({
|
|
2660
|
+
name: z2.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
|
|
2661
|
+
source: z2.enum(SECRET_SOURCES),
|
|
2662
|
+
description: z2.string().max(200).optional(),
|
|
2663
|
+
targets: z2.union([z2.literal("all"), z2.array(z2.string().min(1)).min(1)]).optional()
|
|
2645
2664
|
}).strict();
|
|
2646
|
-
var SecretsSchema =
|
|
2647
|
-
bindings:
|
|
2665
|
+
var SecretsSchema = z2.object({
|
|
2666
|
+
bindings: z2.array(SecretBindingSchema).optional()
|
|
2648
2667
|
}).strict();
|
|
2649
|
-
var BuildSchema =
|
|
2650
|
-
command:
|
|
2651
|
-
destination_dir:
|
|
2652
|
-
root_dir:
|
|
2668
|
+
var BuildSchema = z2.object({
|
|
2669
|
+
command: z2.string().min(1).optional(),
|
|
2670
|
+
destination_dir: z2.string().min(1),
|
|
2671
|
+
root_dir: z2.string()
|
|
2653
2672
|
}).strict();
|
|
2654
2673
|
var STATIC_SERVE_DIR = "static";
|
|
2655
2674
|
function staticNoBuild() {
|
|
2656
2675
|
return { destination_dir: STATIC_SERVE_DIR, root_dir: "" };
|
|
2657
2676
|
}
|
|
2658
|
-
var AppBoundarySchema =
|
|
2659
|
-
root:
|
|
2660
|
-
include:
|
|
2661
|
-
exclude:
|
|
2677
|
+
var AppBoundarySchema = z2.object({
|
|
2678
|
+
root: z2.string().min(1).optional(),
|
|
2679
|
+
include: z2.array(z2.string().min(1)).min(1).optional(),
|
|
2680
|
+
exclude: z2.array(z2.string().min(1)).optional()
|
|
2662
2681
|
}).strict();
|
|
2663
|
-
var ProductionEnvSchema =
|
|
2664
|
-
var VerifySchema =
|
|
2665
|
-
protected_path:
|
|
2682
|
+
var ProductionEnvSchema = z2.record(z2.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z2.string());
|
|
2683
|
+
var VerifySchema = z2.object({
|
|
2684
|
+
protected_path: z2.string().min(1).regex(/^\//, "verify.protected_path must be a root-relative path beginning with '/'")
|
|
2666
2685
|
}).strict();
|
|
2667
|
-
var ManifestSchema =
|
|
2668
|
-
apiVersion:
|
|
2669
|
-
kind:
|
|
2686
|
+
var ManifestSchema = z2.object({
|
|
2687
|
+
apiVersion: z2.literal("launchpad.m-kopa.us/v1alpha1"),
|
|
2688
|
+
kind: z2.literal("App"),
|
|
2670
2689
|
metadata: MetadataSchema,
|
|
2671
2690
|
deployment: DeploymentSchema,
|
|
2672
2691
|
access: AccessSchema,
|
|
2673
|
-
auth:
|
|
2674
|
-
hostnames:
|
|
2675
|
-
containers:
|
|
2692
|
+
auth: z2.enum(AUTH_MODES).optional(),
|
|
2693
|
+
hostnames: z2.array(z2.string().min(1)).optional(),
|
|
2694
|
+
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
2676
2695
|
secrets: SecretsSchema.optional(),
|
|
2677
|
-
targets:
|
|
2696
|
+
targets: z2.array(TargetSchema).optional(),
|
|
2678
2697
|
build: BuildSchema.optional(),
|
|
2679
2698
|
production_env: ProductionEnvSchema.optional(),
|
|
2680
2699
|
app: AppBoundarySchema.optional(),
|
|
2681
2700
|
verify: VerifySchema.optional(),
|
|
2682
|
-
confine_origin:
|
|
2683
|
-
origin:
|
|
2684
|
-
catalogue:
|
|
2685
|
-
sensitive:
|
|
2701
|
+
confine_origin: z2.boolean().optional(),
|
|
2702
|
+
origin: z2.enum(["pages", "dispatch"]).optional(),
|
|
2703
|
+
catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
|
|
2704
|
+
sensitive: z2.boolean().optional()
|
|
2686
2705
|
}).strict().superRefine((m, ctx) => {
|
|
2687
2706
|
const isContainer = m.deployment.type === "container";
|
|
2688
2707
|
if (m.access.identity_types !== undefined && (m.auth ?? "access") !== "gateway") {
|
|
2689
2708
|
ctx.addIssue({
|
|
2690
|
-
code:
|
|
2709
|
+
code: z2.ZodIssueCode.custom,
|
|
2691
2710
|
path: ["access", "identity_types"],
|
|
2692
2711
|
message: "access.identity_types is only valid with auth: gateway"
|
|
2693
2712
|
});
|
|
@@ -2695,19 +2714,19 @@ var ManifestSchema = z.object({
|
|
|
2695
2714
|
if (m.confine_origin !== undefined) {
|
|
2696
2715
|
if (isContainer) {
|
|
2697
2716
|
ctx.addIssue({
|
|
2698
|
-
code:
|
|
2717
|
+
code: z2.ZodIssueCode.custom,
|
|
2699
2718
|
path: ["confine_origin"],
|
|
2700
2719
|
message: "confine_origin is only valid for gateway pages apps (static/react/react+api); container apps have no pages.dev surface."
|
|
2701
2720
|
});
|
|
2702
2721
|
} else if (m.auth === "access") {
|
|
2703
2722
|
ctx.addIssue({
|
|
2704
|
-
code:
|
|
2723
|
+
code: z2.ZodIssueCode.custom,
|
|
2705
2724
|
path: ["confine_origin"],
|
|
2706
2725
|
message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
|
|
2707
2726
|
});
|
|
2708
2727
|
} else if (m.deployment.type === "static" && m.confine_origin === false) {
|
|
2709
2728
|
ctx.addIssue({
|
|
2710
|
-
code:
|
|
2729
|
+
code: z2.ZodIssueCode.custom,
|
|
2711
2730
|
path: ["confine_origin"],
|
|
2712
2731
|
message: "confine_origin: false is not allowed for static apps — they have no verifier, so an unconfined apex would be world-readable (ADR 0031)."
|
|
2713
2732
|
});
|
|
@@ -2715,42 +2734,42 @@ var ManifestSchema = z.object({
|
|
|
2715
2734
|
}
|
|
2716
2735
|
if (isContainer && m.auth === "gateway") {
|
|
2717
2736
|
ctx.addIssue({
|
|
2718
|
-
code:
|
|
2737
|
+
code: z2.ZodIssueCode.custom,
|
|
2719
2738
|
path: ["auth"],
|
|
2720
2739
|
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
2721
2740
|
});
|
|
2722
2741
|
}
|
|
2723
2742
|
if (isContainer && m.containers === undefined) {
|
|
2724
2743
|
ctx.addIssue({
|
|
2725
|
-
code:
|
|
2744
|
+
code: z2.ZodIssueCode.custom,
|
|
2726
2745
|
path: ["containers"],
|
|
2727
2746
|
message: "containers[] is required when deployment.type is 'container'"
|
|
2728
2747
|
});
|
|
2729
2748
|
}
|
|
2730
2749
|
if (!isContainer && m.containers !== undefined) {
|
|
2731
2750
|
ctx.addIssue({
|
|
2732
|
-
code:
|
|
2751
|
+
code: z2.ZodIssueCode.custom,
|
|
2733
2752
|
path: ["containers"],
|
|
2734
2753
|
message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
|
|
2735
2754
|
});
|
|
2736
2755
|
}
|
|
2737
2756
|
if (!isContainer && m.deployment.runtime !== undefined) {
|
|
2738
2757
|
ctx.addIssue({
|
|
2739
|
-
code:
|
|
2758
|
+
code: z2.ZodIssueCode.custom,
|
|
2740
2759
|
path: ["deployment", "runtime"],
|
|
2741
2760
|
message: "deployment.runtime is only meaningful when deployment.type is 'container'"
|
|
2742
2761
|
});
|
|
2743
2762
|
}
|
|
2744
2763
|
if (isContainer && m.build !== undefined) {
|
|
2745
2764
|
ctx.addIssue({
|
|
2746
|
-
code:
|
|
2765
|
+
code: z2.ZodIssueCode.custom,
|
|
2747
2766
|
path: ["build"],
|
|
2748
2767
|
message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
|
|
2749
2768
|
});
|
|
2750
2769
|
}
|
|
2751
2770
|
if (isContainer && m.production_env !== undefined) {
|
|
2752
2771
|
ctx.addIssue({
|
|
2753
|
-
code:
|
|
2772
|
+
code: z2.ZodIssueCode.custom,
|
|
2754
2773
|
path: ["production_env"],
|
|
2755
2774
|
message: "production_env is only allowed for pages-app deployments. Container env vars use containers[].env (names only) + the bot's secrets pipeline for values."
|
|
2756
2775
|
});
|
|
@@ -2759,7 +2778,7 @@ var ManifestSchema = z.object({
|
|
|
2759
2778
|
const ingressContainers = m.containers.filter((c) => c.ingress === true);
|
|
2760
2779
|
if (ingressContainers.length !== 1) {
|
|
2761
2780
|
ctx.addIssue({
|
|
2762
|
-
code:
|
|
2781
|
+
code: z2.ZodIssueCode.custom,
|
|
2763
2782
|
path: ["containers"],
|
|
2764
2783
|
message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
|
|
2765
2784
|
});
|
|
@@ -2772,7 +2791,7 @@ var ManifestSchema = z.object({
|
|
|
2772
2791
|
for (const envName of c.env ?? []) {
|
|
2773
2792
|
if (!declaredBindings.has(envName)) {
|
|
2774
2793
|
ctx.addIssue({
|
|
2775
|
-
code:
|
|
2794
|
+
code: z2.ZodIssueCode.custom,
|
|
2776
2795
|
path: ["containers", i, "env"],
|
|
2777
2796
|
message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
|
|
2778
2797
|
});
|
|
@@ -2788,7 +2807,7 @@ var ManifestSchema = z.object({
|
|
|
2788
2807
|
continue;
|
|
2789
2808
|
if (seen.has(name)) {
|
|
2790
2809
|
ctx.addIssue({
|
|
2791
|
-
code:
|
|
2810
|
+
code: z2.ZodIssueCode.custom,
|
|
2792
2811
|
path: ["containers", i, "name"],
|
|
2793
2812
|
message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
|
|
2794
2813
|
});
|
|
@@ -2805,7 +2824,7 @@ var ManifestSchema = z.object({
|
|
|
2805
2824
|
continue;
|
|
2806
2825
|
if (seen.has(name)) {
|
|
2807
2826
|
ctx.addIssue({
|
|
2808
|
-
code:
|
|
2827
|
+
code: z2.ZodIssueCode.custom,
|
|
2809
2828
|
path: ["secrets", "bindings", i, "name"],
|
|
2810
2829
|
message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
|
|
2811
2830
|
});
|
|
@@ -2818,21 +2837,21 @@ var ManifestSchema = z.object({
|
|
|
2818
2837
|
const t = targets[i];
|
|
2819
2838
|
if (t.kind === "worker" && t.script === undefined) {
|
|
2820
2839
|
ctx.addIssue({
|
|
2821
|
-
code:
|
|
2840
|
+
code: z2.ZodIssueCode.custom,
|
|
2822
2841
|
path: ["targets", i, "script"],
|
|
2823
2842
|
message: "a worker target must declare a script (the sibling Workers script name)"
|
|
2824
2843
|
});
|
|
2825
2844
|
}
|
|
2826
2845
|
if (t.kind === "pages" && t.script !== undefined) {
|
|
2827
2846
|
ctx.addIssue({
|
|
2828
|
-
code:
|
|
2847
|
+
code: z2.ZodIssueCode.custom,
|
|
2829
2848
|
path: ["targets", i, "script"],
|
|
2830
2849
|
message: "a pages target must not declare a script — the primary Pages project is the app slug"
|
|
2831
2850
|
});
|
|
2832
2851
|
}
|
|
2833
2852
|
if (t.kind === "pages" && t.schedule !== undefined) {
|
|
2834
2853
|
ctx.addIssue({
|
|
2835
|
-
code:
|
|
2854
|
+
code: z2.ZodIssueCode.custom,
|
|
2836
2855
|
path: ["targets", i, "schedule"],
|
|
2837
2856
|
message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
|
|
2838
2857
|
});
|
|
@@ -2846,7 +2865,7 @@ var ManifestSchema = z.object({
|
|
|
2846
2865
|
pagesTargets++;
|
|
2847
2866
|
if (pagesTargets > 1) {
|
|
2848
2867
|
ctx.addIssue({
|
|
2849
|
-
code:
|
|
2868
|
+
code: z2.ZodIssueCode.custom,
|
|
2850
2869
|
path: ["targets", i, "kind"],
|
|
2851
2870
|
message: "at most one pages target may be declared (the primary Pages project is singular)"
|
|
2852
2871
|
});
|
|
@@ -2857,7 +2876,7 @@ var ManifestSchema = z.object({
|
|
|
2857
2876
|
continue;
|
|
2858
2877
|
if (t.script === PRIMARY_TARGET || t.script === "pages") {
|
|
2859
2878
|
ctx.addIssue({
|
|
2860
|
-
code:
|
|
2879
|
+
code: z2.ZodIssueCode.custom,
|
|
2861
2880
|
path: ["targets", i, "script"],
|
|
2862
2881
|
message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
|
|
2863
2882
|
});
|
|
@@ -2865,7 +2884,7 @@ var ManifestSchema = z.object({
|
|
|
2865
2884
|
}
|
|
2866
2885
|
if (seenScripts.has(t.script)) {
|
|
2867
2886
|
ctx.addIssue({
|
|
2868
|
-
code:
|
|
2887
|
+
code: z2.ZodIssueCode.custom,
|
|
2869
2888
|
path: ["targets", i, "script"],
|
|
2870
2889
|
message: `duplicate worker target script '${t.script}' — target scripts must be unique`
|
|
2871
2890
|
});
|
|
@@ -2883,7 +2902,7 @@ var ManifestSchema = z.object({
|
|
|
2883
2902
|
for (const ref of sel) {
|
|
2884
2903
|
if (!validRefs.has(ref)) {
|
|
2885
2904
|
ctx.addIssue({
|
|
2886
|
-
code:
|
|
2905
|
+
code: z2.ZodIssueCode.custom,
|
|
2887
2906
|
path: ["secrets", "bindings", bi, "targets"],
|
|
2888
2907
|
message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
|
|
2889
2908
|
});
|
|
@@ -3299,42 +3318,44 @@ var STATUS_STATUS_KEYS = new Set([
|
|
|
3299
3318
|
"status.generated_at"
|
|
3300
3319
|
]);
|
|
3301
3320
|
// ../launchpad-engine/dist/schema-v2.js
|
|
3302
|
-
import { z as
|
|
3303
|
-
var EnvVarSchema =
|
|
3304
|
-
|
|
3305
|
-
|
|
3321
|
+
import { z as z3 } from "zod";
|
|
3322
|
+
var EnvVarSchema = z3.discriminatedUnion("type", [
|
|
3323
|
+
z3.object({ type: z3.literal("secret_text"), secret_ref: z3.string().min(1) }).strict(),
|
|
3324
|
+
z3.object({ type: z3.literal("plain_text"), value: z3.string() }).strict()
|
|
3306
3325
|
]);
|
|
3307
|
-
var SpecSchema =
|
|
3308
|
-
appType:
|
|
3309
|
-
display:
|
|
3310
|
-
name:
|
|
3311
|
-
description:
|
|
3312
|
-
team:
|
|
3326
|
+
var SpecSchema = z3.object({
|
|
3327
|
+
appType: z3.enum(APP_TYPES2),
|
|
3328
|
+
display: z3.object({
|
|
3329
|
+
name: z3.string().min(1),
|
|
3330
|
+
description: z3.string().max(280).optional(),
|
|
3331
|
+
team: z3.string().min(1)
|
|
3313
3332
|
}).strict(),
|
|
3314
|
-
owner:
|
|
3315
|
-
access:
|
|
3316
|
-
|
|
3333
|
+
owner: z3.object({ email: z3.string().email() }).strict(),
|
|
3334
|
+
access: z3.object({
|
|
3335
|
+
audience: z3.literal("private").optional(),
|
|
3336
|
+
private_owner: PrivateOwnerSchema.optional(),
|
|
3337
|
+
allowed_entra_groups: z3.array(z3.string().min(1)).refine((a) => new Set(a).size === a.length, {
|
|
3317
3338
|
message: "allowed_entra_groups must not contain duplicates"
|
|
3318
3339
|
}),
|
|
3319
|
-
require_email_domain:
|
|
3340
|
+
require_email_domain: z3.string().min(1).optional(),
|
|
3320
3341
|
identity_types: IdentityTypesSchema.optional()
|
|
3321
|
-
}).strict(),
|
|
3322
|
-
auth:
|
|
3342
|
+
}).strict().superRefine(validateAudience),
|
|
3343
|
+
auth: z3.enum(AUTH_MODES).optional(),
|
|
3323
3344
|
build: BuildSchema.optional(),
|
|
3324
3345
|
app: AppBoundarySchema.optional(),
|
|
3325
3346
|
verify: VerifySchema.optional(),
|
|
3326
|
-
confine_origin:
|
|
3327
|
-
origin:
|
|
3328
|
-
sensitive:
|
|
3329
|
-
catalogue:
|
|
3330
|
-
env_vars:
|
|
3331
|
-
containers:
|
|
3347
|
+
confine_origin: z3.boolean().optional(),
|
|
3348
|
+
origin: z3.enum(["pages", "dispatch"]).optional(),
|
|
3349
|
+
sensitive: z3.boolean().optional(),
|
|
3350
|
+
catalogue: z3.object({ listed: z3.boolean().default(true) }).strict().optional(),
|
|
3351
|
+
env_vars: z3.record(z3.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
3352
|
+
containers: z3.array(ContainerSchema).min(1).optional(),
|
|
3332
3353
|
secrets: SecretsSchema.optional(),
|
|
3333
|
-
fronted_by_concierge:
|
|
3354
|
+
fronted_by_concierge: z3.boolean().optional()
|
|
3334
3355
|
}).strict().superRefine((s, ctx) => {
|
|
3335
3356
|
if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
|
|
3336
3357
|
ctx.addIssue({
|
|
3337
|
-
code:
|
|
3358
|
+
code: z3.ZodIssueCode.custom,
|
|
3338
3359
|
path: ["access", "identity_types"],
|
|
3339
3360
|
message: "access.identity_types is only valid with auth: gateway"
|
|
3340
3361
|
});
|
|
@@ -3342,7 +3363,7 @@ var SpecSchema = z2.object({
|
|
|
3342
3363
|
for (const [k, v] of Object.entries(s.env_vars ?? {})) {
|
|
3343
3364
|
if (/_(TOKEN|SECRET)$/.test(k) && v.type !== "secret_text") {
|
|
3344
3365
|
ctx.addIssue({
|
|
3345
|
-
code:
|
|
3366
|
+
code: z3.ZodIssueCode.custom,
|
|
3346
3367
|
path: ["env_vars", k],
|
|
3347
3368
|
message: `env var '${k}' is secret-bearing (*_TOKEN/*_SECRET) and must be type secret_text, not '${v.type}'`
|
|
3348
3369
|
});
|
|
@@ -3351,7 +3372,7 @@ var SpecSchema = z2.object({
|
|
|
3351
3372
|
const isContainer = s.appType === "container";
|
|
3352
3373
|
if (isContainer && s.containers === undefined) {
|
|
3353
3374
|
ctx.addIssue({
|
|
3354
|
-
code:
|
|
3375
|
+
code: z3.ZodIssueCode.custom,
|
|
3355
3376
|
path: ["containers"],
|
|
3356
3377
|
message: "containers[] is required when appType is 'container'"
|
|
3357
3378
|
});
|
|
@@ -3359,19 +3380,19 @@ var SpecSchema = z2.object({
|
|
|
3359
3380
|
if (s.confine_origin !== undefined) {
|
|
3360
3381
|
if (isContainer) {
|
|
3361
3382
|
ctx.addIssue({
|
|
3362
|
-
code:
|
|
3383
|
+
code: z3.ZodIssueCode.custom,
|
|
3363
3384
|
path: ["confine_origin"],
|
|
3364
3385
|
message: "confine_origin is only valid for gateway pages apps; container apps have no pages.dev surface."
|
|
3365
3386
|
});
|
|
3366
3387
|
} else if (s.auth === "access") {
|
|
3367
3388
|
ctx.addIssue({
|
|
3368
|
-
code:
|
|
3389
|
+
code: z3.ZodIssueCode.custom,
|
|
3369
3390
|
path: ["confine_origin"],
|
|
3370
3391
|
message: "confine_origin applies to gateway-fronted apps (auth: gateway); it has no meaning under Cloudflare Access."
|
|
3371
3392
|
});
|
|
3372
3393
|
} else if (s.appType === "static" && s.confine_origin === false) {
|
|
3373
3394
|
ctx.addIssue({
|
|
3374
|
-
code:
|
|
3395
|
+
code: z3.ZodIssueCode.custom,
|
|
3375
3396
|
path: ["confine_origin"],
|
|
3376
3397
|
message: "confine_origin: false is not allowed for static apps — an unconfined apex with no verifier is world-readable (ADR 0031)."
|
|
3377
3398
|
});
|
|
@@ -3380,129 +3401,124 @@ var SpecSchema = z2.object({
|
|
|
3380
3401
|
if (s.origin === "dispatch") {
|
|
3381
3402
|
if (isContainer) {
|
|
3382
3403
|
ctx.addIssue({
|
|
3383
|
-
code:
|
|
3404
|
+
code: z3.ZodIssueCode.custom,
|
|
3384
3405
|
path: ["origin"],
|
|
3385
3406
|
message: "origin: dispatch is not available for container apps — they run on the container runtime (ADR 0015/0016), not in a Workers dispatch namespace."
|
|
3386
3407
|
});
|
|
3387
3408
|
}
|
|
3388
3409
|
if ((s.auth ?? "access") !== "gateway") {
|
|
3389
3410
|
ctx.addIssue({
|
|
3390
|
-
code:
|
|
3411
|
+
code: z3.ZodIssueCode.custom,
|
|
3391
3412
|
path: ["origin"],
|
|
3392
3413
|
message: "origin: dispatch requires an explicit auth: gateway (auth defaults to 'access' when omitted). A dispatch-namespace Worker has no public hostname for Cloudflare Access to gate, so the gateway must be the sole gate."
|
|
3393
3414
|
});
|
|
3394
3415
|
}
|
|
3395
3416
|
if (s.confine_origin !== undefined) {
|
|
3396
3417
|
ctx.addIssue({
|
|
3397
|
-
code:
|
|
3418
|
+
code: z3.ZodIssueCode.custom,
|
|
3398
3419
|
path: ["confine_origin"],
|
|
3399
3420
|
message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
|
|
3400
3421
|
});
|
|
3401
3422
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
ctx.addIssue({
|
|
3411
|
-
code: z2.ZodIssueCode.custom,
|
|
3412
|
-
path: ["build"],
|
|
3413
|
-
message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
|
|
3414
|
-
});
|
|
3423
|
+
for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
|
|
3424
|
+
if (envVar.type === "secret_text") {
|
|
3425
|
+
ctx.addIssue({
|
|
3426
|
+
code: z3.ZodIssueCode.custom,
|
|
3427
|
+
path: ["env_vars", name],
|
|
3428
|
+
message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3415
3431
|
}
|
|
3416
3432
|
}
|
|
3417
3433
|
if (!isContainer && s.containers !== undefined) {
|
|
3418
3434
|
ctx.addIssue({
|
|
3419
|
-
code:
|
|
3435
|
+
code: z3.ZodIssueCode.custom,
|
|
3420
3436
|
path: ["containers"],
|
|
3421
3437
|
message: `containers[] is only allowed when appType is 'container' (current appType: '${s.appType}')`
|
|
3422
3438
|
});
|
|
3423
3439
|
}
|
|
3424
3440
|
if (isContainer && s.build !== undefined) {
|
|
3425
3441
|
ctx.addIssue({
|
|
3426
|
-
code:
|
|
3442
|
+
code: z3.ZodIssueCode.custom,
|
|
3427
3443
|
path: ["build"],
|
|
3428
3444
|
message: "build is not allowed when appType is 'container' (container build lives in the Dockerfile)"
|
|
3429
3445
|
});
|
|
3430
3446
|
}
|
|
3431
3447
|
if (isContainer && s.auth === "gateway") {
|
|
3432
3448
|
ctx.addIssue({
|
|
3433
|
-
code:
|
|
3449
|
+
code: z3.ZodIssueCode.custom,
|
|
3434
3450
|
path: ["auth"],
|
|
3435
3451
|
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
3436
3452
|
});
|
|
3437
3453
|
}
|
|
3438
3454
|
if (!isContainer && s.fronted_by_concierge === true) {
|
|
3439
3455
|
ctx.addIssue({
|
|
3440
|
-
code:
|
|
3456
|
+
code: z3.ZodIssueCode.custom,
|
|
3441
3457
|
path: ["fronted_by_concierge"],
|
|
3442
3458
|
message: `fronted_by_concierge is only allowed when appType is 'container' (current appType: '${s.appType}')`
|
|
3443
3459
|
});
|
|
3444
3460
|
}
|
|
3445
3461
|
});
|
|
3446
|
-
var nonEmpty =
|
|
3447
|
-
var StatusSchema =
|
|
3448
|
-
schemaVersion:
|
|
3462
|
+
var nonEmpty = z3.string().min(1);
|
|
3463
|
+
var StatusSchema = z3.object({
|
|
3464
|
+
schemaVersion: z3.literal(2),
|
|
3449
3465
|
manifest_spec_sha: nonEmpty,
|
|
3450
|
-
cloudflare:
|
|
3451
|
-
access:
|
|
3466
|
+
cloudflare: z3.object({
|
|
3467
|
+
access: z3.object({
|
|
3452
3468
|
app_id: nonEmpty,
|
|
3453
3469
|
app_aud: nonEmpty,
|
|
3454
3470
|
team_domain: nonEmpty,
|
|
3455
3471
|
policy_id: nonEmpty
|
|
3456
3472
|
}).strict(),
|
|
3457
|
-
pages:
|
|
3473
|
+
pages: z3.object({
|
|
3458
3474
|
project_name: nonEmpty,
|
|
3459
3475
|
subdomain: nonEmpty,
|
|
3460
3476
|
latest_deployment_id: nonEmpty
|
|
3461
3477
|
}).strict().optional(),
|
|
3462
|
-
worker:
|
|
3478
|
+
worker: z3.object({
|
|
3463
3479
|
dev_url: nonEmpty,
|
|
3464
3480
|
script_name: nonEmpty
|
|
3465
3481
|
}).strict().optional()
|
|
3466
3482
|
}).strict(),
|
|
3467
|
-
dns:
|
|
3468
|
-
terraform:
|
|
3483
|
+
dns: z3.object({ custom_hostname: nonEmpty, cname_target: nonEmpty }).strict().optional(),
|
|
3484
|
+
terraform: z3.object({
|
|
3469
3485
|
workspace_state_key: nonEmpty,
|
|
3470
3486
|
last_applied_at: nonEmpty,
|
|
3471
3487
|
last_applied_sha: nonEmpty
|
|
3472
3488
|
}).strict(),
|
|
3473
|
-
registry:
|
|
3489
|
+
registry: z3.object({ record_present: z3.boolean(), lifecycle: nonEmpty }).strict(),
|
|
3474
3490
|
generated_at: nonEmpty
|
|
3475
3491
|
}).strict().superRefine((s, ctx) => {
|
|
3476
3492
|
const hasPages = s.cloudflare.pages !== undefined;
|
|
3477
3493
|
const hasWorker = s.cloudflare.worker !== undefined;
|
|
3478
3494
|
if (hasPages === hasWorker) {
|
|
3479
3495
|
ctx.addIssue({
|
|
3480
|
-
code:
|
|
3496
|
+
code: z3.ZodIssueCode.custom,
|
|
3481
3497
|
path: ["cloudflare"],
|
|
3482
3498
|
message: "status.cloudflare must carry exactly one of `pages` (pages apps) or `worker` (container apps)"
|
|
3483
3499
|
});
|
|
3484
3500
|
}
|
|
3485
3501
|
if (hasPages && s.dns === undefined) {
|
|
3486
3502
|
ctx.addIssue({
|
|
3487
|
-
code:
|
|
3503
|
+
code: z3.ZodIssueCode.custom,
|
|
3488
3504
|
path: ["dns"],
|
|
3489
3505
|
message: "status.dns is required for a pages-shape status (cloudflare.pages present)"
|
|
3490
3506
|
});
|
|
3491
3507
|
}
|
|
3492
3508
|
if (hasWorker && s.dns !== undefined) {
|
|
3493
3509
|
ctx.addIssue({
|
|
3494
|
-
code:
|
|
3510
|
+
code: z3.ZodIssueCode.custom,
|
|
3495
3511
|
path: ["dns"],
|
|
3496
3512
|
message: "status.dns must be absent for a workers_dev_only container status (no custom hostname)"
|
|
3497
3513
|
});
|
|
3498
3514
|
}
|
|
3499
3515
|
});
|
|
3500
|
-
var ManifestV2Schema =
|
|
3501
|
-
apiVersion:
|
|
3502
|
-
kind:
|
|
3503
|
-
metadata:
|
|
3504
|
-
slug:
|
|
3505
|
-
schemaVersion:
|
|
3516
|
+
var ManifestV2Schema = z3.object({
|
|
3517
|
+
apiVersion: z3.literal("launchpad.m-kopa.us/v2"),
|
|
3518
|
+
kind: z3.literal("App"),
|
|
3519
|
+
metadata: z3.object({
|
|
3520
|
+
slug: z3.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
|
|
3521
|
+
schemaVersion: z3.literal(2)
|
|
3506
3522
|
}).strict(),
|
|
3507
3523
|
spec: SpecSchema,
|
|
3508
3524
|
status: StatusSchema.optional()
|
|
@@ -4232,6 +4248,11 @@ var BROAD_GROUPS = [
|
|
|
4232
4248
|
uuid: "caf78902-35dd-4833-88ce-8f067eda6036",
|
|
4233
4249
|
name: "S_ALL_Employees",
|
|
4234
4250
|
note: "All employees — the broadest grant on the tenant; assigned to the Launchpad enterprise app. Granting this reaches the entire company."
|
|
4251
|
+
},
|
|
4252
|
+
{
|
|
4253
|
+
uuid: "09c87073-2071-4f8e-9e97-aa67f7a153a7",
|
|
4254
|
+
name: "S_ALL",
|
|
4255
|
+
note: "Broad tenant group. Assignment is not a reliable limit on gateway eligibility."
|
|
4235
4256
|
}
|
|
4236
4257
|
];
|
|
4237
4258
|
function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
|
|
@@ -4250,7 +4271,7 @@ function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
|
|
|
4250
4271
|
assertBroadGroupsWellFormed();
|
|
4251
4272
|
var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
|
|
4252
4273
|
// ../launchpad-engine/dist/fleet-manifest.js
|
|
4253
|
-
import { z as
|
|
4274
|
+
import { z as z4 } from "zod";
|
|
4254
4275
|
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
4255
4276
|
var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
|
|
4256
4277
|
var APP_SHAPES = [
|
|
@@ -4278,22 +4299,22 @@ var FLEET_TIERS = [
|
|
|
4278
4299
|
"out-of-scope"
|
|
4279
4300
|
];
|
|
4280
4301
|
var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
|
|
4281
|
-
var GroupId =
|
|
4282
|
-
var FleetAppSchema =
|
|
4283
|
-
slug:
|
|
4284
|
-
residency:
|
|
4285
|
-
shape:
|
|
4286
|
-
declaredAuth:
|
|
4287
|
-
actualAuth:
|
|
4288
|
-
groups:
|
|
4289
|
-
tier:
|
|
4290
|
-
devicePosture:
|
|
4291
|
-
devicePostureReason:
|
|
4292
|
-
notes:
|
|
4302
|
+
var GroupId = z4.string().regex(UUID_RE, "Entra group ids are emitted as canonical UUIDs in the JWT groups claim — display names will not match policies.");
|
|
4303
|
+
var FleetAppSchema = z4.object({
|
|
4304
|
+
slug: z4.string().regex(SLUG_REGEX),
|
|
4305
|
+
residency: z4.enum(TF_RESIDENCIES),
|
|
4306
|
+
shape: z4.enum(APP_SHAPES),
|
|
4307
|
+
declaredAuth: z4.enum(DECLARED_AUTH),
|
|
4308
|
+
actualAuth: z4.enum(ACTUAL_AUTH),
|
|
4309
|
+
groups: z4.array(GroupId).default([]),
|
|
4310
|
+
tier: z4.enum(FLEET_TIERS),
|
|
4311
|
+
devicePosture: z4.enum(DEVICE_POSTURE),
|
|
4312
|
+
devicePostureReason: z4.string().trim().min(1, "devicePosture is only valid with a non-empty reason — the two-tier determination must be a surveyed output, not a default (AC-T0-3)."),
|
|
4313
|
+
notes: z4.string().optional()
|
|
4293
4314
|
}).strict();
|
|
4294
|
-
var FleetManifestSchema =
|
|
4295
|
-
schemaVersion:
|
|
4296
|
-
apps:
|
|
4315
|
+
var FleetManifestSchema = z4.object({
|
|
4316
|
+
schemaVersion: z4.literal(1),
|
|
4317
|
+
apps: z4.array(FleetAppSchema).min(1)
|
|
4297
4318
|
}).strict().superRefine((m, ctx) => {
|
|
4298
4319
|
const seen = new Set;
|
|
4299
4320
|
for (const app of m.apps) {
|
|
@@ -4315,23 +4336,23 @@ var FleetManifestSchema = z3.object({
|
|
|
4315
4336
|
}
|
|
4316
4337
|
});
|
|
4317
4338
|
// ../launchpad-engine/dist/fleet-secret-sets.js
|
|
4318
|
-
import { z as
|
|
4339
|
+
import { z as z5 } from "zod";
|
|
4319
4340
|
var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
4320
|
-
var Slug =
|
|
4321
|
-
var SecretName =
|
|
4322
|
-
var SecretSetExcludeSchema =
|
|
4341
|
+
var Slug = z5.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
|
|
4342
|
+
var SecretName = z5.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
|
|
4343
|
+
var SecretSetExcludeSchema = z5.object({
|
|
4323
4344
|
member: Slug,
|
|
4324
4345
|
secret: SecretName
|
|
4325
4346
|
}).strict();
|
|
4326
|
-
var SecretSetSchema =
|
|
4327
|
-
name:
|
|
4328
|
-
secrets:
|
|
4329
|
-
members:
|
|
4330
|
-
exclude:
|
|
4347
|
+
var SecretSetSchema = z5.object({
|
|
4348
|
+
name: z5.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
|
|
4349
|
+
secrets: z5.array(SecretName).min(1),
|
|
4350
|
+
members: z5.array(Slug).min(1),
|
|
4351
|
+
exclude: z5.array(SecretSetExcludeSchema).optional()
|
|
4331
4352
|
}).strict();
|
|
4332
|
-
var FleetSecretSetsSchema =
|
|
4333
|
-
schemaVersion:
|
|
4334
|
-
secretSets:
|
|
4353
|
+
var FleetSecretSetsSchema = z5.object({
|
|
4354
|
+
schemaVersion: z5.literal(1),
|
|
4355
|
+
secretSets: z5.array(SecretSetSchema).min(1)
|
|
4335
4356
|
}).strict().superRefine((m, ctx) => {
|
|
4336
4357
|
const seenSets = new Set;
|
|
4337
4358
|
for (let i = 0;i < m.secretSets.length; i++) {
|
|
@@ -4403,6 +4424,21 @@ function membersForSecret(set, secret) {
|
|
|
4403
4424
|
}
|
|
4404
4425
|
// ../launchpad-engine/dist/redact-status.js
|
|
4405
4426
|
var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
|
|
4427
|
+
// ../launchpad-engine/dist/deployment-auth.js
|
|
4428
|
+
function deploymentAuthIssue(appType, auth) {
|
|
4429
|
+
if (appType === "container" || auth === "gateway")
|
|
4430
|
+
return null;
|
|
4431
|
+
return {
|
|
4432
|
+
error: "pages_access_retired",
|
|
4433
|
+
message: "Cloudflare Access login is retired for Pages apps. Set auth: gateway (spec.auth in v2). Containers are unchanged."
|
|
4434
|
+
};
|
|
4435
|
+
}
|
|
4436
|
+
// ../launchpad-engine/dist/new-app-origin.js
|
|
4437
|
+
function newAppOrigin(appType, requested) {
|
|
4438
|
+
if (appType === "container")
|
|
4439
|
+
return;
|
|
4440
|
+
return requested ?? (appType === "react+api" ? "pages" : "dispatch");
|
|
4441
|
+
}
|
|
4406
4442
|
// src/bundle/boundary.ts
|
|
4407
4443
|
var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
|
|
4408
4444
|
function applyBoundaryToFiles(args) {
|
|
@@ -4550,7 +4586,7 @@ async function bundleAndDeploy(args) {
|
|
|
4550
4586
|
}
|
|
4551
4587
|
|
|
4552
4588
|
// src/deploy/verify-deploy.ts
|
|
4553
|
-
import { readFileSync as
|
|
4589
|
+
import { readFileSync as readFileSync10, existsSync as existsSync4 } from "node:fs";
|
|
4554
4590
|
import { join as join9 } from "node:path";
|
|
4555
4591
|
|
|
4556
4592
|
// src/deploy/redact-diagnostic.ts
|
|
@@ -4596,11 +4632,67 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
|
4596
4632
|
return {
|
|
4597
4633
|
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
4598
4634
|
supported: raw.supported,
|
|
4635
|
+
...raw.origin === "pages" || raw.origin === "dispatch" ? { origin: raw.origin } : {},
|
|
4636
|
+
...projectV2(raw.v2),
|
|
4599
4637
|
liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
|
|
4600
4638
|
lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
|
|
4601
4639
|
...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
|
|
4602
4640
|
};
|
|
4603
4641
|
}
|
|
4642
|
+
function projectV2(value) {
|
|
4643
|
+
if (value === null || typeof value !== "object")
|
|
4644
|
+
return {};
|
|
4645
|
+
const raw = value;
|
|
4646
|
+
if (!isV2State(raw.state) || !isV2Stage(raw.stage) || typeof raw.commitSha !== "string" || typeof raw.attempts !== "number" || !Number.isFinite(raw.attempts) || typeof raw.updatedAt !== "string") {
|
|
4647
|
+
return {};
|
|
4648
|
+
}
|
|
4649
|
+
if (!isConsistentV2State(raw.state, raw.stage))
|
|
4650
|
+
return {};
|
|
4651
|
+
if (raw.failedAt !== undefined && !isV2FailedStage(raw.failedAt))
|
|
4652
|
+
return {};
|
|
4653
|
+
const v2 = {
|
|
4654
|
+
state: raw.state,
|
|
4655
|
+
stage: raw.stage,
|
|
4656
|
+
commitSha: raw.commitSha,
|
|
4657
|
+
attempts: raw.attempts,
|
|
4658
|
+
updatedAt: raw.updatedAt,
|
|
4659
|
+
...raw.failedAt !== undefined ? { failedAt: raw.failedAt } : {},
|
|
4660
|
+
...typeof raw.reason === "string" ? { reason: redactDiagnostic(raw.reason) } : {},
|
|
4661
|
+
...typeof raw.detail === "string" ? { detail: redactDiagnostic(raw.detail) } : {},
|
|
4662
|
+
...projectRunUrl(raw.runUrl),
|
|
4663
|
+
...typeof raw.deliveredAt === "string" ? { deliveredAt: raw.deliveredAt } : {}
|
|
4664
|
+
};
|
|
4665
|
+
return { v2 };
|
|
4666
|
+
}
|
|
4667
|
+
function projectRunUrl(value) {
|
|
4668
|
+
if (typeof value !== "string")
|
|
4669
|
+
return {};
|
|
4670
|
+
try {
|
|
4671
|
+
const url = new URL(value);
|
|
4672
|
+
return url.protocol === "https:" && url.hostname === "github.com" ? { runUrl: value } : {};
|
|
4673
|
+
} catch {
|
|
4674
|
+
return {};
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
function isV2State(value) {
|
|
4678
|
+
return value === "pending" || value === "delivered" || value === "refused";
|
|
4679
|
+
}
|
|
4680
|
+
function isV2Stage(value) {
|
|
4681
|
+
return ["committed", "building", "verified", "uploading", "live", "failed"].includes(String(value));
|
|
4682
|
+
}
|
|
4683
|
+
function isConsistentV2State(state, stage) {
|
|
4684
|
+
switch (state) {
|
|
4685
|
+
case "delivered":
|
|
4686
|
+
return stage === "live";
|
|
4687
|
+
case "refused":
|
|
4688
|
+
return stage === "failed";
|
|
4689
|
+
case "pending":
|
|
4690
|
+
return stage !== "live" && stage !== "failed";
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
function isV2FailedStage(value) {
|
|
4694
|
+
return ["build", "verify", "download", "expand", "manifest", "plan", "upload"].includes(String(value));
|
|
4695
|
+
}
|
|
4604
4696
|
function projectLastSuccessful(value) {
|
|
4605
4697
|
if (value === null || typeof value !== "object")
|
|
4606
4698
|
return null;
|
|
@@ -4650,20 +4742,27 @@ function shaMatches(a, b) {
|
|
|
4650
4742
|
return x === y;
|
|
4651
4743
|
return x.slice(0, n) === y.slice(0, n);
|
|
4652
4744
|
}
|
|
4653
|
-
async function
|
|
4745
|
+
async function readDeploymentContext(deps, slug) {
|
|
4654
4746
|
let status;
|
|
4655
4747
|
try {
|
|
4656
4748
|
status = await deps.fetchStatus(slug);
|
|
4657
4749
|
} catch {
|
|
4658
|
-
return null;
|
|
4750
|
+
return { baseline: null };
|
|
4751
|
+
}
|
|
4752
|
+
if (status === null)
|
|
4753
|
+
return { baseline: null };
|
|
4754
|
+
const origin = status.origin;
|
|
4755
|
+
if (!status.supported || status.liveDeployment === null) {
|
|
4756
|
+
return { baseline: null, ...origin !== undefined ? { origin } : {} };
|
|
4659
4757
|
}
|
|
4660
|
-
if (status === null || !status.supported || status.liveDeployment === null)
|
|
4661
|
-
return null;
|
|
4662
4758
|
const live = status.liveDeployment;
|
|
4663
4759
|
return {
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4760
|
+
baseline: {
|
|
4761
|
+
id: live.id,
|
|
4762
|
+
commitHash: live.commit?.hash ?? null,
|
|
4763
|
+
createdOn: live.createdOn
|
|
4764
|
+
},
|
|
4765
|
+
...origin !== undefined ? { origin } : {}
|
|
4667
4766
|
};
|
|
4668
4767
|
}
|
|
4669
4768
|
async function waitForCommitDeployment(args, deps) {
|
|
@@ -4671,6 +4770,7 @@ async function waitForCommitDeployment(args, deps) {
|
|
|
4671
4770
|
const start = deps.now();
|
|
4672
4771
|
const baselineId = args.baseline?.id ?? null;
|
|
4673
4772
|
let matchedInProgress = false;
|
|
4773
|
+
let lastV2Stage = null;
|
|
4674
4774
|
for (;; ) {
|
|
4675
4775
|
let status;
|
|
4676
4776
|
try {
|
|
@@ -4684,6 +4784,38 @@ async function waitForCommitDeployment(args, deps) {
|
|
|
4684
4784
|
return { kind: "unsupported" };
|
|
4685
4785
|
const live = status.liveDeployment;
|
|
4686
4786
|
const elapsed = deps.now() - start;
|
|
4787
|
+
if (status.origin === "dispatch" && status.v2 !== undefined && shaMatches(status.v2.commitSha, args.shippedSha)) {
|
|
4788
|
+
const delivery = status.v2;
|
|
4789
|
+
if (delivery.stage !== lastV2Stage) {
|
|
4790
|
+
args.onV2Stage?.(delivery);
|
|
4791
|
+
lastV2Stage = delivery.stage;
|
|
4792
|
+
}
|
|
4793
|
+
if (delivery.state === "refused") {
|
|
4794
|
+
return {
|
|
4795
|
+
kind: "failure",
|
|
4796
|
+
dispatch: true,
|
|
4797
|
+
failedStage: delivery.failedAt ?? delivery.stage,
|
|
4798
|
+
reason: delivery.reason,
|
|
4799
|
+
detail: delivery.detail,
|
|
4800
|
+
runUrl: delivery.runUrl
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
if (delivery.state === "delivered" || delivery.stage === "live") {
|
|
4804
|
+
return {
|
|
4805
|
+
kind: "success",
|
|
4806
|
+
deploymentUrl: live?.url ?? null,
|
|
4807
|
+
v2: {
|
|
4808
|
+
commitSha: delivery.commitSha,
|
|
4809
|
+
...delivery.deliveredAt !== undefined ? { deliveredAt: delivery.deliveredAt } : {}
|
|
4810
|
+
}
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4813
|
+
matchedInProgress = true;
|
|
4814
|
+
if (elapsed >= args.settleTimeoutMs)
|
|
4815
|
+
return { kind: "pending" };
|
|
4816
|
+
await deps.sleep(pollIntervalMs);
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4687
4819
|
if (live !== null && shaMatches(live.commit?.hash, args.shippedSha)) {
|
|
4688
4820
|
if (live.buildStatus === "success") {
|
|
4689
4821
|
return { kind: "success", deploymentUrl: live.url };
|
|
@@ -4795,7 +4927,7 @@ async function verifyServed(args, deps = { fetch }) {
|
|
|
4795
4927
|
}
|
|
4796
4928
|
|
|
4797
4929
|
// src/commands/status.ts
|
|
4798
|
-
import { readFileSync as
|
|
4930
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4799
4931
|
|
|
4800
4932
|
// src/deploy/manifest-state.ts
|
|
4801
4933
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
@@ -5604,6 +5736,88 @@ still provisioning after the watch window — re-run \`launchpad watch ${slug}\`
|
|
|
5604
5736
|
}
|
|
5605
5737
|
}
|
|
5606
5738
|
|
|
5739
|
+
// src/manifest/load.ts
|
|
5740
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5741
|
+
import { parse as parseYaml4, YAMLParseError } from "yaml";
|
|
5742
|
+
function loadManifest(path8) {
|
|
5743
|
+
let raw;
|
|
5744
|
+
try {
|
|
5745
|
+
raw = readFileSync8(path8, "utf8");
|
|
5746
|
+
} catch (err) {
|
|
5747
|
+
const e = err;
|
|
5748
|
+
if (e.code === "ENOENT") {
|
|
5749
|
+
return { kind: "file-not-found", path: path8 };
|
|
5750
|
+
}
|
|
5751
|
+
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
5752
|
+
}
|
|
5753
|
+
return parseManifest2(raw, path8);
|
|
5754
|
+
}
|
|
5755
|
+
function loadManifestAnyVersion(path8) {
|
|
5756
|
+
let raw;
|
|
5757
|
+
try {
|
|
5758
|
+
raw = readFileSync8(path8, "utf8");
|
|
5759
|
+
} catch (err) {
|
|
5760
|
+
const e = err;
|
|
5761
|
+
if (e.code === "ENOENT")
|
|
5762
|
+
return { kind: "file-not-found", path: path8 };
|
|
5763
|
+
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
5764
|
+
}
|
|
5765
|
+
return parseManifestAnyVersion(raw, path8);
|
|
5766
|
+
}
|
|
5767
|
+
function parseManifestAnyVersion(yamlText, path8) {
|
|
5768
|
+
let parsed;
|
|
5769
|
+
try {
|
|
5770
|
+
parsed = parseYaml4(yamlText);
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
5773
|
+
return { kind: "yaml-parse-error", path: path8, message };
|
|
5774
|
+
}
|
|
5775
|
+
const apiVersion = typeof parsed === "object" && parsed !== null ? parsed.apiVersion : undefined;
|
|
5776
|
+
if (apiVersion === "launchpad.m-kopa.us/v2") {
|
|
5777
|
+
const result = parseManifestV2(parsed);
|
|
5778
|
+
return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
|
|
5779
|
+
}
|
|
5780
|
+
if (apiVersion === "launchpad.m-kopa.us/v1alpha1") {
|
|
5781
|
+
const result = parseManifest(parsed);
|
|
5782
|
+
return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
|
|
5783
|
+
}
|
|
5784
|
+
return {
|
|
5785
|
+
kind: "schema-error",
|
|
5786
|
+
path: path8,
|
|
5787
|
+
schema: "launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2",
|
|
5788
|
+
issues: [
|
|
5789
|
+
{
|
|
5790
|
+
path: "apiVersion",
|
|
5791
|
+
message: `unsupported apiVersion ${JSON.stringify(apiVersion)}; expected launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2`
|
|
5792
|
+
}
|
|
5793
|
+
]
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
function parseManifest2(yamlText, path8) {
|
|
5797
|
+
let parsed;
|
|
5798
|
+
try {
|
|
5799
|
+
parsed = parseYaml4(yamlText);
|
|
5800
|
+
} catch (err) {
|
|
5801
|
+
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
5802
|
+
return { kind: "yaml-parse-error", path: path8, message };
|
|
5803
|
+
}
|
|
5804
|
+
const result = ManifestSchema.safeParse(parsed);
|
|
5805
|
+
if (result.success) {
|
|
5806
|
+
return { kind: "ok", manifest: result.data };
|
|
5807
|
+
}
|
|
5808
|
+
return {
|
|
5809
|
+
kind: "schema-error",
|
|
5810
|
+
path: path8,
|
|
5811
|
+
issues: formatIssues(result.error)
|
|
5812
|
+
};
|
|
5813
|
+
}
|
|
5814
|
+
function formatIssues(error) {
|
|
5815
|
+
return error.issues.map((issue) => ({
|
|
5816
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
5817
|
+
message: issue.message
|
|
5818
|
+
}));
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5607
5821
|
// src/commands/status.ts
|
|
5608
5822
|
var statusCommand = {
|
|
5609
5823
|
name: "status",
|
|
@@ -5699,7 +5913,7 @@ async function runStatus(args, io) {
|
|
|
5699
5913
|
}
|
|
5700
5914
|
let localYaml = null;
|
|
5701
5915
|
try {
|
|
5702
|
-
localYaml =
|
|
5916
|
+
localYaml = readFileSync9(parsed.file, "utf8");
|
|
5703
5917
|
} catch (e) {
|
|
5704
5918
|
if (!isEnoent(e)) {
|
|
5705
5919
|
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
|
|
@@ -5708,24 +5922,24 @@ async function runStatus(args, io) {
|
|
|
5708
5922
|
}
|
|
5709
5923
|
let local = null;
|
|
5710
5924
|
if (localYaml !== null) {
|
|
5711
|
-
|
|
5712
|
-
try {
|
|
5713
|
-
const { parse: parseYaml4 } = await import("yaml");
|
|
5714
|
-
localObj = parseYaml4(localYaml);
|
|
5715
|
-
} catch (e) {
|
|
5716
|
-
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
|
|
5717
|
-
return 2;
|
|
5718
|
-
}
|
|
5719
|
-
const localParse = parseManifest(localObj);
|
|
5925
|
+
const localParse = parseManifestAnyVersion(localYaml, parsed.file);
|
|
5720
5926
|
if (localParse.kind !== "ok") {
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5927
|
+
if (localParse.kind === "yaml-parse-error") {
|
|
5928
|
+
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${localParse.message}`);
|
|
5929
|
+
} else if (localParse.kind === "schema-error") {
|
|
5930
|
+
io.err(`launchpad status: ${parsed.file} failed schema validation against ${localParse.schema}:`);
|
|
5931
|
+
for (const issue of localParse.issues) {
|
|
5932
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
5933
|
+
}
|
|
5934
|
+
} else {
|
|
5935
|
+
io.err(`launchpad status: cannot read local manifest at ${parsed.file}`);
|
|
5724
5936
|
}
|
|
5725
5937
|
return 2;
|
|
5726
5938
|
}
|
|
5727
5939
|
local = localParse.manifest;
|
|
5728
|
-
|
|
5940
|
+
if (local.apiVersion === "launchpad.m-kopa.us/v1alpha1") {
|
|
5941
|
+
warnSecretShape(local.production_env, io);
|
|
5942
|
+
}
|
|
5729
5943
|
}
|
|
5730
5944
|
let state;
|
|
5731
5945
|
try {
|
|
@@ -5755,9 +5969,9 @@ async function runStatus(args, io) {
|
|
|
5755
5969
|
}
|
|
5756
5970
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
5757
5971
|
const live = deployment?.liveDeployment ?? null;
|
|
5758
|
-
const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
5972
|
+
const contentIsLive = deployment?.v2?.state === "delivered" || deployment?.v2?.stage === "live" || live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
5759
5973
|
const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
|
|
5760
|
-
const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
|
|
5974
|
+
const platformTracked = state.hasAppFile === true || live?.trigger === "bot" || deployment?.origin === "dispatch";
|
|
5761
5975
|
const out = {
|
|
5762
5976
|
state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
5763
5977
|
...local === null ? { drift: null } : {},
|
|
@@ -5790,7 +6004,7 @@ async function runStatus(args, io) {
|
|
|
5790
6004
|
};
|
|
5791
6005
|
emit4(out, parsed.json, io);
|
|
5792
6006
|
if (parsed.strict) {
|
|
5793
|
-
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
6007
|
+
if (deployment?.liveDeployment?.buildStatus === "failure" || deployment?.v2?.state === "refused") {
|
|
5794
6008
|
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
5795
6009
|
return 1;
|
|
5796
6010
|
}
|
|
@@ -5798,26 +6012,40 @@ async function runStatus(args, io) {
|
|
|
5798
6012
|
}
|
|
5799
6013
|
return 0;
|
|
5800
6014
|
}
|
|
5801
|
-
|
|
5802
|
-
try {
|
|
5803
|
-
const { parse: parseYaml4 } = await import("yaml");
|
|
5804
|
-
deployedObj = parseYaml4(state.manifestYaml);
|
|
5805
|
-
} catch (e) {
|
|
5806
|
-
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
|
|
5807
|
-
return 2;
|
|
5808
|
-
}
|
|
5809
|
-
const deployedParse = parseManifest(deployedObj);
|
|
6015
|
+
const deployedParse = parseManifestAnyVersion(state.manifestYaml, `deployed manifest at ${state.deployedManifestSha}`);
|
|
5810
6016
|
if (deployedParse.kind !== "ok") {
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
6017
|
+
if (deployedParse.kind === "yaml-parse-error") {
|
|
6018
|
+
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${deployedParse.message}`);
|
|
6019
|
+
} else if (deployedParse.kind === "schema-error") {
|
|
6020
|
+
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation against ${deployedParse.schema}:`);
|
|
6021
|
+
for (const issue of deployedParse.issues) {
|
|
6022
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
6023
|
+
}
|
|
5814
6024
|
}
|
|
5815
6025
|
return 2;
|
|
5816
6026
|
}
|
|
5817
6027
|
const deployed = deployedParse.manifest;
|
|
6028
|
+
if (local.apiVersion !== deployed.apiVersion) {
|
|
6029
|
+
const reason = `local manifest uses ${local.apiVersion}, while the deployed manifest uses ${deployed.apiVersion}; ` + "the manifest shapes differ, so drift was not evaluated";
|
|
6030
|
+
const out = {
|
|
6031
|
+
state: "live_drift_unknown",
|
|
6032
|
+
drift: null,
|
|
6033
|
+
driftUnavailableReason: reason,
|
|
6034
|
+
...statusTruthFields({
|
|
6035
|
+
slug: parsed.slug,
|
|
6036
|
+
deployedSha: state.deployedManifestSha,
|
|
6037
|
+
headSha: state.appRepoHeadSha,
|
|
6038
|
+
openPrNumber: state.openPr?.number ?? null,
|
|
6039
|
+
...deploymentKnown ? { deployment } : {},
|
|
6040
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
6041
|
+
})
|
|
6042
|
+
};
|
|
6043
|
+
emit4(out, parsed.json, io);
|
|
6044
|
+
return 0;
|
|
6045
|
+
}
|
|
5818
6046
|
const localGroups = await resolveStatusGroups({
|
|
5819
6047
|
cfg,
|
|
5820
|
-
refs: allowedEntraGroups(local
|
|
6048
|
+
refs: allowedEntraGroups(manifestAccess(local)),
|
|
5821
6049
|
source: "local manifest",
|
|
5822
6050
|
manifestSha: null
|
|
5823
6051
|
});
|
|
@@ -5826,14 +6054,17 @@ async function runStatus(args, io) {
|
|
|
5826
6054
|
}
|
|
5827
6055
|
const deployedGroups = await resolveStatusGroups({
|
|
5828
6056
|
cfg,
|
|
5829
|
-
refs: allowedEntraGroups(deployed
|
|
6057
|
+
refs: allowedEntraGroups(manifestAccess(deployed)),
|
|
5830
6058
|
source: "deployed manifest",
|
|
5831
6059
|
manifestSha: state.deployedManifestSha
|
|
5832
6060
|
});
|
|
5833
6061
|
if (deployedGroups.kind === "error") {
|
|
5834
6062
|
return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
|
|
5835
6063
|
}
|
|
5836
|
-
const drift =
|
|
6064
|
+
const drift = local.apiVersion === "launchpad.m-kopa.us/v2" && deployed.apiVersion === "launchpad.m-kopa.us/v2" ? computeV2SpecDrift(local, deployed, {
|
|
6065
|
+
local: localGroups.ids,
|
|
6066
|
+
deployed: deployedGroups.ids
|
|
6067
|
+
}) : computeDrift(local, deployed, {
|
|
5837
6068
|
local: localGroups.ids,
|
|
5838
6069
|
deployed: deployedGroups.ids
|
|
5839
6070
|
});
|
|
@@ -5850,8 +6081,8 @@ async function runStatus(args, io) {
|
|
|
5850
6081
|
}
|
|
5851
6082
|
accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
|
|
5852
6083
|
}
|
|
5853
|
-
const localAssignmentGroups = (local
|
|
5854
|
-
const deployedAssignmentGroups = (deployed
|
|
6084
|
+
const localAssignmentGroups = manifestAuth(local) === "access" ? localGroups : null;
|
|
6085
|
+
const deployedAssignmentGroups = manifestAuth(deployed) === "access" ? deployedGroups : null;
|
|
5855
6086
|
const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
|
|
5856
6087
|
if (assignmentCheck.kind === "error") {
|
|
5857
6088
|
return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
|
|
@@ -6052,6 +6283,58 @@ function computeDrift(local, deployed, canonicalGroups) {
|
|
|
6052
6283
|
}
|
|
6053
6284
|
return diffs;
|
|
6054
6285
|
}
|
|
6286
|
+
function computeV2SpecDrift(local, deployed, canonicalGroups) {
|
|
6287
|
+
const localSpec = {
|
|
6288
|
+
...local.spec,
|
|
6289
|
+
access: {
|
|
6290
|
+
...local.spec.access,
|
|
6291
|
+
allowed_entra_groups: [
|
|
6292
|
+
...canonicalGroups?.local ?? local.spec.access.allowed_entra_groups
|
|
6293
|
+
].sort()
|
|
6294
|
+
}
|
|
6295
|
+
};
|
|
6296
|
+
const deployedSpec = {
|
|
6297
|
+
...deployed.spec,
|
|
6298
|
+
access: {
|
|
6299
|
+
...deployed.spec.access,
|
|
6300
|
+
allowed_entra_groups: [
|
|
6301
|
+
...canonicalGroups?.deployed ?? deployed.spec.access.allowed_entra_groups
|
|
6302
|
+
].sort()
|
|
6303
|
+
}
|
|
6304
|
+
};
|
|
6305
|
+
const diffs = [];
|
|
6306
|
+
compareV2Value("spec", localSpec, deployedSpec, diffs);
|
|
6307
|
+
return diffs;
|
|
6308
|
+
}
|
|
6309
|
+
function compareV2Value(path8, local, deployed, diffs) {
|
|
6310
|
+
if (Object.is(local, deployed))
|
|
6311
|
+
return;
|
|
6312
|
+
if (Array.isArray(local) || Array.isArray(deployed)) {
|
|
6313
|
+
if (JSON.stringify(local) !== JSON.stringify(deployed)) {
|
|
6314
|
+
diffs.push({ path: path8, local, deployed });
|
|
6315
|
+
}
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
if (typeof local === "object" && local !== null && typeof deployed === "object" && deployed !== null) {
|
|
6319
|
+
const localRecord = local;
|
|
6320
|
+
const deployedRecord = deployed;
|
|
6321
|
+
const keys = new Set([
|
|
6322
|
+
...Object.keys(localRecord),
|
|
6323
|
+
...Object.keys(deployedRecord)
|
|
6324
|
+
]);
|
|
6325
|
+
for (const key of [...keys].sort()) {
|
|
6326
|
+
compareV2Value(`${path8}.${key}`, localRecord[key], deployedRecord[key], diffs);
|
|
6327
|
+
}
|
|
6328
|
+
return;
|
|
6329
|
+
}
|
|
6330
|
+
diffs.push({ path: path8, local, deployed });
|
|
6331
|
+
}
|
|
6332
|
+
function manifestAccess(manifest) {
|
|
6333
|
+
return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.access : manifest.access;
|
|
6334
|
+
}
|
|
6335
|
+
function manifestAuth(manifest) {
|
|
6336
|
+
return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.auth ?? "access" : manifest.auth ?? "access";
|
|
6337
|
+
}
|
|
6055
6338
|
function lifecycleOutput(slug, lc) {
|
|
6056
6339
|
const state = lc.state === "failed" ? "provisioning_failed" : lc.state === "live" ? "in_sync" : lc.state;
|
|
6057
6340
|
return {
|
|
@@ -6119,7 +6402,7 @@ function emit4(out, asJson, io) {
|
|
|
6119
6402
|
surfaceExceptions(out, io);
|
|
6120
6403
|
return;
|
|
6121
6404
|
case "live_content_untracked":
|
|
6122
|
-
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
6405
|
+
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} ` + `(no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
6123
6406
|
surfaceNoLocalManifestNote(out, io);
|
|
6124
6407
|
surfaceHeadVsDeployed(out, io);
|
|
6125
6408
|
surfaceDeployment(out, io);
|
|
@@ -6181,6 +6464,10 @@ function surfaceGroupIssues(out, io) {
|
|
|
6181
6464
|
function surfaceNoLocalManifestNote(out, io) {
|
|
6182
6465
|
if (out.drift !== null)
|
|
6183
6466
|
return;
|
|
6467
|
+
if (out.driftUnavailableReason !== undefined) {
|
|
6468
|
+
io.out(` ${out.driftUnavailableReason}.`);
|
|
6469
|
+
return;
|
|
6470
|
+
}
|
|
6184
6471
|
io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
|
|
6185
6472
|
}
|
|
6186
6473
|
function triggerLabel(trigger) {
|
|
@@ -6206,7 +6493,13 @@ function surfaceFailedLifecycleDeployment(out, io) {
|
|
|
6206
6493
|
}
|
|
6207
6494
|
function surfaceDeployment(out, io) {
|
|
6208
6495
|
const dep = out.deployment;
|
|
6209
|
-
if (dep === undefined || dep === null
|
|
6496
|
+
if (dep === undefined || dep === null)
|
|
6497
|
+
return;
|
|
6498
|
+
if (dep.origin === "dispatch") {
|
|
6499
|
+
surfaceV2Deployment(dep, io);
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
if (!dep.supported)
|
|
6210
6503
|
return;
|
|
6211
6504
|
if (dep.reconciledToLive === true) {
|
|
6212
6505
|
io.out(" ✓ recovered: this app was in a failed state; its verified-live build reconciled the record failed→live (no `launchpad recover` needed).");
|
|
@@ -6246,6 +6539,29 @@ function surfaceDeployment(out, io) {
|
|
|
6246
6539
|
}
|
|
6247
6540
|
}
|
|
6248
6541
|
}
|
|
6542
|
+
function surfaceV2Deployment(dep, io) {
|
|
6543
|
+
for (const line of renderV2DeploymentLines(dep))
|
|
6544
|
+
io.out(line);
|
|
6545
|
+
}
|
|
6546
|
+
function renderV2DeploymentLines(dep) {
|
|
6547
|
+
const delivery = dep.v2;
|
|
6548
|
+
if (delivery === undefined) {
|
|
6549
|
+
return [" v2 delivery: dispatch origin (delivery state unavailable)."];
|
|
6550
|
+
}
|
|
6551
|
+
const lines = [];
|
|
6552
|
+
const deliveredAt = delivery.state === "delivered" && delivery.deliveredAt !== undefined ? `, delivered ${delivery.deliveredAt}` : "";
|
|
6553
|
+
lines.push(` v2 delivery: state ${delivery.state}, stage ${delivery.stage}, commit ${delivery.commitSha.slice(0, 7)}${deliveredAt}`);
|
|
6554
|
+
if (delivery.state === "refused") {
|
|
6555
|
+
const failedAt = delivery.failedAt ?? delivery.stage;
|
|
6556
|
+
lines.push(` failed at ${failedAt}${delivery.reason ? `: ${delivery.reason}` : ""}`);
|
|
6557
|
+
if (delivery.runUrl)
|
|
6558
|
+
lines.push(` GitHub run: ${delivery.runUrl}`);
|
|
6559
|
+
}
|
|
6560
|
+
if (delivery.state !== "delivered" && dep.lastSuccessfulDeployment !== null) {
|
|
6561
|
+
lines.push(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
|
|
6562
|
+
}
|
|
6563
|
+
return lines;
|
|
6564
|
+
}
|
|
6249
6565
|
function surfaceExceptions(out, io) {
|
|
6250
6566
|
const exceptions = out.standingExceptions;
|
|
6251
6567
|
if (exceptions === undefined || exceptions.length === 0)
|
|
@@ -6262,7 +6578,7 @@ function surfaceHeadVsDeployed(out, io) {
|
|
|
6262
6578
|
if (out.hasOpenPr) {
|
|
6263
6579
|
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` an apply may be queued — see PR #${out.openPrNumber})`);
|
|
6264
6580
|
} else {
|
|
6265
|
-
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)}
|
|
6581
|
+
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` main is ahead of deployed)`);
|
|
6266
6582
|
}
|
|
6267
6583
|
}
|
|
6268
6584
|
}
|
|
@@ -6283,7 +6599,7 @@ function warnSecretShape(env, io) {
|
|
|
6283
6599
|
const hasPrefix = prefixes.some((p) => v.startsWith(p));
|
|
6284
6600
|
const longBase64Shape = v.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(v);
|
|
6285
6601
|
if (hasPrefix && longBase64Shape) {
|
|
6286
|
-
io.err(`launchpad status: production_env.${k} looks like a secret. production_env is non-secret by contract; move secrets to the secrets bindings path. (This is informational; status will continue.)`);
|
|
6602
|
+
io.err(`launchpad status: production_env.${k} looks like a secret. ` + `production_env is non-secret by contract; move secrets to the ` + `secrets bindings path. (This is informational; status will continue.)`);
|
|
6287
6603
|
}
|
|
6288
6604
|
}
|
|
6289
6605
|
}
|
|
@@ -6342,7 +6658,7 @@ function parseArgs2(args, cwd = process.cwd(), warn) {
|
|
|
6342
6658
|
if (slug === null) {
|
|
6343
6659
|
const inferred = inferSlug({ cwd, file, warn });
|
|
6344
6660
|
if (inferred === null) {
|
|
6345
|
-
return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
6661
|
+
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
6346
6662
|
}
|
|
6347
6663
|
slug = inferred;
|
|
6348
6664
|
}
|
|
@@ -6401,7 +6717,7 @@ function readLocalEntrypoint(cwd) {
|
|
|
6401
6717
|
for (const rel of ["index.html", join9("static", "index.html")]) {
|
|
6402
6718
|
const p = join9(cwd, rel);
|
|
6403
6719
|
if (existsSync4(p))
|
|
6404
|
-
return new Uint8Array(
|
|
6720
|
+
return new Uint8Array(readFileSync10(p));
|
|
6405
6721
|
}
|
|
6406
6722
|
return null;
|
|
6407
6723
|
}
|
|
@@ -6416,13 +6732,25 @@ function realVerifyDeployDeps(cfg) {
|
|
|
6416
6732
|
}
|
|
6417
6733
|
async function runDeployVerification(args, io, deps) {
|
|
6418
6734
|
io.out("");
|
|
6419
|
-
|
|
6735
|
+
if (args.dispatchOrigin === true) {
|
|
6736
|
+
io.out("Waiting for your repo build and dispatch delivery …");
|
|
6737
|
+
} else {
|
|
6738
|
+
io.out("Verifying a build of your commit went live …");
|
|
6739
|
+
}
|
|
6740
|
+
const seenV2Stages = new Set;
|
|
6741
|
+
const onV2Stage = (delivery) => {
|
|
6742
|
+
if (seenV2Stages.has(delivery.stage))
|
|
6743
|
+
return;
|
|
6744
|
+
seenV2Stages.add(delivery.stage);
|
|
6745
|
+
io.out(` v2 stage: ${v2StageLabel(delivery.stage)}`);
|
|
6746
|
+
};
|
|
6420
6747
|
const settle = await deps.wait({
|
|
6421
6748
|
slug: args.slug,
|
|
6422
6749
|
shippedSha: args.shippedSha,
|
|
6423
6750
|
baseline: args.baseline,
|
|
6424
6751
|
registrationBoundMs: args.registrationBoundMs,
|
|
6425
|
-
settleTimeoutMs: args.timeoutMs
|
|
6752
|
+
settleTimeoutMs: args.timeoutMs,
|
|
6753
|
+
onV2Stage
|
|
6426
6754
|
}, deps.waitDeps);
|
|
6427
6755
|
switch (settle.kind) {
|
|
6428
6756
|
case "unsupported":
|
|
@@ -6451,10 +6779,25 @@ async function runDeployVerification(args, io, deps) {
|
|
|
6451
6779
|
io.out(` It may still be queued. Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6452
6780
|
return 0;
|
|
6453
6781
|
case "pending":
|
|
6782
|
+
if (args.dispatchOrigin === true || seenV2Stages.size > 0) {
|
|
6783
|
+
io.out(`⏳ Dispatch delivery is still in progress after ${Math.round(args.timeoutMs / 1000)}s - couldn't verify yet.`);
|
|
6784
|
+
io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6785
|
+
return 0;
|
|
6786
|
+
}
|
|
6454
6787
|
io.out(`⏳ Your commit's build is still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
|
|
6455
6788
|
io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6456
6789
|
return 0;
|
|
6457
6790
|
case "failure":
|
|
6791
|
+
if (settle.dispatch === true) {
|
|
6792
|
+
io.err(`✗ Dispatch delivery REFUSED at ${settle.failedStage ?? "unknown"} - your commit is NOT live.`);
|
|
6793
|
+
if (settle.reason)
|
|
6794
|
+
io.err(` reason: ${redactDiagnostic(settle.reason)}`);
|
|
6795
|
+
if (settle.detail)
|
|
6796
|
+
io.err(` detail: ${redactDiagnostic(settle.detail)}`);
|
|
6797
|
+
if (settle.runUrl)
|
|
6798
|
+
io.err(` GitHub run: ${settle.runUrl}`);
|
|
6799
|
+
return 1;
|
|
6800
|
+
}
|
|
6458
6801
|
io.err("✗ The Cloudflare Pages build for your commit FAILED — your content is NOT live.");
|
|
6459
6802
|
if (settle.failedStage)
|
|
6460
6803
|
io.err(` failed stage: ${redactDiagnostic(settle.failedStage)}`);
|
|
@@ -6466,9 +6809,30 @@ async function runDeployVerification(args, io, deps) {
|
|
|
6466
6809
|
io.err(` Full logs: \`launchpad status ${args.slug}\``);
|
|
6467
6810
|
return 1;
|
|
6468
6811
|
case "success":
|
|
6812
|
+
if (settle.v2 !== undefined) {
|
|
6813
|
+
const deliveredAt = settle.v2.deliveredAt ? ` at ${settle.v2.deliveredAt}` : "";
|
|
6814
|
+
io.out(`✓ Live in dispatch namespace - delivered commit ${settle.v2.commitSha}${deliveredAt}.`);
|
|
6815
|
+
return 0;
|
|
6816
|
+
}
|
|
6469
6817
|
return verifySuccess(args, settle.deploymentUrl, io, deps);
|
|
6470
6818
|
}
|
|
6471
6819
|
}
|
|
6820
|
+
function v2StageLabel(stage) {
|
|
6821
|
+
switch (stage) {
|
|
6822
|
+
case "committed":
|
|
6823
|
+
return "source committed";
|
|
6824
|
+
case "building":
|
|
6825
|
+
return "CI building";
|
|
6826
|
+
case "verified":
|
|
6827
|
+
return "run verified";
|
|
6828
|
+
case "uploading":
|
|
6829
|
+
return "artifact uploading";
|
|
6830
|
+
case "live":
|
|
6831
|
+
return "live in dispatch namespace";
|
|
6832
|
+
case "failed":
|
|
6833
|
+
return "failed";
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6472
6836
|
function realRedeployVerifyDeps(cfg) {
|
|
6473
6837
|
return {
|
|
6474
6838
|
wait: waitForDeployment,
|
|
@@ -6695,7 +7059,7 @@ function runGit2(sp, cwd, args) {
|
|
|
6695
7059
|
}
|
|
6696
7060
|
|
|
6697
7061
|
// src/deploy/manifest-diff.ts
|
|
6698
|
-
import { parse as
|
|
7062
|
+
import { parse as parseYaml5 } from "yaml";
|
|
6699
7063
|
var MASK = "«redacted»";
|
|
6700
7064
|
var SECRET_KEY_SEGMENT = /(secret|token|api[_-]?key|access[_-]?key|private[_-]?key|secret[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session|credential|value)/i;
|
|
6701
7065
|
function isSecretPath(path8) {
|
|
@@ -6786,7 +7150,7 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6786
7150
|
let local;
|
|
6787
7151
|
let deployed;
|
|
6788
7152
|
try {
|
|
6789
|
-
local =
|
|
7153
|
+
local = parseYaml5(localYaml);
|
|
6790
7154
|
} catch (e) {
|
|
6791
7155
|
return {
|
|
6792
7156
|
inSync: false,
|
|
@@ -6795,7 +7159,7 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6795
7159
|
};
|
|
6796
7160
|
}
|
|
6797
7161
|
try {
|
|
6798
|
-
deployed =
|
|
7162
|
+
deployed = parseYaml5(deployedYaml);
|
|
6799
7163
|
} catch (e) {
|
|
6800
7164
|
return {
|
|
6801
7165
|
inSync: false,
|
|
@@ -6803,6 +7167,22 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6803
7167
|
parseError: `deployed manifest did not parse: ${e.message}`
|
|
6804
7168
|
};
|
|
6805
7169
|
}
|
|
7170
|
+
const localVersion = manifestApiVersion(local);
|
|
7171
|
+
const deployedVersion = manifestApiVersion(deployed);
|
|
7172
|
+
if (localVersion !== deployedVersion && (isV2(localVersion) || isV2(deployedVersion))) {
|
|
7173
|
+
return {
|
|
7174
|
+
inSync: false,
|
|
7175
|
+
changes: [],
|
|
7176
|
+
parseError: null,
|
|
7177
|
+
shapeMismatch: `local uses ${localVersion ?? "an unknown schema"}; deployed uses ${deployedVersion ?? "an unknown schema"}`
|
|
7178
|
+
};
|
|
7179
|
+
}
|
|
7180
|
+
if (isV2(localVersion) && isV2(deployedVersion)) {
|
|
7181
|
+
local = { spec: manifestSpec(local) };
|
|
7182
|
+
deployed = { spec: manifestSpec(deployed) };
|
|
7183
|
+
}
|
|
7184
|
+
local = normalize(local);
|
|
7185
|
+
deployed = normalize(deployed);
|
|
6806
7186
|
if (stableStringify(local) === stableStringify(deployed)) {
|
|
6807
7187
|
return { inSync: true, changes: [], parseError: null };
|
|
6808
7188
|
}
|
|
@@ -6849,6 +7229,18 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6849
7229
|
}
|
|
6850
7230
|
return { inSync: false, changes, parseError: null };
|
|
6851
7231
|
}
|
|
7232
|
+
function manifestApiVersion(value) {
|
|
7233
|
+
if (typeof value !== "object" || value === null)
|
|
7234
|
+
return null;
|
|
7235
|
+
const apiVersion = value.apiVersion;
|
|
7236
|
+
return typeof apiVersion === "string" ? apiVersion : null;
|
|
7237
|
+
}
|
|
7238
|
+
function isV2(apiVersion) {
|
|
7239
|
+
return apiVersion === "launchpad.m-kopa.us/v2";
|
|
7240
|
+
}
|
|
7241
|
+
function manifestSpec(value) {
|
|
7242
|
+
return typeof value === "object" && value !== null ? value.spec : undefined;
|
|
7243
|
+
}
|
|
6852
7244
|
function renderManifestDiffLines(diff, max = 20) {
|
|
6853
7245
|
const lines = [];
|
|
6854
7246
|
for (const c of diff.changes.slice(0, max)) {
|
|
@@ -7256,9 +7648,6 @@ function parseDeployFlags(args) {
|
|
|
7256
7648
|
if (appType === null) {
|
|
7257
7649
|
return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
|
|
7258
7650
|
}
|
|
7259
|
-
if (allowedGroups.length === 0) {
|
|
7260
|
-
return "--new requires at least one --allowed-group";
|
|
7261
|
-
}
|
|
7262
7651
|
return {
|
|
7263
7652
|
mode: {
|
|
7264
7653
|
kind: "new",
|
|
@@ -7277,47 +7666,6 @@ function parseDeployFlags(args) {
|
|
|
7277
7666
|
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
7278
7667
|
import { resolve as resolvePath, join as join10 } from "node:path";
|
|
7279
7668
|
|
|
7280
|
-
// src/manifest/load.ts
|
|
7281
|
-
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7282
|
-
import { parse as parseYaml5, YAMLParseError } from "yaml";
|
|
7283
|
-
function loadManifest(path8) {
|
|
7284
|
-
let raw;
|
|
7285
|
-
try {
|
|
7286
|
-
raw = readFileSync10(path8, "utf8");
|
|
7287
|
-
} catch (err) {
|
|
7288
|
-
const e = err;
|
|
7289
|
-
if (e.code === "ENOENT") {
|
|
7290
|
-
return { kind: "file-not-found", path: path8 };
|
|
7291
|
-
}
|
|
7292
|
-
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
7293
|
-
}
|
|
7294
|
-
return parseManifest2(raw, path8);
|
|
7295
|
-
}
|
|
7296
|
-
function parseManifest2(yamlText, path8) {
|
|
7297
|
-
let parsed;
|
|
7298
|
-
try {
|
|
7299
|
-
parsed = parseYaml5(yamlText);
|
|
7300
|
-
} catch (err) {
|
|
7301
|
-
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
7302
|
-
return { kind: "yaml-parse-error", path: path8, message };
|
|
7303
|
-
}
|
|
7304
|
-
const result = ManifestSchema.safeParse(parsed);
|
|
7305
|
-
if (result.success) {
|
|
7306
|
-
return { kind: "ok", manifest: result.data };
|
|
7307
|
-
}
|
|
7308
|
-
return {
|
|
7309
|
-
kind: "schema-error",
|
|
7310
|
-
path: path8,
|
|
7311
|
-
issues: formatIssues(result.error)
|
|
7312
|
-
};
|
|
7313
|
-
}
|
|
7314
|
-
function formatIssues(error) {
|
|
7315
|
-
return error.issues.map((issue) => ({
|
|
7316
|
-
path: issue.path.map(String).join(".") || "(root)",
|
|
7317
|
-
message: issue.message
|
|
7318
|
-
}));
|
|
7319
|
-
}
|
|
7320
|
-
|
|
7321
7669
|
// src/status-page-output.ts
|
|
7322
7670
|
async function surfaceStatusPageUrl(url, io, options) {
|
|
7323
7671
|
if (typeof url !== "string" || url.length === 0)
|
|
@@ -7392,13 +7740,13 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
7392
7740
|
return 0;
|
|
7393
7741
|
}
|
|
7394
7742
|
const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
7395
|
-
const loaded =
|
|
7743
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
7396
7744
|
if (loaded.kind !== "ok") {
|
|
7397
7745
|
return renderManifestError(loaded, io);
|
|
7398
7746
|
}
|
|
7399
|
-
const slug = loaded
|
|
7747
|
+
const slug = manifestSlug(loaded);
|
|
7400
7748
|
if (!SLUG_REGEX2.test(slug)) {
|
|
7401
|
-
io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
|
|
7749
|
+
io.err(`launchpad deploy --apply: manifest's ${loaded.schema === "launchpad.m-kopa.us/v2" ? "metadata.slug" : "metadata.name"} '${slug}' is not a valid slug`);
|
|
7402
7750
|
return 1;
|
|
7403
7751
|
}
|
|
7404
7752
|
if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
|
|
@@ -7689,12 +8037,12 @@ function deletePinIfPresent(cfg, slug, io) {
|
|
|
7689
8037
|
}
|
|
7690
8038
|
async function loadSlugForResume(opts, io) {
|
|
7691
8039
|
const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
7692
|
-
const loaded =
|
|
8040
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
7693
8041
|
if (loaded.kind !== "ok") {
|
|
7694
8042
|
renderManifestError(loaded, io);
|
|
7695
8043
|
return null;
|
|
7696
8044
|
}
|
|
7697
|
-
return loaded
|
|
8045
|
+
return manifestSlug(loaded);
|
|
7698
8046
|
}
|
|
7699
8047
|
async function defaultPrompt(question) {
|
|
7700
8048
|
const { createInterface } = await import("node:readline/promises");
|
|
@@ -7735,6 +8083,10 @@ function mapHttpError(e, slug, io) {
|
|
|
7735
8083
|
io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
|
|
7736
8084
|
return 2;
|
|
7737
8085
|
}
|
|
8086
|
+
function manifestSlug(loaded) {
|
|
8087
|
+
const { metadata } = loaded.manifest;
|
|
8088
|
+
return "slug" in metadata ? metadata.slug : metadata.name;
|
|
8089
|
+
}
|
|
7738
8090
|
function renderManifestError(loaded, io) {
|
|
7739
8091
|
switch (loaded.kind) {
|
|
7740
8092
|
case "ok":
|
|
@@ -8630,6 +8982,10 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
|
|
|
8630
8982
|
io.out(` config diff: skipped (${diff.parseError}).`);
|
|
8631
8983
|
return;
|
|
8632
8984
|
}
|
|
8985
|
+
if (diff.shapeMismatch !== undefined) {
|
|
8986
|
+
io.out(` config diff: manifest shapes differ (${diff.shapeMismatch}); drift was not evaluated.`);
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8633
8989
|
if (diff.inSync) {
|
|
8634
8990
|
io.out(" config: launchpad.yaml matches the deployed manifest (in sync).");
|
|
8635
8991
|
return;
|
|
@@ -8653,6 +9009,7 @@ async function runModelADeploy(args) {
|
|
|
8653
9009
|
const { cwd, manifestPath, io } = args;
|
|
8654
9010
|
let slug;
|
|
8655
9011
|
let appType = "static";
|
|
9012
|
+
let declaredOrigin;
|
|
8656
9013
|
try {
|
|
8657
9014
|
const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
|
|
8658
9015
|
const metaSlug = resolveManifestSlug(manifestObj);
|
|
@@ -8662,13 +9019,22 @@ async function runModelADeploy(args) {
|
|
|
8662
9019
|
}
|
|
8663
9020
|
slug = metaSlug;
|
|
8664
9021
|
const raw = manifestObj;
|
|
8665
|
-
const
|
|
8666
|
-
|
|
9022
|
+
const isV22 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
|
|
9023
|
+
declaredOrigin = declaredOriginOf(isV22, raw?.spec?.origin);
|
|
9024
|
+
const deploymentType = isV22 ? raw?.spec?.appType : raw?.deployment?.type;
|
|
8667
9025
|
if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
|
|
8668
9026
|
appType = deploymentType;
|
|
8669
9027
|
}
|
|
9028
|
+
if (appType !== undefined) {
|
|
9029
|
+
const auth = isV22 ? raw?.spec?.auth : raw?.auth;
|
|
9030
|
+
const issue = deploymentAuthIssue(appType, auth === "gateway" ? "gateway" : auth === "access" ? "access" : undefined);
|
|
9031
|
+
if (issue !== null) {
|
|
9032
|
+
io.err(`launchpad deploy: ${issue.message}`);
|
|
9033
|
+
return 1;
|
|
9034
|
+
}
|
|
9035
|
+
}
|
|
8670
9036
|
if (deploymentType === "container") {
|
|
8671
|
-
const parsed =
|
|
9037
|
+
const parsed = isV22 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
|
|
8672
9038
|
if (parsed.kind !== "ok") {
|
|
8673
9039
|
io.err("launchpad deploy: manifest is structurally invalid:");
|
|
8674
9040
|
for (const issue of parsed.issues) {
|
|
@@ -8720,7 +9086,8 @@ async function runModelADeploy(args) {
|
|
|
8720
9086
|
try {
|
|
8721
9087
|
await surfaceManifestDiff(cfg, slug, manifestPath, io);
|
|
8722
9088
|
} catch {}
|
|
8723
|
-
const
|
|
9089
|
+
const deployContext = await readDeploymentContext(realCommitWaitDeps(cfg), slug);
|
|
9090
|
+
const deployBaseline = noVerify ? null : deployContext.baseline;
|
|
8724
9091
|
let result;
|
|
8725
9092
|
try {
|
|
8726
9093
|
if (args.allowUnguarded) {
|
|
@@ -8791,6 +9158,11 @@ async function runModelADeploy(args) {
|
|
|
8791
9158
|
allowedHost: cfg.statusPageHost
|
|
8792
9159
|
});
|
|
8793
9160
|
const errorCode = body.error;
|
|
9161
|
+
if (errorCode === "audience_confirmation_required") {
|
|
9162
|
+
io.err(String(body.message));
|
|
9163
|
+
io.err("Only the human builder may confirm. Retry this deployment after confirmation.");
|
|
9164
|
+
return 1;
|
|
9165
|
+
}
|
|
8794
9166
|
if (result.status === 426 && typeof body.minimum === "string" && typeof body.yours === "string") {
|
|
8795
9167
|
io.err(upgradeRequiredMessage(body.minimum, body.yours));
|
|
8796
9168
|
return EXIT_CLI_UPGRADE_REQUIRED;
|
|
@@ -8850,7 +9222,11 @@ async function runModelADeploy(args) {
|
|
|
8850
9222
|
if (accessExit !== 0)
|
|
8851
9223
|
return accessExit;
|
|
8852
9224
|
io.out("");
|
|
8853
|
-
|
|
9225
|
+
const dispatchOrigin = handoffIsDispatch({
|
|
9226
|
+
declared: declaredOrigin,
|
|
9227
|
+
baseline: deployContext.origin
|
|
9228
|
+
});
|
|
9229
|
+
surfaceCommittedHandoff(dispatchOrigin, io, "ascii");
|
|
8854
9230
|
if (noVerify)
|
|
8855
9231
|
return 0;
|
|
8856
9232
|
return runDeployVerification({
|
|
@@ -8860,7 +9236,8 @@ async function runModelADeploy(args) {
|
|
|
8860
9236
|
shippedSha: outcome.manifest_sha,
|
|
8861
9237
|
baseline: deployBaseline,
|
|
8862
9238
|
registrationBoundMs,
|
|
8863
|
-
timeoutMs: verifyTimeoutMs
|
|
9239
|
+
timeoutMs: verifyTimeoutMs,
|
|
9240
|
+
dispatchOrigin
|
|
8864
9241
|
}, io, realVerifyDeployDeps(cfg));
|
|
8865
9242
|
}
|
|
8866
9243
|
io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
|
|
@@ -8956,9 +9333,18 @@ async function runModelADeploy(args) {
|
|
|
8956
9333
|
}
|
|
8957
9334
|
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
8958
9335
|
io.out("");
|
|
8959
|
-
|
|
9336
|
+
const dispatchOrigin = handoffIsDispatch({
|
|
9337
|
+
v2Delivery: success.v2Delivery,
|
|
9338
|
+
declared: declaredOrigin,
|
|
9339
|
+
baseline: deployContext.origin
|
|
9340
|
+
});
|
|
9341
|
+
surfaceCommittedHandoff(dispatchOrigin, io);
|
|
8960
9342
|
if (noVerify) {
|
|
8961
|
-
|
|
9343
|
+
if (dispatchOrigin) {
|
|
9344
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the delivery stage and outcome.`);
|
|
9345
|
+
} else {
|
|
9346
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
9347
|
+
}
|
|
8962
9348
|
return 0;
|
|
8963
9349
|
}
|
|
8964
9350
|
return runDeployVerification({
|
|
@@ -8968,11 +9354,33 @@ async function runModelADeploy(args) {
|
|
|
8968
9354
|
shippedSha: success.commit_sha,
|
|
8969
9355
|
baseline: deployBaseline,
|
|
8970
9356
|
registrationBoundMs,
|
|
8971
|
-
timeoutMs: verifyTimeoutMs
|
|
9357
|
+
timeoutMs: verifyTimeoutMs,
|
|
9358
|
+
dispatchOrigin
|
|
8972
9359
|
}, io, realVerifyDeployDeps(cfg));
|
|
8973
9360
|
}
|
|
8974
9361
|
}
|
|
8975
9362
|
}
|
|
9363
|
+
function surfaceCommittedHandoff(dispatchOrigin, io, pagesStyle = "unicode") {
|
|
9364
|
+
io.out(committedHandoffMessage(dispatchOrigin, pagesStyle));
|
|
9365
|
+
}
|
|
9366
|
+
function declaredOriginOf(isV22, origin) {
|
|
9367
|
+
if (!isV22)
|
|
9368
|
+
return "pages";
|
|
9369
|
+
return origin === "dispatch" ? "dispatch" : "pages";
|
|
9370
|
+
}
|
|
9371
|
+
function handoffIsDispatch(input) {
|
|
9372
|
+
if (input.v2Delivery === "pending" || input.v2Delivery === "deferred")
|
|
9373
|
+
return true;
|
|
9374
|
+
if (input.declared !== undefined)
|
|
9375
|
+
return input.declared === "dispatch";
|
|
9376
|
+
return input.baseline === "dispatch";
|
|
9377
|
+
}
|
|
9378
|
+
function committedHandoffMessage(dispatchOrigin, pagesStyle = "unicode") {
|
|
9379
|
+
if (dispatchOrigin) {
|
|
9380
|
+
return "Committed; your repo's build workflow is building this commit, then the bot delivers it to the dispatch namespace.";
|
|
9381
|
+
}
|
|
9382
|
+
return pagesStyle === "ascii" ? "Committed; build pending - Cloudflare Pages is building this deploy now." : "Committed; build pending — Cloudflare Pages is building this deploy now.";
|
|
9383
|
+
}
|
|
8976
9384
|
function pendingReconcileOutcome(body) {
|
|
8977
9385
|
const manifestSha = body.manifestSha;
|
|
8978
9386
|
const id = body.reconcileId;
|
|
@@ -10364,7 +10772,7 @@ var initCommand = {
|
|
|
10364
10772
|
summary: "scaffold a new launchpad.yaml in the current directory",
|
|
10365
10773
|
run: (args, io) => runInit(args, io, defaultPrompt2)
|
|
10366
10774
|
};
|
|
10367
|
-
async function runInit(args, io, prompt) {
|
|
10775
|
+
async function runInit(args, io, prompt, identity = () => apiJson(loadConfig(), { path: "/me/identity" })) {
|
|
10368
10776
|
const parsed = parseArgs6(args);
|
|
10369
10777
|
if (parsed.kind === "usage-error") {
|
|
10370
10778
|
io.err(`launchpad init: ${parsed.message}`);
|
|
@@ -10400,8 +10808,39 @@ async function runInit(args, io, prompt) {
|
|
|
10400
10808
|
io.err(`launchpad init: ${fillResult.message}`);
|
|
10401
10809
|
return 64;
|
|
10402
10810
|
}
|
|
10403
|
-
const
|
|
10404
|
-
|
|
10811
|
+
const authIssue = deploymentAuthIssue(fillResult.inputs.type, fillResult.inputs.auth ?? "gateway");
|
|
10812
|
+
if (authIssue !== null) {
|
|
10813
|
+
io.err(`launchpad init: ${authIssue.message}`);
|
|
10814
|
+
return 64;
|
|
10815
|
+
}
|
|
10816
|
+
if (fillResult.inputs.type === "container" && inputs.origin !== undefined) {
|
|
10817
|
+
io.err("launchpad init: --origin is not supported for containers");
|
|
10818
|
+
return 64;
|
|
10819
|
+
}
|
|
10820
|
+
const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
|
|
10821
|
+
if (dispatch && fillResult.inputs.type === "react+api") {
|
|
10822
|
+
io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
|
|
10823
|
+
return 64;
|
|
10824
|
+
}
|
|
10825
|
+
if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
|
|
10826
|
+
io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
|
|
10827
|
+
return 64;
|
|
10828
|
+
}
|
|
10829
|
+
let privateOwner;
|
|
10830
|
+
if (fillResult.inputs.groups.length === 0) {
|
|
10831
|
+
try {
|
|
10832
|
+
privateOwner = await identity();
|
|
10833
|
+
} catch (error) {
|
|
10834
|
+
io.err(`launchpad init: cannot verify your identity for a private app: ${error instanceof Error ? error.message : String(error)}`);
|
|
10835
|
+
return 1;
|
|
10836
|
+
}
|
|
10837
|
+
io.out(`Audience: private. Only you (${privateOwner.email}) and platform administrators can open this app.`);
|
|
10838
|
+
} else {
|
|
10839
|
+
io.out(`Audience: selected Entra groups (${fillResult.inputs.groups.join(", ")}), plus platform administrators.`);
|
|
10840
|
+
io.out("Company-wide grants require your explicit browser confirmation before publishing.");
|
|
10841
|
+
}
|
|
10842
|
+
const manifest = buildManifest({ ...fillResult.inputs, ...privateOwner === undefined ? {} : { privateOwner } }, detected);
|
|
10843
|
+
const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
|
|
10405
10844
|
if (!validation.success) {
|
|
10406
10845
|
io.err("launchpad init: invalid manifest input");
|
|
10407
10846
|
for (const issue of validation.error.issues) {
|
|
@@ -10454,8 +10893,11 @@ async function runInit(args, io, prompt) {
|
|
|
10454
10893
|
io.out("");
|
|
10455
10894
|
io.out("Next steps:");
|
|
10456
10895
|
io.out(" launchpad validate # confirm the manifest");
|
|
10457
|
-
|
|
10458
|
-
|
|
10896
|
+
if (!dispatch)
|
|
10897
|
+
io.out(" launchpad secrets template # scaffold .env.example");
|
|
10898
|
+
io.out(" launchpad deploy # build and deploy the app");
|
|
10899
|
+
if (dispatch)
|
|
10900
|
+
io.out(" Dispatch currently supports plain-text environment variables, but not secret bindings.");
|
|
10459
10901
|
return 0;
|
|
10460
10902
|
}
|
|
10461
10903
|
function parseArgs6(args) {
|
|
@@ -10466,6 +10908,7 @@ function parseArgs6(args) {
|
|
|
10466
10908
|
description: undefined,
|
|
10467
10909
|
type: undefined,
|
|
10468
10910
|
auth: undefined,
|
|
10911
|
+
origin: undefined,
|
|
10469
10912
|
groups: [],
|
|
10470
10913
|
sessionDuration: undefined,
|
|
10471
10914
|
hostnames: [],
|
|
@@ -10550,6 +10993,16 @@ function parseArgs6(args) {
|
|
|
10550
10993
|
inputs.type = got;
|
|
10551
10994
|
continue;
|
|
10552
10995
|
}
|
|
10996
|
+
if (a === "--origin") {
|
|
10997
|
+
const got = needsValue();
|
|
10998
|
+
if (typeof got !== "string")
|
|
10999
|
+
return got;
|
|
11000
|
+
if (got !== "pages" && got !== "dispatch") {
|
|
11001
|
+
return { kind: "usage-error", message: "--origin must be pages or dispatch" };
|
|
11002
|
+
}
|
|
11003
|
+
inputs.origin = got;
|
|
11004
|
+
continue;
|
|
11005
|
+
}
|
|
10553
11006
|
if (a === "--auth") {
|
|
10554
11007
|
const got = needsValue();
|
|
10555
11008
|
if (typeof got !== "string")
|
|
@@ -10686,18 +11139,7 @@ async function fillRequired(inputs, options, io, prompt) {
|
|
|
10686
11139
|
io.err(` must be one of: ${APP_TYPES2.join(", ")}`);
|
|
10687
11140
|
}
|
|
10688
11141
|
}
|
|
10689
|
-
|
|
10690
|
-
if (inputs.groups.length > 0) {
|
|
10691
|
-
groupsR = inputs.groups;
|
|
10692
|
-
} else {
|
|
10693
|
-
const raw = await askIfMissing(undefined, "allowed_entra_groups (one or more, comma-separated)", (v) => v.length >= 1 ? undefined : "must not be empty");
|
|
10694
|
-
if (typeof raw !== "string")
|
|
10695
|
-
return raw;
|
|
10696
|
-
groupsR = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
10697
|
-
if (groupsR.length === 0) {
|
|
10698
|
-
return { kind: "usage-error", message: "at least one allowed Entra group is required" };
|
|
10699
|
-
}
|
|
10700
|
-
}
|
|
11142
|
+
const groupsR = inputs.groups;
|
|
10701
11143
|
let containerImage = inputs.containerImage;
|
|
10702
11144
|
let containerPort = inputs.containerPort;
|
|
10703
11145
|
if (typeR === "container") {
|
|
@@ -10744,6 +11186,7 @@ async function fillRequired(inputs, options, io, prompt) {
|
|
|
10744
11186
|
description: inputs.description,
|
|
10745
11187
|
type: typeR,
|
|
10746
11188
|
auth: inputs.auth,
|
|
11189
|
+
origin: inputs.origin,
|
|
10747
11190
|
groups: groupsR,
|
|
10748
11191
|
sessionDuration: inputs.sessionDuration,
|
|
10749
11192
|
hostnames: inputs.hostnames,
|
|
@@ -10762,7 +11205,7 @@ function buildManifest(inputs, detected) {
|
|
|
10762
11205
|
metadata.description = inputs.description;
|
|
10763
11206
|
}
|
|
10764
11207
|
const deployment = { type: inputs.type };
|
|
10765
|
-
const access = inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
|
|
11208
|
+
const access = inputs.privateOwner !== undefined ? { audience: "private", private_owner: inputs.privateOwner, allowed_entra_groups: [] } : inputs.groups.length === 1 ? { allowed_entra_group: inputs.groups[0] } : { allowed_entra_groups: [...inputs.groups] };
|
|
10766
11209
|
if (inputs.sessionDuration !== undefined && inputs.sessionDuration !== "") {
|
|
10767
11210
|
access.session_duration = inputs.sessionDuration;
|
|
10768
11211
|
}
|
|
@@ -10771,6 +11214,7 @@ function buildManifest(inputs, detected) {
|
|
|
10771
11214
|
kind: "App",
|
|
10772
11215
|
metadata,
|
|
10773
11216
|
deployment,
|
|
11217
|
+
catalogue: { listed: false },
|
|
10774
11218
|
access
|
|
10775
11219
|
};
|
|
10776
11220
|
const resolvedAuth = inputs.auth ?? (inputs.type === "container" ? "access" : "gateway");
|
|
@@ -10803,6 +11247,33 @@ function buildManifest(inputs, detected) {
|
|
|
10803
11247
|
root: ".",
|
|
10804
11248
|
include: [...defaultIncludeForAppType(inputs.type)]
|
|
10805
11249
|
};
|
|
11250
|
+
if (newAppOrigin(inputs.type, inputs.origin) === "dispatch") {
|
|
11251
|
+
return {
|
|
11252
|
+
apiVersion: "launchpad.m-kopa.us/v2",
|
|
11253
|
+
kind: "App",
|
|
11254
|
+
metadata: { slug: inputs.name, schemaVersion: 2 },
|
|
11255
|
+
spec: {
|
|
11256
|
+
appType: inputs.type,
|
|
11257
|
+
origin: "dispatch",
|
|
11258
|
+
auth: resolvedAuth,
|
|
11259
|
+
display: {
|
|
11260
|
+
name: inputs.name,
|
|
11261
|
+
team: inputs.team,
|
|
11262
|
+
...inputs.description ? { description: inputs.description } : {}
|
|
11263
|
+
},
|
|
11264
|
+
owner: { email: inputs.owner },
|
|
11265
|
+
access: {
|
|
11266
|
+
...inputs.privateOwner === undefined ? {} : { audience: "private", private_owner: inputs.privateOwner },
|
|
11267
|
+
allowed_entra_groups: [...inputs.groups]
|
|
11268
|
+
},
|
|
11269
|
+
catalogue: { listed: false },
|
|
11270
|
+
app: manifest.app,
|
|
11271
|
+
...manifest.build === undefined ? {} : { build: manifest.build }
|
|
11272
|
+
}
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
11275
|
+
if (inputs.origin === "pages")
|
|
11276
|
+
manifest.origin = "pages";
|
|
10806
11277
|
return manifest;
|
|
10807
11278
|
}
|
|
10808
11279
|
function starterIndexHtml(slug) {
|
|
@@ -13242,11 +13713,15 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
13242
13713
|
const cfg = deps.config ?? loadConfig();
|
|
13243
13714
|
const fetcher = deps.fetcher ?? fetch;
|
|
13244
13715
|
const manifestPath = resolvePath4(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
13245
|
-
const loaded =
|
|
13716
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
13246
13717
|
if (loaded.kind !== "ok") {
|
|
13247
13718
|
return renderManifestError4(loaded, io);
|
|
13248
13719
|
}
|
|
13249
|
-
const
|
|
13720
|
+
const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets push", io);
|
|
13721
|
+
if (v2Refusal !== null)
|
|
13722
|
+
return v2Refusal;
|
|
13723
|
+
const manifest = loaded.manifest;
|
|
13724
|
+
const bindings = manifest.secrets?.bindings ?? [];
|
|
13250
13725
|
const envFileBindings = bindings.filter((b) => b.source === "env-file");
|
|
13251
13726
|
if (envFileBindings.length === 0) {
|
|
13252
13727
|
io.out("No `env-file` secret bindings declared in the manifest.");
|
|
@@ -13328,7 +13803,7 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
13328
13803
|
io.out("Re-run without --dry-run to push.");
|
|
13329
13804
|
return 0;
|
|
13330
13805
|
}
|
|
13331
|
-
const slug =
|
|
13806
|
+
const slug = manifest.metadata.name;
|
|
13332
13807
|
let pushed = 0;
|
|
13333
13808
|
let partial = 0;
|
|
13334
13809
|
let anyPagesUpdated = false;
|
|
@@ -13453,6 +13928,20 @@ function mapPushError(e, slug, name, io) {
|
|
|
13453
13928
|
function describe30(e) {
|
|
13454
13929
|
return e instanceof Error ? e.message : String(e);
|
|
13455
13930
|
}
|
|
13931
|
+
function refuseV2Manifest(loaded, command, io) {
|
|
13932
|
+
if (loaded.schema !== "launchpad.m-kopa.us/v2")
|
|
13933
|
+
return null;
|
|
13934
|
+
const origin = loaded.manifest.spec?.origin;
|
|
13935
|
+
if (origin === "dispatch") {
|
|
13936
|
+
io.err(`${command}: this is a dispatch-origin (v2) app.`);
|
|
13937
|
+
const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
|
|
13938
|
+
io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
|
|
13939
|
+
return 1;
|
|
13940
|
+
}
|
|
13941
|
+
io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
|
|
13942
|
+
io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
|
|
13943
|
+
return 1;
|
|
13944
|
+
}
|
|
13456
13945
|
function renderManifestError4(result, io) {
|
|
13457
13946
|
switch (result.kind) {
|
|
13458
13947
|
case "ok":
|
|
@@ -13490,16 +13979,20 @@ async function runSecretsStatus(opts, io, deps = {}) {
|
|
|
13490
13979
|
const cfg = deps.config ?? loadConfig();
|
|
13491
13980
|
const fetcher = deps.fetcher ?? fetch;
|
|
13492
13981
|
const manifestPath = resolvePath5(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
13493
|
-
const loaded =
|
|
13982
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
13494
13983
|
if (loaded.kind !== "ok")
|
|
13495
13984
|
return renderManifestError5(loaded, io);
|
|
13496
|
-
const
|
|
13985
|
+
const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets status", io);
|
|
13986
|
+
if (v2Refusal !== null)
|
|
13987
|
+
return v2Refusal;
|
|
13988
|
+
const manifest = loaded.manifest;
|
|
13989
|
+
const slug = manifest.metadata.name;
|
|
13497
13990
|
if (opts.slug != null && opts.slug !== slug) {
|
|
13498
13991
|
io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
|
|
13499
13992
|
io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
|
|
13500
13993
|
return 2;
|
|
13501
13994
|
}
|
|
13502
|
-
const bindings =
|
|
13995
|
+
const bindings = manifest.secrets?.bindings ?? [];
|
|
13503
13996
|
if (bindings.length === 0) {
|
|
13504
13997
|
io.out("No secret bindings declared in the manifest.");
|
|
13505
13998
|
return 0;
|
|
@@ -14867,7 +15360,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync
|
|
|
14867
15360
|
import { dirname as dirname12, resolve as resolve17 } from "node:path";
|
|
14868
15361
|
var validateCommand = {
|
|
14869
15362
|
name: "validate",
|
|
14870
|
-
summary: "validate launchpad.yaml against
|
|
15363
|
+
summary: "validate launchpad.yaml against its v1alpha1 or v2 schema",
|
|
14871
15364
|
run: runValidate
|
|
14872
15365
|
};
|
|
14873
15366
|
async function runValidate(args, io) {
|
|
@@ -14879,7 +15372,7 @@ async function runValidate(args, io) {
|
|
|
14879
15372
|
}
|
|
14880
15373
|
const { file, json, strictGroups } = parseResult;
|
|
14881
15374
|
const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
|
|
14882
|
-
const result =
|
|
15375
|
+
const result = loadManifestAnyVersion(path14);
|
|
14883
15376
|
if (result.kind !== "ok") {
|
|
14884
15377
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
14885
15378
|
}
|
|
@@ -14889,16 +15382,17 @@ async function runValidate(args, io) {
|
|
|
14889
15382
|
manifest: result.manifest,
|
|
14890
15383
|
io,
|
|
14891
15384
|
json,
|
|
14892
|
-
structurallyValidName: result.manifest.
|
|
15385
|
+
structurallyValidName: manifestView(result.manifest).name
|
|
14893
15386
|
});
|
|
14894
15387
|
if (unsupported2 !== null)
|
|
14895
15388
|
return unsupported2;
|
|
14896
|
-
const
|
|
15389
|
+
const view = manifestView(result.manifest);
|
|
15390
|
+
const boundary = checkBoundary(path14, view.appDeclared);
|
|
14897
15391
|
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
14898
15392
|
if (serveErrors.length > 0) {
|
|
14899
15393
|
boundary.errors = [...boundary.errors, ...serveErrors];
|
|
14900
15394
|
}
|
|
14901
|
-
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(
|
|
15395
|
+
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(view.access)) : { kind: "skipped" };
|
|
14902
15396
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
14903
15397
|
}
|
|
14904
15398
|
function checkBoundary(manifestPath, declared) {
|
|
@@ -14922,11 +15416,12 @@ function checkBoundary(manifestPath, declared) {
|
|
|
14922
15416
|
return { declared, errors: [], warnings: r.warnings };
|
|
14923
15417
|
}
|
|
14924
15418
|
function checkStaticServeDir(manifestPath, manifest) {
|
|
14925
|
-
|
|
15419
|
+
const view = manifestView(manifest);
|
|
15420
|
+
if (view.deploymentType !== "static")
|
|
14926
15421
|
return [];
|
|
14927
|
-
if (
|
|
15422
|
+
if (view.buildCommand !== undefined)
|
|
14928
15423
|
return [];
|
|
14929
|
-
const serveDir =
|
|
15424
|
+
const serveDir = view.destinationDir ?? STATIC_SERVE_DIR;
|
|
14930
15425
|
const normalized = serveDir.replace(/\\/g, "/");
|
|
14931
15426
|
const segments = normalized.split("/");
|
|
14932
15427
|
if (normalized === "" || normalized === "." || normalized === "./" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.some((s) => s === "..")) {
|
|
@@ -15026,9 +15521,10 @@ async function checkGroups(groups) {
|
|
|
15026
15521
|
return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
|
|
15027
15522
|
}
|
|
15028
15523
|
function renderHumanOk(result, group, boundary, io) {
|
|
15029
|
-
|
|
15030
|
-
io.out(
|
|
15031
|
-
io.out(`
|
|
15524
|
+
const view = manifestView(result.manifest);
|
|
15525
|
+
io.out(`✓ ${view.name} — manifest is valid`);
|
|
15526
|
+
io.out(` apiVersion: ${result.schema}`);
|
|
15527
|
+
io.out(` type: ${view.deploymentType}`);
|
|
15032
15528
|
if (boundary.errors.length > 0) {
|
|
15033
15529
|
for (const e of boundary.errors) {
|
|
15034
15530
|
io.err(`✗ app boundary: ${e}`);
|
|
@@ -15097,6 +15593,7 @@ function renderHumanError(result, io) {
|
|
|
15097
15593
|
return 1;
|
|
15098
15594
|
case "schema-error":
|
|
15099
15595
|
io.err(`✗ ${result.path}`);
|
|
15596
|
+
io.err(` validated against schema: ${result.schema}`);
|
|
15100
15597
|
io.err(` ${result.issues.length} schema issue${result.issues.length === 1 ? "" : "s"}:`);
|
|
15101
15598
|
for (const issue of result.issues) {
|
|
15102
15599
|
io.err(` ${issue.path}: ${issue.message}`);
|
|
@@ -15105,11 +15602,12 @@ function renderHumanError(result, io) {
|
|
|
15105
15602
|
}
|
|
15106
15603
|
}
|
|
15107
15604
|
function renderJsonOk(result, group, boundary, io) {
|
|
15605
|
+
const view = manifestView(result.manifest);
|
|
15108
15606
|
const base = {
|
|
15109
15607
|
manifest_valid: true,
|
|
15110
15608
|
runtime_supported: true,
|
|
15111
|
-
name:
|
|
15112
|
-
deploymentType:
|
|
15609
|
+
name: view.name,
|
|
15610
|
+
deploymentType: view.deploymentType,
|
|
15113
15611
|
appBoundary: {
|
|
15114
15612
|
declared: boundary.declared,
|
|
15115
15613
|
errors: boundary.errors,
|
|
@@ -15189,11 +15687,32 @@ function renderJsonError(result, io) {
|
|
|
15189
15687
|
ok: false,
|
|
15190
15688
|
kind: "schema-error",
|
|
15191
15689
|
path: result.path,
|
|
15690
|
+
schema: result.schema,
|
|
15192
15691
|
issues: result.issues
|
|
15193
15692
|
}));
|
|
15194
15693
|
return 1;
|
|
15195
15694
|
}
|
|
15196
15695
|
}
|
|
15696
|
+
function manifestView(manifest) {
|
|
15697
|
+
if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
|
|
15698
|
+
return {
|
|
15699
|
+
name: manifest.metadata.slug,
|
|
15700
|
+
deploymentType: manifest.spec.appType,
|
|
15701
|
+
access: manifest.spec.access,
|
|
15702
|
+
buildCommand: manifest.spec.build?.command,
|
|
15703
|
+
destinationDir: manifest.spec.build?.destination_dir,
|
|
15704
|
+
appDeclared: manifest.spec.app !== undefined
|
|
15705
|
+
};
|
|
15706
|
+
}
|
|
15707
|
+
return {
|
|
15708
|
+
name: manifest.metadata.name,
|
|
15709
|
+
deploymentType: manifest.deployment.type,
|
|
15710
|
+
access: manifest.access,
|
|
15711
|
+
buildCommand: manifest.build?.command,
|
|
15712
|
+
destinationDir: manifest.build?.destination_dir,
|
|
15713
|
+
appDeclared: manifest.app !== undefined
|
|
15714
|
+
};
|
|
15715
|
+
}
|
|
15197
15716
|
function parseArgs13(args) {
|
|
15198
15717
|
let file;
|
|
15199
15718
|
let json = false;
|