@m-kopa/launchpad-cli 0.47.2 → 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 +57 -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 +781 -228
- 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 +15 -16
- 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 +11 -14
- package/skills/launchpad-deploy/SKILL.md +21 -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 +15 -14
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;
|
|
935
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 });
|
|
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,8 +1999,104 @@ 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 : {})
|
|
2003
|
+
};
|
|
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
|
|
1775
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);
|
|
1776
2100
|
}
|
|
1777
2101
|
|
|
1778
2102
|
// src/bundle/cwd-walker.ts
|
|
@@ -2225,7 +2549,8 @@ var ManifestSchema = z.object({
|
|
|
2225
2549
|
app: AppBoundarySchema.optional(),
|
|
2226
2550
|
verify: VerifySchema.optional(),
|
|
2227
2551
|
confine_origin: z.boolean().optional(),
|
|
2228
|
-
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional()
|
|
2552
|
+
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
|
|
2553
|
+
sensitive: z.boolean().optional()
|
|
2229
2554
|
}).strict().superRefine((m, ctx) => {
|
|
2230
2555
|
const isContainer = m.deployment.type === "container";
|
|
2231
2556
|
if (m.confine_origin !== undefined) {
|
|
@@ -2440,6 +2765,14 @@ function parseManifest(input) {
|
|
|
2440
2765
|
}))
|
|
2441
2766
|
};
|
|
2442
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
|
+
}
|
|
2443
2776
|
// ../launchpad-engine/dist/app-boundary.js
|
|
2444
2777
|
var INCLUDE_EVERYTHING = ["**"];
|
|
2445
2778
|
var COMMON_INCLUDE = [
|
|
@@ -2842,6 +3175,7 @@ var SpecSchema = z2.object({
|
|
|
2842
3175
|
app: AppBoundarySchema.optional(),
|
|
2843
3176
|
verify: VerifySchema.optional(),
|
|
2844
3177
|
confine_origin: z2.boolean().optional(),
|
|
3178
|
+
sensitive: z2.boolean().optional(),
|
|
2845
3179
|
catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
|
|
2846
3180
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
2847
3181
|
containers: z2.array(ContainerSchema).min(1).optional(),
|
|
@@ -3395,44 +3729,51 @@ function emitSecretStep(lines, binding) {
|
|
|
3395
3729
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
3396
3730
|
var R2_S3_ENDPOINT = `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
|
3397
3731
|
// ../launchpad-engine/dist/access-drift.js
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
const t = s.trim();
|
|
3401
|
-
return UUID_RE.test(t) ? t.toLowerCase() : null;
|
|
3402
|
-
}
|
|
3403
|
-
function normalise(tokens, side, resolve6) {
|
|
3404
|
-
const uuids = new Set;
|
|
3405
|
-
const unresolved = [];
|
|
3406
|
-
for (const raw of tokens) {
|
|
3407
|
-
const direct = canonicalUuid(raw);
|
|
3408
|
-
const uuid = direct ?? (() => {
|
|
3409
|
-
const resolved = resolve6(raw);
|
|
3410
|
-
return resolved === null ? null : canonicalUuid(resolved);
|
|
3411
|
-
})();
|
|
3412
|
-
if (uuid === null) {
|
|
3413
|
-
unresolved.push({ side, token: raw });
|
|
3414
|
-
} else {
|
|
3415
|
-
uuids.add(uuid);
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
return { uuids, unresolved };
|
|
3732
|
+
function isUuid(s) {
|
|
3733
|
+
return parseCanonicalGroupId(s.trim()).kind === "ok";
|
|
3419
3734
|
}
|
|
3420
|
-
function
|
|
3421
|
-
const
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
const
|
|
3735
|
+
function canonicalUuid(s) {
|
|
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();
|
|
3426
3744
|
return {
|
|
3427
3745
|
manifestOnly,
|
|
3428
3746
|
liveOnly,
|
|
3429
|
-
unresolved,
|
|
3430
|
-
inSync: manifestOnly.length === 0 && liveOnly.length === 0
|
|
3747
|
+
unresolved: [],
|
|
3748
|
+
inSync: manifestOnly.length === 0 && liveOnly.length === 0
|
|
3431
3749
|
};
|
|
3432
3750
|
}
|
|
3751
|
+
// ../launchpad-engine/dist/broad-groups.js
|
|
3752
|
+
var BROAD_GROUPS = [
|
|
3753
|
+
{
|
|
3754
|
+
uuid: "caf78902-35dd-4833-88ce-8f067eda6036",
|
|
3755
|
+
name: "S_ALL_Employees",
|
|
3756
|
+
note: "All employees — the broadest grant on the tenant; assigned to the Launchpad enterprise app. Granting this reaches the entire company."
|
|
3757
|
+
}
|
|
3758
|
+
];
|
|
3759
|
+
function assertBroadGroupsWellFormed(list = BROAD_GROUPS) {
|
|
3760
|
+
const seen = new Set;
|
|
3761
|
+
for (const g of list) {
|
|
3762
|
+
const key = canonicalUuid(g.uuid);
|
|
3763
|
+
if (key === null || !isUuid(g.uuid)) {
|
|
3764
|
+
throw new Error(`broad-groups: entry '${g.name}' has a malformed UUID: '${g.uuid}'`);
|
|
3765
|
+
}
|
|
3766
|
+
if (seen.has(key)) {
|
|
3767
|
+
throw new Error(`broad-groups: duplicate UUID for '${g.name}': '${g.uuid}'`);
|
|
3768
|
+
}
|
|
3769
|
+
seen.add(key);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
assertBroadGroupsWellFormed();
|
|
3773
|
+
var BROAD_UUIDS = new Set(BROAD_GROUPS.map((g) => canonicalUuid(g.uuid)).filter((u) => u !== null));
|
|
3433
3774
|
// ../launchpad-engine/dist/fleet-manifest.js
|
|
3434
3775
|
import { z as z3 } from "zod";
|
|
3435
|
-
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}$/;
|
|
3436
3777
|
var TF_RESIDENCIES = ["per-app-workspace", "main-tf"];
|
|
3437
3778
|
var APP_SHAPES = [
|
|
3438
3779
|
"pages",
|
|
@@ -3459,7 +3800,7 @@ var FLEET_TIERS = [
|
|
|
3459
3800
|
"out-of-scope"
|
|
3460
3801
|
];
|
|
3461
3802
|
var DEVICE_POSTURE = ["gateway-eligible", "retained-access"];
|
|
3462
|
-
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.");
|
|
3463
3804
|
var FleetAppSchema = z3.object({
|
|
3464
3805
|
slug: z3.string().regex(SLUG_REGEX),
|
|
3465
3806
|
residency: z3.enum(TF_RESIDENCIES),
|
|
@@ -4094,23 +4435,30 @@ async function resolveGroupRefsTenant(cfg, refs, opts = {}) {
|
|
|
4094
4435
|
method: "POST",
|
|
4095
4436
|
path: "/groups/resolve",
|
|
4096
4437
|
jsonBody: { refs },
|
|
4097
|
-
nonThrowingStatuses: [400, 429, 503]
|
|
4438
|
+
nonThrowingStatuses: [400, 409, 422, 429, 503]
|
|
4098
4439
|
}, opts.fetcher);
|
|
4099
4440
|
} catch (e) {
|
|
4100
4441
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError)
|
|
4101
4442
|
throw e;
|
|
4102
4443
|
return { kind: "error", message: describe11(e) };
|
|
4103
4444
|
}
|
|
4445
|
+
const text = await res.text();
|
|
4104
4446
|
let data;
|
|
4105
4447
|
try {
|
|
4106
|
-
data =
|
|
4107
|
-
} catch
|
|
4108
|
-
|
|
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 };
|
|
4109
4455
|
}
|
|
4110
4456
|
if (res.status !== 200) {
|
|
4111
|
-
const
|
|
4112
|
-
|
|
4113
|
-
|
|
4457
|
+
const envelope = decodeErrorEnvelope(data, {
|
|
4458
|
+
status: res.status,
|
|
4459
|
+
endpoint: "/groups/resolve"
|
|
4460
|
+
});
|
|
4461
|
+
return { kind: "error", message: envelope.message, envelope };
|
|
4114
4462
|
}
|
|
4115
4463
|
const body = data;
|
|
4116
4464
|
switch (body.kind) {
|
|
@@ -4142,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
|
|
|
4142
4490
|
function isUuid2(value) {
|
|
4143
4491
|
return UUID_PATTERN.test(value);
|
|
4144
4492
|
}
|
|
4145
|
-
function findGroup(groups, nameOrId) {
|
|
4146
|
-
if (isUuid2(nameOrId)) {
|
|
4147
|
-
const lower = nameOrId.toLowerCase();
|
|
4148
|
-
const hit = groups.find((g) => g.id.toLowerCase() === lower);
|
|
4149
|
-
return hit ? { kind: "found", group: hit } : { kind: "not-found" };
|
|
4150
|
-
}
|
|
4151
|
-
const matches = groups.filter((g) => g.displayName === nameOrId || g.mailNickname !== null && g.mailNickname === nameOrId);
|
|
4152
|
-
if (matches.length === 0)
|
|
4153
|
-
return { kind: "not-found" };
|
|
4154
|
-
if (matches.length === 1)
|
|
4155
|
-
return { kind: "found", group: matches[0] };
|
|
4156
|
-
return { kind: "ambiguous", matches };
|
|
4157
|
-
}
|
|
4158
4493
|
function searchGroups(groups, query) {
|
|
4159
4494
|
const q = query.toLowerCase();
|
|
4160
4495
|
return groups.filter((g) => g.displayName.toLowerCase().includes(q) || g.mailNickname !== null && g.mailNickname.toLowerCase().includes(q) || g.id.toLowerCase().includes(q));
|
|
@@ -4934,17 +5269,48 @@ async function runStatus(args, io) {
|
|
|
4934
5269
|
return 2;
|
|
4935
5270
|
}
|
|
4936
5271
|
const deployed = deployedParse.manifest;
|
|
4937
|
-
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
|
+
});
|
|
4938
5294
|
let accessDrift;
|
|
4939
5295
|
if (state.enforcedGroups !== null) {
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
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);
|
|
4945
5304
|
}
|
|
4946
|
-
accessDrift =
|
|
5305
|
+
accessDrift = detectCanonicalAccessDrift(localGroups.ids, enforcedGroups.ids);
|
|
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);
|
|
4947
5312
|
}
|
|
5313
|
+
const groupIssues = assignmentCheck.issues;
|
|
4948
5314
|
const result = {
|
|
4949
5315
|
state: drift.length === 0 ? "in_sync" : "drift",
|
|
4950
5316
|
slug: parsed.slug,
|
|
@@ -4956,30 +5322,154 @@ async function runStatus(args, io) {
|
|
|
4956
5322
|
driftDetails: drift,
|
|
4957
5323
|
...deploymentKnown ? { deployment } : {},
|
|
4958
5324
|
...standingExceptions !== null ? { standingExceptions } : {},
|
|
4959
|
-
...accessDrift !== undefined ? { accessDrift } : {}
|
|
5325
|
+
...accessDrift !== undefined ? { accessDrift } : {},
|
|
5326
|
+
...groupIssues.length === 0 ? {} : { groupIssues }
|
|
4960
5327
|
};
|
|
4961
5328
|
emit3(result, parsed.json, io);
|
|
4962
|
-
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false)) {
|
|
5329
|
+
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
|
|
4963
5330
|
return 1;
|
|
4964
5331
|
}
|
|
4965
5332
|
return 0;
|
|
4966
5333
|
}
|
|
4967
|
-
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;
|
|
4968
5346
|
try {
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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))
|
|
4975
5387
|
};
|
|
4976
|
-
} catch (e) {
|
|
4977
|
-
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError)
|
|
4978
|
-
throw e;
|
|
4979
|
-
return () => null;
|
|
4980
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;
|
|
4981
5419
|
}
|
|
4982
|
-
function
|
|
5420
|
+
async function findAssignmentIssues(cfg, local, deployed, deployedSha) {
|
|
5421
|
+
const refs = [
|
|
5422
|
+
...local?.refs ?? [],
|
|
5423
|
+
...deployed?.refs ?? []
|
|
5424
|
+
];
|
|
5425
|
+
let assigned;
|
|
5426
|
+
try {
|
|
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)
|
|
5446
|
+
};
|
|
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
|
+
};
|
|
5471
|
+
}
|
|
5472
|
+
function computeDrift(local, deployed, canonicalGroups) {
|
|
4983
5473
|
const diffs = [];
|
|
4984
5474
|
const cmp = (path8, l, d) => {
|
|
4985
5475
|
const li = l ?? null;
|
|
@@ -4999,7 +5489,7 @@ function computeDrift(local, deployed) {
|
|
|
4999
5489
|
cmp("metadata.owner", local.metadata.owner, deployed.metadata.owner);
|
|
5000
5490
|
cmp("metadata.description", local.metadata.description, deployed.metadata.description);
|
|
5001
5491
|
cmp("deployment.type", local.deployment.type, deployed.deployment.type);
|
|
5002
|
-
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(", "));
|
|
5003
5493
|
cmp("hostnames[0]", local.hostnames?.[0], deployed.hostnames?.[0]);
|
|
5004
5494
|
cmp("build.command", local.build?.command, deployed.build?.command);
|
|
5005
5495
|
cmp("build.destination_dir", local.build?.destination_dir, deployed.build?.destination_dir);
|
|
@@ -5091,6 +5581,7 @@ function emit3(out, asJson, io) {
|
|
|
5091
5581
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
5092
5582
|
surfaceHeadVsDeployed(out, io);
|
|
5093
5583
|
surfaceAccessDrift(out, io);
|
|
5584
|
+
surfaceGroupIssues(out, io);
|
|
5094
5585
|
surfaceDeployment(out, io);
|
|
5095
5586
|
surfaceExceptions(out, io);
|
|
5096
5587
|
return;
|
|
@@ -5103,6 +5594,7 @@ function emit3(out, asJson, io) {
|
|
|
5103
5594
|
}
|
|
5104
5595
|
surfaceHeadVsDeployed(out, io);
|
|
5105
5596
|
surfaceAccessDrift(out, io);
|
|
5597
|
+
surfaceGroupIssues(out, io);
|
|
5106
5598
|
surfaceDeployment(out, io);
|
|
5107
5599
|
surfaceExceptions(out, io);
|
|
5108
5600
|
return;
|
|
@@ -5122,8 +5614,14 @@ function surfaceAccessDrift(out, io) {
|
|
|
5122
5614
|
for (const u of ad.unresolved) {
|
|
5123
5615
|
io.out(` unresolved group (${u.side}): ${u.token}`);
|
|
5124
5616
|
}
|
|
5125
|
-
io.out(" `launchpad deploy`
|
|
5126
|
-
|
|
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
|
+
}
|
|
5127
5625
|
}
|
|
5128
5626
|
function surfaceNoLocalManifestNote(out, io) {
|
|
5129
5627
|
if (out.drift !== null)
|
|
@@ -5798,8 +6296,8 @@ function renderManifestDiffLines(diff, max = 20) {
|
|
|
5798
6296
|
|
|
5799
6297
|
// src/commands/deploy-flags.ts
|
|
5800
6298
|
var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
5801
|
-
var
|
|
5802
|
-
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;
|
|
5803
6301
|
function parseDeployFlags(args) {
|
|
5804
6302
|
let mode = "content";
|
|
5805
6303
|
let slug = null;
|
|
@@ -5933,7 +6431,7 @@ function parseDeployFlags(args) {
|
|
|
5933
6431
|
const v = args[i + 1];
|
|
5934
6432
|
if (v === undefined)
|
|
5935
6433
|
return `missing value for ${a}`;
|
|
5936
|
-
if (!
|
|
6434
|
+
if (!GROUP_KEY_RE.test(v) && !GROUP_UUID_RE.test(v)) {
|
|
5937
6435
|
return `invalid --allowed-group "${v}" — expected a group key (HCL identifier, e.g. G_Product_Security) or an Entra Object-ID UUID`;
|
|
5938
6436
|
}
|
|
5939
6437
|
allowedGroups.push(v);
|
|
@@ -6291,6 +6789,14 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6291
6789
|
return mapHttpError(e, slug, io);
|
|
6292
6790
|
}
|
|
6293
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
|
+
}
|
|
6294
6800
|
io.out(`Manifest pinned @ ${manifestSha.slice(0, 7)} (managed repo provenance).`);
|
|
6295
6801
|
for (const w of plan.warnings)
|
|
6296
6802
|
io.err(`! ${w}`);
|
|
@@ -6311,6 +6817,9 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6311
6817
|
return 0;
|
|
6312
6818
|
}
|
|
6313
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
|
+
}
|
|
6314
6823
|
let apply;
|
|
6315
6824
|
try {
|
|
6316
6825
|
apply = await apiJson(cfg, {
|
|
@@ -6321,6 +6830,14 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6321
6830
|
} catch (e) {
|
|
6322
6831
|
return mapHttpError(e, slug, io);
|
|
6323
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
|
+
}
|
|
6324
6841
|
io.out("");
|
|
6325
6842
|
io.out(`PR opened: ${apply.prUrl}`);
|
|
6326
6843
|
io.out(`Branch: ${apply.branch}`);
|
|
@@ -6340,8 +6857,13 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
6340
6857
|
pollIntervalSec,
|
|
6341
6858
|
timeoutMs
|
|
6342
6859
|
});
|
|
6343
|
-
if (outcome.kind !== "applied")
|
|
6344
|
-
|
|
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
|
+
}
|
|
6345
6867
|
deletePinIfPresent(cfg, slug, io);
|
|
6346
6868
|
io.out("");
|
|
6347
6869
|
io.out(`✓ Applied ${slug}. (See ${apply.prUrl} for the merged PR.)`);
|
|
@@ -6379,10 +6901,21 @@ async function pollUntilApplied(args) {
|
|
|
6379
6901
|
mode = "legacy";
|
|
6380
6902
|
continue;
|
|
6381
6903
|
}
|
|
6904
|
+
if (e instanceof ApiError && e.retryable === false) {
|
|
6905
|
+
breakDots();
|
|
6906
|
+
return { kind: "failed", reason: e.message };
|
|
6907
|
+
}
|
|
6382
6908
|
args.io.err(`! apply-status fetch failed (will retry): ${describe13(e)}`);
|
|
6383
6909
|
await sleep(args.pollIntervalSec * 1000);
|
|
6384
6910
|
continue;
|
|
6385
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
|
+
}
|
|
6386
6919
|
if (status.state === "applied") {
|
|
6387
6920
|
breakDots();
|
|
6388
6921
|
return { kind: "applied" };
|
|
@@ -6398,7 +6931,7 @@ async function pollUntilApplied(args) {
|
|
|
6398
6931
|
if (status.state !== lastRendered) {
|
|
6399
6932
|
breakDots();
|
|
6400
6933
|
if (status.state === "waiting-approval") {
|
|
6401
|
-
args.io.out(`⏳ waiting for
|
|
6934
|
+
args.io.out(`⏳ waiting for CI — the apply PR auto-merges when required checks pass (${status.prUrl})`);
|
|
6402
6935
|
} else {
|
|
6403
6936
|
args.io.out(`⚙ PR merged — waiting for the app's terraform apply${status.checkName !== undefined ? ` ('${status.checkName}')` : ""} …`);
|
|
6404
6937
|
}
|
|
@@ -6469,6 +7002,7 @@ function outcomeToExit(outcome, prUrl, io) {
|
|
|
6469
7002
|
}
|
|
6470
7003
|
return 4;
|
|
6471
7004
|
case "transport":
|
|
7005
|
+
err(`! apply status failed: ${outcome.reason}`);
|
|
6472
7006
|
return 2;
|
|
6473
7007
|
}
|
|
6474
7008
|
}
|
|
@@ -6561,76 +7095,58 @@ function renderManifestError(loaded, io) {
|
|
|
6561
7095
|
// src/deploy/access-drift-offer.ts
|
|
6562
7096
|
var asStrings = (v) => Array.isArray(v) ? v.map((x) => String(x)) : [];
|
|
6563
7097
|
function renderAccessDriftSummary(slug, body) {
|
|
6564
|
-
const lines = [`launchpad deploy:
|
|
7098
|
+
const lines = [`launchpad deploy: access change detected on "${slug}".`];
|
|
6565
7099
|
const liveOnly = asStrings(body.live_only);
|
|
6566
7100
|
const manifestOnly = asStrings(body.manifest_only);
|
|
6567
7101
|
if (liveOnly.length > 0) {
|
|
6568
|
-
lines.push(` enforced but NOT declared (
|
|
7102
|
+
lines.push(` enforced but NOT declared (will be removed): ${liveOnly.join(", ")}`);
|
|
6569
7103
|
}
|
|
6570
7104
|
if (manifestOnly.length > 0) {
|
|
6571
|
-
lines.push(` declared but NOT enforced: ${manifestOnly.join(", ")}`);
|
|
7105
|
+
lines.push(` declared but NOT enforced (will be granted): ${manifestOnly.join(", ")}`);
|
|
6572
7106
|
}
|
|
6573
7107
|
lines.push("");
|
|
6574
|
-
lines.push(" Your manifest's access groups
|
|
6575
|
-
lines.push("
|
|
7108
|
+
lines.push(" Your manifest's access groups differ from what the gateway enforces —");
|
|
7109
|
+
lines.push(" reconciling automatically via the gated Terraform apply (no second command needed).");
|
|
6576
7110
|
return lines;
|
|
6577
7111
|
}
|
|
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
7112
|
async function handleAccessDrift(params) {
|
|
6591
|
-
const { slug, manifestPath, body, io, deps } = params;
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
if (!deps.interactive) {
|
|
6595
|
-
for (const line of renderManualApplyGuidance())
|
|
7113
|
+
const { slug, manifestPath, body, io, deps, outcome } = params;
|
|
7114
|
+
if (outcome === undefined) {
|
|
7115
|
+
for (const line of renderAccessDriftSummary(slug, body))
|
|
6596
7116
|
io.err(line);
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
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;
|
|
7117
|
+
} else {
|
|
7118
|
+
io.out("");
|
|
7119
|
+
io.out(`Content committed at ${outcome.manifest_sha}; access now reconciles from that exact commit.`);
|
|
6607
7120
|
}
|
|
6608
7121
|
io.out("");
|
|
6609
|
-
io.out("Reconciling access via
|
|
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 …`);
|
|
6610
7123
|
let applied = false;
|
|
6611
|
-
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: () => {
|
|
6612
7139
|
applied = true;
|
|
6613
7140
|
} });
|
|
6614
7141
|
if (applied) {
|
|
6615
7142
|
io.out("");
|
|
6616
|
-
io.out("✓ Access reconciled.
|
|
7143
|
+
io.out(outcome === undefined ? "✓ Access reconciled." : `✓ Access applied from committed manifest ${outcome.manifest_sha}.`);
|
|
6617
7144
|
return exit;
|
|
6618
7145
|
}
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
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();
|
|
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.");
|
|
6633
7148
|
}
|
|
7149
|
+
return exit === 0 ? 1 : exit;
|
|
6634
7150
|
}
|
|
6635
7151
|
|
|
6636
7152
|
// src/deploy/dry-run.ts
|
|
@@ -7188,9 +7704,8 @@ function deployUsage() {
|
|
|
7188
7704
|
` + `manifest-driven dry-run (read-only preview):
|
|
7189
7705
|
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
7190
7706
|
` + `
|
|
7191
|
-
` + `manifest
|
|
7192
|
-
` +
|
|
7193
|
-
` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
|
|
7707
|
+
` + "access changes reconcile automatically (ADR 0034): plain `launchpad deploy`\n" + `detects a group change vs the deployed manifest and applies it via the gated
|
|
7708
|
+
` + "TF route — no separate verb. (`--apply` remains a hidden alias for one release.)";
|
|
7194
7709
|
}
|
|
7195
7710
|
function parseArgs3(args) {
|
|
7196
7711
|
let message = null;
|
|
@@ -7284,26 +7799,24 @@ function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
|
7284
7799
|
if (adw.manifestOnly !== undefined && adw.manifestOnly.length > 0) {
|
|
7285
7800
|
io.err(` declared but NOT enforced: ${adw.manifestOnly.join(", ")}`);
|
|
7286
7801
|
}
|
|
7287
|
-
io.err(`
|
|
7802
|
+
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
7803
|
}
|
|
7289
7804
|
}
|
|
7290
7805
|
function isStaleBlock(body) {
|
|
7291
|
-
|
|
7292
|
-
return code === "stale_overlap" || code === "stale_unverifiable";
|
|
7806
|
+
return body.error === "stale_overlap" || body.error === "stale_unverifiable";
|
|
7293
7807
|
}
|
|
7294
7808
|
function surfaceStaleBlock(body, io) {
|
|
7295
|
-
const
|
|
7296
|
-
|
|
7297
|
-
if (code === "stale_overlap" && Array.isArray(body.paths)) {
|
|
7809
|
+
const head = body.head_sha ?? null;
|
|
7810
|
+
if (body.error === "stale_overlap" && body.paths !== undefined) {
|
|
7298
7811
|
io.err(`launchpad deploy: BLOCKED — this deploy would roll back ${body.paths.length} file(s) a teammate changed on main since you cloned:`);
|
|
7299
|
-
for (const
|
|
7300
|
-
io.err(` - ${
|
|
7812
|
+
for (const path9 of body.paths.slice(0, 20)) {
|
|
7813
|
+
io.err(` - ${path9}`);
|
|
7301
7814
|
}
|
|
7302
7815
|
if (body.paths.length > 20) {
|
|
7303
7816
|
io.err(` … and ${body.paths.length - 20} more`);
|
|
7304
7817
|
}
|
|
7305
7818
|
} else {
|
|
7306
|
-
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.`);
|
|
7307
7820
|
}
|
|
7308
7821
|
io.err("");
|
|
7309
7822
|
io.err(" To recover (after reconciling the listed file(s) with main):");
|
|
@@ -7469,7 +7982,7 @@ async function runModelADeploy(args) {
|
|
|
7469
7982
|
return 1;
|
|
7470
7983
|
case "upload-error": {
|
|
7471
7984
|
const body = result.body;
|
|
7472
|
-
const errorCode =
|
|
7985
|
+
const errorCode = body.error;
|
|
7473
7986
|
if (result.status === 409 && errorCode === "slug_in_flight") {
|
|
7474
7987
|
io.err(`launchpad deploy: slug "${slug}" is already provisioning.`);
|
|
7475
7988
|
if (typeof body.message === "string") {
|
|
@@ -7495,17 +8008,42 @@ async function runModelADeploy(args) {
|
|
|
7495
8008
|
manifestPath,
|
|
7496
8009
|
body,
|
|
7497
8010
|
io,
|
|
7498
|
-
deps: {
|
|
7499
|
-
interactive: isInteractive(),
|
|
7500
|
-
prompt: defaultPrompt2,
|
|
7501
|
-
runApply: runDeployApply
|
|
7502
|
-
}
|
|
8011
|
+
deps: { runApply: runDeployApply }
|
|
7503
8012
|
});
|
|
7504
8013
|
}
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
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)}`);
|
|
7509
8047
|
if (Array.isArray(body.violations)) {
|
|
7510
8048
|
io.err(" AC8 violations:");
|
|
7511
8049
|
for (const v of body.violations) {
|
|
@@ -7567,6 +8105,18 @@ async function runModelADeploy(args) {
|
|
|
7567
8105
|
return 1;
|
|
7568
8106
|
}
|
|
7569
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
|
+
}
|
|
7570
8120
|
io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
|
|
7571
8121
|
io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
|
|
7572
8122
|
if (result.workerScript !== null) {
|
|
@@ -7597,6 +8147,20 @@ async function runModelADeploy(args) {
|
|
|
7597
8147
|
}
|
|
7598
8148
|
}
|
|
7599
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
|
+
}
|
|
7600
8164
|
|
|
7601
8165
|
// src/commands/redeploy.ts
|
|
7602
8166
|
import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
|
|
@@ -8507,24 +9071,7 @@ async function loadGroups(io, refresh, json) {
|
|
|
8507
9071
|
return { kind: "error", code: 2 };
|
|
8508
9072
|
}
|
|
8509
9073
|
}
|
|
8510
|
-
async function
|
|
8511
|
-
const hit = findGroup(assigned, key);
|
|
8512
|
-
if (hit.kind === "found") {
|
|
8513
|
-
return {
|
|
8514
|
-
kind: "found",
|
|
8515
|
-
group: {
|
|
8516
|
-
id: hit.group.id,
|
|
8517
|
-
displayName: hit.group.displayName,
|
|
8518
|
-
mailNickname: hit.group.mailNickname
|
|
8519
|
-
}
|
|
8520
|
-
};
|
|
8521
|
-
}
|
|
8522
|
-
if (hit.kind === "ambiguous") {
|
|
8523
|
-
return {
|
|
8524
|
-
kind: "ambiguous",
|
|
8525
|
-
matches: hit.matches.map((g) => ({ id: g.id, displayName: g.displayName }))
|
|
8526
|
-
};
|
|
8527
|
-
}
|
|
9074
|
+
async function lookupTenantWide(key) {
|
|
8528
9075
|
try {
|
|
8529
9076
|
const r = await resolveGroupRefsTenant(loadConfig(), [key]);
|
|
8530
9077
|
switch (r.kind) {
|
|
@@ -8550,7 +9097,7 @@ async function lookupTenantAware(assigned, key) {
|
|
|
8550
9097
|
return { kind: "not-found" };
|
|
8551
9098
|
}
|
|
8552
9099
|
case "error":
|
|
8553
|
-
return { kind: "error", message: r.message };
|
|
9100
|
+
return { kind: "error", message: r.message, ...r.envelope === undefined ? {} : { envelope: r.envelope } };
|
|
8554
9101
|
}
|
|
8555
9102
|
} catch (e) {
|
|
8556
9103
|
if (e instanceof UnauthenticatedError)
|
|
@@ -8579,7 +9126,15 @@ function renderLookupFailure(io, json, hit) {
|
|
|
8579
9126
|
return 3;
|
|
8580
9127
|
}
|
|
8581
9128
|
if (hit.kind === "error") {
|
|
8582
|
-
|
|
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
|
+
}
|
|
8583
9138
|
return 2;
|
|
8584
9139
|
}
|
|
8585
9140
|
if (hit.kind === "ineligible") {
|
|
@@ -8694,10 +9249,7 @@ async function runShow(args, io) {
|
|
|
8694
9249
|
return 64;
|
|
8695
9250
|
}
|
|
8696
9251
|
const key = parsed.flags.positional[0];
|
|
8697
|
-
const
|
|
8698
|
-
if (load.kind === "error")
|
|
8699
|
-
return load.code;
|
|
8700
|
-
const hit = await lookupTenantAware(load.result.groups, key);
|
|
9252
|
+
const hit = await lookupTenantWide(key);
|
|
8701
9253
|
const failure = renderLookupFailure(io, parsed.flags.json, hit);
|
|
8702
9254
|
if (failure !== null)
|
|
8703
9255
|
return failure;
|
|
@@ -8746,10 +9298,7 @@ async function runResolve(args, io) {
|
|
|
8746
9298
|
return 64;
|
|
8747
9299
|
}
|
|
8748
9300
|
const key = parsed.flags.positional[0];
|
|
8749
|
-
const
|
|
8750
|
-
if (load.kind === "error")
|
|
8751
|
-
return load.code;
|
|
8752
|
-
const hit = await lookupTenantAware(load.result.groups, key);
|
|
9301
|
+
const hit = await lookupTenantWide(key);
|
|
8753
9302
|
const failure = renderLookupFailure(io, parsed.flags.json, hit);
|
|
8754
9303
|
if (failure !== null)
|
|
8755
9304
|
return failure;
|
|
@@ -8969,7 +9518,7 @@ var SLUG_REGEX3 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
|
8969
9518
|
var initCommand = {
|
|
8970
9519
|
name: "init",
|
|
8971
9520
|
summary: "scaffold a new launchpad.yaml in the current directory",
|
|
8972
|
-
run: (args, io) => runInit(args, io,
|
|
9521
|
+
run: (args, io) => runInit(args, io, defaultPrompt2)
|
|
8973
9522
|
};
|
|
8974
9523
|
async function runInit(args, io, prompt) {
|
|
8975
9524
|
const parsed = parseArgs6(args);
|
|
@@ -9459,7 +10008,7 @@ function ensureGitignoreEntries(path10, entries) {
|
|
|
9459
10008
|
}
|
|
9460
10009
|
return added;
|
|
9461
10010
|
}
|
|
9462
|
-
async function
|
|
10011
|
+
async function defaultPrompt2(question, fallback) {
|
|
9463
10012
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9464
10013
|
try {
|
|
9465
10014
|
const suffix = fallback !== undefined ? ` [${fallback}]` : "";
|
|
@@ -10140,7 +10689,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
10140
10689
|
var destroyCommand = {
|
|
10141
10690
|
name: "destroy",
|
|
10142
10691
|
summary: "tear down a Launchpad app (owner-only, destructive)",
|
|
10143
|
-
run: (args, io) => runDestroy(args, io,
|
|
10692
|
+
run: (args, io) => runDestroy(args, io, defaultPrompt3, defaultIsTty)
|
|
10144
10693
|
};
|
|
10145
10694
|
var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
10146
10695
|
var SLUG_MIN_LENGTH = 3;
|
|
@@ -10372,7 +10921,7 @@ function renderBotError2(status, env, slug, io) {
|
|
|
10372
10921
|
io.err(`launchpad destroy: bot returned HTTP ${status} (error=${errorCode}): ${message}`);
|
|
10373
10922
|
}
|
|
10374
10923
|
}
|
|
10375
|
-
async function
|
|
10924
|
+
async function defaultPrompt3(question) {
|
|
10376
10925
|
const rl = createInterface2({ input, output });
|
|
10377
10926
|
try {
|
|
10378
10927
|
return await rl.question(question);
|
|
@@ -11041,7 +11590,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
11041
11590
|
io.out(`Rollback target: ${verifiedSha.slice(0, 12)} (${manifestRelpath})`);
|
|
11042
11591
|
renderDiff(current, parsed.manifest, io);
|
|
11043
11592
|
if (!opts.yes) {
|
|
11044
|
-
const prompt = deps.prompt ??
|
|
11593
|
+
const prompt = deps.prompt ?? defaultPrompt4;
|
|
11045
11594
|
if (!process.stdin.isTTY && deps.prompt === undefined) {
|
|
11046
11595
|
io.err("launchpad rollback: refusing to rollback non-interactively without --yes.");
|
|
11047
11596
|
return 64;
|
|
@@ -11129,7 +11678,7 @@ function formatHostnames(hs) {
|
|
|
11129
11678
|
return hs[0];
|
|
11130
11679
|
return `[${hs.join(", ")}]`;
|
|
11131
11680
|
}
|
|
11132
|
-
async function
|
|
11681
|
+
async function defaultPrompt4(question) {
|
|
11133
11682
|
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
11134
11683
|
try {
|
|
11135
11684
|
return await rl.question(question);
|
|
@@ -11436,7 +11985,7 @@ async function offerRedeploy(slug, opts, io, deps) {
|
|
|
11436
11985
|
const isTty = (deps.isTty ?? (() => process.stdin.isTTY === true))();
|
|
11437
11986
|
let doRedeploy = wantsAuto;
|
|
11438
11987
|
if (!wantsAuto && isTty) {
|
|
11439
|
-
const prompt = deps.prompt ??
|
|
11988
|
+
const prompt = deps.prompt ?? defaultPrompt5;
|
|
11440
11989
|
let answer;
|
|
11441
11990
|
try {
|
|
11442
11991
|
answer = (await prompt(`Redeploy ${slug} now to activate? [y/N] `)).trim().toLowerCase();
|
|
@@ -11457,7 +12006,7 @@ async function offerRedeploy(slug, opts, io, deps) {
|
|
|
11457
12006
|
};
|
|
11458
12007
|
return run({ slug, file: opts.file ?? null, verify: true, verifyTimeoutMs: 180000 }, io, rdeps);
|
|
11459
12008
|
}
|
|
11460
|
-
async function
|
|
12009
|
+
async function defaultPrompt5(question) {
|
|
11461
12010
|
const rl = createInterface4({ input: input2, output: output2 });
|
|
11462
12011
|
try {
|
|
11463
12012
|
return await rl.question(question);
|
|
@@ -13025,7 +13574,11 @@ async function checkGroup(allowedGroup, cfg) {
|
|
|
13025
13574
|
}
|
|
13026
13575
|
return { kind: "not-found", key: allowedGroup };
|
|
13027
13576
|
case "error":
|
|
13028
|
-
return {
|
|
13577
|
+
return {
|
|
13578
|
+
kind: "network",
|
|
13579
|
+
message: r.message,
|
|
13580
|
+
...r.envelope === undefined ? {} : { envelope: r.envelope }
|
|
13581
|
+
};
|
|
13029
13582
|
}
|
|
13030
13583
|
} catch (e) {
|
|
13031
13584
|
if (e instanceof UnauthenticatedError) {
|
|
@@ -13100,7 +13653,7 @@ function renderHumanOk(result, group, boundary, io) {
|
|
|
13100
13653
|
io.err(` The session is valid but lacks permission for /groups; talk to the platform team.`);
|
|
13101
13654
|
return 3;
|
|
13102
13655
|
case "network":
|
|
13103
|
-
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)}`);
|
|
13104
13657
|
return 2;
|
|
13105
13658
|
}
|
|
13106
13659
|
}
|
|
@@ -13176,11 +13729,11 @@ function renderJsonOk(result, group, boundary, io) {
|
|
|
13176
13729
|
}));
|
|
13177
13730
|
return 3;
|
|
13178
13731
|
case "network":
|
|
13179
|
-
io.out(JSON.stringify({
|
|
13732
|
+
io.out(JSON.stringify(group.envelope === undefined ? {
|
|
13180
13733
|
ok: false,
|
|
13181
13734
|
...base,
|
|
13182
13735
|
group: { kind: "network", message: group.message }
|
|
13183
|
-
}));
|
|
13736
|
+
} : { ok: false, ...base, ...errorEnvelopeJson(group.envelope) }));
|
|
13184
13737
|
return 2;
|
|
13185
13738
|
}
|
|
13186
13739
|
}
|