@m-kopa/launchpad-cli 0.48.0 → 0.49.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 +32 -0
- package/dist/bundle/orchestrate.d.ts +2 -2
- package/dist/bundle/orchestrate.d.ts.map +1 -1
- package/dist/bundle/upload.d.ts +26 -6
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +738 -168
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/groups.d.ts.map +1 -1
- package/dist/commands/status.d.ts +17 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/deploy/access-drift-offer.d.ts +3 -0
- package/dist/deploy/access-drift-offer.d.ts.map +1 -1
- package/dist/deploy/apply.d.ts +8 -0
- package/dist/deploy/apply.d.ts.map +1 -1
- package/dist/groups/client.d.ts +3 -0
- package/dist/groups/client.d.ts.map +1 -1
- package/dist/http/api-client.d.ts +1 -1
- package/dist/http/api-client.d.ts.map +1 -1
- package/dist/http/error-envelope.d.ts +43 -0
- package/dist/http/error-envelope.d.ts.map +1 -0
- package/dist/http/errors.d.ts +15 -5
- package/dist/http/errors.d.ts.map +1 -1
- package/dist/report/fault-signal.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +10 -14
- package/skills/launchpad-deploy/SKILL.md +19 -28
- 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 +11 -9
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.49.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -48,6 +48,159 @@ function loadConfig(env = process.env) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// src/http/error-envelope.ts
|
|
52
|
+
var MAX_TEXT_CHARS = 16384;
|
|
53
|
+
var MAX_PATH_CHARS = 1024;
|
|
54
|
+
var MAX_ARRAY_ITEMS = 100;
|
|
55
|
+
function decodeErrorEnvelope(input, context) {
|
|
56
|
+
const body = isRecord(input) ? input : {};
|
|
57
|
+
const omissions = [];
|
|
58
|
+
const endpoint = boundedOptionalString(body.endpoint, "endpoint", omissions) ?? context.endpoint;
|
|
59
|
+
const legacyError = boundedOptionalString(body.error, "error", omissions);
|
|
60
|
+
const code = boundedOptionalString(body.code, "code", omissions) ?? legacyError ?? "api_error";
|
|
61
|
+
const message = boundedOptionalString(body.message, "message", omissions) ?? `bot returned HTTP ${context.status}`;
|
|
62
|
+
const source = boundedOptionalString(body.source, "source", omissions);
|
|
63
|
+
const sourceSha = nullableBoundedString(body.source_sha ?? body.sourceSha, "source_sha", omissions);
|
|
64
|
+
const manifestSha = boundedOptionalString(body.manifest_sha ?? body.manifestSha ?? body.commit_sha ?? body.commitSha, "manifest_sha", omissions);
|
|
65
|
+
return {
|
|
66
|
+
status: context.status,
|
|
67
|
+
endpoint,
|
|
68
|
+
code,
|
|
69
|
+
message,
|
|
70
|
+
...legacyError === undefined ? {} : { legacyError },
|
|
71
|
+
...source === undefined ? {} : { source },
|
|
72
|
+
...sourceSha === undefined ? {} : { sourceSha },
|
|
73
|
+
...manifestSha === undefined ? {} : { manifestSha },
|
|
74
|
+
offendingRefs: boundedStringArray(body.offending_refs ?? body.offendingRefs, "offending_refs", omissions),
|
|
75
|
+
retryable: typeof body.retryable === "boolean" ? body.retryable : null,
|
|
76
|
+
issues: boundedIssues(body.issues, omissions),
|
|
77
|
+
candidates: boundedCandidates(body.candidates, omissions),
|
|
78
|
+
omissions
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function decodeErrorText(text, context) {
|
|
82
|
+
try {
|
|
83
|
+
return decodeErrorEnvelope(JSON.parse(text), context);
|
|
84
|
+
} catch {
|
|
85
|
+
return {
|
|
86
|
+
status: context.status,
|
|
87
|
+
endpoint: context.endpoint,
|
|
88
|
+
code: "bad_response",
|
|
89
|
+
message: `bot returned a non-JSON response (${text.length} characters); ` + "the unstructured body was omitted",
|
|
90
|
+
offendingRefs: [],
|
|
91
|
+
retryable: null,
|
|
92
|
+
issues: [],
|
|
93
|
+
candidates: [],
|
|
94
|
+
omissions: ["response body omitted because it was not structured JSON"]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function formatErrorEnvelope(envelope) {
|
|
99
|
+
const lines = [
|
|
100
|
+
`bot returned HTTP ${envelope.status} for ${envelope.endpoint} ` + `[${envelope.code}]: ${envelope.message}`
|
|
101
|
+
];
|
|
102
|
+
if (envelope.source !== undefined || envelope.sourceSha !== undefined) {
|
|
103
|
+
lines.push(`source: ${envelope.source ?? "unspecified"}` + (envelope.sourceSha === undefined ? "" : ` @ ${envelope.sourceSha ?? "none"}`));
|
|
104
|
+
}
|
|
105
|
+
if (envelope.manifestSha !== undefined) {
|
|
106
|
+
lines.push(`manifest SHA: ${envelope.manifestSha}`);
|
|
107
|
+
}
|
|
108
|
+
if (envelope.offendingRefs.length > 0) {
|
|
109
|
+
lines.push(`offending refs: ${envelope.offendingRefs.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
for (const issue of envelope.issues) {
|
|
112
|
+
lines.push(`issue: ${issue.path}: ${issue.message}` + (issue.code === undefined ? "" : ` [${issue.code}]`));
|
|
113
|
+
}
|
|
114
|
+
for (const omission of envelope.omissions)
|
|
115
|
+
lines.push(`omission: ${omission}`);
|
|
116
|
+
if (envelope.retryable !== null) {
|
|
117
|
+
lines.push(`retryable: ${String(envelope.retryable)}`);
|
|
118
|
+
}
|
|
119
|
+
return lines.join(`
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
function errorEnvelopeJson(envelope) {
|
|
123
|
+
return {
|
|
124
|
+
error: {
|
|
125
|
+
status: envelope.status,
|
|
126
|
+
endpoint: envelope.endpoint,
|
|
127
|
+
code: envelope.code,
|
|
128
|
+
message: envelope.message,
|
|
129
|
+
...envelope.legacyError === undefined ? {} : { legacy_error: envelope.legacyError },
|
|
130
|
+
...envelope.source === undefined ? {} : { source: envelope.source },
|
|
131
|
+
...envelope.sourceSha === undefined ? {} : { source_sha: envelope.sourceSha },
|
|
132
|
+
...envelope.manifestSha === undefined ? {} : { manifest_sha: envelope.manifestSha },
|
|
133
|
+
offending_refs: envelope.offendingRefs,
|
|
134
|
+
retryable: envelope.retryable,
|
|
135
|
+
issues: envelope.issues,
|
|
136
|
+
candidates: envelope.candidates,
|
|
137
|
+
omissions: envelope.omissions
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function boundedOptionalString(value, field, omissions) {
|
|
142
|
+
if (typeof value !== "string")
|
|
143
|
+
return;
|
|
144
|
+
return boundString(value, field, MAX_TEXT_CHARS, omissions);
|
|
145
|
+
}
|
|
146
|
+
function nullableBoundedString(value, field, omissions) {
|
|
147
|
+
if (value === null)
|
|
148
|
+
return null;
|
|
149
|
+
return boundedOptionalString(value, field, omissions);
|
|
150
|
+
}
|
|
151
|
+
function boundString(value, field, maxChars, omissions) {
|
|
152
|
+
if (value.length <= maxChars)
|
|
153
|
+
return value;
|
|
154
|
+
const omitted = value.length - maxChars;
|
|
155
|
+
omissions.push(`${field}: ${omitted} trailing characters omitted`);
|
|
156
|
+
return `${value.slice(0, maxChars)} [${omitted} trailing characters omitted]`;
|
|
157
|
+
}
|
|
158
|
+
function boundedStringArray(value, field, omissions) {
|
|
159
|
+
if (!Array.isArray(value))
|
|
160
|
+
return [];
|
|
161
|
+
const kept = value.slice(0, MAX_ARRAY_ITEMS).map((entry, index) => boundString(String(entry), `${field}[${index}]`, MAX_TEXT_CHARS, omissions));
|
|
162
|
+
if (value.length > MAX_ARRAY_ITEMS) {
|
|
163
|
+
omissions.push(`${field}: ${value.length - MAX_ARRAY_ITEMS} entries omitted`);
|
|
164
|
+
}
|
|
165
|
+
return kept;
|
|
166
|
+
}
|
|
167
|
+
function boundedIssues(value, omissions) {
|
|
168
|
+
if (!Array.isArray(value))
|
|
169
|
+
return [];
|
|
170
|
+
const issues = value.slice(0, MAX_ARRAY_ITEMS).flatMap((entry, index) => {
|
|
171
|
+
if (!isRecord(entry))
|
|
172
|
+
return [];
|
|
173
|
+
return [{
|
|
174
|
+
path: boundString(typeof entry.path === "string" ? entry.path : "(unspecified)", `issues[${index}].path`, MAX_PATH_CHARS, omissions),
|
|
175
|
+
message: boundString(typeof entry.message === "string" ? entry.message : String(entry.message ?? ""), `issues[${index}].message`, MAX_TEXT_CHARS, omissions),
|
|
176
|
+
...typeof entry.code === "string" ? { code: boundString(entry.code, `issues[${index}].code`, MAX_PATH_CHARS, omissions) } : {}
|
|
177
|
+
}];
|
|
178
|
+
});
|
|
179
|
+
if (value.length > MAX_ARRAY_ITEMS) {
|
|
180
|
+
omissions.push(`issues: ${value.length - MAX_ARRAY_ITEMS} entries omitted`);
|
|
181
|
+
}
|
|
182
|
+
return issues;
|
|
183
|
+
}
|
|
184
|
+
function boundedCandidates(value, omissions) {
|
|
185
|
+
if (!Array.isArray(value))
|
|
186
|
+
return [];
|
|
187
|
+
const candidates = value.slice(0, MAX_ARRAY_ITEMS).flatMap((entry, index) => {
|
|
188
|
+
if (!isRecord(entry) || typeof entry.id !== "string")
|
|
189
|
+
return [];
|
|
190
|
+
return [{
|
|
191
|
+
id: boundString(entry.id, `candidates[${index}].id`, MAX_PATH_CHARS, omissions),
|
|
192
|
+
displayName: boundString(typeof entry.displayName === "string" ? entry.displayName : "", `candidates[${index}].displayName`, MAX_TEXT_CHARS, omissions)
|
|
193
|
+
}];
|
|
194
|
+
});
|
|
195
|
+
if (value.length > MAX_ARRAY_ITEMS) {
|
|
196
|
+
omissions.push(`candidates: ${value.length - MAX_ARRAY_ITEMS} entries omitted`);
|
|
197
|
+
}
|
|
198
|
+
return candidates;
|
|
199
|
+
}
|
|
200
|
+
function isRecord(value) {
|
|
201
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
51
204
|
// src/http/errors.ts
|
|
52
205
|
class UnauthenticatedError extends Error {
|
|
53
206
|
code = "unauthenticated";
|
|
@@ -62,12 +215,37 @@ class NotFoundError extends Error {
|
|
|
62
215
|
}
|
|
63
216
|
|
|
64
217
|
class ApiError extends Error {
|
|
65
|
-
|
|
218
|
+
faultCode = "api_error";
|
|
219
|
+
code;
|
|
66
220
|
status;
|
|
67
|
-
|
|
68
|
-
|
|
221
|
+
endpoint;
|
|
222
|
+
source;
|
|
223
|
+
sourceSha;
|
|
224
|
+
manifestSha;
|
|
225
|
+
offendingRefs;
|
|
226
|
+
retryable;
|
|
227
|
+
issues;
|
|
228
|
+
candidates;
|
|
229
|
+
omissions;
|
|
230
|
+
envelope;
|
|
231
|
+
constructor(envelope) {
|
|
232
|
+
super(formatErrorEnvelope(envelope));
|
|
69
233
|
this.name = "ApiError";
|
|
70
|
-
this.
|
|
234
|
+
this.code = envelope.code;
|
|
235
|
+
this.status = envelope.status;
|
|
236
|
+
this.endpoint = envelope.endpoint;
|
|
237
|
+
if (envelope.source !== undefined)
|
|
238
|
+
this.source = envelope.source;
|
|
239
|
+
if (envelope.sourceSha !== undefined)
|
|
240
|
+
this.sourceSha = envelope.sourceSha;
|
|
241
|
+
if (envelope.manifestSha !== undefined)
|
|
242
|
+
this.manifestSha = envelope.manifestSha;
|
|
243
|
+
this.offendingRefs = envelope.offendingRefs;
|
|
244
|
+
this.retryable = envelope.retryable;
|
|
245
|
+
this.issues = envelope.issues;
|
|
246
|
+
this.candidates = envelope.candidates;
|
|
247
|
+
this.omissions = envelope.omissions;
|
|
248
|
+
this.envelope = envelope;
|
|
71
249
|
}
|
|
72
250
|
}
|
|
73
251
|
|
|
@@ -80,7 +258,7 @@ var last = null;
|
|
|
80
258
|
function recordFault(err) {
|
|
81
259
|
if (err === null || typeof err !== "object")
|
|
82
260
|
return;
|
|
83
|
-
const code = err.code;
|
|
261
|
+
const code = err.faultCode ?? err.code;
|
|
84
262
|
if (typeof code !== "string")
|
|
85
263
|
return;
|
|
86
264
|
const status = err.status;
|
|
@@ -854,6 +1032,7 @@ function buildAuthorizationUrl(params) {
|
|
|
854
1032
|
}
|
|
855
1033
|
|
|
856
1034
|
// src/http/api-client.ts
|
|
1035
|
+
var MAX_ERROR_BODY_BYTES = 256 * 1024;
|
|
857
1036
|
async function apiJson(cfg, opts, fetcher = fetch) {
|
|
858
1037
|
const res = await apiRaw(cfg, opts, fetcher);
|
|
859
1038
|
let parsed;
|
|
@@ -905,34 +1084,75 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
|
|
|
905
1084
|
throw recordedFault(new TransportError(`network error calling ${url}: ${describe7(e)}`));
|
|
906
1085
|
}
|
|
907
1086
|
if (res.status === 401) {
|
|
908
|
-
const detail = await
|
|
1087
|
+
const detail = await errorDetail(res, opts.path);
|
|
909
1088
|
throw recordedFault(new UnauthenticatedError(`bot returned 401 for ${opts.path}: ${detail} — run \`launchpad login\``));
|
|
910
1089
|
}
|
|
911
1090
|
if (res.status === 403) {
|
|
912
|
-
const detail = await
|
|
1091
|
+
const detail = await errorDetail(res, opts.path);
|
|
913
1092
|
throw recordedFault(new ForbiddenError(`bot returned 403 for ${opts.path}: ${detail}`));
|
|
914
1093
|
}
|
|
915
1094
|
if (res.status === 404) {
|
|
916
|
-
const detail = await
|
|
1095
|
+
const detail = await errorDetail(res, opts.path);
|
|
917
1096
|
throw recordedFault(new NotFoundError(`bot returned 404 for ${opts.path}: ${detail}`));
|
|
918
1097
|
}
|
|
919
1098
|
if (!res.ok) {
|
|
920
1099
|
if (opts.nonThrowingStatuses?.includes(res.status) === true) {
|
|
921
1100
|
return res;
|
|
922
1101
|
}
|
|
923
|
-
|
|
924
|
-
throw recordedFault(new ApiError(`bot returned HTTP ${res.status} for ${opts.path}: ${detail}`, res.status));
|
|
1102
|
+
throw recordedFault(new ApiError(await readErrorEnvelope(res, opts.path)));
|
|
925
1103
|
}
|
|
926
1104
|
clearFault();
|
|
927
1105
|
return res;
|
|
928
1106
|
}
|
|
929
|
-
async function
|
|
1107
|
+
async function readErrorText(res) {
|
|
1108
|
+
const reader = res.body?.getReader();
|
|
1109
|
+
if (reader === undefined)
|
|
1110
|
+
return { kind: "text", text: "" };
|
|
1111
|
+
const chunks = [];
|
|
1112
|
+
let byteLength = 0;
|
|
930
1113
|
try {
|
|
931
|
-
|
|
932
|
-
|
|
1114
|
+
while (true) {
|
|
1115
|
+
const { done, value } = await reader.read();
|
|
1116
|
+
if (done)
|
|
1117
|
+
break;
|
|
1118
|
+
byteLength += value.byteLength;
|
|
1119
|
+
if (byteLength > MAX_ERROR_BODY_BYTES) {
|
|
1120
|
+
try {
|
|
1121
|
+
await reader.cancel();
|
|
1122
|
+
} catch {}
|
|
1123
|
+
return { kind: "omitted" };
|
|
1124
|
+
}
|
|
1125
|
+
chunks.push(value);
|
|
1126
|
+
}
|
|
933
1127
|
} catch {
|
|
934
|
-
return "";
|
|
1128
|
+
return { kind: "text", text: "" };
|
|
1129
|
+
} finally {
|
|
1130
|
+
reader.releaseLock();
|
|
1131
|
+
}
|
|
1132
|
+
const bytes = new Uint8Array(byteLength);
|
|
1133
|
+
let offset = 0;
|
|
1134
|
+
for (const chunk of chunks) {
|
|
1135
|
+
bytes.set(chunk, offset);
|
|
1136
|
+
offset += chunk.byteLength;
|
|
1137
|
+
}
|
|
1138
|
+
return { kind: "text", text: new TextDecoder().decode(bytes) };
|
|
1139
|
+
}
|
|
1140
|
+
async function readErrorEnvelope(res, endpoint) {
|
|
1141
|
+
const body = await readErrorText(res);
|
|
1142
|
+
if (body.kind === "text") {
|
|
1143
|
+
return decodeErrorText(body.text, { status: res.status, endpoint });
|
|
935
1144
|
}
|
|
1145
|
+
const omission = `response body omitted because it exceeded ${MAX_ERROR_BODY_BYTES} bytes`;
|
|
1146
|
+
return {
|
|
1147
|
+
...decodeErrorEnvelope({
|
|
1148
|
+
code: "response_body_too_large",
|
|
1149
|
+
message: `bot returned a response body larger than ${MAX_ERROR_BODY_BYTES} bytes; ` + "the body was omitted"
|
|
1150
|
+
}, { status: res.status, endpoint }),
|
|
1151
|
+
omissions: [omission]
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
async function errorDetail(res, endpoint) {
|
|
1155
|
+
return formatErrorEnvelope(await readErrorEnvelope(res, endpoint));
|
|
936
1156
|
}
|
|
937
1157
|
function describe7(e) {
|
|
938
1158
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -1404,8 +1624,6 @@ var createCommand = {
|
|
|
1404
1624
|
};
|
|
1405
1625
|
var APP_TYPES = ["static", "react", "react+api", "container"];
|
|
1406
1626
|
var SLUG_RE2 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
1407
|
-
var GROUP_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1408
|
-
var GROUP_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1409
1627
|
async function runCreate(args, io) {
|
|
1410
1628
|
const parsed = parseArgs(args);
|
|
1411
1629
|
if (typeof parsed === "string") {
|
|
@@ -1513,8 +1731,8 @@ function parseArgs(args) {
|
|
|
1513
1731
|
return "missing required flag --allowed-group (pass at least one)";
|
|
1514
1732
|
}
|
|
1515
1733
|
for (const g of allowedGroups) {
|
|
1516
|
-
if (
|
|
1517
|
-
return
|
|
1734
|
+
if (g.trim().length === 0) {
|
|
1735
|
+
return "invalid --allowed-group: display name or Entra Object-ID UUID must be non-empty";
|
|
1518
1736
|
}
|
|
1519
1737
|
}
|
|
1520
1738
|
return {
|
|
@@ -1531,15 +1749,14 @@ function printUsage(io) {
|
|
|
1531
1749
|
" --slug <slug>",
|
|
1532
1750
|
" --display-name <name>",
|
|
1533
1751
|
` --app-type <${APP_TYPES.join("|")}>`,
|
|
1534
|
-
" --allowed-group <
|
|
1752
|
+
" --allowed-group <name-or-uuid> [--allowed-group <name-or-uuid> ...]",
|
|
1535
1753
|
" [--description <text>]",
|
|
1536
1754
|
"",
|
|
1537
1755
|
" slugs are kebab-case (3–58 chars); they become the",
|
|
1538
1756
|
" <slug>.launchpad.m-kopa.us hostname.",
|
|
1539
1757
|
"",
|
|
1540
|
-
" --allowed-group
|
|
1541
|
-
"
|
|
1542
|
-
" NOT raw Entra UUIDs."
|
|
1758
|
+
" --allowed-group accepts an exact Entra display name or Object-ID UUID.",
|
|
1759
|
+
" The server resolves identity and enforces the app's auth-mode rules."
|
|
1543
1760
|
].join(`
|
|
1544
1761
|
`));
|
|
1545
1762
|
}
|
|
@@ -1707,12 +1924,13 @@ function isErrno3(e) {
|
|
|
1707
1924
|
}
|
|
1708
1925
|
|
|
1709
1926
|
// src/bundle/upload.ts
|
|
1927
|
+
var ACCESS_RECONCILE_EXACT_SHA_V1 = "access-reconcile-exact-sha-v1";
|
|
1710
1928
|
async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact, baseSha, overrideStale) {
|
|
1711
1929
|
const formData = new FormData;
|
|
1712
1930
|
formData.append("manifest", manifestYaml);
|
|
1713
1931
|
const buf = new ArrayBuffer(bundleTarGz.byteLength);
|
|
1714
1932
|
new Uint8Array(buf).set(bundleTarGz);
|
|
1715
|
-
formData.append("bundle", new Blob([buf], { type: "application/gzip" }), "bundle.tar.gz");
|
|
1933
|
+
formData.append("bundle", new Blob([new Uint8Array(buf)], { type: "application/gzip" }), "bundle.tar.gz");
|
|
1716
1934
|
if (workerArtifact !== undefined) {
|
|
1717
1935
|
const descriptor = {
|
|
1718
1936
|
script: workerArtifact.script,
|
|
@@ -1729,7 +1947,8 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1729
1947
|
const { accessToken } = await getValidAccessToken(cfg.sessionPath);
|
|
1730
1948
|
const headers = {
|
|
1731
1949
|
authorization: `Bearer ${accessToken}`,
|
|
1732
|
-
accept: "application/json"
|
|
1950
|
+
accept: "application/json",
|
|
1951
|
+
"x-launchpad-capabilities": ACCESS_RECONCILE_EXACT_SHA_V1
|
|
1733
1952
|
};
|
|
1734
1953
|
if (baseSha)
|
|
1735
1954
|
headers["x-launchpad-base-sha"] = baseSha;
|
|
@@ -1739,13 +1958,15 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1739
1958
|
try {
|
|
1740
1959
|
res = await fetch(url, { method: "POST", headers, body: formData });
|
|
1741
1960
|
} catch (e) {
|
|
1961
|
+
const envelope = decodeErrorEnvelope({
|
|
1962
|
+
error: "transport_failed",
|
|
1963
|
+
message: `network error contacting bot: ${e.message}`,
|
|
1964
|
+
retryable: true
|
|
1965
|
+
}, { status: 0, endpoint: url });
|
|
1742
1966
|
return {
|
|
1743
1967
|
kind: "error",
|
|
1744
1968
|
status: 0,
|
|
1745
|
-
response: {
|
|
1746
|
-
error: "transport_failed",
|
|
1747
|
-
message: `network error contacting bot: ${e.message}`
|
|
1748
|
-
}
|
|
1969
|
+
response: deployBundleError(envelope, {})
|
|
1749
1970
|
};
|
|
1750
1971
|
}
|
|
1751
1972
|
const text = await res.text();
|
|
@@ -1756,14 +1977,21 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1756
1977
|
return {
|
|
1757
1978
|
kind: "error",
|
|
1758
1979
|
status: res.status,
|
|
1759
|
-
response: {
|
|
1760
|
-
error: "bad_response",
|
|
1761
|
-
message: `bot returned non-JSON body (HTTP ${res.status}): ${text.slice(0, 200)}`
|
|
1762
|
-
}
|
|
1980
|
+
response: deployBundleError(decodeErrorText(text, { status: res.status, endpoint: url }), {})
|
|
1763
1981
|
};
|
|
1764
1982
|
}
|
|
1765
1983
|
if (res.status === 202) {
|
|
1766
|
-
|
|
1984
|
+
const parsed = parse202(body, slug);
|
|
1985
|
+
if (parsed !== null)
|
|
1986
|
+
return { kind: "ok", response: parsed };
|
|
1987
|
+
return {
|
|
1988
|
+
kind: "error",
|
|
1989
|
+
status: res.status,
|
|
1990
|
+
response: deployBundleError(decodeErrorEnvelope({
|
|
1991
|
+
error: "bad_response",
|
|
1992
|
+
message: "bot returned a malformed HTTP 202 deploy response"
|
|
1993
|
+
}, { status: res.status, endpoint: url }), {})
|
|
1994
|
+
};
|
|
1767
1995
|
}
|
|
1768
1996
|
if (res.status === 200 && typeof body === "object" && body !== null && body.outcome === "nothing-to-deploy") {
|
|
1769
1997
|
return { kind: "ok", response: body };
|
|
@@ -1771,9 +1999,105 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1771
1999
|
return {
|
|
1772
2000
|
kind: "error",
|
|
1773
2001
|
status: res.status,
|
|
1774
|
-
response: body
|
|
2002
|
+
response: deployBundleError(decodeErrorEnvelope(body, { status: res.status, endpoint: url }), isRecord2(body) ? body : {})
|
|
1775
2003
|
};
|
|
1776
2004
|
}
|
|
2005
|
+
function parse202(body, fallbackSlug) {
|
|
2006
|
+
if (!isRecord2(body))
|
|
2007
|
+
return null;
|
|
2008
|
+
if (body.status === "accepted") {
|
|
2009
|
+
if (typeof body.commit_sha !== "string" || typeof body.repo !== "string") {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
const reconcileRaw = body.reconcile ?? body.access_reconcile;
|
|
2013
|
+
const reconcile = reconcileRaw === undefined ? undefined : parseReconcile(reconcileRaw);
|
|
2014
|
+
if (reconcile === null)
|
|
2015
|
+
return null;
|
|
2016
|
+
const accepted = {
|
|
2017
|
+
...body,
|
|
2018
|
+
slug: typeof body.slug === "string" ? body.slug : fallbackSlug,
|
|
2019
|
+
message: typeof body.message === "string" ? body.message : "bundle committed; deployment is pending"
|
|
2020
|
+
};
|
|
2021
|
+
return reconcile === undefined ? accepted : { ...accepted, reconcile };
|
|
2022
|
+
}
|
|
2023
|
+
if (body.status === "provisioning_started") {
|
|
2024
|
+
if (typeof body.slug !== "string" || typeof body.submissionId !== "string" || typeof body.appType !== "string") {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
...body,
|
|
2029
|
+
message: typeof body.message === "string" ? body.message : "provisioning started"
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
function parseReconcile(value) {
|
|
2035
|
+
if (!isRecord2(value))
|
|
2036
|
+
return null;
|
|
2037
|
+
if (typeof value.id !== "string" || !isReconcileState(value.state) || typeof value.manifest_sha !== "string") {
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
const pr = value.pr === undefined ? undefined : parsePr(value.pr);
|
|
2041
|
+
if (pr === null)
|
|
2042
|
+
return null;
|
|
2043
|
+
const outcome = {
|
|
2044
|
+
id: value.id,
|
|
2045
|
+
state: value.state,
|
|
2046
|
+
manifest_sha: value.manifest_sha
|
|
2047
|
+
};
|
|
2048
|
+
return pr === undefined ? outcome : { ...outcome, pr };
|
|
2049
|
+
}
|
|
2050
|
+
function parsePr(value) {
|
|
2051
|
+
if (!isRecord2(value))
|
|
2052
|
+
return null;
|
|
2053
|
+
const headSha = value.head_sha ?? value.headSha;
|
|
2054
|
+
if (typeof value.number !== "number" || !Number.isInteger(value.number) || typeof value.url !== "string" || typeof headSha !== "string") {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
return { number: value.number, url: value.url, head_sha: headSha };
|
|
2058
|
+
}
|
|
2059
|
+
function deployBundleError(envelope, body) {
|
|
2060
|
+
const pr = parsePr(body.pr);
|
|
2061
|
+
const violations = readViolations(body.violations);
|
|
2062
|
+
const findings = readFindings(body.findings);
|
|
2063
|
+
const liveOnly = readStringArray(body.live_only);
|
|
2064
|
+
const manifestOnly = readStringArray(body.manifest_only);
|
|
2065
|
+
const paths = readStringArray(body.paths);
|
|
2066
|
+
const reconcileId = body.reconcileId ?? body.reconcile_id;
|
|
2067
|
+
return {
|
|
2068
|
+
...envelope,
|
|
2069
|
+
error: envelope.legacyError ?? envelope.code,
|
|
2070
|
+
...violations === undefined ? {} : { violations },
|
|
2071
|
+
...findings === undefined ? {} : { findings },
|
|
2072
|
+
...typeof reconcileId === "string" ? { reconcileId } : {},
|
|
2073
|
+
...isReconcileState(body.state) ? { state: body.state } : {},
|
|
2074
|
+
...pr === null ? {} : { pr },
|
|
2075
|
+
...liveOnly === undefined ? {} : { live_only: liveOnly },
|
|
2076
|
+
...manifestOnly === undefined ? {} : { manifest_only: manifestOnly },
|
|
2077
|
+
...typeof body.head_sha === "string" ? { head_sha: body.head_sha } : {},
|
|
2078
|
+
...paths === undefined ? {} : { paths },
|
|
2079
|
+
...typeof body.reason === "string" ? { reason: body.reason } : {}
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function readStringArray(value) {
|
|
2083
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : undefined;
|
|
2084
|
+
}
|
|
2085
|
+
function readViolations(value) {
|
|
2086
|
+
if (!Array.isArray(value))
|
|
2087
|
+
return;
|
|
2088
|
+
return value.flatMap((entry) => isRecord2(entry) && typeof entry.path === "string" && typeof entry.rule === "string" && typeof entry.message === "string" ? [{ path: entry.path, rule: entry.rule, message: entry.message }] : []);
|
|
2089
|
+
}
|
|
2090
|
+
function readFindings(value) {
|
|
2091
|
+
if (!Array.isArray(value))
|
|
2092
|
+
return;
|
|
2093
|
+
return value.flatMap((entry) => isRecord2(entry) && typeof entry.path === "string" && typeof entry.rule === "string" ? [{ path: entry.path, rule: entry.rule }] : []);
|
|
2094
|
+
}
|
|
2095
|
+
function isReconcileState(value) {
|
|
2096
|
+
return value === "prepared" || value === "committed" || value === "pr_open" || value === "applied" || value === "aborted";
|
|
2097
|
+
}
|
|
2098
|
+
function isRecord2(value) {
|
|
2099
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2100
|
+
}
|
|
1777
2101
|
|
|
1778
2102
|
// src/bundle/cwd-walker.ts
|
|
1779
2103
|
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -2441,6 +2765,14 @@ function parseManifest(input) {
|
|
|
2441
2765
|
}))
|
|
2442
2766
|
};
|
|
2443
2767
|
}
|
|
2768
|
+
// ../launchpad-engine/dist/canonical-group-id.js
|
|
2769
|
+
var CANONICAL_GROUP_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2770
|
+
function parseCanonicalGroupId(value) {
|
|
2771
|
+
if (!CANONICAL_GROUP_ID_PATTERN.test(value)) {
|
|
2772
|
+
return { kind: "invalid", value };
|
|
2773
|
+
}
|
|
2774
|
+
return { kind: "ok", id: value.toLowerCase() };
|
|
2775
|
+
}
|
|
2444
2776
|
// ../launchpad-engine/dist/app-boundary.js
|
|
2445
2777
|
var INCLUDE_EVERYTHING = ["**"];
|
|
2446
2778
|
var COMMON_INCLUDE = [
|
|
@@ -3397,42 +3729,23 @@ function emitSecretStep(lines, binding) {
|
|
|
3397
3729
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
3398
3730
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
3399
3731
|
// ../launchpad-engine/dist/access-drift.js
|
|
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
3732
|
function isUuid(s) {
|
|
3402
|
-
return
|
|
3733
|
+
return parseCanonicalGroupId(s.trim()).kind === "ok";
|
|
3403
3734
|
}
|
|
3404
3735
|
function canonicalUuid(s) {
|
|
3405
|
-
const
|
|
3406
|
-
return
|
|
3407
|
-
}
|
|
3408
|
-
function
|
|
3409
|
-
const
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
const uuid = direct ?? (() => {
|
|
3414
|
-
const resolved = resolve6(raw);
|
|
3415
|
-
return resolved === null ? null : canonicalUuid(resolved);
|
|
3416
|
-
})();
|
|
3417
|
-
if (uuid === null) {
|
|
3418
|
-
unresolved.push({ side, token: raw });
|
|
3419
|
-
} else {
|
|
3420
|
-
uuids.add(uuid);
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
return { uuids, unresolved };
|
|
3424
|
-
}
|
|
3425
|
-
function detectAccessDrift(declaredTokens, enforcedTokens, resolve6) {
|
|
3426
|
-
const declared = normalise(declaredTokens, "manifest", resolve6);
|
|
3427
|
-
const enforced = normalise(enforcedTokens, "enforced", resolve6);
|
|
3428
|
-
const manifestOnly = [...declared.uuids].filter((u) => !enforced.uuids.has(u)).sort();
|
|
3429
|
-
const liveOnly = [...enforced.uuids].filter((u) => !declared.uuids.has(u)).sort();
|
|
3430
|
-
const unresolved = [...declared.unresolved, ...enforced.unresolved];
|
|
3736
|
+
const parsed = parseCanonicalGroupId(s.trim());
|
|
3737
|
+
return parsed.kind === "ok" ? parsed.id : null;
|
|
3738
|
+
}
|
|
3739
|
+
function detectCanonicalAccessDrift(declaredIds, enforcedIds) {
|
|
3740
|
+
const declared = new Set(declaredIds);
|
|
3741
|
+
const enforced = new Set(enforcedIds);
|
|
3742
|
+
const manifestOnly = [...declared].filter((id) => !enforced.has(id)).sort();
|
|
3743
|
+
const liveOnly = [...enforced].filter((id) => !declared.has(id)).sort();
|
|
3431
3744
|
return {
|
|
3432
3745
|
manifestOnly,
|
|
3433
3746
|
liveOnly,
|
|
3434
|
-
unresolved,
|
|
3435
|
-
inSync: manifestOnly.length === 0 && liveOnly.length === 0
|
|
3747
|
+
unresolved: [],
|
|
3748
|
+
inSync: manifestOnly.length === 0 && liveOnly.length === 0
|
|
3436
3749
|
};
|
|
3437
3750
|
}
|
|
3438
3751
|
// ../launchpad-engine/dist/broad-groups.js
|
|
@@ -3460,7 +3773,7 @@ assertBroadGroupsWellFormed();
|
|
|
3460
3773
|
var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
|
|
3461
3774
|
// ../launchpad-engine/dist/fleet-manifest.js
|
|
3462
3775
|
import { z as z3 } from "zod";
|
|
3463
|
-
var
|
|
3776
|
+
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
3464
3777
|
var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
|
|
3465
3778
|
var APP_SHAPES = [
|
|
3466
3779
|
"pages",
|
|
@@ -3487,7 +3800,7 @@ var FLEET_TIERS = [
|
|
|
3487
3800
|
"out-of-scope"
|
|
3488
3801
|
];
|
|
3489
3802
|
var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
|
|
3490
|
-
var GroupId = z3.string().regex(
|
|
3803
|
+
var GroupId = z3.string().regex(UUID_RE, "Entra group ids are emitted as canonical UUIDs in the JWT groups claim — display names will not match policies.");
|
|
3491
3804
|
var FleetAppSchema = z3.object({
|
|
3492
3805
|
slug: z3.string().regex(SLUG_REGEX),
|
|
3493
3806
|
residency: z3.enum(TF_RESIDENCIES),
|
|
@@ -4122,23 +4435,30 @@ async function resolveGroupRefsTenant(cfg, refs, opts = {}) {
|
|
|
4122
4435
|
method: "POST",
|
|
4123
4436
|
path: "/groups/resolve",
|
|
4124
4437
|
jsonBody: { refs },
|
|
4125
|
-
nonThrowingStatuses: [400, 429, 503]
|
|
4438
|
+
nonThrowingStatuses: [400, 409, 422, 429, 503]
|
|
4126
4439
|
}, opts.fetcher);
|
|
4127
4440
|
} catch (e) {
|
|
4128
4441
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError)
|
|
4129
4442
|
throw e;
|
|
4130
4443
|
return { kind: "error", message: describe11(e) };
|
|
4131
4444
|
}
|
|
4445
|
+
const text = await res.text();
|
|
4132
4446
|
let data;
|
|
4133
4447
|
try {
|
|
4134
|
-
data =
|
|
4135
|
-
} catch
|
|
4136
|
-
|
|
4448
|
+
data = JSON.parse(text);
|
|
4449
|
+
} catch {
|
|
4450
|
+
const envelope = decodeErrorText(text, {
|
|
4451
|
+
status: res.status,
|
|
4452
|
+
endpoint: "/groups/resolve"
|
|
4453
|
+
});
|
|
4454
|
+
return { kind: "error", message: envelope.message, envelope };
|
|
4137
4455
|
}
|
|
4138
4456
|
if (res.status !== 200) {
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4141
|
-
|
|
4457
|
+
const envelope = decodeErrorEnvelope(data, {
|
|
4458
|
+
status: res.status,
|
|
4459
|
+
endpoint: "/groups/resolve"
|
|
4460
|
+
});
|
|
4461
|
+
return { kind: "error", message: envelope.message, envelope };
|
|
4142
4462
|
}
|
|
4143
4463
|
const body = data;
|
|
4144
4464
|
switch (body.kind) {
|
|
@@ -4170,19 +4490,6 @@ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
|
|
|
4170
4490
|
function isUuid2(value) {
|
|
4171
4491
|
return UUID_PATTERN.test(value);
|
|
4172
4492
|
}
|
|
4173
|
-
function findGroup(groups, nameOrId) {
|
|
4174
|
-
if (isUuid2(nameOrId)) {
|
|
4175
|
-
const lower = nameOrId.toLowerCase();
|
|
4176
|
-
const hit = groups.find((g) => g.id.toLowerCase() === lower);
|
|
4177
|
-
return hit ? { kind: "found", group: hit } : { kind: "not-found" };
|
|
4178
|
-
}
|
|
4179
|
-
const matches = groups.filter((g) => g.displayName === nameOrId || g.mailNickname !== null && g.mailNickname === nameOrId);
|
|
4180
|
-
if (matches.length === 0)
|
|
4181
|
-
return { kind: "not-found" };
|
|
4182
|
-
if (matches.length === 1)
|
|
4183
|
-
return { kind: "found", group: matches[0] };
|
|
4184
|
-
return { kind: "ambiguous", matches };
|
|
4185
|
-
}
|
|
4186
4493
|
function searchGroups(groups, query) {
|
|
4187
4494
|
const q = query.toLowerCase();
|
|
4188
4495
|
return groups.filter((g) => g.displayName.toLowerCase().includes(q) || g.mailNickname !== null && g.mailNickname.toLowerCase().includes(q) || g.id.toLowerCase().includes(q));
|
|
@@ -4962,17 +5269,48 @@ async function runStatus(args, io) {
|
|
|
4962
5269
|
return 2;
|
|
4963
5270
|
}
|
|
4964
5271
|
const deployed = deployedParse.manifest;
|
|
4965
|
-
const
|
|
5272
|
+
const localGroups = await resolveStatusGroups({
|
|
5273
|
+
cfg,
|
|
5274
|
+
refs: allowedEntraGroups(local.access),
|
|
5275
|
+
source: "local manifest",
|
|
5276
|
+
manifestSha: null
|
|
5277
|
+
});
|
|
5278
|
+
if (localGroups.kind === "error") {
|
|
5279
|
+
return renderStatusGroupError(localGroups.envelope, parsed.json, io);
|
|
5280
|
+
}
|
|
5281
|
+
const deployedGroups = await resolveStatusGroups({
|
|
5282
|
+
cfg,
|
|
5283
|
+
refs: allowedEntraGroups(deployed.access),
|
|
5284
|
+
source: "deployed manifest",
|
|
5285
|
+
manifestSha: state.lastAppliedManifestSha
|
|
5286
|
+
});
|
|
5287
|
+
if (deployedGroups.kind === "error") {
|
|
5288
|
+
return renderStatusGroupError(deployedGroups.envelope, parsed.json, io);
|
|
5289
|
+
}
|
|
5290
|
+
const drift = computeDrift(local, deployed, {
|
|
5291
|
+
local: localGroups.ids,
|
|
5292
|
+
deployed: deployedGroups.ids
|
|
5293
|
+
});
|
|
4966
5294
|
let accessDrift;
|
|
4967
5295
|
if (state.enforcedGroups !== null) {
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5296
|
+
const enforcedGroups = await resolveStatusGroups({
|
|
5297
|
+
cfg,
|
|
5298
|
+
refs: state.enforcedGroups,
|
|
5299
|
+
source: "enforced access",
|
|
5300
|
+
manifestSha: state.lastAppliedManifestSha
|
|
5301
|
+
});
|
|
5302
|
+
if (enforcedGroups.kind === "error") {
|
|
5303
|
+
return renderStatusGroupError(enforcedGroups.envelope, parsed.json, io);
|
|
4973
5304
|
}
|
|
4974
|
-
accessDrift =
|
|
5305
|
+
accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
|
|
4975
5306
|
}
|
|
5307
|
+
const localAssignmentGroups = (local.auth ?? "access") === "access" ? localGroups : null;
|
|
5308
|
+
const deployedAssignmentGroups = (deployed.auth ?? "access") === "access" ? deployedGroups : null;
|
|
5309
|
+
const assignmentCheck = localAssignmentGroups === null && deployedAssignmentGroups === null ? { kind: "ok", issues: [] } : await findAssignmentIssues(cfg, localAssignmentGroups, deployedAssignmentGroups, state.lastAppliedManifestSha);
|
|
5310
|
+
if (assignmentCheck.kind === "error") {
|
|
5311
|
+
return renderStatusGroupError(assignmentCheck.envelope, parsed.json, io);
|
|
5312
|
+
}
|
|
5313
|
+
const groupIssues = assignmentCheck.issues;
|
|
4976
5314
|
const result = {
|
|
4977
5315
|
state: drift.length === 0 ? "in_sync" : "drift",
|
|
4978
5316
|
slug: parsed.slug,
|
|
@@ -4984,30 +5322,154 @@ async function runStatus(args, io) {
|
|
|
4984
5322
|
driftDetails: drift,
|
|
4985
5323
|
...deploymentKnown ? { deployment } : {},
|
|
4986
5324
|
...standingExceptions !== null ? { standingExceptions } : {},
|
|
4987
|
-
...accessDrift !== undefined ? { accessDrift } : {}
|
|
5325
|
+
...accessDrift !== undefined ? { accessDrift } : {},
|
|
5326
|
+
...groupIssues.length === 0 ? {} : { groupIssues }
|
|
4988
5327
|
};
|
|
4989
5328
|
emit3(result, parsed.json, io);
|
|
4990
|
-
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false)) {
|
|
5329
|
+
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
|
|
4991
5330
|
return 1;
|
|
4992
5331
|
}
|
|
4993
5332
|
return 0;
|
|
4994
5333
|
}
|
|
4995
|
-
async function
|
|
5334
|
+
async function resolveStatusGroups(args) {
|
|
5335
|
+
const direct = args.refs.map((ref) => parseCanonicalGroupId(ref));
|
|
5336
|
+
if (direct.every((result2) => result2.kind === "ok")) {
|
|
5337
|
+
const ids = direct.map((result2) => result2.id);
|
|
5338
|
+
return {
|
|
5339
|
+
kind: "ok",
|
|
5340
|
+
refs: args.refs,
|
|
5341
|
+
ids,
|
|
5342
|
+
groups: ids.map((id) => ({ id, displayName: id }))
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
5345
|
+
let result;
|
|
5346
|
+
try {
|
|
5347
|
+
result = await resolveGroupRefsTenant(args.cfg, args.refs);
|
|
5348
|
+
} catch (error) {
|
|
5349
|
+
const code = error instanceof UnauthenticatedError ? "unauthenticated" : error instanceof ForbiddenError ? "forbidden" : "group_resolution_failed";
|
|
5350
|
+
const status = error instanceof UnauthenticatedError ? 401 : error instanceof ForbiddenError ? 403 : 503;
|
|
5351
|
+
return {
|
|
5352
|
+
kind: "error",
|
|
5353
|
+
envelope: sourceEnvelope(args, code, describe12(error), args.refs, [], status)
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5356
|
+
if (result.kind === "ok") {
|
|
5357
|
+
if (result.groups.length === args.refs.length) {
|
|
5358
|
+
const parsed = result.groups.map((group) => parseCanonicalGroupId(group.id));
|
|
5359
|
+
if (parsed.every((entry) => entry.kind === "ok")) {
|
|
5360
|
+
const groups = result.groups.map((group, index) => ({
|
|
5361
|
+
id: parsed[index].id,
|
|
5362
|
+
displayName: group.displayName
|
|
5363
|
+
}));
|
|
5364
|
+
return {
|
|
5365
|
+
kind: "ok",
|
|
5366
|
+
refs: args.refs,
|
|
5367
|
+
ids: groups.map((group) => group.id),
|
|
5368
|
+
groups
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
return {
|
|
5373
|
+
kind: "error",
|
|
5374
|
+
envelope: sourceEnvelope(args, "bad_response", "tenant group resolver returned a malformed canonical projection", args.refs)
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
if (result.kind === "ambiguous") {
|
|
5378
|
+
return {
|
|
5379
|
+
kind: "error",
|
|
5380
|
+
envelope: sourceEnvelope(args, "group_ambiguous", result.message || `group '${result.ref}' is ambiguous`, [result.ref], result.candidates)
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
if (result.kind === "unresolved") {
|
|
5384
|
+
return {
|
|
5385
|
+
kind: "error",
|
|
5386
|
+
envelope: sourceEnvelope(args, result.refs.some((ref) => ref.reason === "unverified") ? "group_unverified" : "group_unresolved", result.message || "one or more group references could not be resolved", result.refs.map((ref) => ref.ref))
|
|
5387
|
+
};
|
|
5388
|
+
}
|
|
5389
|
+
return {
|
|
5390
|
+
kind: "error",
|
|
5391
|
+
envelope: result.envelope === undefined ? sourceEnvelope(args, "group_resolution_failed", result.message, args.refs) : {
|
|
5392
|
+
...result.envelope,
|
|
5393
|
+
source: args.source,
|
|
5394
|
+
sourceSha: args.manifestSha,
|
|
5395
|
+
...args.manifestSha === null ? {} : { manifestSha: args.manifestSha }
|
|
5396
|
+
}
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
5399
|
+
function sourceEnvelope(args, code, message, offendingRefs, candidates = [], status = 422) {
|
|
5400
|
+
return decodeErrorEnvelope({
|
|
5401
|
+
error: "group_resolution_failed",
|
|
5402
|
+
code,
|
|
5403
|
+
message,
|
|
5404
|
+
source: args.source,
|
|
5405
|
+
source_sha: args.manifestSha,
|
|
5406
|
+
manifest_sha: args.manifestSha,
|
|
5407
|
+
offending_refs: offendingRefs,
|
|
5408
|
+
retryable: code === "group_unverified" || code === "group_resolution_failed",
|
|
5409
|
+
candidates
|
|
5410
|
+
}, { status, endpoint: "/groups/resolve" });
|
|
5411
|
+
}
|
|
5412
|
+
function renderStatusGroupError(envelope, json, io) {
|
|
5413
|
+
if (json) {
|
|
5414
|
+
io.out(JSON.stringify(errorEnvelopeJson(envelope), null, 2));
|
|
5415
|
+
} else {
|
|
5416
|
+
io.err(`launchpad status: ${formatErrorEnvelope(envelope)}`);
|
|
5417
|
+
}
|
|
5418
|
+
return 2;
|
|
5419
|
+
}
|
|
5420
|
+
async function findAssignmentIssues(cfg, local, deployed, deployedSha) {
|
|
5421
|
+
const refs = [
|
|
5422
|
+
...local?.refs ?? [],
|
|
5423
|
+
...deployed?.refs ?? []
|
|
5424
|
+
];
|
|
5425
|
+
let assigned;
|
|
4996
5426
|
try {
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5427
|
+
assigned = await fetchGroups(cfg, { forceRefresh: true });
|
|
5428
|
+
} catch (error) {
|
|
5429
|
+
const code = error instanceof UnauthenticatedError ? "unauthenticated" : error instanceof ForbiddenError ? "forbidden" : "group_assignment_lookup_failed";
|
|
5430
|
+
const status = error instanceof UnauthenticatedError ? 401 : error instanceof ForbiddenError ? 403 : 503;
|
|
5431
|
+
return {
|
|
5432
|
+
kind: "error",
|
|
5433
|
+
envelope: sourceEnvelope({
|
|
5434
|
+
source: "enterprise-app group assignments",
|
|
5435
|
+
manifestSha: deployedSha
|
|
5436
|
+
}, code, describe12(error), refs, [], status)
|
|
5437
|
+
};
|
|
5438
|
+
}
|
|
5439
|
+
if (assigned.kind !== "ok") {
|
|
5440
|
+
return {
|
|
5441
|
+
kind: "error",
|
|
5442
|
+
envelope: sourceEnvelope({
|
|
5443
|
+
source: "enterprise-app group assignments",
|
|
5444
|
+
manifestSha: deployedSha
|
|
5445
|
+
}, "group_assignment_lookup_failed", assigned.message, refs, [], 503)
|
|
5003
5446
|
};
|
|
5004
|
-
} catch (e) {
|
|
5005
|
-
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError)
|
|
5006
|
-
throw e;
|
|
5007
|
-
return () => null;
|
|
5008
5447
|
}
|
|
5448
|
+
const assignedIds = new Set(assigned.groups.map((group) => group.id.toLowerCase()));
|
|
5449
|
+
const issue = (source, projection, manifestSha) => {
|
|
5450
|
+
const missing = projection.groups.flatMap((group, index) => assignedIds.has(group.id) ? [] : [{ ref: projection.refs[index] ?? group.id, group }]);
|
|
5451
|
+
if (missing.length === 0)
|
|
5452
|
+
return [];
|
|
5453
|
+
return [{
|
|
5454
|
+
code: "group_not_assigned",
|
|
5455
|
+
source,
|
|
5456
|
+
manifestSha,
|
|
5457
|
+
offendingRefs: missing.map((entry) => entry.ref),
|
|
5458
|
+
candidates: missing.map((entry) => ({
|
|
5459
|
+
id: entry.group.id,
|
|
5460
|
+
displayName: entry.group.displayName
|
|
5461
|
+
}))
|
|
5462
|
+
}];
|
|
5463
|
+
};
|
|
5464
|
+
return {
|
|
5465
|
+
kind: "ok",
|
|
5466
|
+
issues: [
|
|
5467
|
+
...local === null ? [] : issue("local", local, null),
|
|
5468
|
+
...deployed === null ? [] : issue("deployed", deployed, deployedSha)
|
|
5469
|
+
]
|
|
5470
|
+
};
|
|
5009
5471
|
}
|
|
5010
|
-
function computeDrift(local, deployed) {
|
|
5472
|
+
function computeDrift(local, deployed, canonicalGroups) {
|
|
5011
5473
|
const diffs = [];
|
|
5012
5474
|
const cmp = (path8, l, d) => {
|
|
5013
5475
|
const li = l ?? null;
|
|
@@ -5027,7 +5489,7 @@ function computeDrift(local, deployed) {
|
|
|
5027
5489
|
cmp("metadata.owner", local.metadata.owner, deployed.metadata.owner);
|
|
5028
5490
|
cmp("metadata.description", local.metadata.description, deployed.metadata.description);
|
|
5029
5491
|
cmp("deployment.type", local.deployment.type, deployed.deployment.type);
|
|
5030
|
-
cmp("access.allowed_entra_groups", allowedEntraGroups(local.access).join(", "), allowedEntraGroups(deployed.access).join(", "));
|
|
5492
|
+
cmp("access.allowed_entra_groups", (canonicalGroups?.local ?? allowedEntraGroups(local.access)).join(", "), (canonicalGroups?.deployed ?? allowedEntraGroups(deployed.access)).join(", "));
|
|
5031
5493
|
cmp("hostnames[0]", local.hostnames?.[0], deployed.hostnames?.[0]);
|
|
5032
5494
|
cmp("build.command", local.build?.command, deployed.build?.command);
|
|
5033
5495
|
cmp("build.destination_dir", local.build?.destination_dir, deployed.build?.destination_dir);
|
|
@@ -5119,6 +5581,7 @@ function emit3(out, asJson, io) {
|
|
|
5119
5581
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
5120
5582
|
surfaceHeadVsDeployed(out, io);
|
|
5121
5583
|
surfaceAccessDrift(out, io);
|
|
5584
|
+
surfaceGroupIssues(out, io);
|
|
5122
5585
|
surfaceDeployment(out, io);
|
|
5123
5586
|
surfaceExceptions(out, io);
|
|
5124
5587
|
return;
|
|
@@ -5131,6 +5594,7 @@ function emit3(out, asJson, io) {
|
|
|
5131
5594
|
}
|
|
5132
5595
|
surfaceHeadVsDeployed(out, io);
|
|
5133
5596
|
surfaceAccessDrift(out, io);
|
|
5597
|
+
surfaceGroupIssues(out, io);
|
|
5134
5598
|
surfaceDeployment(out, io);
|
|
5135
5599
|
surfaceExceptions(out, io);
|
|
5136
5600
|
return;
|
|
@@ -5150,8 +5614,14 @@ function surfaceAccessDrift(out, io) {
|
|
|
5150
5614
|
for (const u of ad.unresolved) {
|
|
5151
5615
|
io.out(` unresolved group (${u.side}): ${u.token}`);
|
|
5152
5616
|
}
|
|
5153
|
-
io.out(" `launchpad deploy`
|
|
5154
|
-
|
|
5617
|
+
io.out(" `launchpad deploy` reconciles this from the exact content commit through the gated TF route.");
|
|
5618
|
+
}
|
|
5619
|
+
function surfaceGroupIssues(out, io) {
|
|
5620
|
+
for (const issue of out.groupIssues ?? []) {
|
|
5621
|
+
const source = issue.source === "deployed" && issue.manifestSha !== null ? `deployed manifest @ ${issue.manifestSha}` : "local manifest";
|
|
5622
|
+
io.out(` group_not_assigned (${source}): ${issue.offendingRefs.join(", ")}`);
|
|
5623
|
+
io.out(" auth: access requires enterprise-app assignment so Entra emits the groups claim.");
|
|
5624
|
+
}
|
|
5155
5625
|
}
|
|
5156
5626
|
function surfaceNoLocalManifestNote(out, io) {
|
|
5157
5627
|
if (out.drift !== null)
|
|
@@ -5826,8 +6296,8 @@ function renderManifestDiffLines(diff, max = 20) {
|
|
|
5826
6296
|
|
|
5827
6297
|
// src/commands/deploy-flags.ts
|
|
5828
6298
|
var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
5829
|
-
var
|
|
5830
|
-
var
|
|
6299
|
+
var GROUP_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6300
|
+
var GROUP_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5831
6301
|
function parseDeployFlags(args) {
|
|
5832
6302
|
let mode = "content";
|
|
5833
6303
|
let slug = null;
|
|
@@ -5961,7 +6431,7 @@ function parseDeployFlags(args) {
|
|
|
5961
6431
|
const v = args[i + 1];
|
|
5962
6432
|
if (v === undefined)
|
|
5963
6433
|
return `missing value for ${a}`;
|
|
5964
|
-
if (!
|
|
6434
|
+
if (!GROUP_KEY_RE.test(v) && !GROUP_UUID_RE.test(v)) {
|
|
5965
6435
|
return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
|
|
5966
6436
|
}
|
|
5967
6437
|
allowedGroups.push(v);
|
|
@@ -6319,6 +6789,14 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6319
6789
|
return mapHttpError(e, slug, io);
|
|
6320
6790
|
}
|
|
6321
6791
|
const manifestSha = plan.manifestSha;
|
|
6792
|
+
if (!MANIFEST_SHA_REGEX.test(manifestSha)) {
|
|
6793
|
+
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/plan: ` + `invalid manifest SHA '${String(manifestSha)}'`);
|
|
6794
|
+
return 2;
|
|
6795
|
+
}
|
|
6796
|
+
if (opts.atSha !== undefined && manifestSha !== opts.atSha) {
|
|
6797
|
+
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/plan: ` + `requested exact manifest SHA ${opts.atSha}, received ${manifestSha}`);
|
|
6798
|
+
return 2;
|
|
6799
|
+
}
|
|
6322
6800
|
io.out(`Manifest pinned @ ${manifestSha.slice(0, 7)} (managed repo provenance).`);
|
|
6323
6801
|
for (const w of plan.warnings)
|
|
6324
6802
|
io.err(`! ${w}`);
|
|
@@ -6339,6 +6817,9 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6339
6817
|
return 0;
|
|
6340
6818
|
}
|
|
6341
6819
|
}
|
|
6820
|
+
if (opts.preopenedPr !== undefined) {
|
|
6821
|
+
io.out(`Server already opened exact-SHA apply PR #${opts.preopenedPr.number}; ` + "confirming it idempotently before polling.");
|
|
6822
|
+
}
|
|
6342
6823
|
let apply;
|
|
6343
6824
|
try {
|
|
6344
6825
|
apply = await apiJson(cfg, {
|
|
@@ -6349,6 +6830,14 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6349
6830
|
} catch (e) {
|
|
6350
6831
|
return mapHttpError(e, slug, io);
|
|
6351
6832
|
}
|
|
6833
|
+
if (apply.manifestSha !== manifestSha) {
|
|
6834
|
+
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/apply: ` + `requested ${manifestSha}, received ${String(apply.manifestSha)}`);
|
|
6835
|
+
return 2;
|
|
6836
|
+
}
|
|
6837
|
+
if (opts.preopenedPr !== undefined && apply.prNumber !== opts.preopenedPr.number) {
|
|
6838
|
+
io.err(`launchpad deploy --apply: protocol failure from /apps/${slug}/manifest/apply: ` + `server-preopened PR #${opts.preopenedPr.number}, received #${apply.prNumber}`);
|
|
6839
|
+
return 2;
|
|
6840
|
+
}
|
|
6352
6841
|
io.out("");
|
|
6353
6842
|
io.out(`PR opened: ${apply.prUrl}`);
|
|
6354
6843
|
io.out(`Branch: ${apply.branch}`);
|
|
@@ -6368,8 +6857,13 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6368
6857
|
pollIntervalSec,
|
|
6369
6858
|
timeoutMs
|
|
6370
6859
|
});
|
|
6371
|
-
if (outcome.kind !== "applied")
|
|
6372
|
-
|
|
6860
|
+
if (outcome.kind !== "applied") {
|
|
6861
|
+
const exit = outcomeToExit(outcome, apply.prUrl, io);
|
|
6862
|
+
if (outcome.kind === "timed-out" && opts.serverRecoveryActive === true) {
|
|
6863
|
+
io.err(" The CLI stopped waiting, but server recovery remains active for this exact commit.");
|
|
6864
|
+
}
|
|
6865
|
+
return exit;
|
|
6866
|
+
}
|
|
6373
6867
|
deletePinIfPresent(cfg, slug, io);
|
|
6374
6868
|
io.out("");
|
|
6375
6869
|
io.out(`✓ Applied ${slug}. (See ${apply.prUrl} for the merged PR.)`);
|
|
@@ -6407,10 +6901,21 @@ async function pollUntilApplied(args) {
|
|
|
6407
6901
|
mode = "legacy";
|
|
6408
6902
|
continue;
|
|
6409
6903
|
}
|
|
6904
|
+
if (e instanceof ApiError && e.retryable === false) {
|
|
6905
|
+
breakDots();
|
|
6906
|
+
return { kind: "failed", reason: e.message };
|
|
6907
|
+
}
|
|
6410
6908
|
args.io.err(`! apply-status fetch failed (will retry): ${describe13(e)}`);
|
|
6411
6909
|
await sleep(args.pollIntervalSec * 1000);
|
|
6412
6910
|
continue;
|
|
6413
6911
|
}
|
|
6912
|
+
if (status.manifestSha !== args.manifestSha) {
|
|
6913
|
+
breakDots();
|
|
6914
|
+
return {
|
|
6915
|
+
kind: "transport",
|
|
6916
|
+
reason: `protocol failure from /apps/${args.slug}/manifest/apply-status: ` + `requested ${args.manifestSha}, received ${status.manifestSha}`
|
|
6917
|
+
};
|
|
6918
|
+
}
|
|
6414
6919
|
if (status.state === "applied") {
|
|
6415
6920
|
breakDots();
|
|
6416
6921
|
return { kind: "applied" };
|
|
@@ -6426,7 +6931,7 @@ async function pollUntilApplied(args) {
|
|
|
6426
6931
|
if (status.state !== lastRendered) {
|
|
6427
6932
|
breakDots();
|
|
6428
6933
|
if (status.state === "waiting-approval") {
|
|
6429
|
-
args.io.out(`⏳ waiting for
|
|
6934
|
+
args.io.out(`⏳ waiting for CI — the apply PR auto-merges when required checks pass (${status.prUrl})`);
|
|
6430
6935
|
} else {
|
|
6431
6936
|
args.io.out(`⚙ PR merged — waiting for the app's terraform apply${status.checkName !== undefined ? ` ('${status.checkName}')` : ""} …`);
|
|
6432
6937
|
}
|
|
@@ -6497,6 +7002,7 @@ function outcomeToExit(outcome, prUrl, io) {
|
|
|
6497
7002
|
}
|
|
6498
7003
|
return 4;
|
|
6499
7004
|
case "transport":
|
|
7005
|
+
err(`! apply status failed: ${outcome.reason}`);
|
|
6500
7006
|
return 2;
|
|
6501
7007
|
}
|
|
6502
7008
|
}
|
|
@@ -6604,20 +7110,42 @@ function renderAccessDriftSummary(slug, body) {
|
|
|
6604
7110
|
return lines;
|
|
6605
7111
|
}
|
|
6606
7112
|
async function handleAccessDrift(params) {
|
|
6607
|
-
const { slug, manifestPath, body, io, deps } = params;
|
|
6608
|
-
|
|
6609
|
-
|
|
7113
|
+
const { slug, manifestPath, body, io, deps, outcome } = params;
|
|
7114
|
+
if (outcome === undefined) {
|
|
7115
|
+
for (const line of renderAccessDriftSummary(slug, body))
|
|
7116
|
+
io.err(line);
|
|
7117
|
+
} else {
|
|
7118
|
+
io.out("");
|
|
7119
|
+
io.out(`Content committed at ${outcome.manifest_sha}; access now reconciles from that exact commit.`);
|
|
7120
|
+
}
|
|
6610
7121
|
io.out("");
|
|
6611
|
-
io.out("Reconciling access automatically via the gated apply PR
|
|
7122
|
+
io.out(outcome?.pr === undefined ? "Reconciling access automatically via the gated apply PR …" : `Waiting for server-preopened access PR #${outcome.pr.number} to apply …`);
|
|
6612
7123
|
let applied = false;
|
|
6613
|
-
const exit = await deps.runApply({
|
|
7124
|
+
const exit = await deps.runApply({
|
|
7125
|
+
file: manifestPath,
|
|
7126
|
+
platformRepo: null,
|
|
7127
|
+
rePin: false,
|
|
7128
|
+
yes: true,
|
|
7129
|
+
...outcome === undefined ? {} : { atSha: outcome.manifest_sha },
|
|
7130
|
+
...outcome?.pr === undefined ? {} : {
|
|
7131
|
+
preopenedPr: {
|
|
7132
|
+
number: outcome.pr.number,
|
|
7133
|
+
url: outcome.pr.url,
|
|
7134
|
+
headSha: outcome.pr.head_sha
|
|
7135
|
+
}
|
|
7136
|
+
},
|
|
7137
|
+
...outcome === undefined ? {} : { serverRecoveryActive: true }
|
|
7138
|
+
}, io, { onApplied: () => {
|
|
6614
7139
|
applied = true;
|
|
6615
7140
|
} });
|
|
6616
7141
|
if (applied) {
|
|
6617
7142
|
io.out("");
|
|
6618
|
-
io.out("✓ Access reconciled.
|
|
7143
|
+
io.out(outcome === undefined ? "✓ Access reconciled." : `✓ Access applied from committed manifest ${outcome.manifest_sha}.`);
|
|
6619
7144
|
return exit;
|
|
6620
7145
|
}
|
|
7146
|
+
if (outcome !== undefined && exit === 4) {
|
|
7147
|
+
io.err("Access is not yet applied. The CLI timed out, but server recovery remains active for the committed exact SHA.");
|
|
7148
|
+
}
|
|
6621
7149
|
return exit === 0 ? 1 : exit;
|
|
6622
7150
|
}
|
|
6623
7151
|
|
|
@@ -7275,22 +7803,20 @@ function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
|
7275
7803
|
}
|
|
7276
7804
|
}
|
|
7277
7805
|
function isStaleBlock(body) {
|
|
7278
|
-
|
|
7279
|
-
return code === "stale_overlap" || code === "stale_unverifiable";
|
|
7806
|
+
return body.error === "stale_overlap" || body.error === "stale_unverifiable";
|
|
7280
7807
|
}
|
|
7281
7808
|
function surfaceStaleBlock(body, io) {
|
|
7282
|
-
const
|
|
7283
|
-
|
|
7284
|
-
if (code === "stale_overlap" && Array.isArray(body.paths)) {
|
|
7809
|
+
const head = body.head_sha ?? null;
|
|
7810
|
+
if (body.error === "stale_overlap" && body.paths !== undefined) {
|
|
7285
7811
|
io.err(`launchpad deploy: BLOCKED — this deploy would roll back ${body.paths.length} file(s) a teammate changed on main since you cloned:`);
|
|
7286
|
-
for (const
|
|
7287
|
-
io.err(` - ${
|
|
7812
|
+
for (const path9 of body.paths.slice(0, 20)) {
|
|
7813
|
+
io.err(` - ${path9}`);
|
|
7288
7814
|
}
|
|
7289
7815
|
if (body.paths.length > 20) {
|
|
7290
7816
|
io.err(` … and ${body.paths.length - 20} more`);
|
|
7291
7817
|
}
|
|
7292
7818
|
} else {
|
|
7293
|
-
io.err(`launchpad deploy: BLOCKED — main moved since you cloned and the overlap cannot be proven` + (
|
|
7819
|
+
io.err(`launchpad deploy: BLOCKED — main moved since you cloned and the overlap cannot be proven` + (body.reason === undefined ? "" : ` (${body.reason})`) + `; treating as unsafe.`);
|
|
7294
7820
|
}
|
|
7295
7821
|
io.err("");
|
|
7296
7822
|
io.err(" To recover (after reconciling the listed file(s) with main):");
|
|
@@ -7456,7 +7982,7 @@ async function runModelADeploy(args) {
|
|
|
7456
7982
|
return 1;
|
|
7457
7983
|
case "upload-error": {
|
|
7458
7984
|
const body = result.body;
|
|
7459
|
-
const errorCode =
|
|
7985
|
+
const errorCode = body.error;
|
|
7460
7986
|
if (result.status === 409 && errorCode === "slug_in_flight") {
|
|
7461
7987
|
io.err(`launchpad deploy: slug "${slug}" is already provisioning.`);
|
|
7462
7988
|
if (typeof body.message === "string") {
|
|
@@ -7485,10 +8011,39 @@ async function runModelADeploy(args) {
|
|
|
7485
8011
|
deps: { runApply: runDeployApply }
|
|
7486
8012
|
});
|
|
7487
8013
|
}
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
8014
|
+
if (errorCode === "reconcile_pending") {
|
|
8015
|
+
const outcome = pendingReconcileOutcome(body);
|
|
8016
|
+
if (outcome === null) {
|
|
8017
|
+
io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
|
|
8018
|
+
io.err(" The CLI cannot identify the commit yet. Server recovery remains active; this command exits non-zero.");
|
|
8019
|
+
return 1;
|
|
8020
|
+
}
|
|
8021
|
+
await advanceBaseShaMarker(cwd, outcome.manifest_sha, io);
|
|
8022
|
+
const accessExit = await handleAccessDrift({
|
|
8023
|
+
slug,
|
|
8024
|
+
manifestPath,
|
|
8025
|
+
body,
|
|
8026
|
+
outcome,
|
|
8027
|
+
io,
|
|
8028
|
+
deps: { runApply: runDeployApply }
|
|
8029
|
+
});
|
|
8030
|
+
if (accessExit !== 0)
|
|
8031
|
+
return accessExit;
|
|
8032
|
+
io.out("");
|
|
8033
|
+
io.out("Committed; build pending - Cloudflare Pages is building this deploy now.");
|
|
8034
|
+
if (noVerify)
|
|
8035
|
+
return 0;
|
|
8036
|
+
return runDeployVerification({
|
|
8037
|
+
slug,
|
|
8038
|
+
cwd,
|
|
8039
|
+
appType,
|
|
8040
|
+
shippedSha: outcome.manifest_sha,
|
|
8041
|
+
baseline: deployBaseline,
|
|
8042
|
+
registrationBoundMs,
|
|
8043
|
+
timeoutMs: verifyTimeoutMs
|
|
8044
|
+
}, io, realVerifyDeployDeps(cfg));
|
|
8045
|
+
}
|
|
8046
|
+
io.err(`launchpad deploy: ${formatErrorEnvelope(body)}`);
|
|
7492
8047
|
if (Array.isArray(body.violations)) {
|
|
7493
8048
|
io.err(" AC8 violations:");
|
|
7494
8049
|
for (const v of body.violations) {
|
|
@@ -7550,6 +8105,18 @@ async function runModelADeploy(args) {
|
|
|
7550
8105
|
return 1;
|
|
7551
8106
|
}
|
|
7552
8107
|
await advanceBaseShaMarker(cwd, success.commit_sha, io);
|
|
8108
|
+
if (success.reconcile !== undefined) {
|
|
8109
|
+
const accessExit = await handleAccessDrift({
|
|
8110
|
+
slug,
|
|
8111
|
+
manifestPath,
|
|
8112
|
+
body: {},
|
|
8113
|
+
outcome: success.reconcile,
|
|
8114
|
+
io,
|
|
8115
|
+
deps: { runApply: runDeployApply }
|
|
8116
|
+
});
|
|
8117
|
+
if (accessExit !== 0)
|
|
8118
|
+
return accessExit;
|
|
8119
|
+
}
|
|
7553
8120
|
io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
|
|
7554
8121
|
io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
|
|
7555
8122
|
if (result.workerScript !== null) {
|
|
@@ -7580,6 +8147,20 @@ async function runModelADeploy(args) {
|
|
|
7580
8147
|
}
|
|
7581
8148
|
}
|
|
7582
8149
|
}
|
|
8150
|
+
function pendingReconcileOutcome(body) {
|
|
8151
|
+
const manifestSha = body.manifestSha;
|
|
8152
|
+
const id = body.reconcileId;
|
|
8153
|
+
const state = body.state;
|
|
8154
|
+
if (typeof manifestSha !== "string" || !/^[0-9a-f]{40}$/.test(manifestSha) || typeof id !== "string" || !(state === "prepared" || state === "committed" || state === "pr_open" || state === "applied" || state === "aborted")) {
|
|
8155
|
+
return null;
|
|
8156
|
+
}
|
|
8157
|
+
return {
|
|
8158
|
+
id,
|
|
8159
|
+
state,
|
|
8160
|
+
manifest_sha: manifestSha,
|
|
8161
|
+
...body.pr === undefined ? {} : { pr: body.pr }
|
|
8162
|
+
};
|
|
8163
|
+
}
|
|
7583
8164
|
|
|
7584
8165
|
// src/commands/redeploy.ts
|
|
7585
8166
|
import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
|
|
@@ -8490,24 +9071,7 @@ async function loadGroups(io, refresh, json) {
|
|
|
8490
9071
|
return { kind: "error", code: 2 };
|
|
8491
9072
|
}
|
|
8492
9073
|
}
|
|
8493
|
-
async function
|
|
8494
|
-
const hit = findGroup(assigned, key);
|
|
8495
|
-
if (hit.kind === "found") {
|
|
8496
|
-
return {
|
|
8497
|
-
kind: "found",
|
|
8498
|
-
group: {
|
|
8499
|
-
id: hit.group.id,
|
|
8500
|
-
displayName: hit.group.displayName,
|
|
8501
|
-
mailNickname: hit.group.mailNickname
|
|
8502
|
-
}
|
|
8503
|
-
};
|
|
8504
|
-
}
|
|
8505
|
-
if (hit.kind === "ambiguous") {
|
|
8506
|
-
return {
|
|
8507
|
-
kind: "ambiguous",
|
|
8508
|
-
matches: hit.matches.map((g) => ({ id: g.id, displayName: g.displayName }))
|
|
8509
|
-
};
|
|
8510
|
-
}
|
|
9074
|
+
async function lookupTenantWide(key) {
|
|
8511
9075
|
try {
|
|
8512
9076
|
const r = await resolveGroupRefsTenant(loadConfig(), [key]);
|
|
8513
9077
|
switch (r.kind) {
|
|
@@ -8533,7 +9097,7 @@ async function lookupTenantAware(assigned, key) {
|
|
|
8533
9097
|
return { kind: "not-found" };
|
|
8534
9098
|
}
|
|
8535
9099
|
case "error":
|
|
8536
|
-
return { kind: "error", message: r.message };
|
|
9100
|
+
return { kind: "error", message: r.message, ...r.envelope === undefined ? {} : { envelope: r.envelope } };
|
|
8537
9101
|
}
|
|
8538
9102
|
} catch (e) {
|
|
8539
9103
|
if (e instanceof UnauthenticatedError)
|
|
@@ -8562,7 +9126,15 @@ function renderLookupFailure(io, json, hit) {
|
|
|
8562
9126
|
return 3;
|
|
8563
9127
|
}
|
|
8564
9128
|
if (hit.kind === "error") {
|
|
8565
|
-
|
|
9129
|
+
if (hit.envelope !== undefined) {
|
|
9130
|
+
if (json) {
|
|
9131
|
+
io.out(JSON.stringify(errorEnvelopeJson(hit.envelope)));
|
|
9132
|
+
} else {
|
|
9133
|
+
io.err(`launchpad groups: ${formatErrorEnvelope(hit.envelope)}`);
|
|
9134
|
+
}
|
|
9135
|
+
} else {
|
|
9136
|
+
renderFetchError(io, hit.message, json);
|
|
9137
|
+
}
|
|
8566
9138
|
return 2;
|
|
8567
9139
|
}
|
|
8568
9140
|
if (hit.kind === "ineligible") {
|
|
@@ -8677,10 +9249,7 @@ async function runShow(args, io) {
|
|
|
8677
9249
|
return 64;
|
|
8678
9250
|
}
|
|
8679
9251
|
const key = parsed.flags.positional[0];
|
|
8680
|
-
const
|
|
8681
|
-
if (load.kind === "error")
|
|
8682
|
-
return load.code;
|
|
8683
|
-
const hit = await lookupTenantAware(load.result.groups, key);
|
|
9252
|
+
const hit = await lookupTenantWide(key);
|
|
8684
9253
|
const failure = renderLookupFailure(io, parsed.flags.json, hit);
|
|
8685
9254
|
if (failure !== null)
|
|
8686
9255
|
return failure;
|
|
@@ -8729,10 +9298,7 @@ async function runResolve(args, io) {
|
|
|
8729
9298
|
return 64;
|
|
8730
9299
|
}
|
|
8731
9300
|
const key = parsed.flags.positional[0];
|
|
8732
|
-
const
|
|
8733
|
-
if (load.kind === "error")
|
|
8734
|
-
return load.code;
|
|
8735
|
-
const hit = await lookupTenantAware(load.result.groups, key);
|
|
9301
|
+
const hit = await lookupTenantWide(key);
|
|
8736
9302
|
const failure = renderLookupFailure(io, parsed.flags.json, hit);
|
|
8737
9303
|
if (failure !== null)
|
|
8738
9304
|
return failure;
|
|
@@ -13008,7 +13574,11 @@ async function checkGroup(allowedGroup, cfg) {
|
|
|
13008
13574
|
}
|
|
13009
13575
|
return { kind: "not-found", key: allowedGroup };
|
|
13010
13576
|
case "error":
|
|
13011
|
-
return {
|
|
13577
|
+
return {
|
|
13578
|
+
kind: "network",
|
|
13579
|
+
message: r.message,
|
|
13580
|
+
...r.envelope === undefined ? {} : { envelope: r.envelope }
|
|
13581
|
+
};
|
|
13012
13582
|
}
|
|
13013
13583
|
} catch (e) {
|
|
13014
13584
|
if (e instanceof UnauthenticatedError) {
|
|
@@ -13083,7 +13653,7 @@ function renderHumanOk(result, group, boundary, io) {
|
|
|
13083
13653
|
io.err(` The session is valid but lacks permission for /groups; talk to the platform team.`);
|
|
13084
13654
|
return 3;
|
|
13085
13655
|
case "network":
|
|
13086
|
-
io.err(`✗ --strict-groups failed to reach the bot: ${group.message}`);
|
|
13656
|
+
io.err(group.envelope === undefined ? `✗ --strict-groups failed to reach the bot: ${group.message}` : `✗ --strict-groups: ${formatErrorEnvelope(group.envelope)}`);
|
|
13087
13657
|
return 2;
|
|
13088
13658
|
}
|
|
13089
13659
|
}
|
|
@@ -13159,11 +13729,11 @@ function renderJsonOk(result, group, boundary, io) {
|
|
|
13159
13729
|
}));
|
|
13160
13730
|
return 3;
|
|
13161
13731
|
case "network":
|
|
13162
|
-
io.out(JSON.stringify({
|
|
13732
|
+
io.out(JSON.stringify(group.envelope === undefined ? {
|
|
13163
13733
|
ok: false,
|
|
13164
13734
|
...base,
|
|
13165
13735
|
group: { kind: "network", message: group.message }
|
|
13166
|
-
}));
|
|
13736
|
+
} : { ok: false, ...base, ...errorEnvelopeJson(group.envelope) }));
|
|
13167
13737
|
return 2;
|
|
13168
13738
|
}
|
|
13169
13739
|
}
|