@bongos/core 1.21.84 → 1.21.86
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 +33 -23
- package/docs/architecture.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/page-tweak-hold.js +12 -0
- package/modules/lifecycle/routes/tasks.js +4 -3
- package/modules/lifecycle/ship-card.js +18 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/ship-finish.js +4 -2
- package/scripts/gds/ship-flow.js +4 -2
- package/scripts/gds/ship-grade-steps.js +7 -2
- package/scripts/gds/ship-land.js +2 -2
- package/src/bongos/module-store.js +101 -5
- package/src/bongos/routes/modules.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/held_tweak_reporting.mjs +164 -0
- package/tests/module_store_instance_root.mjs +163 -0
- package/tests/page_tweak_hold.mjs +1 -2
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.86",
|
|
6
|
+
"core_contract": "1.21.86",
|
|
7
|
+
"source_commit": "7183f6e501a4de2ea256ffa4c2d19ccc62d7fdc1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-09T04:48:08.126Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 590,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2998,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3617,
|
|
20
|
+
"tree_sha256": "391c304f1f86515a2c812e6764e83ba98b5fe6a67757909baf15d8e88009694b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2327,7 +2327,7 @@
|
|
|
2327
2327
|
{
|
|
2328
2328
|
"path": "docs/architecture.md",
|
|
2329
2329
|
"mode": "0000644",
|
|
2330
|
-
"sha256": "
|
|
2330
|
+
"sha256": "0dac6b0ee55d7836cf6352c695f592000bfd61d0c3ec06d9c17d37b476b1134a"
|
|
2331
2331
|
},
|
|
2332
2332
|
{
|
|
2333
2333
|
"path": "docs/branding-contract.md",
|
|
@@ -2862,7 +2862,7 @@
|
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "docs/module-api-changelog.md",
|
|
2864
2864
|
"mode": "0000644",
|
|
2865
|
-
"sha256": "
|
|
2865
|
+
"sha256": "007f5683bb299be37f78f220958ae14bb8a8069fc59b81cc6fb94a12c5b3712f"
|
|
2866
2866
|
},
|
|
2867
2867
|
{
|
|
2868
2868
|
"path": "docs/modules-contract.md",
|
|
@@ -7202,7 +7202,7 @@
|
|
|
7202
7202
|
{
|
|
7203
7203
|
"path": "modules/lifecycle/page-tweak-hold.js",
|
|
7204
7204
|
"mode": "0000644",
|
|
7205
|
-
"sha256": "
|
|
7205
|
+
"sha256": "54703e1d2814402218057c65691e5fd1f11572eb9e667c1e0148ecb5cc9efe80"
|
|
7206
7206
|
},
|
|
7207
7207
|
{
|
|
7208
7208
|
"path": "modules/lifecycle/page-tweak-reads.js",
|
|
@@ -7377,7 +7377,7 @@
|
|
|
7377
7377
|
{
|
|
7378
7378
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
7379
7379
|
"mode": "0000644",
|
|
7380
|
-
"sha256": "
|
|
7380
|
+
"sha256": "efd442d161d9b212d1e9aa4cad06aa428b1e4c74ac67d22658591ca7c28f2621"
|
|
7381
7381
|
},
|
|
7382
7382
|
{
|
|
7383
7383
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -7402,7 +7402,7 @@
|
|
|
7402
7402
|
{
|
|
7403
7403
|
"path": "modules/lifecycle/ship-card.js",
|
|
7404
7404
|
"mode": "0000644",
|
|
7405
|
-
"sha256": "
|
|
7405
|
+
"sha256": "fc803defc468a0e625d82f305d00118ec77bb63d74d7b75b4d3ba03bdd16659d"
|
|
7406
7406
|
},
|
|
7407
7407
|
{
|
|
7408
7408
|
"path": "modules/lifecycle/ship-files.js",
|
|
@@ -10097,12 +10097,12 @@
|
|
|
10097
10097
|
{
|
|
10098
10098
|
"path": "package-lock.json",
|
|
10099
10099
|
"mode": "0000644",
|
|
10100
|
-
"sha256": "
|
|
10100
|
+
"sha256": "9d3ecc5f558e51dc8e4e66a6f3f0a6e9773e517ce9571ae4ec130b33128536ff"
|
|
10101
10101
|
},
|
|
10102
10102
|
{
|
|
10103
10103
|
"path": "package.json",
|
|
10104
10104
|
"mode": "0000644",
|
|
10105
|
-
"sha256": "
|
|
10105
|
+
"sha256": "ce90ad007650b7bcf0705bf0743acf66507496e0a043d7d6c2d9b84795b94f9b"
|
|
10106
10106
|
},
|
|
10107
10107
|
{
|
|
10108
10108
|
"path": "public-docs/index.html",
|
|
@@ -10122,7 +10122,7 @@
|
|
|
10122
10122
|
{
|
|
10123
10123
|
"path": "release-notes.json",
|
|
10124
10124
|
"mode": "0000644",
|
|
10125
|
-
"sha256": "
|
|
10125
|
+
"sha256": "99cc3e08c1384339c868c499061c081eade1bcb0a996c47d3e644066d22ca536"
|
|
10126
10126
|
},
|
|
10127
10127
|
{
|
|
10128
10128
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -11582,12 +11582,12 @@
|
|
|
11582
11582
|
{
|
|
11583
11583
|
"path": "scripts/gds/ship-finish.js",
|
|
11584
11584
|
"mode": "0000644",
|
|
11585
|
-
"sha256": "
|
|
11585
|
+
"sha256": "23856ecf2ed0224fcc69fa4e67413d2248462813896e3b832a20f3a2d0a886e6"
|
|
11586
11586
|
},
|
|
11587
11587
|
{
|
|
11588
11588
|
"path": "scripts/gds/ship-flow.js",
|
|
11589
11589
|
"mode": "0000644",
|
|
11590
|
-
"sha256": "
|
|
11590
|
+
"sha256": "d114ae690d86270db76770fc3531a5530d1ab64a031a2528368864f4b330ac56"
|
|
11591
11591
|
},
|
|
11592
11592
|
{
|
|
11593
11593
|
"path": "scripts/gds/ship-git.js",
|
|
@@ -11597,7 +11597,7 @@
|
|
|
11597
11597
|
{
|
|
11598
11598
|
"path": "scripts/gds/ship-grade-steps.js",
|
|
11599
11599
|
"mode": "0000644",
|
|
11600
|
-
"sha256": "
|
|
11600
|
+
"sha256": "4a0187643d8e0d74b0199f913c24fbd14acf035c27d6040d4ecdfcf905732398"
|
|
11601
11601
|
},
|
|
11602
11602
|
{
|
|
11603
11603
|
"path": "scripts/gds/ship-grade.js",
|
|
@@ -11617,7 +11617,7 @@
|
|
|
11617
11617
|
{
|
|
11618
11618
|
"path": "scripts/gds/ship-land.js",
|
|
11619
11619
|
"mode": "0000644",
|
|
11620
|
-
"sha256": "
|
|
11620
|
+
"sha256": "e518039d82343b5ff3bb21e1a6e0a5a8e0b9368d05b791be4910d76ea075378e"
|
|
11621
11621
|
},
|
|
11622
11622
|
{
|
|
11623
11623
|
"path": "scripts/gds/ship-memory-sync.js",
|
|
@@ -12137,7 +12137,7 @@
|
|
|
12137
12137
|
{
|
|
12138
12138
|
"path": "src/bongos/module-store.js",
|
|
12139
12139
|
"mode": "0000644",
|
|
12140
|
-
"sha256": "
|
|
12140
|
+
"sha256": "c45fffd2d1320cfadd2177421e8b58643676a71fa1bac7a01e4219c60bfe20b1"
|
|
12141
12141
|
},
|
|
12142
12142
|
{
|
|
12143
12143
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -12292,7 +12292,7 @@
|
|
|
12292
12292
|
{
|
|
12293
12293
|
"path": "src/bongos/routes/modules.js",
|
|
12294
12294
|
"mode": "0000644",
|
|
12295
|
-
"sha256": "
|
|
12295
|
+
"sha256": "aea0a0f28621500bee51e4fec8e794139cf90c3360fdc55caecbbd5a5fc82b69"
|
|
12296
12296
|
},
|
|
12297
12297
|
{
|
|
12298
12298
|
"path": "src/bongos/routes/my-sessions.js",
|
|
@@ -12372,7 +12372,7 @@
|
|
|
12372
12372
|
{
|
|
12373
12373
|
"path": "src/module-api.js",
|
|
12374
12374
|
"mode": "0000644",
|
|
12375
|
-
"sha256": "
|
|
12375
|
+
"sha256": "2fd52ee0afe6fc9ade992a558ebb0fc3ad5904f876f7c5093dd60ea5d201b448"
|
|
12376
12376
|
},
|
|
12377
12377
|
{
|
|
12378
12378
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14874,6 +14874,11 @@
|
|
|
14874
14874
|
"mode": "0000644",
|
|
14875
14875
|
"sha256": "ca7fbfcca238b4ffed576ca1b3fca5d848f4d9d3eab8321353325d54ff505810"
|
|
14876
14876
|
},
|
|
14877
|
+
{
|
|
14878
|
+
"path": "tests/held_tweak_reporting.mjs",
|
|
14879
|
+
"mode": "0000644",
|
|
14880
|
+
"sha256": "f58a95e019d9e5dd22c3fa2321734cbb7c323b356184198e7d3cb4507ee3d04a"
|
|
14881
|
+
},
|
|
14877
14882
|
{
|
|
14878
14883
|
"path": "tests/help_requests.mjs",
|
|
14879
14884
|
"mode": "0000644",
|
|
@@ -15749,6 +15754,11 @@
|
|
|
15749
15754
|
"mode": "0000644",
|
|
15750
15755
|
"sha256": "43828c1a95652180d558e7676cc2053d0599a61c0edf4cd1ab100820fc7de493"
|
|
15751
15756
|
},
|
|
15757
|
+
{
|
|
15758
|
+
"path": "tests/module_store_instance_root.mjs",
|
|
15759
|
+
"mode": "0000644",
|
|
15760
|
+
"sha256": "aa66cdb054eeaef8c3c8babf5238f029d13b668a45a97230efe5f8cfa8908d48"
|
|
15761
|
+
},
|
|
15752
15762
|
{
|
|
15753
15763
|
"path": "tests/module_store_listing.mjs",
|
|
15754
15764
|
"mode": "0000644",
|
|
@@ -16027,7 +16037,7 @@
|
|
|
16027
16037
|
{
|
|
16028
16038
|
"path": "tests/page_tweak_hold.mjs",
|
|
16029
16039
|
"mode": "0000644",
|
|
16030
|
-
"sha256": "
|
|
16040
|
+
"sha256": "c3c3b7d44baacd33d1b42db4609dcb8312d8ba46ac4dab38abc57371a4bde74b"
|
|
16031
16041
|
},
|
|
16032
16042
|
{
|
|
16033
16043
|
"path": "tests/paintbrush.mjs",
|
package/docs/architecture.md
CHANGED
|
@@ -291,7 +291,7 @@ Bongos views: `claimable_tasks` (rebuilt in 006 to use SELECT t.*; rebuilt again
|
|
|
291
291
|
- Own-scoped session self-service ([ADR 0206](adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md), task 1003341), in [`src/bongos/routes/my-sessions.js`](../src/bongos/routes/my-sessions.js): `GET /me/sessions` (every live session on the caller's own account — source, user agent, created/last-used/expires, a `current` flag; the token value is never selected), `POST /me/sessions/revoke` (`{session_id}` — one of your own; a miss answers `revoked: 0` so a stranger's id cannot be probed) and `POST /me/sessions/revoke-all` (sign out everywhere, the requesting session included). All three gate on `requireBuilder` alone and key every statement on `req.builder.id` — an account is never named in the query, body or path. They exist because a bearer refreshes to `now()+24h` on every call, so a leaked one never expires, and the cross-builder pair (`POST /auth/revoke`, `/auth/revoke-all`, both still gated on the admin `auth.session.revoke` atom) needed an Archon the victim might not have. Surfaced in the hall at Settings → Active sessions.
|
|
292
292
|
- Authenticated CLI/web: `GET /me` (returns `rank`), `GET /versions[/progress]`, `GET /tasks[?version=&status=&kind=&discipline=]`, `GET /tasks/claimable[?version=&discipline=]`, `GET /tasks/:id`, `POST /tasks` (accepts `discipline` + `criterion_ids`; **metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)), `PATCH /tasks/:id` (supports `parent_task_id`, `kind`, `discipline`, `goal_id` — (re)assign the task's goal; must be a goal in the task's own version, task 1763; **metic+**), `POST /tasks/:id/promote` (**metic+**), `POST /claims`, `POST /claims/:id/resolve`, `POST /cost`.
|
|
293
293
|
- Criterion rollup + task↔criterion links ([ADR 0025](adr/0025-structured-criterion-task-link.md), [#435](https://example.com/builders#/task/435)/[#438](https://example.com/builders#/task/438)): `GET /versions/:id/progress` rolls up each done-when criterion → its gating tasks (via `task_criteria`) → live status counts + the not-yet-shipped `remaining[]` + `unattributed_tasks` (the read behind the `/status` skill, in `src/bongos/done-when.js criterionProgress`). The link CRUD mirrors the dependency endpoints: `GET /tasks/:id/criteria`, `POST /tasks/:id/criteria` (**metic+** — [ADR 0090](adr/0090-metic-task-authoring.md)) and `DELETE /tasks/:id/criteria/:criterionId` (**metic+**). A criterion ref is a numeric `done_when_criteria.id`, a positional `"Cn"` token, or a `criterion_id` slug, resolved against the task's version (`POST /tasks` links at create time so `/status` counts the task with no backfill).
|
|
294
|
-
- Module store publish (ADR 0338 D1, task 1004271): `POST /store/modules/:key/versions` (`requireBuilder` + `requirePermission('module.submit')`, Metic floor). The body is the raw gzip tarball `bongos module publish` builds (own `express.raw` parser, 5 MB cap; 16 MB / 2000 files unpacked). `scripts/gds/module-artifact.js` re-verifies every hash and the publish gate, then `src/bongos/module-store.js` keeps the file under `var/module-store/` and inserts the `store_module_versions` row in one transaction. The first publish makes the caller the author; later versions are author-only, newer than the last and never overwritten; a delisted key takes none.
|
|
294
|
+
- Module store publish (ADR 0338 D1, task 1004271): `POST /store/modules/:key/versions` (`requireBuilder` + `requirePermission('module.submit')`, Metic floor). The body is the raw gzip tarball `bongos module publish` builds (own `express.raw` parser, 5 MB cap; 16 MB / 2000 files unpacked). `scripts/gds/module-artifact.js` re-verifies every hash and the publish gate, then `src/bongos/module-store.js` keeps the file under the INSTANCE root's `var/module-store/` (`MODULE_STORE_DIR` overrides; never inside the installed core package, which every upgrade replaces, and a boot move carries files from that old path, task 1004399) and inserts the `store_module_versions` row in one transaction. The first publish makes the caller the author; later versions are author-only, newer than the last and never overwritten; a delisted key takes none.
|
|
295
295
|
- Module store install (ADR 0338 D1, task 1003785), all `requireBuilder` and own-scoped (the holder is the caller): `POST /store/modules/:key/acquire` (`{version?}`; grants a free entitlement — no price row, or a zero price, is free — or `402 payment_required` for a priced module; refuses a delisted one), `GET /store/modules/:key/versions/:version/tarball` (served only to a live entitlement holder; `X-Tarball-Sha256`), `POST /store/modules/:key/acquired` (`{version}` → `recordAcquired`). Read side in `src/bongos/module-store.js` (`resolveAcquirable`, `isFreePrice`, `versionArtifactFile`). No channel check yet (tasks 1003815, 1003805). `bongos module update` (task 1003786) reuses the same three calls; the acquire route reads price only for a caller with no entitlement, so a holder is never charged again.
|
|
296
296
|
- Module store delist (ADR 0338 D2, task 1003797): `POST /store/modules/:key/delist` (`requireBuilder` + `requirePermission('module.delist')`, Metic floor, granted by `government_022`; strict body `{reason}`, reason required, recorded by the audit middleware). Sets `store_modules.status='delisted'` and `delisted_at` — one-way, no relist route. Every store read already refuses a delisted key (acquire, tarball, acquired, publish), while `GET /store/modules/latest` still answers with the status so `bongos upgrade` warns the holder. `GET /modules` sets `store_delisted` on an instance-tree module the store delisted (hall pill "delisted · no more updates"), and `bongos module list` marks it DELISTED (fail-soft store check, instance modules only). Never revokes an entitlement, never reaches an installed copy, never writes a score (ADR 0359). 404 unknown key, 409 `already_delisted`.
|
|
297
297
|
- Server-mediated branch publish (ADR 0055 / [#1025](https://example.com/builders#/task/1025) — lets a checkout with no GitHub push credential ship). Owner-gated like ship: `POST /tasks/:id/publish-branch` (`requireBuilder` + `gateTaskOwnership`; own 32 MB json parser for the base64 thin-bundle body, 20 MB decoded cap; the server pushes the branch + opens the PR + auto-merges with a server-side push credential — a **GitHub App** installation token (short-lived, repo-scoped; preferred, [ADR 0055](adr/0055-server-mediated-branch-publish.md) update / task 1028) or the `GITHUB_PUSH_TOKEN` PAT fallback; `503 push_unconfigured` when neither is set; `400 tip_mismatch` if the bundle tip ≠ the claimed `head_sha`) and `GET /tasks/:id/publish-status?branch=…` (polls PR + deploy-prod state derived live from GitHub). `branch` is optional: without it the server finds the task's own PR by its `task <id>: …` title, and the answer names the `branch` it used and its `branch_source` (`query` / `task_pr`, or `null` with a `branch_hint` when there is no such PR) — never a `400` (task 1003764). When the PR is still open it also carries `merge_driver`, whose `reason` says why it has not merged; on a red PR `failing_tests` names the failing unit tests (read from the `unit-report` commit status the `unit` workflow posts) and `checks_unreadable: true` says the check list is blind because the App lacks `Checks: read` — so an empty `checks[]` is never read as "nothing failed" (task 1003988, ADR 0301). Because the server's credential is the PR's AUTHOR, publish also **assigns the PR to the task's claim holder** and names them in the body (`modules/lifecycle/pr-assign.js`, task 1003991) — best-effort: the 201 carries `pr_assignee` (the login, or `null` when GitHub would not assign it) and a failed assignment never fails the publish. `ship.js` uses these in `ci` mode only when `pushVia()` resolves to server (no authenticated `gh` on the machine, or `<PREFIX>_PUSH_VIA_SERVER=1`); a machine with `gh` keeps the local push path unchanged. Implementation in `src/bongos/github-push.js`.
|
|
@@ -3000,5 +3000,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3000
3000
|
landed since 1.21.82 with no explicit bump. run 37869654816. (task 1002620)
|
|
3001
3001
|
1.21.84 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3002
3002
|
landed since 1.21.83 with no explicit bump. run 37876887587. (task 1002620)
|
|
3003
|
+
1.21.85 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3004
|
+
landed since 1.21.84 with no explicit bump. run 37884200093. (task 1002620)
|
|
3005
|
+
1.21.86 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3006
|
+
landed since 1.21.85 with no explicit bump. run 37885516955. (task 1002620)
|
|
3003
3007
|
---------------------------------------------------------------------------
|
|
3004
3008
|
```
|
|
@@ -65,7 +65,19 @@ const HELD_NOT_ARMED = Object.freeze({
|
|
|
65
65
|
reason: 'held for the artist (ADR 0341 D7): a page tweak lands only after its artist approves it',
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
// The closing sentence of the PR body a ship opens (task 1004379). The stock
|
|
69
|
+
// text promised auto-merge, and a held round's PR is deliberately not armed, so
|
|
70
|
+
// the reader of a held PR was told the opposite of what would happen. Both PR
|
|
71
|
+
// writers (the server's publish route and ship.js's ci-land) end with this.
|
|
72
|
+
const ARMED_PR_SENTENCE = 'Auto-merge is enabled — GitHub lands it once the required checks pass.';
|
|
73
|
+
const HELD_PR_SENTENCE = 'Auto-merge is NOT enabled: this page tweak waits for its artist to approve it in the studio (ADR 0341 D7). It lands only after that approval.';
|
|
74
|
+
function prMergeSentence(held) {
|
|
75
|
+
return held ? HELD_PR_SENTENCE : ARMED_PR_SENTENCE;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
module.exports = {
|
|
79
|
+
HELD_PR_SENTENCE,
|
|
80
|
+
prMergeSentence,
|
|
69
81
|
HELD_NOT_ARMED,
|
|
70
82
|
HOLD_STATE,
|
|
71
83
|
isPageTweak,
|
|
@@ -36,7 +36,7 @@ const { gradeQualityWhere } = require('../db-shared');
|
|
|
36
36
|
const { helperJoinSql } = require('../page-tweak-artist'); // task 1004432: a page tweak's applier as a helper
|
|
37
37
|
const auth = api;
|
|
38
38
|
const db = require('../db');
|
|
39
|
-
const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
|
|
39
|
+
const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED, prMergeSentence } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
|
|
40
40
|
const hierarchy = require('../hierarchy');
|
|
41
41
|
const { suggestCriteria } = require('../criterion-suggest');
|
|
42
42
|
// The orphan-task advisory (task 1002539) — pure, fail-open; see goal-advisory.js.
|
|
@@ -1176,12 +1176,13 @@ module.exports = function buildTasksRouter() {
|
|
|
1176
1176
|
const builderLogin = await db.getLastClaimHolder(id)
|
|
1177
1177
|
.then((h) => (h ? h.github_login : req.builder.github_login), () => null);
|
|
1178
1178
|
const builderLine = prAssign.prBuilderLine(builderLogin);
|
|
1179
|
+
const held = await pageTweakReads.heldForArtistFailClosed(id); // ADR 0341 D7: read before the body names it (task 1004379)
|
|
1179
1180
|
const body =
|
|
1180
1181
|
`Auto-opened by GDS server-mediated publish (ADR 0055) for task ${id}.` +
|
|
1181
1182
|
`${builderLine ? ` ${builderLine}` : ''}\n\n` +
|
|
1182
1183
|
`${deHashRefs(valueSummary) || '(no value summary)'}\n\n` +
|
|
1183
1184
|
`Merging triggers .github/workflows/deploy-prod.yml -> ${branding().domains.publicOrigin}. ` +
|
|
1184
|
-
|
|
1185
|
+
prMergeSentence(held);
|
|
1185
1186
|
const pr = await githubPush.openOrFindPullRequest({ branch, title, body });
|
|
1186
1187
|
// Best-effort by contract (pr-assign.js never rejects); the catch is for a future
|
|
1187
1188
|
// edit that forgets — an assignment must not be able to fail a pushed ship.
|
|
@@ -1191,7 +1192,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1191
1192
|
// builder nothing, and the commonest cause is a repo with allow_auto_merge OFF —
|
|
1192
1193
|
// a permanent condition that reads exactly like a transient blip.
|
|
1193
1194
|
// task 1004322 (ADR 0341 D7): a page tweak held for its artist is PUBLISHED, never armed.
|
|
1194
|
-
const autoMergeResult =
|
|
1195
|
+
const autoMergeResult = held ? HELD_NOT_ARMED : await githubPush.enableAutoMergeDetailed({ nodeId: pr.node_id });
|
|
1195
1196
|
const autoMerge = autoMergeResult.ok;
|
|
1196
1197
|
// task 1139 (ADR 0058): attest the author's trust tier on the pushed sha so the
|
|
1197
1198
|
// gate-review check can auto-approve a Metic+ author's gate-surface PR with no
|
|
@@ -78,6 +78,15 @@ const STAGES = Object.freeze({
|
|
|
78
78
|
headline: (id) => `Task #${id} grade failed`,
|
|
79
79
|
at: 4, currentKind: 'failed',
|
|
80
80
|
},
|
|
81
|
+
// task 1004379: a page tweak whose grade PASSED and that now waits for its
|
|
82
|
+
// artist (ADR 0341 D7). It sits at completed, which stageFor() would read as a
|
|
83
|
+
// failed grade, so the caller names this stage explicitly (ship-finish.js
|
|
84
|
+
// finishHeld). The grade is done; the confirm node is the artist's to take.
|
|
85
|
+
held: {
|
|
86
|
+
tone: 'info', icon: 'ti-palette', statusWord: 'Waiting for the artist',
|
|
87
|
+
headline: (id) => `Task #${id} passed — waiting for the artist`,
|
|
88
|
+
at: 4, currentKind: 'held',
|
|
89
|
+
},
|
|
81
90
|
});
|
|
82
91
|
|
|
83
92
|
// Map a post-grade task status to a ship-family card stage. (claimed/review are
|
|
@@ -139,6 +148,8 @@ const GRADE_FAILED = 'grade failed';
|
|
|
139
148
|
// task 1002892: the confirm node's word when the grade did not RUN. Two words, so
|
|
140
149
|
// the " → " joint logic that already keeps 'grade failed' intact covers it too.
|
|
141
150
|
const NOT_GRADED = 'not graded';
|
|
151
|
+
// task 1004379: the confirm node's word while a passed page tweak waits for its artist.
|
|
152
|
+
const AWAITING_ARTIST = 'awaiting artist';
|
|
142
153
|
|
|
143
154
|
// The journey, mapped: one entry per PIPELINE node, conjugated + classified by
|
|
144
155
|
// position relative to the current node (`cfg.at`). Both renderers consume this
|
|
@@ -158,6 +169,7 @@ function trailSteps(cfg) {
|
|
|
158
169
|
// cfg.trailWord is set only by the outage override in buildCardModel; every
|
|
159
170
|
// other caller leaves it undefined and gets GRADE_FAILED exactly as before.
|
|
160
171
|
if (cfg.currentKind === 'failed') return { word: cfg.trailWord || GRADE_FAILED, state: 'current', checked: false, bold: true };
|
|
172
|
+
if (cfg.currentKind === 'held') return { word: AWAITING_ARTIST, state: 'current', checked: false, bold: true };
|
|
161
173
|
if (cfg.currentKind === 'done') return { word: f.done, state: 'current', checked: true, bold: true };
|
|
162
174
|
return { word: f.active, state: 'current', checked: false, bold: true }; // 'active'
|
|
163
175
|
}
|
|
@@ -219,6 +231,7 @@ const DISCIPLINE_SKILL = { ui: 'design' };
|
|
|
219
231
|
function nextActionFor(stage, { taskId, discipline, sandboxUrl } = {}) {
|
|
220
232
|
if (stage === 'shipped') return { kind: 'prompt', label: 'Start next task', cmd: '/builder-start' };
|
|
221
233
|
if (stage === 'confirmed') return { kind: 'prompt', label: 'Land it', cmd: '/merge-mode' };
|
|
234
|
+
if (stage === 'held') return { kind: 'prompt', label: 'Start next task', cmd: '/builder-start' };
|
|
222
235
|
if (stage === 'failed') return { kind: 'prompt', label: 'Fix & re-grade', cmd: `node scripts/gds/ship.js ${taskId} --regrade` };
|
|
223
236
|
if (stage === 'review') return { kind: 'link', label: 'Open in sandbox', url: sandboxUrl || '' };
|
|
224
237
|
if (stage === 'claimed') {
|
|
@@ -391,7 +404,9 @@ function buildCardModel(input = {}) {
|
|
|
391
404
|
}
|
|
392
405
|
// Credits
|
|
393
406
|
let creditsText = null;
|
|
394
|
-
if (stage === '
|
|
407
|
+
if (stage === 'held') {
|
|
408
|
+
creditsText = 'none yet — paid when the artist approves and it lands';
|
|
409
|
+
} else if (stage === 'failed') {
|
|
395
410
|
creditsText = 'none — held at completed';
|
|
396
411
|
} else if (credits.awarded != null) {
|
|
397
412
|
const total = fmtInt(credits.total);
|
|
@@ -448,6 +463,8 @@ function buildCardModel(input = {}) {
|
|
|
448
463
|
// states the automatic reality rather than implying a required command.
|
|
449
464
|
deployText = 'landing automatically';
|
|
450
465
|
}
|
|
466
|
+
} else if (stage === 'held') {
|
|
467
|
+
deployText = 'waiting for the artist to approve it in the studio';
|
|
451
468
|
} else {
|
|
452
469
|
deployText = 'not deployed';
|
|
453
470
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.86",
|
|
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.86",
|
|
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.86",
|
|
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
|
@@ -9330,5 +9330,17 @@
|
|
|
9330
9330
|
"id": "1004629",
|
|
9331
9331
|
"text": "New projects now ask how finished work should ship: Auto (recommended) or Manual. The answer becomes the project's ship mode, which the owner can change later on the settings page."
|
|
9332
9332
|
}
|
|
9333
|
+
],
|
|
9334
|
+
"1.21.85": [
|
|
9335
|
+
{
|
|
9336
|
+
"id": "1004379",
|
|
9337
|
+
"text": "A page tweak that passed its grade and is waiting for its artist no longer reports itself as a failed grade: the closing card, the PR description and the credits line now all say it passed and is waiting for the artist's appro"
|
|
9338
|
+
}
|
|
9339
|
+
],
|
|
9340
|
+
"1.21.86": [
|
|
9341
|
+
{
|
|
9342
|
+
"id": "1004399",
|
|
9343
|
+
"text": "Published module packages are now kept in the project's own data folder instead of inside the installed core, so a core upgrade no longer deletes them; any already stored in the old place are moved over when the server starts."
|
|
9344
|
+
}
|
|
9333
9345
|
]
|
|
9334
9346
|
}
|
|
@@ -244,7 +244,8 @@ async function finishHeld({ taskId, summary, postGradeStatus, card }, deps = {})
|
|
|
244
244
|
console.error(` Re-run to publish it again (the grade is not re-run): node scripts/gds/ship.js ${taskId}`);
|
|
245
245
|
}
|
|
246
246
|
const usage = await (deps.postShipSync || postShipSync)(taskId, 'confirmed');
|
|
247
|
-
|
|
247
|
+
// task 1004379: name the stage — at 'completed' the card would otherwise read a failed grade.
|
|
248
|
+
await (deps.printCompletionCard || printCompletionCard)({ taskId, postGradeStatus, card, usage, land, stage: 'held' });
|
|
248
249
|
return land;
|
|
249
250
|
}
|
|
250
251
|
|
|
@@ -270,7 +271,7 @@ function diffstatFor(baseline) {
|
|
|
270
271
|
// in-feed HTML to a temp file and prints an [otb-card-html] marker the
|
|
271
272
|
// builder-ship skill picks up to render it in the chat feed. Every step is
|
|
272
273
|
// best-effort — a summary card must NEVER fail or undo a completed ship.
|
|
273
|
-
async function printCompletionCard({ taskId, postGradeStatus, card, usage, land = null }) {
|
|
274
|
+
async function printCompletionCard({ taskId, postGradeStatus, card, usage, land = null, stage = undefined }) {
|
|
274
275
|
if (!card) return;
|
|
275
276
|
try {
|
|
276
277
|
const shipCard = require('../../modules/lifecycle/ship-card');
|
|
@@ -329,6 +330,7 @@ async function printCompletionCard({ taskId, postGradeStatus, card, usage, land
|
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
const cardInputs = {
|
|
333
|
+
stage, // task 1004379: 'held' from finishHeld; undefined ⇒ derived from postGradeStatus
|
|
332
334
|
postGradeStatus,
|
|
333
335
|
taskId,
|
|
334
336
|
title: card.title,
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -722,7 +722,7 @@ async function shipMain() {
|
|
|
722
722
|
|
|
723
723
|
printCreditsAndAchievements({
|
|
724
724
|
postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo,
|
|
725
|
-
justUnlocked: postGradeJustUnlocked,
|
|
725
|
+
justUnlocked: postGradeJustUnlocked, held,
|
|
726
726
|
});
|
|
727
727
|
|
|
728
728
|
// ---- Step 3: confirmed→shipped tail (merge + deploy → memory/session sync →
|
|
@@ -1107,6 +1107,7 @@ async function regradeMain() {
|
|
|
1107
1107
|
const g = gradePost.data.grade;
|
|
1108
1108
|
recordGradeOutcome({ taskId, grade: g });
|
|
1109
1109
|
const postGradeStatus = gradePost.data.taskStatus;
|
|
1110
|
+
const held = gradePost.data.held === true; // task 1004322: a page tweak's pass waits for its artist
|
|
1110
1111
|
printGradeOutcome(g, taskId);
|
|
1111
1112
|
printCreditsAndAchievements({
|
|
1112
1113
|
postGradeStatus,
|
|
@@ -1115,13 +1116,14 @@ async function regradeMain() {
|
|
|
1115
1116
|
creditsMultiplier: gradePost.data.creditsMultiplier ?? null,
|
|
1116
1117
|
ideaBonusInfo: gradePost.data.ideaBonus ?? null,
|
|
1117
1118
|
justUnlocked: gradePost.data.just_unlocked || [],
|
|
1119
|
+
held, // task 1004379: a held pass is not 'grade not passed'
|
|
1118
1120
|
});
|
|
1119
1121
|
|
|
1120
1122
|
// On a pass the task is now confirmed — run the SAME confirmed→shipped tail as a
|
|
1121
1123
|
// normal ship (finishShip, #1093/C9) so a re-grade lands identically.
|
|
1122
1124
|
await finishShip({
|
|
1123
1125
|
taskId, summary, advanceToMerge: gradePost.data.advanceToMerge, skipMerge, dbOnly,
|
|
1124
|
-
held
|
|
1126
|
+
held,
|
|
1125
1127
|
postGradeStatus,
|
|
1126
1128
|
doneLine: g.passed ? 'Done — re-grade passed.' : 'Done — re-grade still failing; fix and try again.',
|
|
1127
1129
|
// task 1270: completion card for the re-grade path too.
|
|
@@ -478,9 +478,14 @@ function printGradeOutcome(g, taskId) {
|
|
|
478
478
|
|
|
479
479
|
// Print the credits line + multiplier breakdown + idea bonus + unlocked
|
|
480
480
|
// achievements from a grade/confirm response.
|
|
481
|
-
function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo, justUnlocked }) {
|
|
481
|
+
function printCreditsAndAchievements({ postGradeStatus, credits, creditsRaw, creditsMultiplier, ideaBonusInfo, justUnlocked, held = false }) {
|
|
482
482
|
console.log('');
|
|
483
|
-
if (
|
|
483
|
+
if (held) {
|
|
484
|
+
// task 1004379: a page tweak's PASS waits at completed for its artist (ADR
|
|
485
|
+
// 0341 D7). Reading 'completed' as 'grade not passed' told the reader the
|
|
486
|
+
// opposite of what happened.
|
|
487
|
+
console.log(`Credits awarded: ${credits} (none yet — the grade passed; credits land when the artist approves and it merges)`);
|
|
488
|
+
} else if (postGradeStatus === 'completed') {
|
|
484
489
|
console.log(`Credits awarded: ${credits} (none — task at completed, grade not passed)`);
|
|
485
490
|
} else if (postGradeStatus === 'confirmed') {
|
|
486
491
|
console.log(`Credits awarded: ${credits} (none yet — credits land when the task merges to main)`);
|
package/scripts/gds/ship-land.js
CHANGED
|
@@ -19,6 +19,7 @@ const { plainCardLines } = require('./plain-cards.js');
|
|
|
19
19
|
// The server's own reader of a PR's check rollup (task 1003988), reused so the laptop
|
|
20
20
|
// poll names a red PR exactly the way publish-status does (task 1003764). Dependency-free.
|
|
21
21
|
const { readCheckRollup, UNIT_REPORT_CONTEXT } = require('../../modules/lifecycle/check-rollup.js');
|
|
22
|
+
const { prMergeSentence } = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
22
23
|
|
|
23
24
|
// ---------- the land outcome (task 1002541, part 1) ----------
|
|
24
25
|
//
|
|
@@ -927,8 +928,7 @@ async function ciLandLocal(taskId, branch, valueSummary, { hold = false } = {})
|
|
|
927
928
|
`Auto-opened by ship.js (deploy mode: ci) for task ${taskId}.\n\n` +
|
|
928
929
|
`${sanitizeGitHubRefs(valueSummary) || '(no value summary)'}\n\n` +
|
|
929
930
|
`Merging this PR triggers .github/workflows/deploy-prod.yml to deploy to ` +
|
|
930
|
-
`${deployTargetNote}.
|
|
931
|
-
`required checks pass.`;
|
|
931
|
+
`${deployTargetNote}. ${prMergeSentence(hold)}`; // task 1004379: a held round's PR says it waits
|
|
932
932
|
if (!existing || existing.state === 'CLOSED' || existing.state === 'MERGED') {
|
|
933
933
|
console.log(' ci-land: opening PR -> main...');
|
|
934
934
|
if (!ghArgvOk(buildPrCreateArgv(branch, title, body))) {
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
// 1. stageArtifact — write the tarball under the store dir (a temp name first;
|
|
9
9
|
// commitArtifact moves it into place inside step 2's transaction). ADR 0338 lets the
|
|
10
10
|
// store keep tarballs on the control plane's disk while volume is small, so it
|
|
11
|
-
// does: <MODULE_STORE_DIR or var/module-store>/<key>/<key>-<version>.tgz
|
|
12
|
-
//
|
|
13
|
-
// wipes it.
|
|
11
|
+
// does: <MODULE_STORE_DIR or <instance root>/var/module-store>/<key>/<key>-<version>.tgz
|
|
12
|
+
// (see storeDirFor for why the INSTANCE root, task 1004399).
|
|
14
13
|
// 2. publishVersion — one transaction over the registry tables (core_257): create
|
|
15
14
|
// the store_modules row on a key's first publish (the publisher becomes its
|
|
16
15
|
// author), refuse anyone else's key, refuse a delisted key (ADR 0338 D2: delisting
|
|
@@ -26,9 +25,105 @@ const crypto = require('node:crypto');
|
|
|
26
25
|
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
27
26
|
const { KEY_RE } = require('../module-loader/manifest-schema');
|
|
28
27
|
|
|
28
|
+
// Where the store lives: <INSTANCE ROOT>/var/module-store (task 1004399).
|
|
29
|
+
//
|
|
30
|
+
// THE BUG THIS FIXES. This used to be path.join(__dirname, '..', '..', 'var',
|
|
31
|
+
// 'module-store') — the CORE root. Right in a single checkout, where core root and
|
|
32
|
+
// instance root are one directory; wrong in the consumer layout, where __dirname is
|
|
33
|
+
// node_modules/@bongos/core/src/bongos, so every published tarball was written INSIDE
|
|
34
|
+
// the installed package and every core upgrade (npm ci replaces it) erased them all,
|
|
35
|
+
// leaving registry rows pointing at nothing. Task 1004381 found and fixed the same
|
|
36
|
+
// failure for task visuals (modules/lifecycle/task-visuals.js); this follows it.
|
|
37
|
+
//
|
|
38
|
+
// MODULE_STORE_DIR stays the override (tests, or an operator who wants the tarballs on
|
|
39
|
+
// another disk). The default store carries a `*` .gitignore, because the instance root
|
|
40
|
+
// is a git checkout whose tree `bongos upgrade` requires to be clean.
|
|
41
|
+
let instanceRootCache;
|
|
42
|
+
function instanceRoot() {
|
|
43
|
+
if (instanceRootCache === undefined) {
|
|
44
|
+
try {
|
|
45
|
+
instanceRootCache = require('../instance-config').resolveInstanceRoot();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// A storage-path lookup must never take the server down: fall back to the
|
|
48
|
+
// historical location, which is right in a single checkout.
|
|
49
|
+
instanceRootCache = path.join(__dirname, '..', '..');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return instanceRootCache;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// PURE given its inputs, so the consumer-layout test can prove where it lands.
|
|
56
|
+
function storeDirFor({ env = process.env, root } = {}) {
|
|
57
|
+
if (env.MODULE_STORE_DIR) return env.MODULE_STORE_DIR;
|
|
58
|
+
return path.join(root || instanceRoot(), 'var', 'module-store');
|
|
59
|
+
}
|
|
29
60
|
|
|
30
61
|
function storeDir() {
|
|
31
|
-
return
|
|
62
|
+
return storeDirFor();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The old, in-package location. Read only by migrateLegacyStore.
|
|
66
|
+
function legacyStoreDir() {
|
|
67
|
+
return path.join(__dirname, '..', '..', 'var', 'module-store');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// mkdir -p the store and, for the DEFAULT store, drop a `*` .gitignore in it so the
|
|
71
|
+
// tarballs never show as untracked in the instance's git tree (an untracked path
|
|
72
|
+
// there halts the next `bongos upgrade` pre-flight). An override is the operator's
|
|
73
|
+
// own directory and is left bare. Idempotent; the .gitignore write is best-effort.
|
|
74
|
+
const DIR_GITIGNORE = '# Module store tarballs (task 1004399): runtime data, never committed.\n*\n';
|
|
75
|
+
async function ensureStoreDir(dir, { env = process.env } = {}) {
|
|
76
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
77
|
+
if (env.MODULE_STORE_DIR || path.resolve(dir) !== path.resolve(storeDirFor({ env }))) return;
|
|
78
|
+
try {
|
|
79
|
+
await fs.promises.writeFile(path.join(dir, '.gitignore'), DIR_GITIGNORE, { flag: 'wx' });
|
|
80
|
+
} catch { /* EEXIST is the steady state; anything else is non-fatal */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// One-time boot move: every tarball still at the old in-package path moves into the
|
|
84
|
+
// instance store, keeping its <key>/<key>-<version>.tgz place — the registry stores
|
|
85
|
+
// artifact_path RELATIVE to the store dir, so the rows stay right with no data
|
|
86
|
+
// migration. Only well-formed artifacts move (a key directory matching KEY_RE holding
|
|
87
|
+
// "<key>-<version>.tgz"); a leftover .partial is not one. A no-op when both paths are
|
|
88
|
+
// the same directory (a single checkout), the old one is absent, or MODULE_STORE_DIR
|
|
89
|
+
// is set (the old code honoured it too, so nothing was ever written to the legacy
|
|
90
|
+
// path). Never overwrites, never throws: returns { moved, skipped, failed }.
|
|
91
|
+
async function migrateLegacyStore({ from, to = storeDir(), env = process.env } = {}) {
|
|
92
|
+
const out = { moved: 0, skipped: 0, failed: 0 };
|
|
93
|
+
if (from === undefined) {
|
|
94
|
+
if (env.MODULE_STORE_DIR) return out;
|
|
95
|
+
from = legacyStoreDir();
|
|
96
|
+
}
|
|
97
|
+
if (path.resolve(from) === path.resolve(to)) return out;
|
|
98
|
+
let keys;
|
|
99
|
+
try { keys = await fs.promises.readdir(from); } catch { return out; }
|
|
100
|
+
for (const key of keys.filter((k) => KEY_RE.test(k))) {
|
|
101
|
+
let names;
|
|
102
|
+
try { names = await fs.promises.readdir(path.join(from, key)); } catch { continue; }
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const m = name.startsWith(`${key}-`) && name.endsWith('.tgz') ? name.slice(key.length + 1, -4) : null;
|
|
105
|
+
if (!m || !parseVersion(m)) continue;
|
|
106
|
+
const src = path.join(from, key, name);
|
|
107
|
+
const dest = artifactPath(key, m, { dir: to });
|
|
108
|
+
try {
|
|
109
|
+
try { await fs.promises.access(dest); out.skipped += 1; continue; } catch { /* absent: move it */ }
|
|
110
|
+
await ensureStoreDir(to, { env });
|
|
111
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
112
|
+
try {
|
|
113
|
+
await fs.promises.rename(src, dest);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!err || err.code !== 'EXDEV') throw err;
|
|
116
|
+
// Another filesystem: copy, then remove the original.
|
|
117
|
+
await fs.promises.copyFile(src, dest, fs.constants.COPYFILE_EXCL);
|
|
118
|
+
await fs.promises.unlink(src);
|
|
119
|
+
}
|
|
120
|
+
out.moved += 1;
|
|
121
|
+
} catch {
|
|
122
|
+
out.failed += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
32
127
|
}
|
|
33
128
|
|
|
34
129
|
// Where one version's tarball lives. Key and version are re-validated here so this
|
|
@@ -48,6 +143,7 @@ function artifactPath(key, version, { dir = storeDir() } = {}) {
|
|
|
48
143
|
// Async throughout so a multi-MB write never holds the web process's event loop.
|
|
49
144
|
async function stageArtifact(buf, { key, version, dir = storeDir() } = {}) {
|
|
50
145
|
const final = artifactPath(key, version, { dir });
|
|
146
|
+
await ensureStoreDir(dir);
|
|
51
147
|
await fs.promises.mkdir(path.dirname(final), { recursive: true });
|
|
52
148
|
const tmp = `${final}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.partial`;
|
|
53
149
|
await fs.promises.writeFile(tmp, buf, { flag: 'wx', mode: 0o640 });
|
|
@@ -276,5 +372,5 @@ function versionArtifactFile(row, { dir = storeDir() } = {}) {
|
|
|
276
372
|
module.exports = {
|
|
277
373
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
278
374
|
isFreePrice, resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
279
|
-
delistModule, delistedKeySet,
|
|
375
|
+
delistModule, delistedKeySet, storeDir, storeDirFor, migrateLegacyStore,
|
|
280
376
|
};
|
|
@@ -95,7 +95,7 @@ const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } =
|
|
|
95
95
|
const {
|
|
96
96
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
97
97
|
resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
98
|
-
delistModule, delistedKeySet,
|
|
98
|
+
delistModule, delistedKeySet, migrateLegacyStore,
|
|
99
99
|
} = require('../module-store');
|
|
100
100
|
const {
|
|
101
101
|
isOptedOut, setReporting, installReportError, errorCountError, recordInstallReport, recordErrorCount,
|
|
@@ -210,6 +210,12 @@ function contributeModuleDesk() {
|
|
|
210
210
|
module.exports = function buildModulesRouter() {
|
|
211
211
|
contributeModuleRechecksToDocket();
|
|
212
212
|
contributeModuleDesk();
|
|
213
|
+
// Task 1004399: carry tarballs written inside the installed core package (the old
|
|
214
|
+
// store path) into the instance store before the next upgrade erases them. Async and
|
|
215
|
+
// never fatal; a no-op in a single checkout, where the two paths are one directory.
|
|
216
|
+
migrateLegacyStore()
|
|
217
|
+
.then((r) => { if (r.moved || r.failed) log.info(r, 'module store: moved from the old in-package store'); })
|
|
218
|
+
.catch((err) => log.error({ err: err && err.message }, 'module store: boot move failed'));
|
|
213
219
|
const router = express.Router();
|
|
214
220
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
215
221
|
// router starts empty.
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.86'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tests/held_tweak_reporting.mjs — a held page tweak is reported as a PASS that
|
|
2
|
+
// waits for its artist, never as a failed grade (task 1004379, ADR 0341 D7).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The first live Tweak Mode rounds (1004345, 1004348) passed their grade and
|
|
5
|
+
// held at completed for the artist, as designed — and then three surfaces told the
|
|
6
|
+
// reader the opposite: the closing card said "grade failed" and offered --regrade,
|
|
7
|
+
// the PR the server opened said "Auto-merge is enabled", and a passing --regrade
|
|
8
|
+
// printed "Credits awarded: 0 (grade not passed)". Each case below drives the real
|
|
9
|
+
// code a held round runs through:
|
|
10
|
+
// 1. finishHeld hands the card a 'held' stage, and that card reads as a pass;
|
|
11
|
+
// 2. the publish route's PR body names the hold (and an ordinary PR is unchanged);
|
|
12
|
+
// 3. the credits line a passing (re-)grade prints on a held round says it passed.
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import { makeRunner } from './helpers.mjs';
|
|
18
|
+
|
|
19
|
+
process.env.NODE_ENV = 'test';
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { test, summary } = makeRunner();
|
|
22
|
+
|
|
23
|
+
const shipCard = require('../modules/lifecycle/ship-card.js');
|
|
24
|
+
const shipFinish = require('../scripts/gds/ship-finish.js');
|
|
25
|
+
const shipLand = require('../scripts/gds/ship-land.js');
|
|
26
|
+
const { printCreditsAndAchievements } = require('../scripts/gds/ship-grade-steps.js');
|
|
27
|
+
|
|
28
|
+
const capture = async (fn) => {
|
|
29
|
+
const out = [];
|
|
30
|
+
const orig = console.log;
|
|
31
|
+
console.log = (...a) => out.push(a.join(' '));
|
|
32
|
+
try { await fn(); } finally { console.log = orig; }
|
|
33
|
+
return out.join('\n');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---- 1. the closing card --------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const twelveMinor = Array.from({ length: 12 }, (_, i) => ({ severity: 'minor', file: `f${i}`, summary: 'nit' }));
|
|
39
|
+
|
|
40
|
+
await test('finishHeld names the held stage to the card, even though the task sits at completed', async () => {
|
|
41
|
+
let cardArgs = null;
|
|
42
|
+
await capture(() => shipFinish.finishHeld({ taskId: 5001, summary: 's', postGradeStatus: 'completed', card: { title: 't' } }, {
|
|
43
|
+
publishHeld: async () => shipLand.landHeld({ prUrl: 'https://github.com/o/r/pull/77' }),
|
|
44
|
+
postShipSync: async () => null,
|
|
45
|
+
printCompletionCard: async (a) => { cardArgs = a; },
|
|
46
|
+
}));
|
|
47
|
+
assert.equal(cardArgs.stage, 'held');
|
|
48
|
+
assert.equal(cardArgs.postGradeStatus, 'completed');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await test('a held pass prints a hold card: no "grade failed", no --regrade, a passing grade row', () => {
|
|
52
|
+
const model = shipCard.buildCardModel({
|
|
53
|
+
stage: 'held', postGradeStatus: 'completed', taskId: 5001, title: 't',
|
|
54
|
+
grade: { passed: true, issues: twelveMinor, panel: 4 },
|
|
55
|
+
credits: { awarded: 0 },
|
|
56
|
+
});
|
|
57
|
+
const text = shipCard.renderMonospaceCard(model);
|
|
58
|
+
const html = shipCard.renderCardHtml(model);
|
|
59
|
+
assert.equal(model.stage, 'held');
|
|
60
|
+
assert.match(model.cfg.headline(5001), /waiting for the artist/);
|
|
61
|
+
for (const out of [text, html]) {
|
|
62
|
+
assert.doesNotMatch(out, /grade failed/i, 'the card must not call a pass a failure');
|
|
63
|
+
assert.doesNotMatch(out, /Needs fixes/);
|
|
64
|
+
assert.doesNotMatch(out, /--regrade/);
|
|
65
|
+
assert.doesNotMatch(out, /fail · 12 issues/);
|
|
66
|
+
assert.match(out, /awaiting artist/);
|
|
67
|
+
}
|
|
68
|
+
const row = (k) => (model.rows.find((r) => r.k === k) || {}).v;
|
|
69
|
+
assert.match(row('Grade'), /^pass/);
|
|
70
|
+
assert.match(row('Deploy'), /artist/);
|
|
71
|
+
assert.match(row('Credits'), /artist approves/);
|
|
72
|
+
assert.equal(model.action.cmd, '/builder-start');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await test('an ordinary stop at completed (a real failed grade) still reads as a failed grade', () => {
|
|
76
|
+
const model = shipCard.buildCardModel({
|
|
77
|
+
postGradeStatus: 'completed', taskId: 5002, title: 't',
|
|
78
|
+
grade: { passed: false, issues: twelveMinor },
|
|
79
|
+
});
|
|
80
|
+
assert.equal(model.stage, 'failed');
|
|
81
|
+
assert.match(shipCard.renderMonospaceCard(model), /grade failed/);
|
|
82
|
+
assert.match(model.action.cmd, /--regrade/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- 2. the PR body -------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const express = require('express');
|
|
88
|
+
const api = require('../src/module-api.js');
|
|
89
|
+
const db = require('../modules/lifecycle/db.js');
|
|
90
|
+
const gp = require('../modules/lifecycle/github-push.js');
|
|
91
|
+
const prAssign = require('../modules/lifecycle/pr-assign.js');
|
|
92
|
+
const pageTweakReads = require('../modules/lifecycle/page-tweak-reads.js');
|
|
93
|
+
const hold = require('../modules/lifecycle/page-tweak-hold.js');
|
|
94
|
+
|
|
95
|
+
let heldNow = false;
|
|
96
|
+
let prBody = null;
|
|
97
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'archon', github_login: 'the-archon' }; next(); };
|
|
98
|
+
db.getLastClaimHolder = async () => ({ builder_id: 5, github_login: 'some-builder' });
|
|
99
|
+
db.setClaimPublishedSha = async () => {};
|
|
100
|
+
gp.isConfigured = () => true;
|
|
101
|
+
gp.pushBundle = async () => ({ head_sha: 'a'.repeat(40) });
|
|
102
|
+
gp.openOrFindPullRequest = async ({ body }) => { prBody = body; return { number: 77, url: 'https://github.com/o/r/pull/77', node_id: 'N' }; };
|
|
103
|
+
gp.enableAutoMergeDetailed = async () => ({ ok: true });
|
|
104
|
+
gp.setCommitStatus = async () => true;
|
|
105
|
+
prAssign.assignPullRequest = async () => ({ assigned: true });
|
|
106
|
+
pageTweakReads.heldForArtistFailClosed = async () => heldNow;
|
|
107
|
+
|
|
108
|
+
const app = express();
|
|
109
|
+
app.use((req, res, next) => { res.fail = (code, status, extra) => res.status(typeof status === 'number' ? status : (status && status.status) || 500).json({ error: code, ...(extra || {}) }); next(); });
|
|
110
|
+
app.use(require('../modules/lifecycle/routes/tasks.js')());
|
|
111
|
+
const server = http.createServer(app);
|
|
112
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
113
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
114
|
+
const publish = async () => {
|
|
115
|
+
prBody = null;
|
|
116
|
+
const res = await fetch(`${base}/tasks/5001/publish-branch`, {
|
|
117
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ branch: 'task-5001', head_sha: 'a'.repeat(40), bundle_b64: Buffer.from('b').toString('base64'), value_summary: 'Page tweak.' }),
|
|
119
|
+
});
|
|
120
|
+
return { status: res.status, body: await res.json() };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await test("a held round's PR body says it waits for the artist, and never promises auto-merge", async () => {
|
|
124
|
+
heldNow = true;
|
|
125
|
+
const r = await publish();
|
|
126
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
127
|
+
assert.equal(r.body.auto_merge, false);
|
|
128
|
+
assert.ok(prBody.endsWith(hold.HELD_PR_SENTENCE), prBody);
|
|
129
|
+
assert.match(prBody, /artist/);
|
|
130
|
+
assert.doesNotMatch(prBody, /Auto-merge is enabled/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('every other PR body keeps the auto-merge sentence it always had', async () => {
|
|
134
|
+
heldNow = false;
|
|
135
|
+
const r = await publish();
|
|
136
|
+
assert.equal(r.status, 201);
|
|
137
|
+
assert.equal(r.body.auto_merge, true);
|
|
138
|
+
assert.match(prBody, /Auto-merge is enabled — GitHub lands it once the required checks pass\.$/);
|
|
139
|
+
});
|
|
140
|
+
server.close();
|
|
141
|
+
|
|
142
|
+
// ---- 3. the credits line after a passing (re-)grade -----------------------
|
|
143
|
+
|
|
144
|
+
await test('a passing grade on a held round prints a pass, not "grade not passed"', async () => {
|
|
145
|
+
const text = await capture(() => printCreditsAndAchievements({ postGradeStatus: 'completed', credits: 0, justUnlocked: [], held: true }));
|
|
146
|
+
assert.doesNotMatch(text, /grade not passed/);
|
|
147
|
+
assert.match(text, /grade passed/);
|
|
148
|
+
assert.match(text, /artist approves/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await test('a real failed grade at completed still says the grade did not pass', async () => {
|
|
152
|
+
const text = await capture(() => printCreditsAndAchievements({ postGradeStatus: 'completed', credits: 0, justUnlocked: [] }));
|
|
153
|
+
assert.match(text, /grade not passed/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await test('both grade paths (ship and --regrade) hand the hold to the credits line', () => {
|
|
157
|
+
const fs = require('node:fs');
|
|
158
|
+
const src = fs.readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
|
|
159
|
+
const calls = src.split('printCreditsAndAchievements({').slice(1).map((c) => c.slice(0, c.indexOf('});')));
|
|
160
|
+
const withHold = calls.filter((c) => /\bheld\b/.test(c));
|
|
161
|
+
assert.equal(withHold.length, 2, 'the ship grade and the re-grade both pass held');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
summary();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// tests/module_store_instance_root.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004399: published module tarballs must be stored under the INSTANCE root,
|
|
4
|
+
// never inside the installed core package, so they survive a core upgrade. The
|
|
5
|
+
// sibling of tests/task_visuals_instance_root.mjs (task 1004381), which found the
|
|
6
|
+
// same bug in the task-visuals store.
|
|
7
|
+
//
|
|
8
|
+
// This builds a real consumer tree in a temp dir: a fake installed core package
|
|
9
|
+
// carrying the REAL src/bongos/module-store.js, with its three core requires
|
|
10
|
+
// re-exporting the real files, then proves, through that installed copy:
|
|
11
|
+
// 1. the store is the instance's var/module-store, not inside the package;
|
|
12
|
+
// 2. a published tarball survives the package being deleted and reinstalled,
|
|
13
|
+
// and the registry row's artifact_path still resolves to it;
|
|
14
|
+
// 3. the boot move carries tarballs from the old in-package path, keeping their
|
|
15
|
+
// <key>/ place, once, without overwriting, skipping anything not an artifact,
|
|
16
|
+
// and is a no-op in a single checkout or under MODULE_STORE_DIR;
|
|
17
|
+
// 4. the store keeps the instance's git tree clean (upgrade pre-flight).
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/module_store_instance_root.mjs
|
|
20
|
+
|
|
21
|
+
import { strict as assert } from 'node:assert';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const REAL = (...p) => path.join(REPO, 'src', ...p);
|
|
32
|
+
|
|
33
|
+
let passed = 0, failed = 0;
|
|
34
|
+
async function t(name, fn) {
|
|
35
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
36
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.stack ? e.stack.split('\n').slice(0, 3).join('\n ') : e}`); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TGZ = Buffer.from('1f8b0800000000000003', 'hex');
|
|
40
|
+
|
|
41
|
+
const WORK = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-instance-root-'));
|
|
42
|
+
const CONSUMER = path.join(WORK, 'consumer');
|
|
43
|
+
const CORE = path.join(CONSUMER, 'node_modules', '@bongos', 'core');
|
|
44
|
+
const reexport = (real) => `module.exports = require(${JSON.stringify(real)});\n`;
|
|
45
|
+
|
|
46
|
+
// "npm ci": (re)install the fake core package — the real store, its requires stubbed through.
|
|
47
|
+
function installCore() {
|
|
48
|
+
fs.rmSync(CORE, { recursive: true, force: true });
|
|
49
|
+
fs.mkdirSync(path.join(CORE, 'src', 'bongos'), { recursive: true });
|
|
50
|
+
fs.mkdirSync(path.join(CORE, 'src', 'module-loader'), { recursive: true });
|
|
51
|
+
fs.copyFileSync(REAL('bongos', 'module-store.js'), path.join(CORE, 'src', 'bongos', 'module-store.js'));
|
|
52
|
+
fs.writeFileSync(path.join(CORE, 'src', 'module-loader', 'semver.js'), reexport(REAL('module-loader', 'semver.js')));
|
|
53
|
+
fs.writeFileSync(path.join(CORE, 'src', 'module-loader', 'manifest-schema.js'), reexport(REAL('module-loader', 'manifest-schema.js')));
|
|
54
|
+
fs.writeFileSync(path.join(CORE, 'src', 'instance-config.js'), reexport(REAL('instance-config.js')));
|
|
55
|
+
}
|
|
56
|
+
// A fresh require of the INSTALLED copy (a restarted server).
|
|
57
|
+
function loadInstalled() {
|
|
58
|
+
const p = path.join(CORE, 'src', 'bongos', 'module-store.js');
|
|
59
|
+
delete require.cache[p];
|
|
60
|
+
return require(p);
|
|
61
|
+
}
|
|
62
|
+
const inside = (child, parent) => {
|
|
63
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
64
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
fs.mkdirSync(CONSUMER, { recursive: true });
|
|
68
|
+
installCore();
|
|
69
|
+
delete process.env.MODULE_STORE_DIR;
|
|
70
|
+
process.env.BONGOS_INSTANCE_ROOT = CONSUMER;
|
|
71
|
+
|
|
72
|
+
await t('consumer layout: the store is <instance>/var/module-store, NOT inside the core package', () => {
|
|
73
|
+
const ms = loadInstalled();
|
|
74
|
+
const dir = ms.storeDir();
|
|
75
|
+
assert.equal(path.resolve(dir), path.join(CONSUMER, 'var', 'module-store'));
|
|
76
|
+
assert.ok(!inside(dir, CORE), `store ${dir} is inside the core package ${CORE}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await t('MODULE_STORE_DIR still overrides', () => {
|
|
80
|
+
const ms = loadInstalled();
|
|
81
|
+
assert.equal(ms.storeDirFor({ env: { MODULE_STORE_DIR: '/elsewhere/x' } }), '/elsewhere/x');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await t('the checkout copy resolves the same way: an instance root, or the override', () => {
|
|
85
|
+
const { storeDir, storeDirFor } = require('../src/bongos/module-store.js');
|
|
86
|
+
assert.equal(storeDirFor({ env: {}, root: '/inst' }), path.join('/inst', 'var', 'module-store'));
|
|
87
|
+
assert.equal(path.resolve(storeDir()), path.join(CONSUMER, 'var', 'module-store'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
let row;
|
|
91
|
+
await t('a published tarball survives a core upgrade (package deleted and reinstalled)', async () => {
|
|
92
|
+
const ms = loadInstalled();
|
|
93
|
+
const staged = await ms.stageArtifact(TGZ, { key: 'weather', version: '1.2.0' });
|
|
94
|
+
await ms.commitArtifact(staged);
|
|
95
|
+
row = { module_key: 'weather', version: '1.2.0', artifact_path: ms.relativeArtifactPath(staged.final) };
|
|
96
|
+
assert.equal(row.artifact_path, 'weather/weather-1.2.0.tgz');
|
|
97
|
+
installCore(); // the upgrade
|
|
98
|
+
const after = loadInstalled();
|
|
99
|
+
const file = after.versionArtifactFile(row);
|
|
100
|
+
assert.ok(fs.existsSync(file), 'the tarball is gone after the upgrade');
|
|
101
|
+
assert.deepEqual(fs.readFileSync(file), TGZ);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await t('the default store carries a * .gitignore, so the instance git tree stays clean', () => {
|
|
105
|
+
const gi = fs.readFileSync(path.join(CONSUMER, 'var', 'module-store', '.gitignore'), 'utf8');
|
|
106
|
+
assert.match(gi, /^\*$/m);
|
|
107
|
+
const init = spawnSync('git', ['init', '-q'], { cwd: CONSUMER, encoding: 'utf8' });
|
|
108
|
+
if (init.status !== 0) { console.log(' (git unavailable: .gitignore content checked only)'); return; }
|
|
109
|
+
fs.writeFileSync(path.join(CONSUMER, '.gitignore'), 'node_modules/\n');
|
|
110
|
+
const st = spawnSync('git', ['status', '--porcelain', '--untracked-files=all', '--', 'var'], { cwd: CONSUMER, encoding: 'utf8' });
|
|
111
|
+
assert.equal(st.status, 0);
|
|
112
|
+
assert.equal(st.stdout.trim(), '', `var/ shows in git status:\n${st.stdout}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await t('boot move: tarballs at the old in-package path move into the instance store, once', async () => {
|
|
116
|
+
const ms = loadInstalled();
|
|
117
|
+
const legacy = path.join(CORE, 'var', 'module-store');
|
|
118
|
+
fs.mkdirSync(path.join(legacy, 'notes'), { recursive: true });
|
|
119
|
+
fs.mkdirSync(path.join(legacy, 'weather'), { recursive: true });
|
|
120
|
+
fs.mkdirSync(path.join(legacy, 'Bad Key'), { recursive: true });
|
|
121
|
+
fs.writeFileSync(path.join(legacy, 'notes', 'notes-0.1.0.tgz'), TGZ);
|
|
122
|
+
fs.writeFileSync(path.join(legacy, 'notes', 'notes-0.1.0.tgz.123.456.ab.partial'), 'half');
|
|
123
|
+
fs.writeFileSync(path.join(legacy, 'notes', 'other-0.1.0.tgz'), 'not this key');
|
|
124
|
+
fs.writeFileSync(path.join(legacy, 'Bad Key', 'Bad Key-1.0.0.tgz'), 'x');
|
|
125
|
+
// weather 1.2.0 already exists in the new store: kept, never overwritten.
|
|
126
|
+
fs.writeFileSync(path.join(legacy, 'weather', 'weather-1.2.0.tgz'), 'legacy copy');
|
|
127
|
+
|
|
128
|
+
const r = await ms.migrateLegacyStore();
|
|
129
|
+
assert.deepEqual(r, { moved: 1, skipped: 1, failed: 0 });
|
|
130
|
+
const store = ms.storeDir();
|
|
131
|
+
assert.deepEqual(fs.readFileSync(path.join(store, 'notes', 'notes-0.1.0.tgz')), TGZ);
|
|
132
|
+
assert.ok(!fs.existsSync(path.join(legacy, 'notes', 'notes-0.1.0.tgz')), 'moved, not copied');
|
|
133
|
+
assert.deepEqual(fs.readFileSync(path.join(store, 'weather', 'weather-1.2.0.tgz')), TGZ, 'the existing file is not overwritten');
|
|
134
|
+
assert.ok(!fs.existsSync(path.join(store, 'notes', 'notes-0.1.0.tgz.123.456.ab.partial')), 'a .partial is not an artifact');
|
|
135
|
+
assert.ok(!fs.existsSync(path.join(store, 'Bad Key')), 'a directory that is not a module key is left alone');
|
|
136
|
+
|
|
137
|
+
// A moved tarball is served through its unchanged registry row.
|
|
138
|
+
const moved = ms.versionArtifactFile({ module_key: 'notes', version: '0.1.0', artifact_path: 'notes/notes-0.1.0.tgz' });
|
|
139
|
+
assert.ok(fs.existsSync(moved));
|
|
140
|
+
|
|
141
|
+
const again = await ms.migrateLegacyStore();
|
|
142
|
+
assert.equal(again.moved, 0, 'the second boot moves nothing');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await t('boot move is a no-op in a single checkout and under MODULE_STORE_DIR', async () => {
|
|
146
|
+
const ms = loadInstalled();
|
|
147
|
+
const dir = fs.mkdtempSync(path.join(WORK, 'same-'));
|
|
148
|
+
fs.mkdirSync(path.join(dir, 'notes'));
|
|
149
|
+
fs.writeFileSync(path.join(dir, 'notes', 'notes-0.2.0.tgz'), TGZ);
|
|
150
|
+
assert.deepEqual(await ms.migrateLegacyStore({ from: dir, to: dir }), { moved: 0, skipped: 0, failed: 0 });
|
|
151
|
+
assert.deepEqual(await ms.migrateLegacyStore({ env: { MODULE_STORE_DIR: dir } }), { moved: 0, skipped: 0, failed: 0 });
|
|
152
|
+
assert.ok(fs.existsSync(path.join(dir, 'notes', 'notes-0.2.0.tgz')));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await t('the modules router runs the boot move when it is built', () => {
|
|
156
|
+
const src = fs.readFileSync(path.join(REPO, 'src', 'bongos', 'routes', 'modules.js'), 'utf8');
|
|
157
|
+
const body = src.slice(src.indexOf('module.exports = function buildModulesRouter('));
|
|
158
|
+
assert.ok(body.indexOf('migrateLegacyStore()') > 0 && body.indexOf('migrateLegacyStore()') < body.indexOf('express.Router()'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
fs.rmSync(WORK, { recursive: true, force: true });
|
|
162
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
163
|
+
if (failed) process.exitCode = 1;
|
|
@@ -206,8 +206,7 @@ await test('finishShip routes a held pass away from mergeAndDeploy (the source s
|
|
|
206
206
|
const mergeAt = body.indexOf('await mergeAndDeploy(');
|
|
207
207
|
assert.ok(holdAt > 0 && mergeAt > holdAt, 'the hold is decided BEFORE any merge is attempted');
|
|
208
208
|
const flow = fs.readFileSync(require.resolve('../scripts/gds/ship-flow.js'), 'utf8');
|
|
209
|
-
assert.equal((flow.match(
|
|
210
|
-
assert.equal((flow.match(/held: gradePost\.data\.held === true/g) || []).length, 1, 'regradeMain does too');
|
|
209
|
+
assert.equal((flow.match(/\bheld = gradePost\.data\.held === true/g) || []).length, 2, 'shipMain and regradeMain both read the hold off the grade response');
|
|
211
210
|
assert.match(flow, /tweakHold\.isHeldForArtist\(task, tRes\.data\.grade/, 'a re-run of a held task re-publishes instead of re-grading');
|
|
212
211
|
});
|
|
213
212
|
|