@m-kopa/launchpad-cli 0.23.0 → 0.25.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 +65 -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/bundle/upload.d.ts +34 -2
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +1592 -882
- package/dist/commands/deploy-flags.d.ts +6 -0
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- 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 +15 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/deploy/apply.d.ts +5 -2
- package/dist/deploy/apply.d.ts.map +1 -1
- package/dist/deploy/deployment-status.d.ts +56 -0
- package/dist/deploy/deployment-status.d.ts.map +1 -0
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +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.25.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";
|
|
@@ -1432,6 +1432,9 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1432
1432
|
if (res.status === 202) {
|
|
1433
1433
|
return { kind: "ok", response: body };
|
|
1434
1434
|
}
|
|
1435
|
+
if (res.status === 200 && typeof body === "object" && body !== null && body.outcome === "nothing-to-deploy") {
|
|
1436
|
+
return { kind: "ok", response: body };
|
|
1437
|
+
}
|
|
1435
1438
|
return {
|
|
1436
1439
|
kind: "error",
|
|
1437
1440
|
status: res.status,
|
|
@@ -1777,741 +1780,656 @@ async function buildWorkerArtifact(cwd, manifestYaml, walkFiles) {
|
|
|
1777
1780
|
};
|
|
1778
1781
|
}
|
|
1779
1782
|
|
|
1780
|
-
// src/bundle/
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1783
|
+
// src/bundle/boundary.ts
|
|
1784
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1785
|
+
import { join as join5 } from "node:path";
|
|
1786
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1787
|
+
|
|
1788
|
+
// ../launchpad-engine/dist/schema.js
|
|
1789
|
+
import { z } from "zod";
|
|
1790
|
+
var APP_TYPES2 = ["static", "react", "react+api", "container"];
|
|
1791
|
+
var AUTH_MODES = ["access", "gateway"];
|
|
1792
|
+
var RUNTIMES = ["cloudflare-containers", "aca"];
|
|
1793
|
+
var SECRET_SOURCES = ["env-file", "platform-managed"];
|
|
1794
|
+
var TARGET_KINDS = ["pages", "worker"];
|
|
1795
|
+
var PRIMARY_TARGET = "primary";
|
|
1796
|
+
var SLUG_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
1797
|
+
var ENV_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
1798
|
+
var SESSION_DURATION_REGEX = /^[0-9]+(s|m|h)$/;
|
|
1799
|
+
var MetadataSchema = z.object({
|
|
1800
|
+
name: z.string().min(2).max(63).regex(SLUG_REGEX, "must be lowercase letters, digits, and hyphens; start/end alphanumeric"),
|
|
1801
|
+
team: z.string().min(1),
|
|
1802
|
+
owner: z.string().email(),
|
|
1803
|
+
description: z.string().max(280).optional()
|
|
1804
|
+
}).strict();
|
|
1805
|
+
var DeploymentSchema = z.object({
|
|
1806
|
+
type: z.enum(APP_TYPES2),
|
|
1807
|
+
runtime: z.enum(RUNTIMES).optional()
|
|
1808
|
+
}).strict();
|
|
1809
|
+
var AccessSchema = z.object({
|
|
1810
|
+
allowed_entra_group: z.string().min(1).optional(),
|
|
1811
|
+
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(),
|
|
1812
|
+
session_duration: z.string().regex(SESSION_DURATION_REGEX, "must match Go duration format, e.g. 24h / 30m / 60s").optional()
|
|
1813
|
+
}).strict().superRefine((access2, ctx) => {
|
|
1814
|
+
const hasSingular = access2.allowed_entra_group !== undefined;
|
|
1815
|
+
const hasPlural = access2.allowed_entra_groups !== undefined;
|
|
1816
|
+
if (!hasSingular && !hasPlural) {
|
|
1817
|
+
ctx.addIssue({
|
|
1818
|
+
code: z.ZodIssueCode.custom,
|
|
1819
|
+
message: "either `allowed_entra_group` (singular, legacy) or `allowed_entra_groups` (plural, preferred) must be set",
|
|
1820
|
+
path: ["allowed_entra_groups"]
|
|
1821
|
+
});
|
|
1792
1822
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
return {
|
|
1801
|
-
kind: "walk-error",
|
|
1802
|
-
message: `unexpected walk failure: ${e.message}`
|
|
1803
|
-
};
|
|
1823
|
+
if (hasSingular && hasPlural) {
|
|
1824
|
+
ctx.addIssue({
|
|
1825
|
+
code: z.ZodIssueCode.custom,
|
|
1826
|
+
message: "`allowed_entra_group` (singular) and `allowed_entra_groups` (plural) are mutually exclusive; use the plural form",
|
|
1827
|
+
path: ["allowed_entra_groups"]
|
|
1828
|
+
});
|
|
1804
1829
|
}
|
|
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
|
-
};
|
|
1830
|
+
});
|
|
1831
|
+
function allowedEntraGroups(access2) {
|
|
1832
|
+
if (access2.allowed_entra_groups !== undefined) {
|
|
1833
|
+
return access2.allowed_entra_groups;
|
|
1816
1834
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1835
|
+
return [access2.allowed_entra_group];
|
|
1836
|
+
}
|
|
1837
|
+
var ContainerSchema = z.object({
|
|
1838
|
+
name: z.string().min(2).max(32).regex(SLUG_REGEX, "container name must be lowercase letters, digits, and hyphens"),
|
|
1839
|
+
image: z.string().min(1),
|
|
1840
|
+
port: z.number().int().min(1).max(65535),
|
|
1841
|
+
ingress: z.boolean().optional(),
|
|
1842
|
+
healthcheck: z.string().default("/health"),
|
|
1843
|
+
env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
|
|
1844
|
+
max_instances: z.number().int().min(1).max(100).optional()
|
|
1845
|
+
}).strict();
|
|
1846
|
+
var TargetSchema = z.object({
|
|
1847
|
+
kind: z.enum(TARGET_KINDS),
|
|
1848
|
+
script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
|
|
1849
|
+
schedule: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
|
|
1850
|
+
d1_binding: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "d1_binding must be an UPPER_SNAKE_CASE binding name").optional()
|
|
1851
|
+
}).strict();
|
|
1852
|
+
var SecretBindingSchema = z.object({
|
|
1853
|
+
name: z.string().min(1).max(64).regex(ENV_NAME_REGEX, "secret binding name must be UPPER_SNAKE_CASE"),
|
|
1854
|
+
source: z.enum(SECRET_SOURCES),
|
|
1855
|
+
description: z.string().max(200).optional(),
|
|
1856
|
+
targets: z.union([z.literal("all"), z.array(z.string().min(1)).min(1)]).optional()
|
|
1857
|
+
}).strict();
|
|
1858
|
+
var SecretsSchema = z.object({
|
|
1859
|
+
bindings: z.array(SecretBindingSchema).optional()
|
|
1860
|
+
}).strict();
|
|
1861
|
+
var BuildSchema = z.object({
|
|
1862
|
+
command: z.string().min(1),
|
|
1863
|
+
destination_dir: z.string().min(1),
|
|
1864
|
+
root_dir: z.string()
|
|
1865
|
+
}).strict();
|
|
1866
|
+
var AppBoundarySchema = z.object({
|
|
1867
|
+
root: z.string().min(1).optional(),
|
|
1868
|
+
include: z.array(z.string().min(1)).min(1).optional(),
|
|
1869
|
+
exclude: z.array(z.string().min(1)).optional()
|
|
1870
|
+
}).strict();
|
|
1871
|
+
var ProductionEnvSchema = z.record(z.string().regex(ENV_NAME_REGEX, "production_env keys must be UPPER_SNAKE_CASE"), z.string());
|
|
1872
|
+
var ManifestSchema = z.object({
|
|
1873
|
+
apiVersion: z.literal("launchpad.m-kopa.us/v1alpha1"),
|
|
1874
|
+
kind: z.literal("App"),
|
|
1875
|
+
metadata: MetadataSchema,
|
|
1876
|
+
deployment: DeploymentSchema,
|
|
1877
|
+
access: AccessSchema,
|
|
1878
|
+
auth: z.enum(AUTH_MODES).optional(),
|
|
1879
|
+
hostnames: z.array(z.string().min(1)).optional(),
|
|
1880
|
+
containers: z.array(ContainerSchema).min(1).optional(),
|
|
1881
|
+
secrets: SecretsSchema.optional(),
|
|
1882
|
+
targets: z.array(TargetSchema).optional(),
|
|
1883
|
+
build: BuildSchema.optional(),
|
|
1884
|
+
production_env: ProductionEnvSchema.optional(),
|
|
1885
|
+
app: AppBoundarySchema.optional()
|
|
1886
|
+
}).strict().superRefine((m, ctx) => {
|
|
1887
|
+
const isContainer = m.deployment.type === "container";
|
|
1888
|
+
if (isContainer && m.auth === "gateway") {
|
|
1889
|
+
ctx.addIssue({
|
|
1890
|
+
code: z.ZodIssueCode.custom,
|
|
1891
|
+
path: ["auth"],
|
|
1892
|
+
message: "auth: gateway is only supported for pages-app shapes (static/react/react+api); container apps stay on Cloudflare Access (ADR 0016)."
|
|
1893
|
+
});
|
|
1820
1894
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
body: uploadResult.response
|
|
1828
|
-
};
|
|
1895
|
+
if (isContainer && m.containers === undefined) {
|
|
1896
|
+
ctx.addIssue({
|
|
1897
|
+
code: z.ZodIssueCode.custom,
|
|
1898
|
+
path: ["containers"],
|
|
1899
|
+
message: "containers[] is required when deployment.type is 'container'"
|
|
1900
|
+
});
|
|
1829
1901
|
}
|
|
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();
|
|
1902
|
+
if (!isContainer && m.containers !== undefined) {
|
|
1903
|
+
ctx.addIssue({
|
|
1904
|
+
code: z.ZodIssueCode.custom,
|
|
1905
|
+
path: ["containers"],
|
|
1906
|
+
message: `containers[] is only allowed when deployment.type is 'container' (current type: '${m.deployment.type}')`
|
|
1881
1907
|
});
|
|
1882
|
-
|
|
1883
|
-
|
|
1908
|
+
}
|
|
1909
|
+
if (!isContainer && m.deployment.runtime !== undefined) {
|
|
1910
|
+
ctx.addIssue({
|
|
1911
|
+
code: z.ZodIssueCode.custom,
|
|
1912
|
+
path: ["deployment", "runtime"],
|
|
1913
|
+
message: "deployment.runtime is only meaningful when deployment.type is 'container'"
|
|
1884
1914
|
});
|
|
1885
|
-
|
|
1886
|
-
|
|
1915
|
+
}
|
|
1916
|
+
if (isContainer && m.build !== undefined) {
|
|
1917
|
+
ctx.addIssue({
|
|
1918
|
+
code: z.ZodIssueCode.custom,
|
|
1919
|
+
path: ["build"],
|
|
1920
|
+
message: "build is only allowed for pages-app deployments (static/react/react+api). Container builds live in the Dockerfile."
|
|
1887
1921
|
});
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1922
|
+
}
|
|
1923
|
+
if (isContainer && m.production_env !== undefined) {
|
|
1924
|
+
ctx.addIssue({
|
|
1925
|
+
code: z.ZodIssueCode.custom,
|
|
1926
|
+
path: ["production_env"],
|
|
1927
|
+
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
1928
|
});
|
|
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;
|
|
1929
|
+
}
|
|
1930
|
+
if (isContainer && m.containers !== undefined && m.containers.length > 1) {
|
|
1931
|
+
const ingressContainers = m.containers.filter((c) => c.ingress === true);
|
|
1932
|
+
if (ingressContainers.length !== 1) {
|
|
1933
|
+
ctx.addIssue({
|
|
1934
|
+
code: z.ZodIssueCode.custom,
|
|
1935
|
+
path: ["containers"],
|
|
1936
|
+
message: `multi-container apps must mark exactly one container ingress: true (found ${ingressContainers.length})`
|
|
1937
|
+
});
|
|
1933
1938
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
+
}
|
|
1940
|
+
if (isContainer && m.containers !== undefined) {
|
|
1941
|
+
const declaredBindings = new Set((m.secrets?.bindings ?? []).map((b) => b.name));
|
|
1942
|
+
for (let i = 0;i < m.containers.length; i++) {
|
|
1943
|
+
const c = m.containers[i];
|
|
1944
|
+
for (const envName of c.env ?? []) {
|
|
1945
|
+
if (!declaredBindings.has(envName)) {
|
|
1946
|
+
ctx.addIssue({
|
|
1947
|
+
code: z.ZodIssueCode.custom,
|
|
1948
|
+
path: ["containers", i, "env"],
|
|
1949
|
+
message: `env reference '${envName}' has no matching entry in secrets.bindings[]`
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1939
1953
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1954
|
+
}
|
|
1955
|
+
if (isContainer && m.containers !== undefined) {
|
|
1956
|
+
const seen = new Set;
|
|
1957
|
+
for (let i = 0;i < m.containers.length; i++) {
|
|
1958
|
+
const name = m.containers[i]?.name;
|
|
1959
|
+
if (name === undefined)
|
|
1960
|
+
continue;
|
|
1961
|
+
if (seen.has(name)) {
|
|
1962
|
+
ctx.addIssue({
|
|
1963
|
+
code: z.ZodIssueCode.custom,
|
|
1964
|
+
path: ["containers", i, "name"],
|
|
1965
|
+
message: `duplicate container name '${name}' — names become DO class identifiers and must be unique`
|
|
1966
|
+
});
|
|
1944
1967
|
}
|
|
1945
|
-
|
|
1946
|
-
i += 2;
|
|
1947
|
-
continue;
|
|
1968
|
+
seen.add(name);
|
|
1948
1969
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1970
|
+
}
|
|
1971
|
+
const bindings = m.secrets?.bindings ?? [];
|
|
1972
|
+
if (bindings.length > 0) {
|
|
1973
|
+
const seen = new Set;
|
|
1974
|
+
for (let i = 0;i < bindings.length; i++) {
|
|
1975
|
+
const name = bindings[i]?.name;
|
|
1976
|
+
if (name === undefined)
|
|
1977
|
+
continue;
|
|
1978
|
+
if (seen.has(name)) {
|
|
1979
|
+
ctx.addIssue({
|
|
1980
|
+
code: z.ZodIssueCode.custom,
|
|
1981
|
+
path: ["secrets", "bindings", i, "name"],
|
|
1982
|
+
message: `duplicate secret binding name '${name}' — names become env-var keys and must be unique`
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
seen.add(name);
|
|
1953
1986
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1987
|
+
}
|
|
1988
|
+
const targets = m.targets ?? [];
|
|
1989
|
+
for (let i = 0;i < targets.length; i++) {
|
|
1990
|
+
const t = targets[i];
|
|
1991
|
+
if (t.kind === "worker" && t.script === undefined) {
|
|
1992
|
+
ctx.addIssue({
|
|
1993
|
+
code: z.ZodIssueCode.custom,
|
|
1994
|
+
path: ["targets", i, "script"],
|
|
1995
|
+
message: "a worker target must declare a script (the sibling Workers script name)"
|
|
1996
|
+
});
|
|
1958
1997
|
}
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
modeFlagsSeen += 1;
|
|
1966
|
-
i += 2;
|
|
1967
|
-
continue;
|
|
1998
|
+
if (t.kind === "pages" && t.script !== undefined) {
|
|
1999
|
+
ctx.addIssue({
|
|
2000
|
+
code: z.ZodIssueCode.custom,
|
|
2001
|
+
path: ["targets", i, "script"],
|
|
2002
|
+
message: "a pages target must not declare a script — the primary Pages project is the app slug"
|
|
2003
|
+
});
|
|
1968
2004
|
}
|
|
1969
|
-
if (
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
modeFlagsSeen += 1;
|
|
1976
|
-
i += 2;
|
|
1977
|
-
continue;
|
|
2005
|
+
if (t.kind === "pages" && t.schedule !== undefined) {
|
|
2006
|
+
ctx.addIssue({
|
|
2007
|
+
code: z.ZodIssueCode.custom,
|
|
2008
|
+
path: ["targets", i, "schedule"],
|
|
2009
|
+
message: "schedule is only valid on a worker target (the cron deploy contract — ADR 0022)"
|
|
2010
|
+
});
|
|
1978
2011
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2012
|
+
}
|
|
2013
|
+
const seenScripts = new Set;
|
|
2014
|
+
let pagesTargets = 0;
|
|
2015
|
+
for (let i = 0;i < targets.length; i++) {
|
|
2016
|
+
const t = targets[i];
|
|
2017
|
+
if (t.kind === "pages") {
|
|
2018
|
+
pagesTargets++;
|
|
2019
|
+
if (pagesTargets > 1) {
|
|
2020
|
+
ctx.addIssue({
|
|
2021
|
+
code: z.ZodIssueCode.custom,
|
|
2022
|
+
path: ["targets", i, "kind"],
|
|
2023
|
+
message: "at most one pages target may be declared (the primary Pages project is singular)"
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
1985
2026
|
continue;
|
|
1986
2027
|
}
|
|
1987
|
-
if (
|
|
1988
|
-
dryRunJson = true;
|
|
1989
|
-
i += 1;
|
|
2028
|
+
if (t.script === undefined)
|
|
1990
2029
|
continue;
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
i += 2;
|
|
2030
|
+
if (t.script === PRIMARY_TARGET || t.script === "pages") {
|
|
2031
|
+
ctx.addIssue({
|
|
2032
|
+
code: z.ZodIssueCode.custom,
|
|
2033
|
+
path: ["targets", i, "script"],
|
|
2034
|
+
message: `worker target script '${t.script}' is a reserved selector token — use a different script name`
|
|
2035
|
+
});
|
|
1998
2036
|
continue;
|
|
1999
2037
|
}
|
|
2000
|
-
if (
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
continue;
|
|
2038
|
+
if (seenScripts.has(t.script)) {
|
|
2039
|
+
ctx.addIssue({
|
|
2040
|
+
code: z.ZodIssueCode.custom,
|
|
2041
|
+
path: ["targets", i, "script"],
|
|
2042
|
+
message: `duplicate worker target script '${t.script}' — target scripts must be unique`
|
|
2043
|
+
});
|
|
2007
2044
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2045
|
+
seenScripts.add(t.script);
|
|
2046
|
+
}
|
|
2047
|
+
const validRefs = new Set([PRIMARY_TARGET, ...seenScripts]);
|
|
2048
|
+
if (pagesTargets > 0)
|
|
2049
|
+
validRefs.add("pages");
|
|
2050
|
+
for (let bi = 0;bi < bindings.length; bi++) {
|
|
2051
|
+
const b = bindings[bi];
|
|
2052
|
+
const sel = b?.targets;
|
|
2053
|
+
if (sel === undefined || sel === "all")
|
|
2014
2054
|
continue;
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2055
|
+
for (const ref of sel) {
|
|
2056
|
+
if (!validRefs.has(ref)) {
|
|
2057
|
+
ctx.addIssue({
|
|
2058
|
+
code: z.ZodIssueCode.custom,
|
|
2059
|
+
path: ["secrets", "bindings", bi, "targets"],
|
|
2060
|
+
message: `secret binding '${b?.name}' references unknown target '${ref}' — declare it in targets[] or use "primary"`
|
|
2061
|
+
});
|
|
2022
2062
|
}
|
|
2023
|
-
appType = v;
|
|
2024
|
-
i += 2;
|
|
2025
|
-
continue;
|
|
2026
2063
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
function parseManifest(input) {
|
|
2067
|
+
const result = ManifestSchema.safeParse(input);
|
|
2068
|
+
if (result.success) {
|
|
2069
|
+
return { kind: "ok", manifest: result.data };
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
kind: "schema-error",
|
|
2073
|
+
issues: result.error.issues.map((issue) => ({
|
|
2074
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
2075
|
+
message: issue.message
|
|
2076
|
+
}))
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
// ../launchpad-engine/dist/app-boundary.js
|
|
2080
|
+
var INCLUDE_EVERYTHING = ["**"];
|
|
2081
|
+
var COMMON_INCLUDE = [
|
|
2082
|
+
"launchpad.yaml",
|
|
2083
|
+
".gitignore",
|
|
2084
|
+
".launchpadignore",
|
|
2085
|
+
"package.json",
|
|
2086
|
+
"package-lock.json",
|
|
2087
|
+
"bun.lock",
|
|
2088
|
+
"bun.lockb",
|
|
2089
|
+
"yarn.lock",
|
|
2090
|
+
"pnpm-lock.yaml",
|
|
2091
|
+
"tsconfig.json",
|
|
2092
|
+
"tsconfig.*.json",
|
|
2093
|
+
"README.md",
|
|
2094
|
+
"LICENSE",
|
|
2095
|
+
"index.html",
|
|
2096
|
+
"vite.config.*",
|
|
2097
|
+
"src/**",
|
|
2098
|
+
"public/**",
|
|
2099
|
+
"worker/**"
|
|
2100
|
+
];
|
|
2101
|
+
function defaultIncludeForAppType(appType) {
|
|
2102
|
+
switch (appType) {
|
|
2103
|
+
case "static":
|
|
2104
|
+
return [...COMMON_INCLUDE, "static/**", "assets/**", "css/**", "js/**", "images/**", "*.html", "*.css", "*.js"];
|
|
2105
|
+
case "react":
|
|
2106
|
+
return COMMON_INCLUDE;
|
|
2107
|
+
case "react+api":
|
|
2108
|
+
return [...COMMON_INCLUDE, "functions/**"];
|
|
2109
|
+
case "container":
|
|
2110
|
+
return [...COMMON_INCLUDE, "container/**", "Dockerfile", "Dockerfile.*", "wrangler.toml"];
|
|
2111
|
+
default:
|
|
2112
|
+
return INCLUDE_EVERYTHING;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function resolveAppBoundary(input) {
|
|
2116
|
+
if (input.app === undefined) {
|
|
2117
|
+
return { root: ".", include: INCLUDE_EVERYTHING, exclude: [], source: "inferred" };
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
root: normaliseRoot(input.app.root ?? "."),
|
|
2121
|
+
include: input.app.include ?? defaultIncludeForAppType(input.appType),
|
|
2122
|
+
exclude: input.app.exclude ?? [],
|
|
2123
|
+
source: "declared"
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function normaliseRoot(root) {
|
|
2127
|
+
let r = root.replace(/\\/g, "/").trim();
|
|
2128
|
+
if (r.startsWith("./"))
|
|
2129
|
+
r = r.slice(2);
|
|
2130
|
+
while (r.endsWith("/"))
|
|
2131
|
+
r = r.slice(0, -1);
|
|
2132
|
+
return r === "" ? "." : r;
|
|
2133
|
+
}
|
|
2134
|
+
var APP_TYPE_SET = new Set(["static", "react", "react+api", "container"]);
|
|
2135
|
+
function extractBoundaryInput(parsedManifest) {
|
|
2136
|
+
const m = asRecord(parsedManifest);
|
|
2137
|
+
if (m === undefined) {
|
|
2138
|
+
return { kind: "ok", appType: undefined, app: undefined, build: undefined };
|
|
2139
|
+
}
|
|
2140
|
+
const spec = asRecord(m["spec"]);
|
|
2141
|
+
const v2 = spec !== undefined;
|
|
2142
|
+
const rawType = v2 ? spec["appType"] : asRecord(m["deployment"])?.["type"];
|
|
2143
|
+
const appType = typeof rawType === "string" && APP_TYPE_SET.has(rawType) ? rawType : undefined;
|
|
2144
|
+
const rawApp = v2 ? spec["app"] : m["app"];
|
|
2145
|
+
let app;
|
|
2146
|
+
if (rawApp !== undefined) {
|
|
2147
|
+
const parsed = AppBoundarySchema.safeParse(rawApp);
|
|
2148
|
+
if (!parsed.success) {
|
|
2149
|
+
return {
|
|
2150
|
+
kind: "schema-error",
|
|
2151
|
+
issues: parsed.error.issues.map((i) => ({
|
|
2152
|
+
path: `${v2 ? "spec." : ""}app${i.path.length > 0 ? "." + i.path.map(String).join(".") : ""}`,
|
|
2153
|
+
message: i.message
|
|
2154
|
+
}))
|
|
2155
|
+
};
|
|
2037
2156
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
return `invalid --timeout-seconds "${v}" — expected positive integer`;
|
|
2053
|
-
}
|
|
2054
|
-
timeoutSeconds = n;
|
|
2055
|
-
i += 2;
|
|
2056
|
-
continue;
|
|
2057
|
-
}
|
|
2058
|
-
if (a === "--timeout-minutes") {
|
|
2059
|
-
const v = args[i + 1];
|
|
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;
|
|
2157
|
+
app = parsed.data;
|
|
2158
|
+
}
|
|
2159
|
+
const rawBuild = asRecord(v2 ? spec["build"] : m["build"]);
|
|
2160
|
+
const build = rawBuild !== undefined && typeof rawBuild["command"] === "string" && typeof rawBuild["root_dir"] === "string" ? { command: rawBuild["command"], root_dir: rawBuild["root_dir"] } : undefined;
|
|
2161
|
+
return { kind: "ok", appType, app, build };
|
|
2162
|
+
}
|
|
2163
|
+
function asRecord(v) {
|
|
2164
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
2165
|
+
}
|
|
2166
|
+
function parseLaunchpadIgnore(content) {
|
|
2167
|
+
const out = [];
|
|
2168
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
2169
|
+
const line = raw.trim();
|
|
2170
|
+
if (line === "" || line.startsWith("#") || line.startsWith("!"))
|
|
2068
2171
|
continue;
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2172
|
+
out.push(line);
|
|
2173
|
+
}
|
|
2174
|
+
return out;
|
|
2175
|
+
}
|
|
2176
|
+
function globToRegExp2(glob) {
|
|
2177
|
+
let re = "";
|
|
2178
|
+
let i = 0;
|
|
2179
|
+
while (i < glob.length) {
|
|
2180
|
+
const ch = glob[i];
|
|
2181
|
+
if (ch === "*") {
|
|
2182
|
+
if (glob[i + 1] === "*") {
|
|
2183
|
+
if (glob[i + 2] === "/") {
|
|
2184
|
+
re += "(?:.*/)?";
|
|
2185
|
+
i += 3;
|
|
2186
|
+
} else {
|
|
2187
|
+
re += ".*";
|
|
2188
|
+
i += 2;
|
|
2189
|
+
}
|
|
2190
|
+
} else {
|
|
2191
|
+
re += "[^/]*";
|
|
2192
|
+
i += 1;
|
|
2077
2193
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2194
|
+
} else if (ch === "?") {
|
|
2195
|
+
re += "[^/]";
|
|
2196
|
+
i += 1;
|
|
2197
|
+
} else if (ch === "/" || /[A-Za-z0-9_-]/.test(ch)) {
|
|
2198
|
+
re += ch;
|
|
2199
|
+
i += 1;
|
|
2200
|
+
} else {
|
|
2201
|
+
re += `\\${ch}`;
|
|
2202
|
+
i += 1;
|
|
2081
2203
|
}
|
|
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
2204
|
}
|
|
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
|
-
};
|
|
2205
|
+
return new RegExp(`^${re}$`);
|
|
2206
|
+
}
|
|
2207
|
+
var GLOB_CHARS = /[*?]/;
|
|
2208
|
+
function matchContractPattern(pattern, relPath) {
|
|
2209
|
+
const p = pattern.replace(/\/+$/, "");
|
|
2210
|
+
if (!GLOB_CHARS.test(p)) {
|
|
2211
|
+
return relPath === p || relPath.startsWith(`${p}/`);
|
|
2144
2212
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2213
|
+
return globToRegExp2(p).test(relPath);
|
|
2214
|
+
}
|
|
2215
|
+
function matchIgnorePattern(pattern, relPath) {
|
|
2216
|
+
let p = pattern;
|
|
2217
|
+
const dirOnly = p.endsWith("/");
|
|
2218
|
+
if (dirOnly)
|
|
2219
|
+
p = p.replace(/\/+$/, "");
|
|
2220
|
+
const anchored = p.startsWith("/");
|
|
2221
|
+
if (anchored)
|
|
2222
|
+
p = p.slice(1);
|
|
2223
|
+
if (!anchored && !p.includes("/")) {
|
|
2224
|
+
const re2 = globToRegExp2(p);
|
|
2225
|
+
const segments = relPath.split("/");
|
|
2226
|
+
const candidates = dirOnly ? segments.slice(0, -1) : segments;
|
|
2227
|
+
return candidates.some((seg) => re2.test(seg));
|
|
2147
2228
|
}
|
|
2148
|
-
if (
|
|
2149
|
-
return
|
|
2229
|
+
if (!GLOB_CHARS.test(p)) {
|
|
2230
|
+
return !dirOnly && relPath === p || relPath.startsWith(`${p}/`);
|
|
2150
2231
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2232
|
+
const re = globToRegExp2(p);
|
|
2233
|
+
if (dirOnly)
|
|
2234
|
+
return re.test(relPath.split("/").slice(0, -1).join("/"));
|
|
2235
|
+
return re.test(relPath) || re.test(relPath.split("/").slice(0, -1).join("/"));
|
|
2236
|
+
}
|
|
2237
|
+
var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
|
|
2238
|
+
".github/workflows/launchpad-review.yml",
|
|
2239
|
+
".github/workflows/ci.yml",
|
|
2240
|
+
".github/scripts/_gha-cmd.mjs",
|
|
2241
|
+
".github/scripts/parse-eslint.mjs",
|
|
2242
|
+
".github/scripts/parse-tsc.mjs",
|
|
2243
|
+
".github/scripts/parse-vitest.mjs",
|
|
2244
|
+
".github/scripts/parse-gitleaks.mjs",
|
|
2245
|
+
".github/scripts/parse-bun-audit.mjs"
|
|
2246
|
+
]);
|
|
2247
|
+
function checkDenyList(path6) {
|
|
2248
|
+
const segments = path6.split("/");
|
|
2249
|
+
const basename = segments[segments.length - 1];
|
|
2250
|
+
const isExample = basename.endsWith(".example");
|
|
2251
|
+
if (basename.startsWith(".env") && !isExample) {
|
|
2252
|
+
return { path: path6, rule: "env-file", message: `'${path6}' is an environment file (.env*) — never shippable` };
|
|
2153
2253
|
}
|
|
2154
|
-
if (
|
|
2155
|
-
return "
|
|
2254
|
+
if (basename === ".npmrc") {
|
|
2255
|
+
return { path: path6, rule: "npmrc", message: `'${path6}' (.npmrc) may carry registry auth — never shippable` };
|
|
2156
2256
|
}
|
|
2157
|
-
if (
|
|
2158
|
-
return "
|
|
2257
|
+
if (basename.startsWith(".dev.vars") && !isExample) {
|
|
2258
|
+
return { path: path6, rule: "dev-vars", message: `'${path6}' is a wrangler dev-vars file — never shippable` };
|
|
2159
2259
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
return "
|
|
2260
|
+
if (segments.includes(".claude")) {
|
|
2261
|
+
return { path: path6, rule: "claude-dir", message: `'${path6}' is under a .claude/ directory (AI-workspace state) — never shippable` };
|
|
2162
2262
|
}
|
|
2163
|
-
if (
|
|
2164
|
-
return "
|
|
2263
|
+
if (segments.includes(".git")) {
|
|
2264
|
+
return { path: path6, rule: "git-dir", message: `'${path6}' is under .git/ — never shippable` };
|
|
2165
2265
|
}
|
|
2166
|
-
if (
|
|
2167
|
-
if (
|
|
2168
|
-
return
|
|
2169
|
-
}
|
|
2266
|
+
if (segments[0] === ".github") {
|
|
2267
|
+
if (PLATFORM_SEEDED_GITHUB_PATHS.has(path6))
|
|
2268
|
+
return "seeded";
|
|
2170
2269
|
return {
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2270
|
+
path: path6,
|
|
2271
|
+
rule: "github-dir",
|
|
2272
|
+
message: `'${path6}' — repo-root .github/ is platform-owned (workflows execute under the M-KOPA Actions identity); developer-supplied .github/** is not shippable`
|
|
2174
2273
|
};
|
|
2175
2274
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
2277
|
+
function checkSecretShape(path6, readFileContent) {
|
|
2278
|
+
const segments = path6.split("/");
|
|
2279
|
+
const basename = segments[segments.length - 1];
|
|
2280
|
+
const deny = (what) => ({
|
|
2281
|
+
path: path6,
|
|
2282
|
+
rule: "secret-shape",
|
|
2283
|
+
message: `'${path6}' looks like ${what} — never shippable; if this is a false positive, rename the file`
|
|
2284
|
+
});
|
|
2285
|
+
if (basename.endsWith(".pem"))
|
|
2286
|
+
return deny("PEM key material (*.pem)");
|
|
2287
|
+
if (basename.endsWith(".key"))
|
|
2288
|
+
return deny("private key material (*.key)");
|
|
2289
|
+
if (basename.startsWith("id_rsa"))
|
|
2290
|
+
return deny("an SSH key (id_rsa*)");
|
|
2291
|
+
if (basename.endsWith(".p12") || basename.endsWith(".pfx")) {
|
|
2292
|
+
return deny("a PKCS#12 key store (*.p12 / *.pfx)");
|
|
2293
|
+
}
|
|
2294
|
+
if (basename === "credentials.json")
|
|
2295
|
+
return deny("a credentials file (credentials.json)");
|
|
2296
|
+
if (segments.includes(".aws"))
|
|
2297
|
+
return deny("AWS credentials (.aws/**)");
|
|
2298
|
+
if (basename.endsWith(".json") && readFileContent !== undefined) {
|
|
2299
|
+
const content = readFileContent(path6);
|
|
2300
|
+
if (content !== undefined && /"private_key"\s*:/.test(content)) {
|
|
2301
|
+
return deny('service-account-shaped JSON (carries a "private_key" field)');
|
|
2179
2302
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2303
|
+
}
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
var ALWAYS_SHIP = "launchpad.yaml";
|
|
2307
|
+
function applyAppBoundary(contract, files, options = {}) {
|
|
2308
|
+
const ignorePatterns = options.launchpadIgnore ?? [];
|
|
2309
|
+
const included = [];
|
|
2310
|
+
const excluded = [];
|
|
2311
|
+
const denied = [];
|
|
2312
|
+
const rootPrefix = contract.root === "." ? "" : `${contract.root}/`;
|
|
2313
|
+
for (const path6 of [...files].sort()) {
|
|
2314
|
+
if (path6 !== ALWAYS_SHIP) {
|
|
2315
|
+
if (rootPrefix !== "" && !path6.startsWith(rootPrefix)) {
|
|
2316
|
+
excluded.push({ path: path6, reason: "outside-root" });
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const rel = rootPrefix === "" ? path6 : path6.slice(rootPrefix.length);
|
|
2320
|
+
if (!contract.include.some((p) => matchContractPattern(p, rel))) {
|
|
2321
|
+
excluded.push({ path: path6, reason: "not-included" });
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
if (contract.exclude.some((p) => matchContractPattern(p, rel))) {
|
|
2325
|
+
excluded.push({ path: path6, reason: "exclude" });
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (ignorePatterns.some((p) => matchIgnorePattern(p, path6))) {
|
|
2329
|
+
excluded.push({ path: path6, reason: "launchpadignore" });
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2182
2332
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2333
|
+
const denial = checkDenyList(path6);
|
|
2334
|
+
if (denial === "seeded") {
|
|
2335
|
+
excluded.push({ path: path6, reason: "platform-seeded" });
|
|
2336
|
+
continue;
|
|
2185
2337
|
}
|
|
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";
|
|
2338
|
+
if (denial !== null) {
|
|
2339
|
+
denied.push(denial);
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
const secret = checkSecretShape(path6, options.readFileContent);
|
|
2343
|
+
if (secret !== null) {
|
|
2344
|
+
denied.push(secret);
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
included.push(path6);
|
|
2207
2348
|
}
|
|
2208
|
-
return {
|
|
2209
|
-
mode: {
|
|
2210
|
-
kind: "new",
|
|
2211
|
-
slug,
|
|
2212
|
-
displayName,
|
|
2213
|
-
description,
|
|
2214
|
-
appType,
|
|
2215
|
-
allowedGroups
|
|
2216
|
-
},
|
|
2217
|
-
message,
|
|
2218
|
-
timeoutSeconds
|
|
2219
|
-
};
|
|
2349
|
+
return { files: included, excluded, denied };
|
|
2220
2350
|
}
|
|
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"]
|
|
2351
|
+
var PLAIN_PATH = /^(\.\/)?[A-Za-z0-9_@][A-Za-z0-9_@.\/-]*$/;
|
|
2352
|
+
var OPAQUE_SEGMENT = /[$`(){}<]/;
|
|
2353
|
+
function verifyBuildInputs(build, files) {
|
|
2354
|
+
if (build === undefined)
|
|
2355
|
+
return [];
|
|
2356
|
+
const issues = [];
|
|
2357
|
+
const fileSet = new Set(files);
|
|
2358
|
+
const dirSet = new Set;
|
|
2359
|
+
for (const f of files) {
|
|
2360
|
+
const parts = f.split("/");
|
|
2361
|
+
for (let i = 1;i < parts.length; i++) {
|
|
2362
|
+
dirSet.add(parts.slice(0, i).join("/"));
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
const exists = (p) => fileSet.has(p) || dirSet.has(p);
|
|
2366
|
+
const rootDir = normaliseRoot(build.root_dir === "" ? "." : build.root_dir);
|
|
2367
|
+
if (rootDir !== "." && !dirSet.has(rootDir)) {
|
|
2368
|
+
issues.push({
|
|
2369
|
+
input: rootDir,
|
|
2370
|
+
message: `build.root_dir '${rootDir}' does not exist in the bundle — the build would run in a missing directory`
|
|
2264
2371
|
});
|
|
2372
|
+
return issues;
|
|
2265
2373
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
healthcheck: z.string().default("/health"),
|
|
2286
|
-
env: z.array(z.string().regex(ENV_NAME_REGEX, "env entries must be UPPER_SNAKE_CASE")).optional(),
|
|
2287
|
-
max_instances: z.number().int().min(1).max(100).optional()
|
|
2288
|
-
}).strict();
|
|
2289
|
-
var TargetSchema = z.object({
|
|
2290
|
-
kind: z.enum(TARGET_KINDS),
|
|
2291
|
-
script: z.string().min(2).max(63).regex(SLUG_REGEX, "target script must be lowercase letters, digits, and hyphens").optional(),
|
|
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)
|
|
2374
|
+
let cwd = rootDir === "." ? "" : rootDir;
|
|
2375
|
+
const resolve5 = (p) => {
|
|
2376
|
+
const cleaned = p.startsWith("./") ? p.slice(2) : p;
|
|
2377
|
+
const trimmed = cleaned.replace(/\/+$/, "");
|
|
2378
|
+
return cwd === "" ? trimmed : `${cwd}/${trimmed}`;
|
|
2379
|
+
};
|
|
2380
|
+
const segments = build.command.split(/&&|\|\||[;|\n]/);
|
|
2381
|
+
for (const segment of segments) {
|
|
2382
|
+
if (cwd === null)
|
|
2383
|
+
break;
|
|
2384
|
+
const s = segment.trim();
|
|
2385
|
+
if (s === "" || OPAQUE_SEGMENT.test(s))
|
|
2386
|
+
continue;
|
|
2387
|
+
const tokens = s.split(/\s+/);
|
|
2388
|
+
const cmd = tokens[0];
|
|
2389
|
+
if (cmd === "cd") {
|
|
2390
|
+
const target = tokens[1];
|
|
2391
|
+
if (target === undefined || !PLAIN_PATH.test(target)) {
|
|
2392
|
+
cwd = null;
|
|
2414
2393
|
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
2394
|
}
|
|
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)"
|
|
2395
|
+
const resolved = resolve5(target);
|
|
2396
|
+
if (!dirSet.has(resolved)) {
|
|
2397
|
+
issues.push({
|
|
2398
|
+
input: target,
|
|
2399
|
+
message: `build command segment '${s}' changes into '${resolved}', which does not exist in the bundle`
|
|
2461
2400
|
});
|
|
2401
|
+
cwd = null;
|
|
2402
|
+
continue;
|
|
2462
2403
|
}
|
|
2404
|
+
cwd = resolved;
|
|
2463
2405
|
continue;
|
|
2464
2406
|
}
|
|
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
|
-
});
|
|
2407
|
+
if (cmd === "cp" || cmd === "cat") {
|
|
2408
|
+
const rawArgs = [];
|
|
2409
|
+
for (const t of tokens.slice(1)) {
|
|
2410
|
+
if (t === ">" || t.startsWith(">"))
|
|
2411
|
+
break;
|
|
2412
|
+
if (t.startsWith("-"))
|
|
2413
|
+
continue;
|
|
2414
|
+
rawArgs.push(t);
|
|
2415
|
+
}
|
|
2416
|
+
const sources = cmd === "cp" ? rawArgs.slice(0, -1) : rawArgs;
|
|
2417
|
+
if (cmd === "cp" && rawArgs.length < 2)
|
|
2418
|
+
continue;
|
|
2419
|
+
for (const src of sources) {
|
|
2420
|
+
if (!PLAIN_PATH.test(src))
|
|
2421
|
+
continue;
|
|
2422
|
+
const resolved = resolve5(src);
|
|
2423
|
+
if (!exists(resolved)) {
|
|
2424
|
+
issues.push({
|
|
2425
|
+
input: src,
|
|
2426
|
+
message: `build command references '${src}' (resolved: '${resolved}'), which does not exist in the bundle — did the content move? (cf. sp-reloc1)`
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2499
2429
|
}
|
|
2500
2430
|
}
|
|
2501
2431
|
}
|
|
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
|
-
};
|
|
2432
|
+
return issues;
|
|
2515
2433
|
}
|
|
2516
2434
|
// ../launchpad-engine/dist/status-author.js
|
|
2517
2435
|
var STATUS_STATUS_KEYS = new Set([
|
|
@@ -2557,6 +2475,7 @@ var SpecSchema = z2.object({
|
|
|
2557
2475
|
}).strict(),
|
|
2558
2476
|
auth: z2.enum(AUTH_MODES).optional(),
|
|
2559
2477
|
build: BuildSchema.optional(),
|
|
2478
|
+
app: AppBoundarySchema.optional(),
|
|
2560
2479
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
2561
2480
|
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
2562
2481
|
secrets: SecretsSchema.optional(),
|
|
@@ -3151,101 +3070,650 @@ var FleetManifestSchema = z3.object({
|
|
|
3151
3070
|
});
|
|
3152
3071
|
}
|
|
3153
3072
|
}
|
|
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
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
seenSets.add(set.name);
|
|
3185
|
-
const seenMembers = new Set;
|
|
3186
|
-
for (const member of set.members) {
|
|
3187
|
-
if (seenMembers.has(member)) {
|
|
3188
|
-
ctx.addIssue({
|
|
3189
|
-
code: "custom",
|
|
3190
|
-
message: `duplicate member "${member}" in set "${set.name}"`,
|
|
3191
|
-
path: ["secretSets", i, "members"]
|
|
3192
|
-
});
|
|
3193
|
-
}
|
|
3194
|
-
seenMembers.add(member);
|
|
3073
|
+
});
|
|
3074
|
+
// ../launchpad-engine/dist/fleet-secret-sets.js
|
|
3075
|
+
import { z as z4 } from "zod";
|
|
3076
|
+
var SET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3077
|
+
var Slug = z4.string().regex(SLUG_REGEX, "member must be a lowercase-kebab app slug");
|
|
3078
|
+
var SecretName = z4.string().regex(ENV_NAME_REGEX, "secret name must be UPPER_SNAKE_CASE");
|
|
3079
|
+
var SecretSetExcludeSchema = z4.object({
|
|
3080
|
+
member: Slug,
|
|
3081
|
+
secret: SecretName
|
|
3082
|
+
}).strict();
|
|
3083
|
+
var SecretSetSchema = z4.object({
|
|
3084
|
+
name: z4.string().min(1).max(64).regex(SET_NAME_REGEX, "set name must be lowercase-kebab"),
|
|
3085
|
+
secrets: z4.array(SecretName).min(1),
|
|
3086
|
+
members: z4.array(Slug).min(1),
|
|
3087
|
+
exclude: z4.array(SecretSetExcludeSchema).optional()
|
|
3088
|
+
}).strict();
|
|
3089
|
+
var FleetSecretSetsSchema = z4.object({
|
|
3090
|
+
schemaVersion: z4.literal(1),
|
|
3091
|
+
secretSets: z4.array(SecretSetSchema).min(1)
|
|
3092
|
+
}).strict().superRefine((m, ctx) => {
|
|
3093
|
+
const seenSets = new Set;
|
|
3094
|
+
for (let i = 0;i < m.secretSets.length; i++) {
|
|
3095
|
+
const set = m.secretSets[i];
|
|
3096
|
+
if (seenSets.has(set.name)) {
|
|
3097
|
+
ctx.addIssue({
|
|
3098
|
+
code: "custom",
|
|
3099
|
+
message: `duplicate secretSet name "${set.name}" — each set name appears once`,
|
|
3100
|
+
path: ["secretSets", i, "name"]
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
seenSets.add(set.name);
|
|
3104
|
+
const seenMembers = new Set;
|
|
3105
|
+
for (const member of set.members) {
|
|
3106
|
+
if (seenMembers.has(member)) {
|
|
3107
|
+
ctx.addIssue({
|
|
3108
|
+
code: "custom",
|
|
3109
|
+
message: `duplicate member "${member}" in set "${set.name}"`,
|
|
3110
|
+
path: ["secretSets", i, "members"]
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
seenMembers.add(member);
|
|
3114
|
+
}
|
|
3115
|
+
const seenSecrets = new Set;
|
|
3116
|
+
for (const secret of set.secrets) {
|
|
3117
|
+
if (seenSecrets.has(secret)) {
|
|
3118
|
+
ctx.addIssue({
|
|
3119
|
+
code: "custom",
|
|
3120
|
+
message: `duplicate secret "${secret}" in set "${set.name}"`,
|
|
3121
|
+
path: ["secretSets", i, "secrets"]
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
seenSecrets.add(secret);
|
|
3125
|
+
}
|
|
3126
|
+
for (let j = 0;j < (set.exclude ?? []).length; j++) {
|
|
3127
|
+
const ex = set.exclude[j];
|
|
3128
|
+
if (!seenMembers.has(ex.member)) {
|
|
3129
|
+
ctx.addIssue({
|
|
3130
|
+
code: "custom",
|
|
3131
|
+
message: `exclude references member "${ex.member}" which is not in set "${set.name}".members`,
|
|
3132
|
+
path: ["secretSets", i, "exclude", j, "member"]
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
if (!seenSecrets.has(ex.secret)) {
|
|
3136
|
+
ctx.addIssue({
|
|
3137
|
+
code: "custom",
|
|
3138
|
+
message: `exclude references secret "${ex.secret}" which is not in set "${set.name}".secrets`,
|
|
3139
|
+
path: ["secretSets", i, "exclude", j, "secret"]
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
function parseFleetSecretSets(input) {
|
|
3146
|
+
const res = FleetSecretSetsSchema.safeParse(input);
|
|
3147
|
+
if (res.success)
|
|
3148
|
+
return { ok: true, manifest: res.data };
|
|
3149
|
+
return {
|
|
3150
|
+
ok: false,
|
|
3151
|
+
issues: res.error.issues.map((i) => ({
|
|
3152
|
+
path: i.path.join("."),
|
|
3153
|
+
message: i.message
|
|
3154
|
+
}))
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
function membersForSecret(set, secret) {
|
|
3158
|
+
const excluded = new Set((set.exclude ?? []).filter((e) => e.secret === secret).map((e) => e.member));
|
|
3159
|
+
return set.members.filter((m) => !excluded.has(m));
|
|
3160
|
+
}
|
|
3161
|
+
// ../launchpad-engine/dist/redact-status.js
|
|
3162
|
+
var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
|
|
3163
|
+
// src/bundle/boundary.ts
|
|
3164
|
+
var MAX_CONTENT_PROBE_BYTES = 1024 * 1024;
|
|
3165
|
+
function applyBoundaryToFiles(args) {
|
|
3166
|
+
const { cwd, manifestYaml, files } = args;
|
|
3167
|
+
let parsed;
|
|
3168
|
+
if (manifestYaml !== null) {
|
|
3169
|
+
try {
|
|
3170
|
+
parsed = parseYaml2(manifestYaml);
|
|
3171
|
+
} catch {
|
|
3172
|
+
parsed = undefined;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
const input = extractBoundaryInput(parsed);
|
|
3176
|
+
if (input.kind === "schema-error") {
|
|
3177
|
+
const detail = input.issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
3178
|
+
return { kind: "error", message: `launchpad.yaml app: block is invalid — ${detail}` };
|
|
3179
|
+
}
|
|
3180
|
+
const contract = resolveAppBoundary({ appType: input.appType, app: input.app });
|
|
3181
|
+
let launchpadIgnore = [];
|
|
3182
|
+
const ignorePath = join5(cwd, ".launchpadignore");
|
|
3183
|
+
if (existsSync2(ignorePath)) {
|
|
3184
|
+
try {
|
|
3185
|
+
launchpadIgnore = parseLaunchpadIgnore(readFileSync3(ignorePath, "utf8"));
|
|
3186
|
+
} catch {}
|
|
3187
|
+
}
|
|
3188
|
+
const result = applyAppBoundary(contract, files, {
|
|
3189
|
+
launchpadIgnore,
|
|
3190
|
+
readFileContent: (path6) => {
|
|
3191
|
+
try {
|
|
3192
|
+
const abs = join5(cwd, path6);
|
|
3193
|
+
const stat = lstatSync2(abs);
|
|
3194
|
+
if (!stat.isFile() || stat.size > MAX_CONTENT_PROBE_BYTES)
|
|
3195
|
+
return;
|
|
3196
|
+
return readFileSync3(abs, "utf8");
|
|
3197
|
+
} catch {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
const warnings = [];
|
|
3203
|
+
if (result.denied.length > 0) {
|
|
3204
|
+
if (contract.source === "declared") {
|
|
3205
|
+
const lines = result.denied.map((d) => ` - ${d.message}`).join(`
|
|
3206
|
+
`);
|
|
3207
|
+
return {
|
|
3208
|
+
kind: "error",
|
|
3209
|
+
message: `bundle blocked — ${result.denied.length} file(s) matched by app.include can never ship:
|
|
3210
|
+
${lines}
|
|
3211
|
+
` + ` Remove the file(s) or narrow app.include / add app.exclude in launchpad.yaml.`
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
for (const d of result.denied) {
|
|
3215
|
+
warnings.push(`stripped ${d.path} — ${d.message}`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (result.files.length === 0) {
|
|
3219
|
+
return {
|
|
3220
|
+
kind: "error",
|
|
3221
|
+
message: "the app boundary excluded every file — nothing to deploy. " + "Check app.root / app.include in launchpad.yaml."
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
const buildIssues = verifyBuildInputs(input.build, result.files);
|
|
3225
|
+
if (buildIssues.length > 0) {
|
|
3226
|
+
const lines = buildIssues.map((i) => ` - ${i.message}`).join(`
|
|
3227
|
+
`);
|
|
3228
|
+
return {
|
|
3229
|
+
kind: "error",
|
|
3230
|
+
message: `build-inputs check failed — the build command references missing inputs:
|
|
3231
|
+
${lines}`
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
return {
|
|
3235
|
+
kind: "ok",
|
|
3236
|
+
files: result.files,
|
|
3237
|
+
warnings,
|
|
3238
|
+
inferred: contract.source === "inferred"
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/bundle/orchestrate.ts
|
|
3243
|
+
async function bundleAndDeploy(args) {
|
|
3244
|
+
const { cfg, cwd, slug } = args;
|
|
3245
|
+
const manifestPath = join6(cwd, "launchpad.yaml");
|
|
3246
|
+
let manifestYaml;
|
|
3247
|
+
try {
|
|
3248
|
+
manifestYaml = readFileSync4(manifestPath, "utf8");
|
|
3249
|
+
} catch (e) {
|
|
3250
|
+
return {
|
|
3251
|
+
kind: "no-manifest",
|
|
3252
|
+
message: `no launchpad.yaml at ${manifestPath} — run \`launchpad init\` from inside your app directory first. ` + `(${e.message})`
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
let walk;
|
|
3256
|
+
try {
|
|
3257
|
+
walk = walkCwd(cwd);
|
|
3258
|
+
} catch (e) {
|
|
3259
|
+
if (e instanceof WalkError) {
|
|
3260
|
+
return { kind: "walk-error", message: e.message };
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
kind: "walk-error",
|
|
3264
|
+
message: `unexpected walk failure: ${e.message}`
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
const boundary = applyBoundaryToFiles({ cwd, manifestYaml, files: walk.files });
|
|
3268
|
+
if (boundary.kind === "error") {
|
|
3269
|
+
return { kind: "boundary-error", message: boundary.message };
|
|
3270
|
+
}
|
|
3271
|
+
const bundleFiles = boundary.files;
|
|
3272
|
+
let packResult;
|
|
3273
|
+
try {
|
|
3274
|
+
packResult = await packTarGz(cwd, bundleFiles);
|
|
3275
|
+
} catch (e) {
|
|
3276
|
+
if (e instanceof TarPackError) {
|
|
3277
|
+
return { kind: "pack-error", message: e.message };
|
|
3278
|
+
}
|
|
3279
|
+
return {
|
|
3280
|
+
kind: "pack-error",
|
|
3281
|
+
message: `unexpected pack failure: ${e.message}`
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
const workerBuild = await buildWorkerArtifact(cwd, manifestYaml, bundleFiles);
|
|
3285
|
+
if (workerBuild.kind === "error") {
|
|
3286
|
+
return { kind: "worker-build-error", message: workerBuild.message };
|
|
3287
|
+
}
|
|
3288
|
+
const workerArtifact = workerBuild.kind === "ok" ? workerBuild.artifact : undefined;
|
|
3289
|
+
const uploadResult = await uploadBundle(cfg, slug, manifestYaml, packResult.bytes, workerArtifact);
|
|
3290
|
+
if (uploadResult.kind !== "ok") {
|
|
3291
|
+
return {
|
|
3292
|
+
kind: "upload-error",
|
|
3293
|
+
status: uploadResult.status,
|
|
3294
|
+
body: uploadResult.response
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
return {
|
|
3298
|
+
kind: "ok",
|
|
3299
|
+
result: uploadResult.response,
|
|
3300
|
+
walk,
|
|
3301
|
+
fileCount: packResult.fileCount,
|
|
3302
|
+
compressedBytes: packResult.bytes.byteLength,
|
|
3303
|
+
workerScript: workerArtifact?.script ?? null,
|
|
3304
|
+
boundaryWarnings: boundary.warnings
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// src/commands/deploy.ts
|
|
3309
|
+
import { parse as parseYaml4 } from "yaml";
|
|
3310
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3311
|
+
|
|
3312
|
+
// src/deploy/git-files.ts
|
|
3313
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3314
|
+
|
|
3315
|
+
class GitFilesError extends Error {
|
|
3316
|
+
code = "git_files_error";
|
|
3317
|
+
}
|
|
3318
|
+
var PROTECTED_PATHS = new Set([
|
|
3319
|
+
".github/workflows/launchpad-review.yml",
|
|
3320
|
+
".github/workflows/ci.yml",
|
|
3321
|
+
"wrangler.toml",
|
|
3322
|
+
".github/scripts/_gha-cmd.mjs",
|
|
3323
|
+
".github/scripts/parse-eslint.mjs",
|
|
3324
|
+
".github/scripts/parse-tsc.mjs",
|
|
3325
|
+
".github/scripts/parse-vitest.mjs",
|
|
3326
|
+
".github/scripts/parse-gitleaks.mjs",
|
|
3327
|
+
".github/scripts/parse-bun-audit.mjs"
|
|
3328
|
+
]);
|
|
3329
|
+
async function listDeployFiles(opts) {
|
|
3330
|
+
const sp = opts.spawner ?? spawn3;
|
|
3331
|
+
const stdout = await runGit2(sp, opts.cwd, [
|
|
3332
|
+
"ls-files",
|
|
3333
|
+
"-c",
|
|
3334
|
+
"-o",
|
|
3335
|
+
"--exclude-standard",
|
|
3336
|
+
"-z"
|
|
3337
|
+
]);
|
|
3338
|
+
const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
|
|
3339
|
+
return [...paths].sort();
|
|
3340
|
+
}
|
|
3341
|
+
function runGit2(sp, cwd, args) {
|
|
3342
|
+
return new Promise((resolve5, reject) => {
|
|
3343
|
+
const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
|
|
3344
|
+
const child = sp("git", [...args], spawnOpts);
|
|
3345
|
+
let stdout = "";
|
|
3346
|
+
let stderr = "";
|
|
3347
|
+
child.stdout?.on("data", (chunk) => {
|
|
3348
|
+
stdout += chunk.toString();
|
|
3349
|
+
});
|
|
3350
|
+
child.stderr?.on("data", (chunk) => {
|
|
3351
|
+
stderr += chunk.toString();
|
|
3352
|
+
});
|
|
3353
|
+
child.once("error", (err) => {
|
|
3354
|
+
reject(new GitFilesError(`could not run \`git ${args.join(" ")}\`: ${err.message} (is git installed?)`));
|
|
3355
|
+
});
|
|
3356
|
+
child.once("close", (code) => {
|
|
3357
|
+
if (code === 0) {
|
|
3358
|
+
resolve5(stdout);
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
|
|
3362
|
+
});
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
// src/commands/deploy-flags.ts
|
|
3367
|
+
var SLUG_RE3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3368
|
+
var GROUP_KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3369
|
+
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;
|
|
3370
|
+
function parseDeployFlags(args) {
|
|
3371
|
+
let mode = "content";
|
|
3372
|
+
let slug = null;
|
|
3373
|
+
let displayName = null;
|
|
3374
|
+
let description = null;
|
|
3375
|
+
let appType = null;
|
|
3376
|
+
const allowedGroups = [];
|
|
3377
|
+
let message = null;
|
|
3378
|
+
let timeoutSeconds = null;
|
|
3379
|
+
let manifestFile = null;
|
|
3380
|
+
let dryRunJson = false;
|
|
3381
|
+
let platformRepo = null;
|
|
3382
|
+
let rePin = false;
|
|
3383
|
+
let yes = false;
|
|
3384
|
+
let resumePrNumber = null;
|
|
3385
|
+
let timeoutMinutes = null;
|
|
3386
|
+
let atSha = null;
|
|
3387
|
+
let modeFlagsSeen = 0;
|
|
3388
|
+
let i = 0;
|
|
3389
|
+
while (i < args.length) {
|
|
3390
|
+
const a = args[i] ?? "";
|
|
3391
|
+
if (a === "--new") {
|
|
3392
|
+
mode = "new";
|
|
3393
|
+
modeFlagsSeen += 1;
|
|
3394
|
+
i += 1;
|
|
3395
|
+
continue;
|
|
3396
|
+
}
|
|
3397
|
+
if (a === "--dry-run") {
|
|
3398
|
+
mode = "dry-run";
|
|
3399
|
+
modeFlagsSeen += 1;
|
|
3400
|
+
i += 1;
|
|
3401
|
+
continue;
|
|
3402
|
+
}
|
|
3403
|
+
if (a === "--apply") {
|
|
3404
|
+
mode = "apply";
|
|
3405
|
+
modeFlagsSeen += 1;
|
|
3406
|
+
i += 1;
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
if (a === "--platform-repo") {
|
|
3410
|
+
const v = args[i + 1];
|
|
3411
|
+
if (v === undefined || v.trim().length === 0) {
|
|
3412
|
+
return `missing value for ${a}`;
|
|
3413
|
+
}
|
|
3414
|
+
platformRepo = v.trim();
|
|
3415
|
+
i += 2;
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
if (a === "--re-pin") {
|
|
3419
|
+
rePin = true;
|
|
3420
|
+
i += 1;
|
|
3421
|
+
continue;
|
|
3422
|
+
}
|
|
3423
|
+
if (a === "--yes" || a === "-y") {
|
|
3424
|
+
yes = true;
|
|
3425
|
+
i += 1;
|
|
3426
|
+
continue;
|
|
3427
|
+
}
|
|
3428
|
+
if (a === "--resume") {
|
|
3429
|
+
const v = args[i + 1];
|
|
3430
|
+
if (v === undefined)
|
|
3431
|
+
return `missing value for ${a}`;
|
|
3432
|
+
mode = "resume";
|
|
3433
|
+
slug = v;
|
|
3434
|
+
modeFlagsSeen += 1;
|
|
3435
|
+
i += 2;
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
if (a === "--abandon") {
|
|
3439
|
+
const v = args[i + 1];
|
|
3440
|
+
if (v === undefined)
|
|
3441
|
+
return `missing value for ${a}`;
|
|
3442
|
+
mode = "abandon";
|
|
3443
|
+
slug = v;
|
|
3444
|
+
modeFlagsSeen += 1;
|
|
3445
|
+
i += 2;
|
|
3446
|
+
continue;
|
|
3447
|
+
}
|
|
3448
|
+
if (a === "--file") {
|
|
3449
|
+
const v = args[i + 1];
|
|
3450
|
+
if (v === undefined)
|
|
3451
|
+
return `missing value for ${a}`;
|
|
3452
|
+
manifestFile = v;
|
|
3453
|
+
i += 2;
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3456
|
+
if (a === "--json") {
|
|
3457
|
+
dryRunJson = true;
|
|
3458
|
+
i += 1;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (a === "--slug") {
|
|
3462
|
+
const v = args[i + 1];
|
|
3463
|
+
if (v === undefined)
|
|
3464
|
+
return `missing value for ${a}`;
|
|
3465
|
+
slug = v;
|
|
3466
|
+
i += 2;
|
|
3467
|
+
continue;
|
|
3468
|
+
}
|
|
3469
|
+
if (a === "--display-name" || a === "--name") {
|
|
3470
|
+
const v = args[i + 1];
|
|
3471
|
+
if (v === undefined)
|
|
3472
|
+
return `missing value for ${a}`;
|
|
3473
|
+
displayName = v;
|
|
3474
|
+
i += 2;
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (a === "--description") {
|
|
3478
|
+
const v = args[i + 1];
|
|
3479
|
+
if (v === undefined)
|
|
3480
|
+
return `missing value for ${a}`;
|
|
3481
|
+
description = v;
|
|
3482
|
+
i += 2;
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
if (a === "--app-type" || a === "--type") {
|
|
3486
|
+
const v = args[i + 1];
|
|
3487
|
+
if (v === undefined)
|
|
3488
|
+
return `missing value for ${a}`;
|
|
3489
|
+
if (!APP_TYPES.includes(v)) {
|
|
3490
|
+
return `invalid --app-type "${v}" — must be one of ${APP_TYPES.join(", ")}`;
|
|
3491
|
+
}
|
|
3492
|
+
appType = v;
|
|
3493
|
+
i += 2;
|
|
3494
|
+
continue;
|
|
3495
|
+
}
|
|
3496
|
+
if (a === "--allowed-group" || a === "--group") {
|
|
3497
|
+
const v = args[i + 1];
|
|
3498
|
+
if (v === undefined)
|
|
3499
|
+
return `missing value for ${a}`;
|
|
3500
|
+
if (!GROUP_KEY_RE2.test(v) && !GROUP_UUID_RE2.test(v)) {
|
|
3501
|
+
return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
|
|
3502
|
+
}
|
|
3503
|
+
allowedGroups.push(v);
|
|
3504
|
+
i += 2;
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
if (a === "--message" || a === "-m") {
|
|
3508
|
+
const v = args[i + 1];
|
|
3509
|
+
if (v === undefined)
|
|
3510
|
+
return `missing value for ${a}`;
|
|
3511
|
+
message = v;
|
|
3512
|
+
i += 2;
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3515
|
+
if (a === "--timeout-seconds") {
|
|
3516
|
+
const v = args[i + 1];
|
|
3517
|
+
if (v === undefined)
|
|
3518
|
+
return `missing value for ${a}`;
|
|
3519
|
+
const n = Number(v);
|
|
3520
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3521
|
+
return `invalid --timeout-seconds "${v}" — expected positive integer`;
|
|
3522
|
+
}
|
|
3523
|
+
timeoutSeconds = n;
|
|
3524
|
+
i += 2;
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (a === "--timeout-minutes") {
|
|
3528
|
+
const v = args[i + 1];
|
|
3529
|
+
if (v === undefined)
|
|
3530
|
+
return `missing value for ${a}`;
|
|
3531
|
+
const n = Number(v);
|
|
3532
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3533
|
+
return `invalid --timeout-minutes "${v}" — expected positive integer`;
|
|
3534
|
+
}
|
|
3535
|
+
timeoutMinutes = n;
|
|
3536
|
+
i += 2;
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
if (a === "--at") {
|
|
3540
|
+
const v = args[i + 1];
|
|
3541
|
+
if (v === undefined)
|
|
3542
|
+
return `missing value for ${a}`;
|
|
3543
|
+
if (!/^[0-9a-f]{40}$/.test(v)) {
|
|
3544
|
+
return `invalid --at "${v}" — expected a 40-char lowercase hex git SHA from the MANAGED repo`;
|
|
3545
|
+
}
|
|
3546
|
+
atSha = v;
|
|
3547
|
+
i += 2;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
if (a === "--resume-pr") {
|
|
3551
|
+
const v = args[i + 1];
|
|
3552
|
+
if (v === undefined)
|
|
3553
|
+
return `missing value for ${a}`;
|
|
3554
|
+
const n = Number(v);
|
|
3555
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
3556
|
+
return `invalid --resume-pr "${v}" — expected positive integer PR number`;
|
|
3557
|
+
}
|
|
3558
|
+
resumePrNumber = n;
|
|
3559
|
+
i += 2;
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
return `unknown argument "${a}"`;
|
|
3563
|
+
}
|
|
3564
|
+
if (modeFlagsSeen > 1) {
|
|
3565
|
+
return "--new, --resume, --abandon, --dry-run, and --apply are mutually exclusive";
|
|
3566
|
+
}
|
|
3567
|
+
if (mode === "dry-run") {
|
|
3568
|
+
if (slug !== null)
|
|
3569
|
+
return "--dry-run does not accept --slug (slug is read from launchpad.yaml)";
|
|
3570
|
+
if (displayName !== null)
|
|
3571
|
+
return "--dry-run does not accept --display-name";
|
|
3572
|
+
if (description !== null)
|
|
3573
|
+
return "--dry-run does not accept --description";
|
|
3574
|
+
if (appType !== null)
|
|
3575
|
+
return "--dry-run does not accept --app-type";
|
|
3576
|
+
if (allowedGroups.length > 0)
|
|
3577
|
+
return "--dry-run does not accept --allowed-group (read from launchpad.yaml)";
|
|
3578
|
+
if (message !== null)
|
|
3579
|
+
return "--dry-run does not accept --message";
|
|
3580
|
+
if (timeoutSeconds !== null)
|
|
3581
|
+
return "--dry-run does not accept --timeout-seconds";
|
|
3582
|
+
if (platformRepo !== null)
|
|
3583
|
+
return "--dry-run does not accept --platform-repo (apply-only)";
|
|
3584
|
+
if (rePin)
|
|
3585
|
+
return "--dry-run does not accept --re-pin (apply-only)";
|
|
3586
|
+
if (yes)
|
|
3587
|
+
return "--dry-run does not accept --yes (apply-only)";
|
|
3588
|
+
return {
|
|
3589
|
+
mode: { kind: "dry-run", file: manifestFile, json: dryRunJson },
|
|
3590
|
+
message: null,
|
|
3591
|
+
timeoutSeconds: null
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
if (mode === "apply") {
|
|
3595
|
+
if (slug !== null)
|
|
3596
|
+
return "--apply does not accept --slug (slug is read from launchpad.yaml)";
|
|
3597
|
+
if (displayName !== null)
|
|
3598
|
+
return "--apply does not accept --display-name";
|
|
3599
|
+
if (description !== null)
|
|
3600
|
+
return "--apply does not accept --description";
|
|
3601
|
+
if (appType !== null)
|
|
3602
|
+
return "--apply does not accept --app-type";
|
|
3603
|
+
if (allowedGroups.length > 0)
|
|
3604
|
+
return "--apply does not accept --allowed-group (read from launchpad.yaml)";
|
|
3605
|
+
if (message !== null)
|
|
3606
|
+
return "--apply does not accept --message";
|
|
3607
|
+
if (timeoutSeconds !== null)
|
|
3608
|
+
return "--apply does not accept --timeout-seconds (use --timeout-minutes instead — apply polling is multi-minute)";
|
|
3609
|
+
if (dryRunJson)
|
|
3610
|
+
return "--apply does not accept --json";
|
|
3611
|
+
return {
|
|
3612
|
+
mode: {
|
|
3613
|
+
kind: "apply",
|
|
3614
|
+
file: manifestFile,
|
|
3615
|
+
platformRepo,
|
|
3616
|
+
rePin,
|
|
3617
|
+
yes,
|
|
3618
|
+
resumePrNumber,
|
|
3619
|
+
atSha,
|
|
3620
|
+
timeoutMinutes
|
|
3621
|
+
},
|
|
3622
|
+
message: null,
|
|
3623
|
+
timeoutSeconds: null
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
if (resumePrNumber !== null) {
|
|
3627
|
+
return "--resume-pr is only valid with --apply";
|
|
3628
|
+
}
|
|
3629
|
+
if (atSha !== null) {
|
|
3630
|
+
return "--at is only valid with --apply";
|
|
3631
|
+
}
|
|
3632
|
+
if (timeoutMinutes !== null) {
|
|
3633
|
+
return "--timeout-minutes is only valid with --apply";
|
|
3634
|
+
}
|
|
3635
|
+
if (manifestFile !== null) {
|
|
3636
|
+
return "--file is only valid with --dry-run or --apply";
|
|
3637
|
+
}
|
|
3638
|
+
if (dryRunJson) {
|
|
3639
|
+
return "--json is only valid with --dry-run";
|
|
3640
|
+
}
|
|
3641
|
+
if (platformRepo !== null) {
|
|
3642
|
+
return "--platform-repo is only valid with --apply";
|
|
3643
|
+
}
|
|
3644
|
+
if (rePin) {
|
|
3645
|
+
return "--re-pin is only valid with --apply";
|
|
3646
|
+
}
|
|
3647
|
+
if (yes) {
|
|
3648
|
+
return "--yes is only valid with --apply";
|
|
3649
|
+
}
|
|
3650
|
+
if (mode === "content") {
|
|
3651
|
+
if (slug !== null && !SLUG_RE3.test(slug)) {
|
|
3652
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
|
|
3653
|
+
}
|
|
3654
|
+
return {
|
|
3655
|
+
mode: { kind: "content", slug },
|
|
3656
|
+
message,
|
|
3657
|
+
timeoutSeconds
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
if (mode === "resume" || mode === "abandon") {
|
|
3661
|
+
if (slug === null || !SLUG_RE3.test(slug)) {
|
|
3662
|
+
return `--${mode} requires a valid slug (kebab-case)`;
|
|
3195
3663
|
}
|
|
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);
|
|
3664
|
+
if (displayName !== null || appType !== null || allowedGroups.length > 0) {
|
|
3665
|
+
return `--${mode} does not accept create-only flags (--display-name / --app-type / --allowed-group)`;
|
|
3206
3666
|
}
|
|
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
|
-
}
|
|
3667
|
+
if (message !== null) {
|
|
3668
|
+
return `--${mode} does not accept --message`;
|
|
3223
3669
|
}
|
|
3670
|
+
return {
|
|
3671
|
+
mode: { kind: mode, slug },
|
|
3672
|
+
message: null,
|
|
3673
|
+
timeoutSeconds
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
if (slug === null)
|
|
3677
|
+
return "--new requires --slug";
|
|
3678
|
+
if (!SLUG_RE3.test(slug))
|
|
3679
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE3.source}`;
|
|
3680
|
+
if (slug.length < 3 || slug.length > 58) {
|
|
3681
|
+
return `slug length out of bounds (3–58 chars): "${slug}"`;
|
|
3682
|
+
}
|
|
3683
|
+
if (displayName === null || displayName.length === 0) {
|
|
3684
|
+
return "--new requires --display-name";
|
|
3685
|
+
}
|
|
3686
|
+
if (appType === null) {
|
|
3687
|
+
return `--new requires --app-type (one of ${APP_TYPES.join(", ")})`;
|
|
3688
|
+
}
|
|
3689
|
+
if (allowedGroups.length === 0) {
|
|
3690
|
+
return "--new requires at least one --allowed-group";
|
|
3224
3691
|
}
|
|
3225
|
-
});
|
|
3226
|
-
function parseFleetSecretSets(input) {
|
|
3227
|
-
const res = FleetSecretSetsSchema.safeParse(input);
|
|
3228
|
-
if (res.success)
|
|
3229
|
-
return { ok: true, manifest: res.data };
|
|
3230
3692
|
return {
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3693
|
+
mode: {
|
|
3694
|
+
kind: "new",
|
|
3695
|
+
slug,
|
|
3696
|
+
displayName,
|
|
3697
|
+
description,
|
|
3698
|
+
appType,
|
|
3699
|
+
allowedGroups
|
|
3700
|
+
},
|
|
3701
|
+
message,
|
|
3702
|
+
timeoutSeconds
|
|
3236
3703
|
};
|
|
3237
3704
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
var UNSAFE_KEY = new Set(["__proto__", "prototype", "constructor"]);
|
|
3705
|
+
|
|
3706
|
+
// src/deploy/apply.ts
|
|
3707
|
+
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
3708
|
+
import { resolve as resolvePath, join as join7 } from "node:path";
|
|
3709
|
+
|
|
3244
3710
|
// src/manifest/load.ts
|
|
3711
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3712
|
+
import { parse as parseYaml3, YAMLParseError } from "yaml";
|
|
3245
3713
|
function loadManifest(path6) {
|
|
3246
3714
|
let raw;
|
|
3247
3715
|
try {
|
|
3248
|
-
raw =
|
|
3716
|
+
raw = readFileSync5(path6, "utf8");
|
|
3249
3717
|
} catch (err) {
|
|
3250
3718
|
const e = err;
|
|
3251
3719
|
if (e.code === "ENOENT") {
|
|
@@ -3258,7 +3726,7 @@ function loadManifest(path6) {
|
|
|
3258
3726
|
function parseManifest2(yamlText, path6) {
|
|
3259
3727
|
let parsed;
|
|
3260
3728
|
try {
|
|
3261
|
-
parsed =
|
|
3729
|
+
parsed = parseYaml3(yamlText);
|
|
3262
3730
|
} catch (err) {
|
|
3263
3731
|
const message = err instanceof YAMLParseError ? err.message : err.message ?? String(err);
|
|
3264
3732
|
return { kind: "yaml-parse-error", path: path6, message };
|
|
@@ -3330,29 +3798,23 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
3330
3798
|
io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
|
|
3331
3799
|
return 1;
|
|
3332
3800
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
} catch (e) {
|
|
3337
|
-
io.err(`launchpad deploy --apply: failed to resolve git HEAD in ${process.cwd()}: ${describe10(e)}`);
|
|
3338
|
-
io.err(" Apply needs a clean commit so portal-bot can fetch launchpad.yaml at that sha.");
|
|
3339
|
-
return 2;
|
|
3340
|
-
}
|
|
3341
|
-
if (!MANIFEST_SHA_REGEX.test(manifestSha)) {
|
|
3342
|
-
io.err(`launchpad deploy --apply: git HEAD returned an unexpected sha shape: '${manifestSha}'`);
|
|
3343
|
-
return 2;
|
|
3801
|
+
if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
|
|
3802
|
+
io.err(`launchpad deploy --apply: --at expects a 40-char lowercase hex git SHA, got '${opts.atSha}'`);
|
|
3803
|
+
return 64;
|
|
3344
3804
|
}
|
|
3345
|
-
io.out(`Planning apply for ${slug} @ ${
|
|
3805
|
+
io.out(opts.atSha !== undefined ? `Planning apply for ${slug} @ ${opts.atSha.slice(0, 7)} (managed repo) …` : `Planning apply for ${slug} (bot resolves managed main HEAD) …`);
|
|
3346
3806
|
let plan;
|
|
3347
3807
|
try {
|
|
3348
3808
|
plan = await apiJson(cfg, {
|
|
3349
3809
|
method: "POST",
|
|
3350
3810
|
path: `/apps/${slug}/manifest/plan`,
|
|
3351
|
-
jsonBody: { manifestSha }
|
|
3811
|
+
jsonBody: opts.atSha !== undefined ? { manifestSha: opts.atSha } : {}
|
|
3352
3812
|
}, fetcher);
|
|
3353
3813
|
} catch (e) {
|
|
3354
3814
|
return mapHttpError(e, slug, io);
|
|
3355
3815
|
}
|
|
3816
|
+
const manifestSha = plan.manifestSha;
|
|
3817
|
+
io.out(`Manifest pinned @ ${manifestSha.slice(0, 7)} (managed repo provenance).`);
|
|
3356
3818
|
for (const w of plan.warnings)
|
|
3357
3819
|
io.err(`! ${w}`);
|
|
3358
3820
|
io.out("");
|
|
@@ -3472,17 +3934,9 @@ function sleep(ms) {
|
|
|
3472
3934
|
return Promise.resolve();
|
|
3473
3935
|
return new Promise((res) => setTimeout(res, ms));
|
|
3474
3936
|
}
|
|
3475
|
-
function defaultGitHeadSha(cwd) {
|
|
3476
|
-
const out = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
3477
|
-
cwd,
|
|
3478
|
-
encoding: "utf8",
|
|
3479
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3480
|
-
});
|
|
3481
|
-
return out.trim();
|
|
3482
|
-
}
|
|
3483
3937
|
function deletePinIfPresent(cfg, slug, io) {
|
|
3484
|
-
const pinPath =
|
|
3485
|
-
if (!
|
|
3938
|
+
const pinPath = join7(cfg.stateDir, slug, "group.json");
|
|
3939
|
+
if (!existsSync3(pinPath))
|
|
3486
3940
|
return;
|
|
3487
3941
|
try {
|
|
3488
3942
|
rmSync(pinPath, { force: true });
|
|
@@ -3562,7 +4016,7 @@ function renderManifestError(loaded, io) {
|
|
|
3562
4016
|
}
|
|
3563
4017
|
|
|
3564
4018
|
// src/deploy/dry-run.ts
|
|
3565
|
-
import { execFileSync
|
|
4019
|
+
import { execFileSync } from "node:child_process";
|
|
3566
4020
|
import { resolve as resolve5 } from "node:path";
|
|
3567
4021
|
var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
|
|
3568
4022
|
async function runDeployDryRun(opts, io, deps = {}) {
|
|
@@ -3576,7 +4030,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
3576
4030
|
const slug = loaded.manifest.metadata.name;
|
|
3577
4031
|
let manifestSha;
|
|
3578
4032
|
try {
|
|
3579
|
-
manifestSha = (deps.gitHeadSha ??
|
|
4033
|
+
manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
|
|
3580
4034
|
} catch (e) {
|
|
3581
4035
|
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe11(e)}`;
|
|
3582
4036
|
if (opts.json) {
|
|
@@ -3619,8 +4073,8 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
3619
4073
|
}
|
|
3620
4074
|
return 0;
|
|
3621
4075
|
}
|
|
3622
|
-
function
|
|
3623
|
-
const out =
|
|
4076
|
+
function defaultGitHeadSha(cwd) {
|
|
4077
|
+
const out = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
3624
4078
|
cwd,
|
|
3625
4079
|
encoding: "utf8",
|
|
3626
4080
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -3952,6 +4406,7 @@ async function runDeploy(args, io) {
|
|
|
3952
4406
|
platformRepo: flags.mode.platformRepo,
|
|
3953
4407
|
rePin: flags.mode.rePin,
|
|
3954
4408
|
yes: flags.mode.yes,
|
|
4409
|
+
...flags.mode.atSha !== null ? { atSha: flags.mode.atSha } : {},
|
|
3955
4410
|
...flags.mode.resumePrNumber !== null ? { resumePrNumber: flags.mode.resumePrNumber } : {},
|
|
3956
4411
|
...flags.mode.timeoutMinutes !== null ? { timeoutMinutes: flags.mode.timeoutMinutes } : {}
|
|
3957
4412
|
}, io);
|
|
@@ -3974,7 +4429,7 @@ async function runDeploy(args, io) {
|
|
|
3974
4429
|
}
|
|
3975
4430
|
const cwd = process.cwd();
|
|
3976
4431
|
const manifestPath = path6.join(cwd, "launchpad.yaml");
|
|
3977
|
-
if (
|
|
4432
|
+
if (existsSync4(manifestPath)) {
|
|
3978
4433
|
return runModelADeploy({ cwd, manifestPath, argv: args, io });
|
|
3979
4434
|
}
|
|
3980
4435
|
const parsed = parseArgs2(args);
|
|
@@ -4001,12 +4456,34 @@ async function runDeploy(args, io) {
|
|
|
4001
4456
|
try {
|
|
4002
4457
|
const cfg = loadConfig();
|
|
4003
4458
|
io.out(`Packaging working tree for ${slug} …`);
|
|
4004
|
-
const
|
|
4005
|
-
if (
|
|
4459
|
+
const rawFiles = await listDeployFiles({ cwd: process.cwd() });
|
|
4460
|
+
if (rawFiles.length === 0) {
|
|
4006
4461
|
io.err("launchpad deploy: no files to deploy (git ls-files returned empty).");
|
|
4007
4462
|
io.err(" did you run this from inside the cloned working tree?");
|
|
4008
4463
|
return 1;
|
|
4009
4464
|
}
|
|
4465
|
+
let contentManifestYaml = null;
|
|
4466
|
+
const contentManifestPath = path6.join(process.cwd(), "launchpad.yaml");
|
|
4467
|
+
if (existsSync4(contentManifestPath)) {
|
|
4468
|
+
try {
|
|
4469
|
+
contentManifestYaml = readFileSync6(contentManifestPath, "utf8");
|
|
4470
|
+
} catch {
|
|
4471
|
+
contentManifestYaml = null;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
const boundary = applyBoundaryToFiles({
|
|
4475
|
+
cwd: process.cwd(),
|
|
4476
|
+
manifestYaml: contentManifestYaml,
|
|
4477
|
+
files: rawFiles
|
|
4478
|
+
});
|
|
4479
|
+
if (boundary.kind === "error") {
|
|
4480
|
+
io.err(`launchpad deploy: ${boundary.message}`);
|
|
4481
|
+
return 1;
|
|
4482
|
+
}
|
|
4483
|
+
for (const w of boundary.warnings) {
|
|
4484
|
+
io.err(`warning: ${w}`);
|
|
4485
|
+
}
|
|
4486
|
+
const files = boundary.files;
|
|
4010
4487
|
const packed = await packTarGz(process.cwd(), files);
|
|
4011
4488
|
io.out(`Packed ${packed.fileCount} files (${formatBytes2(packed.uncompressedBytes)}` + ` uncompressed, ${formatBytes2(packed.bytes.length)} gzipped)`);
|
|
4012
4489
|
io.out(`Uploading to ${cfg.botUrl}/apps/${slug}/deploy-pr …`);
|
|
@@ -4026,6 +4503,9 @@ async function runDeploy(args, io) {
|
|
|
4026
4503
|
io.out(`PR opened: ${response.reviewUrl}`);
|
|
4027
4504
|
io.out(`Branch: ${response.branch}`);
|
|
4028
4505
|
io.out(`Number: #${response.prNumber}`);
|
|
4506
|
+
io.out("");
|
|
4507
|
+
io.out(`Review from the CLI: launchpad review ${response.prNumber} --slug ${slug}`);
|
|
4508
|
+
io.out(`Merge when approved: launchpad merge ${response.prNumber} --slug ${slug}`);
|
|
4029
4509
|
if (response.bootstrapWorkflow === true) {
|
|
4030
4510
|
io.out("");
|
|
4031
4511
|
io.out("(first deploy: included canonical launchpad-review.yml workflow file)");
|
|
@@ -4122,6 +4602,28 @@ function formatBytes2(n) {
|
|
|
4122
4602
|
function describe12(e) {
|
|
4123
4603
|
return e instanceof Error ? e.message : String(e);
|
|
4124
4604
|
}
|
|
4605
|
+
function surfaceDeployExtras(body, io, slug) {
|
|
4606
|
+
if (body.boundary_stripped !== undefined && body.boundary_stripped.length > 0) {
|
|
4607
|
+
io.err(`warning: the bot stripped ${body.boundary_stripped.length} never-shippable file(s) server-side:`);
|
|
4608
|
+
for (const p of body.boundary_stripped.slice(0, 10)) {
|
|
4609
|
+
io.err(` - ${p}`);
|
|
4610
|
+
}
|
|
4611
|
+
if (body.boundary_stripped.length > 10) {
|
|
4612
|
+
io.err(` … and ${body.boundary_stripped.length - 10} more`);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
const se = body.standing_exceptions;
|
|
4616
|
+
if (se !== undefined && se.count > 0) {
|
|
4617
|
+
io.out(` ${se.count} standing policy exception(s) observed in content already live on main (non-blocking):`);
|
|
4618
|
+
for (const e of se.entries.slice(0, 5)) {
|
|
4619
|
+
io.out(` - ${e.path} [${e.rule}]`);
|
|
4620
|
+
}
|
|
4621
|
+
if (se.entries.length > 5) {
|
|
4622
|
+
io.out(` … and ${se.entries.length - 5} more`);
|
|
4623
|
+
}
|
|
4624
|
+
io.out(` Full list: \`launchpad status ${slug}\`.`);
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4125
4627
|
function resolveManifestSlug(parsed) {
|
|
4126
4628
|
if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
|
|
4127
4629
|
return null;
|
|
@@ -4137,7 +4639,7 @@ async function runModelADeploy(args) {
|
|
|
4137
4639
|
const { cwd, manifestPath, io } = args;
|
|
4138
4640
|
let slug;
|
|
4139
4641
|
try {
|
|
4140
|
-
const metaSlug = resolveManifestSlug(
|
|
4642
|
+
const metaSlug = resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
|
|
4141
4643
|
if (metaSlug === null) {
|
|
4142
4644
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
4143
4645
|
return 64;
|
|
@@ -4178,6 +4680,9 @@ async function runModelADeploy(args) {
|
|
|
4178
4680
|
case "walk-error":
|
|
4179
4681
|
io.err(`launchpad deploy: walk failed — ${result.message}`);
|
|
4180
4682
|
return 1;
|
|
4683
|
+
case "boundary-error":
|
|
4684
|
+
io.err(`launchpad deploy: ${result.message}`);
|
|
4685
|
+
return 1;
|
|
4181
4686
|
case "pack-error":
|
|
4182
4687
|
io.err(`launchpad deploy: pack failed — ${result.message}`);
|
|
4183
4688
|
return 1;
|
|
@@ -4213,10 +4718,22 @@ async function runModelADeploy(args) {
|
|
|
4213
4718
|
return 1;
|
|
4214
4719
|
}
|
|
4215
4720
|
case "ok": {
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4721
|
+
for (const w of result.boundaryWarnings) {
|
|
4722
|
+
io.err(`warning: ${w}`);
|
|
4723
|
+
}
|
|
4724
|
+
const success = result.result;
|
|
4725
|
+
if ("outcome" in success) {
|
|
4726
|
+
io.out("Nothing to deploy — your app is already live at this content.");
|
|
4727
|
+
if (typeof success.head_sha === "string") {
|
|
4728
|
+
io.out(` (managed main @ ${success.head_sha.slice(0, 8)} matches the bundle)`);
|
|
4729
|
+
}
|
|
4730
|
+
surfaceDeployExtras(success, io, slug);
|
|
4731
|
+
return 0;
|
|
4732
|
+
}
|
|
4733
|
+
if (success.status === "provisioning_started") {
|
|
4734
|
+
const submissionId = typeof success.submissionId === "string" ? success.submissionId : "(missing)";
|
|
4735
|
+
const appType = typeof success.appType === "string" ? success.appType : "(missing)";
|
|
4736
|
+
const message = typeof success.message === "string" ? success.message : "Bot accepted the upload and started provisioning.";
|
|
4220
4737
|
io.out(`✓ First-time deploy — provisioning workflow started for ${slug}`);
|
|
4221
4738
|
io.out(` submission: ${submissionId}`);
|
|
4222
4739
|
io.out(` appType: ${appType}`);
|
|
@@ -4228,12 +4745,12 @@ async function runModelADeploy(args) {
|
|
|
4228
4745
|
io.out(` launchpad deploy # re-run once lifecycle is "live"`);
|
|
4229
4746
|
return 0;
|
|
4230
4747
|
}
|
|
4231
|
-
if (typeof
|
|
4748
|
+
if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
|
|
4232
4749
|
io.err(`launchpad deploy: bot returned an unexpected 202 body — missing commit_sha or repo.`);
|
|
4233
|
-
io.err(` got: ${JSON.stringify(
|
|
4750
|
+
io.err(` got: ${JSON.stringify(success)}`);
|
|
4234
4751
|
return 1;
|
|
4235
4752
|
}
|
|
4236
|
-
io.out(`✓ Bundle accepted — committed as ${
|
|
4753
|
+
io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
|
|
4237
4754
|
io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
|
|
4238
4755
|
if (result.workerScript !== null) {
|
|
4239
4756
|
io.out(` cron Worker '${result.workerScript}' bundled + shipped (the bot deploys it after the commit)`);
|
|
@@ -4241,13 +4758,13 @@ async function runModelADeploy(args) {
|
|
|
4241
4758
|
if (result.walk.skipped.length > 0) {
|
|
4242
4759
|
io.out(` ${result.walk.skipped.length} entries skipped (.gitignore / default-ignore / symlink)`);
|
|
4243
4760
|
}
|
|
4244
|
-
if (typeof
|
|
4245
|
-
io.out(` ${
|
|
4761
|
+
if (typeof success.message === "string") {
|
|
4762
|
+
io.out(` ${success.message}`);
|
|
4246
4763
|
}
|
|
4764
|
+
surfaceDeployExtras(success, io, slug);
|
|
4247
4765
|
io.out("");
|
|
4248
|
-
io.out("
|
|
4249
|
-
io.out(`
|
|
4250
|
-
io.out(` launchpad apps # see lifecycle`);
|
|
4766
|
+
io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
|
|
4767
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
4251
4768
|
return 0;
|
|
4252
4769
|
}
|
|
4253
4770
|
}
|
|
@@ -4450,7 +4967,7 @@ function describe13(e) {
|
|
|
4450
4967
|
}
|
|
4451
4968
|
|
|
4452
4969
|
// src/commands/generate.ts
|
|
4453
|
-
import { mkdirSync, readFileSync as
|
|
4970
|
+
import { mkdirSync, readFileSync as readFileSync7, writeFileSync } from "node:fs";
|
|
4454
4971
|
import { dirname as dirname4, resolve as resolve6, relative as relative3 } from "node:path";
|
|
4455
4972
|
var generateCommand = {
|
|
4456
4973
|
name: "generate",
|
|
@@ -4538,7 +5055,7 @@ function applyOne(artefact, path8, out, force) {
|
|
|
4538
5055
|
}
|
|
4539
5056
|
function readIfExists(path8) {
|
|
4540
5057
|
try {
|
|
4541
|
-
return { kind: "ok", content:
|
|
5058
|
+
return { kind: "ok", content: readFileSync7(path8, "utf8") };
|
|
4542
5059
|
} catch (err) {
|
|
4543
5060
|
const e = err;
|
|
4544
5061
|
if (e.code === "ENOENT")
|
|
@@ -4749,13 +5266,13 @@ function parseFlags(args) {
|
|
|
4749
5266
|
}
|
|
4750
5267
|
|
|
4751
5268
|
// src/groups/client.ts
|
|
4752
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
4753
|
-
import { dirname as dirname5, join as
|
|
5269
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5270
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
4754
5271
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
4755
5272
|
var CACHE_FILENAME = "groups.json";
|
|
4756
5273
|
async function fetchGroups(cfg, opts = {}) {
|
|
4757
5274
|
const now = opts.now ?? Date.now;
|
|
4758
|
-
const cachePath =
|
|
5275
|
+
const cachePath = join9(cfg.cacheDir, CACHE_FILENAME);
|
|
4759
5276
|
if (opts.forceRefresh !== true) {
|
|
4760
5277
|
const cached = readCache(cachePath);
|
|
4761
5278
|
if (cached !== null) {
|
|
@@ -4793,7 +5310,7 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
4793
5310
|
function readCache(path8) {
|
|
4794
5311
|
let raw;
|
|
4795
5312
|
try {
|
|
4796
|
-
raw =
|
|
5313
|
+
raw = readFileSync8(path8, "utf8");
|
|
4797
5314
|
} catch {
|
|
4798
5315
|
return null;
|
|
4799
5316
|
}
|
|
@@ -5294,17 +5811,17 @@ function describe17(e) {
|
|
|
5294
5811
|
|
|
5295
5812
|
// src/commands/init.ts
|
|
5296
5813
|
import { createInterface } from "node:readline/promises";
|
|
5297
|
-
import { existsSync as
|
|
5814
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5298
5815
|
import { resolve as resolve7 } from "node:path";
|
|
5299
5816
|
import { stringify as yamlStringify } from "yaml";
|
|
5300
5817
|
|
|
5301
5818
|
// src/detect/index.ts
|
|
5302
|
-
import { existsSync as
|
|
5303
|
-
import { join as
|
|
5819
|
+
import { existsSync as existsSync5, readFileSync as readFileSync9, statSync } from "node:fs";
|
|
5820
|
+
import { join as join10 } from "node:path";
|
|
5304
5821
|
function detectAppShape(cwd) {
|
|
5305
|
-
const hasPackageJson =
|
|
5306
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
5307
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
5822
|
+
const hasPackageJson = existsSync5(join10(cwd, "package.json"));
|
|
5823
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync5(join10(cwd, file)));
|
|
5824
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync5(join10(cwd, n)));
|
|
5308
5825
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
5309
5826
|
return {
|
|
5310
5827
|
kind: "not-applicable",
|
|
@@ -5350,7 +5867,7 @@ var LOCKFILES = [
|
|
|
5350
5867
|
{ file: "yarn.lock", pm: "yarn" }
|
|
5351
5868
|
];
|
|
5352
5869
|
function detectPackageManager(cwd) {
|
|
5353
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
5870
|
+
const present = LOCKFILES.filter(({ file }) => existsSync5(join10(cwd, file)));
|
|
5354
5871
|
if (present.length === 0) {
|
|
5355
5872
|
return {
|
|
5356
5873
|
kind: "ambiguous",
|
|
@@ -5372,8 +5889,8 @@ function detectPackageManager(cwd) {
|
|
|
5372
5889
|
}
|
|
5373
5890
|
function detectVitePresence(cwd) {
|
|
5374
5891
|
for (const name of VITE_CONFIG_NAMES) {
|
|
5375
|
-
const p =
|
|
5376
|
-
if (
|
|
5892
|
+
const p = join10(cwd, name);
|
|
5893
|
+
if (existsSync5(p)) {
|
|
5377
5894
|
return { kind: "ok", value: { path: p } };
|
|
5378
5895
|
}
|
|
5379
5896
|
}
|
|
@@ -5383,9 +5900,9 @@ function detectVitePresence(cwd) {
|
|
|
5383
5900
|
};
|
|
5384
5901
|
}
|
|
5385
5902
|
function detectAppType(cwd) {
|
|
5386
|
-
const fnDir =
|
|
5903
|
+
const fnDir = join10(cwd, "functions");
|
|
5387
5904
|
let hasFunctionsDir = false;
|
|
5388
|
-
if (
|
|
5905
|
+
if (existsSync5(fnDir)) {
|
|
5389
5906
|
try {
|
|
5390
5907
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
5391
5908
|
} catch {
|
|
@@ -5395,8 +5912,8 @@ function detectAppType(cwd) {
|
|
|
5395
5912
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
5396
5913
|
}
|
|
5397
5914
|
function detectBuildCommand(cwd, pm) {
|
|
5398
|
-
const pkgJsonPath =
|
|
5399
|
-
if (!
|
|
5915
|
+
const pkgJsonPath = join10(cwd, "package.json");
|
|
5916
|
+
if (!existsSync5(pkgJsonPath)) {
|
|
5400
5917
|
return {
|
|
5401
5918
|
kind: "ambiguous",
|
|
5402
5919
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -5404,7 +5921,7 @@ function detectBuildCommand(cwd, pm) {
|
|
|
5404
5921
|
}
|
|
5405
5922
|
let pkgJson;
|
|
5406
5923
|
try {
|
|
5407
|
-
pkgJson = JSON.parse(
|
|
5924
|
+
pkgJson = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
|
|
5408
5925
|
} catch (e) {
|
|
5409
5926
|
return {
|
|
5410
5927
|
kind: "ambiguous",
|
|
@@ -5439,7 +5956,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
|
|
|
5439
5956
|
function detectDestinationDir(cwd, vite) {
|
|
5440
5957
|
let text;
|
|
5441
5958
|
try {
|
|
5442
|
-
text =
|
|
5959
|
+
text = readFileSync9(vite.path, "utf8");
|
|
5443
5960
|
} catch (e) {
|
|
5444
5961
|
return {
|
|
5445
5962
|
kind: "ambiguous",
|
|
@@ -5469,7 +5986,7 @@ async function runInit(args, io, prompt) {
|
|
|
5469
5986
|
}
|
|
5470
5987
|
const { inputs, options } = parsed;
|
|
5471
5988
|
const outPath = resolve7(process.cwd(), options.out);
|
|
5472
|
-
if (
|
|
5989
|
+
if (existsSync6(outPath) && !options.force) {
|
|
5473
5990
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
5474
5991
|
io.err("Pass --force to overwrite.");
|
|
5475
5992
|
return 64;
|
|
@@ -5876,6 +6393,10 @@ function buildManifest(inputs, detected) {
|
|
|
5876
6393
|
}
|
|
5877
6394
|
];
|
|
5878
6395
|
}
|
|
6396
|
+
manifest.app = {
|
|
6397
|
+
root: ".",
|
|
6398
|
+
include: [...defaultIncludeForAppType(inputs.type)]
|
|
6399
|
+
};
|
|
5879
6400
|
return manifest;
|
|
5880
6401
|
}
|
|
5881
6402
|
function renderYaml(manifest) {
|
|
@@ -5883,8 +6404,8 @@ function renderYaml(manifest) {
|
|
|
5883
6404
|
}
|
|
5884
6405
|
function ensureGitignoreEntries(path8, entries) {
|
|
5885
6406
|
let current = "";
|
|
5886
|
-
if (
|
|
5887
|
-
current =
|
|
6407
|
+
if (existsSync6(path8)) {
|
|
6408
|
+
current = readFileSync10(path8, "utf8");
|
|
5888
6409
|
}
|
|
5889
6410
|
const lines = current.split(/\r?\n/);
|
|
5890
6411
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -6016,7 +6537,7 @@ async function runLogs(args, io) {
|
|
|
6016
6537
|
if (r.deployments.length === 0) {
|
|
6017
6538
|
io.out("");
|
|
6018
6539
|
io.out("(note: this command shows deploy history, not runtime logs —");
|
|
6019
|
-
io.out(
|
|
6540
|
+
io.out(` run \`launchpad status ${slug}\` for the latest build outcome)`);
|
|
6020
6541
|
}
|
|
6021
6542
|
return 0;
|
|
6022
6543
|
} catch (e) {
|
|
@@ -6184,6 +6705,8 @@ async function runMerge(args, io) {
|
|
|
6184
6705
|
if (body.selfMerge) {
|
|
6185
6706
|
io.out("Note: self-merge (last commit author == caller)");
|
|
6186
6707
|
}
|
|
6708
|
+
io.out("");
|
|
6709
|
+
io.out(`Merged; build pending — run \`launchpad status ${slug}\` to confirm the build outcome.`);
|
|
6187
6710
|
return 0;
|
|
6188
6711
|
}
|
|
6189
6712
|
let env = null;
|
|
@@ -6296,7 +6819,7 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
6296
6819
|
io.err(`launchpad merge: GitHub upstream error (${detail ?? code}).`);
|
|
6297
6820
|
return;
|
|
6298
6821
|
case "merge_unprocessable":
|
|
6299
|
-
io.err(`launchpad merge: GitHub refused the merge (422): ${detail ?? "
|
|
6822
|
+
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
6823
|
return;
|
|
6301
6824
|
default:
|
|
6302
6825
|
io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
|
|
@@ -6783,6 +7306,39 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
|
6783
7306
|
return apiJson(cfg, { path: path10 }, fetcher);
|
|
6784
7307
|
}
|
|
6785
7308
|
|
|
7309
|
+
// src/deploy/deployment-status.ts
|
|
7310
|
+
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
7311
|
+
let raw;
|
|
7312
|
+
try {
|
|
7313
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/exceptions` }, fetcher);
|
|
7314
|
+
} catch (e) {
|
|
7315
|
+
if (e instanceof NotFoundError)
|
|
7316
|
+
return null;
|
|
7317
|
+
throw e;
|
|
7318
|
+
}
|
|
7319
|
+
if (!Array.isArray(raw.exceptions))
|
|
7320
|
+
return null;
|
|
7321
|
+
return raw.exceptions.filter((e) => typeof e === "object" && e !== null && typeof e.path === "string" && typeof e.rule === "string" && typeof e.detectedAt === "string" && typeof e.deployRef === "string");
|
|
7322
|
+
}
|
|
7323
|
+
async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
7324
|
+
let raw;
|
|
7325
|
+
try {
|
|
7326
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/deployment-status` }, fetcher);
|
|
7327
|
+
} catch (e) {
|
|
7328
|
+
if (e instanceof NotFoundError)
|
|
7329
|
+
return null;
|
|
7330
|
+
throw e;
|
|
7331
|
+
}
|
|
7332
|
+
if (typeof raw.supported !== "boolean")
|
|
7333
|
+
return null;
|
|
7334
|
+
return {
|
|
7335
|
+
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
7336
|
+
supported: raw.supported,
|
|
7337
|
+
liveDeployment: raw.liveDeployment ?? null,
|
|
7338
|
+
lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null
|
|
7339
|
+
};
|
|
7340
|
+
}
|
|
7341
|
+
|
|
6786
7342
|
// src/commands/pull.ts
|
|
6787
7343
|
var pullCommand = {
|
|
6788
7344
|
name: "pull",
|
|
@@ -6828,6 +7384,12 @@ async function runPull(args, io) {
|
|
|
6828
7384
|
includeManifest: true
|
|
6829
7385
|
});
|
|
6830
7386
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7387
|
+
const live = await fetchLiveDeploymentBestEffort(cfg, parsed.slug);
|
|
7388
|
+
if (live?.liveDeployment != null && (live.liveDeployment.buildStatus === "success" || live.lastSuccessfulDeployment != null)) {
|
|
7389
|
+
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}).`);
|
|
7390
|
+
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.`);
|
|
7391
|
+
return 1;
|
|
7392
|
+
}
|
|
6831
7393
|
io.err(`launchpad pull: no deployed manifest for "${parsed.slug}" yet — ` + `run \`launchpad deploy\` first, or check \`launchpad status\` for the apply state.`);
|
|
6832
7394
|
return 1;
|
|
6833
7395
|
}
|
|
@@ -6864,6 +7426,13 @@ async function runPull(args, io) {
|
|
|
6864
7426
|
return 1;
|
|
6865
7427
|
}
|
|
6866
7428
|
}
|
|
7429
|
+
async function fetchLiveDeploymentBestEffort(cfg, slug) {
|
|
7430
|
+
try {
|
|
7431
|
+
return await fetchDeploymentStatus(cfg, slug);
|
|
7432
|
+
} catch {
|
|
7433
|
+
return null;
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
6867
7436
|
function parseArgs8(args, cwd = process.cwd()) {
|
|
6868
7437
|
let slug = null;
|
|
6869
7438
|
let out = null;
|
|
@@ -6938,7 +7507,7 @@ function describe23(e) {
|
|
|
6938
7507
|
}
|
|
6939
7508
|
|
|
6940
7509
|
// src/commands/status.ts
|
|
6941
|
-
import { readFileSync as
|
|
7510
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
6942
7511
|
var statusCommand = {
|
|
6943
7512
|
name: "status",
|
|
6944
7513
|
summary: "show drift between local launchpad.yaml and deployed state",
|
|
@@ -6995,7 +7564,7 @@ async function runStatus(args, io) {
|
|
|
6995
7564
|
}
|
|
6996
7565
|
let localYaml;
|
|
6997
7566
|
try {
|
|
6998
|
-
localYaml =
|
|
7567
|
+
localYaml = readFileSync11(parsed.file, "utf8");
|
|
6999
7568
|
} catch (e) {
|
|
7000
7569
|
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
|
|
7001
7570
|
if (lifecycle !== null && lifecycle.state === "live") {
|
|
@@ -7005,8 +7574,8 @@ async function runStatus(args, io) {
|
|
|
7005
7574
|
}
|
|
7006
7575
|
let localObj;
|
|
7007
7576
|
try {
|
|
7008
|
-
const { parse:
|
|
7009
|
-
localObj =
|
|
7577
|
+
const { parse: parseYaml5 } = await import("yaml");
|
|
7578
|
+
localObj = parseYaml5(localYaml);
|
|
7010
7579
|
} catch (e) {
|
|
7011
7580
|
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
|
|
7012
7581
|
return 2;
|
|
@@ -7027,25 +7596,49 @@ async function runStatus(args, io) {
|
|
|
7027
7596
|
} catch (e) {
|
|
7028
7597
|
return mapBotError(e, parsed.slug, io);
|
|
7029
7598
|
}
|
|
7599
|
+
let deployment = null;
|
|
7600
|
+
let deploymentKnown = false;
|
|
7601
|
+
try {
|
|
7602
|
+
deployment = await fetchDeploymentStatus(cfg, parsed.slug);
|
|
7603
|
+
deploymentKnown = deployment !== null;
|
|
7604
|
+
} catch (e) {
|
|
7605
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7606
|
+
return mapBotError(e, parsed.slug, io);
|
|
7607
|
+
}
|
|
7608
|
+
io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
7609
|
+
}
|
|
7610
|
+
let standingExceptions = null;
|
|
7611
|
+
try {
|
|
7612
|
+
standingExceptions = await fetchStandingExceptions(cfg, parsed.slug);
|
|
7613
|
+
} catch (e) {
|
|
7614
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7615
|
+
return mapBotError(e, parsed.slug, io);
|
|
7616
|
+
}
|
|
7617
|
+
io.err(`launchpad status: standing-exception inventory unavailable (${describe24(e)}).`);
|
|
7618
|
+
}
|
|
7030
7619
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7620
|
+
const live = deployment?.liveDeployment ?? null;
|
|
7621
|
+
const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
7031
7622
|
const liveButEmpty = lifecycle !== null && lifecycle.state === "live";
|
|
7032
7623
|
const out = {
|
|
7033
|
-
state: liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7624
|
+
state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7034
7625
|
slug: parsed.slug,
|
|
7035
7626
|
deployedSha: state.lastAppliedManifestSha,
|
|
7036
7627
|
headSha: state.appRepoHeadSha,
|
|
7037
7628
|
hasOpenPr: state.openPr !== null,
|
|
7038
7629
|
openPrNumber: state.openPr?.number ?? null,
|
|
7039
7630
|
driftFields: [],
|
|
7040
|
-
driftDetails: []
|
|
7631
|
+
driftDetails: [],
|
|
7632
|
+
...deploymentKnown ? { deployment } : {},
|
|
7633
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
7041
7634
|
};
|
|
7042
7635
|
emit3(out, parsed.json, io);
|
|
7043
7636
|
return 0;
|
|
7044
7637
|
}
|
|
7045
7638
|
let deployedObj;
|
|
7046
7639
|
try {
|
|
7047
|
-
const { parse:
|
|
7048
|
-
deployedObj =
|
|
7640
|
+
const { parse: parseYaml5 } = await import("yaml");
|
|
7641
|
+
deployedObj = parseYaml5(state.manifestYaml);
|
|
7049
7642
|
} catch (e) {
|
|
7050
7643
|
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
|
|
7051
7644
|
return 2;
|
|
@@ -7068,7 +7661,9 @@ async function runStatus(args, io) {
|
|
|
7068
7661
|
hasOpenPr: state.openPr !== null,
|
|
7069
7662
|
openPrNumber: state.openPr?.number ?? null,
|
|
7070
7663
|
driftFields: drift.map((d) => d.path),
|
|
7071
|
-
driftDetails: drift
|
|
7664
|
+
driftDetails: drift,
|
|
7665
|
+
...deploymentKnown ? { deployment } : {},
|
|
7666
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
7072
7667
|
};
|
|
7073
7668
|
emit3(result, parsed.json, io);
|
|
7074
7669
|
if (result.state === "drift" && parsed.strict) {
|
|
@@ -7154,14 +7749,26 @@ function emit3(out, asJson, io) {
|
|
|
7154
7749
|
case "live_no_content":
|
|
7155
7750
|
io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
|
|
7156
7751
|
surfaceHeadVsDeployed(out, io);
|
|
7752
|
+
surfaceDeployment(out, io);
|
|
7753
|
+
surfaceExceptions(out, io);
|
|
7157
7754
|
return;
|
|
7158
7755
|
case "no_deployed_manifest":
|
|
7159
7756
|
io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
|
|
7160
7757
|
surfaceHeadVsDeployed(out, io);
|
|
7758
|
+
surfaceDeployment(out, io);
|
|
7759
|
+
surfaceExceptions(out, io);
|
|
7760
|
+
return;
|
|
7761
|
+
case "live_content_untracked":
|
|
7762
|
+
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
7763
|
+
surfaceHeadVsDeployed(out, io);
|
|
7764
|
+
surfaceDeployment(out, io);
|
|
7765
|
+
surfaceExceptions(out, io);
|
|
7161
7766
|
return;
|
|
7162
7767
|
case "in_sync":
|
|
7163
7768
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
7164
7769
|
surfaceHeadVsDeployed(out, io);
|
|
7770
|
+
surfaceDeployment(out, io);
|
|
7771
|
+
surfaceExceptions(out, io);
|
|
7165
7772
|
return;
|
|
7166
7773
|
case "drift":
|
|
7167
7774
|
io.out(`${out.slug}: live, drift: ${out.driftFields.join(", ")}`);
|
|
@@ -7171,8 +7778,66 @@ function emit3(out, asJson, io) {
|
|
|
7171
7778
|
io.out(` deployed: ${formatValue(d.deployed)}`);
|
|
7172
7779
|
}
|
|
7173
7780
|
surfaceHeadVsDeployed(out, io);
|
|
7781
|
+
surfaceDeployment(out, io);
|
|
7782
|
+
surfaceExceptions(out, io);
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
function triggerLabel(trigger) {
|
|
7787
|
+
if (trigger === "git-push")
|
|
7788
|
+
return "git push";
|
|
7789
|
+
if (trigger === "bot")
|
|
7790
|
+
return "launchpad deploy (bot)";
|
|
7791
|
+
return "an unknown mechanism";
|
|
7792
|
+
}
|
|
7793
|
+
function surfaceDeployment(out, io) {
|
|
7794
|
+
const dep = out.deployment;
|
|
7795
|
+
if (dep === undefined || dep === null || !dep.supported)
|
|
7796
|
+
return;
|
|
7797
|
+
const live = dep.liveDeployment;
|
|
7798
|
+
if (live === null) {
|
|
7799
|
+
io.out(" last deployment: none — Cloudflare Pages has no production deployment yet.");
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const commit = live.commit !== undefined ? `, commit ${live.commit.hash.slice(0, 7)}` : "";
|
|
7803
|
+
const via = triggerLabel(live.trigger);
|
|
7804
|
+
switch (live.buildStatus) {
|
|
7805
|
+
case "success":
|
|
7806
|
+
io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
|
|
7807
|
+
return;
|
|
7808
|
+
case "in_progress":
|
|
7809
|
+
io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
|
|
7810
|
+
io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
|
|
7811
|
+
return;
|
|
7812
|
+
case "failure": {
|
|
7813
|
+
const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
|
|
7814
|
+
io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
|
|
7815
|
+
if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
|
|
7816
|
+
io.out(" build log (excerpt):");
|
|
7817
|
+
for (const line of live.logExcerpt.split(`
|
|
7818
|
+
`)) {
|
|
7819
|
+
io.out(` ${line}`);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
if (dep.lastSuccessfulDeployment !== null) {
|
|
7823
|
+
io.out(` serving: previous successful deployment from ${dep.lastSuccessfulDeployment.createdOn}`);
|
|
7824
|
+
} else {
|
|
7825
|
+
io.out(" serving: nothing — no successful deployment exists yet.");
|
|
7826
|
+
}
|
|
7827
|
+
io.out(" next: fix the build locally (check build.command / build.root_dir in launchpad.yaml), then run `launchpad deploy`.");
|
|
7174
7828
|
return;
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
function surfaceExceptions(out, io) {
|
|
7833
|
+
const exceptions = out.standingExceptions;
|
|
7834
|
+
if (exceptions === undefined || exceptions.length === 0)
|
|
7835
|
+
return;
|
|
7836
|
+
io.out(` standing exceptions: ${exceptions.length} policy violation(s) in content already live on main (non-blocking):`);
|
|
7837
|
+
for (const e of exceptions) {
|
|
7838
|
+
io.out(` - ${e.path} [${e.rule}]`);
|
|
7175
7839
|
}
|
|
7840
|
+
io.out(" these never block a deploy that doesn't change them, but they are never grandfathered silently — clean them up in a future deploy.");
|
|
7176
7841
|
}
|
|
7177
7842
|
function surfaceHeadVsDeployed(out, io) {
|
|
7178
7843
|
if (out.headSha !== null && out.deployedSha !== null && out.headSha !== out.deployedSha) {
|
|
@@ -7451,7 +8116,7 @@ function describe25(e) {
|
|
|
7451
8116
|
}
|
|
7452
8117
|
|
|
7453
8118
|
// src/deploy/rollback.ts
|
|
7454
|
-
import { existsSync as
|
|
8119
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7455
8120
|
import { resolve as resolvePath2 } from "node:path";
|
|
7456
8121
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
7457
8122
|
|
|
@@ -7571,15 +8236,16 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
7571
8236
|
file: opts.file,
|
|
7572
8237
|
platformRepo: null,
|
|
7573
8238
|
rePin: false,
|
|
8239
|
+
atSha: verifiedSha,
|
|
7574
8240
|
yes: true
|
|
7575
8241
|
}, io, applyDeps);
|
|
7576
8242
|
}
|
|
7577
8243
|
function readCurrentManifest(path11) {
|
|
7578
|
-
if (!
|
|
8244
|
+
if (!existsSync7(path11))
|
|
7579
8245
|
return null;
|
|
7580
8246
|
let raw;
|
|
7581
8247
|
try {
|
|
7582
|
-
raw =
|
|
8248
|
+
raw = readFileSync12(path11, "utf8");
|
|
7583
8249
|
} catch {
|
|
7584
8250
|
return null;
|
|
7585
8251
|
}
|
|
@@ -7735,7 +8401,7 @@ function parseArgs11(args) {
|
|
|
7735
8401
|
}
|
|
7736
8402
|
|
|
7737
8403
|
// src/secrets/push.ts
|
|
7738
|
-
import { existsSync as
|
|
8404
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13 } from "node:fs";
|
|
7739
8405
|
import { resolve as resolvePath3 } from "node:path";
|
|
7740
8406
|
|
|
7741
8407
|
// src/secrets/env-parse.ts
|
|
@@ -7790,14 +8456,14 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
7790
8456
|
return 0;
|
|
7791
8457
|
}
|
|
7792
8458
|
const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
|
|
7793
|
-
if (!
|
|
8459
|
+
if (!existsSync8(envPath)) {
|
|
7794
8460
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
7795
8461
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
7796
8462
|
return 2;
|
|
7797
8463
|
}
|
|
7798
8464
|
let envText;
|
|
7799
8465
|
try {
|
|
7800
|
-
envText =
|
|
8466
|
+
envText = readFileSync13(envPath, "utf8");
|
|
7801
8467
|
} catch (e) {
|
|
7802
8468
|
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
|
|
7803
8469
|
return 2;
|
|
@@ -8069,9 +8735,9 @@ function renderManifestError5(result, io) {
|
|
|
8069
8735
|
}
|
|
8070
8736
|
|
|
8071
8737
|
// src/secrets/set.ts
|
|
8072
|
-
import { existsSync as
|
|
8738
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
|
|
8073
8739
|
import { resolve as resolvePath5 } from "node:path";
|
|
8074
|
-
import { parse as
|
|
8740
|
+
import { parse as parseYaml5 } from "yaml";
|
|
8075
8741
|
var CELL_LABEL2 = {
|
|
8076
8742
|
present: "PRESENT",
|
|
8077
8743
|
missing: "MISSING",
|
|
@@ -8079,14 +8745,14 @@ var CELL_LABEL2 = {
|
|
|
8079
8745
|
};
|
|
8080
8746
|
function loadSet(fleetFile, setName, io) {
|
|
8081
8747
|
const path11 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
8082
|
-
if (!
|
|
8748
|
+
if (!existsSync9(path11)) {
|
|
8083
8749
|
io.err(`✗ ${path11}`);
|
|
8084
8750
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
8085
8751
|
return 2;
|
|
8086
8752
|
}
|
|
8087
8753
|
let obj;
|
|
8088
8754
|
try {
|
|
8089
|
-
obj =
|
|
8755
|
+
obj = parseYaml5(readFileSync14(path11, "utf8"));
|
|
8090
8756
|
} catch (e) {
|
|
8091
8757
|
io.err(`✗ ${path11}`);
|
|
8092
8758
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -8171,14 +8837,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
8171
8837
|
if (typeof set === "number")
|
|
8172
8838
|
return set;
|
|
8173
8839
|
const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
|
|
8174
|
-
if (!
|
|
8840
|
+
if (!existsSync9(envPath)) {
|
|
8175
8841
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
8176
8842
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
8177
8843
|
return 2;
|
|
8178
8844
|
}
|
|
8179
8845
|
let envText;
|
|
8180
8846
|
try {
|
|
8181
|
-
envText =
|
|
8847
|
+
envText = readFileSync14(envPath, "utf8");
|
|
8182
8848
|
} catch (e) {
|
|
8183
8849
|
io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8184
8850
|
return 2;
|
|
@@ -8322,7 +8988,7 @@ function setPushExit(e) {
|
|
|
8322
8988
|
}
|
|
8323
8989
|
|
|
8324
8990
|
// src/commands/secrets-template.ts
|
|
8325
|
-
import { existsSync as
|
|
8991
|
+
import { existsSync as existsSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
8326
8992
|
import { resolve as resolve9 } from "node:path";
|
|
8327
8993
|
async function runSecretsTemplate(args, io) {
|
|
8328
8994
|
const flags = parseFlags4(args);
|
|
@@ -8346,7 +9012,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
8346
9012
|
return 0;
|
|
8347
9013
|
}
|
|
8348
9014
|
const outPath = resolve9(process.cwd(), flags.out);
|
|
8349
|
-
if (
|
|
9015
|
+
if (existsSync10(outPath) && !flags.force) {
|
|
8350
9016
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
8351
9017
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
8352
9018
|
return 64;
|
|
@@ -8634,8 +9300,8 @@ function printHelp2(io) {
|
|
|
8634
9300
|
|
|
8635
9301
|
// src/commands/skills.ts
|
|
8636
9302
|
import { fileURLToPath } from "node:url";
|
|
8637
|
-
import { dirname as dirname6, join as
|
|
8638
|
-
import { promises as fs5, existsSync as
|
|
9303
|
+
import { dirname as dirname6, join as join11, resolve as resolve10 } from "node:path";
|
|
9304
|
+
import { promises as fs5, existsSync as existsSync11 } from "node:fs";
|
|
8639
9305
|
import { homedir as homedir2 } from "node:os";
|
|
8640
9306
|
var BUNDLE_PREFIX = "launchpad-";
|
|
8641
9307
|
var BUNDLED_SKILLS = [
|
|
@@ -8696,7 +9362,7 @@ function printHelp3(io) {
|
|
|
8696
9362
|
}
|
|
8697
9363
|
function resolveInstallEnv() {
|
|
8698
9364
|
const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
|
|
8699
|
-
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
9365
|
+
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join11(homedir2(), ".claude", "skills");
|
|
8700
9366
|
return { bundleDir, userSkillsDir };
|
|
8701
9367
|
}
|
|
8702
9368
|
function defaultBundleDir() {
|
|
@@ -8706,7 +9372,7 @@ function defaultBundleDir() {
|
|
|
8706
9372
|
resolve10(here, "..", "..", "skills")
|
|
8707
9373
|
];
|
|
8708
9374
|
for (const c of candidates) {
|
|
8709
|
-
if (
|
|
9375
|
+
if (existsSync11(join11(c, "launchpad-onboard", "SKILL.md"))) {
|
|
8710
9376
|
return c;
|
|
8711
9377
|
}
|
|
8712
9378
|
}
|
|
@@ -8727,12 +9393,12 @@ async function doInstall(io) {
|
|
|
8727
9393
|
}
|
|
8728
9394
|
let installed = 0;
|
|
8729
9395
|
for (const skill of BUNDLED_SKILLS) {
|
|
8730
|
-
const src =
|
|
9396
|
+
const src = join11(env.bundleDir, skill);
|
|
8731
9397
|
if (!await isDir(src)) {
|
|
8732
9398
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
8733
9399
|
return 1;
|
|
8734
9400
|
}
|
|
8735
|
-
const dest =
|
|
9401
|
+
const dest = join11(env.userSkillsDir, skill);
|
|
8736
9402
|
await fs5.rm(dest, { recursive: true, force: true });
|
|
8737
9403
|
await fs5.cp(src, dest, { recursive: true });
|
|
8738
9404
|
installed++;
|
|
@@ -8757,7 +9423,7 @@ async function doUninstall(io) {
|
|
|
8757
9423
|
continue;
|
|
8758
9424
|
if (!isBundleManaged(entry.name))
|
|
8759
9425
|
continue;
|
|
8760
|
-
const target =
|
|
9426
|
+
const target = join11(env.userSkillsDir, entry.name);
|
|
8761
9427
|
await fs5.rm(target, { recursive: true, force: true });
|
|
8762
9428
|
removed++;
|
|
8763
9429
|
io.out(`✗ removed ${target}`);
|
|
@@ -8780,7 +9446,7 @@ async function doList(io) {
|
|
|
8780
9446
|
}
|
|
8781
9447
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
8782
9448
|
for (const name of managedDirs) {
|
|
8783
|
-
const skillFile =
|
|
9449
|
+
const skillFile = join11(env.userSkillsDir, name, "SKILL.md");
|
|
8784
9450
|
const version = await readVersion(skillFile);
|
|
8785
9451
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
8786
9452
|
}
|
|
@@ -8815,9 +9481,9 @@ function describe28(e) {
|
|
|
8815
9481
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
8816
9482
|
import { promisify } from "node:util";
|
|
8817
9483
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8818
|
-
import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as
|
|
9484
|
+
import { dirname as dirname7, resolve as resolve11, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
|
|
8819
9485
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
8820
|
-
import { readFileSync as
|
|
9486
|
+
import { readFileSync as readFileSync15, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
8821
9487
|
|
|
8822
9488
|
// src/commands/channel-auth.ts
|
|
8823
9489
|
import { createServer as createServer2 } from "node:http";
|
|
@@ -8938,7 +9604,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
8938
9604
|
var REGISTRY = "https://registry.npmjs.org";
|
|
8939
9605
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
8940
9606
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
8941
|
-
var CHANNEL_MARKER =
|
|
9607
|
+
var CHANNEL_MARKER = join12(homedir3(), ".launchpad", "channel");
|
|
8942
9608
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
8943
9609
|
var UPGRADE_ARGS = {
|
|
8944
9610
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -9021,8 +9687,8 @@ async function openSystemBrowser(url) {
|
|
|
9021
9687
|
await execFileAsync(opener, [url]);
|
|
9022
9688
|
}
|
|
9023
9689
|
async function runInstallerScript(script) {
|
|
9024
|
-
const dir = mkdtempSync(
|
|
9025
|
-
const file =
|
|
9690
|
+
const dir = mkdtempSync(join12(tmpdir(), "launchpad-update-"));
|
|
9691
|
+
const file = join12(dir, "install.sh");
|
|
9026
9692
|
try {
|
|
9027
9693
|
writeFileSync7(file, script, { mode: 448 });
|
|
9028
9694
|
return await new Promise((resolvePromise) => {
|
|
@@ -9046,7 +9712,7 @@ function resolveLatestVersion() {
|
|
|
9046
9712
|
}
|
|
9047
9713
|
function detectInstallChannel() {
|
|
9048
9714
|
try {
|
|
9049
|
-
return
|
|
9715
|
+
return readFileSync15(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
|
|
9050
9716
|
} catch {
|
|
9051
9717
|
return "github";
|
|
9052
9718
|
}
|
|
@@ -9253,7 +9919,8 @@ function printHelp4(io) {
|
|
|
9253
9919
|
}
|
|
9254
9920
|
|
|
9255
9921
|
// src/commands/validate.ts
|
|
9256
|
-
import {
|
|
9922
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
9923
|
+
import { dirname as dirname8, resolve as resolve12 } from "node:path";
|
|
9257
9924
|
var validateCommand = {
|
|
9258
9925
|
name: "validate",
|
|
9259
9926
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -9272,9 +9939,31 @@ async function runValidate(args, io) {
|
|
|
9272
9939
|
if (result.kind !== "ok") {
|
|
9273
9940
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
9274
9941
|
}
|
|
9942
|
+
const boundary = checkBoundary(path11, result.manifest.app !== undefined);
|
|
9275
9943
|
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
|
|
9276
|
-
return json ? renderJsonOk(result, groupCheck, io) : renderHumanOk(result, groupCheck, io);
|
|
9944
|
+
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
9945
|
+
}
|
|
9946
|
+
function checkBoundary(manifestPath, declared) {
|
|
9947
|
+
const dir = dirname8(manifestPath);
|
|
9948
|
+
let files;
|
|
9949
|
+
try {
|
|
9950
|
+
files = walkCwd(dir).files;
|
|
9951
|
+
} catch {
|
|
9952
|
+
return { declared, errors: [], warnings: [] };
|
|
9953
|
+
}
|
|
9954
|
+
let manifestYaml;
|
|
9955
|
+
try {
|
|
9956
|
+
manifestYaml = readFileSync16(manifestPath, "utf8");
|
|
9957
|
+
} catch {
|
|
9958
|
+
manifestYaml = null;
|
|
9959
|
+
}
|
|
9960
|
+
const r = applyBoundaryToFiles({ cwd: dir, manifestYaml, files });
|
|
9961
|
+
if (r.kind === "error") {
|
|
9962
|
+
return { declared, errors: [r.message], warnings: [] };
|
|
9963
|
+
}
|
|
9964
|
+
return { declared, errors: [], warnings: r.warnings };
|
|
9277
9965
|
}
|
|
9966
|
+
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
9967
|
async function checkGroup(allowedGroup, cfg) {
|
|
9279
9968
|
try {
|
|
9280
9969
|
const r = await fetchGroups(cfg);
|
|
@@ -9321,10 +10010,22 @@ async function checkGroups(groups) {
|
|
|
9321
10010
|
const ok = perGroup.every((r) => r.kind === "ok");
|
|
9322
10011
|
return ok ? { kind: "all-ok", perGroup } : { kind: "some-failed", perGroup };
|
|
9323
10012
|
}
|
|
9324
|
-
function renderHumanOk(result, group, io) {
|
|
10013
|
+
function renderHumanOk(result, group, boundary, io) {
|
|
9325
10014
|
io.out(`✓ ${result.manifest.metadata.name} — manifest is valid`);
|
|
9326
10015
|
io.out(` apiVersion: launchpad.m-kopa.us/v1alpha1`);
|
|
9327
10016
|
io.out(` type: ${result.manifest.deployment.type}`);
|
|
10017
|
+
if (boundary.errors.length > 0) {
|
|
10018
|
+
for (const e of boundary.errors) {
|
|
10019
|
+
io.err(`✗ app boundary: ${e}`);
|
|
10020
|
+
}
|
|
10021
|
+
return 1;
|
|
10022
|
+
}
|
|
10023
|
+
for (const w of boundary.warnings) {
|
|
10024
|
+
io.out(`⚠ app boundary: ${w}`);
|
|
10025
|
+
}
|
|
10026
|
+
if (!boundary.declared) {
|
|
10027
|
+
io.out(`⚠ ${DECLARE_APP_HINT}`);
|
|
10028
|
+
}
|
|
9328
10029
|
switch (group.kind) {
|
|
9329
10030
|
case "skipped":
|
|
9330
10031
|
return 0;
|
|
@@ -9388,11 +10089,20 @@ function renderHumanError(result, io) {
|
|
|
9388
10089
|
return 1;
|
|
9389
10090
|
}
|
|
9390
10091
|
}
|
|
9391
|
-
function renderJsonOk(result, group, io) {
|
|
10092
|
+
function renderJsonOk(result, group, boundary, io) {
|
|
9392
10093
|
const base = {
|
|
9393
10094
|
name: result.manifest.metadata.name,
|
|
9394
|
-
deploymentType: result.manifest.deployment.type
|
|
10095
|
+
deploymentType: result.manifest.deployment.type,
|
|
10096
|
+
appBoundary: {
|
|
10097
|
+
declared: boundary.declared,
|
|
10098
|
+
errors: boundary.errors,
|
|
10099
|
+
warnings: boundary.warnings
|
|
10100
|
+
}
|
|
9395
10101
|
};
|
|
10102
|
+
if (boundary.errors.length > 0) {
|
|
10103
|
+
io.out(JSON.stringify({ ok: false, ...base }));
|
|
10104
|
+
return 1;
|
|
10105
|
+
}
|
|
9396
10106
|
switch (group.kind) {
|
|
9397
10107
|
case "skipped":
|
|
9398
10108
|
io.out(JSON.stringify({ ok: true, ...base }));
|
|
@@ -9557,15 +10267,15 @@ function describe30(e) {
|
|
|
9557
10267
|
// src/update-notifier.ts
|
|
9558
10268
|
import { spawn as spawn6 } from "node:child_process";
|
|
9559
10269
|
import { homedir as homedir4 } from "node:os";
|
|
9560
|
-
import { join as
|
|
9561
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
10270
|
+
import { join as join13 } from "node:path";
|
|
10271
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
|
|
9562
10272
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
9563
10273
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9564
10274
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
9565
|
-
var CACHE_FILE =
|
|
10275
|
+
var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
|
|
9566
10276
|
function readCache2() {
|
|
9567
10277
|
try {
|
|
9568
|
-
const raw = JSON.parse(
|
|
10278
|
+
const raw = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
|
|
9569
10279
|
if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
|
|
9570
10280
|
const latest = raw.latest;
|
|
9571
10281
|
return {
|
|
@@ -9578,7 +10288,7 @@ function readCache2() {
|
|
|
9578
10288
|
}
|
|
9579
10289
|
function writeCache2(state) {
|
|
9580
10290
|
try {
|
|
9581
|
-
mkdirSync3(
|
|
10291
|
+
mkdirSync3(join13(homedir4(), ".launchpad"), { recursive: true });
|
|
9582
10292
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
9583
10293
|
`, { mode: 384 });
|
|
9584
10294
|
} catch {}
|