@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -18,7 +18,7 @@ const { captureSurpriseLearning, promptSurprise } = require('./ship-surprise.js'
|
|
|
18
18
|
// task 1003109: the optional `--visual` leg of a ship (goal 1000072).
|
|
19
19
|
const shipVisual = require('./ship-visual.js');
|
|
20
20
|
const { gitOk } = require('./ship-git.js');
|
|
21
|
-
const { autoLinkifyChangedDocs, mergeMainIntoBranch, preflight } = require('./ship-preflight-steps.js');
|
|
21
|
+
const { autoLinkifyChangedDocs, mergeMainIntoBranch, preflight, shipInputPaths } = require('./ship-preflight-steps.js');
|
|
22
22
|
const { applyBaseOverride, computeChangedFiles, computeGraderBaseline, runSmokeAsync } = require('./ship-grade.js');
|
|
23
23
|
const { buildPriorRoundContext, computeGradeResult, gradeAndRecord, printCreditsAndAchievements, printGradeOutcome } = require('./ship-grade-steps.js');
|
|
24
24
|
const { apiErrorLine, normalizeApiError, isUnknownFieldRejection } = require('./ship-io.js');
|
|
@@ -196,7 +196,9 @@ async function shipMain() {
|
|
|
196
196
|
// ---- Step 0: preflight (working tree + branch freshness) ----
|
|
197
197
|
console.log(`Verifying claim #${ac.claim_id} on task #${ac.task_id}: ${ac.title}`);
|
|
198
198
|
console.log('Preflight (working tree + branch freshness)...');
|
|
199
|
-
|
|
199
|
+
// task 1001436: inputFiles keeps --notes-file/--summary-file/--visual out of the
|
|
200
|
+
// preflight's untracked-file warning — they are this ship's input, not its artifact.
|
|
201
|
+
const pre = preflight(ac, { force, inputFiles: shipInputPaths(args) });
|
|
200
202
|
if (!pre.ok) {
|
|
201
203
|
console.error('');
|
|
202
204
|
console.error(`Ship aborted: ${pre.reason}. No claim resolved, no credits awarded.`);
|
|
@@ -929,7 +931,7 @@ async function regradeMain() {
|
|
|
929
931
|
// if the local ship/grade tooling is behind main (a stale grader silently
|
|
930
932
|
// fails the same way — the #662 lesson). --force overrides the freshness leg.
|
|
931
933
|
console.log('Preflight (working tree + branch freshness)...');
|
|
932
|
-
const pre = preflight(claimLike, { force });
|
|
934
|
+
const pre = preflight(claimLike, { force, inputFiles: shipInputPaths(args) });
|
|
933
935
|
if (!pre.ok) {
|
|
934
936
|
console.error('');
|
|
935
937
|
console.error(`Re-grade aborted: ${pre.reason}. Task stays at completed.`);
|
|
@@ -20,6 +20,8 @@ const { plainCardLines } = require('./plain-cards.js');
|
|
|
20
20
|
// gives, and the two can never drift.
|
|
21
21
|
const { preflightChecks } = require('./preflight.js');
|
|
22
22
|
const { computeRawDiffHash, reconstructPanelIssues, runNoArtifactGrader, runSubagentGrader, shouldReuseGrade } = require('./ship-grade.js');
|
|
23
|
+
// task 1002903: the severity gate's own reading of which low scores had no finding.
|
|
24
|
+
const { unbackedLowScores } = require('../../modules/grading/grader-workers/worker-verdict.js');
|
|
23
25
|
|
|
24
26
|
// ---------- Shared grade-step helpers (used by shipMain AND regradeMain) ----------
|
|
25
27
|
//
|
|
@@ -396,6 +398,37 @@ function formatGradeOutcome(g, taskId, cliFinding = null) {
|
|
|
396
398
|
return out.join('\n');
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
// task 1002903: a reviewer scored below threshold and named NO major/blocker
|
|
402
|
+
// finding, so the severity gate (task 1002663) downgraded it — the ship parks for
|
|
403
|
+
// a human rather than hard-failing. The ordinary block below would print "FAIL"
|
|
404
|
+
// and "Fix the issues above" over an empty list, which is how task 1002721's
|
|
405
|
+
// builder read a real grade as a broken one. Sits below the outage and
|
|
406
|
+
// authorization shapes, and steps aside when any finding IS substantive or a
|
|
407
|
+
// gating worker hard-failed (then the ordinary block and its findings apply).
|
|
408
|
+
// The record's own list wins; the workers are the fallback for a grade written
|
|
409
|
+
// before signals.unbacked_scores existed.
|
|
410
|
+
const gradeThreshold = Number(g.threshold);
|
|
411
|
+
const unbacked = Array.isArray(g.signals?.unbacked_scores)
|
|
412
|
+
? g.signals.unbacked_scores
|
|
413
|
+
: Number.isFinite(gradeThreshold) ? unbackedLowScores(g.signals?.workers, { threshold: gradeThreshold }) : [];
|
|
414
|
+
const hasSubstantive = (g.issues || []).some((i) => i && (i.severity === 'blocker' || i.severity === 'major'));
|
|
415
|
+
if (!g.passed && unbacked.length > 0 && !hasSubstantive && g.signals?.panel_outcome !== 'fail') {
|
|
416
|
+
out.push('');
|
|
417
|
+
out.push(`Subagent grade: NOT PASSED — UNBACKED LOW SCORE · a reviewer scored below ${g.threshold ?? '?'} without naming a finding — NOT a findings-backed rejection`);
|
|
418
|
+
for (const u of unbacked) {
|
|
419
|
+
out.push(` ${u.kind}: ${u.axis} ${u.score}/10 — no major or blocker finding stands behind it`);
|
|
420
|
+
}
|
|
421
|
+
if (g.notes_md) out.push(g.notes_md.split('\n').map((l) => ' ' + l).join('\n'));
|
|
422
|
+
out.push('');
|
|
423
|
+
out.push('There is nothing listed to fix. The severity gate does not let an unbacked score hard-fail a ship, so the task stays at completed for a human instead.');
|
|
424
|
+
out.push('Re-grade once — an unbacked score often does not repeat:');
|
|
425
|
+
out.push(` node scripts/gds/ship.js ${taskId} --regrade`);
|
|
426
|
+
out.push('If it repeats on work you can defend, file an override-request with your evidence (recorded; an Archon decides):');
|
|
427
|
+
out.push(` node scripts/gds/api.js POST /api/bongos/tasks/${taskId}/override-request --body-file rationale.json # {"rationale":"<why the grade is wrong, specifically>"}`);
|
|
428
|
+
out.push(...plainCardLines('grade_unbacked_score'));
|
|
429
|
+
return out.join('\n');
|
|
430
|
+
}
|
|
431
|
+
|
|
399
432
|
const verdict = g.passed ? 'PASS' : 'FAIL';
|
|
400
433
|
out.push('');
|
|
401
434
|
out.push(`Subagent grade: ${verdict} · blended ${g.score}/10 · threshold ${g.threshold} (both axes)`);
|
package/scripts/gds/ship-land.js
CHANGED
|
@@ -16,6 +16,9 @@ const { gitArgvOk, gitOk, sanitizeGitHubRefs, shellOk } = require('./ship-git.js
|
|
|
16
16
|
const { buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, deployedCommitStatus, fetchDeployedVersion, ghArgvOk, ghJson, pushVia, resolveInstanceBase } = require('./ship-deploy-target.js');
|
|
17
17
|
const { normalizeApiError } = require('./ship-io.js');
|
|
18
18
|
const { plainCardLines } = require('./plain-cards.js');
|
|
19
|
+
// The server's own reader of a PR's check rollup (task 1003988), reused so the laptop
|
|
20
|
+
// poll names a red PR exactly the way publish-status does (task 1003764). Dependency-free.
|
|
21
|
+
const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('../../modules/lifecycle/check-rollup.js');
|
|
19
22
|
|
|
20
23
|
// ---------- the land outcome (task 1002541, part 1) ----------
|
|
21
24
|
//
|
|
@@ -284,7 +287,19 @@ function strandNextStep(taskId, via) {
|
|
|
284
287
|
// too: the server flags a strand needs_rebase (definitive shapes within a sweep,
|
|
285
288
|
// ambiguous ones at ~30 min), /builder-start warns, and new claims gate on it.
|
|
286
289
|
// Pure + exported so the wording is pinned by tests/ship_ci_deploy.mjs.
|
|
287
|
-
|
|
290
|
+
//
|
|
291
|
+
// task 1003764: `red` — the PR was RED on the poll's last read. "NOT a failure" is then
|
|
292
|
+
// false: a required check that failed does not pass by waiting, so the banner says the
|
|
293
|
+
// land is stuck on it and defers to the red lines above for the remedy.
|
|
294
|
+
function landPendingLines(taskId, { merged, red } = {}) {
|
|
295
|
+
if (red) {
|
|
296
|
+
return [
|
|
297
|
+
' ci-land: Published, but the PR is RED (the check named above), so it has NOT merged and will not',
|
|
298
|
+
' until that check passes. The task stays at \'confirmed\'. Do what the lines above say, then re-run',
|
|
299
|
+
` \`node scripts/gds/ship.js ${taskId}\` — it resumes from here (task 941). If it stays red, the server flags`,
|
|
300
|
+
' the task needs_rebase — /builder-start will warn you and new claims are gated until it lands or is released.',
|
|
301
|
+
];
|
|
302
|
+
}
|
|
288
303
|
return [
|
|
289
304
|
` ci-land: ✓ Published — auto-merge is armed${merged ? ' and the PR has merged' : ''}. This is NOT a failure.`,
|
|
290
305
|
` CI is still finishing; GitHub will ${merged ? 'deploy' : 'merge + deploy'} automatically once the required checks pass,`,
|
|
@@ -298,7 +313,9 @@ function landPendingLines(taskId, { merged } = {}) {
|
|
|
298
313
|
function printLandPending(taskId, opts = {}) {
|
|
299
314
|
console.log('');
|
|
300
315
|
for (const line of landPendingLines(taskId, opts)) console.log(line);
|
|
301
|
-
|
|
316
|
+
// The land_pending card says there is nothing to do, which a red PR makes untrue; the
|
|
317
|
+
// red line already printed the failing_check card (task 1003764).
|
|
318
|
+
if (!opts.red) for (const line of plainCardLines('land_pending')) console.log(line);
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
// task 1002645 — the honest close-out when the PR MERGED but the server refused the
|
|
@@ -349,9 +366,150 @@ function redPrLine(md) {
|
|
|
349
366
|
// redPrLines — the red line followed by the same reason in plain words (task 1003989).
|
|
350
367
|
// The card is keyed on the merge reason; when the failing tests were readable that
|
|
351
368
|
// reason is `failing_check:<name>`, which resolves to the failing_check card.
|
|
352
|
-
|
|
369
|
+
// task 1003764: the merge trap sits between them. `opts` = { taskId, via } for its wording.
|
|
370
|
+
function redPrLines(md, opts = {}) {
|
|
353
371
|
const red = redPrLine(md);
|
|
354
|
-
return red ? [red, ...plainCardLines((md && md.reason) || 'failing_check')] : [];
|
|
372
|
+
return red ? [red, ...mergeTrapLines(md, opts), ...plainCardLines((md && md.reason) || 'failing_check')] : [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The unit tests that gate GENERATED files — each compares a committed artifact with a
|
|
376
|
+
// fresh build of it. `fitness` holds the combined gate (fitness.js "generated artifacts
|
|
377
|
+
// fresh": copy registry, API docs, file map, nested CLAUDE.md blocks, the §13 snippet).
|
|
378
|
+
// tests/ship_land_red_pr.mjs fails if one of these names stops being a real test file.
|
|
379
|
+
const FRESHNESS_TESTS = new Set(['fitness', 'api_docs', 'api_client', 'file_map', 'repo_map', 'session_index', 'interaction_posture']);
|
|
380
|
+
|
|
381
|
+
// failingTestNames — "2 failed: a, b" (the unit-report line, run-unit-tests.js
|
|
382
|
+
// describeFailures) → ['a', 'b']. A line cut at the status limit ends in '…'; the cut
|
|
383
|
+
// name is dropped rather than guessed at. Pure.
|
|
384
|
+
function failingTestNames(text) {
|
|
385
|
+
const m = /^\s*\d+\s+failed:\s*(.*)$/.exec(String(text || ''));
|
|
386
|
+
if (!m) return [];
|
|
387
|
+
const names = m[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
388
|
+
if (names.length && names[names.length - 1].endsWith('…')) names.pop();
|
|
389
|
+
return names;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// mergeTrapLines — task 1003764. CI tests the MERGE, not the branch: a pull_request run
|
|
393
|
+
// checks out the PR merged into the CURRENT main, so a branch that was green when pushed
|
|
394
|
+
// goes red when main moves under it, and re-running the test on the branch shows nothing
|
|
395
|
+
// wrong. Task 1003747's land sat "waiting" for ~20 minutes of git archaeology on exactly
|
|
396
|
+
// that — main moved six commits, the copy registry went stale in the merged tree only.
|
|
397
|
+
// Every red PR gets the trap named; a failing freshness gate also gets its healing command,
|
|
398
|
+
// which differs by push path (a credential-less machine cannot merge main locally, and the
|
|
399
|
+
// server heals it for them — publish-reconciler refreshStaleGenerated). Pure + exported.
|
|
400
|
+
function mergeTrapLines(md, { taskId = '<id>', via = 'local' } = {}) {
|
|
401
|
+
const lines = [
|
|
402
|
+
' ci-land: CI tests your branch MERGED with the current main, not the branch alone — if the failure',
|
|
403
|
+
via === 'server'
|
|
404
|
+
? ' does not reproduce on your branch, main moved under the PR after it was pushed.'
|
|
405
|
+
: ' does not reproduce on your branch, main moved under it: `git fetch origin && git merge origin/main`, then re-run it.',
|
|
406
|
+
];
|
|
407
|
+
const gates = failingTestNames(md && md.failing_tests).filter((n) => FRESHNESS_TESTS.has(n));
|
|
408
|
+
if (!gates.length) return lines;
|
|
409
|
+
const named = gates.map((g) => `\`${g}\``).join(', ');
|
|
410
|
+
lines.push(` ci-land: ${named} ${gates.length > 1 ? 'are freshness gates' : 'is a freshness gate'}, so "fix it" here means regenerate: a generated file (copy`);
|
|
411
|
+
lines.push(' registry, API docs, repo map…) is stale in the MERGED tree even though your branch was fresh.');
|
|
412
|
+
if (via === 'server') {
|
|
413
|
+
lines.push(' The server regenerates stale generated files on the merged tree itself (every ~5 min, up to 3 tries),');
|
|
414
|
+
lines.push(' so this usually clears without you; if it stays red the server flags the task needs_rebase.');
|
|
415
|
+
} else {
|
|
416
|
+
lines.push(` Heal it: \`git fetch origin && git merge origin/main\`, then re-run \`node scripts/gds/ship.js ${taskId}\` — the ship`);
|
|
417
|
+
lines.push(' regenerates every generated file on the merged tree before it pushes (`node scripts/gds/ship-check.js` shows which).');
|
|
418
|
+
}
|
|
419
|
+
lines.push(' A busy main can re-stale it while CI re-runs; a repeat is that race, not a new fault.');
|
|
420
|
+
return lines;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// redBlocksMerge — is this red the kind that stops the merge? A failing REQUIRED check
|
|
424
|
+
// (`failing_check…`) does; a failing optional one does not (GitHub still merges an UNSTABLE
|
|
425
|
+
// PR), so only the first may turn the closing banner red (task 1003764). Pure + exported.
|
|
426
|
+
function redBlocksMerge(md) {
|
|
427
|
+
return /^failing_check/.test(String((md && md.reason) || ''));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// A check conclusion (CheckRun) or state (status context) that means FAILED. Pending and
|
|
431
|
+
// expected are waiting, not red — reporting them as failures would be the mirror image of
|
|
432
|
+
// the bug this names (a failure dressed as waiting).
|
|
433
|
+
const RED_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'TIMED_OUT', 'CANCELLED', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);
|
|
434
|
+
|
|
435
|
+
// localMergeDriver — task 1003764. The laptop path watches its PR with the builder's own
|
|
436
|
+
// gh, and read only state + mergeCommit, so a red required check was invisible for the
|
|
437
|
+
// whole wait. This reads `gh pr view --json statusCheckRollup,mergeStateStatus,headRefOid`
|
|
438
|
+
// through the server's parser and answers in publish-status's merge_driver shape, so both
|
|
439
|
+
// poll paths hand redPrLines the same thing. `failingTestsFor(sha)` reads the unit-report
|
|
440
|
+
// status (gh pr view omits a status's description) and is asked only once that status is
|
|
441
|
+
// itself red, i.e. posted. Null when nothing is red. Pure + exported.
|
|
442
|
+
function localMergeDriver(pr, failingTestsFor = () => null) {
|
|
443
|
+
if (!pr || pr.state !== 'OPEN' || !Array.isArray(pr.statusCheckRollup)) return null;
|
|
444
|
+
const { checks } = readCheckRollup({ contexts: { nodes: pr.statusCheckRollup } });
|
|
445
|
+
const red = checks.filter((c) => RED_CONCLUSIONS.has(String(c.conclusion || '').toUpperCase()));
|
|
446
|
+
if (!red.length) return null;
|
|
447
|
+
const kind = pr.mergeStateStatus === 'UNSTABLE' ? 'non_required_check_failing' : 'failing_check';
|
|
448
|
+
const reported = red.some((c) => c.name === UNIT_REPORT_CONTEXT);
|
|
449
|
+
return { reason: `${kind}:${red[0].name}`, failing_tests: (reported && failingTestsFor(pr.headRefOid)) || null };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// unitReportFor — the failing-test line the `unit` workflow posted on `sha`, read with this
|
|
453
|
+
// machine's gh through the same parser the server uses. Null when unreadable; the red line
|
|
454
|
+
// then names the check instead, as it did before the report existed. `gh` is injectable.
|
|
455
|
+
function unitReportFor(sha, gh = ghJson) {
|
|
456
|
+
if (!/^[0-9a-f]{7,64}$/i.test(String(sha || ''))) return null;
|
|
457
|
+
const combined = gh(['api', `repos/{owner}/{repo}/commits/${sha}/status`]);
|
|
458
|
+
const nodes = ((combined && combined.statuses) || []).map((s) => ({ ...s, __typename: 'StatusContext' }));
|
|
459
|
+
return readCheckRollup({ contexts: { nodes } }).failing_tests;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// task 1003499 — a CONFLICTING PR is a land that cannot happen, however green it is.
|
|
463
|
+
// GitHub's auto-merge waits for a clean merge as well as for green checks, so a PR
|
|
464
|
+
// whose branch conflicts with main sits open under a fully green rollup and never
|
|
465
|
+
// merges. PR #497 sat like that for ~25 minutes while both polls below said nothing
|
|
466
|
+
// and then printed "auto-merge is armed ... NOT a failure", the banner a land that IS
|
|
467
|
+
// on its way gets. The server already knew (mergeBlockReason -> 'merge_conflict'); the
|
|
468
|
+
// ship never asked. Pure + exported. Takes both shapes it is handed: `gh pr view`'s
|
|
469
|
+
// camelCase (mergeable, mergeStateStatus) and publish-status's snake_case merge_driver
|
|
470
|
+
// (mergeable, merge_state, reason). A read that failed (null) is not a conflict.
|
|
471
|
+
function prConflicting(pr) {
|
|
472
|
+
if (!pr) return false;
|
|
473
|
+
const state = pr.mergeStateStatus ?? pr.merge_state;
|
|
474
|
+
return pr.mergeable === 'CONFLICTING' || state === 'DIRTY' || pr.reason === 'merge_conflict';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// What the poll says the moment it sees the conflict. Pure + exported so the wording
|
|
478
|
+
// is pinned by tests/ship_land_conflict.mjs. The poll keeps waiting afterwards on
|
|
479
|
+
// purpose: a conflict only in generated files is healed by the server's own resolver
|
|
480
|
+
// on its next sweep (ADR 0082), and the PR then merges inside this same window.
|
|
481
|
+
function conflictPrLines(taskId) {
|
|
482
|
+
return [
|
|
483
|
+
' ci-land: the PR CONFLICTS with main, so auto-merge cannot fire until that is resolved, however green its checks are.',
|
|
484
|
+
' ci-land: a conflict only in generated files is healed by the server on its next sweep (~5 min), so this keeps waiting.',
|
|
485
|
+
' ci-land: anything hand-written waits for you. From this worktree:',
|
|
486
|
+
' git fetch -q origin && git merge origin/main # resolve the conflicts, commit',
|
|
487
|
+
` node scripts/gds/ship.js ${taskId} # resumes the land; it does not re-grade`,
|
|
488
|
+
...plainCardLines('merge_conflict'),
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// noteConflict — one poll's worth of conflict tracking, shared by both loops. Prints
|
|
493
|
+
// the lines above when the PR BECOMES conflicting (not on every 10s poll, and once
|
|
494
|
+
// more if main moves on and undoes a heal), and returns whether it conflicts now.
|
|
495
|
+
// A failed read keeps the last answer rather than inventing a new one.
|
|
496
|
+
function noteConflict(taskId, pr, wasConflicting) {
|
|
497
|
+
if (!pr) return wasConflicting;
|
|
498
|
+
const now = prConflicting(pr);
|
|
499
|
+
if (now && !wasConflicting) for (const line of conflictPrLines(taskId)) console.log(line);
|
|
500
|
+
return now;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// landConflicted — the verdict when the wait ends with the PR STILL conflicting. It is
|
|
504
|
+
// not landing on its own, so it must not print the "landing automatically" banner or
|
|
505
|
+
// the land_pending card ("Nothing to do. This is normal, not a failure."): task
|
|
506
|
+
// 1002541's rule that a land which is not happening must not announce itself as
|
|
507
|
+
// success. No bailId, so ship-merge never reroutes it: the PR already exists and is
|
|
508
|
+
// the problem. /strand-fix walks the cure (its case C: merge main in, resolve, re-ship).
|
|
509
|
+
function landConflicted(taskId) {
|
|
510
|
+
console.log('');
|
|
511
|
+
console.log(` ci-land: still CONFLICTING when the wait ended, so this will NOT land on its own. The task stays at 'confirmed' until the conflict is resolved (/strand-fix ${taskId} walks it).`);
|
|
512
|
+
return landBailed('the PR conflicts with main, so it cannot merge until the conflict is resolved', `/strand-fix ${taskId}`);
|
|
355
513
|
}
|
|
356
514
|
|
|
357
515
|
// One module-scope sleep command shared by both poll loops (task 1093 / C9). A
|
|
@@ -366,8 +524,12 @@ const POLL_SLEEP_CMD = 'node -e "setTimeout(()=>{}, 10000)"';
|
|
|
366
524
|
// reconciles the task to shipped when it observes merge+deploy — task 1049) until
|
|
367
525
|
// the PR merges + deploy-prod succeeds. Returns true when prod is confirmed live;
|
|
368
526
|
// a timeout with an armed PR is success-PENDING (the publish worked), not failure.
|
|
369
|
-
|
|
527
|
+
// A timeout with a CONFLICTING PR is not (task 1003499): see landConflicted.
|
|
528
|
+
// `deps` exists for tests/ship_land_conflict.mjs; a real ship passes none.
|
|
529
|
+
async function pollServerPublish(taskId, branch, deps = {}) {
|
|
370
530
|
const TRIES = 54; // ~9 min at 10s — checks + auto-merge + Actions deploy
|
|
531
|
+
const readStatus = deps.readStatus || (async (q) => (await cliClient()).tasks.getTasksIdPublishStatus(q));
|
|
532
|
+
const pause = deps.pause || (() => shellOk(POLL_SLEEP_CMD));
|
|
371
533
|
console.log(' ci-land: waiting for auto-merge + prod deploy (polling the server)...');
|
|
372
534
|
// task 1002645: GitHub can report merged+deployed while the SERVER refused the
|
|
373
535
|
// confirmed→shipped flip (reconcile_blocked — the task-1547 land proof missed).
|
|
@@ -376,14 +538,29 @@ async function pollServerPublish(taskId, branch) {
|
|
|
376
538
|
// pushed tip, so the proof usually heals within the window.
|
|
377
539
|
let blockedReason = null;
|
|
378
540
|
let redLine = null;
|
|
541
|
+
let conflicting = false;
|
|
542
|
+
let stuckRed = false; // task 1003764: a failing required check on the LAST read turns the closing banner red
|
|
543
|
+
let readFailure = null;
|
|
379
544
|
for (let i = 0; i < TRIES; i++) {
|
|
380
|
-
const r = await (
|
|
545
|
+
const r = await readStatus({ id: taskId, query: { branch } });
|
|
546
|
+
if (!r.ok) {
|
|
547
|
+
// task 1003764: a failed status read used to be skipped in silence, so a broken
|
|
548
|
+
// diagnostic looked exactly like slow CI. Say so once per distinct failure.
|
|
549
|
+
const { code } = normalizeApiError(r.data);
|
|
550
|
+
const why = `HTTP ${r.status}${code ? ` ${code}` : ''}`;
|
|
551
|
+
if (why !== readFailure) {
|
|
552
|
+
readFailure = why;
|
|
553
|
+
console.log(` ci-land: could not read the land status from the server (${why}) — still polling.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
381
556
|
if (r.ok && r.data) {
|
|
382
557
|
const red = redPrLine(r.data.merge_driver);
|
|
383
558
|
if (red && red !== redLine) {
|
|
384
559
|
redLine = red;
|
|
385
|
-
for (const line of redPrLines(r.data.merge_driver)) console.log(line);
|
|
560
|
+
for (const line of redPrLines(r.data.merge_driver, { taskId, via: 'server' })) console.log(line);
|
|
386
561
|
}
|
|
562
|
+
stuckRed = !!red && redBlocksMerge(r.data.merge_driver);
|
|
563
|
+
conflicting = noteConflict(taskId, r.data.merge_driver, conflicting);
|
|
387
564
|
const outcome = classifyLandOutcome(r.data);
|
|
388
565
|
if (outcome === 'shipped') {
|
|
389
566
|
if (r.data.reconcile_blocked) {
|
|
@@ -401,7 +578,7 @@ async function pollServerPublish(taskId, branch) {
|
|
|
401
578
|
return landBailed('the PR was closed without merging (a check failed, or it was closed manually)', `node scripts/gds/ship.js ${taskId}`);
|
|
402
579
|
}
|
|
403
580
|
}
|
|
404
|
-
|
|
581
|
+
pause();
|
|
405
582
|
}
|
|
406
583
|
if (blockedReason) {
|
|
407
584
|
console.log('');
|
|
@@ -409,7 +586,8 @@ async function pollServerPublish(taskId, branch) {
|
|
|
409
586
|
for (const line of plainCardLines('reconcile_blocked')) console.log(line);
|
|
410
587
|
return landBailed(`the PR merged but the server could not prove the land is this task's: ${blockedReason}`, `node scripts/gds/ship.js ${taskId}`);
|
|
411
588
|
}
|
|
412
|
-
|
|
589
|
+
if (conflicting) return landConflicted(taskId);
|
|
590
|
+
printLandPending(taskId, { red: stuckRed });
|
|
413
591
|
return landPending();
|
|
414
592
|
}
|
|
415
593
|
|
|
@@ -723,14 +901,33 @@ async function ciLandLocal(taskId, branch, valueSummary) {
|
|
|
723
901
|
// 3. public edge healthz -> 200 (belt-and-braces, a different vantage).
|
|
724
902
|
// Times out conservatively; on any timeout the task stays at confirmed with a
|
|
725
903
|
// precise next step rather than a false "shipped".
|
|
726
|
-
|
|
904
|
+
// `deps` exists for tests/ship_land_conflict.mjs; a real ship passes none.
|
|
905
|
+
function pollCiDeploy(taskId, branch, headSha, deps = {}) {
|
|
727
906
|
const MERGE_TRIES = 30; // ~5 min at 10s — checks + auto-merge
|
|
728
907
|
const DEPLOY_TRIES = 24; // ~4 min at 10s — Actions deploy
|
|
908
|
+
const readJson = deps.ghJson || ghJson;
|
|
909
|
+
const pause = deps.pause || (() => shellOk(POLL_SLEEP_CMD));
|
|
729
910
|
|
|
730
911
|
console.log(' ci-land: waiting for auto-merge (checks + branch protection)...');
|
|
731
912
|
let mergeSha = null;
|
|
913
|
+
let conflicting = false;
|
|
914
|
+
// task 1003764: say WHY it has not merged. This loop read only state + mergeCommit,
|
|
915
|
+
// so a red required check — the likeliest reason — went unnamed for the whole wait.
|
|
916
|
+
// The unit-report line (which failing tests) is fetched once per head sha.
|
|
917
|
+
let redLine = null;
|
|
918
|
+
let stuckRed = false;
|
|
919
|
+
let report = { sha: undefined, text: null };
|
|
920
|
+
const failingTestsFor = (sha) => {
|
|
921
|
+
if (sha !== report.sha) report = { sha, text: unitReportFor(sha, readJson) };
|
|
922
|
+
return report.text;
|
|
923
|
+
};
|
|
732
924
|
for (let i = 0; i < MERGE_TRIES; i++) {
|
|
733
|
-
|
|
925
|
+
// task 1003499: mergeable + mergeStateStatus ride the same read, so a conflict is
|
|
926
|
+
// named on the poll it appears; the two-field read is the fallback for a gh that
|
|
927
|
+
// predates those fields, which keeps this no worse than before on any machine.
|
|
928
|
+
// task 1003764: so do headRefOid + statusCheckRollup, so a red check is named too.
|
|
929
|
+
const pr = readJson(['pr', 'view', branch, '--json', 'state,mergeCommit,mergeable,mergeStateStatus,headRefOid,statusCheckRollup'])
|
|
930
|
+
|| readJson(['pr', 'view', branch, '--json', 'state,mergeCommit']);
|
|
734
931
|
if (pr && pr.state === 'MERGED') {
|
|
735
932
|
mergeSha = pr.mergeCommit && pr.mergeCommit.oid;
|
|
736
933
|
break;
|
|
@@ -739,18 +936,31 @@ function pollCiDeploy(taskId, branch, headSha) {
|
|
|
739
936
|
console.log(` ci-land: PR was CLOSED without merging (a required check failed, or it was closed manually). Fix it and re-run \`node scripts/gds/ship.js ${taskId}\` to resume. Task stays at 'confirmed'.`);
|
|
740
937
|
return landBailed('the PR was closed without merging (a check failed, or it was closed manually)', `node scripts/gds/ship.js ${taskId}`);
|
|
741
938
|
}
|
|
742
|
-
|
|
939
|
+
if (pr) {
|
|
940
|
+
const md = localMergeDriver(pr, failingTestsFor);
|
|
941
|
+
const red = redPrLine(md);
|
|
942
|
+
if (red && red !== redLine) {
|
|
943
|
+
redLine = red;
|
|
944
|
+
for (const line of redPrLines(md, { taskId, via: 'local' })) console.log(line);
|
|
945
|
+
}
|
|
946
|
+
stuckRed = !!red && redBlocksMerge(md);
|
|
947
|
+
}
|
|
948
|
+
conflicting = noteConflict(taskId, pr, conflicting);
|
|
949
|
+
pause();
|
|
743
950
|
}
|
|
744
951
|
if (!mergeSha) {
|
|
745
952
|
// task 1049: PR open + auto-merge armed but not merged in-window = the publish
|
|
746
|
-
// SUCCEEDED and is still in CI — success-pending, not a failure.
|
|
747
|
-
|
|
953
|
+
// SUCCEEDED and is still in CI — success-pending, not a failure. Unless it is
|
|
954
|
+
// CONFLICTING (task 1003499): then nothing is still in CI, and it never merges.
|
|
955
|
+
// Or RED (task 1003764): then the banner says the land is stuck on that check.
|
|
956
|
+
if (conflicting) return landConflicted(taskId);
|
|
957
|
+
printLandPending(taskId, { red: stuckRed });
|
|
748
958
|
return landPending();
|
|
749
959
|
}
|
|
750
960
|
console.log(` ci-land: merged to main (${mergeSha.slice(0, 8)}). Waiting for the prod deploy...`);
|
|
751
961
|
|
|
752
962
|
for (let i = 0; i < DEPLOY_TRIES; i++) {
|
|
753
|
-
const runs =
|
|
963
|
+
const runs = readJson([
|
|
754
964
|
'run', 'list', '--workflow', 'deploy-prod.yml', '--branch', 'main',
|
|
755
965
|
'--json', 'headSha,status,conclusion', '--limit', '15',
|
|
756
966
|
]);
|
|
@@ -775,7 +985,7 @@ function pollCiDeploy(taskId, branch, headSha) {
|
|
|
775
985
|
}
|
|
776
986
|
return landed();
|
|
777
987
|
}
|
|
778
|
-
|
|
988
|
+
pause();
|
|
779
989
|
}
|
|
780
990
|
// task 1049: merged but the deploy run hasn't confirmed in-window — still
|
|
781
991
|
// success-pending (it deploys on its own), not a failure.
|
|
@@ -811,6 +1021,21 @@ module.exports = {
|
|
|
811
1021
|
prAssigneeLine,
|
|
812
1022
|
redPrLine,
|
|
813
1023
|
redPrLines,
|
|
1024
|
+
// task 1003499: a CONFLICTING PR is named while the land waits, and a wait that
|
|
1025
|
+
// ends conflicting is a bail, not "landing automatically". The polls are exported
|
|
1026
|
+
// with their optional `deps` so the test drives the real loops.
|
|
1027
|
+
prConflicting,
|
|
1028
|
+
conflictPrLines,
|
|
1029
|
+
landConflicted,
|
|
1030
|
+
pollCiDeploy,
|
|
1031
|
+
pollServerPublish,
|
|
1032
|
+
// task 1003764: name WHY a published PR has not merged when a check is red.
|
|
1033
|
+
FRESHNESS_TESTS,
|
|
1034
|
+
failingTestNames,
|
|
1035
|
+
mergeTrapLines,
|
|
1036
|
+
redBlocksMerge,
|
|
1037
|
+
localMergeDriver,
|
|
1038
|
+
unitReportFor,
|
|
814
1039
|
reconciledCardStatus,
|
|
815
1040
|
strandCommand,
|
|
816
1041
|
strandNextStep,
|
|
@@ -32,6 +32,7 @@ const { gateMergeResult } = require('./ship-merge-gate.js');
|
|
|
32
32
|
const { dbOnlyIgnoredNotice } = require('./ship-honesty.js');
|
|
33
33
|
const { currentBranch, mainWorktreeBusy, regenerateApiClient, regenerateApiDocs, regenerateCopyInventory, regenerateDiagrams, regenerateFileMap, regenerateRepoMap, regenerateSessionIndex } = require('./ship-regen.js');
|
|
34
34
|
const { REPO_ROOT } = require('./ship-state.js');
|
|
35
|
+
const { envPrefix } = require('../../src/instance-config');
|
|
35
36
|
|
|
36
37
|
// zeroCommitVerdict — dbOnly is deliberately consulted ONLY at zero commits
|
|
37
38
|
// ahead: a branch carrying real commits must be merged, whatever flag was typed.
|
|
@@ -287,7 +288,7 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
287
288
|
|
|
288
289
|
console.log(' pushing merged main to origin...');
|
|
289
290
|
// SEC #863: route through push-main.js, which carries the
|
|
290
|
-
//
|
|
291
|
+
// <PREFIX>_ALLOW_MAIN_PUSH override so the pre-push permission gate lets THIS
|
|
291
292
|
// graded+confirmed pipeline push through while still blocking ad-hoc
|
|
292
293
|
// `git push origin main`. process.execPath (quoted) for the same
|
|
293
294
|
// cross-platform reason as the other node spawns here (#660).
|
|
@@ -298,7 +299,7 @@ async function mergeLocally(taskId, valueSummary, { dbOnly = false } = {}) {
|
|
|
298
299
|
// the ci PR path before this point; reaching this with protection on
|
|
299
300
|
// means an env-forced 'laptop' or a failed protection probe.
|
|
300
301
|
if (mainIsProtected() === true) {
|
|
301
|
-
return landBailed(
|
|
302
|
+
return landBailed(`main is branch-protected — the direct push can never land; unset the DEPLOY_MODE env override (${envPrefix()}_DEPLOY_MODE, or the legacy OTB_DEPLOY_MODE) — the ci PR path lands it`, `node scripts/gds/ship.js ${taskId}`);
|
|
302
303
|
}
|
|
303
304
|
return landBailed('could not push the merged main to origin', '/merge-mode');
|
|
304
305
|
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
-
const { trackedDirty, cleanupMacFinderDupRefs } = require('../../modules/lifecycle/ship-preflight');
|
|
14
|
+
const { trackedDirty, cleanupMacFinderDupRefs, untrackedLeftBehind, untrackedWarningLines } = require('../../modules/lifecycle/ship-preflight');
|
|
15
|
+
const { arg } = require('./cli-lib');
|
|
15
16
|
const mergeRegen = require('./git-merge-regen');
|
|
16
17
|
const { argvOut, gitArgvOk, gitOk } = require('./ship-git.js');
|
|
17
18
|
const { pushVia } = require('./ship-deploy-target.js');
|
|
@@ -40,6 +41,63 @@ function parseDeletedUpstreamPaths(rawLogOutput) {
|
|
|
40
41
|
return [...new Set(rawLogOutput.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// shipInputPaths — the repo-relative paths of the files a ship was HANDED as input
|
|
45
|
+
// (task 1001436), so preflight's untracked-file warning never lists them. The
|
|
46
|
+
// builder-ship skill writes notes.md + summary.txt into the worktree and passes them
|
|
47
|
+
// here; left in, they would put the warning on nearly every ship and teach everyone
|
|
48
|
+
// to skip it. `-` (stdin), a missing value and a path outside the checkout add
|
|
49
|
+
// nothing — a file outside the tree cannot be an untracked file in it. Both sides go
|
|
50
|
+
// through realpath, so a /var vs /private/var spelling of one file still matches.
|
|
51
|
+
// `cwd`, `repoRoot` and `realpath` are injectable for the unit test.
|
|
52
|
+
const SHIP_INPUT_FLAGS = ['--notes-file', '--summary-file', '--response-file', '--visual'];
|
|
53
|
+
|
|
54
|
+
function safeRealpath(p) {
|
|
55
|
+
try { return fs.realpathSync.native(p); } catch (_) { return path.resolve(p); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shipInputPaths(args, { cwd = process.cwd(), repoRoot = null, realpath = safeRealpath } = {}) {
|
|
59
|
+
const root = realpath(repoRoot || gitOk('git rev-parse --show-toplevel') || cwd);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const flag of SHIP_INPUT_FLAGS) {
|
|
62
|
+
const value = arg(flag, args || []);
|
|
63
|
+
if (typeof value !== 'string' || value === '' || value === '-' || value.startsWith('--')) continue;
|
|
64
|
+
const rel = path.relative(root, realpath(path.resolve(cwd, value)));
|
|
65
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
66
|
+
out.push(rel.split(path.sep).join('/'));
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The code that RUNS the ship and the grade on this machine — what step (c) of
|
|
72
|
+
// preflight() refuses to run from a stale copy. PATHSPECS, not a file list (task
|
|
73
|
+
// 1004273): the list used to name ship.js and grader.js alone, and two carves moved
|
|
74
|
+
// the logic out from under it (task 1003205 split ship.js into ~20 ship-*.js files;
|
|
75
|
+
// grader.js now delegates to grader-*.js, grader-workers/ and grade-engine.js), so a
|
|
76
|
+
// branch behind main on ship-land.js or grader-score.js sailed through. A prefix
|
|
77
|
+
// covers the next carve too. They reach git as argv, never through a shell, so a
|
|
78
|
+
// file that exists ONLY on origin/main (a new carve file) still matches — a shell
|
|
79
|
+
// would have expanded the glob against this checkout and missed exactly that one.
|
|
80
|
+
// tests/ship_preflight.mjs fails if a ship*.js / grader* / grade-engine file is not covered.
|
|
81
|
+
const TOOLING_PATHSPECS = Object.freeze([
|
|
82
|
+
'scripts/gds/ship*.js', // ship.js + every ship-*.js carved out of it
|
|
83
|
+
'scripts/gds/smoke-gds.js',
|
|
84
|
+
'scripts/gds/cli-lib.js',
|
|
85
|
+
'modules/grading/grader*', // grader.js, grader-*.js, grader-rubric.json, grader-workers/
|
|
86
|
+
'modules/grading/grade-engine.js', // grader.js requires it (task 1002839)
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// staleToolingFiles — the tooling files origin/main has changed since this branch's
|
|
90
|
+
// merge-base. Three-dot (HEAD...origin/main) scopes to the origin/main side, so the
|
|
91
|
+
// builder's OWN edits to these files (shipping a ship fix) do not count. A failed git
|
|
92
|
+
// call reads as nothing stale, as it always has. `cwd` is injectable for the test.
|
|
93
|
+
function staleToolingFiles({ cwd } = {}) {
|
|
94
|
+
const out = argvOut('git', ['diff', '--name-only', 'HEAD...origin/main', '--', ...TOOLING_PATHSPECS], cwd ? { cwd } : {});
|
|
95
|
+
return out ? out.split('\n').map((s) => s.trim()).filter(Boolean) : [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// opts: { force, inputFiles } — inputFiles is shipInputPaths(args) from the caller;
|
|
99
|
+
// omitted (the resume path has no prose flags), nothing is excluded from the
|
|
100
|
+
// untracked-file warning.
|
|
43
101
|
function preflight(claim, opts = {}) {
|
|
44
102
|
// (a) Working-tree check — refuse if any TRACKED file carries an uncommitted
|
|
45
103
|
// modification or staged change. This fires post-commit, so a clean tree
|
|
@@ -92,14 +150,7 @@ function preflight(claim, opts = {}) {
|
|
|
92
150
|
// (HEAD...origin/main) scopes to changes on the origin/main side since the
|
|
93
151
|
// merge-base, so the builder's OWN edits to these files (e.g. shipping a
|
|
94
152
|
// ship.js fix) do not trip it.
|
|
95
|
-
const
|
|
96
|
-
'scripts/gds/ship.js',
|
|
97
|
-
'scripts/gds/smoke-gds.js',
|
|
98
|
-
'scripts/gds/cli-lib.js',
|
|
99
|
-
'modules/grading/grader.js', // BV1.R73: grading carved to modules/grading/
|
|
100
|
-
];
|
|
101
|
-
const staleToolingStr = gitOk(`git diff --name-only HEAD...origin/main -- ${TOOLING_PATHS.join(' ')}`);
|
|
102
|
-
const staleTooling = staleToolingStr ? staleToolingStr.trim().split('\n').filter(Boolean) : [];
|
|
153
|
+
const staleTooling = staleToolingFiles();
|
|
103
154
|
if (staleTooling.length > 0) {
|
|
104
155
|
if (opts.force) {
|
|
105
156
|
console.log(` preflight: ship tooling is behind origin/main (${staleTooling.join(', ')}) — proceeding due to --force`);
|
|
@@ -145,8 +196,29 @@ function preflight(claim, opts = {}) {
|
|
|
145
196
|
}
|
|
146
197
|
}
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
199
|
+
// (e) Untracked-file warning (task 1001436) — WARN-ONLY, never blocks, never
|
|
200
|
+
// changes the exit code. Step (a) ignores untracked files so scratch cannot
|
|
201
|
+
// false-refuse a ship, which also meant a NEW deliverable left un-added beside
|
|
202
|
+
// committed work shipped without it, in silence (the residual of the task-1363
|
|
203
|
+
// phantom ship). This names those files while stopping is still free. It needs
|
|
204
|
+
// committed work to warn about — upstreamDelTouched is this branch's committed
|
|
205
|
+
// diff from step (d); with none, the empty-diff guard already refuses and lists
|
|
206
|
+
// them. Its own `git status`, on purpose: --untracked-files=all names each file
|
|
207
|
+
// rather than a collapsed `dir/`, so the ship's input files can be excluded
|
|
208
|
+
// exactly; and a failed or oversized read here stays a skipped warning, never a
|
|
209
|
+
// blank porcelain for the refusal in step (a).
|
|
210
|
+
const untrackedPorcelain = gitOk('git status --porcelain --untracked-files=all', { maxBuffer: 64 * 1024 * 1024 });
|
|
211
|
+
const leftBehind = untrackedLeftBehind(untrackedPorcelain, {
|
|
212
|
+
committedFileCount: upstreamDelTouched.length,
|
|
213
|
+
inputs: opts.inputFiles || [],
|
|
214
|
+
});
|
|
215
|
+
for (const line of untrackedWarningLines(leftBehind, { taskId: claim && claim.task_id })) console.error(line);
|
|
216
|
+
|
|
217
|
+
const treeState = leftBehind.length > 0
|
|
218
|
+
? `tracked files clean (${leftBehind.length} untracked file(s) will not ship — see the warning)`
|
|
219
|
+
: 'working tree clean';
|
|
220
|
+
console.log(` preflight: ${treeState}, branch ${behind} commits behind main, ship tooling current — OK`);
|
|
221
|
+
return { ok: true, untrackedLeftBehind: leftBehind };
|
|
150
222
|
}
|
|
151
223
|
|
|
152
224
|
// mergeMainIntoBranch — pull origin/main INTO the current feature branch BEFORE
|
|
@@ -320,4 +392,7 @@ module.exports = {
|
|
|
320
392
|
mergeMainIntoBranch,
|
|
321
393
|
parseDeletedUpstreamPaths,
|
|
322
394
|
preflight,
|
|
395
|
+
shipInputPaths,
|
|
396
|
+
staleToolingFiles,
|
|
397
|
+
TOOLING_PATHSPECS,
|
|
323
398
|
};
|
package/scripts/gds/ship.js
CHANGED
|
@@ -354,7 +354,7 @@ module.exports = {
|
|
|
354
354
|
// task 1045: the gate that defers the broadcast to the server on the server-
|
|
355
355
|
// mediated publish path — exported so the truth table (ci+server → true, laptop
|
|
356
356
|
// / non-ci → false) is unit-tested with no gh/network (env-driven via
|
|
357
|
-
//
|
|
357
|
+
// <PREFIX>_DEPLOY_MODE + <PREFIX>_PUSH_VIA_SERVER).
|
|
358
358
|
broadcastHandledByServer,
|
|
359
359
|
fatalRecoveryLines, // G04 re-export (definition in ship-honesty.js)
|
|
360
360
|
};
|
|
@@ -31,7 +31,9 @@ ETC=/etc/systemd/system
|
|
|
31
31
|
CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
32
32
|
case "$CORE_DIR" in */node_modules/@bongos/core) INSTANCE_DIR="${INSTANCE_DIR:-${CORE_DIR%/node_modules/@bongos/core}}" ;; esac
|
|
33
33
|
[ -n "${PROVISION_APP_USER:-}${INSTANCE_DIR:-}" ] || { echo "set INSTANCE_DIR to the control plane's instance checkout (or PROVISION_APP_USER) — this sweep is not running from one"; exit 1; }
|
|
34
|
-
|
|
34
|
+
# Owner by name: GNU stat `-c %U`, else BSD/macOS stat `-f %Su` — each refuses the other's
|
|
35
|
+
# flag (task 1004170), the same lookup address-park-sweep.sh makes.
|
|
36
|
+
APP_USER="${PROVISION_APP_USER:-$(stat -c %U "$INSTANCE_DIR" 2>/dev/null || stat -f %Su "$INSTANCE_DIR" 2>/dev/null)}"
|
|
35
37
|
if [ "$APP_USER" = root ] || ! [[ "$APP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
|
|
36
38
|
echo "cannot use '$APP_USER' as the app account — set PROVISION_APP_USER to the unprivileged account that owns ${INSTANCE_DIR:-the instance checkout}"; exit 1
|
|
37
39
|
fi
|