@bongos/core 1.21.14 → 1.21.16
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 +51 -41
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +3 -0
- package/docs/adr/0364-repo-access-is-asked-at-sign-in-and-held-for-the-session.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +255 -255
- package/docs/copy-registry.json +288 -288
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +363 -362
- package/docs/recipes/upgrading-the-core.md +1 -0
- package/modules/hall-ui/public/deploy.js +3 -0
- package/modules/public-landing/public/account.html +2 -2
- package/modules/public-landing/public/account.probes.json +2 -2
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +22 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-core-upgrade.js +52 -20
- package/scripts/gds/upgrade-migrate.js +101 -4
- package/scripts/gds/upgrade-outcome.js +6 -0
- package/scripts/gds/upgrade.js +39 -44
- package/scripts/gds/wedge-remedy.js +10 -4
- package/src/bongos/db.js +7 -6
- package/src/bongos/routes/auth.js +17 -4
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +34 -10
- package/tests/github_token_session_life.mjs +110 -0
- package/tests/hub_account_page.mjs +2 -2
- package/tests/instance_db_migrated_on_code_move.mjs +20 -2
- package/tests/landing_page.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +2 -1
- package/tests/upgrade.mjs +147 -0
- package/tests/upgrade_outcome.mjs +3 -0
- package/tests/wedge_remedy.mjs +36 -2
- package/tests/wizard_draft_resume.mjs +49 -0
- package/tests/wizard_intent_resume.mjs +4 -2
|
@@ -222,6 +222,7 @@ running a go-live.
|
|
|
222
222
|
## Gotchas
|
|
223
223
|
|
|
224
224
|
- **The Deploy door fixes three known snags by itself, once** (task 1004448, `scripts/gds/wedge-remedy.js`). When a core move through `/deploy` fails on a **dirty tree** (it commits the tracked changes inside the project), a **full disk** (it prunes that project's own nightly dumps down to the newest 7) or a **database behind its code** (it migrates that database with its own `PGDATABASE` before the code moves), the runner applies the fix, records a `core-upgrade-remedy` event on the project, and tries the move once more on the next tick. If the retry fails for the same reason, or the fix itself fails, the move is retired as `unknown` (`remedy_did_not_hold` / `remedy_failed`) for a person — never a second fix. The manual fixes below still apply to the unattended nightly sweep, which does not go through the door.
|
|
225
|
+
- **A core move uses TWO unix accounts, and which one does what is not interchangeable** (task 1004512, `resolveRunAs` in `scripts/gds/provision-core-upgrade.js`). The move runs as the account that owns the checkout — the app user, on every hosting shape — because everything it writes lives there: the pin, `node_modules`, `.claude/`, the pin commit, and the `git status` it starts with. Only the **database** steps (identity pre-check, migrate, the ledger insert) run as the project's own `bongos-<slug>` account, handed over with `--migrate-as`; that account owns the database and `PUBLIC` has no `CONNECT`, so nothing else can reach it. The flag is emitted only when the two differ, so a control plane and a project older than ADR 0281 run the command they always did. *The symptom when this is wrong:* every move on a project with its own account fails with `fatal: detected dubious ownership in repository`, which the clean-tree pre-flight used to report as a dirty tree — the remedy below then commits a tree nobody touched and the retry fails identically. A `git status` that could not answer is now its own reason (`git_unreadable`, class `unknown`), so it goes to a person instead of to that fix. **Do not "fix" a recurrence with `safe.directory`** — git would then read the tree and the next write step would fail instead.
|
|
225
226
|
- **A Deploy-door move nobody can fix files a blocker and posts to Discord — once** (task 1004449, `scripts/gds/move-escalation.js`). A move retired as `unknown` (including `remedy_did_not_hold`, `remedy_failed` and a failed rollback) files one blocker (`source=provisioning`, `source_ref=core-move:<instance id>:<intent id>`) with the reason and what the move reported, and posts one notice to `#ship-news` that names the project only if it is public. The next core move on that project that lands closes the blocker by itself and posts an all-clear. Refusals the owner can act on (`needs_decision`) are shown on the deploy page instead, not filed.
|
|
226
227
|
- **`--registry` needs no credential at all** (task 1003878). `@bongos/core` is **public** on npm (task 1003872), so `bongos upgrade --registry` resolves it from the default registry like any other dependency — no token, no login, and no instance `.npmrc`. An install failure on this path is therefore a *real* failure (network, registry outage, or a version that was never published), never a missing credential.
|
|
227
228
|
*History, so the symptom is recognisable on an old instance:* the core used to publish private, and the instance `.npmrc` read `_authToken=<redacted> npm gives that per-project key precedence, so with the env var unset it expanded EMPTY and **masked a perfectly valid literal token in `~/.npmrc`** — npm went anonymous, the private core 404'd, and the error blamed a credential the box actually held. Task 1002754 worked around it by resolving the token in-process; task 1003878 deleted the whole mechanism along with the `.npmrc` that caused it. An instance still carrying a committed `.npmrc` from a pre-1003878 scaffold can safely delete it.
|
|
@@ -142,6 +142,9 @@
|
|
|
142
142
|
not_published: 'that version is not published yet',
|
|
143
143
|
npm_install: 'installing the new core stopped part-way',
|
|
144
144
|
dirty_tree: 'the project had unsaved file changes on its machine',
|
|
145
|
+
// Deliberately not a variant of dirty_tree above: nothing is known about the files here,
|
|
146
|
+
// which is the whole difference (task 1004512).
|
|
147
|
+
git_unreadable: 'the project’s files could not be read on its machine, so the move never started — nothing changed, and someone is looking at it',
|
|
145
148
|
disk_full: 'the machine ran out of disk space',
|
|
146
149
|
schema_pending: 'the project’s database was behind its code',
|
|
147
150
|
outside_channel: 'that version is outside this project’s update rule',
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
<!-- the identity chip IS the profile trigger; paintBar fills it from GET /me.
|
|
105
105
|
Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
106
106
|
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
107
|
-
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/account">Sign in</a>
|
|
107
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/account&repos=1">Sign in</a>
|
|
108
108
|
</div>
|
|
109
109
|
</header>
|
|
110
110
|
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
<section class="aDoor" id="door" hidden aria-labelledby="door-h">
|
|
139
139
|
<h2 class="vh" id="door-h">Sign in</h2>
|
|
140
140
|
<p>Sign in with GitHub to change your handle, your bio and links, and who can find you.</p>
|
|
141
|
-
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/account">
|
|
141
|
+
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/account&repos=1">
|
|
142
142
|
<!-- keep the spaces inside this path's numbers: same shape, and they are load-bearing (task 1004401) -->
|
|
143
143
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23 .82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2 .82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12 .51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29 .25.54.73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15 .46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
144
144
|
Sign in with GitHub
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"probes": [
|
|
6
6
|
{ "name": "signed out: the door, and sign-in returns to /account", "auth": false, "width": 1440,
|
|
7
7
|
"steps": [["wait", 300]],
|
|
8
|
-
"expect": { "visible": ["#door"], "hidden": ["#acctBody"], "href": [["#signin", "/api/bongos/auth/web/start?return=/account"]] } },
|
|
8
|
+
"expect": { "visible": ["#door"], "hidden": ["#acctBody"], "href": [["#signin", "/api/bongos/auth/web/start?return=/account&repos=1"]] } },
|
|
9
9
|
{ "name": "signed in: nothing changed saves nothing", "auth": true, "width": 1440,
|
|
10
10
|
"steps": [["wait", 400], ["click", "#profileSave"], ["wait", 150]],
|
|
11
11
|
"expect": { "text": [["#profileMsg", "Nothing has changed."]] } },
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"expect": { "closed": "#menuProjects" } },
|
|
57
57
|
{ "name": "signed out: no chip, the sign-in door in its slot, returning to /account", "auth": false, "width": 1440,
|
|
58
58
|
"steps": [["wait", 300]],
|
|
59
|
-
"expect": { "hidden": ["#who"], "visible": ["#signinTop", "#projTrig"], "href": [["#signinTop", "/api/bongos/auth/web/start?return=/account"]] } },
|
|
59
|
+
"expect": { "hidden": ["#who"], "visible": ["#signinTop", "#projTrig"], "href": [["#signinTop", "/api/bongos/auth/web/start?return=/account&repos=1"]] } },
|
|
60
60
|
{ "name": "signed out the Projects band is Sky / Table / New project (no Your projects)", "continue": true,
|
|
61
61
|
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
62
62
|
"expect": { "open": "#menuProjects", "count": [["#menuProjects .bandCell:not([hidden])", 3]], "hidden": ["#mineCell"] } },
|
|
@@ -673,7 +673,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
673
673
|
<!-- the identity slot: the sign-in door signed out; the chip (the profile trigger) once
|
|
674
674
|
the session resolves — swapped client-side, never a wrong-state flash -->
|
|
675
675
|
<span class="whoSlot" id="who">
|
|
676
|
-
<a class="pill signin" href="/api/bongos/auth/web/start?return
|
|
676
|
+
<a class="pill signin" href="/api/bongos/auth/web/start?return=/&repos=1">Sign in</a>
|
|
677
677
|
</span>
|
|
678
678
|
</div>
|
|
679
679
|
</header>
|
|
@@ -705,7 +705,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
705
705
|
<!-- the identity chip IS the profile trigger; renderWho fills it. Signed
|
|
706
706
|
out it does not exist and the sign-in door takes its slot. -->
|
|
707
707
|
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden></button>
|
|
708
|
-
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/projects">Sign in</a>
|
|
708
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/projects&repos=1">Sign in</a>
|
|
709
709
|
</div>
|
|
710
710
|
</header>
|
|
711
711
|
|
|
@@ -897,7 +897,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
897
897
|
/create, listing your own instances needs no repo scope) -->
|
|
898
898
|
<section id="gate" hidden>
|
|
899
899
|
<p class="lede">Every project you own, in one place — its live status, its address, and the controls that matter. Sign in with GitHub to see yours.</p>
|
|
900
|
-
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
|
|
900
|
+
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine&repos=1">
|
|
901
901
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
902
902
|
Sign in with GitHub
|
|
903
903
|
</a>
|
|
@@ -999,7 +999,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
999
999
|
<!-- signed out: the same identity-only door the mine view uses (ADR 0143) -->
|
|
1000
1000
|
<section id="mGate" class="card" hidden>
|
|
1001
1001
|
<p class="lede">This is a project’s management page — it answers only to the person who owns it. Sign in with GitHub to open yours.</p>
|
|
1002
|
-
<a class="btn" id="mSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
|
|
1002
|
+
<a class="btn" id="mSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine&repos=1">
|
|
1003
1003
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
1004
1004
|
Sign in with GitHub
|
|
1005
1005
|
</a>
|
|
@@ -1395,7 +1395,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1395
1395
|
<!-- The wizard's own sign-in opts into repo scope (read:user + repo, ADR 0155) up
|
|
1396
1396
|
front (&repos=1) — anyone here intends to create a project, which needs repo
|
|
1397
1397
|
access, so we ask once at sign-in instead of a second "Connect" step later.
|
|
1398
|
-
|
|
1398
|
+
Every hub sign-in door does the same since ADR 0364; the hall + CLI
|
|
1399
|
+
sign-ins stay identity-only (ADR 0143 least-privilege default). -->
|
|
1399
1400
|
<a class="btn big" id="wizSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dnew&repos=1">
|
|
1400
1401
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
1401
1402
|
Continue with GitHub
|
|
@@ -2494,11 +2495,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2494
2495
|
else history.replaceState(null, '', '?view=' + v + idPart);
|
|
2495
2496
|
} catch (e) {}
|
|
2496
2497
|
if (v !== 'manage') document.title = BASE_TITLE;
|
|
2497
|
-
/* the shell's sign-in door returns the visitor to the view they left
|
|
2498
|
-
|
|
2499
|
-
|
|
2498
|
+
/* the shell's sign-in door returns the visitor to the view they left. Every
|
|
2499
|
+
sign-in door on the hub's own pages carries the repo scope (task 1004549,
|
|
2500
|
+
ADR 0364): it is asked once, at sign-in, and held for the whole session, so
|
|
2501
|
+
no flow ever stops halfway to ask for it */
|
|
2500
2502
|
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
|
|
2501
|
-
idPart.replace('&', '%26').replace('=', '%3D') +
|
|
2503
|
+
idPart.replace('&', '%26').replace('=', '%3D') + '&repos=1';
|
|
2502
2504
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
2503
2505
|
card that changed while you were on the map is not stale — including
|
|
2504
2506
|
the membership + invitations lists (task 1003774), the same re-read
|
|
@@ -7190,8 +7192,15 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7190
7192
|
function enter(focus) {
|
|
7191
7193
|
if (authKnown && !me) { show(0, !!focus); return; }
|
|
7192
7194
|
var created = authKnown ? createdRecord() : null;
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
+
/* A draft part-way through the wizard (steps 1–5) beside a stored create is a
|
|
7196
|
+
SECOND project being typed. A reload or a GitHub round-trip (repo access) is
|
|
7197
|
+
that same visit continuing, so the draft keeps its place and the earlier
|
|
7198
|
+
project stays one click away on the resume line — it used to land on the
|
|
7199
|
+
earlier project's done panel, dropping the founder out of the draft
|
|
7200
|
+
(task 1004549). A deliberate nav still goes through enterCreated. */
|
|
7201
|
+
var draftInFlight = state.step >= 1 && state.step <= 5;
|
|
7202
|
+
if (created && created.id && (focus || !draftInFlight || resumeIntent)) { enterCreated(created, !!focus); return; }
|
|
7203
|
+
paintResumeOffer(created && created.id ? created : null);
|
|
7195
7204
|
/* before identity settles, never render past step 1 (the fork and its terms
|
|
7196
7205
|
tick, since task 1004504): deeper steps carry authenticated side effects (the Home-step repo
|
|
7197
7206
|
probe), and onAuth() re-enters to the real draft step the moment /me answers */
|
|
@@ -7345,8 +7354,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7345
7354
|
function jsonOrEmpty(res) { return res.json().catch(function () { return {}; }); }
|
|
7346
7355
|
|
|
7347
7356
|
function signInAgain() {
|
|
7348
|
-
/*
|
|
7349
|
-
var back = currentView === 'new' ? '/projects%3Fview%3Dnew
|
|
7357
|
+
/* the re-sign-in keeps the repo scope, like every hub sign-in door (ADR 0364) */
|
|
7358
|
+
var back = (currentView === 'new' ? '/projects%3Fview%3Dnew' : '/projects') + '&repos=1';
|
|
7350
7359
|
return 'Your session expired — <a href="' + API + '/auth/web/start?return=' + back + '">sign in again</a>.';
|
|
7351
7360
|
}
|
|
7352
7361
|
|
|
@@ -7948,7 +7957,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7948
7957
|
in hand so "checked Xm ago" keeps telling the truth (C6). ── */
|
|
7949
7958
|
|
|
7950
7959
|
function manageSignin() {
|
|
7951
|
-
return API + '/auth/web/start?return=' +
|
|
7960
|
+
return API + '/auth/web/start?repos=1&return=' +
|
|
7952
7961
|
encodeURIComponent('/projects?view=manage&id=' + manageId);
|
|
7953
7962
|
}
|
|
7954
7963
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.16",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.16",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.16",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8840,5 +8840,17 @@
|
|
|
8840
8840
|
"id": "1004537",
|
|
8841
8841
|
"text": "Autobongos no longer picks tasks that can only be done by logging into a live server. They are skipped before any claim and listed as \"needs_host_access\", so no more wasted runs and pointless owner blockers."
|
|
8842
8842
|
}
|
|
8843
|
+
],
|
|
8844
|
+
"1.21.15": [
|
|
8845
|
+
{
|
|
8846
|
+
"id": "1004512",
|
|
8847
|
+
"text": "Deploying a core update to a project that runs under its own account used to fail every time, blaming unsaved file changes that were never there. The update now writes files as the account that owns them and touches the databa"
|
|
8848
|
+
}
|
|
8849
|
+
],
|
|
8850
|
+
"1.21.16": [
|
|
8851
|
+
{
|
|
8852
|
+
"id": "1004549",
|
|
8853
|
+
"text": "Signing in on cloudbongos.com now asks GitHub for repo access once and keeps it while you stay signed in, so project setup no longer stops to ask — and a trip to GitHub mid-setup brings you back exactly where you were."
|
|
8854
|
+
}
|
|
8843
8855
|
]
|
|
8844
8856
|
}
|
|
@@ -319,8 +319,8 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
319
319
|
// The preview probes; the move must probe identically or the two commands diverge on
|
|
320
320
|
// the one field task 1004121 exists to keep identical. Skipped when !apply, where the
|
|
321
321
|
// command is printed and never run — a plan should not spend a subprocess per tick.
|
|
322
|
-
const { runAs } = resolveRunAs(inst, deps, { probe: apply });
|
|
323
|
-
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy, selfRoot, runAs });
|
|
322
|
+
const { runAs, migrateAs } = resolveRunAs(inst, deps, { probe: apply });
|
|
323
|
+
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy, selfRoot, runAs, migrateAs });
|
|
324
324
|
|
|
325
325
|
if (!apply) {
|
|
326
326
|
// A --dry-run runner tick used to only PRINT this command. It now runs the CLI's own
|
|
@@ -571,7 +571,24 @@ function parsePreflight(out) {
|
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
/**
|
|
574
|
-
* Which unix
|
|
574
|
+
* Which unix accounts does this instance's upgrade need? Asks the box (task 1004128).
|
|
575
|
+
*
|
|
576
|
+
* TWO ANSWERS, NOT ONE (task 1004512). Task 1004128 asked a single question — "which
|
|
577
|
+
* account does this instance run as?" — and ran the WHOLE move as it. That is right for
|
|
578
|
+
* the database and wrong for the files, and the two have different owners on every
|
|
579
|
+
* project provisioned since task 1003369:
|
|
580
|
+
*
|
|
581
|
+
* • `runAs` — the account that owns the CHECKOUT, which is the app user on every
|
|
582
|
+
* hosting shape (provision-repo.js scaffolds /srv/<base>/<slug> as the runner, and
|
|
583
|
+
* grantInstanceRepoReadCmd gives the project's own account group READ, nothing more).
|
|
584
|
+
* Everything the move writes — the pin, node_modules, .claude/, the pin commit — and
|
|
585
|
+
* the `git status` it starts with, belong to this account. Running them as the
|
|
586
|
+
* project's account is what made every such move die on `fatal: detected dubious
|
|
587
|
+
* ownership in repository` and report a dirty tree that did not exist.
|
|
588
|
+
* • `migrateAs` — the project's OWN account, which is also its Postgres login role
|
|
589
|
+
* (provision-repo.js instanceDbRole); its database is owned by that role with CONNECT
|
|
590
|
+
* revoked from PUBLIC, so this is the only identity that can migrate it. That is where
|
|
591
|
+
* the isolation of task 1003369 actually lives, and it is kept.
|
|
575
592
|
*
|
|
576
593
|
* The fleet is heterogeneous: instances provisioned before task 1003369 run as the
|
|
577
594
|
* shared app user, instances provisioned after run as their own `bongos-<slug>`. Neither
|
|
@@ -580,13 +597,15 @@ function parsePreflight(out) {
|
|
|
580
597
|
* tenant leg has been broken.
|
|
581
598
|
*
|
|
582
599
|
* Falling back is NOT a privilege escalation: an instance with no account of its own is
|
|
583
|
-
* already running as the app user (its unit says so), so this
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
600
|
+
* already running as the app user (its unit says so), so this migrates as precisely the
|
|
601
|
+
* account that owns the database it is about to touch. It IS reported, because a missing
|
|
602
|
+
* per-instance account is a provisioning gap someone should close, and a silent fallback
|
|
603
|
+
* would hide it forever. On that shape the two answers coincide and the move is byte-for-
|
|
604
|
+
* byte what it was before this task.
|
|
587
605
|
*/
|
|
588
606
|
function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
589
|
-
|
|
607
|
+
const owner = appUser(); // owns the checkout on every shape — see above
|
|
608
|
+
if (inst.hosting_shape === 'control-plane') return { runAs: owner, migrateAs: owner, fellBack: false };
|
|
590
609
|
// Re-checked HERE rather than left to instanceUser()'s own throw, for the reason
|
|
591
610
|
// upgradeInvocation states about itself: a call site whose safety depends on a
|
|
592
611
|
// helper's internals is one refactor away from a shell-interpolation path.
|
|
@@ -594,7 +613,7 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
594
613
|
throw new Error(`resolveRunAs: refusing to probe an account for invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
595
614
|
}
|
|
596
615
|
const wanted = instanceUser(inst);
|
|
597
|
-
if (!allowProbe) return { runAs: wanted, fellBack: false };
|
|
616
|
+
if (!allowProbe) return { runAs: owner, migrateAs: wanted, fellBack: false };
|
|
598
617
|
// THE SAME EXECUTOR THE UPGRADE ITSELF WILL USE. Every sibling here picks
|
|
599
618
|
// `standalone ? (controlExec || exec) : exec`, because under PROVISION_REMOTE_HOST
|
|
600
619
|
// (ADR 0130) `exec` is the REMOTE ssh executor while `controlExec` stays local. A
|
|
@@ -604,7 +623,7 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
604
623
|
const boxExec = inst.hosting_shape === 'cloud-host' ? (deps.controlExec || deps.exec) : deps.exec;
|
|
605
624
|
const r = boxExec(`id -u ${wanted}`, { allowFail: true });
|
|
606
625
|
const failed = !r || r.softFailed === true || r.ok === false;
|
|
607
|
-
if (!failed) return { runAs: wanted, fellBack: false };
|
|
626
|
+
if (!failed) return { runAs: owner, migrateAs: wanted, fellBack: false };
|
|
608
627
|
// ONLY A DEFINITIVE "no such user" JUSTIFIES THE FALLBACK. A probe that could not
|
|
609
628
|
// answer — an exec timeout, an NSS hiccup — must not silently narrow the per-instance
|
|
610
629
|
// isolation of task 1003369 for this operation. So an inconclusive probe keeps the
|
|
@@ -612,9 +631,9 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
612
631
|
// task a failure now carries the reason instead of "the dry run did not complete".
|
|
613
632
|
const said = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
614
633
|
if (!/no such user|unknown user/i.test(said)) {
|
|
615
|
-
return { runAs: wanted, fellBack: false, probeInconclusive: true };
|
|
634
|
+
return { runAs: owner, migrateAs: wanted, fellBack: false, probeInconclusive: true };
|
|
616
635
|
}
|
|
617
|
-
return { runAs:
|
|
636
|
+
return { runAs: owner, migrateAs: owner, fellBack: true, wanted };
|
|
618
637
|
}
|
|
619
638
|
|
|
620
639
|
// ONE BUILDER FOR BOTH LEGS (task 1004121).
|
|
@@ -630,7 +649,7 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
630
649
|
// HERE rather than left to instanceUser()'s own throw: that throw is correct today, but
|
|
631
650
|
// a call site whose safety depends on a helper's internals is one refactor away from a
|
|
632
651
|
// shell-injection path (and the apply leg's own guard sits at the route, not here).
|
|
633
|
-
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot, runAs = null } = {}) {
|
|
652
|
+
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot, runAs = null, migrateAs = null } = {}) {
|
|
634
653
|
if (!isValidSlug(inst && inst.slug)) {
|
|
635
654
|
throw new Error(`upgradeInvocation: refusing to build a shell command from an invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
636
655
|
}
|
|
@@ -665,11 +684,22 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
665
684
|
// (resolveRunAs below) instead of either side assuming. Passing it in keeps this
|
|
666
685
|
// builder pure; the default preserves the old derivation for callers that have not
|
|
667
686
|
// probed.
|
|
668
|
-
|
|
687
|
+
//
|
|
688
|
+
// AND WHICH ACCOUNT DOES WHAT (task 1004512). `account` runs the move; `dbAccount` runs
|
|
689
|
+
// its three database steps. They differ on exactly the projects that have an account of
|
|
690
|
+
// their own, because the checkout is the app user's and the database is theirs —
|
|
691
|
+
// resolveRunAs above argues the split. `--migrate-as` is emitted ONLY when they differ,
|
|
692
|
+
// so a control plane and a pre-1003369 project produce the command they always did.
|
|
693
|
+
const account = runAs || appUser();
|
|
694
|
+
const dbAccount = migrateAs || (inst.hosting_shape === 'control-plane' ? appUser() : instanceUser(inst));
|
|
669
695
|
const sudoP = privileged ? `sudo -u ${account} env PGDATABASE=${dbName(inst)} ` : '';
|
|
670
696
|
const cli = path.posix.join('scripts', 'gds', 'upgrade.js');
|
|
671
697
|
const shared = [`${sudoP}node ${cli}`, '--registry', `--to ${to}`,
|
|
672
|
-
instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}
|
|
698
|
+
instanceDir === REPO_ROOT ? '' : `--instance ${instanceDir}`, // REPO_ROOT is where the CLI runs (runDir below)
|
|
699
|
+
// SHARED, not in the tail: the dry run's own database pre-check connects as this
|
|
700
|
+
// account too, so a preview that omitted it would test a different identity than the
|
|
701
|
+
// move — the exact drift one builder for both legs exists to prevent.
|
|
702
|
+
dbAccount === account ? '' : `--migrate-as ${dbAccount}`];
|
|
673
703
|
const tail = dryRun
|
|
674
704
|
? ['--dry-run']
|
|
675
705
|
: [`--service ${inst.slug}`,
|
|
@@ -681,7 +711,7 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
681
711
|
hubPushesPin(inst) ? '--pin-no-push' : '',
|
|
682
712
|
requestedBy ? `--requested-by ${requestedBy}` : '',
|
|
683
713
|
requestedBy ? '--upgrade-source owner-control' : ''];
|
|
684
|
-
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs: account };
|
|
714
|
+
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs: account, migrateAs: dbAccount };
|
|
685
715
|
}
|
|
686
716
|
|
|
687
717
|
// Ask the upgrade CLI what it WOULD do, changing nothing. `allowFail` because a refusal
|
|
@@ -692,8 +722,8 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
692
722
|
// A distinct local name: `deps.selfInstanceRoot || selfInstanceRoot` with a const of
|
|
693
723
|
// the same name would TDZ-shadow the import and crash every uninjected call.
|
|
694
724
|
const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
|
|
695
|
-
const { runAs, fellBack, wanted } = resolveRunAs(inst, deps);
|
|
696
|
-
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, dryRun: true, selfRoot, runAs });
|
|
725
|
+
const { runAs, migrateAs, fellBack, wanted } = resolveRunAs(inst, deps);
|
|
726
|
+
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, dryRun: true, selfRoot, runAs, migrateAs });
|
|
697
727
|
const r = boxExec(cmd, { cwd: runDir, allowFail: true });
|
|
698
728
|
const out = `${(r && r.stdout) || ''}\n${(r && r.stderr) || ''}`;
|
|
699
729
|
const { checks, error } = parsePreflight(out);
|
|
@@ -709,10 +739,12 @@ function preflightUpgrade(inst, to, deps) {
|
|
|
709
739
|
const raw = out.trim().split('\n').filter(Boolean).slice(-4).join('\n').slice(0, 600);
|
|
710
740
|
const why = error || (raw || null);
|
|
711
741
|
const note = fellBack
|
|
712
|
-
? `
|
|
742
|
+
? `migrated as ${migrateAs} — this instance has no ${wanted} account yet (it predates the per-instance account model)`
|
|
713
743
|
: null;
|
|
714
744
|
return {
|
|
715
|
-
|
|
745
|
+
// Both accounts, because since task 1004512 there are two and "which user did this run
|
|
746
|
+
// as" has no single answer: `ran_as` writes the files, `migrated_as` touches the database.
|
|
747
|
+
target: to, ok, lines: checks, ran_as: runAs, migrated_as: migrateAs, account_note: note,
|
|
716
748
|
error: ok ? null : (why || 'the dry run did not complete, and said nothing this could report'),
|
|
717
749
|
};
|
|
718
750
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// scripts/gds/upgrade-migrate.js —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// scripts/gds/upgrade-migrate.js — the DATABASE side of a core move: which command
|
|
4
|
+
// migrates this instance (task 1004274, found preparing the task 1004047 catch-up pass),
|
|
5
|
+
// WHICH ACCOUNT that command runs as, and whether that account can reach the database at
|
|
6
|
+
// all (task 1004512). Kept out of upgrade.js, which sits at the 1500-line cap.
|
|
6
7
|
//
|
|
7
8
|
// THE GAP. upgrade.js migrated with `npm run migrate`, and that only works where the
|
|
8
9
|
// instance's package.json DECLARES a migrate script. A greenfield scaffold always does
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
|
|
27
28
|
const fs = require('node:fs');
|
|
28
29
|
const path = require('node:path');
|
|
30
|
+
const { spawnSync } = require('node:child_process');
|
|
29
31
|
|
|
30
32
|
// Relative to the instance root, POSIX on purpose: it is an argument to bash, and it
|
|
31
33
|
// must stay byte-identical to the scaffold's script (tests/upgrade_migrate.mjs pins it
|
|
@@ -64,4 +66,99 @@ function migratePlan(instanceDir, { fsImpl = fs, env = process.env } = {}) {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
// ---- which ACCOUNT the database work runs as (task 1004512) -----------------
|
|
70
|
+
//
|
|
71
|
+
// THE BUG THIS EXISTS FOR. A core move on a project provisioned since task 1003369 ran
|
|
72
|
+
// the WHOLE of upgrade.js as the project's own `bongos-<slug>` account. That account only
|
|
73
|
+
// READS the checkout (provision-repo.js grantInstanceRepoReadCmd adds it to the app user's
|
|
74
|
+
// group; /srv/<base>/<slug> is owned by the app user), so the move died at its very first
|
|
75
|
+
// step: `git status` refused with `fatal: detected dubious ownership in repository`, the
|
|
76
|
+
// clean-tree pre-flight reported "working tree not clean", and the self-heal chain
|
|
77
|
+
// committed a tree that was never dirty and retried into the identical failure. Every move
|
|
78
|
+
// on such a project ended in a blocker, and the dirt it named did not exist. Had git been
|
|
79
|
+
// appeased, `npm install`, the pin commit and the .claude materialization — all WRITES to a
|
|
80
|
+
// checkout that account cannot write — would have failed next.
|
|
81
|
+
//
|
|
82
|
+
// SO THE MOVE IS SPLIT, rather than papered over with a `safe.directory` entry:
|
|
83
|
+
// • everything that touches the CHECKOUT runs as the account that owns it (the app
|
|
84
|
+
// user) — that is also what pushUpgradePin and the wedge remedy's `git` already do;
|
|
85
|
+
// • everything that touches the DATABASE runs as the project's own account, which is
|
|
86
|
+
// where the isolation actually lives: it is the project's Postgres login role
|
|
87
|
+
// (provision-repo.js instanceDbRole), the database is owned by it, and CONNECT is
|
|
88
|
+
// revoked from PUBLIC — so the app user cannot reach it even by accident.
|
|
89
|
+
//
|
|
90
|
+
// The runner passes `--migrate-as <account>` ONLY when the two differ. A control plane and
|
|
91
|
+
// a project old enough to still run as the app user emit no flag and behave exactly as
|
|
92
|
+
// they did before, which is the half of the fleet that was already working.
|
|
93
|
+
//
|
|
94
|
+
// Narrower than a unix account name may legally be, on purpose: the value arrives on a
|
|
95
|
+
// command line and is refused rather than quoted if it falls outside. ONE copy for the
|
|
96
|
+
// upgrade CLI — wedge-remedy.js imports this rather than re-declaring it, because two
|
|
97
|
+
// copies of a security predicate drift one edit at a time. provision-repo.js keeps its own
|
|
98
|
+
// UNIX_ACCOUNT_RE: that file is the control-plane provisioner and nothing it does should
|
|
99
|
+
// depend on the upgrade CLI, so the duplication there buys a layer boundary.
|
|
100
|
+
const ACCOUNT_RE = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `cmd args` as `account`, or unchanged when there is no account to drop to. PURE.
|
|
104
|
+
*
|
|
105
|
+
* argv form, never a shell string, so nothing here is quoted or interpolated. `sudo -n`
|
|
106
|
+
* because a sudoers rule that would PROMPT must fail fast: an upgrade runs unattended and
|
|
107
|
+
* a password prompt nobody can answer would hang the deploy rather than fail it.
|
|
108
|
+
*
|
|
109
|
+
* sudo resets the environment, so the variables migrate genuinely needs are re-stated as
|
|
110
|
+
* `env K=V` arguments: PGDATABASE (migrate.sh defaults to production when it is unset, so
|
|
111
|
+
* it is always passed and never inherited) and, for the core-script fallback, INIT_CWD
|
|
112
|
+
* (which is how migrate.sh finds the instance — task 2180).
|
|
113
|
+
*/
|
|
114
|
+
function asAccount(account, cmd, args, extraEnv = {}, env = process.env) {
|
|
115
|
+
if (!account) return { cmd, args };
|
|
116
|
+
if (!ACCOUNT_RE.test(String(account))) throw new Error(`asAccount: ${JSON.stringify(account)} is not a unix account name`);
|
|
117
|
+
const carry = { ...(env.PGDATABASE ? { PGDATABASE: env.PGDATABASE } : {}), ...extraEnv };
|
|
118
|
+
const pairs = Object.entries(carry).map(([k, v]) => `${k}=${v}`);
|
|
119
|
+
return { cmd: 'sudo', args: ['-n', '-u', account, 'env', ...pairs, cmd, ...args] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// migrate.sh reaches Postgres over the local socket as the CURRENT OS USER (peer
|
|
123
|
+
// auth) — so running an upgrade under `sudo` connects as `root`, which usually has
|
|
124
|
+
// no Postgres role. The failure surfaces mid-run as a raw
|
|
125
|
+
// `FATAL: role "root" does not exist`, AFTER the pin has already moved, which then
|
|
126
|
+
// triggers the whole auto-rollback dance for what is really an operator mistake.
|
|
127
|
+
// This names it up front instead (task 1002712). `account` (task 1004512) makes the
|
|
128
|
+
// probe connect as whoever migrate will, so the pre-flight and the migration cannot
|
|
129
|
+
// disagree about which role is being tested.
|
|
130
|
+
function preflightDbIdentity({ instanceDir, db, account = null }, run = spawnSync) {
|
|
131
|
+
const q = asAccount(account, 'psql', ['-tAc', 'select 1'], db ? { PGDATABASE: db } : {});
|
|
132
|
+
const r = run(q.cmd, q.args, {
|
|
133
|
+
cwd: instanceDir, encoding: 'utf8',
|
|
134
|
+
env: { ...process.env, ...(db ? { PGDATABASE: db } : {}) },
|
|
135
|
+
});
|
|
136
|
+
if (!r || r.error) return { ok: true, skipped: 'psql not on PATH — leaving it to migrate' };
|
|
137
|
+
if (r.status === 0) return { ok: true };
|
|
138
|
+
|
|
139
|
+
const out = `${r.stderr || ''}${r.stdout || ''}`;
|
|
140
|
+
// The escalation itself not working is this pre-flight's business, not migrate's (task
|
|
141
|
+
// 1004512). Soft-skipping it would let the pin move and then fail at migrate for the one
|
|
142
|
+
// reason that was knowable up front, costing a full rollback — the exact shape the
|
|
143
|
+
// role-does-not-exist check below exists to avoid. Only ever reachable with an account.
|
|
144
|
+
if (account && /^sudo:/m.test(out)) {
|
|
145
|
+
return { ok: false, reason: `the database steps cannot run as "${account}" — sudo refused.\n`
|
|
146
|
+
+ ` ${out.trim().split('\n').find((l) => l.startsWith('sudo:')) || ''}\n`
|
|
147
|
+
+ ` The move writes files as the account that owns the checkout and hands only the\n`
|
|
148
|
+
+ ` database to the project's own account (--migrate-as), so the first needs\n`
|
|
149
|
+
+ ` passwordless sudo to the second. Checked BEFORE the pin moves.` };
|
|
150
|
+
}
|
|
151
|
+
const m = out.match(/role "([^"]+)" does not exist/i);
|
|
152
|
+
if (!m) return { ok: true, skipped: 'psql failed for another reason — leaving it to migrate' };
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
role: m[1],
|
|
156
|
+
reason: `Postgres has no role "${m[1]}" — migrate would fail after the pin moved.\n`
|
|
157
|
+
+ ` migrate.sh connects over the local socket as the CURRENT OS USER. Running the\n`
|
|
158
|
+
+ ` upgrade under sudo connects as root, which is almost never a Postgres role.\n`
|
|
159
|
+
+ ` Re-run as the user that owns the instance (e.g. sudo -u <owner> …), passing the\n`
|
|
160
|
+
+ ` npm token through if the core comes from the registry.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { migratePlan, preflightDbIdentity, asAccount, ACCOUNT_RE, CORE_MIGRATE_SCRIPT };
|
|
@@ -45,6 +45,11 @@ const REASON_CLASS = Object.freeze({
|
|
|
45
45
|
schema_pending: 'known_wedge', // the database is behind the code it runs NOW (task 1004448)
|
|
46
46
|
// upgrade.js refusals, before the pin moves
|
|
47
47
|
dirty_tree: 'known_wedge',
|
|
48
|
+
// git REFUSED to read the checkout, so nothing is known about the tree (task 1004512).
|
|
49
|
+
// Deliberately NOT known_wedge: the dirty_tree remedy commits whatever git reports, and
|
|
50
|
+
// here git reported nothing — the live shape is `detected dubious ownership`, a tree that
|
|
51
|
+
// was never dirty. The fix is which ACCOUNT the move runs as, which no remedy can apply.
|
|
52
|
+
git_unreadable: 'unknown',
|
|
48
53
|
downgrade: 'needs_decision',
|
|
49
54
|
artist_gate: 'needs_decision',
|
|
50
55
|
module_incompatible: 'needs_decision',
|
|
@@ -73,6 +78,7 @@ const UPGRADE_REASONS = [
|
|
|
73
78
|
[/no space left on device|ENOSPC/i, 'disk_full'],
|
|
74
79
|
[/^refusing to DOWNGRADE/, 'downgrade'],
|
|
75
80
|
[/^already on /, 'already_on'],
|
|
81
|
+
[/^git could not read the working tree/, 'git_unreadable'],
|
|
76
82
|
[/^working tree not clean/, 'dirty_tree'],
|
|
77
83
|
[/^module pre-check failed/, 'module_incompatible'],
|
|
78
84
|
[/^cannot determine which database/, 'db_unresolvable'],
|