@bongos/core 1.19.348 → 1.19.349
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/.bongos-core.json +12 -12
- package/modules/lifecycle/github-push.js +51 -4
- package/modules/lifecycle/routes/tasks.js +22 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-land.js +24 -2
- package/src/module-api.js +3 -1
- package/tests/ship_publish_error.mjs +86 -2
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.349",
|
|
6
|
+
"core_contract": "1.19.349",
|
|
7
|
+
"source_commit": "e54f95adb8fcdf1913b4594cffacffd27ad8589b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-25T02:
|
|
9
|
+
"built_at": "2026-08-25T02:56:45.668Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 328,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1849,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "8c67ac08217f3c2062ec02b6f17c7db9d813a2ac3aafb642fefca9ca3830f579",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -3997,7 +3997,7 @@
|
|
|
3997
3997
|
{
|
|
3998
3998
|
"path": "modules/lifecycle/github-push.js",
|
|
3999
3999
|
"mode": "0000644",
|
|
4000
|
-
"sha256": "
|
|
4000
|
+
"sha256": "d50b04376a9725bae0f8fe9c81166224eb47242655fec49f33f89b0f87b555f0"
|
|
4001
4001
|
},
|
|
4002
4002
|
{
|
|
4003
4003
|
"path": "modules/lifecycle/goal-advisory.js",
|
|
@@ -4132,7 +4132,7 @@
|
|
|
4132
4132
|
{
|
|
4133
4133
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
4134
4134
|
"mode": "0000644",
|
|
4135
|
-
"sha256": "
|
|
4135
|
+
"sha256": "b29d9bffce9c81e0f5e02ea2da387e15dc6f0bf8d96820931c29275861eeaf66"
|
|
4136
4136
|
},
|
|
4137
4137
|
{
|
|
4138
4138
|
"path": "modules/lifecycle/routes/versions.js",
|
|
@@ -4762,12 +4762,12 @@
|
|
|
4762
4762
|
{
|
|
4763
4763
|
"path": "package-lock.json",
|
|
4764
4764
|
"mode": "0000644",
|
|
4765
|
-
"sha256": "
|
|
4765
|
+
"sha256": "e4dcce1602dec36614f7978f6140d1022f23cb4d52f043d4fe4c1b065641dc39"
|
|
4766
4766
|
},
|
|
4767
4767
|
{
|
|
4768
4768
|
"path": "package.json",
|
|
4769
4769
|
"mode": "0000644",
|
|
4770
|
-
"sha256": "
|
|
4770
|
+
"sha256": "74a97d3caf479ffc429b577c4f96cdc33bf45e71f66c0748ac0a4ef22cdb3e60"
|
|
4771
4771
|
},
|
|
4772
4772
|
{
|
|
4773
4773
|
"path": "public-docs/index.html",
|
|
@@ -6102,7 +6102,7 @@
|
|
|
6102
6102
|
{
|
|
6103
6103
|
"path": "scripts/gds/ship-land.js",
|
|
6104
6104
|
"mode": "0000644",
|
|
6105
|
-
"sha256": "
|
|
6105
|
+
"sha256": "e091557b4cc29c081cb82f9cd650b4c110f1dede4e72b704893f8397cd7bbaf2"
|
|
6106
6106
|
},
|
|
6107
6107
|
{
|
|
6108
6108
|
"path": "scripts/gds/ship-memory-sync.js",
|
|
@@ -6622,7 +6622,7 @@
|
|
|
6622
6622
|
{
|
|
6623
6623
|
"path": "src/module-api.js",
|
|
6624
6624
|
"mode": "0000644",
|
|
6625
|
-
"sha256": "
|
|
6625
|
+
"sha256": "2509fad9871925f5c6233c9b85e7e31bad63f764892bddd6cc52295442dbc1f9"
|
|
6626
6626
|
},
|
|
6627
6627
|
{
|
|
6628
6628
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8977,7 +8977,7 @@
|
|
|
8977
8977
|
{
|
|
8978
8978
|
"path": "tests/ship_publish_error.mjs",
|
|
8979
8979
|
"mode": "0000644",
|
|
8980
|
-
"sha256": "
|
|
8980
|
+
"sha256": "5c0d5961408ac202eb53fe648c4c8b00e5552b231b35f39a39e006b705cb34f1"
|
|
8981
8981
|
},
|
|
8982
8982
|
{
|
|
8983
8983
|
"path": "tests/ship_resume.mjs",
|
|
@@ -130,6 +130,23 @@ function scrubCredential(s) {
|
|
|
130
130
|
.replace(/(ghs_|ghp_|github_pat_)[A-Za-z0-9_]+/g, '$1***');
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// How much scrubbed stderr travels with a failure (task 1003222). Sized to hold a
|
|
134
|
+
// real git failure whole — the 'remote:' lines that name the cause arrive LAST, so a
|
|
135
|
+
// short cap deletes precisely the part worth reading. It is a payload bound, nothing
|
|
136
|
+
// more; when it bites, truncateLoudly says so rather than ending mid-word.
|
|
137
|
+
const STDERR_MAX = 8000;
|
|
138
|
+
|
|
139
|
+
// Truncate in a way the reader can SEE. A silent clip is worse than no message: it
|
|
140
|
+
// looks like the complete text, which is how 'error: failed' got mistaken for the
|
|
141
|
+
// whole story. Keeps the TAIL as well as the head, because git's useful line is last.
|
|
142
|
+
function truncateLoudly(text, max = STDERR_MAX) {
|
|
143
|
+
const s = String(text || '');
|
|
144
|
+
if (s.length <= max) return s;
|
|
145
|
+
const head = Math.floor(max * 0.4);
|
|
146
|
+
const tail = max - head;
|
|
147
|
+
return `${s.slice(0, head)}\n\n…[${s.length - max} characters omitted — full text in the server log]…\n\n${s.slice(-tail)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
// Default git seam: execFile arg-array, shell:false, stdout captured. On failure
|
|
134
151
|
// throws GIT_FAILED. We never echo the (possibly credentialed) argv, but DO attach
|
|
135
152
|
// the credential-scrubbed stderr + the caller's step label (task 1547) so a real
|
|
@@ -149,9 +166,30 @@ async function defaultExec(args, opts = {}) {
|
|
|
149
166
|
return stdout;
|
|
150
167
|
} catch (e) {
|
|
151
168
|
// Identify the failing step by the caller's label, never by echoing argv.
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
//
|
|
170
|
+
// THE STDERR IS NOT CLIPPED SHORT ANY MORE (task 1003222). It used to be cut at
|
|
171
|
+
// 500 chars here and again at 200 on the way to the client, so a builder saw
|
|
172
|
+
// 'error: failed' — clipped mid-word, naming neither cause nor remedy — while the
|
|
173
|
+
// line that mattered ('remote: fatal error in commit_refs') and the exit code
|
|
174
|
+
// existed only server-side. That cost hours on the blocker-1000117 outage AND
|
|
175
|
+
// produced a WRONG first diagnosis, because the real message was never in the
|
|
176
|
+
// room. git puts the useful line LAST (the 'remote:' lines follow the progress
|
|
177
|
+
// chatter), which is exactly what a head-truncation throws away.
|
|
178
|
+
//
|
|
179
|
+
// The cap is a payload guard, not an editorial decision: generous enough that a
|
|
180
|
+
// real push failure fits whole, and when it does bite the text SAYS SO instead of
|
|
181
|
+
// ending mid-word and reading like the whole message.
|
|
182
|
+
const raw = scrubCredential(e && (e.stderr || e.message)).trim();
|
|
183
|
+
const stderr = truncateLoudly(raw, STDERR_MAX);
|
|
184
|
+
throw err('GIT_FAILED', {
|
|
185
|
+
git_step: step || (args && args[0]) || null,
|
|
186
|
+
stderr,
|
|
187
|
+
stderr_bytes: raw.length,
|
|
188
|
+
// The exit code distinguishes failure SHAPES that read identically in prose —
|
|
189
|
+
// 128 (git said no) vs 1 (a ref was rejected) vs null (killed by the timeout).
|
|
190
|
+
exit_code: (e && typeof e.code === 'number') ? e.code : null,
|
|
191
|
+
timed_out: !!(e && e.killed),
|
|
192
|
+
});
|
|
155
193
|
}
|
|
156
194
|
}
|
|
157
195
|
|
|
@@ -456,7 +494,14 @@ async function openOrFindPullRequest({ branch, title, body }, deps = {}) {
|
|
|
456
494
|
});
|
|
457
495
|
if (!createRes.ok) {
|
|
458
496
|
const detail = await createRes.text().catch(() => '');
|
|
459
|
-
|
|
497
|
+
// Same reasoning as GIT_FAILED's stderr (task 1003222): GitHub's refusal body
|
|
498
|
+
// names the actual rule that refused ('No commits between main and X', a
|
|
499
|
+
// protected-branch rule, a validation error naming the field) and a 300-char
|
|
500
|
+
// clip regularly cut it off. Scrubbed, because a body can echo a request header.
|
|
501
|
+
throw err('PR_CREATE_FAILED', {
|
|
502
|
+
status: createRes.status,
|
|
503
|
+
detail: truncateLoudly(scrubCredential(detail), STDERR_MAX),
|
|
504
|
+
});
|
|
460
505
|
}
|
|
461
506
|
const pr = await createRes.json();
|
|
462
507
|
return { number: pr.number, url: pr.html_url, node_id: pr.node_id };
|
|
@@ -1360,6 +1405,8 @@ module.exports = {
|
|
|
1360
1405
|
prReferencesTask,
|
|
1361
1406
|
findTaskPullRequest,
|
|
1362
1407
|
scrubCredential,
|
|
1408
|
+
truncateLoudly, // task 1003222 — exported so its behaviour is testable, not just its effect
|
|
1409
|
+
STDERR_MAX,
|
|
1363
1410
|
isAncestorOfDefaultBranch,
|
|
1364
1411
|
resolveToken,
|
|
1365
1412
|
getInstallationToken,
|
|
@@ -979,7 +979,12 @@ module.exports = function buildTasksRouter() {
|
|
|
979
979
|
if (e.code === 'NO_REPO_INFO') return res.fail('no_repo_info', 500);
|
|
980
980
|
if (e.code === 'PUSH_UNCONFIGURED') return res.fail('push_unconfigured', { status: 503, message: PUSH_UNCONFIGURED_MSG });
|
|
981
981
|
if (e.code === 'APP_NOT_INSTALLED') return res.fail('push_unconfigured', { status: 503, message: appNotInstalledMessage(e) });
|
|
982
|
-
|
|
982
|
+
// The GitHub HTTP status travels WITH the body (task 1003222). 422 ('no commits
|
|
983
|
+
// between'), 403 (a protected-branch rule) and 404 (the App cannot see the repo)
|
|
984
|
+
// are three different remedies, and the body alone does not always separate them.
|
|
985
|
+
if (e.code === 'PR_CREATE_FAILED') {
|
|
986
|
+
return res.fail('pr_create_failed', 502, { detail: e.detail, github_status: e.status ?? null });
|
|
987
|
+
}
|
|
983
988
|
// task 1547: GIT_FAILED now carries WHICH git step failed + the credential-
|
|
984
989
|
// scrubbed stderr (github-push.defaultExec). Log both so the next push outage
|
|
985
990
|
// is a one-line diagnosis, not a multi-hour opaque-`publish_failed` mystery
|
|
@@ -990,7 +995,22 @@ module.exports = function buildTasksRouter() {
|
|
|
990
995
|
e.git_step ? `step=${e.git_step}` : '',
|
|
991
996
|
e.stderr ? `stderr=${e.stderr}` : ''
|
|
992
997
|
);
|
|
993
|
-
|
|
998
|
+
// NO SECOND TRUNCATION (task 1003222). This line used to clip the already-
|
|
999
|
+
// shortened stderr to 200 chars, so the builder got 'error: failed' — cut
|
|
1000
|
+
// mid-word, naming neither cause nor remedy — while the real remote message sat
|
|
1001
|
+
// in a log they could not reach. A publish failure must be diagnosable from
|
|
1002
|
+
// where the builder is STANDING; that is the whole point of the task.
|
|
1003
|
+
//
|
|
1004
|
+
// `reason` keeps its name and position so existing readers (ship.js prints it)
|
|
1005
|
+
// keep working; it is simply no longer amputated. Everything here is already
|
|
1006
|
+
// credential-scrubbed at the throw site by github-push.scrubCredential.
|
|
1007
|
+
return res.fail('publish_failed', 500, {
|
|
1008
|
+
...(e.git_step ? { step: e.git_step } : {}),
|
|
1009
|
+
...(e.stderr ? { reason: e.stderr } : {}),
|
|
1010
|
+
...(e.stderr_bytes != null ? { reason_bytes: e.stderr_bytes } : {}),
|
|
1011
|
+
...(e.exit_code != null ? { git_exit_code: e.exit_code } : {}),
|
|
1012
|
+
...(e.timed_out ? { timed_out: true } : {}),
|
|
1013
|
+
});
|
|
994
1014
|
} finally {
|
|
995
1015
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
996
1016
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.349",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.349",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.349",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/ship-land.js
CHANGED
|
@@ -89,12 +89,23 @@ function formatPublishFailure(data, status, branch, opts = {}) {
|
|
|
89
89
|
const envelopeDetails = (data && data.error && typeof data.error === 'object' && data.error.details) || null;
|
|
90
90
|
const details = envelopeDetails || data || {};
|
|
91
91
|
const label = message || code || (data && data.message) || `HTTP ${status}`;
|
|
92
|
-
//
|
|
92
|
+
// Flattened ONLY for the matchers below and the one-line summary. The readable
|
|
93
|
+
// copy is printed verbatim underneath (task 1003222): the server no longer clips
|
|
94
|
+
// stderr, and git's useful line is the LAST one, so collapsing a 20-line push
|
|
95
|
+
// failure into a single run-on sentence hides the very thing that was un-hidden.
|
|
93
96
|
const reason = details.reason ? String(details.reason).replace(/\s+/g, ' ').trim() : '';
|
|
94
97
|
const parts = [];
|
|
95
98
|
if (details.step) parts.push(`step: ${details.step}`);
|
|
96
|
-
if (
|
|
99
|
+
if (details.git_exit_code != null) parts.push(`git exit ${details.git_exit_code}`);
|
|
100
|
+
if (details.github_status != null) parts.push(`GitHub HTTP ${details.github_status}`);
|
|
101
|
+
if (details.timed_out) parts.push('TIMED OUT');
|
|
102
|
+
// The one-line summary stays short and scannable; the full text follows it.
|
|
103
|
+
if (reason) parts.push(`reason: ${reason.length > 200 ? `${reason.slice(0, 200)}…` : reason}`);
|
|
97
104
|
let out = parts.length ? `${label} (${parts.join('; ')})` : label;
|
|
105
|
+
|
|
106
|
+
// ORDER MATTERS: summary, then the REMEDY, then the verbatim block last. The
|
|
107
|
+
// remedy is the actionable sentence and must not end up stranded below twenty
|
|
108
|
+
// lines of git chatter where it reads as a footnote to the quote.
|
|
98
109
|
if (NON_FAST_FORWARD_RE.test(reason)) {
|
|
99
110
|
out += opts.localHistoryRewritten === true
|
|
100
111
|
? ` — your LOCAL history was rewritten (a rebase or amend), so ${branch} is no longer a `
|
|
@@ -104,6 +115,17 @@ function formatPublishFailure(data, status, branch, opts = {}) {
|
|
|
104
115
|
: ` — the server already moved ${branch}: fetch origin, reset onto origin/${branch}, `
|
|
105
116
|
+ 're-run the generators, and re-ship so the push fast-forwards.';
|
|
106
117
|
}
|
|
118
|
+
|
|
119
|
+
// The untruncated remote message, verbatim and indented so it reads as a quotation
|
|
120
|
+
// rather than as more ship output. Only when it is multi-line — a single-line
|
|
121
|
+
// stderr is already fully present in the summary above, and repeating it is noise.
|
|
122
|
+
const full = details.reason ? String(details.reason).trimEnd() : '';
|
|
123
|
+
if (full && /\n/.test(full)) {
|
|
124
|
+
out += `\n\n --- what the remote actually said ---\n${full.split('\n').map((l) => ` ${l}`).join('\n')}\n --- end ---`;
|
|
125
|
+
if (details.reason_bytes != null && details.reason_bytes > full.length) {
|
|
126
|
+
out += `\n (${details.reason_bytes} bytes at the server; the middle was omitted here)`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
107
129
|
return out;
|
|
108
130
|
}
|
|
109
131
|
|
package/src/module-api.js
CHANGED
|
@@ -1126,8 +1126,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1126
1126
|
// landed since 1.19.346 with no explicit bump. run 32802265337. (task 1002620)
|
|
1127
1127
|
// 1.19.348 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1128
1128
|
// landed since 1.19.347 with no explicit bump. run 32802746363. (task 1002620)
|
|
1129
|
+
// 1.19.349 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1130
|
+
// landed since 1.19.348 with no explicit bump. run 32803276871. (task 1002620)
|
|
1129
1131
|
// ---------------------------------------------------------------------------
|
|
1130
|
-
const CORE_VERSION = '1.19.
|
|
1132
|
+
const CORE_VERSION = '1.19.349'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1131
1133
|
|
|
1132
1134
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1133
1135
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -99,7 +99,19 @@ test('a "fetch first" rejection names the branch and the reset-onto-origin remed
|
|
|
99
99
|
assert.match(out, /reset/i, 'names the reset');
|
|
100
100
|
assert.match(out, /generator/i, 'and the regenerate step before re-shipping');
|
|
101
101
|
assert.match(out, /re-ship|fast-forward/i, 'and why: so the push fast-forwards');
|
|
102
|
-
|
|
102
|
+
// CONTRACT CHANGED, task 1003222. This used to assert the OPPOSITE — that the
|
|
103
|
+
// whole thing collapsed to one line — which was right while the server clipped
|
|
104
|
+
// stderr to 200 chars, and wrong once it stopped. The summary line is still
|
|
105
|
+
// collapsed and scannable; the verbatim text is quoted BELOW it, because git puts
|
|
106
|
+
// the line that names the cause last and a one-line squash buries it.
|
|
107
|
+
const [summary] = out.split('\n');
|
|
108
|
+
assert.ok(!/\n/.test(summary), 'the SUMMARY is still one scannable line');
|
|
109
|
+
assert.match(summary, /reason:/, 'and still carries a reason inline');
|
|
110
|
+
assert.match(out, /what the remote actually said/, 'the untruncated stderr is quoted below');
|
|
111
|
+
assert.ok(
|
|
112
|
+
out.includes("error: failed to push some refs to 'github.com/org/repo.git'"),
|
|
113
|
+
'the remote message survives VERBATIM, on its own line — the whole point of the task',
|
|
114
|
+
);
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
test('the "non-fast-forward" wording triggers the same hint through the envelope', () => {
|
|
@@ -158,5 +170,77 @@ test('1003221 a non-NFF failure gains no hint from either flag', () => {
|
|
|
158
170
|
}
|
|
159
171
|
});
|
|
160
172
|
|
|
173
|
+
|
|
174
|
+
// --- task 1003222: the remote's own words reach the builder ------------------
|
|
175
|
+
//
|
|
176
|
+
// The failure this closes: a builder saw `reason: error: failed` — clipped
|
|
177
|
+
// mid-word by a 200-char slice on top of an earlier 500-char one — while the line
|
|
178
|
+
// that named the cause (`remote: fatal error in commit_refs`) and the HTTP status
|
|
179
|
+
// existed server-side only. That cost hours AND produced a wrong first diagnosis,
|
|
180
|
+
// because the real message was never in the room.
|
|
181
|
+
|
|
182
|
+
const LONG_STDERR = [
|
|
183
|
+
'To https://github.com/org/repo.git',
|
|
184
|
+
' ! [remote rejected] b -> b (pre-receive hook declined)',
|
|
185
|
+
'error: failed to push some refs',
|
|
186
|
+
'remote: fatal error in commit_refs', // git's useful line is LAST
|
|
187
|
+
].join('\n');
|
|
188
|
+
|
|
189
|
+
test('1003222 the LAST line survives — a head-truncation would have eaten it', () => {
|
|
190
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: LONG_STDERR });
|
|
191
|
+
assert.ok(
|
|
192
|
+
out.includes('remote: fatal error in commit_refs'),
|
|
193
|
+
'the cause git printed last must reach the builder',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('1003222 the git exit code is reported — 128 and 1 are different remedies', () => {
|
|
198
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'boom', git_exit_code: 128 });
|
|
199
|
+
assert.match(out, /git exit 128/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('1003222 a GitHub HTTP status is reported when the API refused', () => {
|
|
203
|
+
const out = fmt({ error: 'pr_create_failed', github_status: 422, reason: 'No commits between main and b' });
|
|
204
|
+
assert.match(out, /GitHub HTTP 422/, '422 vs 403 vs 404 are three different fixes');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('1003222 a timeout says so rather than looking like a rejection', () => {
|
|
208
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'killed', timed_out: true });
|
|
209
|
+
assert.match(out, /TIMED OUT/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('1003222 a single-line stderr is NOT repeated in a quote block', () => {
|
|
213
|
+
// It is already fully present in the summary; quoting it again is noise.
|
|
214
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'fatal: repository not found' });
|
|
215
|
+
assert.ok(!out.includes('what the remote actually said'), 'no block for a one-liner');
|
|
216
|
+
assert.match(out, /fatal: repository not found/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('1003222 an omitted middle is DECLARED, never silently clipped', () => {
|
|
220
|
+
const out = fmt({
|
|
221
|
+
error: 'publish_failed', step: 'push', reason: LONG_STDERR, reason_bytes: 99999,
|
|
222
|
+
});
|
|
223
|
+
assert.match(out, /99999 bytes at the server/, 'the reader is told text is missing');
|
|
224
|
+
assert.match(out, /omitted/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('1003222 the remedy precedes the quote block, not stranded under it', () => {
|
|
228
|
+
// The remedy is the actionable sentence. Below twenty lines of git chatter it
|
|
229
|
+
// reads as a footnote to the quotation and gets missed.
|
|
230
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: NON_FF_STDERR });
|
|
231
|
+
const remedy = out.indexOf('reset onto');
|
|
232
|
+
const quote = out.indexOf('what the remote actually said');
|
|
233
|
+
assert.ok(remedy > -1 && quote > -1, 'both present');
|
|
234
|
+
assert.ok(remedy < quote, 'remedy comes first');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('1003222 nothing regressed for a failure carrying no stderr at all', () => {
|
|
238
|
+
const out = fmt({ error: 'publish_failed' });
|
|
239
|
+
assert.match(out, /publish_failed/);
|
|
240
|
+
assert.ok(!out.includes('what the remote actually said'));
|
|
241
|
+
assert.ok(!/undefined|null|\[object/.test(out), 'no placeholder leakage');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
|
|
161
245
|
console.log(`ship_publish_error: ${passed} passed, ${failed} failed`);
|
|
162
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
246
|
+
process.exit(failed === 0 ? 0 : 1);
|