@m-kopa/launchpad-cli 0.23.0 → 0.24.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 +44 -0
- package/dist/bundle/boundary.d.ts +26 -0
- package/dist/bundle/boundary.d.ts.map +1 -0
- package/dist/bundle/orchestrate.d.ts +5 -0
- package/dist/bundle/orchestrate.d.ts.map +1 -1
- package/dist/cli.js +1480 -847
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/merge.d.ts.map +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/status.d.ts +11 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/deploy/deployment-status.d.ts +38 -0
- package/dist/deploy/deployment-status.d.ts.map +1 -0
- 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 +1 -1
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -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.24.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -1219,12 +1219,12 @@ function describe9(e) {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
1221
|
// src/commands/deploy.ts
|
|
1222
|
-
import { existsSync as
|
|
1222
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1223
1223
|
import * as path6 from "node:path";
|
|
1224
1224
|
|
|
1225
1225
|
// src/bundle/orchestrate.ts
|
|
1226
|
-
import { readFileSync as
|
|
1227
|
-
import { join as
|
|
1226
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1227
|
+
import { join as join6 } from "node:path";
|
|
1228
1228
|
|
|
1229
1229
|
// src/deploy/tar-pack.ts
|
|
1230
1230
|
import * as fs4 from "node:fs/promises";
|
|
@@ -1777,741 +1777,656 @@ async function buildWorkerArtifact(cwd, manifestYaml, walkFiles) {
|
|
|
1777
1777
|
};
|
|
1778
1778
|
}
|
|
1779
1779
|
|
|
1780
|
-
// src/bundle/
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1780
|
+
// src/bundle/boundary.ts
|
|
1781
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1782
|
+
import { join as join5 } from "node:path";
|
|
1783
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1784
|
+
|
|
1785
|
+
// ../launchpad-engine/dist/schema.js
|
|
1786
|
+
import { z } from "zod";
|
|
1787
|
+
var APP_TYPES2 = ["static", "react", "react+api", "container"];
|
|
1788
|
+
var AUTH_MODES = ["access", "gateway"];
|
|
1789
|
+
var RUNTIMES = ["cloudflare-containers", "aca"];
|
|
1790
|
+
var SECRET_SOURCES = ["env-file", "platform-managed"];
|
|
1791
|
+
var TARGET_KINDS = ["pages", "worker"];
|
|
1792
|
+
var PRIMARY_TARGET = "primary";
|
|
1793
|
+
var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
1794
|
+
var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
1795
|
+
var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
|
|
1796
|
+
var MetadataSchema = z.object({
|
|
1797
|
+
name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
|
|
1798
|
+
team: z.string().min(1),
|
|
1799
|
+
owner: z.string().email(),
|
|
1800
|
+
description: z.string().max(280).optional()
|
|
1801
|
+
}).strict();
|
|
1802
|
+
var DeploymentSchema = z.object({
|
|
1803
|
+
type: z.enum(APP_TYPES2),
|
|
1804
|
+
runtime: z.enum(RUNTIMES).optional()
|
|
1805
|
+
}).strict();
|
|
1806
|
+
var AccessSchema = z.object({
|
|
1807
|
+
allowed_entra_group: z.string().min(1).optional(),
|
|
1808
|
+
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(),
|
|
1809
|
+
session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
|
|
1810
|
+
}).strict().superRefine((access2, ctx) => {
|
|
1811
|
+
const hasSingular = access2.allowed_entra_group !== undefined;
|
|
1812
|
+
const hasPlural = access2.allowed_entra_groups !== undefined;
|
|
1813
|
+
if (!hasSingular && !hasPlural) {
|
|
1814
|
+
ctx.addIssue({
|
|
1815
|
+
code: z.ZodIssueCode.custom,
|
|
1816
|
+
message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
|
|
1817
|
+
path: ["allowed_entra_groups"]
|
|
1818
|
+
});
|
|
1792
1819
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
return {
|
|
1801
|
-
kind: "walk-error",
|
|
1802
|
-
message: `unexpected walk failure: ${e.message}`
|
|
1803
|
-
};
|
|
1820
|
+
if (hasSingular && hasPlural) {
|
|
1821
|
+
ctx.addIssue({
|
|
1822
|
+
code: z.ZodIssueCode.custom,
|
|
1823
|
+
message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
|
|
1824
|
+
path: ["allowed_entra_groups"]
|
|
1825
|
+
});
|
|
1804
1826
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (e instanceof TarPackError) {
|
|
1810
|
-
return { kind: "pack-error", message: e.message };
|
|
1811
|
-
}
|
|
1812
|
-
return {
|
|
1813
|
-
kind: "pack-error",
|
|
1814
|
-
message: `unexpected pack failure: ${e.message}`
|
|
1815
|
-
};
|
|
1827
|
+
});
|
|
1828
|
+
function allowedEntraGroups(access2) {
|
|
1829
|
+
if (access2.allowed_entra_groups !== undefined) {
|
|
1830
|
+
return access2.allowed_entra_groups;
|
|
1816
1831
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1832
|
+
return [access2.allowed_entra_group];
|
|
1833
|
+
}
|
|
1834
|
+
var ContainerSchema = z.object({
|
|
1835
|
+
name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
|
|
1836
|
+
image: z.string().min(1),
|
|
1837
|
+
port: z.number().int().min(1).max(65535),
|
|
1838
|
+
ingress: z.boolean().optional(),
|
|
1839
|
+
healthcheck: z.string().default("/health"),
|
|
1840
|
+
env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
|
|
1841
|
+
max_instances: z.number().int().min(1).max(100).optional()
|
|
1842
|
+
}).strict();
|
|
1843
|
+
var TargetSchema = z.object({
|
|
1844
|
+
kind: z.enum(TARGET_KINDS),
|
|
1845
|
+
script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
|
|
1846
|
+
schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
|
|
1847
|
+
d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
|
|
1848
|
+
}).strict();
|
|
1849
|
+
var SecretBindingSchema = z.object({
|
|
1850
|
+
name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
|
|
1851
|
+
source: z.enum(SECRET_SOURCES),
|
|
1852
|
+
description: z.string().max(200).optional(),
|
|
1853
|
+
targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
|
|
1854
|
+
}).strict();
|
|
1855
|
+
var SecretsSchema = z.object({
|
|
1856
|
+
bindings: z.array(SecretBindingSchema).optional()
|
|
1857
|
+
}).strict();
|
|
1858
|
+
var BuildSchema = z.object({
|
|
1859
|
+
command: z.string().min(1),
|
|
1860
|
+
destination_dir: z.string().min(1),
|
|
1861
|
+
root_dir: z.string()
|
|
1862
|
+
}).strict();
|
|
1863
|
+
var AppBoundarySchema = z.object({
|
|
1864
|
+
root: z.string().min(1).optional(),
|
|
1865
|
+
include: z.array(z.string().min(1)).min(1).optional(),
|
|
1866
|
+
exclude: z.array(z.string().min(1)).optional()
|
|
1867
|
+
}).strict();
|
|
1868
|
+
var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
|
|
1869
|
+
var ManifestSchema = z.object({
|
|
1870
|
+
apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
|
|
1871
|
+
kind: z.literal("App"),
|
|
1872
|
+
metadata: MetadataSchema,
|
|
1873
|
+
deployment: DeploymentSchema,
|
|
1874
|
+
access: AccessSchema,
|
|
1875
|
+
auth: z.enum(AUTH_MODES).optional(),
|
|
1876
|
+
hostnames: z.array(z.string().min(1)).optional(),
|
|
1877
|
+
containers: z.array(ContainerSchema).min(1).optional(),
|
|
1878
|
+
secrets: SecretsSchema.optional(),
|
|
1879
|
+
targets: z.array(TargetSchema).optional(),
|
|
1880
|
+
build: BuildSchema.optional(),
|
|
1881
|
+
production_env: ProductionEnvSchema.optional(),
|
|
1882
|
+
app: AppBoundarySchema.optional()
|
|
1883
|
+
}).strict().superRefine((m, ctx) => {
|
|
1884
|
+
const isContainer = m.deployment.type === "container";
|
|
1885
|
+
if (isContainer && m.auth === "gateway") {
|
|
1886
|
+
ctx.addIssue({
|
|
1887
|
+
code: z.ZodIssueCode.custom,
|
|
1888
|
+
path: ["auth"],
|
|
1889
|
+
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
1890
|
+
});
|
|
1820
1891
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
body: uploadResult.response
|
|
1828
|
-
};
|
|
1892
|
+
if (isContainer && m.containers === undefined) {
|
|
1893
|
+
ctx.addIssue({
|
|
1894
|
+
code: z.ZodIssueCode.custom,
|
|
1895
|
+
path: ["containers"],
|
|
1896
|
+
message: "containers[] is required when deployment.type is 'container'"
|
|
1897
|
+
});
|
|
1829
1898
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
compressedBytes: packResult.bytes.byteLength,
|
|
1836
|
-
workerScript: workerArtifact?.script ?? null
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// src/commands/deploy.ts
|
|
1841
|
-
import { parse as parseYaml3 } from "yaml";
|
|
1842
|
-
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1843
|
-
|
|
1844
|
-
// src/deploy/git-files.ts
|
|
1845
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
1846
|
-
|
|
1847
|
-
class GitFilesError extends Error {
|
|
1848
|
-
code = "git_files_error";
|
|
1849
|
-
}
|
|
1850
|
-
var PROTECTED_PATHS = new Set([
|
|
1851
|
-
".github/workflows/launchpad-review.yml",
|
|
1852
|
-
".github/workflows/ci.yml",
|
|
1853
|
-
"wrangler.toml",
|
|
1854
|
-
".github/scripts/_gha-cmd.mjs",
|
|
1855
|
-
".github/scripts/parse-eslint.mjs",
|
|
1856
|
-
".github/scripts/parse-tsc.mjs",
|
|
1857
|
-
".github/scripts/parse-vitest.mjs",
|
|
1858
|
-
".github/scripts/parse-gitleaks.mjs",
|
|
1859
|
-
".github/scripts/parse-bun-audit.mjs"
|
|
1860
|
-
]);
|
|
1861
|
-
async function listDeployFiles(opts) {
|
|
1862
|
-
const sp = opts.spawner ?? spawn3;
|
|
1863
|
-
const stdout = await runGit2(sp, opts.cwd, [
|
|
1864
|
-
"ls-files",
|
|
1865
|
-
"-c",
|
|
1866
|
-
"-o",
|
|
1867
|
-
"--exclude-standard",
|
|
1868
|
-
"-z"
|
|
1869
|
-
]);
|
|
1870
|
-
const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
|
|
1871
|
-
return [...paths].sort();
|
|
1872
|
-
}
|
|
1873
|
-
function runGit2(sp, cwd, args) {
|
|
1874
|
-
return new Promise((resolve5, reject) => {
|
|
1875
|
-
const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
|
|
1876
|
-
const child = sp("git", [...args], spawnOpts);
|
|
1877
|
-
let stdout = "";
|
|
1878
|
-
let stderr = "";
|
|
1879
|
-
child.stdout?.on("data", (chunk) => {
|
|
1880
|
-
stdout += chunk.toString();
|
|
1899
|
+
if (!isContainer && m.containers !== undefined) {
|
|
1900
|
+
ctx.addIssue({
|
|
1901
|
+
code: z.ZodIssueCode.custom,
|
|
1902
|
+
path: ["containers"],
|
|
1903
|
+
message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
|
|
1881
1904
|
});
|
|
1882
|
-
|
|
1883
|
-
|
|
1905
|
+
}
|
|
1906
|
+
if (!isContainer && m.deployment.runtime !== undefined) {
|
|
1907
|
+
ctx.addIssue({
|
|
1908
|
+
code: z.ZodIssueCode.custom,
|
|
1909
|
+
path: ["deployment", "runtime"],
|
|
1910
|
+
message: "deployment.runtime is only meaningful when deployment.type is 'container'"
|
|
1884
1911
|
});
|
|
1885
|
-
|
|
1886
|
-
|
|
1912
|
+
}
|
|
1913
|
+
if (isContainer && m.build !== undefined) {
|
|
1914
|
+
ctx.addIssue({
|
|
1915
|
+
code: z.ZodIssueCode.custom,
|
|
1916
|
+
path: ["build"],
|
|
1917
|
+
message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
|
|
1887
1918
|
});
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1919
|
+
}
|
|
1920
|
+
if (isContainer && m.production_env !== undefined) {
|
|
1921
|
+
ctx.addIssue({
|
|
1922
|
+
code: z.ZodIssueCode.custom,
|
|
1923
|
+
path: ["production_env"],
|
|
1924
|
+
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."
|
|
1894
1925
|
});
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
let slug = null;
|
|
1905
|
-
let displayName = null;
|
|
1906
|
-
let description = null;
|
|
1907
|
-
let appType = null;
|
|
1908
|
-
const allowedGroups = [];
|
|
1909
|
-
let message = null;
|
|
1910
|
-
let timeoutSeconds = null;
|
|
1911
|
-
let manifestFile = null;
|
|
1912
|
-
let dryRunJson = false;
|
|
1913
|
-
let platformRepo = null;
|
|
1914
|
-
let rePin = false;
|
|
1915
|
-
let yes = false;
|
|
1916
|
-
let resumePrNumber = null;
|
|
1917
|
-
let timeoutMinutes = null;
|
|
1918
|
-
let modeFlagsSeen = 0;
|
|
1919
|
-
let i = 0;
|
|
1920
|
-
while (i < args.length) {
|
|
1921
|
-
const a = args[i] ?? "";
|
|
1922
|
-
if (a === "--new") {
|
|
1923
|
-
mode = "new";
|
|
1924
|
-
modeFlagsSeen += 1;
|
|
1925
|
-
i += 1;
|
|
1926
|
-
continue;
|
|
1927
|
-
}
|
|
1928
|
-
if (a === "--dry-run") {
|
|
1929
|
-
mode = "dry-run";
|
|
1930
|
-
modeFlagsSeen += 1;
|
|
1931
|
-
i += 1;
|
|
1932
|
-
continue;
|
|
1933
|
-
}
|
|
1934
|
-
if (a === "--apply") {
|
|
1935
|
-
mode = "apply";
|
|
1936
|
-
modeFlagsSeen += 1;
|
|
1937
|
-
i += 1;
|
|
1938
|
-
continue;
|
|
1926
|
+
}
|
|
1927
|
+
if (isContainer && m.containers !== undefined && m.containers.length > 1) {
|
|
1928
|
+
const ingressContainers = m.containers.filter((c) => c.ingress === true);
|
|
1929
|
+
if (ingressContainers.length !== 1) {
|
|
1930
|
+
ctx.addIssue({
|
|
1931
|
+
code: z.ZodIssueCode.custom,
|
|
1932
|
+
path: ["containers"],
|
|
1933
|
+
message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
|
|
1934
|
+
});
|
|
1939
1935
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1936
|
+
}
|
|
1937
|
+
if (isContainer && m.containers !== undefined) {
|
|
1938
|
+
const declaredBindings = new Set((m.secrets?.bindings ?? []).map((b) => b.name));
|
|
1939
|
+
for (let i = 0;i < m.containers.length; i++) {
|
|
1940
|
+
const c = m.containers[i];
|
|
1941
|
+
for (const envName of c.env ?? []) {
|
|
1942
|
+
if (!declaredBindings.has(envName)) {
|
|
1943
|
+
ctx.addIssue({
|
|
1944
|
+
code: z.ZodIssueCode.custom,
|
|
1945
|
+
path: ["containers", i, "env"],
|
|
1946
|
+
message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1944
1949
|
}
|
|
1945
|
-
platformRepo = v.trim();
|
|
1946
|
-
i += 2;
|
|
1947
|
-
continue;
|
|
1948
1950
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1951
|
+
}
|
|
1952
|
+
if (isContainer && m.containers !== undefined) {
|
|
1953
|
+
const seen = new Set;
|
|
1954
|
+
for (let i = 0;i < m.containers.length; i++) {
|
|
1955
|
+
const name = m.containers[i]?.name;
|
|
1956
|
+
if (name === undefined)
|
|
1957
|
+
continue;
|
|
1958
|
+
if (seen.has(name)) {
|
|
1959
|
+
ctx.addIssue({
|
|
1960
|
+
code: z.ZodIssueCode.custom,
|
|
1961
|
+
path: ["containers", i, "name"],
|
|
1962
|
+
message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
seen.add(name);
|
|
1953
1966
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1967
|
+
}
|
|
1968
|
+
const bindings = m.secrets?.bindings ?? [];
|
|
1969
|
+
if (bindings.length > 0) {
|
|
1970
|
+
const seen = new Set;
|
|
1971
|
+
for (let i = 0;i < bindings.length; i++) {
|
|
1972
|
+
const name = bindings[i]?.name;
|
|
1973
|
+
if (name === undefined)
|
|
1974
|
+
continue;
|
|
1975
|
+
if (seen.has(name)) {
|
|
1976
|
+
ctx.addIssue({
|
|
1977
|
+
code: z.ZodIssueCode.custom,
|
|
1978
|
+
path: ["secrets", "bindings", i, "name"],
|
|
1979
|
+
message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
seen.add(name);
|
|
1958
1983
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1984
|
+
}
|
|
1985
|
+
const targets = m.targets ?? [];
|
|
1986
|
+
for (let i = 0;i < targets.length; i++) {
|
|
1987
|
+
const t = targets[i];
|
|
1988
|
+
if (t.kind === "worker" && t.script === undefined) {
|
|
1989
|
+
ctx.addIssue({
|
|
1990
|
+
code: z.ZodIssueCode.custom,
|
|
1991
|
+
path: ["targets", i, "script"],
|
|
1992
|
+
message: "a worker target must declare a script (the sibling Workers script name)"
|
|
1993
|
+
});
|
|
1968
1994
|
}
|
|
1969
|
-
if (
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
modeFlagsSeen += 1;
|
|
1976
|
-
i += 2;
|
|
1977
|
-
continue;
|
|
1995
|
+
if (t.kind === "pages" && t.script !== undefined) {
|
|
1996
|
+
ctx.addIssue({
|
|
1997
|
+
code: z.ZodIssueCode.custom,
|
|
1998
|
+
path: ["targets", i, "script"],
|
|
1999
|
+
message: "a pages target must not declare a script — the primary Pages project is the app slug"
|
|
2000
|
+
});
|
|
1978
2001
|
}
|
|
1979
|
-
if (
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
continue;
|
|
2002
|
+
if (t.kind === "pages" && t.schedule !== undefined) {
|
|
2003
|
+
ctx.addIssue({
|
|
2004
|
+
code: z.ZodIssueCode.custom,
|
|
2005
|
+
path: ["targets", i, "schedule"],
|
|
2006
|
+
message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
|
|
2007
|
+
});
|
|
1986
2008
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2009
|
+
}
|
|
2010
|
+
const seenScripts = new Set;
|
|
2011
|
+
let pagesTargets = 0;
|
|
2012
|
+
for (let i = 0;i < targets.length; i++) {
|
|
2013
|
+
const t = targets[i];
|
|
2014
|
+
if (t.kind === "pages") {
|
|
2015
|
+
pagesTargets++;
|
|
2016
|
+
if (pagesTargets > 1) {
|
|
2017
|
+
ctx.addIssue({
|
|
2018
|
+
code: z.ZodIssueCode.custom,
|
|
2019
|
+
path: ["targets", i, "kind"],
|
|
2020
|
+
message: "at most one pages target may be declared (the primary Pages project is singular)"
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
1990
2023
|
continue;
|
|
1991
2024
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
const v = args[i + 1];
|
|
1994
|
-
if (v === undefined)
|
|
1995
|
-
return `missing value for ${a}`;
|
|
1996
|
-
slug = v;
|
|
1997
|
-
i += 2;
|
|
2025
|
+
if (t.script === undefined)
|
|
1998
2026
|
continue;
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
i += 2;
|
|
2027
|
+
if (t.script === PRIMARY_TARGET || t.script === "pages") {
|
|
2028
|
+
ctx.addIssue({
|
|
2029
|
+
code: z.ZodIssueCode.custom,
|
|
2030
|
+
path: ["targets", i, "script"],
|
|
2031
|
+
message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
|
|
2032
|
+
});
|
|
2006
2033
|
continue;
|
|
2007
2034
|
}
|
|
2008
|
-
if (
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
continue;
|
|
2035
|
+
if (seenScripts.has(t.script)) {
|
|
2036
|
+
ctx.addIssue({
|
|
2037
|
+
code: z.ZodIssueCode.custom,
|
|
2038
|
+
path: ["targets", i, "script"],
|
|
2039
|
+
message: `duplicate worker target script '${t.script}' — target scripts must be unique`
|
|
2040
|
+
});
|
|
2015
2041
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2042
|
+
seenScripts.add(t.script);
|
|
2043
|
+
}
|
|
2044
|
+
const validRefs = new Set([PRIMARY_TARGET, ...seenScripts]);
|
|
2045
|
+
if (pagesTargets > 0)
|
|
2046
|
+
validRefs.add("pages");
|
|
2047
|
+
for (let bi = 0;bi < bindings.length; bi++) {
|
|
2048
|
+
const b = bindings[bi];
|
|
2049
|
+
const sel = b?.targets;
|
|
2050
|
+
if (sel === undefined || sel === "all")
|
|
2025
2051
|
continue;
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2052
|
+
for (const ref of sel) {
|
|
2053
|
+
if (!validRefs.has(ref)) {
|
|
2054
|
+
ctx.addIssue({
|
|
2055
|
+
code: z.ZodIssueCode.custom,
|
|
2056
|
+
path: ["secrets", "bindings", bi, "targets"],
|
|
2057
|
+
message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
|
|
2058
|
+
});
|
|
2033
2059
|
}
|
|
2034
|
-
allowedGroups.push(v);
|
|
2035
|
-
i += 2;
|
|
2036
|
-
continue;
|
|
2037
2060
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
function parseManifest(input) {
|
|
2064
|
+
const result = ManifestSchema.safeParse(input);
|
|
2065
|
+
if (result.success) {
|
|
2066
|
+
return { kind: "ok", manifest: result.data };
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
kind: "schema-error",
|
|
2070
|
+
issues: result.error.issues.map((issue) => ({
|
|
2071
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
2072
|
+
message: issue.message
|
|
2073
|
+
}))
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
// ../launchpad-engine/dist/app-boundary.js
|
|
2077
|
+
var INCLUDE_EVERYTHING = ["**"];
|
|
2078
|
+
var COMMON_INCLUDE = [
|
|
2079
|
+
"launchpad.yaml",
|
|
2080
|
+
".gitignore",
|
|
2081
|
+
".launchpadignore",
|
|
2082
|
+
"package.json",
|
|
2083
|
+
"package-lock.json",
|
|
2084
|
+
"bun.lock",
|
|
2085
|
+
"bun.lockb",
|
|
2086
|
+
"yarn.lock",
|
|
2087
|
+
"pnpm-lock.yaml",
|
|
2088
|
+
"tsconfig.json",
|
|
2089
|
+
"tsconfig.*.json",
|
|
2090
|
+
"README.md",
|
|
2091
|
+
"LICENSE",
|
|
2092
|
+
"index.html",
|
|
2093
|
+
"vite.config.*",
|
|
2094
|
+
"src/**",
|
|
2095
|
+
"public/**",
|
|
2096
|
+
"worker/**"
|
|
2097
|
+
];
|
|
2098
|
+
function defaultIncludeForAppType(appType) {
|
|
2099
|
+
switch (appType) {
|
|
2100
|
+
case "static":
|
|
2101
|
+
return [...COMMON_INCLUDE, "static/**", "assets/**", "css/**", "js/**", "images/**", "*.html", "*.css", "*.js"];
|
|
2102
|
+
case "react":
|
|
2103
|
+
return COMMON_INCLUDE;
|
|
2104
|
+
case "react+api":
|
|
2105
|
+
return [...COMMON_INCLUDE, "functions/**"];
|
|
2106
|
+
case "container":
|
|
2107
|
+
return [...COMMON_INCLUDE, "container/**", "Dockerfile", "Dockerfile.*", "wrangler.toml"];
|
|
2108
|
+
default:
|
|
2109
|
+
return INCLUDE_EVERYTHING;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function resolveAppBoundary(input) {
|
|
2113
|
+
if (input.app === undefined) {
|
|
2114
|
+
return { root: ".", include: INCLUDE_EVERYTHING, exclude: [], source: "inferred" };
|
|
2115
|
+
}
|
|
2116
|
+
return {
|
|
2117
|
+
root: normaliseRoot(input.app.root ?? "."),
|
|
2118
|
+
include: input.app.include ?? defaultIncludeForAppType(input.appType),
|
|
2119
|
+
exclude: input.app.exclude ?? [],
|
|
2120
|
+
source: "declared"
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
function normaliseRoot(root) {
|
|
2124
|
+
let r = root.replace(/\\/g, "/").trim();
|
|
2125
|
+
if (r.startsWith("./"))
|
|
2126
|
+
r = r.slice(2);
|
|
2127
|
+
while (r.endsWith("/"))
|
|
2128
|
+
r = r.slice(0, -1);
|
|
2129
|
+
return r === "" ? "." : r;
|
|
2130
|
+
}
|
|
2131
|
+
var APP_TYPE_SET = new Set(["static", "react", "react+api", "container"]);
|
|
2132
|
+
function extractBoundaryInput(parsedManifest) {
|
|
2133
|
+
const m = asRecord(parsedManifest);
|
|
2134
|
+
if (m === undefined) {
|
|
2135
|
+
return { kind: "ok", appType: undefined, app: undefined, build: undefined };
|
|
2136
|
+
}
|
|
2137
|
+
const spec = asRecord(m["spec"]);
|
|
2138
|
+
const v2 = spec !== undefined;
|
|
2139
|
+
const rawType = v2 ? spec["appType"] : asRecord(m["deployment"])?.["type"];
|
|
2140
|
+
const appType = typeof rawType === "string" && APP_TYPE_SET.has(rawType) ? rawType : undefined;
|
|
2141
|
+
const rawApp = v2 ? spec["app"] : m["app"];
|
|
2142
|
+
let app;
|
|
2143
|
+
if (rawApp !== undefined) {
|
|
2144
|
+
const parsed = AppBoundarySchema.safeParse(rawApp);
|
|
2145
|
+
if (!parsed.success) {
|
|
2146
|
+
return {
|
|
2147
|
+
kind: "schema-error",
|
|
2148
|
+
issues: parsed.error.issues.map((i) => ({
|
|
2149
|
+
path: `${v2 ? "spec." : ""}app${i.path.length > 0 ? "." + i.path.map(String).join(".") : ""}`,
|
|
2150
|
+
message: i.message
|
|
2151
|
+
}))
|
|
2152
|
+
};
|
|
2045
2153
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (v === undefined)
|
|
2061
|
-
return `missing value for ${a}`;
|
|
2062
|
-
const n = Number(v);
|
|
2063
|
-
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
2064
|
-
return `invalid --timeout-minutes "${v}" — expected positive integer`;
|
|
2065
|
-
}
|
|
2066
|
-
timeoutMinutes = n;
|
|
2067
|
-
i += 2;
|
|
2154
|
+
app = parsed.data;
|
|
2155
|
+
}
|
|
2156
|
+
const rawBuild = asRecord(v2 ? spec["build"] : m["build"]);
|
|
2157
|
+
const build = rawBuild !== undefined && typeof rawBuild["command"] === "string" && typeof rawBuild["root_dir"] === "string" ? { command: rawBuild["command"], root_dir: rawBuild["root_dir"] } : undefined;
|
|
2158
|
+
return { kind: "ok", appType, app, build };
|
|
2159
|
+
}
|
|
2160
|
+
function asRecord(v) {
|
|
2161
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
2162
|
+
}
|
|
2163
|
+
function parseLaunchpadIgnore(content) {
|
|
2164
|
+
const out = [];
|
|
2165
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
2166
|
+
const line = raw.trim();
|
|
2167
|
+
if (line === "" || line.startsWith("#") || line.startsWith("!"))
|
|
2068
2168
|
continue;
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2169
|
+
out.push(line);
|
|
2170
|
+
}
|
|
2171
|
+
return out;
|
|
2172
|
+
}
|
|
2173
|
+
function globToRegExp2(glob) {
|
|
2174
|
+
let re = "";
|
|
2175
|
+
let i = 0;
|
|
2176
|
+
while (i < glob.length) {
|
|
2177
|
+
const ch = glob[i];
|
|
2178
|
+
if (ch === "*") {
|
|
2179
|
+
if (glob[i + 1] === "*") {
|
|
2180
|
+
if (glob[i + 2] === "/") {
|
|
2181
|
+
re += "(?:.*/)?";
|
|
2182
|
+
i += 3;
|
|
2183
|
+
} else {
|
|
2184
|
+
re += ".*";
|
|
2185
|
+
i += 2;
|
|
2186
|
+
}
|
|
2187
|
+
} else {
|
|
2188
|
+
re += "[^/]*";
|
|
2189
|
+
i += 1;
|
|
2077
2190
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2191
|
+
} else if (ch === "?") {
|
|
2192
|
+
re += "[^/]";
|
|
2193
|
+
i += 1;
|
|
2194
|
+
} else if (ch === "/" || /[A-Za-z0-9_-]/.test(ch)) {
|
|
2195
|
+
re += ch;
|
|
2196
|
+
i += 1;
|
|
2197
|
+
} else {
|
|
2198
|
+
re += `\\${ch}`;
|
|
2199
|
+
i += 1;
|
|
2081
2200
|
}
|
|
2082
|
-
return `unknown argument "${a}"`;
|
|
2083
|
-
}
|
|
2084
|
-
if (modeFlagsSeen > 1) {
|
|
2085
|
-
return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
|
|
2086
|
-
}
|
|
2087
|
-
if (mode === "dry-run") {
|
|
2088
|
-
if (slug !== null)
|
|
2089
|
-
return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
|
|
2090
|
-
if (displayName !== null)
|
|
2091
|
-
return "--dry-run does not accept --display-name";
|
|
2092
|
-
if (description !== null)
|
|
2093
|
-
return "--dry-run does not accept --description";
|
|
2094
|
-
if (appType !== null)
|
|
2095
|
-
return "--dry-run does not accept --app-type";
|
|
2096
|
-
if (allowedGroups.length > 0)
|
|
2097
|
-
return "--dry-run does not accept --allowed-group (read from launchpad.yaml)";
|
|
2098
|
-
if (message !== null)
|
|
2099
|
-
return "--dry-run does not accept --message";
|
|
2100
|
-
if (timeoutSeconds !== null)
|
|
2101
|
-
return "--dry-run does not accept --timeout-seconds";
|
|
2102
|
-
if (platformRepo !== null)
|
|
2103
|
-
return "--dry-run does not accept --platform-repo (apply-only)";
|
|
2104
|
-
if (rePin)
|
|
2105
|
-
return "--dry-run does not accept --re-pin (apply-only)";
|
|
2106
|
-
if (yes)
|
|
2107
|
-
return "--dry-run does not accept --yes (apply-only)";
|
|
2108
|
-
return {
|
|
2109
|
-
mode: { kind: "dry-run", file: manifestFile, json: dryRunJson },
|
|
2110
|
-
message: null,
|
|
2111
|
-
timeoutSeconds: null
|
|
2112
|
-
};
|
|
2113
2201
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (appType !== null)
|
|
2122
|
-
return "--apply does not accept --app-type";
|
|
2123
|
-
if (allowedGroups.length > 0)
|
|
2124
|
-
return "--apply does not accept --allowed-group (read from launchpad.yaml)";
|
|
2125
|
-
if (message !== null)
|
|
2126
|
-
return "--apply does not accept --message";
|
|
2127
|
-
if (timeoutSeconds !== null)
|
|
2128
|
-
return "--apply does not accept --timeout-seconds (use --timeout-minutes instead — apply polling is multi-minute)";
|
|
2129
|
-
if (dryRunJson)
|
|
2130
|
-
return "--apply does not accept --json";
|
|
2131
|
-
return {
|
|
2132
|
-
mode: {
|
|
2133
|
-
kind: "apply",
|
|
2134
|
-
file: manifestFile,
|
|
2135
|
-
platformRepo,
|
|
2136
|
-
rePin,
|
|
2137
|
-
yes,
|
|
2138
|
-
resumePrNumber,
|
|
2139
|
-
timeoutMinutes
|
|
2140
|
-
},
|
|
2141
|
-
message: null,
|
|
2142
|
-
timeoutSeconds: null
|
|
2143
|
-
};
|
|
2202
|
+
return new RegExp(`^${re}$`);
|
|
2203
|
+
}
|
|
2204
|
+
var GLOB_CHARS = /[*?]/;
|
|
2205
|
+
function matchContractPattern(pattern, relPath) {
|
|
2206
|
+
const p = pattern.replace(/\/+$/, "");
|
|
2207
|
+
if (!GLOB_CHARS.test(p)) {
|
|
2208
|
+
return relPath === p || relPath.startsWith(`${p}/`);
|
|
2144
2209
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2210
|
+
return globToRegExp2(p).test(relPath);
|
|
2211
|
+
}
|
|
2212
|
+
function matchIgnorePattern(pattern, relPath) {
|
|
2213
|
+
let p = pattern;
|
|
2214
|
+
const dirOnly = p.endsWith("/");
|
|
2215
|
+
if (dirOnly)
|
|
2216
|
+
p = p.replace(/\/+$/, "");
|
|
2217
|
+
const anchored = p.startsWith("/");
|
|
2218
|
+
if (anchored)
|
|
2219
|
+
p = p.slice(1);
|
|
2220
|
+
if (!anchored && !p.includes("/")) {
|
|
2221
|
+
const re2 = globToRegExp2(p);
|
|
2222
|
+
const segments = relPath.split("/");
|
|
2223
|
+
const candidates = dirOnly ? segments.slice(0, -1) : segments;
|
|
2224
|
+
return candidates.some((seg) => re2.test(seg));
|
|
2147
2225
|
}
|
|
2148
|
-
if (
|
|
2149
|
-
return
|
|
2226
|
+
if (!GLOB_CHARS.test(p)) {
|
|
2227
|
+
return !dirOnly && relPath === p || relPath.startsWith(`${p}/`);
|
|
2150
2228
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2229
|
+
const re = globToRegExp2(p);
|
|
2230
|
+
if (dirOnly)
|
|
2231
|
+
return re.test(relPath.split("/").slice(0, -1).join("/"));
|
|
2232
|
+
return re.test(relPath) || re.test(relPath.split("/").slice(0, -1).join("/"));
|
|
2233
|
+
}
|
|
2234
|
+
var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
|
|
2235
|
+
".github/workflows/launchpad-review.yml",
|
|
2236
|
+
".github/workflows/ci.yml",
|
|
2237
|
+
".github/scripts/_gha-cmd.mjs",
|
|
2238
|
+
".github/scripts/parse-eslint.mjs",
|
|
2239
|
+
".github/scripts/parse-tsc.mjs",
|
|
2240
|
+
".github/scripts/parse-vitest.mjs",
|
|
2241
|
+
".github/scripts/parse-gitleaks.mjs",
|
|
2242
|
+
".github/scripts/parse-bun-audit.mjs"
|
|
2243
|
+
]);
|
|
2244
|
+
function checkDenyList(path6) {
|
|
2245
|
+
const segments = path6.split("/");
|
|
2246
|
+
const basename = segments[segments.length - 1];
|
|
2247
|
+
const isExample = basename.endsWith(".example");
|
|
2248
|
+
if (basename.startsWith(".env") && !isExample) {
|
|
2249
|
+
return { path: path6, rule: "env-file", message: `'${path6}' is an environment file (.env*) — never shippable` };
|
|
2153
2250
|
}
|
|
2154
|
-
if (
|
|
2155
|
-
return "
|
|
2251
|
+
if (basename === ".npmrc") {
|
|
2252
|
+
return { path: path6, rule: "npmrc", message: `'${path6}' (.npmrc) may carry registry auth — never shippable` };
|
|
2156
2253
|
}
|
|
2157
|
-
if (
|
|
2158
|
-
return "
|
|
2254
|
+
if (basename.startsWith(".dev.vars") && !isExample) {
|
|
2255
|
+
return { path: path6, rule: "dev-vars", message: `'${path6}' is a wrangler dev-vars file — never shippable` };
|
|
2159
2256
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
return "
|
|
2257
|
+
if (segments.includes(".claude")) {
|
|
2258
|
+
return { path: path6, rule: "claude-dir", message: `'${path6}' is under a .claude/ directory (AI-workspace state) — never shippable` };
|
|
2162
2259
|
}
|
|
2163
|
-
if (
|
|
2164
|
-
return "
|
|
2260
|
+
if (segments.includes(".git")) {
|
|
2261
|
+
return { path: path6, rule: "git-dir", message: `'${path6}' is under .git/ — never shippable` };
|
|
2165
2262
|
}
|
|
2166
|
-
if (
|
|
2167
|
-
if (
|
|
2168
|
-
return
|
|
2169
|
-
}
|
|
2263
|
+
if (segments[0] === ".github") {
|
|
2264
|
+
if (PLATFORM_SEEDED_GITHUB_PATHS.has(path6))
|
|
2265
|
+
return "seeded";
|
|
2170
2266
|
return {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2267
|
+
path: path6,
|
|
2268
|
+
rule: "github-dir",
|
|
2269
|
+
message: `'${path6}' — repo-root .github/ is platform-owned (workflows execute under the M-KOPA Actions identity); developer-supplied .github/** is not shippable`
|
|
2174
2270
|
};
|
|
2175
2271
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
function checkSecretShape(path6, readFileContent) {
|
|
2275
|
+
const segments = path6.split("/");
|
|
2276
|
+
const basename = segments[segments.length - 1];
|
|
2277
|
+
const deny = (what) => ({
|
|
2278
|
+
path: path6,
|
|
2279
|
+
rule: "secret-shape",
|
|
2280
|
+
message: `'${path6}' looks like ${what} — never shippable; if this is a false positive, rename the file`
|
|
2281
|
+
});
|
|
2282
|
+
if (basename.endsWith(".pem"))
|
|
2283
|
+
return deny("PEM key material (*.pem)");
|
|
2284
|
+
if (basename.endsWith(".key"))
|
|
2285
|
+
return deny("private key material (*.key)");
|
|
2286
|
+
if (basename.startsWith("id_rsa"))
|
|
2287
|
+
return deny("an SSH key (id_rsa*)");
|
|
2288
|
+
if (basename.endsWith(".p12") || basename.endsWith(".pfx")) {
|
|
2289
|
+
return deny("a PKCS#12 key store (*.p12 / *.pfx)");
|
|
2290
|
+
}
|
|
2291
|
+
if (basename === "credentials.json")
|
|
2292
|
+
return deny("a credentials file (credentials.json)");
|
|
2293
|
+
if (segments.includes(".aws"))
|
|
2294
|
+
return deny("AWS credentials (.aws/**)");
|
|
2295
|
+
if (basename.endsWith(".json") && readFileContent !== undefined) {
|
|
2296
|
+
const content = readFileContent(path6);
|
|
2297
|
+
if (content !== undefined && /"private_key"\s*:/.test(content)) {
|
|
2298
|
+
return deny('service-account-shaped JSON (carries a "private_key" field)');
|
|
2179
2299
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2300
|
+
}
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
var ALWAYS_SHIP = "launchpad.yaml";
|
|
2304
|
+
function applyAppBoundary(contract, files, options = {}) {
|
|
2305
|
+
const ignorePatterns = options.launchpadIgnore ?? [];
|
|
2306
|
+
const included = [];
|
|
2307
|
+
const excluded = [];
|
|
2308
|
+
const denied = [];
|
|
2309
|
+
const rootPrefix = contract.root === "." ? "" : `${contract.root}/`;
|
|
2310
|
+
for (const path6 of [...files].sort()) {
|
|
2311
|
+
if (path6 !== ALWAYS_SHIP) {
|
|
2312
|
+
if (rootPrefix !== "" && !path6.startsWith(rootPrefix)) {
|
|
2313
|
+
excluded.push({ path: path6, reason: "outside-root" });
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
const rel = rootPrefix === "" ? path6 : path6.slice(rootPrefix.length);
|
|
2317
|
+
if (!contract.include.some((p) => matchContractPattern(p, rel))) {
|
|
2318
|
+
excluded.push({ path: path6, reason: "not-included" });
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (contract.exclude.some((p) => matchContractPattern(p, rel))) {
|
|
2322
|
+
excluded.push({ path: path6, reason: "exclude" });
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (ignorePatterns.some((p) => matchIgnorePattern(p, path6))) {
|
|
2326
|
+
excluded.push({ path: path6, reason: "launchpadignore" });
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2182
2329
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2330
|
+
const denial = checkDenyList(path6);
|
|
2331
|
+
if (denial === "seeded") {
|
|
2332
|
+
excluded.push({ path: path6, reason: "platform-seeded" });
|
|
2333
|
+
continue;
|
|
2185
2334
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
if (slug.length < 3 || slug.length > 58) {
|
|
2197
|
-
return `slug length out of bounds (3–58 chars): "${slug}"`;
|
|
2198
|
-
}
|
|
2199
|
-
if (displayName === null || displayName.length === 0) {
|
|
2200
|
-
return "--new requires --display-name";
|
|
2201
|
-
}
|
|
2202
|
-
if (appType === null) {
|
|
2203
|
-
return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
|
|
2204
|
-
}
|
|
2205
|
-
if (allowedGroups.length === 0) {
|
|
2206
|
-
return "--new requires at least one --allowed-group";
|
|
2335
|
+
if (denial !== null) {
|
|
2336
|
+
denied.push(denial);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
const secret = checkSecretShape(path6, options.readFileContent);
|
|
2340
|
+
if (secret !== null) {
|
|
2341
|
+
denied.push(secret);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
included.push(path6);
|
|
2207
2345
|
}
|
|
2208
|
-
return {
|
|
2209
|
-
mode: {
|
|
2210
|
-
kind: "new",
|
|
2211
|
-
slug,
|
|
2212
|
-
displayName,
|
|
2213
|
-
description,
|
|
2214
|
-
appType,
|
|
2215
|
-
allowedGroups
|
|
2216
|
-
},
|
|
2217
|
-
message,
|
|
2218
|
-
timeoutSeconds
|
|
2219
|
-
};
|
|
2346
|
+
return { files: included, excluded, denied };
|
|
2220
2347
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
|
|
2242
|
-
var MetadataSchema = z.object({
|
|
2243
|
-
name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
|
|
2244
|
-
team: z.string().min(1),
|
|
2245
|
-
owner: z.string().email(),
|
|
2246
|
-
description: z.string().max(280).optional()
|
|
2247
|
-
}).strict();
|
|
2248
|
-
var DeploymentSchema = z.object({
|
|
2249
|
-
type: z.enum(APP_TYPES2),
|
|
2250
|
-
runtime: z.enum(RUNTIMES).optional()
|
|
2251
|
-
}).strict();
|
|
2252
|
-
var AccessSchema = z.object({
|
|
2253
|
-
allowed_entra_group: z.string().min(1).optional(),
|
|
2254
|
-
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(),
|
|
2255
|
-
session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
|
|
2256
|
-
}).strict().superRefine((access2, ctx) => {
|
|
2257
|
-
const hasSingular = access2.allowed_entra_group !== undefined;
|
|
2258
|
-
const hasPlural = access2.allowed_entra_groups !== undefined;
|
|
2259
|
-
if (!hasSingular && !hasPlural) {
|
|
2260
|
-
ctx.addIssue({
|
|
2261
|
-
code: z.ZodIssueCode.custom,
|
|
2262
|
-
message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
|
|
2263
|
-
path: ["allowed_entra_groups"]
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
if (hasSingular && hasPlural) {
|
|
2267
|
-
ctx.addIssue({
|
|
2268
|
-
code: z.ZodIssueCode.custom,
|
|
2269
|
-
message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
|
|
2270
|
-
path: ["allowed_entra_groups"]
|
|
2348
|
+
var PLAIN_PATH = /^(\.\/)?[A-Za-z0-9_@][A-Za-z0-9_@.\/-]*$/;
|
|
2349
|
+
var OPAQUE_SEGMENT = /[$`(){}<]/;
|
|
2350
|
+
function verifyBuildInputs(build, files) {
|
|
2351
|
+
if (build === undefined)
|
|
2352
|
+
return [];
|
|
2353
|
+
const issues = [];
|
|
2354
|
+
const fileSet = new Set(files);
|
|
2355
|
+
const dirSet = new Set;
|
|
2356
|
+
for (const f of files) {
|
|
2357
|
+
const parts = f.split("/");
|
|
2358
|
+
for (let i = 1;i < parts.length; i++) {
|
|
2359
|
+
dirSet.add(parts.slice(0, i).join("/"));
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const exists = (p) => fileSet.has(p) || dirSet.has(p);
|
|
2363
|
+
const rootDir = normaliseRoot(build.root_dir === "" ? "." : build.root_dir);
|
|
2364
|
+
if (rootDir !== "." && !dirSet.has(rootDir)) {
|
|
2365
|
+
issues.push({
|
|
2366
|
+
input: rootDir,
|
|
2367
|
+
message: `build.root_dir '${rootDir}' does not exist in the bundle — the build would run in a missing directory`
|
|
2271
2368
|
});
|
|
2369
|
+
return issues;
|
|
2272
2370
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
|
|
2293
|
-
d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
|
|
2294
|
-
}).strict();
|
|
2295
|
-
var SecretBindingSchema = z.object({
|
|
2296
|
-
name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
|
|
2297
|
-
source: z.enum(SECRET_SOURCES),
|
|
2298
|
-
description: z.string().max(200).optional(),
|
|
2299
|
-
targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
|
|
2300
|
-
}).strict();
|
|
2301
|
-
var SecretsSchema = z.object({
|
|
2302
|
-
bindings: z.array(SecretBindingSchema).optional()
|
|
2303
|
-
}).strict();
|
|
2304
|
-
var BuildSchema = z.object({
|
|
2305
|
-
command: z.string().min(1),
|
|
2306
|
-
destination_dir: z.string().min(1),
|
|
2307
|
-
root_dir: z.string()
|
|
2308
|
-
}).strict();
|
|
2309
|
-
var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
|
|
2310
|
-
var ManifestSchema = z.object({
|
|
2311
|
-
apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
|
|
2312
|
-
kind: z.literal("App"),
|
|
2313
|
-
metadata: MetadataSchema,
|
|
2314
|
-
deployment: DeploymentSchema,
|
|
2315
|
-
access: AccessSchema,
|
|
2316
|
-
auth: z.enum(AUTH_MODES).optional(),
|
|
2317
|
-
hostnames: z.array(z.string().min(1)).optional(),
|
|
2318
|
-
containers: z.array(ContainerSchema).min(1).optional(),
|
|
2319
|
-
secrets: SecretsSchema.optional(),
|
|
2320
|
-
targets: z.array(TargetSchema).optional(),
|
|
2321
|
-
build: BuildSchema.optional(),
|
|
2322
|
-
production_env: ProductionEnvSchema.optional()
|
|
2323
|
-
}).strict().superRefine((m, ctx) => {
|
|
2324
|
-
const isContainer = m.deployment.type === "container";
|
|
2325
|
-
if (isContainer && m.auth === "gateway") {
|
|
2326
|
-
ctx.addIssue({
|
|
2327
|
-
code: z.ZodIssueCode.custom,
|
|
2328
|
-
path: ["auth"],
|
|
2329
|
-
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
2330
|
-
});
|
|
2331
|
-
}
|
|
2332
|
-
if (isContainer && m.containers === undefined) {
|
|
2333
|
-
ctx.addIssue({
|
|
2334
|
-
code: z.ZodIssueCode.custom,
|
|
2335
|
-
path: ["containers"],
|
|
2336
|
-
message: "containers[] is required when deployment.type is 'container'"
|
|
2337
|
-
});
|
|
2338
|
-
}
|
|
2339
|
-
if (!isContainer && m.containers !== undefined) {
|
|
2340
|
-
ctx.addIssue({
|
|
2341
|
-
code: z.ZodIssueCode.custom,
|
|
2342
|
-
path: ["containers"],
|
|
2343
|
-
message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
if (!isContainer && m.deployment.runtime !== undefined) {
|
|
2347
|
-
ctx.addIssue({
|
|
2348
|
-
code: z.ZodIssueCode.custom,
|
|
2349
|
-
path: ["deployment", "runtime"],
|
|
2350
|
-
message: "deployment.runtime is only meaningful when deployment.type is 'container'"
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
if (isContainer && m.build !== undefined) {
|
|
2354
|
-
ctx.addIssue({
|
|
2355
|
-
code: z.ZodIssueCode.custom,
|
|
2356
|
-
path: ["build"],
|
|
2357
|
-
message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
|
|
2358
|
-
});
|
|
2359
|
-
}
|
|
2360
|
-
if (isContainer && m.production_env !== undefined) {
|
|
2361
|
-
ctx.addIssue({
|
|
2362
|
-
code: z.ZodIssueCode.custom,
|
|
2363
|
-
path: ["production_env"],
|
|
2364
|
-
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."
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
if (isContainer && m.containers !== undefined && m.containers.length > 1) {
|
|
2368
|
-
const ingressContainers = m.containers.filter((c) => c.ingress === true);
|
|
2369
|
-
if (ingressContainers.length !== 1) {
|
|
2370
|
-
ctx.addIssue({
|
|
2371
|
-
code: z.ZodIssueCode.custom,
|
|
2372
|
-
path: ["containers"],
|
|
2373
|
-
message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
if (isContainer && m.containers !== undefined) {
|
|
2378
|
-
const declaredBindings = new Set((m.secrets?.bindings ?? []).map((b) => b.name));
|
|
2379
|
-
for (let i = 0;i < m.containers.length; i++) {
|
|
2380
|
-
const c = m.containers[i];
|
|
2381
|
-
for (const envName of c.env ?? []) {
|
|
2382
|
-
if (!declaredBindings.has(envName)) {
|
|
2383
|
-
ctx.addIssue({
|
|
2384
|
-
code: z.ZodIssueCode.custom,
|
|
2385
|
-
path: ["containers", i, "env"],
|
|
2386
|
-
message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
|
|
2387
|
-
});
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
if (isContainer && m.containers !== undefined) {
|
|
2393
|
-
const seen = new Set;
|
|
2394
|
-
for (let i = 0;i < m.containers.length; i++) {
|
|
2395
|
-
const name = m.containers[i]?.name;
|
|
2396
|
-
if (name === undefined)
|
|
2397
|
-
continue;
|
|
2398
|
-
if (seen.has(name)) {
|
|
2399
|
-
ctx.addIssue({
|
|
2400
|
-
code: z.ZodIssueCode.custom,
|
|
2401
|
-
path: ["containers", i, "name"],
|
|
2402
|
-
message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
|
|
2403
|
-
});
|
|
2404
|
-
}
|
|
2405
|
-
seen.add(name);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
const bindings = m.secrets?.bindings ?? [];
|
|
2409
|
-
if (bindings.length > 0) {
|
|
2410
|
-
const seen = new Set;
|
|
2411
|
-
for (let i = 0;i < bindings.length; i++) {
|
|
2412
|
-
const name = bindings[i]?.name;
|
|
2413
|
-
if (name === undefined)
|
|
2371
|
+
let cwd = rootDir === "." ? "" : rootDir;
|
|
2372
|
+
const resolve5 = (p) => {
|
|
2373
|
+
const cleaned = p.startsWith("./") ? p.slice(2) : p;
|
|
2374
|
+
const trimmed = cleaned.replace(/\/+$/, "");
|
|
2375
|
+
return cwd === "" ? trimmed : `${cwd}/${trimmed}`;
|
|
2376
|
+
};
|
|
2377
|
+
const segments = build.command.split(/&&|\|\||[;|\n]/);
|
|
2378
|
+
for (const segment of segments) {
|
|
2379
|
+
if (cwd === null)
|
|
2380
|
+
break;
|
|
2381
|
+
const s = segment.trim();
|
|
2382
|
+
if (s === "" || OPAQUE_SEGMENT.test(s))
|
|
2383
|
+
continue;
|
|
2384
|
+
const tokens = s.split(/\s+/);
|
|
2385
|
+
const cmd = tokens[0];
|
|
2386
|
+
if (cmd === "cd") {
|
|
2387
|
+
const target = tokens[1];
|
|
2388
|
+
if (target === undefined || !PLAIN_PATH.test(target)) {
|
|
2389
|
+
cwd = null;
|
|
2414
2390
|
continue;
|
|
2415
|
-
if (seen.has(name)) {
|
|
2416
|
-
ctx.addIssue({
|
|
2417
|
-
code: z.ZodIssueCode.custom,
|
|
2418
|
-
path: ["secrets", "bindings", i, "name"],
|
|
2419
|
-
message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
|
|
2420
|
-
});
|
|
2421
2391
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
const t = targets[i];
|
|
2428
|
-
if (t.kind === "worker" && t.script === undefined) {
|
|
2429
|
-
ctx.addIssue({
|
|
2430
|
-
code: z.ZodIssueCode.custom,
|
|
2431
|
-
path: ["targets", i, "script"],
|
|
2432
|
-
message: "a worker target must declare a script (the sibling Workers script name)"
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
if (t.kind === "pages" && t.script !== undefined) {
|
|
2436
|
-
ctx.addIssue({
|
|
2437
|
-
code: z.ZodIssueCode.custom,
|
|
2438
|
-
path: ["targets", i, "script"],
|
|
2439
|
-
message: "a pages target must not declare a script — the primary Pages project is the app slug"
|
|
2440
|
-
});
|
|
2441
|
-
}
|
|
2442
|
-
if (t.kind === "pages" && t.schedule !== undefined) {
|
|
2443
|
-
ctx.addIssue({
|
|
2444
|
-
code: z.ZodIssueCode.custom,
|
|
2445
|
-
path: ["targets", i, "schedule"],
|
|
2446
|
-
message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
|
|
2447
|
-
});
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
const seenScripts = new Set;
|
|
2451
|
-
let pagesTargets = 0;
|
|
2452
|
-
for (let i = 0;i < targets.length; i++) {
|
|
2453
|
-
const t = targets[i];
|
|
2454
|
-
if (t.kind === "pages") {
|
|
2455
|
-
pagesTargets++;
|
|
2456
|
-
if (pagesTargets > 1) {
|
|
2457
|
-
ctx.addIssue({
|
|
2458
|
-
code: z.ZodIssueCode.custom,
|
|
2459
|
-
path: ["targets", i, "kind"],
|
|
2460
|
-
message: "at most one pages target may be declared (the primary Pages project is singular)"
|
|
2392
|
+
const resolved = resolve5(target);
|
|
2393
|
+
if (!dirSet.has(resolved)) {
|
|
2394
|
+
issues.push({
|
|
2395
|
+
input: target,
|
|
2396
|
+
message: `build command segment '${s}' changes into '${resolved}', which does not exist in the bundle`
|
|
2461
2397
|
});
|
|
2398
|
+
cwd = null;
|
|
2399
|
+
continue;
|
|
2462
2400
|
}
|
|
2401
|
+
cwd = resolved;
|
|
2463
2402
|
continue;
|
|
2464
2403
|
}
|
|
2465
|
-
if (
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
for (let bi = 0;bi < bindings.length; bi++) {
|
|
2488
|
-
const b = bindings[bi];
|
|
2489
|
-
const sel = b?.targets;
|
|
2490
|
-
if (sel === undefined || sel === "all")
|
|
2491
|
-
continue;
|
|
2492
|
-
for (const ref of sel) {
|
|
2493
|
-
if (!validRefs.has(ref)) {
|
|
2494
|
-
ctx.addIssue({
|
|
2495
|
-
code: z.ZodIssueCode.custom,
|
|
2496
|
-
path: ["secrets", "bindings", bi, "targets"],
|
|
2497
|
-
message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
|
|
2498
|
-
});
|
|
2404
|
+
if (cmd === "cp" || cmd === "cat") {
|
|
2405
|
+
const rawArgs = [];
|
|
2406
|
+
for (const t of tokens.slice(1)) {
|
|
2407
|
+
if (t === ">" || t.startsWith(">"))
|
|
2408
|
+
break;
|
|
2409
|
+
if (t.startsWith("-"))
|
|
2410
|
+
continue;
|
|
2411
|
+
rawArgs.push(t);
|
|
2412
|
+
}
|
|
2413
|
+
const sources = cmd === "cp" ? rawArgs.slice(0, -1) : rawArgs;
|
|
2414
|
+
if (cmd === "cp" && rawArgs.length < 2)
|
|
2415
|
+
continue;
|
|
2416
|
+
for (const src of sources) {
|
|
2417
|
+
if (!PLAIN_PATH.test(src))
|
|
2418
|
+
continue;
|
|
2419
|
+
const resolved = resolve5(src);
|
|
2420
|
+
if (!exists(resolved)) {
|
|
2421
|
+
issues.push({
|
|
2422
|
+
input: src,
|
|
2423
|
+
message: `build command references '${src}' (resolved: '${resolved}'), which does not exist in the bundle — did the content move? (cf. sp-reloc1)`
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2499
2426
|
}
|
|
2500
2427
|
}
|
|
2501
2428
|
}
|
|
2502
|
-
|
|
2503
|
-
function parseManifest(input) {
|
|
2504
|
-
const result = ManifestSchema.safeParse(input);
|
|
2505
|
-
if (result.success) {
|
|
2506
|
-
return { kind: "ok", manifest: result.data };
|
|
2507
|
-
}
|
|
2508
|
-
return {
|
|
2509
|
-
kind: "schema-error",
|
|
2510
|
-
issues: result.error.issues.map((issue) => ({
|
|
2511
|
-
path: issue.path.map(String).join(".") || "(root)",
|
|
2512
|
-
message: issue.message
|
|
2513
|
-
}))
|
|
2514
|
-
};
|
|
2429
|
+
return issues;
|
|
2515
2430
|
}
|
|
2516
2431
|
// ../launchpad-engine/dist/status-author.js
|
|
2517
2432
|
var STATUS_STATUS_KEYS = new Set([
|
|
@@ -2557,6 +2472,7 @@ var SpecSchema = z2.object({
|
|
|
2557
2472
|
}).strict(),
|
|
2558
2473
|
auth: z2.enum(AUTH_MODES).optional(),
|
|
2559
2474
|
build: BuildSchema.optional(),
|
|
2475
|
+
app: AppBoundarySchema.optional(),
|
|
2560
2476
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
2561
2477
|
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
2562
2478
|
secrets: SecretsSchema.optional(),
|
|
@@ -3151,101 +3067,635 @@ var FleetManifestSchema = z3.object({
|
|
|
3151
3067
|
});
|
|
3152
3068
|
}
|
|
3153
3069
|
}
|
|
3154
|
-
});
|
|
3155
|
-
// ../launchpad-engine/dist/fleet-secret-sets.js
|
|
3156
|
-
import { z as z4 } from "zod";
|
|
3157
|
-
var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3158
|
-
var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
|
|
3159
|
-
var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
|
|
3160
|
-
var SecretSetExcludeSchema = z4.object({
|
|
3161
|
-
member: Slug,
|
|
3162
|
-
secret: SecretName
|
|
3163
|
-
}).strict();
|
|
3164
|
-
var SecretSetSchema = z4.object({
|
|
3165
|
-
name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
|
|
3166
|
-
secrets: z4.array(SecretName).min(1),
|
|
3167
|
-
members: z4.array(Slug).min(1),
|
|
3168
|
-
exclude: z4.array(SecretSetExcludeSchema).optional()
|
|
3169
|
-
}).strict();
|
|
3170
|
-
var FleetSecretSetsSchema = z4.object({
|
|
3171
|
-
schemaVersion: z4.literal(1),
|
|
3172
|
-
secretSets: z4.array(SecretSetSchema).min(1)
|
|
3173
|
-
}).strict().superRefine((m, ctx) => {
|
|
3174
|
-
const seenSets = new Set;
|
|
3175
|
-
for (let i = 0;i < m.secretSets.length; i++) {
|
|
3176
|
-
const set = m.secretSets[i];
|
|
3177
|
-
if (seenSets.has(set.name)) {
|
|
3178
|
-
ctx.addIssue({
|
|
3179
|
-
code: "custom",
|
|
3180
|
-
message: `duplicate secretSet name "${set.name}" — each set name appears once`,
|
|
3181
|
-
path: ["secretSets", i, "name"]
|
|
3182
|
-
});
|
|
3070
|
+
});
|
|
3071
|
+
// ../launchpad-engine/dist/fleet-secret-sets.js
|
|
3072
|
+
import { z as z4 } from "zod";
|
|
3073
|
+
var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3074
|
+
var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
|
|
3075
|
+
var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
|
|
3076
|
+
var SecretSetExcludeSchema = z4.object({
|
|
3077
|
+
member: Slug,
|
|
3078
|
+
secret: SecretName
|
|
3079
|
+
}).strict();
|
|
3080
|
+
var SecretSetSchema = z4.object({
|
|
3081
|
+
name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
|
|
3082
|
+
secrets: z4.array(SecretName).min(1),
|
|
3083
|
+
members: z4.array(Slug).min(1),
|
|
3084
|
+
exclude: z4.array(SecretSetExcludeSchema).optional()
|
|
3085
|
+
}).strict();
|
|
3086
|
+
var FleetSecretSetsSchema = z4.object({
|
|
3087
|
+
schemaVersion: z4.literal(1),
|
|
3088
|
+
secretSets: z4.array(SecretSetSchema).min(1)
|
|
3089
|
+
}).strict().superRefine((m, ctx) => {
|
|
3090
|
+
const seenSets = new Set;
|
|
3091
|
+
for (let i = 0;i < m.secretSets.length; i++) {
|
|
3092
|
+
const set = m.secretSets[i];
|
|
3093
|
+
if (seenSets.has(set.name)) {
|
|
3094
|
+
ctx.addIssue({
|
|
3095
|
+
code: "custom",
|
|
3096
|
+
message: `duplicate secretSet name "${set.name}" — each set name appears once`,
|
|
3097
|
+
path: ["secretSets", i, "name"]
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
seenSets.add(set.name);
|
|
3101
|
+
const seenMembers = new Set;
|
|
3102
|
+
for (const member of set.members) {
|
|
3103
|
+
if (seenMembers.has(member)) {
|
|
3104
|
+
ctx.addIssue({
|
|
3105
|
+
code: "custom",
|
|
3106
|
+
message: `duplicate member "${member}" in set "${set.name}"`,
|
|
3107
|
+
path: ["secretSets", i, "members"]
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
seenMembers.add(member);
|
|
3111
|
+
}
|
|
3112
|
+
const seenSecrets = new Set;
|
|
3113
|
+
for (const secret of set.secrets) {
|
|
3114
|
+
if (seenSecrets.has(secret)) {
|
|
3115
|
+
ctx.addIssue({
|
|
3116
|
+
code: "custom",
|
|
3117
|
+
message: `duplicate secret "${secret}" in set "${set.name}"`,
|
|
3118
|
+
path: ["secretSets", i, "secrets"]
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
seenSecrets.add(secret);
|
|
3122
|
+
}
|
|
3123
|
+
for (let j = 0;j < (set.exclude ?? []).length; j++) {
|
|
3124
|
+
const ex = set.exclude[j];
|
|
3125
|
+
if (!seenMembers.has(ex.member)) {
|
|
3126
|
+
ctx.addIssue({
|
|
3127
|
+
code: "custom",
|
|
3128
|
+
message: `exclude references member "${ex.member}" which is not in set "${set.name}".members`,
|
|
3129
|
+
path: ["secretSets", i, "exclude", j, "member"]
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
if (!seenSecrets.has(ex.secret)) {
|
|
3133
|
+
ctx.addIssue({
|
|
3134
|
+
code: "custom",
|
|
3135
|
+
message: `exclude references secret "${ex.secret}" which is not in set "${set.name}".secrets`,
|
|
3136
|
+
path: ["secretSets", i, "exclude", j, "secret"]
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
function parseFleetSecretSets(input) {
|
|
3143
|
+
const res = FleetSecretSetsSchema.safeParse(input);
|
|
3144
|
+
if (res.success)
|
|
3145
|
+
return { ok: true, manifest: res.data };
|
|
3146
|
+
return {
|
|
3147
|
+
ok: false,
|
|
3148
|
+
issues: res.error.issues.map((i) => ({
|
|
3149
|
+
path: i.path.join("."),
|
|
3150
|
+
message: i.message
|
|
3151
|
+
}))
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
function membersForSecret(set, secret) {
|
|
3155
|
+
const excluded = new Set((set.exclude ?? []).filter((e) => e.secret === secret).map((e) => e.member));
|
|
3156
|
+
return set.members.filter((m) => !excluded.has(m));
|
|
3157
|
+
}
|
|
3158
|
+
// ../launchpad-engine/dist/redact-status.js
|
|
3159
|
+
var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
|
|
3160
|
+
// src/bundle/boundary.ts
|
|
3161
|
+
var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
|
|
3162
|
+
function applyBoundaryToFiles(args) {
|
|
3163
|
+
const { cwd, manifestYaml, files } = args;
|
|
3164
|
+
let parsed;
|
|
3165
|
+
if (manifestYaml !== null) {
|
|
3166
|
+
try {
|
|
3167
|
+
parsed = parseYaml2(manifestYaml);
|
|
3168
|
+
} catch {
|
|
3169
|
+
parsed = undefined;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const input = extractBoundaryInput(parsed);
|
|
3173
|
+
if (input.kind === "schema-error") {
|
|
3174
|
+
const detail = input.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
3175
|
+
return { kind: "error", message: `launchpad.yaml app: block is invalid — ${detail}` };
|
|
3176
|
+
}
|
|
3177
|
+
const contract = resolveAppBoundary({ appType: input.appType, app: input.app });
|
|
3178
|
+
let launchpadIgnore = [];
|
|
3179
|
+
const ignorePath = join5(cwd, ".launchpadignore");
|
|
3180
|
+
if (existsSync2(ignorePath)) {
|
|
3181
|
+
try {
|
|
3182
|
+
launchpadIgnore = parseLaunchpadIgnore(readFileSync3(ignorePath, "utf8"));
|
|
3183
|
+
} catch {}
|
|
3184
|
+
}
|
|
3185
|
+
const result = applyAppBoundary(contract, files, {
|
|
3186
|
+
launchpadIgnore,
|
|
3187
|
+
readFileContent: (path6) => {
|
|
3188
|
+
try {
|
|
3189
|
+
const abs = join5(cwd, path6);
|
|
3190
|
+
const stat = lstatSync2(abs);
|
|
3191
|
+
if (!stat.isFile() || stat.size > MAX_CONTENT_PROBE_BYTES)
|
|
3192
|
+
return;
|
|
3193
|
+
return readFileSync3(abs, "utf8");
|
|
3194
|
+
} catch {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
const warnings = [];
|
|
3200
|
+
if (result.denied.length > 0) {
|
|
3201
|
+
if (contract.source === "declared") {
|
|
3202
|
+
const lines = result.denied.map((d) => ` - ${d.message}`).join(`
|
|
3203
|
+
`);
|
|
3204
|
+
return {
|
|
3205
|
+
kind: "error",
|
|
3206
|
+
message: `bundle blocked — ${result.denied.length} file(s) matched by app.include can never ship:
|
|
3207
|
+
${lines}
|
|
3208
|
+
` + ` Remove the file(s) or narrow app.include / add app.exclude in launchpad.yaml.`
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
for (const d of result.denied) {
|
|
3212
|
+
warnings.push(`stripped ${d.path} — ${d.message}`);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (result.files.length === 0) {
|
|
3216
|
+
return {
|
|
3217
|
+
kind: "error",
|
|
3218
|
+
message: "the app boundary excluded every file — nothing to deploy. " + "Check app.root / app.include in launchpad.yaml."
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
const buildIssues = verifyBuildInputs(input.build, result.files);
|
|
3222
|
+
if (buildIssues.length > 0) {
|
|
3223
|
+
const lines = buildIssues.map((i) => ` - ${i.message}`).join(`
|
|
3224
|
+
`);
|
|
3225
|
+
return {
|
|
3226
|
+
kind: "error",
|
|
3227
|
+
message: `build-inputs check failed — the build command references missing inputs:
|
|
3228
|
+
${lines}`
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
return {
|
|
3232
|
+
kind: "ok",
|
|
3233
|
+
files: result.files,
|
|
3234
|
+
warnings,
|
|
3235
|
+
inferred: contract.source === "inferred"
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// src/bundle/orchestrate.ts
|
|
3240
|
+
async function bundleAndDeploy(args) {
|
|
3241
|
+
const { cfg, cwd, slug } = args;
|
|
3242
|
+
const manifestPath = join6(cwd, "launchpad.yaml");
|
|
3243
|
+
let manifestYaml;
|
|
3244
|
+
try {
|
|
3245
|
+
manifestYaml = readFileSync4(manifestPath, "utf8");
|
|
3246
|
+
} catch (e) {
|
|
3247
|
+
return {
|
|
3248
|
+
kind: "no-manifest",
|
|
3249
|
+
message: `no launchpad.yaml at ${manifestPath} — run \`launchpad init\` from inside your app directory first. ` + `(${e.message})`
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
let walk;
|
|
3253
|
+
try {
|
|
3254
|
+
walk = walkCwd(cwd);
|
|
3255
|
+
} catch (e) {
|
|
3256
|
+
if (e instanceof WalkError) {
|
|
3257
|
+
return { kind: "walk-error", message: e.message };
|
|
3258
|
+
}
|
|
3259
|
+
return {
|
|
3260
|
+
kind: "walk-error",
|
|
3261
|
+
message: `unexpected walk failure: ${e.message}`
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
const boundary = applyBoundaryToFiles({ cwd, manifestYaml, files: walk.files });
|
|
3265
|
+
if (boundary.kind === "error") {
|
|
3266
|
+
return { kind: "boundary-error", message: boundary.message };
|
|
3267
|
+
}
|
|
3268
|
+
const bundleFiles = boundary.files;
|
|
3269
|
+
let packResult;
|
|
3270
|
+
try {
|
|
3271
|
+
packResult = await packTarGz(cwd, bundleFiles);
|
|
3272
|
+
} catch (e) {
|
|
3273
|
+
if (e instanceof TarPackError) {
|
|
3274
|
+
return { kind: "pack-error", message: e.message };
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
kind: "pack-error",
|
|
3278
|
+
message: `unexpected pack failure: ${e.message}`
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
const workerBuild = await buildWorkerArtifact(cwd, manifestYaml, bundleFiles);
|
|
3282
|
+
if (workerBuild.kind === "error") {
|
|
3283
|
+
return { kind: "worker-build-error", message: workerBuild.message };
|
|
3284
|
+
}
|
|
3285
|
+
const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
|
|
3286
|
+
const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact);
|
|
3287
|
+
if (uploadResult.kind !== "ok") {
|
|
3288
|
+
return {
|
|
3289
|
+
kind: "upload-error",
|
|
3290
|
+
status: uploadResult.status,
|
|
3291
|
+
body: uploadResult.response
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
return {
|
|
3295
|
+
kind: "ok",
|
|
3296
|
+
result: uploadResult.response,
|
|
3297
|
+
walk,
|
|
3298
|
+
fileCount: packResult.fileCount,
|
|
3299
|
+
compressedBytes: packResult.bytes.byteLength,
|
|
3300
|
+
workerScript: workerArtifact?.script ?? null,
|
|
3301
|
+
boundaryWarnings: boundary.warnings
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// src/commands/deploy.ts
|
|
3306
|
+
import { parse as parseYaml4 } from "yaml";
|
|
3307
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3308
|
+
|
|
3309
|
+
// src/deploy/git-files.ts
|
|
3310
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3311
|
+
|
|
3312
|
+
class GitFilesError extends Error {
|
|
3313
|
+
code = "git_files_error";
|
|
3314
|
+
}
|
|
3315
|
+
var PROTECTED_PATHS = new Set([
|
|
3316
|
+
".github/workflows/launchpad-review.yml",
|
|
3317
|
+
".github/workflows/ci.yml",
|
|
3318
|
+
"wrangler.toml",
|
|
3319
|
+
".github/scripts/_gha-cmd.mjs",
|
|
3320
|
+
".github/scripts/parse-eslint.mjs",
|
|
3321
|
+
".github/scripts/parse-tsc.mjs",
|
|
3322
|
+
".github/scripts/parse-vitest.mjs",
|
|
3323
|
+
".github/scripts/parse-gitleaks.mjs",
|
|
3324
|
+
".github/scripts/parse-bun-audit.mjs"
|
|
3325
|
+
]);
|
|
3326
|
+
async function listDeployFiles(opts) {
|
|
3327
|
+
const sp = opts.spawner ?? spawn3;
|
|
3328
|
+
const stdout = await runGit2(sp, opts.cwd, [
|
|
3329
|
+
"ls-files",
|
|
3330
|
+
"-c",
|
|
3331
|
+
"-o",
|
|
3332
|
+
"--exclude-standard",
|
|
3333
|
+
"-z"
|
|
3334
|
+
]);
|
|
3335
|
+
const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
|
|
3336
|
+
return [...paths].sort();
|
|
3337
|
+
}
|
|
3338
|
+
function runGit2(sp, cwd, args) {
|
|
3339
|
+
return new Promise((resolve5, reject) => {
|
|
3340
|
+
const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
|
|
3341
|
+
const child = sp("git", [...args], spawnOpts);
|
|
3342
|
+
let stdout = "";
|
|
3343
|
+
let stderr = "";
|
|
3344
|
+
child.stdout?.on("data", (chunk) => {
|
|
3345
|
+
stdout += chunk.toString();
|
|
3346
|
+
});
|
|
3347
|
+
child.stderr?.on("data", (chunk) => {
|
|
3348
|
+
stderr += chunk.toString();
|
|
3349
|
+
});
|
|
3350
|
+
child.once("error", (err) => {
|
|
3351
|
+
reject(new GitFilesError(`could not run \`git ${args.join(" ")}\`: ${err.message} (is git installed?)`));
|
|
3352
|
+
});
|
|
3353
|
+
child.once("close", (code) => {
|
|
3354
|
+
if (code === 0) {
|
|
3355
|
+
resolve5(stdout);
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
|
|
3359
|
+
});
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// src/commands/deploy-flags.ts
|
|
3364
|
+
var SLUG_RE3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3365
|
+
var GROUP_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3366
|
+
var GROUP_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3367
|
+
function parseDeployFlags(args) {
|
|
3368
|
+
let mode = "content";
|
|
3369
|
+
let slug = null;
|
|
3370
|
+
let displayName = null;
|
|
3371
|
+
let description = null;
|
|
3372
|
+
let appType = null;
|
|
3373
|
+
const allowedGroups = [];
|
|
3374
|
+
let message = null;
|
|
3375
|
+
let timeoutSeconds = null;
|
|
3376
|
+
let manifestFile = null;
|
|
3377
|
+
let dryRunJson = false;
|
|
3378
|
+
let platformRepo = null;
|
|
3379
|
+
let rePin = false;
|
|
3380
|
+
let yes = false;
|
|
3381
|
+
let resumePrNumber = null;
|
|
3382
|
+
let timeoutMinutes = null;
|
|
3383
|
+
let modeFlagsSeen = 0;
|
|
3384
|
+
let i = 0;
|
|
3385
|
+
while (i < args.length) {
|
|
3386
|
+
const a = args[i] ?? "";
|
|
3387
|
+
if (a === "--new") {
|
|
3388
|
+
mode = "new";
|
|
3389
|
+
modeFlagsSeen += 1;
|
|
3390
|
+
i += 1;
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3393
|
+
if (a === "--dry-run") {
|
|
3394
|
+
mode = "dry-run";
|
|
3395
|
+
modeFlagsSeen += 1;
|
|
3396
|
+
i += 1;
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
if (a === "--apply") {
|
|
3400
|
+
mode = "apply";
|
|
3401
|
+
modeFlagsSeen += 1;
|
|
3402
|
+
i += 1;
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
if (a === "--platform-repo") {
|
|
3406
|
+
const v = args[i + 1];
|
|
3407
|
+
if (v === undefined || v.trim().length === 0) {
|
|
3408
|
+
return `missing value for ${a}`;
|
|
3409
|
+
}
|
|
3410
|
+
platformRepo = v.trim();
|
|
3411
|
+
i += 2;
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
if (a === "--re-pin") {
|
|
3415
|
+
rePin = true;
|
|
3416
|
+
i += 1;
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
if (a === "--yes" || a === "-y") {
|
|
3420
|
+
yes = true;
|
|
3421
|
+
i += 1;
|
|
3422
|
+
continue;
|
|
3423
|
+
}
|
|
3424
|
+
if (a === "--resume") {
|
|
3425
|
+
const v = args[i + 1];
|
|
3426
|
+
if (v === undefined)
|
|
3427
|
+
return `missing value for ${a}`;
|
|
3428
|
+
mode = "resume";
|
|
3429
|
+
slug = v;
|
|
3430
|
+
modeFlagsSeen += 1;
|
|
3431
|
+
i += 2;
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
if (a === "--abandon") {
|
|
3435
|
+
const v = args[i + 1];
|
|
3436
|
+
if (v === undefined)
|
|
3437
|
+
return `missing value for ${a}`;
|
|
3438
|
+
mode = "abandon";
|
|
3439
|
+
slug = v;
|
|
3440
|
+
modeFlagsSeen += 1;
|
|
3441
|
+
i += 2;
|
|
3442
|
+
continue;
|
|
3443
|
+
}
|
|
3444
|
+
if (a === "--file") {
|
|
3445
|
+
const v = args[i + 1];
|
|
3446
|
+
if (v === undefined)
|
|
3447
|
+
return `missing value for ${a}`;
|
|
3448
|
+
manifestFile = v;
|
|
3449
|
+
i += 2;
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
if (a === "--json") {
|
|
3453
|
+
dryRunJson = true;
|
|
3454
|
+
i += 1;
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
if (a === "--slug") {
|
|
3458
|
+
const v = args[i + 1];
|
|
3459
|
+
if (v === undefined)
|
|
3460
|
+
return `missing value for ${a}`;
|
|
3461
|
+
slug = v;
|
|
3462
|
+
i += 2;
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (a === "--display-name" || a === "--name") {
|
|
3466
|
+
const v = args[i + 1];
|
|
3467
|
+
if (v === undefined)
|
|
3468
|
+
return `missing value for ${a}`;
|
|
3469
|
+
displayName = v;
|
|
3470
|
+
i += 2;
|
|
3471
|
+
continue;
|
|
3472
|
+
}
|
|
3473
|
+
if (a === "--description") {
|
|
3474
|
+
const v = args[i + 1];
|
|
3475
|
+
if (v === undefined)
|
|
3476
|
+
return `missing value for ${a}`;
|
|
3477
|
+
description = v;
|
|
3478
|
+
i += 2;
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
if (a === "--app-type" || a === "--type") {
|
|
3482
|
+
const v = args[i + 1];
|
|
3483
|
+
if (v === undefined)
|
|
3484
|
+
return `missing value for ${a}`;
|
|
3485
|
+
if (!APP_TYPES.includes(v)) {
|
|
3486
|
+
return `invalid --app-type "${v}" — must be one of ${APP_TYPES.join(", ")}`;
|
|
3487
|
+
}
|
|
3488
|
+
appType = v;
|
|
3489
|
+
i += 2;
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
if (a === "--allowed-group" || a === "--group") {
|
|
3493
|
+
const v = args[i + 1];
|
|
3494
|
+
if (v === undefined)
|
|
3495
|
+
return `missing value for ${a}`;
|
|
3496
|
+
if (!GROUP_KEY_RE2.test(v) && !GROUP_UUID_RE2.test(v)) {
|
|
3497
|
+
return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
|
|
3498
|
+
}
|
|
3499
|
+
allowedGroups.push(v);
|
|
3500
|
+
i += 2;
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
if (a === "--message" || a === "-m") {
|
|
3504
|
+
const v = args[i + 1];
|
|
3505
|
+
if (v === undefined)
|
|
3506
|
+
return `missing value for ${a}`;
|
|
3507
|
+
message = v;
|
|
3508
|
+
i += 2;
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
if (a === "--timeout-seconds") {
|
|
3512
|
+
const v = args[i + 1];
|
|
3513
|
+
if (v === undefined)
|
|
3514
|
+
return `missing value for ${a}`;
|
|
3515
|
+
const n = Number(v);
|
|
3516
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3517
|
+
return `invalid --timeout-seconds "${v}" — expected positive integer`;
|
|
3518
|
+
}
|
|
3519
|
+
timeoutSeconds = n;
|
|
3520
|
+
i += 2;
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (a === "--timeout-minutes") {
|
|
3524
|
+
const v = args[i + 1];
|
|
3525
|
+
if (v === undefined)
|
|
3526
|
+
return `missing value for ${a}`;
|
|
3527
|
+
const n = Number(v);
|
|
3528
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3529
|
+
return `invalid --timeout-minutes "${v}" — expected positive integer`;
|
|
3530
|
+
}
|
|
3531
|
+
timeoutMinutes = n;
|
|
3532
|
+
i += 2;
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
if (a === "--resume-pr") {
|
|
3536
|
+
const v = args[i + 1];
|
|
3537
|
+
if (v === undefined)
|
|
3538
|
+
return `missing value for ${a}`;
|
|
3539
|
+
const n = Number(v);
|
|
3540
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3541
|
+
return `invalid --resume-pr "${v}" — expected positive integer PR number`;
|
|
3542
|
+
}
|
|
3543
|
+
resumePrNumber = n;
|
|
3544
|
+
i += 2;
|
|
3545
|
+
continue;
|
|
3546
|
+
}
|
|
3547
|
+
return `unknown argument "${a}"`;
|
|
3548
|
+
}
|
|
3549
|
+
if (modeFlagsSeen > 1) {
|
|
3550
|
+
return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
|
|
3551
|
+
}
|
|
3552
|
+
if (mode === "dry-run") {
|
|
3553
|
+
if (slug !== null)
|
|
3554
|
+
return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
|
|
3555
|
+
if (displayName !== null)
|
|
3556
|
+
return "--dry-run does not accept --display-name";
|
|
3557
|
+
if (description !== null)
|
|
3558
|
+
return "--dry-run does not accept --description";
|
|
3559
|
+
if (appType !== null)
|
|
3560
|
+
return "--dry-run does not accept --app-type";
|
|
3561
|
+
if (allowedGroups.length > 0)
|
|
3562
|
+
return "--dry-run does not accept --allowed-group (read from launchpad.yaml)";
|
|
3563
|
+
if (message !== null)
|
|
3564
|
+
return "--dry-run does not accept --message";
|
|
3565
|
+
if (timeoutSeconds !== null)
|
|
3566
|
+
return "--dry-run does not accept --timeout-seconds";
|
|
3567
|
+
if (platformRepo !== null)
|
|
3568
|
+
return "--dry-run does not accept --platform-repo (apply-only)";
|
|
3569
|
+
if (rePin)
|
|
3570
|
+
return "--dry-run does not accept --re-pin (apply-only)";
|
|
3571
|
+
if (yes)
|
|
3572
|
+
return "--dry-run does not accept --yes (apply-only)";
|
|
3573
|
+
return {
|
|
3574
|
+
mode: { kind: "dry-run", file: manifestFile, json: dryRunJson },
|
|
3575
|
+
message: null,
|
|
3576
|
+
timeoutSeconds: null
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
if (mode === "apply") {
|
|
3580
|
+
if (slug !== null)
|
|
3581
|
+
return "--apply does not accept --slug (slug is read from launchpad.yaml)";
|
|
3582
|
+
if (displayName !== null)
|
|
3583
|
+
return "--apply does not accept --display-name";
|
|
3584
|
+
if (description !== null)
|
|
3585
|
+
return "--apply does not accept --description";
|
|
3586
|
+
if (appType !== null)
|
|
3587
|
+
return "--apply does not accept --app-type";
|
|
3588
|
+
if (allowedGroups.length > 0)
|
|
3589
|
+
return "--apply does not accept --allowed-group (read from launchpad.yaml)";
|
|
3590
|
+
if (message !== null)
|
|
3591
|
+
return "--apply does not accept --message";
|
|
3592
|
+
if (timeoutSeconds !== null)
|
|
3593
|
+
return "--apply does not accept --timeout-seconds (use --timeout-minutes instead — apply polling is multi-minute)";
|
|
3594
|
+
if (dryRunJson)
|
|
3595
|
+
return "--apply does not accept --json";
|
|
3596
|
+
return {
|
|
3597
|
+
mode: {
|
|
3598
|
+
kind: "apply",
|
|
3599
|
+
file: manifestFile,
|
|
3600
|
+
platformRepo,
|
|
3601
|
+
rePin,
|
|
3602
|
+
yes,
|
|
3603
|
+
resumePrNumber,
|
|
3604
|
+
timeoutMinutes
|
|
3605
|
+
},
|
|
3606
|
+
message: null,
|
|
3607
|
+
timeoutSeconds: null
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
if (resumePrNumber !== null) {
|
|
3611
|
+
return "--resume-pr is only valid with --apply";
|
|
3612
|
+
}
|
|
3613
|
+
if (timeoutMinutes !== null) {
|
|
3614
|
+
return "--timeout-minutes is only valid with --apply";
|
|
3615
|
+
}
|
|
3616
|
+
if (manifestFile !== null) {
|
|
3617
|
+
return "--file is only valid with --dry-run or --apply";
|
|
3618
|
+
}
|
|
3619
|
+
if (dryRunJson) {
|
|
3620
|
+
return "--json is only valid with --dry-run";
|
|
3621
|
+
}
|
|
3622
|
+
if (platformRepo !== null) {
|
|
3623
|
+
return "--platform-repo is only valid with --apply";
|
|
3624
|
+
}
|
|
3625
|
+
if (rePin) {
|
|
3626
|
+
return "--re-pin is only valid with --apply";
|
|
3627
|
+
}
|
|
3628
|
+
if (yes) {
|
|
3629
|
+
return "--yes is only valid with --apply";
|
|
3630
|
+
}
|
|
3631
|
+
if (mode === "content") {
|
|
3632
|
+
if (slug !== null && !SLUG_RE3.test(slug)) {
|
|
3633
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
|
|
3183
3634
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
}
|
|
3194
|
-
seenMembers.add(member);
|
|
3635
|
+
return {
|
|
3636
|
+
mode: { kind: "content", slug },
|
|
3637
|
+
message,
|
|
3638
|
+
timeoutSeconds
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
if (mode === "resume" || mode === "abandon") {
|
|
3642
|
+
if (slug === null || !SLUG_RE3.test(slug)) {
|
|
3643
|
+
return `--${mode} requires a valid slug (kebab-case)`;
|
|
3195
3644
|
}
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
if (seenSecrets.has(secret)) {
|
|
3199
|
-
ctx.addIssue({
|
|
3200
|
-
code: "custom",
|
|
3201
|
-
message: `duplicate secret "${secret}" in set "${set.name}"`,
|
|
3202
|
-
path: ["secretSets", i, "secrets"]
|
|
3203
|
-
});
|
|
3204
|
-
}
|
|
3205
|
-
seenSecrets.add(secret);
|
|
3645
|
+
if (displayName !== null || appType !== null || allowedGroups.length > 0) {
|
|
3646
|
+
return `--${mode} does not accept create-only flags (--display-name / --app-type / --allowed-group)`;
|
|
3206
3647
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
if (!seenMembers.has(ex.member)) {
|
|
3210
|
-
ctx.addIssue({
|
|
3211
|
-
code: "custom",
|
|
3212
|
-
message: `exclude references member "${ex.member}" which is not in set "${set.name}".members`,
|
|
3213
|
-
path: ["secretSets", i, "exclude", j, "member"]
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
if (!seenSecrets.has(ex.secret)) {
|
|
3217
|
-
ctx.addIssue({
|
|
3218
|
-
code: "custom",
|
|
3219
|
-
message: `exclude references secret "${ex.secret}" which is not in set "${set.name}".secrets`,
|
|
3220
|
-
path: ["secretSets", i, "exclude", j, "secret"]
|
|
3221
|
-
});
|
|
3222
|
-
}
|
|
3648
|
+
if (message !== null) {
|
|
3649
|
+
return `--${mode} does not accept --message`;
|
|
3223
3650
|
}
|
|
3651
|
+
return {
|
|
3652
|
+
mode: { kind: mode, slug },
|
|
3653
|
+
message: null,
|
|
3654
|
+
timeoutSeconds
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
if (slug === null)
|
|
3658
|
+
return "--new requires --slug";
|
|
3659
|
+
if (!SLUG_RE3.test(slug))
|
|
3660
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
|
|
3661
|
+
if (slug.length < 3 || slug.length > 58) {
|
|
3662
|
+
return `slug length out of bounds (3–58 chars): "${slug}"`;
|
|
3663
|
+
}
|
|
3664
|
+
if (displayName === null || displayName.length === 0) {
|
|
3665
|
+
return "--new requires --display-name";
|
|
3666
|
+
}
|
|
3667
|
+
if (appType === null) {
|
|
3668
|
+
return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
|
|
3669
|
+
}
|
|
3670
|
+
if (allowedGroups.length === 0) {
|
|
3671
|
+
return "--new requires at least one --allowed-group";
|
|
3224
3672
|
}
|
|
3225
|
-
});
|
|
3226
|
-
function parseFleetSecretSets(input) {
|
|
3227
|
-
const res = FleetSecretSetsSchema.safeParse(input);
|
|
3228
|
-
if (res.success)
|
|
3229
|
-
return { ok: true, manifest: res.data };
|
|
3230
3673
|
return {
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3674
|
+
mode: {
|
|
3675
|
+
kind: "new",
|
|
3676
|
+
slug,
|
|
3677
|
+
displayName,
|
|
3678
|
+
description,
|
|
3679
|
+
appType,
|
|
3680
|
+
allowedGroups
|
|
3681
|
+
},
|
|
3682
|
+
message,
|
|
3683
|
+
timeoutSeconds
|
|
3236
3684
|
};
|
|
3237
3685
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3686
|
+
|
|
3687
|
+
// src/deploy/apply.ts
|
|
3688
|
+
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
3689
|
+
import { resolve as resolvePath, join as join7 } from "node:path";
|
|
3690
|
+
import { execFileSync } from "node:child_process";
|
|
3691
|
+
|
|
3244
3692
|
// src/manifest/load.ts
|
|
3693
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3694
|
+
import { parse as parseYaml3, YAMLParseError } from "yaml";
|
|
3245
3695
|
function loadManifest(path6) {
|
|
3246
3696
|
let raw;
|
|
3247
3697
|
try {
|
|
3248
|
-
raw =
|
|
3698
|
+
raw = readFileSync5(path6, "utf8");
|
|
3249
3699
|
} catch (err) {
|
|
3250
3700
|
const e = err;
|
|
3251
3701
|
if (e.code === "ENOENT") {
|
|
@@ -3258,7 +3708,7 @@ function loadManifest(path6) {
|
|
|
3258
3708
|
function parseManifest2(yamlText, path6) {
|
|
3259
3709
|
let parsed;
|
|
3260
3710
|
try {
|
|
3261
|
-
parsed =
|
|
3711
|
+
parsed = parseYaml3(yamlText);
|
|
3262
3712
|
} catch (err) {
|
|
3263
3713
|
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
3264
3714
|
return { kind: "yaml-parse-error", path: path6, message };
|
|
@@ -3481,8 +3931,8 @@ function defaultGitHeadSha(cwd) {
|
|
|
3481
3931
|
return out.trim();
|
|
3482
3932
|
}
|
|
3483
3933
|
function deletePinIfPresent(cfg, slug, io) {
|
|
3484
|
-
const pinPath =
|
|
3485
|
-
if (!
|
|
3934
|
+
const pinPath = join7(cfg.stateDir, slug, "group.json");
|
|
3935
|
+
if (!existsSync3(pinPath))
|
|
3486
3936
|
return;
|
|
3487
3937
|
try {
|
|
3488
3938
|
rmSync(pinPath, { force: true });
|
|
@@ -3974,7 +4424,7 @@ async function runDeploy(args, io) {
|
|
|
3974
4424
|
}
|
|
3975
4425
|
const cwd = process.cwd();
|
|
3976
4426
|
const manifestPath = path6.join(cwd, "launchpad.yaml");
|
|
3977
|
-
if (
|
|
4427
|
+
if (existsSync4(manifestPath)) {
|
|
3978
4428
|
return runModelADeploy({ cwd, manifestPath, argv: args, io });
|
|
3979
4429
|
}
|
|
3980
4430
|
const parsed = parseArgs2(args);
|
|
@@ -4001,12 +4451,34 @@ async function runDeploy(args, io) {
|
|
|
4001
4451
|
try {
|
|
4002
4452
|
const cfg = loadConfig();
|
|
4003
4453
|
io.out(`Packaging working tree for ${slug} …`);
|
|
4004
|
-
const
|
|
4005
|
-
if (
|
|
4454
|
+
const rawFiles = await listDeployFiles({ cwd: process.cwd() });
|
|
4455
|
+
if (rawFiles.length === 0) {
|
|
4006
4456
|
io.err("launchpad deploy: no files to deploy (git ls-files returned empty).");
|
|
4007
4457
|
io.err(" did you run this from inside the cloned working tree?");
|
|
4008
4458
|
return 1;
|
|
4009
4459
|
}
|
|
4460
|
+
let contentManifestYaml = null;
|
|
4461
|
+
const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
|
|
4462
|
+
if (existsSync4(contentManifestPath)) {
|
|
4463
|
+
try {
|
|
4464
|
+
contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
|
|
4465
|
+
} catch {
|
|
4466
|
+
contentManifestYaml = null;
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
const boundary = applyBoundaryToFiles({
|
|
4470
|
+
cwd: process.cwd(),
|
|
4471
|
+
manifestYaml: contentManifestYaml,
|
|
4472
|
+
files: rawFiles
|
|
4473
|
+
});
|
|
4474
|
+
if (boundary.kind === "error") {
|
|
4475
|
+
io.err(`launchpad deploy: ${boundary.message}`);
|
|
4476
|
+
return 1;
|
|
4477
|
+
}
|
|
4478
|
+
for (const w of boundary.warnings) {
|
|
4479
|
+
io.err(`warning: ${w}`);
|
|
4480
|
+
}
|
|
4481
|
+
const files = boundary.files;
|
|
4010
4482
|
const packed = await packTarGz(process.cwd(), files);
|
|
4011
4483
|
io.out(`Packed ${packed.fileCount} files (${formatBytes2(packed.uncompressedBytes)}` + ` uncompressed, ${formatBytes2(packed.bytes.length)} gzipped)`);
|
|
4012
4484
|
io.out(`Uploading to ${cfg.botUrl}/apps/${slug}/deploy-pr …`);
|
|
@@ -4026,6 +4498,9 @@ async function runDeploy(args, io) {
|
|
|
4026
4498
|
io.out(`PR opened: ${response.reviewUrl}`);
|
|
4027
4499
|
io.out(`Branch: ${response.branch}`);
|
|
4028
4500
|
io.out(`Number: #${response.prNumber}`);
|
|
4501
|
+
io.out("");
|
|
4502
|
+
io.out(`Review from the CLI: launchpad review ${response.prNumber} --slug ${slug}`);
|
|
4503
|
+
io.out(`Merge when approved: launchpad merge ${response.prNumber} --slug ${slug}`);
|
|
4029
4504
|
if (response.bootstrapWorkflow === true) {
|
|
4030
4505
|
io.out("");
|
|
4031
4506
|
io.out("(first deploy: included canonical launchpad-review.yml workflow file)");
|
|
@@ -4137,7 +4612,7 @@ async function runModelADeploy(args) {
|
|
|
4137
4612
|
const { cwd, manifestPath, io } = args;
|
|
4138
4613
|
let slug;
|
|
4139
4614
|
try {
|
|
4140
|
-
const metaSlug = resolveManifestSlug(
|
|
4615
|
+
const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
|
|
4141
4616
|
if (metaSlug === null) {
|
|
4142
4617
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
4143
4618
|
return 64;
|
|
@@ -4178,6 +4653,9 @@ async function runModelADeploy(args) {
|
|
|
4178
4653
|
case "walk-error":
|
|
4179
4654
|
io.err(`launchpad deploy: walk failed — ${result.message}`);
|
|
4180
4655
|
return 1;
|
|
4656
|
+
case "boundary-error":
|
|
4657
|
+
io.err(`launchpad deploy: ${result.message}`);
|
|
4658
|
+
return 1;
|
|
4181
4659
|
case "pack-error":
|
|
4182
4660
|
io.err(`launchpad deploy: pack failed — ${result.message}`);
|
|
4183
4661
|
return 1;
|
|
@@ -4213,6 +4691,9 @@ async function runModelADeploy(args) {
|
|
|
4213
4691
|
return 1;
|
|
4214
4692
|
}
|
|
4215
4693
|
case "ok": {
|
|
4694
|
+
for (const w of result.boundaryWarnings) {
|
|
4695
|
+
io.err(`warning: ${w}`);
|
|
4696
|
+
}
|
|
4216
4697
|
if (result.result.status === "provisioning_started") {
|
|
4217
4698
|
const submissionId = typeof result.result.submissionId === "string" ? result.result.submissionId : "(missing)";
|
|
4218
4699
|
const appType = typeof result.result.appType === "string" ? result.result.appType : "(missing)";
|
|
@@ -4245,9 +4726,8 @@ async function runModelADeploy(args) {
|
|
|
4245
4726
|
io.out(` ${result.result.message}`);
|
|
4246
4727
|
}
|
|
4247
4728
|
io.out("");
|
|
4248
|
-
io.out("
|
|
4249
|
-
io.out(`
|
|
4250
|
-
io.out(` launchpad apps # see lifecycle`);
|
|
4729
|
+
io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
|
|
4730
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
4251
4731
|
return 0;
|
|
4252
4732
|
}
|
|
4253
4733
|
}
|
|
@@ -4450,7 +4930,7 @@ function describe13(e) {
|
|
|
4450
4930
|
}
|
|
4451
4931
|
|
|
4452
4932
|
// src/commands/generate.ts
|
|
4453
|
-
import { mkdirSync, readFileSync as
|
|
4933
|
+
import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
|
|
4454
4934
|
import { dirname as dirname4, resolve as resolve6, relative as relative3 } from "node:path";
|
|
4455
4935
|
var generateCommand = {
|
|
4456
4936
|
name: "generate",
|
|
@@ -4538,7 +5018,7 @@ function applyOne(artefact, path8, out, force) {
|
|
|
4538
5018
|
}
|
|
4539
5019
|
function readIfExists(path8) {
|
|
4540
5020
|
try {
|
|
4541
|
-
return { kind: "ok", content:
|
|
5021
|
+
return { kind: "ok", content: readFileSync7(path8, "utf8") };
|
|
4542
5022
|
} catch (err) {
|
|
4543
5023
|
const e = err;
|
|
4544
5024
|
if (e.code === "ENOENT")
|
|
@@ -4749,13 +5229,13 @@ function parseFlags(args) {
|
|
|
4749
5229
|
}
|
|
4750
5230
|
|
|
4751
5231
|
// src/groups/client.ts
|
|
4752
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
4753
|
-
import { dirname as dirname5, join as
|
|
5232
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5233
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
4754
5234
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
4755
5235
|
var CACHE_FILENAME = "groups.json";
|
|
4756
5236
|
async function fetchGroups(cfg, opts = {}) {
|
|
4757
5237
|
const now = opts.now ?? Date.now;
|
|
4758
|
-
const cachePath =
|
|
5238
|
+
const cachePath = join9(cfg.cacheDir, CACHE_FILENAME);
|
|
4759
5239
|
if (opts.forceRefresh !== true) {
|
|
4760
5240
|
const cached = readCache(cachePath);
|
|
4761
5241
|
if (cached !== null) {
|
|
@@ -4793,7 +5273,7 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
4793
5273
|
function readCache(path8) {
|
|
4794
5274
|
let raw;
|
|
4795
5275
|
try {
|
|
4796
|
-
raw =
|
|
5276
|
+
raw = readFileSync8(path8, "utf8");
|
|
4797
5277
|
} catch {
|
|
4798
5278
|
return null;
|
|
4799
5279
|
}
|
|
@@ -5294,17 +5774,17 @@ function describe17(e) {
|
|
|
5294
5774
|
|
|
5295
5775
|
// src/commands/init.ts
|
|
5296
5776
|
import { createInterface } from "node:readline/promises";
|
|
5297
|
-
import { existsSync as
|
|
5777
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5298
5778
|
import { resolve as resolve7 } from "node:path";
|
|
5299
5779
|
import { stringify as yamlStringify } from "yaml";
|
|
5300
5780
|
|
|
5301
5781
|
// src/detect/index.ts
|
|
5302
|
-
import { existsSync as
|
|
5303
|
-
import { join as
|
|
5782
|
+
import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
|
|
5783
|
+
import { join as join10 } from "node:path";
|
|
5304
5784
|
function detectAppShape(cwd) {
|
|
5305
|
-
const hasPackageJson =
|
|
5306
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
5307
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
5785
|
+
const hasPackageJson = existsSync5(join10(cwd, "package.json"));
|
|
5786
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync5(join10(cwd, file)));
|
|
5787
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync5(join10(cwd, n)));
|
|
5308
5788
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
5309
5789
|
return {
|
|
5310
5790
|
kind: "not-applicable",
|
|
@@ -5350,7 +5830,7 @@ var LOCKFILES = [
|
|
|
5350
5830
|
{ file: "yarn.lock", pm: "yarn" }
|
|
5351
5831
|
];
|
|
5352
5832
|
function detectPackageManager(cwd) {
|
|
5353
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
5833
|
+
const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
|
|
5354
5834
|
if (present.length === 0) {
|
|
5355
5835
|
return {
|
|
5356
5836
|
kind: "ambiguous",
|
|
@@ -5372,8 +5852,8 @@ function detectPackageManager(cwd) {
|
|
|
5372
5852
|
}
|
|
5373
5853
|
function detectVitePresence(cwd) {
|
|
5374
5854
|
for (const name of VITE_CONFIG_NAMES) {
|
|
5375
|
-
const p =
|
|
5376
|
-
if (
|
|
5855
|
+
const p = join10(cwd, name);
|
|
5856
|
+
if (existsSync5(p)) {
|
|
5377
5857
|
return { kind: "ok", value: { path: p } };
|
|
5378
5858
|
}
|
|
5379
5859
|
}
|
|
@@ -5383,9 +5863,9 @@ function detectVitePresence(cwd) {
|
|
|
5383
5863
|
};
|
|
5384
5864
|
}
|
|
5385
5865
|
function detectAppType(cwd) {
|
|
5386
|
-
const fnDir =
|
|
5866
|
+
const fnDir = join10(cwd, "functions");
|
|
5387
5867
|
let hasFunctionsDir = false;
|
|
5388
|
-
if (
|
|
5868
|
+
if (existsSync5(fnDir)) {
|
|
5389
5869
|
try {
|
|
5390
5870
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
5391
5871
|
} catch {
|
|
@@ -5395,8 +5875,8 @@ function detectAppType(cwd) {
|
|
|
5395
5875
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
5396
5876
|
}
|
|
5397
5877
|
function detectBuildCommand(cwd, pm) {
|
|
5398
|
-
const pkgJsonPath =
|
|
5399
|
-
if (!
|
|
5878
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
5879
|
+
if (!existsSync5(pkgJsonPath)) {
|
|
5400
5880
|
return {
|
|
5401
5881
|
kind: "ambiguous",
|
|
5402
5882
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -5404,7 +5884,7 @@ function detectBuildCommand(cwd, pm) {
|
|
|
5404
5884
|
}
|
|
5405
5885
|
let pkgJson;
|
|
5406
5886
|
try {
|
|
5407
|
-
pkgJson = JSON.parse(
|
|
5887
|
+
pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
|
|
5408
5888
|
} catch (e) {
|
|
5409
5889
|
return {
|
|
5410
5890
|
kind: "ambiguous",
|
|
@@ -5439,7 +5919,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
|
|
|
5439
5919
|
function detectDestinationDir(cwd, vite) {
|
|
5440
5920
|
let text;
|
|
5441
5921
|
try {
|
|
5442
|
-
text =
|
|
5922
|
+
text = readFileSync9(vite.path, "utf8");
|
|
5443
5923
|
} catch (e) {
|
|
5444
5924
|
return {
|
|
5445
5925
|
kind: "ambiguous",
|
|
@@ -5469,7 +5949,7 @@ async function runInit(args, io, prompt) {
|
|
|
5469
5949
|
}
|
|
5470
5950
|
const { inputs, options } = parsed;
|
|
5471
5951
|
const outPath = resolve7(process.cwd(), options.out);
|
|
5472
|
-
if (
|
|
5952
|
+
if (existsSync6(outPath) && !options.force) {
|
|
5473
5953
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
5474
5954
|
io.err("Pass --force to overwrite.");
|
|
5475
5955
|
return 64;
|
|
@@ -5876,6 +6356,10 @@ function buildManifest(inputs, detected) {
|
|
|
5876
6356
|
}
|
|
5877
6357
|
];
|
|
5878
6358
|
}
|
|
6359
|
+
manifest.app = {
|
|
6360
|
+
root: ".",
|
|
6361
|
+
include: [...defaultIncludeForAppType(inputs.type)]
|
|
6362
|
+
};
|
|
5879
6363
|
return manifest;
|
|
5880
6364
|
}
|
|
5881
6365
|
function renderYaml(manifest) {
|
|
@@ -5883,8 +6367,8 @@ function renderYaml(manifest) {
|
|
|
5883
6367
|
}
|
|
5884
6368
|
function ensureGitignoreEntries(path8, entries) {
|
|
5885
6369
|
let current = "";
|
|
5886
|
-
if (
|
|
5887
|
-
current =
|
|
6370
|
+
if (existsSync6(path8)) {
|
|
6371
|
+
current = readFileSync10(path8, "utf8");
|
|
5888
6372
|
}
|
|
5889
6373
|
const lines = current.split(/\r?\n/);
|
|
5890
6374
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -6016,7 +6500,7 @@ async function runLogs(args, io) {
|
|
|
6016
6500
|
if (r.deployments.length === 0) {
|
|
6017
6501
|
io.out("");
|
|
6018
6502
|
io.out("(note: this command shows deploy history, not runtime logs —");
|
|
6019
|
-
io.out(
|
|
6503
|
+
io.out(` run \`launchpad status ${slug}\` for the latest build outcome)`);
|
|
6020
6504
|
}
|
|
6021
6505
|
return 0;
|
|
6022
6506
|
} catch (e) {
|
|
@@ -6184,6 +6668,8 @@ async function runMerge(args, io) {
|
|
|
6184
6668
|
if (body.selfMerge) {
|
|
6185
6669
|
io.out("Note: self-merge (last commit author == caller)");
|
|
6186
6670
|
}
|
|
6671
|
+
io.out("");
|
|
6672
|
+
io.out(`Merged; build pending — run \`launchpad status ${slug}\` to confirm the build outcome.`);
|
|
6187
6673
|
return 0;
|
|
6188
6674
|
}
|
|
6189
6675
|
let env = null;
|
|
@@ -6296,7 +6782,7 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
6296
6782
|
io.err(`launchpad merge: GitHub upstream error (${detail ?? code}).`);
|
|
6297
6783
|
return;
|
|
6298
6784
|
case "merge_unprocessable":
|
|
6299
|
-
io.err(`launchpad merge: GitHub refused the merge (422): ${detail ?? "
|
|
6785
|
+
io.err(`launchpad merge: GitHub refused the merge (422): ${detail ?? (prNumber !== null ? `run \`launchpad review ${prNumber}\` for details` : "run `launchpad review <prNumber>` for details")}`);
|
|
6300
6786
|
return;
|
|
6301
6787
|
default:
|
|
6302
6788
|
io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
|
|
@@ -6783,6 +7269,26 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
|
6783
7269
|
return apiJson(cfg, { path: path10 }, fetcher);
|
|
6784
7270
|
}
|
|
6785
7271
|
|
|
7272
|
+
// src/deploy/deployment-status.ts
|
|
7273
|
+
async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
7274
|
+
let raw;
|
|
7275
|
+
try {
|
|
7276
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/deployment-status` }, fetcher);
|
|
7277
|
+
} catch (e) {
|
|
7278
|
+
if (e instanceof NotFoundError)
|
|
7279
|
+
return null;
|
|
7280
|
+
throw e;
|
|
7281
|
+
}
|
|
7282
|
+
if (typeof raw.supported !== "boolean")
|
|
7283
|
+
return null;
|
|
7284
|
+
return {
|
|
7285
|
+
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
7286
|
+
supported: raw.supported,
|
|
7287
|
+
liveDeployment: raw.liveDeployment ?? null,
|
|
7288
|
+
lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null
|
|
7289
|
+
};
|
|
7290
|
+
}
|
|
7291
|
+
|
|
6786
7292
|
// src/commands/pull.ts
|
|
6787
7293
|
var pullCommand = {
|
|
6788
7294
|
name: "pull",
|
|
@@ -6828,6 +7334,12 @@ async function runPull(args, io) {
|
|
|
6828
7334
|
includeManifest: true
|
|
6829
7335
|
});
|
|
6830
7336
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7337
|
+
const live = await fetchLiveDeploymentBestEffort(cfg, parsed.slug);
|
|
7338
|
+
if (live?.liveDeployment != null && (live.liveDeployment.buildStatus === "success" || live.lastSuccessfulDeployment != null)) {
|
|
7339
|
+
io.err(`launchpad pull: no platform-tracked manifest for "${parsed.slug}", ` + `but the app HAS live content (last deployment ${live.liveDeployment.createdOn} ` + `via ${live.liveDeployment.trigger === "git-push" ? "git push" : live.liveDeployment.trigger}, ` + `build ${live.liveDeployment.buildStatus}).`);
|
|
7340
|
+
io.err(` This app deploys outside \`launchpad deploy\`, so there is no manifest to pull. ` + `Run \`launchpad status ${parsed.slug}\` for the live deployment detail.`);
|
|
7341
|
+
return 1;
|
|
7342
|
+
}
|
|
6831
7343
|
io.err(`launchpad pull: no deployed manifest for "${parsed.slug}" yet — ` + `run \`launchpad deploy\` first, or check \`launchpad status\` for the apply state.`);
|
|
6832
7344
|
return 1;
|
|
6833
7345
|
}
|
|
@@ -6864,6 +7376,13 @@ async function runPull(args, io) {
|
|
|
6864
7376
|
return 1;
|
|
6865
7377
|
}
|
|
6866
7378
|
}
|
|
7379
|
+
async function fetchLiveDeploymentBestEffort(cfg, slug) {
|
|
7380
|
+
try {
|
|
7381
|
+
return await fetchDeploymentStatus(cfg, slug);
|
|
7382
|
+
} catch {
|
|
7383
|
+
return null;
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
6867
7386
|
function parseArgs8(args, cwd = process.cwd()) {
|
|
6868
7387
|
let slug = null;
|
|
6869
7388
|
let out = null;
|
|
@@ -6938,7 +7457,7 @@ function describe23(e) {
|
|
|
6938
7457
|
}
|
|
6939
7458
|
|
|
6940
7459
|
// src/commands/status.ts
|
|
6941
|
-
import { readFileSync as
|
|
7460
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
6942
7461
|
var statusCommand = {
|
|
6943
7462
|
name: "status",
|
|
6944
7463
|
summary: "show drift between local launchpad.yaml and deployed state",
|
|
@@ -6995,7 +7514,7 @@ async function runStatus(args, io) {
|
|
|
6995
7514
|
}
|
|
6996
7515
|
let localYaml;
|
|
6997
7516
|
try {
|
|
6998
|
-
localYaml =
|
|
7517
|
+
localYaml = readFileSync11(parsed.file, "utf8");
|
|
6999
7518
|
} catch (e) {
|
|
7000
7519
|
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
|
|
7001
7520
|
if (lifecycle !== null && lifecycle.state === "live") {
|
|
@@ -7005,8 +7524,8 @@ async function runStatus(args, io) {
|
|
|
7005
7524
|
}
|
|
7006
7525
|
let localObj;
|
|
7007
7526
|
try {
|
|
7008
|
-
const { parse:
|
|
7009
|
-
localObj =
|
|
7527
|
+
const { parse: parseYaml5 } = await import("yaml");
|
|
7528
|
+
localObj = parseYaml5(localYaml);
|
|
7010
7529
|
} catch (e) {
|
|
7011
7530
|
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
|
|
7012
7531
|
return 2;
|
|
@@ -7027,25 +7546,39 @@ async function runStatus(args, io) {
|
|
|
7027
7546
|
} catch (e) {
|
|
7028
7547
|
return mapBotError(e, parsed.slug, io);
|
|
7029
7548
|
}
|
|
7549
|
+
let deployment = null;
|
|
7550
|
+
let deploymentKnown = false;
|
|
7551
|
+
try {
|
|
7552
|
+
deployment = await fetchDeploymentStatus(cfg, parsed.slug);
|
|
7553
|
+
deploymentKnown = deployment !== null;
|
|
7554
|
+
} catch (e) {
|
|
7555
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7556
|
+
return mapBotError(e, parsed.slug, io);
|
|
7557
|
+
}
|
|
7558
|
+
io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
7559
|
+
}
|
|
7030
7560
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7561
|
+
const live = deployment?.liveDeployment ?? null;
|
|
7562
|
+
const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
7031
7563
|
const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
|
|
7032
7564
|
const out = {
|
|
7033
|
-
state: liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7565
|
+
state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7034
7566
|
slug: parsed.slug,
|
|
7035
7567
|
deployedSha: state.lastAppliedManifestSha,
|
|
7036
7568
|
headSha: state.appRepoHeadSha,
|
|
7037
7569
|
hasOpenPr: state.openPr !== null,
|
|
7038
7570
|
openPrNumber: state.openPr?.number ?? null,
|
|
7039
7571
|
driftFields: [],
|
|
7040
|
-
driftDetails: []
|
|
7572
|
+
driftDetails: [],
|
|
7573
|
+
...deploymentKnown ? { deployment } : {}
|
|
7041
7574
|
};
|
|
7042
7575
|
emit3(out, parsed.json, io);
|
|
7043
7576
|
return 0;
|
|
7044
7577
|
}
|
|
7045
7578
|
let deployedObj;
|
|
7046
7579
|
try {
|
|
7047
|
-
const { parse:
|
|
7048
|
-
deployedObj =
|
|
7580
|
+
const { parse: parseYaml5 } = await import("yaml");
|
|
7581
|
+
deployedObj = parseYaml5(state.manifestYaml);
|
|
7049
7582
|
} catch (e) {
|
|
7050
7583
|
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
|
|
7051
7584
|
return 2;
|
|
@@ -7068,7 +7601,8 @@ async function runStatus(args, io) {
|
|
|
7068
7601
|
hasOpenPr: state.openPr !== null,
|
|
7069
7602
|
openPrNumber: state.openPr?.number ?? null,
|
|
7070
7603
|
driftFields: drift.map((d) => d.path),
|
|
7071
|
-
driftDetails: drift
|
|
7604
|
+
driftDetails: drift,
|
|
7605
|
+
...deploymentKnown ? { deployment } : {}
|
|
7072
7606
|
};
|
|
7073
7607
|
emit3(result, parsed.json, io);
|
|
7074
7608
|
if (result.state === "drift" && parsed.strict) {
|
|
@@ -7154,14 +7688,22 @@ function emit3(out, asJson, io) {
|
|
|
7154
7688
|
case "live_no_content":
|
|
7155
7689
|
io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
|
|
7156
7690
|
surfaceHeadVsDeployed(out, io);
|
|
7691
|
+
surfaceDeployment(out, io);
|
|
7157
7692
|
return;
|
|
7158
7693
|
case "no_deployed_manifest":
|
|
7159
7694
|
io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
|
|
7160
7695
|
surfaceHeadVsDeployed(out, io);
|
|
7696
|
+
surfaceDeployment(out, io);
|
|
7697
|
+
return;
|
|
7698
|
+
case "live_content_untracked":
|
|
7699
|
+
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
7700
|
+
surfaceHeadVsDeployed(out, io);
|
|
7701
|
+
surfaceDeployment(out, io);
|
|
7161
7702
|
return;
|
|
7162
7703
|
case "in_sync":
|
|
7163
7704
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
7164
7705
|
surfaceHeadVsDeployed(out, io);
|
|
7706
|
+
surfaceDeployment(out, io);
|
|
7165
7707
|
return;
|
|
7166
7708
|
case "drift":
|
|
7167
7709
|
io.out(`${out.slug}: live, drift: ${out.driftFields.join(", ")}`);
|
|
@@ -7171,7 +7713,54 @@ function emit3(out, asJson, io) {
|
|
|
7171
7713
|
io.out(` deployed: ${formatValue(d.deployed)}`);
|
|
7172
7714
|
}
|
|
7173
7715
|
surfaceHeadVsDeployed(out, io);
|
|
7716
|
+
surfaceDeployment(out, io);
|
|
7717
|
+
return;
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
function triggerLabel(trigger) {
|
|
7721
|
+
if (trigger === "git-push")
|
|
7722
|
+
return "git push";
|
|
7723
|
+
if (trigger === "bot")
|
|
7724
|
+
return "launchpad deploy (bot)";
|
|
7725
|
+
return "an unknown mechanism";
|
|
7726
|
+
}
|
|
7727
|
+
function surfaceDeployment(out, io) {
|
|
7728
|
+
const dep = out.deployment;
|
|
7729
|
+
if (dep === undefined || dep === null || !dep.supported)
|
|
7730
|
+
return;
|
|
7731
|
+
const live = dep.liveDeployment;
|
|
7732
|
+
if (live === null) {
|
|
7733
|
+
io.out(" last deployment: none — Cloudflare Pages has no production deployment yet.");
|
|
7734
|
+
return;
|
|
7735
|
+
}
|
|
7736
|
+
const commit = live.commit !== undefined ? `, commit ${live.commit.hash.slice(0, 7)}` : "";
|
|
7737
|
+
const via = triggerLabel(live.trigger);
|
|
7738
|
+
switch (live.buildStatus) {
|
|
7739
|
+
case "success":
|
|
7740
|
+
io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
|
|
7174
7741
|
return;
|
|
7742
|
+
case "in_progress":
|
|
7743
|
+
io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
|
|
7744
|
+
io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
|
|
7745
|
+
return;
|
|
7746
|
+
case "failure": {
|
|
7747
|
+
const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
|
|
7748
|
+
io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
|
|
7749
|
+
if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
|
|
7750
|
+
io.out(" build log (excerpt):");
|
|
7751
|
+
for (const line of live.logExcerpt.split(`
|
|
7752
|
+
`)) {
|
|
7753
|
+
io.out(` ${line}`);
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
if (dep.lastSuccessfulDeployment !== null) {
|
|
7757
|
+
io.out(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
|
|
7758
|
+
} else {
|
|
7759
|
+
io.out(" serving: nothing — no successful deployment exists yet.");
|
|
7760
|
+
}
|
|
7761
|
+
io.out(" next: fix the build locally (check build.command / build.root_dir in launchpad.yaml), then run `launchpad deploy`.");
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7175
7764
|
}
|
|
7176
7765
|
}
|
|
7177
7766
|
function surfaceHeadVsDeployed(out, io) {
|
|
@@ -7451,7 +8040,7 @@ function describe25(e) {
|
|
|
7451
8040
|
}
|
|
7452
8041
|
|
|
7453
8042
|
// src/deploy/rollback.ts
|
|
7454
|
-
import { existsSync as
|
|
8043
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7455
8044
|
import { resolve as resolvePath2 } from "node:path";
|
|
7456
8045
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
7457
8046
|
|
|
@@ -7575,11 +8164,11 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
7575
8164
|
}, io, applyDeps);
|
|
7576
8165
|
}
|
|
7577
8166
|
function readCurrentManifest(path11) {
|
|
7578
|
-
if (!
|
|
8167
|
+
if (!existsSync7(path11))
|
|
7579
8168
|
return null;
|
|
7580
8169
|
let raw;
|
|
7581
8170
|
try {
|
|
7582
|
-
raw =
|
|
8171
|
+
raw = readFileSync12(path11, "utf8");
|
|
7583
8172
|
} catch {
|
|
7584
8173
|
return null;
|
|
7585
8174
|
}
|
|
@@ -7735,7 +8324,7 @@ function parseArgs11(args) {
|
|
|
7735
8324
|
}
|
|
7736
8325
|
|
|
7737
8326
|
// src/secrets/push.ts
|
|
7738
|
-
import { existsSync as
|
|
8327
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
|
|
7739
8328
|
import { resolve as resolvePath3 } from "node:path";
|
|
7740
8329
|
|
|
7741
8330
|
// src/secrets/env-parse.ts
|
|
@@ -7790,14 +8379,14 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
7790
8379
|
return 0;
|
|
7791
8380
|
}
|
|
7792
8381
|
const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
|
|
7793
|
-
if (!
|
|
8382
|
+
if (!existsSync8(envPath)) {
|
|
7794
8383
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
7795
8384
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
7796
8385
|
return 2;
|
|
7797
8386
|
}
|
|
7798
8387
|
let envText;
|
|
7799
8388
|
try {
|
|
7800
|
-
envText =
|
|
8389
|
+
envText = readFileSync13(envPath, "utf8");
|
|
7801
8390
|
} catch (e) {
|
|
7802
8391
|
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
|
|
7803
8392
|
return 2;
|
|
@@ -8069,9 +8658,9 @@ function renderManifestError5(result, io) {
|
|
|
8069
8658
|
}
|
|
8070
8659
|
|
|
8071
8660
|
// src/secrets/set.ts
|
|
8072
|
-
import { existsSync as
|
|
8661
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
|
|
8073
8662
|
import { resolve as resolvePath5 } from "node:path";
|
|
8074
|
-
import { parse as
|
|
8663
|
+
import { parse as parseYaml5 } from "yaml";
|
|
8075
8664
|
var CELL_LABEL2 = {
|
|
8076
8665
|
present: "PRESENT",
|
|
8077
8666
|
missing: "MISSING",
|
|
@@ -8079,14 +8668,14 @@ var CELL_LABEL2 = {
|
|
|
8079
8668
|
};
|
|
8080
8669
|
function loadSet(fleetFile, setName, io) {
|
|
8081
8670
|
const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
8082
|
-
if (!
|
|
8671
|
+
if (!existsSync9(path11)) {
|
|
8083
8672
|
io.err(`✗ ${path11}`);
|
|
8084
8673
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
8085
8674
|
return 2;
|
|
8086
8675
|
}
|
|
8087
8676
|
let obj;
|
|
8088
8677
|
try {
|
|
8089
|
-
obj =
|
|
8678
|
+
obj = parseYaml5(readFileSync14(path11, "utf8"));
|
|
8090
8679
|
} catch (e) {
|
|
8091
8680
|
io.err(`✗ ${path11}`);
|
|
8092
8681
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -8171,14 +8760,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
8171
8760
|
if (typeof set === "number")
|
|
8172
8761
|
return set;
|
|
8173
8762
|
const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
|
|
8174
|
-
if (!
|
|
8763
|
+
if (!existsSync9(envPath)) {
|
|
8175
8764
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
8176
8765
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
8177
8766
|
return 2;
|
|
8178
8767
|
}
|
|
8179
8768
|
let envText;
|
|
8180
8769
|
try {
|
|
8181
|
-
envText =
|
|
8770
|
+
envText = readFileSync14(envPath, "utf8");
|
|
8182
8771
|
} catch (e) {
|
|
8183
8772
|
io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8184
8773
|
return 2;
|
|
@@ -8322,7 +8911,7 @@ function setPushExit(e) {
|
|
|
8322
8911
|
}
|
|
8323
8912
|
|
|
8324
8913
|
// src/commands/secrets-template.ts
|
|
8325
|
-
import { existsSync as
|
|
8914
|
+
import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8326
8915
|
import { resolve as resolve9 } from "node:path";
|
|
8327
8916
|
async function runSecretsTemplate(args, io) {
|
|
8328
8917
|
const flags = parseFlags4(args);
|
|
@@ -8346,7 +8935,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
8346
8935
|
return 0;
|
|
8347
8936
|
}
|
|
8348
8937
|
const outPath = resolve9(process.cwd(), flags.out);
|
|
8349
|
-
if (
|
|
8938
|
+
if (existsSync10(outPath) && !flags.force) {
|
|
8350
8939
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
8351
8940
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
8352
8941
|
return 64;
|
|
@@ -8634,8 +9223,8 @@ function printHelp2(io) {
|
|
|
8634
9223
|
|
|
8635
9224
|
// src/commands/skills.ts
|
|
8636
9225
|
import { fileURLToPath } from "node:url";
|
|
8637
|
-
import { dirname as dirname6, join as
|
|
8638
|
-
import { promises as fs5, existsSync as
|
|
9226
|
+
import { dirname as dirname6, join as join11, resolve as resolve10 } from "node:path";
|
|
9227
|
+
import { promises as fs5, existsSync as existsSync11 } from "node:fs";
|
|
8639
9228
|
import { homedir as homedir2 } from "node:os";
|
|
8640
9229
|
var BUNDLE_PREFIX = "launchpad-";
|
|
8641
9230
|
var BUNDLED_SKILLS = [
|
|
@@ -8696,7 +9285,7 @@ function printHelp3(io) {
|
|
|
8696
9285
|
}
|
|
8697
9286
|
function resolveInstallEnv() {
|
|
8698
9287
|
const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
|
|
8699
|
-
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
9288
|
+
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join11(homedir2(), ".claude", "skills");
|
|
8700
9289
|
return { bundleDir, userSkillsDir };
|
|
8701
9290
|
}
|
|
8702
9291
|
function defaultBundleDir() {
|
|
@@ -8706,7 +9295,7 @@ function defaultBundleDir() {
|
|
|
8706
9295
|
resolve10(here, "..", "..", "skills")
|
|
8707
9296
|
];
|
|
8708
9297
|
for (const c of candidates) {
|
|
8709
|
-
if (
|
|
9298
|
+
if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
|
|
8710
9299
|
return c;
|
|
8711
9300
|
}
|
|
8712
9301
|
}
|
|
@@ -8727,12 +9316,12 @@ async function doInstall(io) {
|
|
|
8727
9316
|
}
|
|
8728
9317
|
let installed = 0;
|
|
8729
9318
|
for (const skill of BUNDLED_SKILLS) {
|
|
8730
|
-
const src =
|
|
9319
|
+
const src = join11(env.bundleDir, skill);
|
|
8731
9320
|
if (!await isDir(src)) {
|
|
8732
9321
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
8733
9322
|
return 1;
|
|
8734
9323
|
}
|
|
8735
|
-
const dest =
|
|
9324
|
+
const dest = join11(env.userSkillsDir, skill);
|
|
8736
9325
|
await fs5.rm(dest, { recursive: true, force: true });
|
|
8737
9326
|
await fs5.cp(src, dest, { recursive: true });
|
|
8738
9327
|
installed++;
|
|
@@ -8757,7 +9346,7 @@ async function doUninstall(io) {
|
|
|
8757
9346
|
continue;
|
|
8758
9347
|
if (!isBundleManaged(entry.name))
|
|
8759
9348
|
continue;
|
|
8760
|
-
const target =
|
|
9349
|
+
const target = join11(env.userSkillsDir, entry.name);
|
|
8761
9350
|
await fs5.rm(target, { recursive: true, force: true });
|
|
8762
9351
|
removed++;
|
|
8763
9352
|
io.out(`✗ removed ${target}`);
|
|
@@ -8780,7 +9369,7 @@ async function doList(io) {
|
|
|
8780
9369
|
}
|
|
8781
9370
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
8782
9371
|
for (const name of managedDirs) {
|
|
8783
|
-
const skillFile =
|
|
9372
|
+
const skillFile = join11(env.userSkillsDir, name, "SKILL.md");
|
|
8784
9373
|
const version = await readVersion(skillFile);
|
|
8785
9374
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
8786
9375
|
}
|
|
@@ -8815,9 +9404,9 @@ function describe28(e) {
|
|
|
8815
9404
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
8816
9405
|
import { promisify } from "node:util";
|
|
8817
9406
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8818
|
-
import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as
|
|
9407
|
+
import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
|
|
8819
9408
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
8820
|
-
import { readFileSync as
|
|
9409
|
+
import { readFileSync as readFileSync15, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
8821
9410
|
|
|
8822
9411
|
// src/commands/channel-auth.ts
|
|
8823
9412
|
import { createServer as createServer2 } from "node:http";
|
|
@@ -8938,7 +9527,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
8938
9527
|
var REGISTRY = "https://registry.npmjs.org";
|
|
8939
9528
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
8940
9529
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
8941
|
-
var CHANNEL_MARKER =
|
|
9530
|
+
var CHANNEL_MARKER = join12(homedir3(), ".launchpad", "channel");
|
|
8942
9531
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
8943
9532
|
var UPGRADE_ARGS = {
|
|
8944
9533
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -9021,8 +9610,8 @@ async function openSystemBrowser(url) {
|
|
|
9021
9610
|
await execFileAsync(opener, [url]);
|
|
9022
9611
|
}
|
|
9023
9612
|
async function runInstallerScript(script) {
|
|
9024
|
-
const dir = mkdtempSync(
|
|
9025
|
-
const file =
|
|
9613
|
+
const dir = mkdtempSync(join12(tmpdir(), "launchpad-update-"));
|
|
9614
|
+
const file = join12(dir, "install.sh");
|
|
9026
9615
|
try {
|
|
9027
9616
|
writeFileSync7(file, script, { mode: 448 });
|
|
9028
9617
|
return await new Promise((resolvePromise) => {
|
|
@@ -9046,7 +9635,7 @@ function resolveLatestVersion() {
|
|
|
9046
9635
|
}
|
|
9047
9636
|
function detectInstallChannel() {
|
|
9048
9637
|
try {
|
|
9049
|
-
return
|
|
9638
|
+
return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
|
|
9050
9639
|
} catch {
|
|
9051
9640
|
return "github";
|
|
9052
9641
|
}
|
|
@@ -9253,7 +9842,8 @@ function printHelp4(io) {
|
|
|
9253
9842
|
}
|
|
9254
9843
|
|
|
9255
9844
|
// src/commands/validate.ts
|
|
9256
|
-
import {
|
|
9845
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
9846
|
+
import { dirname as dirname8, resolve as resolve12 } from "node:path";
|
|
9257
9847
|
var validateCommand = {
|
|
9258
9848
|
name: "validate",
|
|
9259
9849
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -9272,9 +9862,31 @@ async function runValidate(args, io) {
|
|
|
9272
9862
|
if (result.kind !== "ok") {
|
|
9273
9863
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
9274
9864
|
}
|
|
9865
|
+
const boundary = checkBoundary(path11, result.manifest.app !== undefined);
|
|
9275
9866
|
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
|
|
9276
|
-
return json ? renderJsonOk(result, groupCheck, io) : renderHumanOk(result, groupCheck, io);
|
|
9867
|
+
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
9868
|
+
}
|
|
9869
|
+
function checkBoundary(manifestPath, declared) {
|
|
9870
|
+
const dir = dirname8(manifestPath);
|
|
9871
|
+
let files;
|
|
9872
|
+
try {
|
|
9873
|
+
files = walkCwd(dir).files;
|
|
9874
|
+
} catch {
|
|
9875
|
+
return { declared, errors: [], warnings: [] };
|
|
9876
|
+
}
|
|
9877
|
+
let manifestYaml;
|
|
9878
|
+
try {
|
|
9879
|
+
manifestYaml = readFileSync16(manifestPath, "utf8");
|
|
9880
|
+
} catch {
|
|
9881
|
+
manifestYaml = null;
|
|
9882
|
+
}
|
|
9883
|
+
const r = applyBoundaryToFiles({ cwd: dir, manifestYaml, files });
|
|
9884
|
+
if (r.kind === "error") {
|
|
9885
|
+
return { declared, errors: [r.message], warnings: [] };
|
|
9886
|
+
}
|
|
9887
|
+
return { declared, errors: [], warnings: r.warnings };
|
|
9277
9888
|
}
|
|
9889
|
+
var DECLARE_APP_HINT = "no app: boundary declared — deploys bundle the whole working tree minus the platform deny-list. " + "Declare app: (root / include / exclude) in launchpad.yaml — see schema/README.md.";
|
|
9278
9890
|
async function checkGroup(allowedGroup, cfg) {
|
|
9279
9891
|
try {
|
|
9280
9892
|
const r = await fetchGroups(cfg);
|
|
@@ -9321,10 +9933,22 @@ async function checkGroups(groups) {
|
|
|
9321
9933
|
const ok = perGroup.every((r) => r.kind === "ok");
|
|
9322
9934
|
return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
|
|
9323
9935
|
}
|
|
9324
|
-
function renderHumanOk(result, group, io) {
|
|
9936
|
+
function renderHumanOk(result, group, boundary, io) {
|
|
9325
9937
|
io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
|
|
9326
9938
|
io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
|
|
9327
9939
|
io.out(` type: ${result.manifest.deployment.type}`);
|
|
9940
|
+
if (boundary.errors.length > 0) {
|
|
9941
|
+
for (const e of boundary.errors) {
|
|
9942
|
+
io.err(`✗ app boundary: ${e}`);
|
|
9943
|
+
}
|
|
9944
|
+
return 1;
|
|
9945
|
+
}
|
|
9946
|
+
for (const w of boundary.warnings) {
|
|
9947
|
+
io.out(`⚠ app boundary: ${w}`);
|
|
9948
|
+
}
|
|
9949
|
+
if (!boundary.declared) {
|
|
9950
|
+
io.out(`⚠ ${DECLARE_APP_HINT}`);
|
|
9951
|
+
}
|
|
9328
9952
|
switch (group.kind) {
|
|
9329
9953
|
case "skipped":
|
|
9330
9954
|
return 0;
|
|
@@ -9388,11 +10012,20 @@ function renderHumanError(result, io) {
|
|
|
9388
10012
|
return 1;
|
|
9389
10013
|
}
|
|
9390
10014
|
}
|
|
9391
|
-
function renderJsonOk(result, group, io) {
|
|
10015
|
+
function renderJsonOk(result, group, boundary, io) {
|
|
9392
10016
|
const base = {
|
|
9393
10017
|
name: result.manifest.metadata.name,
|
|
9394
|
-
deploymentType: result.manifest.deployment.type
|
|
10018
|
+
deploymentType: result.manifest.deployment.type,
|
|
10019
|
+
appBoundary: {
|
|
10020
|
+
declared: boundary.declared,
|
|
10021
|
+
errors: boundary.errors,
|
|
10022
|
+
warnings: boundary.warnings
|
|
10023
|
+
}
|
|
9395
10024
|
};
|
|
10025
|
+
if (boundary.errors.length > 0) {
|
|
10026
|
+
io.out(JSON.stringify({ ok: false, ...base }));
|
|
10027
|
+
return 1;
|
|
10028
|
+
}
|
|
9396
10029
|
switch (group.kind) {
|
|
9397
10030
|
case "skipped":
|
|
9398
10031
|
io.out(JSON.stringify({ ok: true, ...base }));
|
|
@@ -9557,15 +10190,15 @@ function describe30(e) {
|
|
|
9557
10190
|
// src/update-notifier.ts
|
|
9558
10191
|
import { spawn as spawn6 } from "node:child_process";
|
|
9559
10192
|
import { homedir as homedir4 } from "node:os";
|
|
9560
|
-
import { join as
|
|
9561
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
10193
|
+
import { join as join13 } from "node:path";
|
|
10194
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
|
|
9562
10195
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
9563
10196
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9564
10197
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
9565
|
-
var CACHE_FILE =
|
|
10198
|
+
var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
|
|
9566
10199
|
function readCache2() {
|
|
9567
10200
|
try {
|
|
9568
|
-
const raw = JSON.parse(
|
|
10201
|
+
const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
|
|
9569
10202
|
if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
|
|
9570
10203
|
const latest = raw.latest;
|
|
9571
10204
|
return {
|
|
@@ -9578,7 +10211,7 @@ function readCache2() {
|
|
|
9578
10211
|
}
|
|
9579
10212
|
function writeCache2(state) {
|
|
9580
10213
|
try {
|
|
9581
|
-
mkdirSync3(
|
|
10214
|
+
mkdirSync3(join13(homedir4(), ".launchpad"), { recursive: true });
|
|
9582
10215
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
9583
10216
|
`, { mode: 384 });
|
|
9584
10217
|
} catch {}
|