@m-kopa/launchpad-cli 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/dist/cli.js +62 -66
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/deploy/access-drift-offer.d.ts +12 -16
- package/dist/deploy/access-drift-offer.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +15 -14
- package/skills/launchpad-deploy/SKILL.md +15 -13
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +13 -14
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
|
|
7
7
|
pre-1.0 minor bumps may carry breaking changes per ADR 0005.
|
|
8
8
|
|
|
9
|
+
## 0.48.0 — 2026-07-08
|
|
10
|
+
|
|
11
|
+
**One-command access reconcile — `launchpad deploy` now applies access changes
|
|
12
|
+
automatically (PS-2117 / ADR 0034).** Plain `launchpad deploy` reconciles both
|
|
13
|
+
content and access in one command: when it detects that your manifest's
|
|
14
|
+
`allowed_entra_groups` differ from what the gateway enforces, it no longer
|
|
15
|
+
stops at a message or asks — it routes straight into the gated apply flow
|
|
16
|
+
(opens the bot's `[launchpad] apply <slug>` PR, which auto-merges on its
|
|
17
|
+
non-bypassable automated checks). No `--apply` verb, no y/N prompt, no human
|
|
18
|
+
approval. This supersedes the 0.47 interactive inline offer (PS-2071); `--apply`
|
|
19
|
+
remains a **hidden alias for one release** and will be removed.
|
|
20
|
+
|
|
21
|
+
The platform no longer **blocks** an access change — it is self-serve and
|
|
22
|
+
owner-accountable (ADR 0034). A *dangerous* shape (a widening on a `sensitive`
|
|
23
|
+
app, or a broad-group grant like `S_ALL_Employees`) fires an **after-the-fact,
|
|
24
|
+
non-blocking** Product Security alert instead. The former two-person
|
|
25
|
+
`allowed-groups-change` label gate is **retired**; the byte-equal validator +
|
|
26
|
+
per-app `tf plan` are the non-bypassable trust root.
|
|
27
|
+
|
|
28
|
+
New: `launchpad.yaml` supports a self-declared `sensitive: true` flag (drives
|
|
29
|
+
the alert). To revert a bad access change fast, `launchpad rollback <prior-sha>`
|
|
30
|
+
re-applies the previous manifest through the same gated flow. The honest-exit
|
|
31
|
+
behaviour from 0.47.1 is preserved (a non-applied reconcile never reports
|
|
32
|
+
success).
|
|
33
|
+
|
|
34
|
+
## 0.47.2 — 2026-07-08
|
|
35
|
+
|
|
36
|
+
**Fix: `launchpad deploy` no longer leaves you falsely "stale" after your own
|
|
37
|
+
deploys (PS-2119).** The clone staleness guard compares `.launchpad/base-sha`
|
|
38
|
+
against managed `main` head, but the marker only ever advanced on
|
|
39
|
+
`--rebase-onto-head` — a normal deploy left it frozen at clone time. So each
|
|
40
|
+
deploy you made moved `main` forward while your marker stayed put, and the next
|
|
41
|
+
deploy re-warned "a teammate may have shipped changes you don't have" (and could
|
|
42
|
+
BLOCK), when the only "teammate" was your own prior commits. `launchpad deploy`
|
|
43
|
+
now re-pins the marker after a successful deploy — to the new commit (committed)
|
|
44
|
+
or to managed main head (nothing-to-deploy). It only re-stamps a marker that
|
|
45
|
+
already exists (a git-less `launchpad init` tree gains nothing), and a stamp
|
|
46
|
+
failure never fails the deploy. If you were stuck on this, a single
|
|
47
|
+
`launchpad update` + `launchpad deploy` clears it — no re-clone.
|
|
48
|
+
|
|
49
|
+
(Companion bot-side fix PS-2120, shipped separately: co-owners were wrongly 403'd
|
|
50
|
+
by `launchpad status` / hidden from `launchpad apps` on their own apps — several
|
|
51
|
+
read endpoints resolved the role without `coOwners`. Now co-owner-aware.)
|
|
52
|
+
|
|
9
53
|
## 0.47.1 — 2026-07-08
|
|
10
54
|
|
|
11
55
|
**Fix: no false-success exit code when the inline access-drift apply is aborted
|
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.48.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -2225,7 +2225,8 @@ var ManifestSchema = z.object({
|
|
|
2225
2225
|
app: AppBoundarySchema.optional(),
|
|
2226
2226
|
verify: VerifySchema.optional(),
|
|
2227
2227
|
confine_origin: z.boolean().optional(),
|
|
2228
|
-
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional()
|
|
2228
|
+
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
|
|
2229
|
+
sensitive: z.boolean().optional()
|
|
2229
2230
|
}).strict().superRefine((m, ctx) => {
|
|
2230
2231
|
const isContainer = m.deployment.type === "container";
|
|
2231
2232
|
if (m.confine_origin !== undefined) {
|
|
@@ -2842,6 +2843,7 @@ var SpecSchema = z2.object({
|
|
|
2842
2843
|
app: AppBoundarySchema.optional(),
|
|
2843
2844
|
verify: VerifySchema.optional(),
|
|
2844
2845
|
confine_origin: z2.boolean().optional(),
|
|
2846
|
+
sensitive: z2.boolean().optional(),
|
|
2845
2847
|
catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
|
|
2846
2848
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
2847
2849
|
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
@@ -3396,6 +3398,9 @@ var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
|
3396
3398
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
3397
3399
|
// ../launchpad-engine/dist/access-drift.js
|
|
3398
3400
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3401
|
+
function isUuid(s) {
|
|
3402
|
+
return UUID_RE.test(s.trim());
|
|
3403
|
+
}
|
|
3399
3404
|
function canonicalUuid(s) {
|
|
3400
3405
|
const t = s.trim();
|
|
3401
3406
|
return UUID_RE.test(t) ? t.toLowerCase() : null;
|
|
@@ -3430,6 +3435,29 @@ function detectAccessDrift(declaredTokens, enforcedTokens, resolve6) {
|
|
|
3430
3435
|
inSync: manifestOnly.length === 0 && liveOnly.length === 0 && unresolved.length === 0
|
|
3431
3436
|
};
|
|
3432
3437
|
}
|
|
3438
|
+
// ../launchpad-engine/dist/broad-groups.js
|
|
3439
|
+
var BROAD_GROUPS = [
|
|
3440
|
+
{
|
|
3441
|
+
uuid: "caf78902-35dd-4833-88ce-8f067eda6036",
|
|
3442
|
+
name: "S_ALL_Employees",
|
|
3443
|
+
note: "All employees — the broadest grant on the tenant; assigned to the Launchpad enterprise app. Granting this reaches the entire company."
|
|
3444
|
+
}
|
|
3445
|
+
];
|
|
3446
|
+
function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
|
|
3447
|
+
const seen = new Set;
|
|
3448
|
+
for (const g of list) {
|
|
3449
|
+
const key = canonicalUuid(g.uuid);
|
|
3450
|
+
if (key === null || !isUuid(g.uuid)) {
|
|
3451
|
+
throw new Error(`broad-groups: entry '${g.name}' has a malformed UUID: '${g.uuid}'`);
|
|
3452
|
+
}
|
|
3453
|
+
if (seen.has(key)) {
|
|
3454
|
+
throw new Error(`broad-groups: duplicate UUID for '${g.name}': '${g.uuid}'`);
|
|
3455
|
+
}
|
|
3456
|
+
seen.add(key);
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
assertBroadGroupsWellFormed();
|
|
3460
|
+
var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
|
|
3433
3461
|
// ../launchpad-engine/dist/fleet-manifest.js
|
|
3434
3462
|
import { z as z3 } from "zod";
|
|
3435
3463
|
var UUID_RE2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
@@ -6561,54 +6589,28 @@ function renderManifestError(loaded, io) {
|
|
|
6561
6589
|
// src/deploy/access-drift-offer.ts
|
|
6562
6590
|
var asStrings = (v) => Array.isArray(v) ? v.map((x) => String(x)) : [];
|
|
6563
6591
|
function renderAccessDriftSummary(slug, body) {
|
|
6564
|
-
const lines = [`launchpad deploy:
|
|
6592
|
+
const lines = [`launchpad deploy: access change detected on "${slug}".`];
|
|
6565
6593
|
const liveOnly = asStrings(body.live_only);
|
|
6566
6594
|
const manifestOnly = asStrings(body.manifest_only);
|
|
6567
6595
|
if (liveOnly.length > 0) {
|
|
6568
|
-
lines.push(` enforced but NOT declared (
|
|
6596
|
+
lines.push(` enforced but NOT declared (will be removed): ${liveOnly.join(", ")}`);
|
|
6569
6597
|
}
|
|
6570
6598
|
if (manifestOnly.length > 0) {
|
|
6571
|
-
lines.push(` declared but NOT enforced: ${manifestOnly.join(", ")}`);
|
|
6599
|
+
lines.push(` declared but NOT enforced (will be granted): ${manifestOnly.join(", ")}`);
|
|
6572
6600
|
}
|
|
6573
6601
|
lines.push("");
|
|
6574
|
-
lines.push(" Your manifest's access groups
|
|
6575
|
-
lines.push("
|
|
6602
|
+
lines.push(" Your manifest's access groups differ from what the gateway enforces —");
|
|
6603
|
+
lines.push(" reconciling automatically via the gated Terraform apply (no second command needed).");
|
|
6576
6604
|
return lines;
|
|
6577
6605
|
}
|
|
6578
|
-
function renderManualApplyGuidance() {
|
|
6579
|
-
return [
|
|
6580
|
-
" Run `launchpad deploy --apply` to reconcile — it opens the gated TF PR",
|
|
6581
|
-
" (an access-widening change needs the allowed-groups-change label from a",
|
|
6582
|
-
" second person) and waits for the apply verdict. Then redeploy your content.",
|
|
6583
|
-
" Nothing was committed by this attempt."
|
|
6584
|
-
];
|
|
6585
|
-
}
|
|
6586
|
-
function isYes(answer) {
|
|
6587
|
-
const a = answer.trim().toLowerCase();
|
|
6588
|
-
return a === "y" || a === "yes";
|
|
6589
|
-
}
|
|
6590
6606
|
async function handleAccessDrift(params) {
|
|
6591
6607
|
const { slug, manifestPath, body, io, deps } = params;
|
|
6592
6608
|
for (const line of renderAccessDriftSummary(slug, body))
|
|
6593
6609
|
io.err(line);
|
|
6594
|
-
if (!deps.interactive) {
|
|
6595
|
-
for (const line of renderManualApplyGuidance())
|
|
6596
|
-
io.err(line);
|
|
6597
|
-
return 1;
|
|
6598
|
-
}
|
|
6599
|
-
io.err("");
|
|
6600
|
-
const answer = await deps.prompt("Reconcile access now via the gated apply PR? You'll review the exact plan next. [y/N] ");
|
|
6601
|
-
if (!isYes(answer)) {
|
|
6602
|
-
io.err("");
|
|
6603
|
-
io.err("Skipped — no access change was made.");
|
|
6604
|
-
for (const line of renderManualApplyGuidance())
|
|
6605
|
-
io.err(line);
|
|
6606
|
-
return 1;
|
|
6607
|
-
}
|
|
6608
6610
|
io.out("");
|
|
6609
|
-
io.out("Reconciling access via
|
|
6611
|
+
io.out("Reconciling access automatically via the gated apply PR (no second command needed) …");
|
|
6610
6612
|
let applied = false;
|
|
6611
|
-
const exit = await deps.runApply({ file: manifestPath, platformRepo: null, rePin: false, yes:
|
|
6613
|
+
const exit = await deps.runApply({ file: manifestPath, platformRepo: null, rePin: false, yes: true }, io, { onApplied: () => {
|
|
6612
6614
|
applied = true;
|
|
6613
6615
|
} });
|
|
6614
6616
|
if (applied) {
|
|
@@ -6619,20 +6621,6 @@ async function handleAccessDrift(params) {
|
|
|
6619
6621
|
return exit === 0 ? 1 : exit;
|
|
6620
6622
|
}
|
|
6621
6623
|
|
|
6622
|
-
// src/io/prompt.ts
|
|
6623
|
-
function isInteractive() {
|
|
6624
|
-
return Boolean(process.stdin.isTTY);
|
|
6625
|
-
}
|
|
6626
|
-
async function defaultPrompt2(question) {
|
|
6627
|
-
const { createInterface } = await import("node:readline/promises");
|
|
6628
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6629
|
-
try {
|
|
6630
|
-
return await rl.question(question);
|
|
6631
|
-
} finally {
|
|
6632
|
-
rl.close();
|
|
6633
|
-
}
|
|
6634
|
-
}
|
|
6635
|
-
|
|
6636
6624
|
// src/deploy/dry-run.ts
|
|
6637
6625
|
import { execFileSync } from "node:child_process";
|
|
6638
6626
|
import { resolve as resolve7 } from "node:path";
|
|
@@ -7188,9 +7176,8 @@ function deployUsage() {
|
|
|
7188
7176
|
` + `manifest-driven dry-run (read-only preview):
|
|
7189
7177
|
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
7190
7178
|
` + `
|
|
7191
|
-
` + `manifest
|
|
7192
|
-
` +
|
|
7193
|
-
` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
|
|
7179
|
+
` + "access changes reconcile automatically (ADR 0034): plain `launchpad deploy`\n" + `detects a group change vs the deployed manifest and applies it via the gated
|
|
7180
|
+
` + "TF route — no separate verb. (`--apply` remains a hidden alias for one release.)";
|
|
7194
7181
|
}
|
|
7195
7182
|
function parseArgs3(args) {
|
|
7196
7183
|
let message = null;
|
|
@@ -7284,7 +7271,7 @@ function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
|
7284
7271
|
if (adw.manifestOnly !== undefined && adw.manifestOnly.length > 0) {
|
|
7285
7272
|
io.err(` declared but NOT enforced: ${adw.manifestOnly.join(", ")}`);
|
|
7286
7273
|
}
|
|
7287
|
-
io.err(`
|
|
7274
|
+
io.err(` re-run \`launchpad deploy\` to reconcile — it applies an access change automatically via the gated TF route (ADR 0034); no separate verb needed.`);
|
|
7288
7275
|
}
|
|
7289
7276
|
}
|
|
7290
7277
|
function isStaleBlock(body) {
|
|
@@ -7358,6 +7345,17 @@ async function surfaceManifestDiff(cfg, slug, manifestPath, io) {
|
|
|
7358
7345
|
for (const line of renderManifestDiffLines(diff))
|
|
7359
7346
|
io.out(line);
|
|
7360
7347
|
}
|
|
7348
|
+
async function advanceBaseShaMarker(cwd, sha, io) {
|
|
7349
|
+
try {
|
|
7350
|
+
if (typeof sha !== "string" || !isValidBaseSha(sha))
|
|
7351
|
+
return;
|
|
7352
|
+
if (await readBaseShaMarker(cwd) === null)
|
|
7353
|
+
return;
|
|
7354
|
+
await writeBaseShaMarker(cwd, sha);
|
|
7355
|
+
} catch (e) {
|
|
7356
|
+
io.err(`warning: couldn't update the local base-sha marker (${describe15(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7361
7359
|
async function runModelADeploy(args) {
|
|
7362
7360
|
const { cwd, manifestPath, io } = args;
|
|
7363
7361
|
let slug;
|
|
@@ -7484,11 +7482,7 @@ async function runModelADeploy(args) {
|
|
|
7484
7482
|
manifestPath,
|
|
7485
7483
|
body,
|
|
7486
7484
|
io,
|
|
7487
|
-
deps: {
|
|
7488
|
-
interactive: isInteractive(),
|
|
7489
|
-
prompt: defaultPrompt2,
|
|
7490
|
-
runApply: runDeployApply
|
|
7491
|
-
}
|
|
7485
|
+
deps: { runApply: runDeployApply }
|
|
7492
7486
|
});
|
|
7493
7487
|
}
|
|
7494
7488
|
io.err(`launchpad deploy: bot rejected the upload (HTTP ${result.status}, ${errorCode}).`);
|
|
@@ -7525,6 +7519,7 @@ async function runModelADeploy(args) {
|
|
|
7525
7519
|
io.out(` (managed main @ ${success.head_sha.slice(0, 8)} matches the bundle)`);
|
|
7526
7520
|
}
|
|
7527
7521
|
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
7522
|
+
await advanceBaseShaMarker(cwd, success.head_sha, io);
|
|
7528
7523
|
return 0;
|
|
7529
7524
|
}
|
|
7530
7525
|
if (success.status === "provisioning_started") {
|
|
@@ -7554,6 +7549,7 @@ async function runModelADeploy(args) {
|
|
|
7554
7549
|
io.err(` got: ${JSON.stringify(success)}`);
|
|
7555
7550
|
return 1;
|
|
7556
7551
|
}
|
|
7552
|
+
await advanceBaseShaMarker(cwd, success.commit_sha, io);
|
|
7557
7553
|
io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
|
|
7558
7554
|
io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
|
|
7559
7555
|
if (result.workerScript !== null) {
|
|
@@ -8956,7 +8952,7 @@ var SLUG_REGEX3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
|
8956
8952
|
var initCommand = {
|
|
8957
8953
|
name: "init",
|
|
8958
8954
|
summary: "scaffold a new launchpad.yaml in the current directory",
|
|
8959
|
-
run: (args, io) => runInit(args, io,
|
|
8955
|
+
run: (args, io) => runInit(args, io, defaultPrompt2)
|
|
8960
8956
|
};
|
|
8961
8957
|
async function runInit(args, io, prompt) {
|
|
8962
8958
|
const parsed = parseArgs6(args);
|
|
@@ -9446,7 +9442,7 @@ function ensureGitignoreEntries(path10, entries) {
|
|
|
9446
9442
|
}
|
|
9447
9443
|
return added;
|
|
9448
9444
|
}
|
|
9449
|
-
async function
|
|
9445
|
+
async function defaultPrompt2(question, fallback) {
|
|
9450
9446
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9451
9447
|
try {
|
|
9452
9448
|
const suffix = fallback !== undefined ? ` [${fallback}]` : "";
|
|
@@ -10127,7 +10123,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
10127
10123
|
var destroyCommand = {
|
|
10128
10124
|
name: "destroy",
|
|
10129
10125
|
summary: "tear down a Launchpad app (owner-only, destructive)",
|
|
10130
|
-
run: (args, io) => runDestroy(args, io,
|
|
10126
|
+
run: (args, io) => runDestroy(args, io, defaultPrompt3, defaultIsTty)
|
|
10131
10127
|
};
|
|
10132
10128
|
var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
10133
10129
|
var SLUG_MIN_LENGTH = 3;
|
|
@@ -10359,7 +10355,7 @@ function renderBotError2(status, env, slug, io) {
|
|
|
10359
10355
|
io.err(`launchpad destroy: bot returned HTTP ${status} (error=${errorCode}): ${message}`);
|
|
10360
10356
|
}
|
|
10361
10357
|
}
|
|
10362
|
-
async function
|
|
10358
|
+
async function defaultPrompt3(question) {
|
|
10363
10359
|
const rl = createInterface2({ input, output });
|
|
10364
10360
|
try {
|
|
10365
10361
|
return await rl.question(question);
|
|
@@ -11028,7 +11024,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
11028
11024
|
io.out(`Rollback target: ${verifiedSha.slice(0, 12)} (${manifestRelpath})`);
|
|
11029
11025
|
renderDiff(current, parsed.manifest, io);
|
|
11030
11026
|
if (!opts.yes) {
|
|
11031
|
-
const prompt = deps.prompt ??
|
|
11027
|
+
const prompt = deps.prompt ?? defaultPrompt4;
|
|
11032
11028
|
if (!process.stdin.isTTY && deps.prompt === undefined) {
|
|
11033
11029
|
io.err("launchpad rollback: refusing to rollback non-interactively without --yes.");
|
|
11034
11030
|
return 64;
|
|
@@ -11116,7 +11112,7 @@ function formatHostnames(hs) {
|
|
|
11116
11112
|
return hs[0];
|
|
11117
11113
|
return `[${hs.join(", ")}]`;
|
|
11118
11114
|
}
|
|
11119
|
-
async function
|
|
11115
|
+
async function defaultPrompt4(question) {
|
|
11120
11116
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
11121
11117
|
try {
|
|
11122
11118
|
return await rl.question(question);
|
|
@@ -11423,7 +11419,7 @@ async function offerRedeploy(slug, opts, io, deps) {
|
|
|
11423
11419
|
const isTty = (deps.isTty ?? (() => process.stdin.isTTY === true))();
|
|
11424
11420
|
let doRedeploy = wantsAuto;
|
|
11425
11421
|
if (!wantsAuto && isTty) {
|
|
11426
|
-
const prompt = deps.prompt ??
|
|
11422
|
+
const prompt = deps.prompt ?? defaultPrompt5;
|
|
11427
11423
|
let answer;
|
|
11428
11424
|
try {
|
|
11429
11425
|
answer = (await prompt(`Redeploy ${slug} now to activate? [y/N] `)).trim().toLowerCase();
|
|
@@ -11444,7 +11440,7 @@ async function offerRedeploy(slug, opts, io, deps) {
|
|
|
11444
11440
|
};
|
|
11445
11441
|
return run({ slug, file: opts.file ?? null, verify: true, verifyTimeoutMs: 180000 }, io, rdeps);
|
|
11446
11442
|
}
|
|
11447
|
-
async function
|
|
11443
|
+
async function defaultPrompt5(question) {
|
|
11448
11444
|
const rl = createInterface4({ input: input2, output: output2 });
|
|
11449
11445
|
try {
|
|
11450
11446
|
return await rl.question(question);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAsEA,OAAO,EAGL,4BAA4B,EAC7B,MAAM,uBAAuB,CAAC;AAO/B,OAAO,EAAa,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAKjE,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;AAEjD,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAmQD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAoBpC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,IAAI,CAwBpE;AAkMD,2BAA2B;AAC3B,OAAO,EAAE,4BAA4B,EAAE,CAAC"}
|
|
@@ -6,35 +6,31 @@ export interface AccessDriftBody {
|
|
|
6
6
|
readonly manifest_only?: unknown;
|
|
7
7
|
}
|
|
8
8
|
export interface AccessDriftDeps {
|
|
9
|
-
/** True when we may prompt (stdin is a TTY). */
|
|
10
|
-
readonly interactive: boolean;
|
|
11
|
-
/** Ask a yes/no question; resolves the raw typed line. */
|
|
12
|
-
readonly prompt: (question: string) => Promise<string>;
|
|
13
9
|
/**
|
|
14
10
|
* Run the gated apply flow. Defaults to `runDeployApply` at the call site.
|
|
15
11
|
* The third arg carries an `onApplied` hook the flow fires only when a
|
|
16
12
|
* terraform apply actually completes — so we never claim success when the
|
|
17
|
-
*
|
|
13
|
+
* apply reaches a non-applied terminal state (both paths can exit 0).
|
|
18
14
|
*/
|
|
19
15
|
readonly runApply: (opts: ApplyOptions, io: CliIo, deps?: {
|
|
20
16
|
readonly onApplied?: () => void;
|
|
21
17
|
}) => Promise<ExitCode>;
|
|
22
18
|
}
|
|
23
19
|
/**
|
|
24
|
-
* The drift summary — printed
|
|
25
|
-
* directions of the divergence so the operator can see exactly what
|
|
26
|
-
* change
|
|
20
|
+
* The drift summary — printed before the automatic reconcile. Names both
|
|
21
|
+
* directions of the divergence so the operator can see exactly what is about
|
|
22
|
+
* to change.
|
|
27
23
|
*/
|
|
28
24
|
export declare function renderAccessDriftSummary(slug: string, body: AccessDriftBody): string[];
|
|
29
25
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
26
|
+
* Surface an access-drift refusal and reconcile it automatically via the gated
|
|
27
|
+
* apply flow. Returns the exit code the deploy command should return.
|
|
28
|
+
*
|
|
29
|
+
* Honest exit (carried forward from the 0.47.1 fix): the apply flow exits 0
|
|
30
|
+
* both when a terraform apply actually completes AND when it reaches a
|
|
31
|
+
* non-applied terminal state without changing anything, so we only report
|
|
32
|
+
* "reconciled" — and only return the apply's 0 — when `onApplied` fired. A bare
|
|
33
|
+
* 0 with nothing applied would be false success a script/CI would observe.
|
|
38
34
|
*/
|
|
39
35
|
export declare function handleAccessDrift(params: {
|
|
40
36
|
readonly slug: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-drift-offer.d.ts","sourceRoot":"","sources":["../../src/deploy/access-drift-offer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"access-drift-offer.d.ts","sourceRoot":"","sources":["../../src/deploy/access-drift-offer.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,KAAK,EACT,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KACvC,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxB;AAKD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,eAAe,GACpB,MAAM,EAAE,CAoBV;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;CAChC,GAAG,OAAO,CAAC,QAAQ,CAAC,CA+BpB"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.48.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-content-pr
|
|
3
3
|
description: Push a content change to a Launchpad app via `launchpad deploy` and verify it shipped via `launchpad status`. Covers the post-first-deploy iteration loop (edit → deploy → verify) — subsequent deploys commit directly to the app repo's main and the Pages build runs asynchronously, so verification is its own step. Use when someone says "push a content change", "ship an update", "/launchpad-content-pr", "verify my deploy", or after `/launchpad-deploy` reports `done` and they want to follow up with an edit.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -189,19 +189,20 @@ Both are offline by default (no bot). `launchpad validate
|
|
|
189
189
|
resolves against the **whole tenant**, so a valid *unassigned* tenant
|
|
190
190
|
group passes (no need to use `deploy --dry-run` as a workaround).
|
|
191
191
|
|
|
192
|
-
> **Changing the access group
|
|
193
|
-
> `access.allowed_entra_groups` and
|
|
194
|
-
>
|
|
195
|
-
>
|
|
196
|
-
>
|
|
197
|
-
>
|
|
198
|
-
>
|
|
199
|
-
>
|
|
200
|
-
>
|
|
201
|
-
>
|
|
202
|
-
>
|
|
203
|
-
>
|
|
204
|
-
> self-serve
|
|
192
|
+
> **Changing the access group needs no special command (PS-2117 / ADR
|
|
193
|
+
> 0034).** Edit `access.allowed_entra_groups` and run a plain
|
|
194
|
+
> `launchpad deploy`: it detects the group change and **reconciles
|
|
195
|
+
> access automatically** via the gated apply PR, which auto-merges on
|
|
196
|
+
> its automated checks (byte-equal validator + `tf plan`) with no
|
|
197
|
+
> `--apply` verb, no prompt, and no human approval. The content bundle
|
|
198
|
+
> is refused only for that one deploy (access reconciles first); re-run
|
|
199
|
+
> `launchpad deploy` afterwards to ship content. The platform does not
|
|
200
|
+
> block the change — a dangerous shape (a widening on a `sensitive` app,
|
|
201
|
+
> or a broad-group grant) fires an after-the-fact Product Security
|
|
202
|
+
> alert. Preview with `--dry-run`; undo a bad change with
|
|
203
|
+
> `launchpad rollback <prior-sha>`. Do **not** tell the user to raise a
|
|
204
|
+
> request with the platform team — it is fully self-serve. See
|
|
205
|
+
> `/launchpad-status` and the
|
|
205
206
|
> [auth & groups](https://get.launchpad.m-kopa.us/docs/concepts/auth-groups#changing-access-on-an-existing-app)
|
|
206
207
|
> doc.
|
|
207
208
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-deploy
|
|
3
3
|
description: Walk a Launchpad user through deploying an app from their local working directory (Model A — `launchpad init` + `launchpad deploy`). Wraps the CLI verbs end-to-end: detects the app shape, scaffolds `launchpad.yaml`, resolves the allowed Entra group via `launchpad groups`, bundles the CWD via `launchpad deploy`, and watches the rollout via `launchpad status`. Use when someone says "deploy a new app", "ship my app to Launchpad", "/launchpad-deploy", "I have an app locally — get it on Launchpad", or any variant. Resume/abandon for legacy in-flight provisioning is at the bottom.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -234,16 +234,18 @@ are authorised at runtime via a Microsoft Graph membership check
|
|
|
234
234
|
Use whichever fits; `--dry-run` additionally previews the Terraform.
|
|
235
235
|
|
|
236
236
|
This `--group` flow sets the group at **first deploy**. Changing the
|
|
237
|
-
group on an **existing** app
|
|
238
|
-
deploy`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
237
|
+
group on an **existing** app needs no different command: a plain
|
|
238
|
+
`launchpad deploy` **reconciles access automatically** (PS-2117 / ADR
|
|
239
|
+
0034). When it detects that the manifest's groups differ from what the
|
|
240
|
+
gateway enforces, it routes straight into the gated apply flow — opens
|
|
241
|
+
the bot's `[launchpad] apply <slug>` PR, which **auto-merges on its
|
|
242
|
+
automated checks** (byte-equal validator + `tf plan`) with no `--apply`
|
|
243
|
+
verb, no prompt, and no human approval. Once access is in sync, re-run
|
|
244
|
+
`launchpad deploy` to ship your content. The platform does not block an
|
|
245
|
+
access change; a dangerous shape (a widening on a `sensitive` app, or a
|
|
246
|
+
broad-group grant) fires an after-the-fact Product Security alert. Never
|
|
247
|
+
route the user to the platform team for a group change, and never tell
|
|
248
|
+
them access drift is unfixable — it just reconciles.
|
|
247
249
|
|
|
248
250
|
If `launchpad groups list` fails with:
|
|
249
251
|
|
|
@@ -377,8 +379,8 @@ Flags — there is nothing useful to pass on the Model A path:
|
|
|
377
379
|
- `--message` is sent as a request header on the legacy path only,
|
|
378
380
|
and the bot currently **ignores** it — don't offer it as a
|
|
379
381
|
change-log mechanism.
|
|
380
|
-
- `--file <path>` is valid only with the `--dry-run`
|
|
381
|
-
|
|
382
|
+
- `--file <path>` is valid only with the `--dry-run` manifest preview
|
|
383
|
+
(and the hidden `--apply` alias), not with a bundle deploy.
|
|
382
384
|
|
|
383
385
|
### A.6 Terminal handling
|
|
384
386
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-deploy-status
|
|
3
3
|
description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket), or watch provisioning live with `launchpad watch`. Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "watch my deploy go live", "watch provisioning", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-destroy
|
|
3
3
|
description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-identity
|
|
3
3
|
description: Teach an app author how to use the signed-in user's identity inside a Launchpad app — read the gateway-forwarded X-Launchpad-User-Assertion in a Pages Function, VERIFY it with @m-kopa/platform-auth (fail-closed), and show who's logged in (sub/email/name). Use when someone says "who is logged in", "show the current user", "get the user's email in my app", "auth in my launchpad app", "read the user identity", "/launchpad-identity", or is wiring up an /api/me for a gateway-fronted app.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-onboard
|
|
3
3
|
description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-report
|
|
3
3
|
description: File a bug report or feature request to the Launchpad team's tracker from the CLI. Use when someone reports something broken, hits an error in a launchpad command, or wishes a feature existed — e.g. "this is broken", "report a bug", "can you file that", "I wish launchpad could…", "/launchpad-bug", "/launchpad-feature". Always confirm and show exactly what you'll send before filing; never file silently.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: launchpad-status
|
|
3
3
|
description: Show whether a Launchpad app's local launchpad.yaml matches what's deployed, and read the deployed manifest. Wraps `launchpad pull` (fetch deployed YAML) and `launchpad status` (drift report). Use when someone says "is my app in sync", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or after `launchpad deploy` to verify the change landed.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
|
|
@@ -331,19 +331,18 @@ which the bot handles.)
|
|
|
331
331
|
### "drift: access.allowed_entra_groups"
|
|
332
332
|
|
|
333
333
|
Group binding changed in your local manifest. This is the
|
|
334
|
-
**load-bearing** drift — it controls who can access the app.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
exits non-zero on any drift, so it can gate CI.
|
|
334
|
+
**load-bearing** drift — it controls who can access the app.
|
|
335
|
+
|
|
336
|
+
Reconcile it by just running **`launchpad deploy`** (PS-2117 / ADR
|
|
337
|
+
0034): a plain deploy detects the group divergence and reconciles it
|
|
338
|
+
automatically via the gated apply PR, which auto-merges on its
|
|
339
|
+
automated checks (byte-equal validator + `tf plan`) with no `--apply`
|
|
340
|
+
verb, no prompt, and no human approval. The platform does not block the
|
|
341
|
+
change; a dangerous shape (a widening on a `sensitive` app, or a
|
|
342
|
+
broad-group grant) fires an after-the-fact Product Security alert. To
|
|
343
|
+
undo a bad change fast, `launchpad rollback <prior-sha>`.
|
|
344
|
+
`launchpad status --strict` exits non-zero on any drift, so it can gate
|
|
345
|
+
CI.
|
|
347
346
|
|
|
348
347
|
## Related skills
|
|
349
348
|
|