@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
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
// 3. Per file, IN MEMORY (processCoreFile): DOCS (.md) are genericized + secret-scrubbed
|
|
35
35
|
// like the mirror; FUNCTIONAL files are kept VERBATIM and scanned only for an exact
|
|
36
36
|
// live-credential leak (brand/IP hits are non-fatal ADVISORIES, surfaced for a human).
|
|
37
|
+
// 3b. RELEASE NOTES (task 1004218): release-notes.json — what each release brought in,
|
|
38
|
+
// generated from the ref's HISTORY (scripts/gds/release-notes.js), its prose redacted
|
|
39
|
+
// exactly like a doc, and pinned like every other file. Skipped, with a line saying
|
|
40
|
+
// so, where there is no history to read (--from-worktree, a shallow clone).
|
|
37
41
|
// 4. NO-LEAK GATE (fail-closed): if any DOC leaks post-redaction, or any FUNCTIONAL file
|
|
38
42
|
// carries a live credential verbatim, PRODUCE NOTHING — dist/ is never touched. The
|
|
39
43
|
// in-memory pass makes the abort atomic (same discipline as the mirror export).
|
|
@@ -105,6 +109,12 @@ const {
|
|
|
105
109
|
const { collectEnvSecrets } = require(path.join(REPO_ROOT, 'modules/security/secret-scrub.js'));
|
|
106
110
|
const { redactBlob, parseLsTree } = require(path.join(REPO_ROOT, 'scripts/public-mirror-export.js'));
|
|
107
111
|
const { partitionHits, unusedEntries } = require(path.join(REPO_ROOT, 'scripts/gds/leak-scan-allowlist.js'));
|
|
112
|
+
// The release notes shipped beside package.json (task 1004218). Required by a literal path:
|
|
113
|
+
// the module is pure and requires nothing, so it adds no computed require to this file's
|
|
114
|
+
// executed closure (tests/executed_closure.mjs counts those).
|
|
115
|
+
const {
|
|
116
|
+
RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES, generateReleaseNotes, boundReleaseNotes, serializeReleaseNotes,
|
|
117
|
+
} = require('./release-notes.js');
|
|
108
118
|
|
|
109
119
|
const MANIFEST_SCHEMA = 1;
|
|
110
120
|
const ARTIFACT_NAME = 'bongos-core';
|
|
@@ -734,6 +744,42 @@ function scanArtifactForLeaks(files, { literals = PRIOR_INSTANCE_LITERALS } = {}
|
|
|
734
744
|
return { literalHits, secretWarnings };
|
|
735
745
|
}
|
|
736
746
|
|
|
747
|
+
// release-notes.json — the one file the core carries that is derived from HISTORY rather
|
|
748
|
+
// than from a committed file (task 1004218). It only exists where the history does, which
|
|
749
|
+
// is the CI pack (publish.yml checks out at fetch-depth 0). main() stages it into the SAME
|
|
750
|
+
// staged/filesMeta arrays as package.json, so the pin manifest covers it and --self-verify
|
|
751
|
+
// reproduces it; a file written into the stage dir behind the manifest's back would fail
|
|
752
|
+
// self-verify at publish time.
|
|
753
|
+
//
|
|
754
|
+
// ITS PROSE IS TREATED AS A DOC. These are merge summaries — builder-authored sentences — so
|
|
755
|
+
// the neutral floor applies exactly as it does to a shipped .md: redacted with the same rules
|
|
756
|
+
// (redactBlob), and any residue after redaction is a fatal no-leak failure. Not optional:
|
|
757
|
+
// ten of the 1,399 summaries in the history (measured 2026-09-25) name a prior instance or
|
|
758
|
+
// its owner, and history cannot be edited — shipped verbatim, they would fail the fail-closed
|
|
759
|
+
// post-build scan on EVERY pack from now on, and the lane would never publish again. That
|
|
760
|
+
// scan still reads the finished file, as it reads every staged one.
|
|
761
|
+
//
|
|
762
|
+
// Pure: notes + the rules main() already built in, `{ buf, notes, dropped, summaries,
|
|
763
|
+
// redacted, leaks }` out.
|
|
764
|
+
function releaseNotesArtifact(notes, { rules, sensitiveLiterals, envSecrets, knownDocNames, maxBytes = RELEASE_NOTES_MAX_BYTES }) {
|
|
765
|
+
const leaks = [];
|
|
766
|
+
let redacted = 0;
|
|
767
|
+
const clean = {};
|
|
768
|
+
for (const [version, entries] of Object.entries(notes || {})) {
|
|
769
|
+
clean[version] = entries.map((e) => {
|
|
770
|
+
const r = redactBlob({ buf: Buffer.from(e.text, 'utf8'), rules, sensitiveLiterals, envSecrets, knownDocNames });
|
|
771
|
+
for (const l of r.leaks) leaks.push({ label: `doc:${l.label}`, sample: l.sample });
|
|
772
|
+
if (r.secret) leaks.push({ label: 'doc:secret-shape' });
|
|
773
|
+
const text = r.buf.toString('utf8');
|
|
774
|
+
if (text !== e.text) redacted++;
|
|
775
|
+
return e.id ? { id: e.id, text } : { text };
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const bounded = boundReleaseNotes(clean, maxBytes);
|
|
779
|
+
const summaries = Object.values(bounded.notes).reduce((n, list) => n + list.length, 0);
|
|
780
|
+
return { buf: serializeReleaseNotes(bounded.notes), notes: bounded.notes, dropped: bounded.dropped, summaries, redacted, leaks };
|
|
781
|
+
}
|
|
782
|
+
|
|
737
783
|
// Self-verify the built artifact (task 1002218 / audit H3, findings F1+F5). The packager
|
|
738
784
|
// claims a consumer "verifies an install by recomputing [the pin] from the unpacked tree" —
|
|
739
785
|
// this proves the packager's OWN output actually does, at build time, on the produce side:
|
|
@@ -964,6 +1010,29 @@ function main() {
|
|
|
964
1010
|
staged.push({ name: TAR_ROOT + relPath, mode: 0o644, buf });
|
|
965
1011
|
}
|
|
966
1012
|
|
|
1013
|
+
// 4b. The release notes (task 1004218): what each release brought in, generated from this
|
|
1014
|
+
// ref's history, redacted as a doc, and staged into the same arrays so the pin covers
|
|
1015
|
+
// them. A worktree build or a shallow clone has no history to read, so it ships without
|
|
1016
|
+
// — and an upgrade onto it says its notes are unavailable rather than listing nothing.
|
|
1017
|
+
const notesGen = fromWorktree
|
|
1018
|
+
? { skipped: 'built from the working tree, which carries no history' }
|
|
1019
|
+
: generateReleaseNotes({ git, ref: sha, version });
|
|
1020
|
+
let notesStaged = false;
|
|
1021
|
+
if (notesGen.skipped) {
|
|
1022
|
+
console.error(`[package-core] ! ${RELEASE_NOTES_FILE} not generated — ${notesGen.skipped}.`);
|
|
1023
|
+
} else {
|
|
1024
|
+
const art = releaseNotesArtifact(notesGen.notes, { rules, sensitiveLiterals, envSecrets, knownDocNames });
|
|
1025
|
+
for (const l of art.leaks) leaks.push({ path: RELEASE_NOTES_FILE, label: l.label, sample: l.sample });
|
|
1026
|
+
filesMeta.push({ path: RELEASE_NOTES_FILE, mode: '100644', sha256: sha256hex(art.buf) });
|
|
1027
|
+
staged.push({ name: TAR_ROOT + RELEASE_NOTES_FILE, mode: 0o644, buf: art.buf });
|
|
1028
|
+
notesStaged = true;
|
|
1029
|
+
const kept = Object.keys(art.notes);
|
|
1030
|
+
const trimmed = art.dropped.length
|
|
1031
|
+
? ` — the ${art.dropped.length} oldest release(s) DROPPED to stay under ${RELEASE_NOTES_MAX_BYTES / 1024} KiB`
|
|
1032
|
+
: '';
|
|
1033
|
+
console.error(`[package-core] ${RELEASE_NOTES_FILE}: ${kept.length} release(s) ${kept[0]} → ${kept[kept.length - 1]}, ${art.summaries} summaries (${art.redacted} redacted), ${(art.buf.length / 1024).toFixed(1)} KiB${trimmed}.`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
967
1036
|
if (leaks.length > 0) {
|
|
968
1037
|
console.error(`\n[package-core] NO-LEAK GATE FAILED — ${leaks.length} real leak(s) (doc redaction residue or a live credential in a functional file). PRODUCING NOTHING (fail-closed):`);
|
|
969
1038
|
for (const l of leaks.slice(0, 50)) {
|
|
@@ -977,7 +1046,7 @@ function main() {
|
|
|
977
1046
|
console.error(' for a live-credential leak, remove the secret at SOURCE (it should never be committed). Then re-run. dist/ was NOT touched.');
|
|
978
1047
|
process.exit(1);
|
|
979
1048
|
}
|
|
980
|
-
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${stubCount} agent-docs stubbed, ${funcCount} functional verbatim).`);
|
|
1049
|
+
console.error(`[package-core] no-leak gate PASSED — ${staged.length} files clean (${docCount} docs redacted, ${stubCount} agent-docs stubbed, ${funcCount} functional verbatim${notesStaged ? `, ${RELEASE_NOTES_FILE} generated` : ''}).`);
|
|
981
1050
|
|
|
982
1051
|
// Advisory: brand-identity + public-IP literals that ship VERBATIM inside functional
|
|
983
1052
|
// files. Not a leak (these are public, not secrets; a downstream rebrands via config) —
|
|
@@ -1139,6 +1208,8 @@ module.exports = {
|
|
|
1139
1208
|
PRIOR_INSTANCE_FLOOR,
|
|
1140
1209
|
// task 1003995 — --from-worktree's enumerator, for a core checkout with no git history
|
|
1141
1210
|
walkWorktreeEntries,
|
|
1211
|
+
// task 1004218 — release-notes.json: the history-derived file, redacted as a doc and bounded
|
|
1212
|
+
releaseNotesArtifact,
|
|
1142
1213
|
};
|
|
1143
1214
|
|
|
1144
1215
|
if (require.main === module) main();
|
|
@@ -281,6 +281,10 @@ const CARDS = Object.freeze({
|
|
|
281
281
|
'Your work touches a protected part of the project that your rank may not change, so the server would not accept it.',
|
|
282
282
|
'Asking again will be refused again. I will take the protected files out and hand in the rest, or we pass the task to someone with the rank.',
|
|
283
283
|
'A builder with a high enough rank, for the protected part.', RULE),
|
|
284
|
+
grade_unbacked_score: card(
|
|
285
|
+
'One of the automatic reviewers gave your work a low mark without saying what is wrong, so there is nothing listed to fix. It is not accepted yet, but it was not turned down for a named problem either.',
|
|
286
|
+
'I will ask for the review again. If the low mark comes back still without a reason, I will ask for your work to be accepted by hand, with an explanation.',
|
|
287
|
+
'Only if it keeps happening — an Archon decides whether to accept it by hand.', FAULT),
|
|
284
288
|
grade_failed: card(
|
|
285
289
|
'The automatic reviewers found problems in your work, so it is not accepted yet.',
|
|
286
290
|
'I will fix what they listed, save it, and ask for the review again. That costs nothing extra.',
|
package/scripts/gds/preflight.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
const nodeFs = require('node:fs');
|
|
29
29
|
const nodePath = require('node:path');
|
|
30
30
|
const { spawnSync } = require('node:child_process');
|
|
31
|
+
const { resolveEnv, envPrefix, LEGACY_ENV_PREFIXES } = require('../../src/instance-config');
|
|
31
32
|
|
|
32
33
|
const REPO_ROOT = nodePath.resolve(__dirname, '..', '..');
|
|
33
34
|
const MIN_NODE_MAJOR = 22;
|
|
@@ -155,9 +156,16 @@ function preflightChecks({
|
|
|
155
156
|
// /builder-start caller via depsFinding() (task 1002403).
|
|
156
157
|
out.push(depsFinding({ repoRoot, fs }));
|
|
157
158
|
|
|
158
|
-
// 3. `claude` CLI launchable — same resolution order the grader uses
|
|
159
|
-
|
|
160
|
-
const
|
|
159
|
+
// 3. `claude` CLI launchable — same resolution order the grader uses:
|
|
160
|
+
// <PREFIX>_CLAUDE_BIN (legacy spellings still read), then CLAUDE_BIN, then `claude`.
|
|
161
|
+
const prefixed = resolveEnv('CLAUDE_BIN', { env });
|
|
162
|
+
const bin = prefixed || env.CLAUDE_BIN || 'claude';
|
|
163
|
+
// The label names the spelling that actually supplied the value, so a builder who
|
|
164
|
+
// set the legacy OTB_CLAUDE_BIN is told so rather than shown a name they never set.
|
|
165
|
+
const viaName = prefixed
|
|
166
|
+
? [envPrefix(), ...LEGACY_ENV_PREFIXES].map((p) => `${p}_CLAUDE_BIN`).find((n) => env[n] === prefixed)
|
|
167
|
+
: env.CLAUDE_BIN ? 'CLAUDE_BIN' : null;
|
|
168
|
+
const via = viaName ? ` (via ${viaName})` : '';
|
|
161
169
|
let r;
|
|
162
170
|
try {
|
|
163
171
|
r = spawn(bin, ['--version'], { encoding: 'utf8', shell: platform === 'win32', windowsHide: true, timeout: 15000 });
|
|
@@ -174,7 +182,7 @@ function preflightChecks({
|
|
|
174
182
|
level: 'fail',
|
|
175
183
|
title: 'claude CLI',
|
|
176
184
|
detail: `not launchable (${bin}: ${why}) — the grader spawns it per worker, so ships fail grading with SUBAGENT_SPAWN_FAILED.`,
|
|
177
|
-
fix: 'Run `npm i -g @anthropic-ai/claude-code` (puts a real claude on PATH), or set
|
|
185
|
+
fix: 'Run `npm i -g @anthropic-ai/claude-code` (puts a real claude on PATH), or set ' + envPrefix() + '_CLAUDE_BIN to its full path.',
|
|
178
186
|
});
|
|
179
187
|
}
|
|
180
188
|
|
|
@@ -28,10 +28,15 @@
|
|
|
28
28
|
// Its own file because scripts/gds/provision.js sits at the 1500-line oversized-file
|
|
29
29
|
// ratchet, the same reason provision-net.js and provision-units.js were carved out.
|
|
30
30
|
|
|
31
|
+
const crypto = require('crypto');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const os = require('os');
|
|
31
34
|
const path = require('path');
|
|
35
|
+
const { spawnSync } = require('child_process');
|
|
32
36
|
const { CONFIG, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, REPO_ROOT } = require('./provision-config.js');
|
|
33
|
-
const { appUser, dbName, instanceUser, selfInstanceRoot, standaloneRoot } = require('./provision-repo.js');
|
|
37
|
+
const { appUser, dbName, instanceUser, parseTargetRef, resolveOwnerGithubToken, selfInstanceRoot, standaloneRoot } = require('./provision-repo.js');
|
|
34
38
|
const { isValidSlug } = require('../../modules/provisioning/provisioning.js');
|
|
39
|
+
const { RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES, parseChangeRecords, notesBetween } = require('./release-notes.js');
|
|
35
40
|
|
|
36
41
|
// The shapes whose box this runner can actually reach. Mirrors UPGRADE_SHAPES in the
|
|
37
42
|
// web-tier half, and deliberately EXCLUDES 'dedicated': a dedicated droplet is stood up
|
|
@@ -269,14 +274,161 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
269
274
|
// reaching here means the two disagree. Report what was SEEN rather than a verdict.
|
|
270
275
|
return { ok: false, error: `the box is serving core ${served}, not the requested ${to} — the move did not take` };
|
|
271
276
|
}
|
|
277
|
+
|
|
278
|
+
// ---- 5. the pin reaches the project's own repo, pushed from HERE (task 1004065) ----
|
|
279
|
+
// Only now: a pin worth saving is one the box is serving. upgrade.js was told
|
|
280
|
+
// --pin-no-push for this shape, so it committed and stopped; see pushUpgradePin.
|
|
281
|
+
const repo = hubPushesPin(inst) ? await pushUpgradePin(inst, deps, { instanceDir }) : null;
|
|
282
|
+
const unsaved = repo && !repo.pushed ? `; GitHub was not updated (${repo.code})` : '';
|
|
272
283
|
await provisioning.recordEvent(db, {
|
|
273
284
|
instanceId: inst.id, ownerBuilderId: inst.owner_builder_id, event: 'core-upgraded',
|
|
274
|
-
detail: served ? `core pin moved to ${served}` : `core pin moved to ${to}; the box did not answer /version, so this is unconfirmed
|
|
285
|
+
detail: (served ? `core pin moved to ${served}` : `core pin moved to ${to}; the box did not answer /version, so this is unconfirmed`) + unsaved,
|
|
275
286
|
actor: intent.requested_by || 'runner',
|
|
276
287
|
}).catch(() => {});
|
|
277
288
|
if (provisioning.clearErrorNote) await provisioning.clearErrorNote(db, inst.id, 'core-upgrade').catch(() => {});
|
|
278
289
|
log(served ? ` ✓ ${inst.slug} serves core ${served}` : ` ⚠ ${inst.slug} upgraded to ${to}, but did not answer /version — unconfirmed`);
|
|
279
|
-
|
|
290
|
+
// `doneNote`: the drain loop resolves the intent DONE and stores this beside it, which
|
|
291
|
+
// is how the deploy page learns the upgrade landed but GitHub did not get it.
|
|
292
|
+
return { ok: true, served, confirmed: served === to, repo, doneNote: repo && !repo.pushed ? repo.note : null };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── the pin reaches the project's GitHub repo, pushed from the HUB (task 1004065) ──
|
|
296
|
+
//
|
|
297
|
+
// THE BUG THIS EXISTS FOR. upgrade.js commits the new pin on the box and then pushes it.
|
|
298
|
+
// On a hosted project it runs connected to the PROJECT's database — it has to, for
|
|
299
|
+
// migrations — while the owner's GitHub token lives in the HUB's. Its token lookup found
|
|
300
|
+
// no provisioning table there (ADR 0281), fell back to `git push origin`, and origin is a
|
|
301
|
+
// read-only deploy key (ADR 0176). GitHub refused, the refusal reached only the journal
|
|
302
|
+
// and named the wrong cause, and the intent resolved `done`. The box ran the new core,
|
|
303
|
+
// GitHub still pinned the old one, and the box's local commit stopped its fast-forward
|
|
304
|
+
// pulls from taking the owner's new work: hermeslines-marketing served 1.19.994 against
|
|
305
|
+
// a repo pinning 1.19.881, and pages pushed to that repo 404'd on the live site.
|
|
306
|
+
//
|
|
307
|
+
// WHY THE RUNNER PUSHES rather than handing upgrade.js the token. This process already
|
|
308
|
+
// holds the hub's database, where the token is, and it runs as the account that owns the
|
|
309
|
+
// checkout (the project's own account only READS it — grantInstanceRepoReadCmd). Passing
|
|
310
|
+
// the token into the upgrade would put a credential that can write the repo inside the
|
|
311
|
+
// project's own process. So the move passes --pin-no-push, upgrade.js commits, and the
|
|
312
|
+
// push happens here, in the shape refreshStandaloneCorePin already uses for this repo.
|
|
313
|
+
//
|
|
314
|
+
// A FAILED PUSH IS NOT A FAILED UPGRADE. The site runs the new core, and rolling a good
|
|
315
|
+
// deploy back would be worse than the split. The move still resolves `done`, carrying a
|
|
316
|
+
// note the deploy page reads: `repo_not_updated:<code>: <sentence>`, parsed by
|
|
317
|
+
// parseRepoNote in modules/provisioning/core-upgrade.js (re-stated there because a
|
|
318
|
+
// module may not require a script; tests/core_upgrade_runner.mjs round-trips the two).
|
|
319
|
+
//
|
|
320
|
+
// NOTHING IS FORCED OR REBASED. When GitHub has commits the box does not, putting the pin
|
|
321
|
+
// on top of them would change what the live box runs with no restart and no health check.
|
|
322
|
+
// That is reported for an operator to reconcile, never resolved here.
|
|
323
|
+
//
|
|
324
|
+
// THE LIMIT, stated so nobody mistakes this for durable: the owner's token is kept for an
|
|
325
|
+
// hour after they sign in with repository access (GITHUB_TOKEN_TTL_MS in src/bongos/db.js
|
|
326
|
+
// — one wizard sitting). An upgrade days later usually finds none; the box's own remote
|
|
327
|
+
// is then tried, and it can only read a private repo. The note says so in words.
|
|
328
|
+
|
|
329
|
+
const REPO_NOTE_PREFIX = 'repo_not_updated';
|
|
330
|
+
const REPO_NOTES = Object.freeze({
|
|
331
|
+
no_repo: 'This project has no GitHub repository on record, so the new version was saved on the server only.',
|
|
332
|
+
no_branch: 'The server\'s copy of the project is not on a branch, so there was nothing to save to GitHub.',
|
|
333
|
+
no_access: 'The hub had no permission to write to the project\'s GitHub repository: the owner\'s GitHub access is only kept for an hour after they connect GitHub, and the server\'s own key can only read the repository.',
|
|
334
|
+
refused: 'GitHub turned down the owner\'s GitHub access — it may have expired, or lack permission to write to the repository.',
|
|
335
|
+
behind: 'GitHub has changes the server does not have, so the new version could not be added on top of them. An operator needs to bring the two back in line; the site itself is running normally.',
|
|
336
|
+
failed: 'The hub could not save the new version to GitHub.',
|
|
337
|
+
});
|
|
338
|
+
// A branch name that is safe to interpolate. Narrower than git's own ref grammar on
|
|
339
|
+
// purpose: a name outside it is refused, never quoted.
|
|
340
|
+
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/;
|
|
341
|
+
|
|
342
|
+
/** The note a done-but-unsaved move carries, bounded for the DOM it reaches. PURE. */
|
|
343
|
+
function repoNote(code, extra = '') {
|
|
344
|
+
const known = Object.prototype.hasOwnProperty.call(REPO_NOTES, code) ? code : 'failed';
|
|
345
|
+
return `${REPO_NOTE_PREFIX}:${known}: ${REPO_NOTES[known]}${extra ? ` ${extra}` : ''}`.slice(0, 600);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Which shapes get their pin pushed from here: a hosted project, whose repo is the
|
|
349
|
+
// owner's own. The platform's own row pushes to a repo its key CAN write, and a
|
|
350
|
+
// co-tenant shares the platform's checkout, so neither changes.
|
|
351
|
+
function hubPushesPin(inst) { return !!inst && inst.hosting_shape === 'cloud-host'; }
|
|
352
|
+
|
|
353
|
+
// Why a push did not happen, from what git said. `behind` is checked first: a rejected
|
|
354
|
+
// update means the credential WORKED and the history is what stopped it.
|
|
355
|
+
function pushFault(r) {
|
|
356
|
+
const said = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
357
|
+
if (/\[rejected\]|non-fast-forward|fetch first|Updates were rejected/i.test(said)) return 'behind';
|
|
358
|
+
if (/\b403\b|Authentication failed|Invalid username or password|Write access to repository not granted|Permission to \S+ denied|marked as read only|Permission denied \(publickey\)|could not read Username/i.test(said)) return 'refused';
|
|
359
|
+
return 'failed';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// GitHub's own line beats git's closing `fatal:`, which names no reason (upgrade.js
|
|
363
|
+
// gitSaid has the same preference).
|
|
364
|
+
function gitLine(r) {
|
|
365
|
+
const lines = String((r && r.stderr) || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
366
|
+
const server = lines.filter((l) => /^(ERROR:|remote:|!)/i.test(l));
|
|
367
|
+
const said = server.length ? server[server.length - 1] : lines[lines.length - 1];
|
|
368
|
+
return said ? `(git: ${said.slice(0, 200)})` : '';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Push a confirmed upgrade's committed pin to the project's repo. NEVER THROWS: every
|
|
373
|
+
* miss returns `{ pushed:false, code, note }`, because the drain loop turns a throw into
|
|
374
|
+
* a broken instance and this instance is running fine.
|
|
375
|
+
*
|
|
376
|
+
* The token rides a chmod-600 credential file that is removed whatever the push did —
|
|
377
|
+
* never argv (ps, the journal), never a log line. The helper list is reset first
|
|
378
|
+
* (helpers are cumulative, so a global one would answer with some other account) and
|
|
379
|
+
* hooks are off, so nothing in the project's checkout runs as this account.
|
|
380
|
+
*/
|
|
381
|
+
async function pushUpgradePin(inst, deps, { instanceDir }) {
|
|
382
|
+
const log = deps.log || (() => {});
|
|
383
|
+
// The CONTROL PLANE's executor and writer: the checkout, the hub db and the token are
|
|
384
|
+
// all here, never on a remote co-hosting box (refreshStandaloneCorePin's routing).
|
|
385
|
+
const exec = deps.controlExec || deps.exec;
|
|
386
|
+
const writeFile = deps.controlWriteFile || deps.writeFile || ((p, body, o) => fs.writeFileSync(p, body, o));
|
|
387
|
+
const failedRun = (r) => !r || r.softFailed === true || r.ok === false;
|
|
388
|
+
const miss = (code, extra = '') => {
|
|
389
|
+
log(` ⚠ ${inst.slug}: GitHub was not updated — ${REPO_NOTES[code] || REPO_NOTES.failed}${extra ? ` ${extra}` : ''}`);
|
|
390
|
+
return { pushed: false, code, note: repoNote(code, extra) };
|
|
391
|
+
};
|
|
392
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
393
|
+
if (!repo) return miss('no_repo');
|
|
394
|
+
const head = exec(`git -C ${instanceDir} symbolic-ref --short HEAD`, { allowFail: true });
|
|
395
|
+
const branch = failedRun(head) ? '' : String(head.stdout || '').trim();
|
|
396
|
+
if (!BRANCH_RE.test(branch)) return miss('no_branch');
|
|
397
|
+
const at = `${repo.owner}/${repo.name}@${branch}`;
|
|
398
|
+
const hardened = `git -C ${instanceDir} -c core.hooksPath=/dev/null`;
|
|
399
|
+
const env = { GIT_TERMINAL_PROMPT: '0' }; // a refused credential fails; it never waits at a prompt
|
|
400
|
+
|
|
401
|
+
const token = await resolveOwnerGithubToken(inst, deps, { tag: '[core-upgrade]' });
|
|
402
|
+
if (token) {
|
|
403
|
+
const credFile = path.join(os.tmpdir(), `bongos-gitcred-upgrade-${inst.slug}-${process.pid}-${crypto.randomBytes(6).toString('hex')}`);
|
|
404
|
+
let r = null;
|
|
405
|
+
try {
|
|
406
|
+
writeFile(credFile, `https://x-access-token:${token}@github.com\n`, { mode: 0o600 });
|
|
407
|
+
r = exec(`${hardened} -c credential.helper= -c credential.helper='store --file=${credFile}' push https://x-access-token@github.com/${repo.owner}/${repo.name}.git HEAD:${branch}`,
|
|
408
|
+
{ allowFail: true, env });
|
|
409
|
+
} catch (e) {
|
|
410
|
+
return miss('failed', `(could not stage the owner's credential: ${String((e && e.message) || e).slice(0, 160)})`);
|
|
411
|
+
} finally {
|
|
412
|
+
exec(`rm -f ${credFile}`, { allowFail: true });
|
|
413
|
+
}
|
|
414
|
+
if (!failedRun(r)) {
|
|
415
|
+
log(` ✓ pushed the pin to ${at} from the hub, over the owner's GitHub token`);
|
|
416
|
+
return { pushed: true, via: 'owner-token', branch };
|
|
417
|
+
}
|
|
418
|
+
return miss(pushFault(r), gitLine(r));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// No token stored — which is the ordinary case (see THE LIMIT above). The box's own
|
|
422
|
+
// remote is still tried: a checkout wired with a key that can write is not wrong to use.
|
|
423
|
+
const r = exec(`${hardened} push origin HEAD:${branch}`, { allowFail: true, env });
|
|
424
|
+
if (!failedRun(r)) {
|
|
425
|
+
log(` ✓ pushed the pin to ${at} from the hub, over the checkout's own remote`);
|
|
426
|
+
return { pushed: true, via: 'origin', branch };
|
|
427
|
+
}
|
|
428
|
+
const fault = pushFault(r);
|
|
429
|
+
// A refusal here is not the owner's access being turned down — there was none to turn
|
|
430
|
+
// down. It is the read-only key, which is the no-access case.
|
|
431
|
+
return miss(fault === 'refused' ? 'no_access' : fault, gitLine(r));
|
|
280
432
|
}
|
|
281
433
|
|
|
282
434
|
// The pre-flight lines a dry run emits, and the refusal when it has one. upgrade.js writes
|
|
@@ -400,6 +552,9 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
400
552
|
`--health-url http://127.0.0.1:${inst.port}/healthz`,
|
|
401
553
|
`--version-url http://127.0.0.1:${inst.port}/version`,
|
|
402
554
|
'--commit-pin',
|
|
555
|
+
// A hosted project's pin is pushed by the RUNNER, which can reach the owner's
|
|
556
|
+
// token; the upgrade process cannot (task 1004065, pushUpgradePin).
|
|
557
|
+
hubPushesPin(inst) ? '--pin-no-push' : '',
|
|
403
558
|
requestedBy ? `--requested-by ${requestedBy}` : '',
|
|
404
559
|
requestedBy ? '--upgrade-source owner-control' : ''];
|
|
405
560
|
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs: account };
|
|
@@ -452,74 +607,101 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
452
607
|
// requires one and posts it to Discord. So this reads the merge summaries between the two
|
|
453
608
|
// releases rather than inventing a changelog nobody would maintain.
|
|
454
609
|
//
|
|
455
|
-
// IT READS THE
|
|
456
|
-
//
|
|
457
|
-
// the CORE repository, and
|
|
458
|
-
// repo with the core vendored in from npm
|
|
459
|
-
//
|
|
460
|
-
//
|
|
610
|
+
// IT READS THE NOTES THE CORE PACKAGE CARRIES, and a core checkout only as a fallback
|
|
611
|
+
// (task 1004218). "What changed in the core between 1.19.881 and 1.19.972" is a question
|
|
612
|
+
// about the CORE repository, and no tenant box will ever hold its history — every project
|
|
613
|
+
// is a checkout of its OWN repo with the core vendored in from npm. So the packer writes
|
|
614
|
+
// the summaries down once, from the full history CI packs with (release-notes.json, built
|
|
615
|
+
// by scripts/gds/release-notes.js), and any machine that can download a core version can
|
|
616
|
+
// read what it carries. The checkout path below is kept for a machine that has one.
|
|
461
617
|
const CHANGE_CAP = 20;
|
|
462
|
-
|
|
618
|
+
// One registry download of a core tarball. The preview's owner is waiting on the dry run
|
|
619
|
+
// beside it anyway, but a hung registry must not hold the runner forever.
|
|
620
|
+
const NOTES_FETCH_TIMEOUT_MS = 60000;
|
|
621
|
+
|
|
622
|
+
function runFault(r) {
|
|
623
|
+
if (!r) return 'no answer';
|
|
624
|
+
if (r.error) return String(r.error.message || r.error).slice(0, 160);
|
|
625
|
+
const tail = String(r.stderr || '').trim().split('\n').filter(Boolean).slice(-1)[0] || '';
|
|
626
|
+
return (tail || `exit ${r.status}`).slice(0, 160);
|
|
627
|
+
}
|
|
463
628
|
|
|
464
629
|
/**
|
|
465
|
-
* The
|
|
630
|
+
* The release notes the core THIS runner executes from carries, if any. Free to read, and
|
|
631
|
+
* enough on its own whenever the target is at or below this core's own version — a tenant
|
|
632
|
+
* catching up to where the platform already is.
|
|
633
|
+
*/
|
|
634
|
+
function readInstalledNotes(deps = {}) {
|
|
635
|
+
const fsImpl = deps.fs || fs;
|
|
636
|
+
let raw;
|
|
637
|
+
try { raw = fsImpl.readFileSync(path.join(REPO_ROOT, RELEASE_NOTES_FILE), 'utf8'); }
|
|
638
|
+
catch { return { missing: 'this core carries no release notes' }; }
|
|
639
|
+
try { return { notes: JSON.parse(raw) }; }
|
|
640
|
+
catch { return { error: "this core's release notes are not valid JSON" }; }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The release notes inside ONE published core package — the TARGET's own (task 1004218).
|
|
466
645
|
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
646
|
+
* WHY THE TARGET'S. The installed core's notes stop at its own version, which is zero
|
|
647
|
+
* coverage for the move looked at most — the platform's own, onto a release newer than
|
|
648
|
+
* itself. The target's package is the one guaranteed to reach the target, because that is
|
|
649
|
+
* where the target was packed. `npm pack` downloads it (the core is public, so no
|
|
650
|
+
* credential — the same registry listAvailableVersions reads) and `tar -xzOf` reads the one
|
|
651
|
+
* file out of it rather than unpacking the rest.
|
|
473
652
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* are the mechanics of landing work, not the work, and listing them would bury the
|
|
477
|
-
* eleven sentences the owner actually wants under a hundred they do not.
|
|
653
|
+
* Returns `{ notes }`, `{ missing }` when the package carries none (every release published
|
|
654
|
+
* before the notes were packaged), or `{ error }`. The download never outlives the call.
|
|
478
655
|
*/
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// The negative lookahead is what keeps the two housekeeping shapes out: both begin
|
|
491
|
-
// "Merge " and both would otherwise match a `<token>:` somewhere later in the line.
|
|
492
|
-
let m = /^Merge\s+(?!pull request\b|remote-tracking\b)([^\s:]+):\s*(\S.*)$/.exec(subject);
|
|
493
|
-
if (m) {
|
|
494
|
-
text = m[2];
|
|
495
|
-
} else {
|
|
496
|
-
const first = body.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
497
|
-
m = /^task\s+(\d+):\s*(\S.*)$/.exec(first);
|
|
498
|
-
if (m) { id = m[1]; text = m[2]; }
|
|
656
|
+
function readPackagedNotes(version, deps = {}) {
|
|
657
|
+
if (!CORE_VERSION_RE.test(String(version || ''))) return { error: 'could not work out which package to read' };
|
|
658
|
+
const run = deps.spawn || spawnSync;
|
|
659
|
+
const fsImpl = deps.fs || fs;
|
|
660
|
+
let dir = null;
|
|
661
|
+
try {
|
|
662
|
+
dir = fsImpl.mkdtempSync(path.join(deps.tmpDir || os.tmpdir(), 'bongos-release-notes-'));
|
|
663
|
+
const packed = run('npm', ['pack', `@bongos/core@${version}`, '--pack-destination', dir, '--silent'],
|
|
664
|
+
{ cwd: dir, env: deps.env || process.env, encoding: 'utf8', timeout: NOTES_FETCH_TIMEOUT_MS });
|
|
665
|
+
if (!packed || packed.error || packed.status !== 0) {
|
|
666
|
+
return { error: `could not download the core ${version} package to read its release notes (${runFault(packed)})` };
|
|
499
667
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
668
|
+
// Whatever npm named it: a scoped package's tarball is <scope>-<name>-<version>.tgz today.
|
|
669
|
+
const tgz = fsImpl.readdirSync(dir).find((f) => f.endsWith('.tgz'));
|
|
670
|
+
if (!tgz) return { error: `npm reported the core ${version} package downloaded, but left no tarball` };
|
|
671
|
+
const x = run('tar', ['-xzOf', path.join(dir, tgz), `package/${RELEASE_NOTES_FILE}`],
|
|
672
|
+
{ encoding: 'utf8', maxBuffer: 4 * RELEASE_NOTES_MAX_BYTES, timeout: NOTES_FETCH_TIMEOUT_MS });
|
|
673
|
+
if (!x || x.error || x.status !== 0) {
|
|
674
|
+
if (/not found in archive/i.test(String((x && x.stderr) || ''))) {
|
|
675
|
+
return { missing: `the core ${version} package carries no release notes` };
|
|
676
|
+
}
|
|
677
|
+
return { error: `could not read the release notes out of the core ${version} package (${runFault(x)})` };
|
|
678
|
+
}
|
|
679
|
+
try { return { notes: JSON.parse(x.stdout) }; }
|
|
680
|
+
catch { return { error: `the release notes in the core ${version} package are not valid JSON` }; }
|
|
681
|
+
} catch (e) {
|
|
682
|
+
return { error: `could not fetch the core ${version} package (${String((e && e.message) || e).slice(0, 160)})` };
|
|
683
|
+
} finally {
|
|
684
|
+
if (dir) { try { fsImpl.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
506
685
|
}
|
|
507
|
-
return out;
|
|
508
686
|
}
|
|
509
687
|
|
|
510
688
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
515
|
-
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
516
|
-
* must not give, and it is exactly what a missing checkout or a shallow clone produces.
|
|
517
|
-
*
|
|
518
|
-
* Every version reaching a shell is re-checked against the strict three-number form
|
|
519
|
-
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
520
|
-
* is interpolated into a command is checked where it is interpolated — the rule
|
|
521
|
-
* upgradeInvocation states about the slug.
|
|
689
|
+
* The range, answered from release notes alone: `{ entries }`, or `{ why }` they cannot.
|
|
690
|
+
* The installed core first, when its notes already reach the target; the target's own
|
|
691
|
+
* package otherwise. No git, no SHAs, no checkout.
|
|
522
692
|
*/
|
|
693
|
+
function changesFromNotes(fromVersion, toVersion, deps) {
|
|
694
|
+
const installed = (deps.readInstalledNotes || readInstalledNotes)(deps);
|
|
695
|
+
if (installed.notes) {
|
|
696
|
+
const r = notesBetween(installed.notes, fromVersion, toVersion);
|
|
697
|
+
if (r.entries) return r;
|
|
698
|
+
}
|
|
699
|
+
const target = (deps.readPackagedNotes || readPackagedNotes)(toVersion, deps);
|
|
700
|
+
if (!target.notes) return { why: target.missing || target.error || `the core ${toVersion} package's release notes could not be read` };
|
|
701
|
+
const r = notesBetween(target.notes, fromVersion, toVersion);
|
|
702
|
+
return r.entries ? r : { why: `the release notes in the core ${toVersion} package ${r.uncovered}` };
|
|
703
|
+
}
|
|
704
|
+
|
|
523
705
|
/**
|
|
524
706
|
* Classify a git command that did not succeed. PURE.
|
|
525
707
|
*
|
|
@@ -598,21 +780,11 @@ function findCoreCheckout(deps, git) {
|
|
|
598
780
|
}
|
|
599
781
|
|
|
600
782
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
605
|
-
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
606
|
-
* must not give, and it is exactly what a missing checkout produces.
|
|
607
|
-
*
|
|
608
|
-
* Every version reaching a shell is re-checked against the strict three-number form
|
|
609
|
-
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
610
|
-
* is interpolated into a command is checked where it is interpolated — the rule
|
|
611
|
-
* upgradeInvocation states about the slug.
|
|
783
|
+
* The range, answered from a core CHECKOUT's git history: `{ entries }` or `{ error }`.
|
|
784
|
+
* The fallback for a machine that has one, since the package's notes only exist for
|
|
785
|
+
* releases published after they were introduced (task 1004218).
|
|
612
786
|
*/
|
|
613
|
-
function
|
|
614
|
-
const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
|
|
615
|
-
if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
|
|
787
|
+
function changesFromCheckout(fromVersion, toVersion, deps) {
|
|
616
788
|
// The CONTROL PLANE's executor, never the previewed instance's: under
|
|
617
789
|
// PROVISION_REMOTE_HOST `exec` is the remote box, while the core history — and the whole
|
|
618
790
|
// question of what changed in the CORE — belongs to the machine that builds it.
|
|
@@ -635,7 +807,7 @@ function changesBetween(fromVersion, toVersion, deps) {
|
|
|
635
807
|
};
|
|
636
808
|
};
|
|
637
809
|
const found = findCoreCheckout(deps, git);
|
|
638
|
-
if (found.error) return {
|
|
810
|
+
if (found.error) return { error: found.error };
|
|
639
811
|
const dir = found.dir;
|
|
640
812
|
const shaOf = (v) => {
|
|
641
813
|
const r = git(dir, `log --format=%H --max-count=1 -F --grep='release: core ${v} '`);
|
|
@@ -645,18 +817,48 @@ function changesBetween(fromVersion, toVersion, deps) {
|
|
|
645
817
|
};
|
|
646
818
|
const from = shaOf(fromVersion);
|
|
647
819
|
const to = shaOf(toVersion);
|
|
648
|
-
if (from.fault || to.fault) return {
|
|
820
|
+
if (from.fault || to.fault) return { error: from.fault || to.fault };
|
|
649
821
|
// LOOKED, AND DID NOT FIND — a different sentence from "could not look", and it names
|
|
650
822
|
// WHICH version is missing so the reader can tell a not-yet-published target from a
|
|
651
823
|
// checkout that has not been fetched.
|
|
652
824
|
const missing = [!from.sha && fromVersion, !to.sha && toVersion].filter(Boolean);
|
|
653
825
|
if (missing.length) {
|
|
654
|
-
return {
|
|
826
|
+
return { error: `core ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not in the core history on this machine yet` };
|
|
655
827
|
}
|
|
656
828
|
const r = git(dir, `log --merges --format=%s%x1f%b%x1e ${from.sha}..${to.sha}`);
|
|
657
|
-
if (!r.ok) return {
|
|
658
|
-
|
|
659
|
-
|
|
829
|
+
if (!r.ok) return { error: gitFault(r, dir) };
|
|
830
|
+
return { entries: parseChangeRecords(r.stdout) };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* What landed between two core releases, as the summaries their ships wrote.
|
|
835
|
+
*
|
|
836
|
+
* Returns `{ from, to, entries, more, source }`, or `{ from, to, error }` — NEVER an empty
|
|
837
|
+
* `entries` for a range it could not resolve. An empty list reads as "nothing changed",
|
|
838
|
+
* which on a page whose job is to say what a restart buys is the one wrong impression it
|
|
839
|
+
* must not give, and it is exactly what a missing checkout produces.
|
|
840
|
+
*
|
|
841
|
+
* THE PACKAGE'S NOTES FIRST, THE CHECKOUT SECOND (task 1004218). The notes answer on every
|
|
842
|
+
* box; the checkout only where one exists, which is never a tenant. When neither can, the
|
|
843
|
+
* error carries BOTH reasons — the notes' and the checkout's own precise sentence (task
|
|
844
|
+
* 1004212) — so the reader is told everything that was tried and why each fell short.
|
|
845
|
+
*
|
|
846
|
+
* Every version reaching a shell is re-checked against the strict three-number form
|
|
847
|
+
* FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
|
|
848
|
+
* is interpolated into a command is checked where it is interpolated — the rule
|
|
849
|
+
* upgradeInvocation states about the slug.
|
|
850
|
+
*/
|
|
851
|
+
function changesBetween(fromVersion, toVersion, deps) {
|
|
852
|
+
const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
|
|
853
|
+
if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
|
|
854
|
+
const listed = (all, source) => ({
|
|
855
|
+
from: fromVersion, to: toVersion, entries: all.slice(0, CHANGE_CAP), more: Math.max(0, all.length - CHANGE_CAP), source,
|
|
856
|
+
});
|
|
857
|
+
const notes = changesFromNotes(fromVersion, toVersion, deps);
|
|
858
|
+
if (notes.entries) return listed(notes.entries, 'release-notes');
|
|
859
|
+
const checkout = changesFromCheckout(fromVersion, toVersion, deps);
|
|
860
|
+
if (checkout.entries) return listed(checkout.entries, 'checkout');
|
|
861
|
+
return { from: fromVersion, to: toVersion, error: `${notes.why}; ${checkout.error}` };
|
|
660
862
|
}
|
|
661
863
|
|
|
662
864
|
// Run a dry run for one instance and cache its verdict — the ON-DEMAND leg, drained from
|
|
@@ -780,4 +982,4 @@ async function snapshotAllCoreVersions(deps) {
|
|
|
780
982
|
return { seen };
|
|
781
983
|
}
|
|
782
984
|
|
|
783
|
-
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
985
|
+
module.exports = { UPGRADE_SHAPES, CHANGE_CAP, REPO_NOTES, repoNote, parseChangeRecords, readInstalledNotes, readPackagedNotes, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|