@m-kopa/launchpad-cli 0.56.1 → 0.60.1
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 +84 -0
- package/dist/bundle/upload.d.ts +2 -0
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +656 -147
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- 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 +1 -1
- package/skills/launchpad-deploy/SKILL.md +32 -10
- 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.60.1";
|
|
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") {
|
|
@@ -2570,6 +2573,12 @@ import { parse as parseYaml2 } from "yaml";
|
|
|
2570
2573
|
import { z } from "zod";
|
|
2571
2574
|
var APP_TYPES2 = ["static", "react", "react+api", "container"];
|
|
2572
2575
|
var AUTH_MODES = ["access", "gateway"];
|
|
2576
|
+
var IDENTITY_TYPES = ["employee", "dsr"];
|
|
2577
|
+
var IdentityTypesSchema = z.array(z.enum(IDENTITY_TYPES)).min(1).refine((values) => new Set(values).size === values.length, {
|
|
2578
|
+
message: "identity_types must not contain duplicates"
|
|
2579
|
+
}).refine((values) => values.includes("employee"), {
|
|
2580
|
+
message: "identity_types must include employee"
|
|
2581
|
+
});
|
|
2573
2582
|
var RUNTIMES = ["cloudflare-containers", "aca"];
|
|
2574
2583
|
var SECRET_SOURCES = ["env-file", "platform-managed"];
|
|
2575
2584
|
var TARGET_KINDS = ["pages", "worker"];
|
|
@@ -2590,7 +2599,8 @@ var DeploymentSchema = z.object({
|
|
|
2590
2599
|
var AccessSchema = z.object({
|
|
2591
2600
|
allowed_entra_group: z.string().min(1).optional(),
|
|
2592
2601
|
allowed_entra_groups: z.array(z.string().min(1)).min(1).refine((arr) => new Set(arr).size === arr.length, { message: "allowed_entra_groups must not contain duplicates" }).optional(),
|
|
2593
|
-
session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
|
|
2602
|
+
session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional(),
|
|
2603
|
+
identity_types: IdentityTypesSchema.optional()
|
|
2594
2604
|
}).strict().superRefine((access, ctx) => {
|
|
2595
2605
|
const hasSingular = access.allowed_entra_group !== undefined;
|
|
2596
2606
|
const hasPlural = access.allowed_entra_groups !== undefined;
|
|
@@ -2678,6 +2688,13 @@ var ManifestSchema = z.object({
|
|
|
2678
2688
|
sensitive: z.boolean().optional()
|
|
2679
2689
|
}).strict().superRefine((m, ctx) => {
|
|
2680
2690
|
const isContainer = m.deployment.type === "container";
|
|
2691
|
+
if (m.access.identity_types !== undefined && (m.auth ?? "access") !== "gateway") {
|
|
2692
|
+
ctx.addIssue({
|
|
2693
|
+
code: z.ZodIssueCode.custom,
|
|
2694
|
+
path: ["access", "identity_types"],
|
|
2695
|
+
message: "access.identity_types is only valid with auth: gateway"
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2681
2698
|
if (m.confine_origin !== undefined) {
|
|
2682
2699
|
if (isContainer) {
|
|
2683
2700
|
ctx.addIssue({
|
|
@@ -3302,7 +3319,8 @@ var SpecSchema = z2.object({
|
|
|
3302
3319
|
allowed_entra_groups: z2.array(z2.string().min(1)).min(1).refine((a) => new Set(a).size === a.length, {
|
|
3303
3320
|
message: "allowed_entra_groups must not contain duplicates"
|
|
3304
3321
|
}),
|
|
3305
|
-
require_email_domain: z2.string().min(1).optional()
|
|
3322
|
+
require_email_domain: z2.string().min(1).optional(),
|
|
3323
|
+
identity_types: IdentityTypesSchema.optional()
|
|
3306
3324
|
}).strict(),
|
|
3307
3325
|
auth: z2.enum(AUTH_MODES).optional(),
|
|
3308
3326
|
build: BuildSchema.optional(),
|
|
@@ -3317,6 +3335,13 @@ var SpecSchema = z2.object({
|
|
|
3317
3335
|
secrets: SecretsSchema.optional(),
|
|
3318
3336
|
fronted_by_concierge: z2.boolean().optional()
|
|
3319
3337
|
}).strict().superRefine((s, ctx) => {
|
|
3338
|
+
if (s.access.identity_types !== undefined && (s.auth ?? "access") !== "gateway") {
|
|
3339
|
+
ctx.addIssue({
|
|
3340
|
+
code: z2.ZodIssueCode.custom,
|
|
3341
|
+
path: ["access", "identity_types"],
|
|
3342
|
+
message: "access.identity_types is only valid with auth: gateway"
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3320
3345
|
for (const [k, v] of Object.entries(s.env_vars ?? {})) {
|
|
3321
3346
|
if (/_(TOKEN|SECRET)$/.test(k) && v.type !== "secret_text") {
|
|
3322
3347
|
ctx.addIssue({
|
|
@@ -3377,19 +3402,14 @@ var SpecSchema = z2.object({
|
|
|
3377
3402
|
message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
|
|
3378
3403
|
});
|
|
3379
3404
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
ctx.addIssue({
|
|
3389
|
-
code: z2.ZodIssueCode.custom,
|
|
3390
|
-
path: ["build"],
|
|
3391
|
-
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."
|
|
3392
|
-
});
|
|
3405
|
+
for (const [name, envVar] of Object.entries(s.env_vars ?? {})) {
|
|
3406
|
+
if (envVar.type === "secret_text") {
|
|
3407
|
+
ctx.addIssue({
|
|
3408
|
+
code: z2.ZodIssueCode.custom,
|
|
3409
|
+
path: ["env_vars", name],
|
|
3410
|
+
message: "secret_text env vars are not supported with origin: dispatch until PS-2740 (secrets on v2) provides their runtime consumer."
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3393
3413
|
}
|
|
3394
3414
|
}
|
|
3395
3415
|
if (!isContainer && s.containers !== undefined) {
|
|
@@ -4381,6 +4401,21 @@ function membersForSecret(set, secret) {
|
|
|
4381
4401
|
}
|
|
4382
4402
|
// ../launchpad-engine/dist/redact-status.js
|
|
4383
4403
|
var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
|
|
4404
|
+
// ../launchpad-engine/dist/deployment-auth.js
|
|
4405
|
+
function deploymentAuthIssue(appType, auth) {
|
|
4406
|
+
if (appType === "container" || auth === "gateway")
|
|
4407
|
+
return null;
|
|
4408
|
+
return {
|
|
4409
|
+
error: "pages_access_retired",
|
|
4410
|
+
message: "Cloudflare Access login is retired for Pages apps. Set auth: gateway (spec.auth in v2). Containers are unchanged."
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
// ../launchpad-engine/dist/new-app-origin.js
|
|
4414
|
+
function newAppOrigin(appType, requested) {
|
|
4415
|
+
if (appType === "container")
|
|
4416
|
+
return;
|
|
4417
|
+
return requested ?? (appType === "react+api" ? "pages" : "dispatch");
|
|
4418
|
+
}
|
|
4384
4419
|
// src/bundle/boundary.ts
|
|
4385
4420
|
var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
|
|
4386
4421
|
function applyBoundaryToFiles(args) {
|
|
@@ -4528,7 +4563,7 @@ async function bundleAndDeploy(args) {
|
|
|
4528
4563
|
}
|
|
4529
4564
|
|
|
4530
4565
|
// src/deploy/verify-deploy.ts
|
|
4531
|
-
import { readFileSync as
|
|
4566
|
+
import { readFileSync as readFileSync10, existsSync as existsSync4 } from "node:fs";
|
|
4532
4567
|
import { join as join9 } from "node:path";
|
|
4533
4568
|
|
|
4534
4569
|
// src/deploy/redact-diagnostic.ts
|
|
@@ -4574,11 +4609,67 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
|
4574
4609
|
return {
|
|
4575
4610
|
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
4576
4611
|
supported: raw.supported,
|
|
4612
|
+
...raw.origin === "pages" || raw.origin === "dispatch" ? { origin: raw.origin } : {},
|
|
4613
|
+
...projectV2(raw.v2),
|
|
4577
4614
|
liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
|
|
4578
4615
|
lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
|
|
4579
4616
|
...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
|
|
4580
4617
|
};
|
|
4581
4618
|
}
|
|
4619
|
+
function projectV2(value) {
|
|
4620
|
+
if (value === null || typeof value !== "object")
|
|
4621
|
+
return {};
|
|
4622
|
+
const raw = value;
|
|
4623
|
+
if (!isV2State(raw.state) || !isV2Stage(raw.stage) || typeof raw.commitSha !== "string" || typeof raw.attempts !== "number" || !Number.isFinite(raw.attempts) || typeof raw.updatedAt !== "string") {
|
|
4624
|
+
return {};
|
|
4625
|
+
}
|
|
4626
|
+
if (!isConsistentV2State(raw.state, raw.stage))
|
|
4627
|
+
return {};
|
|
4628
|
+
if (raw.failedAt !== undefined && !isV2FailedStage(raw.failedAt))
|
|
4629
|
+
return {};
|
|
4630
|
+
const v2 = {
|
|
4631
|
+
state: raw.state,
|
|
4632
|
+
stage: raw.stage,
|
|
4633
|
+
commitSha: raw.commitSha,
|
|
4634
|
+
attempts: raw.attempts,
|
|
4635
|
+
updatedAt: raw.updatedAt,
|
|
4636
|
+
...raw.failedAt !== undefined ? { failedAt: raw.failedAt } : {},
|
|
4637
|
+
...typeof raw.reason === "string" ? { reason: redactDiagnostic(raw.reason) } : {},
|
|
4638
|
+
...typeof raw.detail === "string" ? { detail: redactDiagnostic(raw.detail) } : {},
|
|
4639
|
+
...projectRunUrl(raw.runUrl),
|
|
4640
|
+
...typeof raw.deliveredAt === "string" ? { deliveredAt: raw.deliveredAt } : {}
|
|
4641
|
+
};
|
|
4642
|
+
return { v2 };
|
|
4643
|
+
}
|
|
4644
|
+
function projectRunUrl(value) {
|
|
4645
|
+
if (typeof value !== "string")
|
|
4646
|
+
return {};
|
|
4647
|
+
try {
|
|
4648
|
+
const url = new URL(value);
|
|
4649
|
+
return url.protocol === "https:" && url.hostname === "github.com" ? { runUrl: value } : {};
|
|
4650
|
+
} catch {
|
|
4651
|
+
return {};
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
function isV2State(value) {
|
|
4655
|
+
return value === "pending" || value === "delivered" || value === "refused";
|
|
4656
|
+
}
|
|
4657
|
+
function isV2Stage(value) {
|
|
4658
|
+
return ["committed", "building", "verified", "uploading", "live", "failed"].includes(String(value));
|
|
4659
|
+
}
|
|
4660
|
+
function isConsistentV2State(state, stage) {
|
|
4661
|
+
switch (state) {
|
|
4662
|
+
case "delivered":
|
|
4663
|
+
return stage === "live";
|
|
4664
|
+
case "refused":
|
|
4665
|
+
return stage === "failed";
|
|
4666
|
+
case "pending":
|
|
4667
|
+
return stage !== "live" && stage !== "failed";
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
function isV2FailedStage(value) {
|
|
4671
|
+
return ["build", "verify", "download", "expand", "manifest", "plan", "upload"].includes(String(value));
|
|
4672
|
+
}
|
|
4582
4673
|
function projectLastSuccessful(value) {
|
|
4583
4674
|
if (value === null || typeof value !== "object")
|
|
4584
4675
|
return null;
|
|
@@ -4628,20 +4719,27 @@ function shaMatches(a, b) {
|
|
|
4628
4719
|
return x === y;
|
|
4629
4720
|
return x.slice(0, n) === y.slice(0, n);
|
|
4630
4721
|
}
|
|
4631
|
-
async function
|
|
4722
|
+
async function readDeploymentContext(deps, slug) {
|
|
4632
4723
|
let status;
|
|
4633
4724
|
try {
|
|
4634
4725
|
status = await deps.fetchStatus(slug);
|
|
4635
4726
|
} catch {
|
|
4636
|
-
return null;
|
|
4727
|
+
return { baseline: null };
|
|
4728
|
+
}
|
|
4729
|
+
if (status === null)
|
|
4730
|
+
return { baseline: null };
|
|
4731
|
+
const origin = status.origin;
|
|
4732
|
+
if (!status.supported || status.liveDeployment === null) {
|
|
4733
|
+
return { baseline: null, ...origin !== undefined ? { origin } : {} };
|
|
4637
4734
|
}
|
|
4638
|
-
if (status === null || !status.supported || status.liveDeployment === null)
|
|
4639
|
-
return null;
|
|
4640
4735
|
const live = status.liveDeployment;
|
|
4641
4736
|
return {
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4737
|
+
baseline: {
|
|
4738
|
+
id: live.id,
|
|
4739
|
+
commitHash: live.commit?.hash ?? null,
|
|
4740
|
+
createdOn: live.createdOn
|
|
4741
|
+
},
|
|
4742
|
+
...origin !== undefined ? { origin } : {}
|
|
4645
4743
|
};
|
|
4646
4744
|
}
|
|
4647
4745
|
async function waitForCommitDeployment(args, deps) {
|
|
@@ -4649,6 +4747,7 @@ async function waitForCommitDeployment(args, deps) {
|
|
|
4649
4747
|
const start = deps.now();
|
|
4650
4748
|
const baselineId = args.baseline?.id ?? null;
|
|
4651
4749
|
let matchedInProgress = false;
|
|
4750
|
+
let lastV2Stage = null;
|
|
4652
4751
|
for (;; ) {
|
|
4653
4752
|
let status;
|
|
4654
4753
|
try {
|
|
@@ -4662,6 +4761,38 @@ async function waitForCommitDeployment(args, deps) {
|
|
|
4662
4761
|
return { kind: "unsupported" };
|
|
4663
4762
|
const live = status.liveDeployment;
|
|
4664
4763
|
const elapsed = deps.now() - start;
|
|
4764
|
+
if (status.origin === "dispatch" && status.v2 !== undefined && shaMatches(status.v2.commitSha, args.shippedSha)) {
|
|
4765
|
+
const delivery = status.v2;
|
|
4766
|
+
if (delivery.stage !== lastV2Stage) {
|
|
4767
|
+
args.onV2Stage?.(delivery);
|
|
4768
|
+
lastV2Stage = delivery.stage;
|
|
4769
|
+
}
|
|
4770
|
+
if (delivery.state === "refused") {
|
|
4771
|
+
return {
|
|
4772
|
+
kind: "failure",
|
|
4773
|
+
dispatch: true,
|
|
4774
|
+
failedStage: delivery.failedAt ?? delivery.stage,
|
|
4775
|
+
reason: delivery.reason,
|
|
4776
|
+
detail: delivery.detail,
|
|
4777
|
+
runUrl: delivery.runUrl
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
if (delivery.state === "delivered" || delivery.stage === "live") {
|
|
4781
|
+
return {
|
|
4782
|
+
kind: "success",
|
|
4783
|
+
deploymentUrl: live?.url ?? null,
|
|
4784
|
+
v2: {
|
|
4785
|
+
commitSha: delivery.commitSha,
|
|
4786
|
+
...delivery.deliveredAt !== undefined ? { deliveredAt: delivery.deliveredAt } : {}
|
|
4787
|
+
}
|
|
4788
|
+
};
|
|
4789
|
+
}
|
|
4790
|
+
matchedInProgress = true;
|
|
4791
|
+
if (elapsed >= args.settleTimeoutMs)
|
|
4792
|
+
return { kind: "pending" };
|
|
4793
|
+
await deps.sleep(pollIntervalMs);
|
|
4794
|
+
continue;
|
|
4795
|
+
}
|
|
4665
4796
|
if (live !== null && shaMatches(live.commit?.hash, args.shippedSha)) {
|
|
4666
4797
|
if (live.buildStatus === "success") {
|
|
4667
4798
|
return { kind: "success", deploymentUrl: live.url };
|
|
@@ -4773,7 +4904,7 @@ async function verifyServed(args, deps = { fetch }) {
|
|
|
4773
4904
|
}
|
|
4774
4905
|
|
|
4775
4906
|
// src/commands/status.ts
|
|
4776
|
-
import { readFileSync as
|
|
4907
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4777
4908
|
|
|
4778
4909
|
// src/deploy/manifest-state.ts
|
|
4779
4910
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
@@ -5582,6 +5713,88 @@ still provisioning after the watch window — re-run \`launchpad watch ${slug}\`
|
|
|
5582
5713
|
}
|
|
5583
5714
|
}
|
|
5584
5715
|
|
|
5716
|
+
// src/manifest/load.ts
|
|
5717
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5718
|
+
import { parse as parseYaml4, YAMLParseError } from "yaml";
|
|
5719
|
+
function loadManifest(path8) {
|
|
5720
|
+
let raw;
|
|
5721
|
+
try {
|
|
5722
|
+
raw = readFileSync8(path8, "utf8");
|
|
5723
|
+
} catch (err) {
|
|
5724
|
+
const e = err;
|
|
5725
|
+
if (e.code === "ENOENT") {
|
|
5726
|
+
return { kind: "file-not-found", path: path8 };
|
|
5727
|
+
}
|
|
5728
|
+
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
5729
|
+
}
|
|
5730
|
+
return parseManifest2(raw, path8);
|
|
5731
|
+
}
|
|
5732
|
+
function loadManifestAnyVersion(path8) {
|
|
5733
|
+
let raw;
|
|
5734
|
+
try {
|
|
5735
|
+
raw = readFileSync8(path8, "utf8");
|
|
5736
|
+
} catch (err) {
|
|
5737
|
+
const e = err;
|
|
5738
|
+
if (e.code === "ENOENT")
|
|
5739
|
+
return { kind: "file-not-found", path: path8 };
|
|
5740
|
+
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
5741
|
+
}
|
|
5742
|
+
return parseManifestAnyVersion(raw, path8);
|
|
5743
|
+
}
|
|
5744
|
+
function parseManifestAnyVersion(yamlText, path8) {
|
|
5745
|
+
let parsed;
|
|
5746
|
+
try {
|
|
5747
|
+
parsed = parseYaml4(yamlText);
|
|
5748
|
+
} catch (err) {
|
|
5749
|
+
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
5750
|
+
return { kind: "yaml-parse-error", path: path8, message };
|
|
5751
|
+
}
|
|
5752
|
+
const apiVersion = typeof parsed === "object" && parsed !== null ? parsed.apiVersion : undefined;
|
|
5753
|
+
if (apiVersion === "launchpad.m-kopa.us/v2") {
|
|
5754
|
+
const result = parseManifestV2(parsed);
|
|
5755
|
+
return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
|
|
5756
|
+
}
|
|
5757
|
+
if (apiVersion === "launchpad.m-kopa.us/v1alpha1") {
|
|
5758
|
+
const result = parseManifest(parsed);
|
|
5759
|
+
return result.kind === "ok" ? { kind: "ok", manifest: result.manifest, schema: apiVersion } : { kind: "schema-error", path: path8, schema: apiVersion, issues: [...result.issues] };
|
|
5760
|
+
}
|
|
5761
|
+
return {
|
|
5762
|
+
kind: "schema-error",
|
|
5763
|
+
path: path8,
|
|
5764
|
+
schema: "launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2",
|
|
5765
|
+
issues: [
|
|
5766
|
+
{
|
|
5767
|
+
path: "apiVersion",
|
|
5768
|
+
message: `unsupported apiVersion ${JSON.stringify(apiVersion)}; expected launchpad.m-kopa.us/v1alpha1 or launchpad.m-kopa.us/v2`
|
|
5769
|
+
}
|
|
5770
|
+
]
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
function parseManifest2(yamlText, path8) {
|
|
5774
|
+
let parsed;
|
|
5775
|
+
try {
|
|
5776
|
+
parsed = parseYaml4(yamlText);
|
|
5777
|
+
} catch (err) {
|
|
5778
|
+
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
5779
|
+
return { kind: "yaml-parse-error", path: path8, message };
|
|
5780
|
+
}
|
|
5781
|
+
const result = ManifestSchema.safeParse(parsed);
|
|
5782
|
+
if (result.success) {
|
|
5783
|
+
return { kind: "ok", manifest: result.data };
|
|
5784
|
+
}
|
|
5785
|
+
return {
|
|
5786
|
+
kind: "schema-error",
|
|
5787
|
+
path: path8,
|
|
5788
|
+
issues: formatIssues(result.error)
|
|
5789
|
+
};
|
|
5790
|
+
}
|
|
5791
|
+
function formatIssues(error) {
|
|
5792
|
+
return error.issues.map((issue) => ({
|
|
5793
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
5794
|
+
message: issue.message
|
|
5795
|
+
}));
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5585
5798
|
// src/commands/status.ts
|
|
5586
5799
|
var statusCommand = {
|
|
5587
5800
|
name: "status",
|
|
@@ -5677,7 +5890,7 @@ async function runStatus(args, io) {
|
|
|
5677
5890
|
}
|
|
5678
5891
|
let localYaml = null;
|
|
5679
5892
|
try {
|
|
5680
|
-
localYaml =
|
|
5893
|
+
localYaml = readFileSync9(parsed.file, "utf8");
|
|
5681
5894
|
} catch (e) {
|
|
5682
5895
|
if (!isEnoent(e)) {
|
|
5683
5896
|
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe12(e)}`);
|
|
@@ -5686,24 +5899,24 @@ async function runStatus(args, io) {
|
|
|
5686
5899
|
}
|
|
5687
5900
|
let local = null;
|
|
5688
5901
|
if (localYaml !== null) {
|
|
5689
|
-
|
|
5690
|
-
try {
|
|
5691
|
-
const { parse: parseYaml4 } = await import("yaml");
|
|
5692
|
-
localObj = parseYaml4(localYaml);
|
|
5693
|
-
} catch (e) {
|
|
5694
|
-
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe12(e)}`);
|
|
5695
|
-
return 2;
|
|
5696
|
-
}
|
|
5697
|
-
const localParse = parseManifest(localObj);
|
|
5902
|
+
const localParse = parseManifestAnyVersion(localYaml, parsed.file);
|
|
5698
5903
|
if (localParse.kind !== "ok") {
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5904
|
+
if (localParse.kind === "yaml-parse-error") {
|
|
5905
|
+
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${localParse.message}`);
|
|
5906
|
+
} else if (localParse.kind === "schema-error") {
|
|
5907
|
+
io.err(`launchpad status: ${parsed.file} failed schema validation against ${localParse.schema}:`);
|
|
5908
|
+
for (const issue of localParse.issues) {
|
|
5909
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
5910
|
+
}
|
|
5911
|
+
} else {
|
|
5912
|
+
io.err(`launchpad status: cannot read local manifest at ${parsed.file}`);
|
|
5702
5913
|
}
|
|
5703
5914
|
return 2;
|
|
5704
5915
|
}
|
|
5705
5916
|
local = localParse.manifest;
|
|
5706
|
-
|
|
5917
|
+
if (local.apiVersion === "launchpad.m-kopa.us/v1alpha1") {
|
|
5918
|
+
warnSecretShape(local.production_env, io);
|
|
5919
|
+
}
|
|
5707
5920
|
}
|
|
5708
5921
|
let state;
|
|
5709
5922
|
try {
|
|
@@ -5733,9 +5946,9 @@ async function runStatus(args, io) {
|
|
|
5733
5946
|
}
|
|
5734
5947
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
5735
5948
|
const live = deployment?.liveDeployment ?? null;
|
|
5736
|
-
const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
5949
|
+
const contentIsLive = deployment?.v2?.state === "delivered" || deployment?.v2?.stage === "live" || live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
5737
5950
|
const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
|
|
5738
|
-
const platformTracked = state.hasAppFile === true || live?.trigger === "bot";
|
|
5951
|
+
const platformTracked = state.hasAppFile === true || live?.trigger === "bot" || deployment?.origin === "dispatch";
|
|
5739
5952
|
const out = {
|
|
5740
5953
|
state: contentIsLive ? platformTracked ? "live_drift_unknown" : "live_content_untracked" : platformTracked && !deploymentKnown ? "live_drift_unknown" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
5741
5954
|
...local === null ? { drift: null } : {},
|
|
@@ -5768,7 +5981,7 @@ async function runStatus(args, io) {
|
|
|
5768
5981
|
};
|
|
5769
5982
|
emit4(out, parsed.json, io);
|
|
5770
5983
|
if (parsed.strict) {
|
|
5771
|
-
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
5984
|
+
if (deployment?.liveDeployment?.buildStatus === "failure" || deployment?.v2?.state === "refused") {
|
|
5772
5985
|
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
5773
5986
|
return 1;
|
|
5774
5987
|
}
|
|
@@ -5776,26 +5989,40 @@ async function runStatus(args, io) {
|
|
|
5776
5989
|
}
|
|
5777
5990
|
return 0;
|
|
5778
5991
|
}
|
|
5779
|
-
|
|
5780
|
-
try {
|
|
5781
|
-
const { parse: parseYaml4 } = await import("yaml");
|
|
5782
|
-
deployedObj = parseYaml4(state.manifestYaml);
|
|
5783
|
-
} catch (e) {
|
|
5784
|
-
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${describe12(e)}`);
|
|
5785
|
-
return 2;
|
|
5786
|
-
}
|
|
5787
|
-
const deployedParse = parseManifest(deployedObj);
|
|
5992
|
+
const deployedParse = parseManifestAnyVersion(state.manifestYaml, `deployed manifest at ${state.deployedManifestSha}`);
|
|
5788
5993
|
if (deployedParse.kind !== "ok") {
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5994
|
+
if (deployedParse.kind === "yaml-parse-error") {
|
|
5995
|
+
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} is not valid YAML: ${deployedParse.message}`);
|
|
5996
|
+
} else if (deployedParse.kind === "schema-error") {
|
|
5997
|
+
io.err(`launchpad status: deployed manifest at ${state.deployedManifestSha} failed schema validation against ${deployedParse.schema}:`);
|
|
5998
|
+
for (const issue of deployedParse.issues) {
|
|
5999
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
6000
|
+
}
|
|
5792
6001
|
}
|
|
5793
6002
|
return 2;
|
|
5794
6003
|
}
|
|
5795
6004
|
const deployed = deployedParse.manifest;
|
|
6005
|
+
if (local.apiVersion !== deployed.apiVersion) {
|
|
6006
|
+
const reason = `local manifest uses ${local.apiVersion}, while the deployed manifest uses ${deployed.apiVersion}; ` + "the manifest shapes differ, so drift was not evaluated";
|
|
6007
|
+
const out = {
|
|
6008
|
+
state: "live_drift_unknown",
|
|
6009
|
+
drift: null,
|
|
6010
|
+
driftUnavailableReason: reason,
|
|
6011
|
+
...statusTruthFields({
|
|
6012
|
+
slug: parsed.slug,
|
|
6013
|
+
deployedSha: state.deployedManifestSha,
|
|
6014
|
+
headSha: state.appRepoHeadSha,
|
|
6015
|
+
openPrNumber: state.openPr?.number ?? null,
|
|
6016
|
+
...deploymentKnown ? { deployment } : {},
|
|
6017
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
6018
|
+
})
|
|
6019
|
+
};
|
|
6020
|
+
emit4(out, parsed.json, io);
|
|
6021
|
+
return 0;
|
|
6022
|
+
}
|
|
5796
6023
|
const localGroups = await resolveStatusGroups({
|
|
5797
6024
|
cfg,
|
|
5798
|
-
refs: allowedEntraGroups(local
|
|
6025
|
+
refs: allowedEntraGroups(manifestAccess(local)),
|
|
5799
6026
|
source: "local manifest",
|
|
5800
6027
|
manifestSha: null
|
|
5801
6028
|
});
|
|
@@ -5804,14 +6031,17 @@ async function runStatus(args, io) {
|
|
|
5804
6031
|
}
|
|
5805
6032
|
const deployedGroups = await resolveStatusGroups({
|
|
5806
6033
|
cfg,
|
|
5807
|
-
refs: allowedEntraGroups(deployed
|
|
6034
|
+
refs: allowedEntraGroups(manifestAccess(deployed)),
|
|
5808
6035
|
source: "deployed manifest",
|
|
5809
6036
|
manifestSha: state.deployedManifestSha
|
|
5810
6037
|
});
|
|
5811
6038
|
if (deployedGroups.kind === "error") {
|
|
5812
6039
|
return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
|
|
5813
6040
|
}
|
|
5814
|
-
const drift =
|
|
6041
|
+
const drift = local.apiVersion === "launchpad.m-kopa.us/v2" && deployed.apiVersion === "launchpad.m-kopa.us/v2" ? computeV2SpecDrift(local, deployed, {
|
|
6042
|
+
local: localGroups.ids,
|
|
6043
|
+
deployed: deployedGroups.ids
|
|
6044
|
+
}) : computeDrift(local, deployed, {
|
|
5815
6045
|
local: localGroups.ids,
|
|
5816
6046
|
deployed: deployedGroups.ids
|
|
5817
6047
|
});
|
|
@@ -5828,8 +6058,8 @@ async function runStatus(args, io) {
|
|
|
5828
6058
|
}
|
|
5829
6059
|
accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
|
|
5830
6060
|
}
|
|
5831
|
-
const localAssignmentGroups = (local
|
|
5832
|
-
const deployedAssignmentGroups = (deployed
|
|
6061
|
+
const localAssignmentGroups = manifestAuth(local) === "access" ? localGroups : null;
|
|
6062
|
+
const deployedAssignmentGroups = manifestAuth(deployed) === "access" ? deployedGroups : null;
|
|
5833
6063
|
const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.deployedManifestSha);
|
|
5834
6064
|
if (assignmentCheck.kind === "error") {
|
|
5835
6065
|
return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
|
|
@@ -6030,6 +6260,58 @@ function computeDrift(local, deployed, canonicalGroups) {
|
|
|
6030
6260
|
}
|
|
6031
6261
|
return diffs;
|
|
6032
6262
|
}
|
|
6263
|
+
function computeV2SpecDrift(local, deployed, canonicalGroups) {
|
|
6264
|
+
const localSpec = {
|
|
6265
|
+
...local.spec,
|
|
6266
|
+
access: {
|
|
6267
|
+
...local.spec.access,
|
|
6268
|
+
allowed_entra_groups: [
|
|
6269
|
+
...canonicalGroups?.local ?? local.spec.access.allowed_entra_groups
|
|
6270
|
+
].sort()
|
|
6271
|
+
}
|
|
6272
|
+
};
|
|
6273
|
+
const deployedSpec = {
|
|
6274
|
+
...deployed.spec,
|
|
6275
|
+
access: {
|
|
6276
|
+
...deployed.spec.access,
|
|
6277
|
+
allowed_entra_groups: [
|
|
6278
|
+
...canonicalGroups?.deployed ?? deployed.spec.access.allowed_entra_groups
|
|
6279
|
+
].sort()
|
|
6280
|
+
}
|
|
6281
|
+
};
|
|
6282
|
+
const diffs = [];
|
|
6283
|
+
compareV2Value("spec", localSpec, deployedSpec, diffs);
|
|
6284
|
+
return diffs;
|
|
6285
|
+
}
|
|
6286
|
+
function compareV2Value(path8, local, deployed, diffs) {
|
|
6287
|
+
if (Object.is(local, deployed))
|
|
6288
|
+
return;
|
|
6289
|
+
if (Array.isArray(local) || Array.isArray(deployed)) {
|
|
6290
|
+
if (JSON.stringify(local) !== JSON.stringify(deployed)) {
|
|
6291
|
+
diffs.push({ path: path8, local, deployed });
|
|
6292
|
+
}
|
|
6293
|
+
return;
|
|
6294
|
+
}
|
|
6295
|
+
if (typeof local === "object" && local !== null && typeof deployed === "object" && deployed !== null) {
|
|
6296
|
+
const localRecord = local;
|
|
6297
|
+
const deployedRecord = deployed;
|
|
6298
|
+
const keys = new Set([
|
|
6299
|
+
...Object.keys(localRecord),
|
|
6300
|
+
...Object.keys(deployedRecord)
|
|
6301
|
+
]);
|
|
6302
|
+
for (const key of [...keys].sort()) {
|
|
6303
|
+
compareV2Value(`${path8}.${key}`, localRecord[key], deployedRecord[key], diffs);
|
|
6304
|
+
}
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6307
|
+
diffs.push({ path: path8, local, deployed });
|
|
6308
|
+
}
|
|
6309
|
+
function manifestAccess(manifest) {
|
|
6310
|
+
return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.access : manifest.access;
|
|
6311
|
+
}
|
|
6312
|
+
function manifestAuth(manifest) {
|
|
6313
|
+
return manifest.apiVersion === "launchpad.m-kopa.us/v2" ? manifest.spec.auth ?? "access" : manifest.auth ?? "access";
|
|
6314
|
+
}
|
|
6033
6315
|
function lifecycleOutput(slug, lc) {
|
|
6034
6316
|
const state = lc.state === "failed" ? "provisioning_failed" : lc.state === "live" ? "in_sync" : lc.state;
|
|
6035
6317
|
return {
|
|
@@ -6097,7 +6379,7 @@ function emit4(out, asJson, io) {
|
|
|
6097
6379
|
surfaceExceptions(out, io);
|
|
6098
6380
|
return;
|
|
6099
6381
|
case "live_content_untracked":
|
|
6100
|
-
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
6382
|
+
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} ` + `(no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
6101
6383
|
surfaceNoLocalManifestNote(out, io);
|
|
6102
6384
|
surfaceHeadVsDeployed(out, io);
|
|
6103
6385
|
surfaceDeployment(out, io);
|
|
@@ -6159,6 +6441,10 @@ function surfaceGroupIssues(out, io) {
|
|
|
6159
6441
|
function surfaceNoLocalManifestNote(out, io) {
|
|
6160
6442
|
if (out.drift !== null)
|
|
6161
6443
|
return;
|
|
6444
|
+
if (out.driftUnavailableReason !== undefined) {
|
|
6445
|
+
io.out(` ${out.driftUnavailableReason}.`);
|
|
6446
|
+
return;
|
|
6447
|
+
}
|
|
6162
6448
|
io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
|
|
6163
6449
|
}
|
|
6164
6450
|
function triggerLabel(trigger) {
|
|
@@ -6184,7 +6470,13 @@ function surfaceFailedLifecycleDeployment(out, io) {
|
|
|
6184
6470
|
}
|
|
6185
6471
|
function surfaceDeployment(out, io) {
|
|
6186
6472
|
const dep = out.deployment;
|
|
6187
|
-
if (dep === undefined || dep === null
|
|
6473
|
+
if (dep === undefined || dep === null)
|
|
6474
|
+
return;
|
|
6475
|
+
if (dep.origin === "dispatch") {
|
|
6476
|
+
surfaceV2Deployment(dep, io);
|
|
6477
|
+
return;
|
|
6478
|
+
}
|
|
6479
|
+
if (!dep.supported)
|
|
6188
6480
|
return;
|
|
6189
6481
|
if (dep.reconciledToLive === true) {
|
|
6190
6482
|
io.out(" ✓ recovered: this app was in a failed state; its verified-live build reconciled the record failed→live (no `launchpad recover` needed).");
|
|
@@ -6224,6 +6516,29 @@ function surfaceDeployment(out, io) {
|
|
|
6224
6516
|
}
|
|
6225
6517
|
}
|
|
6226
6518
|
}
|
|
6519
|
+
function surfaceV2Deployment(dep, io) {
|
|
6520
|
+
for (const line of renderV2DeploymentLines(dep))
|
|
6521
|
+
io.out(line);
|
|
6522
|
+
}
|
|
6523
|
+
function renderV2DeploymentLines(dep) {
|
|
6524
|
+
const delivery = dep.v2;
|
|
6525
|
+
if (delivery === undefined) {
|
|
6526
|
+
return [" v2 delivery: dispatch origin (delivery state unavailable)."];
|
|
6527
|
+
}
|
|
6528
|
+
const lines = [];
|
|
6529
|
+
const deliveredAt = delivery.state === "delivered" && delivery.deliveredAt !== undefined ? `, delivered ${delivery.deliveredAt}` : "";
|
|
6530
|
+
lines.push(` v2 delivery: state ${delivery.state}, stage ${delivery.stage}, commit ${delivery.commitSha.slice(0, 7)}${deliveredAt}`);
|
|
6531
|
+
if (delivery.state === "refused") {
|
|
6532
|
+
const failedAt = delivery.failedAt ?? delivery.stage;
|
|
6533
|
+
lines.push(` failed at ${failedAt}${delivery.reason ? `: ${delivery.reason}` : ""}`);
|
|
6534
|
+
if (delivery.runUrl)
|
|
6535
|
+
lines.push(` GitHub run: ${delivery.runUrl}`);
|
|
6536
|
+
}
|
|
6537
|
+
if (delivery.state !== "delivered" && dep.lastSuccessfulDeployment !== null) {
|
|
6538
|
+
lines.push(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
|
|
6539
|
+
}
|
|
6540
|
+
return lines;
|
|
6541
|
+
}
|
|
6227
6542
|
function surfaceExceptions(out, io) {
|
|
6228
6543
|
const exceptions = out.standingExceptions;
|
|
6229
6544
|
if (exceptions === undefined || exceptions.length === 0)
|
|
@@ -6240,7 +6555,7 @@ function surfaceHeadVsDeployed(out, io) {
|
|
|
6240
6555
|
if (out.hasOpenPr) {
|
|
6241
6556
|
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` an apply may be queued — see PR #${out.openPrNumber})`);
|
|
6242
6557
|
} else {
|
|
6243
|
-
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)}
|
|
6558
|
+
io.out(` (deployed sha ${short(out.deployedSha)} is behind app-repo HEAD ${short(out.headSha)};` + ` main is ahead of deployed)`);
|
|
6244
6559
|
}
|
|
6245
6560
|
}
|
|
6246
6561
|
}
|
|
@@ -6261,7 +6576,7 @@ function warnSecretShape(env, io) {
|
|
|
6261
6576
|
const hasPrefix = prefixes.some((p) => v.startsWith(p));
|
|
6262
6577
|
const longBase64Shape = v.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(v);
|
|
6263
6578
|
if (hasPrefix && longBase64Shape) {
|
|
6264
|
-
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.)`);
|
|
6579
|
+
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.)`);
|
|
6265
6580
|
}
|
|
6266
6581
|
}
|
|
6267
6582
|
}
|
|
@@ -6320,7 +6635,7 @@ function parseArgs2(args, cwd = process.cwd(), warn) {
|
|
|
6320
6635
|
if (slug === null) {
|
|
6321
6636
|
const inferred = inferSlug({ cwd, file, warn });
|
|
6322
6637
|
if (inferred === null) {
|
|
6323
|
-
return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
6638
|
+
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
6324
6639
|
}
|
|
6325
6640
|
slug = inferred;
|
|
6326
6641
|
}
|
|
@@ -6379,7 +6694,7 @@ function readLocalEntrypoint(cwd) {
|
|
|
6379
6694
|
for (const rel of ["index.html", join9("static", "index.html")]) {
|
|
6380
6695
|
const p = join9(cwd, rel);
|
|
6381
6696
|
if (existsSync4(p))
|
|
6382
|
-
return new Uint8Array(
|
|
6697
|
+
return new Uint8Array(readFileSync10(p));
|
|
6383
6698
|
}
|
|
6384
6699
|
return null;
|
|
6385
6700
|
}
|
|
@@ -6394,13 +6709,25 @@ function realVerifyDeployDeps(cfg) {
|
|
|
6394
6709
|
}
|
|
6395
6710
|
async function runDeployVerification(args, io, deps) {
|
|
6396
6711
|
io.out("");
|
|
6397
|
-
|
|
6712
|
+
if (args.dispatchOrigin === true) {
|
|
6713
|
+
io.out("Waiting for your repo build and dispatch delivery …");
|
|
6714
|
+
} else {
|
|
6715
|
+
io.out("Verifying a build of your commit went live …");
|
|
6716
|
+
}
|
|
6717
|
+
const seenV2Stages = new Set;
|
|
6718
|
+
const onV2Stage = (delivery) => {
|
|
6719
|
+
if (seenV2Stages.has(delivery.stage))
|
|
6720
|
+
return;
|
|
6721
|
+
seenV2Stages.add(delivery.stage);
|
|
6722
|
+
io.out(` v2 stage: ${v2StageLabel(delivery.stage)}`);
|
|
6723
|
+
};
|
|
6398
6724
|
const settle = await deps.wait({
|
|
6399
6725
|
slug: args.slug,
|
|
6400
6726
|
shippedSha: args.shippedSha,
|
|
6401
6727
|
baseline: args.baseline,
|
|
6402
6728
|
registrationBoundMs: args.registrationBoundMs,
|
|
6403
|
-
settleTimeoutMs: args.timeoutMs
|
|
6729
|
+
settleTimeoutMs: args.timeoutMs,
|
|
6730
|
+
onV2Stage
|
|
6404
6731
|
}, deps.waitDeps);
|
|
6405
6732
|
switch (settle.kind) {
|
|
6406
6733
|
case "unsupported":
|
|
@@ -6429,10 +6756,25 @@ async function runDeployVerification(args, io, deps) {
|
|
|
6429
6756
|
io.out(` It may still be queued. Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6430
6757
|
return 0;
|
|
6431
6758
|
case "pending":
|
|
6759
|
+
if (args.dispatchOrigin === true || seenV2Stages.size > 0) {
|
|
6760
|
+
io.out(`⏳ Dispatch delivery is still in progress after ${Math.round(args.timeoutMs / 1000)}s - couldn't verify yet.`);
|
|
6761
|
+
io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6762
|
+
return 0;
|
|
6763
|
+
}
|
|
6432
6764
|
io.out(`⏳ Your commit's build is still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
|
|
6433
6765
|
io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
6434
6766
|
return 0;
|
|
6435
6767
|
case "failure":
|
|
6768
|
+
if (settle.dispatch === true) {
|
|
6769
|
+
io.err(`✗ Dispatch delivery REFUSED at ${settle.failedStage ?? "unknown"} - your commit is NOT live.`);
|
|
6770
|
+
if (settle.reason)
|
|
6771
|
+
io.err(` reason: ${redactDiagnostic(settle.reason)}`);
|
|
6772
|
+
if (settle.detail)
|
|
6773
|
+
io.err(` detail: ${redactDiagnostic(settle.detail)}`);
|
|
6774
|
+
if (settle.runUrl)
|
|
6775
|
+
io.err(` GitHub run: ${settle.runUrl}`);
|
|
6776
|
+
return 1;
|
|
6777
|
+
}
|
|
6436
6778
|
io.err("✗ The Cloudflare Pages build for your commit FAILED — your content is NOT live.");
|
|
6437
6779
|
if (settle.failedStage)
|
|
6438
6780
|
io.err(` failed stage: ${redactDiagnostic(settle.failedStage)}`);
|
|
@@ -6444,9 +6786,30 @@ async function runDeployVerification(args, io, deps) {
|
|
|
6444
6786
|
io.err(` Full logs: \`launchpad status ${args.slug}\``);
|
|
6445
6787
|
return 1;
|
|
6446
6788
|
case "success":
|
|
6789
|
+
if (settle.v2 !== undefined) {
|
|
6790
|
+
const deliveredAt = settle.v2.deliveredAt ? ` at ${settle.v2.deliveredAt}` : "";
|
|
6791
|
+
io.out(`✓ Live in dispatch namespace - delivered commit ${settle.v2.commitSha}${deliveredAt}.`);
|
|
6792
|
+
return 0;
|
|
6793
|
+
}
|
|
6447
6794
|
return verifySuccess(args, settle.deploymentUrl, io, deps);
|
|
6448
6795
|
}
|
|
6449
6796
|
}
|
|
6797
|
+
function v2StageLabel(stage) {
|
|
6798
|
+
switch (stage) {
|
|
6799
|
+
case "committed":
|
|
6800
|
+
return "source committed";
|
|
6801
|
+
case "building":
|
|
6802
|
+
return "CI building";
|
|
6803
|
+
case "verified":
|
|
6804
|
+
return "run verified";
|
|
6805
|
+
case "uploading":
|
|
6806
|
+
return "artifact uploading";
|
|
6807
|
+
case "live":
|
|
6808
|
+
return "live in dispatch namespace";
|
|
6809
|
+
case "failed":
|
|
6810
|
+
return "failed";
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6450
6813
|
function realRedeployVerifyDeps(cfg) {
|
|
6451
6814
|
return {
|
|
6452
6815
|
wait: waitForDeployment,
|
|
@@ -6673,7 +7036,7 @@ function runGit2(sp, cwd, args) {
|
|
|
6673
7036
|
}
|
|
6674
7037
|
|
|
6675
7038
|
// src/deploy/manifest-diff.ts
|
|
6676
|
-
import { parse as
|
|
7039
|
+
import { parse as parseYaml5 } from "yaml";
|
|
6677
7040
|
var MASK = "«redacted»";
|
|
6678
7041
|
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;
|
|
6679
7042
|
function isSecretPath(path8) {
|
|
@@ -6764,7 +7127,7 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6764
7127
|
let local;
|
|
6765
7128
|
let deployed;
|
|
6766
7129
|
try {
|
|
6767
|
-
local =
|
|
7130
|
+
local = parseYaml5(localYaml);
|
|
6768
7131
|
} catch (e) {
|
|
6769
7132
|
return {
|
|
6770
7133
|
inSync: false,
|
|
@@ -6773,7 +7136,7 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6773
7136
|
};
|
|
6774
7137
|
}
|
|
6775
7138
|
try {
|
|
6776
|
-
deployed =
|
|
7139
|
+
deployed = parseYaml5(deployedYaml);
|
|
6777
7140
|
} catch (e) {
|
|
6778
7141
|
return {
|
|
6779
7142
|
inSync: false,
|
|
@@ -6781,6 +7144,22 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6781
7144
|
parseError: `deployed manifest did not parse: ${e.message}`
|
|
6782
7145
|
};
|
|
6783
7146
|
}
|
|
7147
|
+
const localVersion = manifestApiVersion(local);
|
|
7148
|
+
const deployedVersion = manifestApiVersion(deployed);
|
|
7149
|
+
if (localVersion !== deployedVersion && (isV2(localVersion) || isV2(deployedVersion))) {
|
|
7150
|
+
return {
|
|
7151
|
+
inSync: false,
|
|
7152
|
+
changes: [],
|
|
7153
|
+
parseError: null,
|
|
7154
|
+
shapeMismatch: `local uses ${localVersion ?? "an unknown schema"}; deployed uses ${deployedVersion ?? "an unknown schema"}`
|
|
7155
|
+
};
|
|
7156
|
+
}
|
|
7157
|
+
if (isV2(localVersion) && isV2(deployedVersion)) {
|
|
7158
|
+
local = { spec: manifestSpec(local) };
|
|
7159
|
+
deployed = { spec: manifestSpec(deployed) };
|
|
7160
|
+
}
|
|
7161
|
+
local = normalize(local);
|
|
7162
|
+
deployed = normalize(deployed);
|
|
6784
7163
|
if (stableStringify(local) === stableStringify(deployed)) {
|
|
6785
7164
|
return { inSync: true, changes: [], parseError: null };
|
|
6786
7165
|
}
|
|
@@ -6827,6 +7206,18 @@ function diffManifests(localYaml, deployedYaml) {
|
|
|
6827
7206
|
}
|
|
6828
7207
|
return { inSync: false, changes, parseError: null };
|
|
6829
7208
|
}
|
|
7209
|
+
function manifestApiVersion(value) {
|
|
7210
|
+
if (typeof value !== "object" || value === null)
|
|
7211
|
+
return null;
|
|
7212
|
+
const apiVersion = value.apiVersion;
|
|
7213
|
+
return typeof apiVersion === "string" ? apiVersion : null;
|
|
7214
|
+
}
|
|
7215
|
+
function isV2(apiVersion) {
|
|
7216
|
+
return apiVersion === "launchpad.m-kopa.us/v2";
|
|
7217
|
+
}
|
|
7218
|
+
function manifestSpec(value) {
|
|
7219
|
+
return typeof value === "object" && value !== null ? value.spec : undefined;
|
|
7220
|
+
}
|
|
6830
7221
|
function renderManifestDiffLines(diff, max = 20) {
|
|
6831
7222
|
const lines = [];
|
|
6832
7223
|
for (const c of diff.changes.slice(0, max)) {
|
|
@@ -7255,47 +7646,6 @@ function parseDeployFlags(args) {
|
|
|
7255
7646
|
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
7256
7647
|
import { resolve as resolvePath, join as join10 } from "node:path";
|
|
7257
7648
|
|
|
7258
|
-
// src/manifest/load.ts
|
|
7259
|
-
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7260
|
-
import { parse as parseYaml5, YAMLParseError } from "yaml";
|
|
7261
|
-
function loadManifest(path8) {
|
|
7262
|
-
let raw;
|
|
7263
|
-
try {
|
|
7264
|
-
raw = readFileSync10(path8, "utf8");
|
|
7265
|
-
} catch (err) {
|
|
7266
|
-
const e = err;
|
|
7267
|
-
if (e.code === "ENOENT") {
|
|
7268
|
-
return { kind: "file-not-found", path: path8 };
|
|
7269
|
-
}
|
|
7270
|
-
return { kind: "read-error", path: path8, message: e.message ?? String(err) };
|
|
7271
|
-
}
|
|
7272
|
-
return parseManifest2(raw, path8);
|
|
7273
|
-
}
|
|
7274
|
-
function parseManifest2(yamlText, path8) {
|
|
7275
|
-
let parsed;
|
|
7276
|
-
try {
|
|
7277
|
-
parsed = parseYaml5(yamlText);
|
|
7278
|
-
} catch (err) {
|
|
7279
|
-
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
7280
|
-
return { kind: "yaml-parse-error", path: path8, message };
|
|
7281
|
-
}
|
|
7282
|
-
const result = ManifestSchema.safeParse(parsed);
|
|
7283
|
-
if (result.success) {
|
|
7284
|
-
return { kind: "ok", manifest: result.data };
|
|
7285
|
-
}
|
|
7286
|
-
return {
|
|
7287
|
-
kind: "schema-error",
|
|
7288
|
-
path: path8,
|
|
7289
|
-
issues: formatIssues(result.error)
|
|
7290
|
-
};
|
|
7291
|
-
}
|
|
7292
|
-
function formatIssues(error) {
|
|
7293
|
-
return error.issues.map((issue) => ({
|
|
7294
|
-
path: issue.path.map(String).join(".") || "(root)",
|
|
7295
|
-
message: issue.message
|
|
7296
|
-
}));
|
|
7297
|
-
}
|
|
7298
|
-
|
|
7299
7649
|
// src/status-page-output.ts
|
|
7300
7650
|
async function surfaceStatusPageUrl(url, io, options) {
|
|
7301
7651
|
if (typeof url !== "string" || url.length === 0)
|
|
@@ -7370,13 +7720,13 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
7370
7720
|
return 0;
|
|
7371
7721
|
}
|
|
7372
7722
|
const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
7373
|
-
const loaded =
|
|
7723
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
7374
7724
|
if (loaded.kind !== "ok") {
|
|
7375
7725
|
return renderManifestError(loaded, io);
|
|
7376
7726
|
}
|
|
7377
|
-
const slug = loaded
|
|
7727
|
+
const slug = manifestSlug(loaded);
|
|
7378
7728
|
if (!SLUG_REGEX2.test(slug)) {
|
|
7379
|
-
io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
|
|
7729
|
+
io.err(`launchpad deploy --apply: manifest's ${loaded.schema === "launchpad.m-kopa.us/v2" ? "metadata.slug" : "metadata.name"} '${slug}' is not a valid slug`);
|
|
7380
7730
|
return 1;
|
|
7381
7731
|
}
|
|
7382
7732
|
if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
|
|
@@ -7667,12 +8017,12 @@ function deletePinIfPresent(cfg, slug, io) {
|
|
|
7667
8017
|
}
|
|
7668
8018
|
async function loadSlugForResume(opts, io) {
|
|
7669
8019
|
const manifestPath = resolvePath(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
7670
|
-
const loaded =
|
|
8020
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
7671
8021
|
if (loaded.kind !== "ok") {
|
|
7672
8022
|
renderManifestError(loaded, io);
|
|
7673
8023
|
return null;
|
|
7674
8024
|
}
|
|
7675
|
-
return loaded
|
|
8025
|
+
return manifestSlug(loaded);
|
|
7676
8026
|
}
|
|
7677
8027
|
async function defaultPrompt(question) {
|
|
7678
8028
|
const { createInterface } = await import("node:readline/promises");
|
|
@@ -7713,6 +8063,10 @@ function mapHttpError(e, slug, io) {
|
|
|
7713
8063
|
io.err(`launchpad deploy --apply failed: ${describe14(e)}`);
|
|
7714
8064
|
return 2;
|
|
7715
8065
|
}
|
|
8066
|
+
function manifestSlug(loaded) {
|
|
8067
|
+
const { metadata } = loaded.manifest;
|
|
8068
|
+
return "slug" in metadata ? metadata.slug : metadata.name;
|
|
8069
|
+
}
|
|
7716
8070
|
function renderManifestError(loaded, io) {
|
|
7717
8071
|
switch (loaded.kind) {
|
|
7718
8072
|
case "ok":
|
|
@@ -8608,6 +8962,10 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
|
|
|
8608
8962
|
io.out(` config diff: skipped (${diff.parseError}).`);
|
|
8609
8963
|
return;
|
|
8610
8964
|
}
|
|
8965
|
+
if (diff.shapeMismatch !== undefined) {
|
|
8966
|
+
io.out(` config diff: manifest shapes differ (${diff.shapeMismatch}); drift was not evaluated.`);
|
|
8967
|
+
return;
|
|
8968
|
+
}
|
|
8611
8969
|
if (diff.inSync) {
|
|
8612
8970
|
io.out(" config: launchpad.yaml matches the deployed manifest (in sync).");
|
|
8613
8971
|
return;
|
|
@@ -8631,6 +8989,7 @@ async function runModelADeploy(args) {
|
|
|
8631
8989
|
const { cwd, manifestPath, io } = args;
|
|
8632
8990
|
let slug;
|
|
8633
8991
|
let appType = "static";
|
|
8992
|
+
let declaredOrigin;
|
|
8634
8993
|
try {
|
|
8635
8994
|
const manifestObj = parseYaml6(readFileSync11(manifestPath, "utf8"));
|
|
8636
8995
|
const metaSlug = resolveManifestSlug(manifestObj);
|
|
@@ -8640,13 +8999,22 @@ async function runModelADeploy(args) {
|
|
|
8640
8999
|
}
|
|
8641
9000
|
slug = metaSlug;
|
|
8642
9001
|
const raw = manifestObj;
|
|
8643
|
-
const
|
|
8644
|
-
|
|
9002
|
+
const isV22 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
|
|
9003
|
+
declaredOrigin = declaredOriginOf(isV22, raw?.spec?.origin);
|
|
9004
|
+
const deploymentType = isV22 ? raw?.spec?.appType : raw?.deployment?.type;
|
|
8645
9005
|
if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
|
|
8646
9006
|
appType = deploymentType;
|
|
8647
9007
|
}
|
|
9008
|
+
if (appType !== undefined) {
|
|
9009
|
+
const auth = isV22 ? raw?.spec?.auth : raw?.auth;
|
|
9010
|
+
const issue = deploymentAuthIssue(appType, auth === "gateway" ? "gateway" : auth === "access" ? "access" : undefined);
|
|
9011
|
+
if (issue !== null) {
|
|
9012
|
+
io.err(`launchpad deploy: ${issue.message}`);
|
|
9013
|
+
return 1;
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
8648
9016
|
if (deploymentType === "container") {
|
|
8649
|
-
const parsed =
|
|
9017
|
+
const parsed = isV22 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
|
|
8650
9018
|
if (parsed.kind !== "ok") {
|
|
8651
9019
|
io.err("launchpad deploy: manifest is structurally invalid:");
|
|
8652
9020
|
for (const issue of parsed.issues) {
|
|
@@ -8698,7 +9066,8 @@ async function runModelADeploy(args) {
|
|
|
8698
9066
|
try {
|
|
8699
9067
|
await surfaceManifestDiff(cfg, slug, manifestPath, io);
|
|
8700
9068
|
} catch {}
|
|
8701
|
-
const
|
|
9069
|
+
const deployContext = await readDeploymentContext(realCommitWaitDeps(cfg), slug);
|
|
9070
|
+
const deployBaseline = noVerify ? null : deployContext.baseline;
|
|
8702
9071
|
let result;
|
|
8703
9072
|
try {
|
|
8704
9073
|
if (args.allowUnguarded) {
|
|
@@ -8828,7 +9197,11 @@ async function runModelADeploy(args) {
|
|
|
8828
9197
|
if (accessExit !== 0)
|
|
8829
9198
|
return accessExit;
|
|
8830
9199
|
io.out("");
|
|
8831
|
-
|
|
9200
|
+
const dispatchOrigin = handoffIsDispatch({
|
|
9201
|
+
declared: declaredOrigin,
|
|
9202
|
+
baseline: deployContext.origin
|
|
9203
|
+
});
|
|
9204
|
+
surfaceCommittedHandoff(dispatchOrigin, io, "ascii");
|
|
8832
9205
|
if (noVerify)
|
|
8833
9206
|
return 0;
|
|
8834
9207
|
return runDeployVerification({
|
|
@@ -8838,7 +9211,8 @@ async function runModelADeploy(args) {
|
|
|
8838
9211
|
shippedSha: outcome.manifest_sha,
|
|
8839
9212
|
baseline: deployBaseline,
|
|
8840
9213
|
registrationBoundMs,
|
|
8841
|
-
timeoutMs: verifyTimeoutMs
|
|
9214
|
+
timeoutMs: verifyTimeoutMs,
|
|
9215
|
+
dispatchOrigin
|
|
8842
9216
|
}, io, realVerifyDeployDeps(cfg));
|
|
8843
9217
|
}
|
|
8844
9218
|
io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
|
|
@@ -8934,9 +9308,18 @@ async function runModelADeploy(args) {
|
|
|
8934
9308
|
}
|
|
8935
9309
|
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
8936
9310
|
io.out("");
|
|
8937
|
-
|
|
9311
|
+
const dispatchOrigin = handoffIsDispatch({
|
|
9312
|
+
v2Delivery: success.v2Delivery,
|
|
9313
|
+
declared: declaredOrigin,
|
|
9314
|
+
baseline: deployContext.origin
|
|
9315
|
+
});
|
|
9316
|
+
surfaceCommittedHandoff(dispatchOrigin, io);
|
|
8938
9317
|
if (noVerify) {
|
|
8939
|
-
|
|
9318
|
+
if (dispatchOrigin) {
|
|
9319
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the delivery stage and outcome.`);
|
|
9320
|
+
} else {
|
|
9321
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
9322
|
+
}
|
|
8940
9323
|
return 0;
|
|
8941
9324
|
}
|
|
8942
9325
|
return runDeployVerification({
|
|
@@ -8946,11 +9329,33 @@ async function runModelADeploy(args) {
|
|
|
8946
9329
|
shippedSha: success.commit_sha,
|
|
8947
9330
|
baseline: deployBaseline,
|
|
8948
9331
|
registrationBoundMs,
|
|
8949
|
-
timeoutMs: verifyTimeoutMs
|
|
9332
|
+
timeoutMs: verifyTimeoutMs,
|
|
9333
|
+
dispatchOrigin
|
|
8950
9334
|
}, io, realVerifyDeployDeps(cfg));
|
|
8951
9335
|
}
|
|
8952
9336
|
}
|
|
8953
9337
|
}
|
|
9338
|
+
function surfaceCommittedHandoff(dispatchOrigin, io, pagesStyle = "unicode") {
|
|
9339
|
+
io.out(committedHandoffMessage(dispatchOrigin, pagesStyle));
|
|
9340
|
+
}
|
|
9341
|
+
function declaredOriginOf(isV22, origin) {
|
|
9342
|
+
if (!isV22)
|
|
9343
|
+
return "pages";
|
|
9344
|
+
return origin === "dispatch" ? "dispatch" : "pages";
|
|
9345
|
+
}
|
|
9346
|
+
function handoffIsDispatch(input) {
|
|
9347
|
+
if (input.v2Delivery === "pending" || input.v2Delivery === "deferred")
|
|
9348
|
+
return true;
|
|
9349
|
+
if (input.declared !== undefined)
|
|
9350
|
+
return input.declared === "dispatch";
|
|
9351
|
+
return input.baseline === "dispatch";
|
|
9352
|
+
}
|
|
9353
|
+
function committedHandoffMessage(dispatchOrigin, pagesStyle = "unicode") {
|
|
9354
|
+
if (dispatchOrigin) {
|
|
9355
|
+
return "Committed; your repo's build workflow is building this commit, then the bot delivers it to the dispatch namespace.";
|
|
9356
|
+
}
|
|
9357
|
+
return pagesStyle === "ascii" ? "Committed; build pending - Cloudflare Pages is building this deploy now." : "Committed; build pending — Cloudflare Pages is building this deploy now.";
|
|
9358
|
+
}
|
|
8954
9359
|
function pendingReconcileOutcome(body) {
|
|
8955
9360
|
const manifestSha = body.manifestSha;
|
|
8956
9361
|
const id = body.reconcileId;
|
|
@@ -10378,8 +10783,26 @@ async function runInit(args, io, prompt) {
|
|
|
10378
10783
|
io.err(`launchpad init: ${fillResult.message}`);
|
|
10379
10784
|
return 64;
|
|
10380
10785
|
}
|
|
10786
|
+
const authIssue = deploymentAuthIssue(fillResult.inputs.type, fillResult.inputs.auth ?? "gateway");
|
|
10787
|
+
if (authIssue !== null) {
|
|
10788
|
+
io.err(`launchpad init: ${authIssue.message}`);
|
|
10789
|
+
return 64;
|
|
10790
|
+
}
|
|
10791
|
+
if (fillResult.inputs.type === "container" && inputs.origin !== undefined) {
|
|
10792
|
+
io.err("launchpad init: --origin is not supported for containers");
|
|
10793
|
+
return 64;
|
|
10794
|
+
}
|
|
10795
|
+
const dispatch = newAppOrigin(fillResult.inputs.type, inputs.origin) === "dispatch";
|
|
10796
|
+
if (dispatch && fillResult.inputs.type === "react+api") {
|
|
10797
|
+
io.err("launchpad init: API starter dispatch is not available yet; use --origin pages");
|
|
10798
|
+
return 64;
|
|
10799
|
+
}
|
|
10800
|
+
if (dispatch && (inputs.hostnames.length > 0 || inputs.sessionDuration !== undefined)) {
|
|
10801
|
+
io.err("launchpad init: dispatch does not support --hostname or --session-duration; use --origin pages for a Pages manifest");
|
|
10802
|
+
return 64;
|
|
10803
|
+
}
|
|
10381
10804
|
const manifest = buildManifest(fillResult.inputs, detected);
|
|
10382
|
-
const validation = ManifestSchema.safeParse(manifest);
|
|
10805
|
+
const validation = manifest.apiVersion === "launchpad.m-kopa.us/v2" ? ManifestV2Schema.safeParse(manifest) : ManifestSchema.safeParse(manifest);
|
|
10383
10806
|
if (!validation.success) {
|
|
10384
10807
|
io.err("launchpad init: invalid manifest input");
|
|
10385
10808
|
for (const issue of validation.error.issues) {
|
|
@@ -10432,8 +10855,11 @@ async function runInit(args, io, prompt) {
|
|
|
10432
10855
|
io.out("");
|
|
10433
10856
|
io.out("Next steps:");
|
|
10434
10857
|
io.out(" launchpad validate # confirm the manifest");
|
|
10435
|
-
|
|
10436
|
-
|
|
10858
|
+
if (!dispatch)
|
|
10859
|
+
io.out(" launchpad secrets template # scaffold .env.example");
|
|
10860
|
+
io.out(" launchpad deploy # build and deploy the app");
|
|
10861
|
+
if (dispatch)
|
|
10862
|
+
io.out(" Dispatch currently supports plain-text environment variables, but not secret bindings.");
|
|
10437
10863
|
return 0;
|
|
10438
10864
|
}
|
|
10439
10865
|
function parseArgs6(args) {
|
|
@@ -10444,6 +10870,7 @@ function parseArgs6(args) {
|
|
|
10444
10870
|
description: undefined,
|
|
10445
10871
|
type: undefined,
|
|
10446
10872
|
auth: undefined,
|
|
10873
|
+
origin: undefined,
|
|
10447
10874
|
groups: [],
|
|
10448
10875
|
sessionDuration: undefined,
|
|
10449
10876
|
hostnames: [],
|
|
@@ -10528,6 +10955,16 @@ function parseArgs6(args) {
|
|
|
10528
10955
|
inputs.type = got;
|
|
10529
10956
|
continue;
|
|
10530
10957
|
}
|
|
10958
|
+
if (a === "--origin") {
|
|
10959
|
+
const got = needsValue();
|
|
10960
|
+
if (typeof got !== "string")
|
|
10961
|
+
return got;
|
|
10962
|
+
if (got !== "pages" && got !== "dispatch") {
|
|
10963
|
+
return { kind: "usage-error", message: "--origin must be pages or dispatch" };
|
|
10964
|
+
}
|
|
10965
|
+
inputs.origin = got;
|
|
10966
|
+
continue;
|
|
10967
|
+
}
|
|
10531
10968
|
if (a === "--auth") {
|
|
10532
10969
|
const got = needsValue();
|
|
10533
10970
|
if (typeof got !== "string")
|
|
@@ -10722,6 +11159,7 @@ async function fillRequired(inputs, options, io, prompt) {
|
|
|
10722
11159
|
description: inputs.description,
|
|
10723
11160
|
type: typeR,
|
|
10724
11161
|
auth: inputs.auth,
|
|
11162
|
+
origin: inputs.origin,
|
|
10725
11163
|
groups: groupsR,
|
|
10726
11164
|
sessionDuration: inputs.sessionDuration,
|
|
10727
11165
|
hostnames: inputs.hostnames,
|
|
@@ -10781,6 +11219,29 @@ function buildManifest(inputs, detected) {
|
|
|
10781
11219
|
root: ".",
|
|
10782
11220
|
include: [...defaultIncludeForAppType(inputs.type)]
|
|
10783
11221
|
};
|
|
11222
|
+
if (newAppOrigin(inputs.type, inputs.origin) === "dispatch") {
|
|
11223
|
+
return {
|
|
11224
|
+
apiVersion: "launchpad.m-kopa.us/v2",
|
|
11225
|
+
kind: "App",
|
|
11226
|
+
metadata: { slug: inputs.name, schemaVersion: 2 },
|
|
11227
|
+
spec: {
|
|
11228
|
+
appType: inputs.type,
|
|
11229
|
+
origin: "dispatch",
|
|
11230
|
+
auth: resolvedAuth,
|
|
11231
|
+
display: {
|
|
11232
|
+
name: inputs.name,
|
|
11233
|
+
team: inputs.team,
|
|
11234
|
+
...inputs.description ? { description: inputs.description } : {}
|
|
11235
|
+
},
|
|
11236
|
+
owner: { email: inputs.owner },
|
|
11237
|
+
access: { allowed_entra_groups: [...inputs.groups] },
|
|
11238
|
+
app: manifest.app,
|
|
11239
|
+
...manifest.build === undefined ? {} : { build: manifest.build }
|
|
11240
|
+
}
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
11243
|
+
if (inputs.origin === "pages")
|
|
11244
|
+
manifest.origin = "pages";
|
|
10784
11245
|
return manifest;
|
|
10785
11246
|
}
|
|
10786
11247
|
function starterIndexHtml(slug) {
|
|
@@ -13220,11 +13681,15 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
13220
13681
|
const cfg = deps.config ?? loadConfig();
|
|
13221
13682
|
const fetcher = deps.fetcher ?? fetch;
|
|
13222
13683
|
const manifestPath = resolvePath4(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
13223
|
-
const loaded =
|
|
13684
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
13224
13685
|
if (loaded.kind !== "ok") {
|
|
13225
13686
|
return renderManifestError4(loaded, io);
|
|
13226
13687
|
}
|
|
13227
|
-
const
|
|
13688
|
+
const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets push", io);
|
|
13689
|
+
if (v2Refusal !== null)
|
|
13690
|
+
return v2Refusal;
|
|
13691
|
+
const manifest = loaded.manifest;
|
|
13692
|
+
const bindings = manifest.secrets?.bindings ?? [];
|
|
13228
13693
|
const envFileBindings = bindings.filter((b) => b.source === "env-file");
|
|
13229
13694
|
if (envFileBindings.length === 0) {
|
|
13230
13695
|
io.out("No `env-file` secret bindings declared in the manifest.");
|
|
@@ -13306,7 +13771,7 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
13306
13771
|
io.out("Re-run without --dry-run to push.");
|
|
13307
13772
|
return 0;
|
|
13308
13773
|
}
|
|
13309
|
-
const slug =
|
|
13774
|
+
const slug = manifest.metadata.name;
|
|
13310
13775
|
let pushed = 0;
|
|
13311
13776
|
let partial = 0;
|
|
13312
13777
|
let anyPagesUpdated = false;
|
|
@@ -13431,6 +13896,20 @@ function mapPushError(e, slug, name, io) {
|
|
|
13431
13896
|
function describe30(e) {
|
|
13432
13897
|
return e instanceof Error ? e.message : String(e);
|
|
13433
13898
|
}
|
|
13899
|
+
function refuseV2Manifest(loaded, command, io) {
|
|
13900
|
+
if (loaded.schema !== "launchpad.m-kopa.us/v2")
|
|
13901
|
+
return null;
|
|
13902
|
+
const origin = loaded.manifest.spec?.origin;
|
|
13903
|
+
if (origin === "dispatch") {
|
|
13904
|
+
io.err(`${command}: this is a dispatch-origin (v2) app.`);
|
|
13905
|
+
const outcome = command.endsWith("push") ? "nothing was pushed" : "nothing was read";
|
|
13906
|
+
io.err(` Secrets are not delivered to the dispatch namespace yet (PS-2740); ${outcome}.`);
|
|
13907
|
+
return 1;
|
|
13908
|
+
}
|
|
13909
|
+
io.err(`${command}: this is a launchpad.m-kopa.us/v2 manifest.`);
|
|
13910
|
+
io.err(" This command reads v1alpha1 `secrets.bindings`; v2 declares secrets as `spec.env_vars` `secret_text` entries, which it does not push yet.");
|
|
13911
|
+
return 1;
|
|
13912
|
+
}
|
|
13434
13913
|
function renderManifestError4(result, io) {
|
|
13435
13914
|
switch (result.kind) {
|
|
13436
13915
|
case "ok":
|
|
@@ -13468,16 +13947,20 @@ async function runSecretsStatus(opts, io, deps = {}) {
|
|
|
13468
13947
|
const cfg = deps.config ?? loadConfig();
|
|
13469
13948
|
const fetcher = deps.fetcher ?? fetch;
|
|
13470
13949
|
const manifestPath = resolvePath5(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
13471
|
-
const loaded =
|
|
13950
|
+
const loaded = loadManifestAnyVersion(manifestPath);
|
|
13472
13951
|
if (loaded.kind !== "ok")
|
|
13473
13952
|
return renderManifestError5(loaded, io);
|
|
13474
|
-
const
|
|
13953
|
+
const v2Refusal = refuseV2Manifest(loaded, "launchpad secrets status", io);
|
|
13954
|
+
if (v2Refusal !== null)
|
|
13955
|
+
return v2Refusal;
|
|
13956
|
+
const manifest = loaded.manifest;
|
|
13957
|
+
const slug = manifest.metadata.name;
|
|
13475
13958
|
if (opts.slug != null && opts.slug !== slug) {
|
|
13476
13959
|
io.err(`launchpad secrets status: slug "${opts.slug}" does not match this directory's ` + `manifest (metadata.name: "${slug}").`);
|
|
13477
13960
|
io.err(" secrets status audits the manifest in the current directory — omit the slug, " + "or run from the app's directory.");
|
|
13478
13961
|
return 2;
|
|
13479
13962
|
}
|
|
13480
|
-
const bindings =
|
|
13963
|
+
const bindings = manifest.secrets?.bindings ?? [];
|
|
13481
13964
|
if (bindings.length === 0) {
|
|
13482
13965
|
io.out("No secret bindings declared in the manifest.");
|
|
13483
13966
|
return 0;
|
|
@@ -14845,7 +15328,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync21, readdirSync
|
|
|
14845
15328
|
import { dirname as dirname12, resolve as resolve17 } from "node:path";
|
|
14846
15329
|
var validateCommand = {
|
|
14847
15330
|
name: "validate",
|
|
14848
|
-
summary: "validate launchpad.yaml against
|
|
15331
|
+
summary: "validate launchpad.yaml against its v1alpha1 or v2 schema",
|
|
14849
15332
|
run: runValidate
|
|
14850
15333
|
};
|
|
14851
15334
|
async function runValidate(args, io) {
|
|
@@ -14857,7 +15340,7 @@ async function runValidate(args, io) {
|
|
|
14857
15340
|
}
|
|
14858
15341
|
const { file, json, strictGroups } = parseResult;
|
|
14859
15342
|
const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
|
|
14860
|
-
const result =
|
|
15343
|
+
const result = loadManifestAnyVersion(path14);
|
|
14861
15344
|
if (result.kind !== "ok") {
|
|
14862
15345
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
14863
15346
|
}
|
|
@@ -14867,16 +15350,17 @@ async function runValidate(args, io) {
|
|
|
14867
15350
|
manifest: result.manifest,
|
|
14868
15351
|
io,
|
|
14869
15352
|
json,
|
|
14870
|
-
structurallyValidName: result.manifest.
|
|
15353
|
+
structurallyValidName: manifestView(result.manifest).name
|
|
14871
15354
|
});
|
|
14872
15355
|
if (unsupported2 !== null)
|
|
14873
15356
|
return unsupported2;
|
|
14874
|
-
const
|
|
15357
|
+
const view = manifestView(result.manifest);
|
|
15358
|
+
const boundary = checkBoundary(path14, view.appDeclared);
|
|
14875
15359
|
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
14876
15360
|
if (serveErrors.length > 0) {
|
|
14877
15361
|
boundary.errors = [...boundary.errors, ...serveErrors];
|
|
14878
15362
|
}
|
|
14879
|
-
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(
|
|
15363
|
+
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(view.access)) : { kind: "skipped" };
|
|
14880
15364
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
14881
15365
|
}
|
|
14882
15366
|
function checkBoundary(manifestPath, declared) {
|
|
@@ -14900,11 +15384,12 @@ function checkBoundary(manifestPath, declared) {
|
|
|
14900
15384
|
return { declared, errors: [], warnings: r.warnings };
|
|
14901
15385
|
}
|
|
14902
15386
|
function checkStaticServeDir(manifestPath, manifest) {
|
|
14903
|
-
|
|
15387
|
+
const view = manifestView(manifest);
|
|
15388
|
+
if (view.deploymentType !== "static")
|
|
14904
15389
|
return [];
|
|
14905
|
-
if (
|
|
15390
|
+
if (view.buildCommand !== undefined)
|
|
14906
15391
|
return [];
|
|
14907
|
-
const serveDir =
|
|
15392
|
+
const serveDir = view.destinationDir ?? STATIC_SERVE_DIR;
|
|
14908
15393
|
const normalized = serveDir.replace(/\\/g, "/");
|
|
14909
15394
|
const segments = normalized.split("/");
|
|
14910
15395
|
if (normalized === "" || normalized === "." || normalized === "./" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.some((s) => s === "..")) {
|
|
@@ -15004,9 +15489,10 @@ async function checkGroups(groups) {
|
|
|
15004
15489
|
return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
|
|
15005
15490
|
}
|
|
15006
15491
|
function renderHumanOk(result, group, boundary, io) {
|
|
15007
|
-
|
|
15008
|
-
io.out(
|
|
15009
|
-
io.out(`
|
|
15492
|
+
const view = manifestView(result.manifest);
|
|
15493
|
+
io.out(`✓ ${view.name} — manifest is valid`);
|
|
15494
|
+
io.out(` apiVersion: ${result.schema}`);
|
|
15495
|
+
io.out(` type: ${view.deploymentType}`);
|
|
15010
15496
|
if (boundary.errors.length > 0) {
|
|
15011
15497
|
for (const e of boundary.errors) {
|
|
15012
15498
|
io.err(`✗ app boundary: ${e}`);
|
|
@@ -15075,6 +15561,7 @@ function renderHumanError(result, io) {
|
|
|
15075
15561
|
return 1;
|
|
15076
15562
|
case "schema-error":
|
|
15077
15563
|
io.err(`✗ ${result.path}`);
|
|
15564
|
+
io.err(` validated against schema: ${result.schema}`);
|
|
15078
15565
|
io.err(` ${result.issues.length} schema issue${result.issues.length === 1 ? "" : "s"}:`);
|
|
15079
15566
|
for (const issue of result.issues) {
|
|
15080
15567
|
io.err(` ${issue.path}: ${issue.message}`);
|
|
@@ -15083,11 +15570,12 @@ function renderHumanError(result, io) {
|
|
|
15083
15570
|
}
|
|
15084
15571
|
}
|
|
15085
15572
|
function renderJsonOk(result, group, boundary, io) {
|
|
15573
|
+
const view = manifestView(result.manifest);
|
|
15086
15574
|
const base = {
|
|
15087
15575
|
manifest_valid: true,
|
|
15088
15576
|
runtime_supported: true,
|
|
15089
|
-
name:
|
|
15090
|
-
deploymentType:
|
|
15577
|
+
name: view.name,
|
|
15578
|
+
deploymentType: view.deploymentType,
|
|
15091
15579
|
appBoundary: {
|
|
15092
15580
|
declared: boundary.declared,
|
|
15093
15581
|
errors: boundary.errors,
|
|
@@ -15167,11 +15655,32 @@ function renderJsonError(result, io) {
|
|
|
15167
15655
|
ok: false,
|
|
15168
15656
|
kind: "schema-error",
|
|
15169
15657
|
path: result.path,
|
|
15658
|
+
schema: result.schema,
|
|
15170
15659
|
issues: result.issues
|
|
15171
15660
|
}));
|
|
15172
15661
|
return 1;
|
|
15173
15662
|
}
|
|
15174
15663
|
}
|
|
15664
|
+
function manifestView(manifest) {
|
|
15665
|
+
if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
|
|
15666
|
+
return {
|
|
15667
|
+
name: manifest.metadata.slug,
|
|
15668
|
+
deploymentType: manifest.spec.appType,
|
|
15669
|
+
access: manifest.spec.access,
|
|
15670
|
+
buildCommand: manifest.spec.build?.command,
|
|
15671
|
+
destinationDir: manifest.spec.build?.destination_dir,
|
|
15672
|
+
appDeclared: manifest.spec.app !== undefined
|
|
15673
|
+
};
|
|
15674
|
+
}
|
|
15675
|
+
return {
|
|
15676
|
+
name: manifest.metadata.name,
|
|
15677
|
+
deploymentType: manifest.deployment.type,
|
|
15678
|
+
access: manifest.access,
|
|
15679
|
+
buildCommand: manifest.build?.command,
|
|
15680
|
+
destinationDir: manifest.build?.destination_dir,
|
|
15681
|
+
appDeclared: manifest.app !== undefined
|
|
15682
|
+
};
|
|
15683
|
+
}
|
|
15175
15684
|
function parseArgs13(args) {
|
|
15176
15685
|
let file;
|
|
15177
15686
|
let json = false;
|