Skip to content

Commit 7d11067

Browse files
fix(cloud-connection): stage the ledger adoption copy without mkdtempSync, so main's dispatch-gates lane is green again (#30487)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9MmNtLnaKdSpq9S7Ah47uy)_ domain:server#3 · os-bill · session_01Utp6CZ9g1bEV7DPMneGKAx Fixes #30467 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 本地安装账本首次从工作目录复制到 `OS_HOME` 时用的暂存目录,改用「随机名 + 独占 mkdirSync」创建,不再用 `mkdtempSync`。行为不变:仍是账本目录旁的唯一兄弟目录,复制完重命名到位,失败即删除。 - **为什么改:** PR #30351(#30217)落地后 `main` 的 `Lint · PM dispatch-gates` 一直红:dispatch-gates 自测要求每个 `mkdtempSync` 的基路径都能从源码读出,而这里的基路径是运行时才解析的账本位置。 - **风险与回滚:** 只改一处暂存目录的创建方式,另加一行 `node:crypto` 导入;名字碰撞时非递归 `mkdirSync` 抛 EEXIST,与 `mkdtempSync` 同样不复用已有目录。回滚即撤 PR(main 会再次变红)。 - **先看:** `packages/cloud-connection/src/local-manifest-source.ts` 的 `adoptLegacyEntries`(第 333 行起那段)。 - **两分钟验证:** `pnpm --filter @objectstack/cloud-connection exec vitest run src/local-manifest-source.test.ts`,预期 adoptLegacyEntries 四个用例全过(含「失败不留账本目录」「不留暂存目录」)。 - **需要你做的:** 无,席位落地。 ## What changes `LocalManifestSource.adoptLegacyEntries()` (added by #30351, the breaking commit `bc254be33e`) staged its copy with `mkdtempSync(join(dirname(this.dir), ...))`. `this.dir` is the ledger location resolved at run time (`OS_HOME`, or an explicit `storageDir`), so the in-tree scratch scan in `scripts/pm/dispatch-gates.mjs` cannot read that base. The scan holds `mkdtempSync` to completeness (an unresolved base is a failure), so the dispatch-gates self-test case "no mkdtempSync site in this tree takes a base the scan cannot read" has been red on every `main` run since. The staging directory is now spelled as an exclusive, non-recursive `mkdirSync` of a randomly named sibling: `.LEDGER_BASENAME.adopting-HEX12` (12 hex characters from `randomBytes(6)`) under `dirname(this.dir)`. Why this spelling and not one the scan resolves: no spelling of this base can be resolved from source, because the base really is a run-time value (it may even land inside a project checkout when `OS_HOME` points there). `mkdirSync` is the shape the scan deliberately does not hold to completeness (see the "In-tree scratch directories" block of `dispatch-gates.mjs`: runtime bases are named in the report and not failed). The uniqueness guarantee is kept: a non-recursive `mkdirSync` creates the directory atomically and throws `EEXIST` rather than reuse one that is already there, as `mkdtempSync` does. The rest is byte-for-byte unchanged: the files are copied into the staging directory, it is renamed into place, and it is removed if the copy fails. `scripts/pm/dispatch-gates.mjs` and its self-test are not touched. ## Tests No test changes. The four existing `adoptLegacyEntries` cases in `packages/cloud-connection/src/local-manifest-source.test.ts` already pin the behaviour this keeps: every file copied, no staging directory left beside the ledger, nothing adopted once the ledger exists, and a failed copy leaving neither a ledger nor a staging directory. ## Acceptance notes - Ablation: not run. The diff changes how a directory is created, not a tested behaviour; the existing cases above are the behaviour pins and pass unchanged. ## Verification All readings on head `c4fb1fe5`. <details><summary>The failing self-test case, before and after</summary> - CI, `main` at `c58008187` (run 42025638313, `Lint · PM dispatch-gates`): `✗ no mkdtempSync site in this tree takes a base the scan cannot read — UNRESOLVED: packages/cloud-connection/src/local-manifest-source.ts:333 (a base this scan cannot read: this.dir)`, then `✗ dispatch-gates self-test: 1 of 2234 case(s) failed.` - The same scan (`exposedScratchDirs` from `scripts/pm/dispatch-gates.mjs`), local: `origin/main f506b79: mkdtempSync unresolved = 1 packages/cloud-connection/src/local-manifest-source.ts:333 (a base this scan cannot read: this.dir)`; `head c4fb1fe: mkdtempSync unresolved = 0 ; exposed = 0`. - `node scripts/pm/check-dispatch-gates.mjs --slow` (both tiers, as `main`'s push and schedule runs do) on head: `✓ no mkdtempSync site in this tree takes a base the scan cannot read` and `✓ every in-tree directory this tree's sources create is covered by a tracked ignore rule, or is tracked itself`. The battery's total verdict line is quoted in the os-dev-report on #30467. - `pnpm check:pm-dispatch-gates` (fast tier, as PR and queue runs do) on head: `✓ dispatch-gates self-test (fast tier): 1945 cases pass; 7 slow section(s) deferred.`, exit 0. The case this PR fixes sits in a slow section, so this PR's own CI does not run it; the first CI run that does is `main`'s push battery after merge. </details> <details><summary>Package tests and type check</summary> - `pnpm --filter '@objectstack/cloud-connection^...' build`: exit 0. - `pnpm --filter @objectstack/cloud-connection test`: `Test Files 70 passed (70)`, `Tests 877 passed (877)`. - `pnpm --filter @objectstack/cloud-connection typecheck`: exit 0. </details> <details><summary>Derived gates (dispatch-gates --commands, 51 commands)</summary> `node scripts/pm/dispatch-gates.mjs --ran` on head: `✓ dispatch-gates --ran: 51 derived famil(ies) accounted for — 49 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).` All 49 run exit 0. NOT MEASURED, left to CI: `node scripts/check-plugin-teardown-shape.mjs --self-test` (shallow clone cannot reach its pinned control commit) and `pnpm check:dual-build-cjs-loads` (needs every package built). </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CZ9g1bEV7DPMneGKAx)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 455b4f0 commit 7d11067

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
The one-time copy of the working-directory install ledger into its `OS_HOME` location (#30217) now creates its staging directory with an exclusive `mkdirSync` of a randomly named sibling instead of `mkdtempSync`. Behaviour is unchanged: the copy still lands in a unique sibling of the ledger directory, is renamed into place, and is removed if the copy fails. This repairs the red `main` build (#30467), where the repository's in-tree scratch scan could not read the `mkdtempSync` base.

‎packages/cloud-connection/src/local-manifest-source.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
* the same ledger without going through HTTP.
3131
*/
3232

33+
import { randomBytes } from 'node:crypto';
3334
import {
34-
copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync,
35-
writeFileSync,
35+
copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync,
3636
} from 'node:fs';
3737
import { homedir } from 'node:os';
3838
import { basename, dirname, join, resolve } from 'node:path';
@@ -330,7 +330,14 @@ export class LocalManifestSource {
330330
const files = readdirSync(from).filter((name) => name.endsWith('.json'));
331331
if (files.length === 0) return null;
332332
mkdirSync(dirname(this.dir), { recursive: true });
333-
const staging = mkdtempSync(join(dirname(this.dir), `.${basename(this.dir)}.adopting-`));
333+
// A unique sibling, created exclusively: a non-recursive mkdirSync
334+
// throws EEXIST rather than reuse a directory that is already there,
335+
// which is the guarantee mkdtempSync gave. mkdtempSync is not used
336+
// because the repo's in-tree scratch scan holds every mkdtempSync to a
337+
// base it can read from source, and this base is the ledger location
338+
// resolved at run time.
339+
const staging = join(dirname(this.dir), `.${basename(this.dir)}.adopting-${randomBytes(6).toString('hex')}`);
340+
mkdirSync(staging);
334341
try {
335342
for (const name of files) copyFileSync(join(from, name), join(staging, name));
336343
renameSync(staging, this.dir);

0 commit comments

Comments
 (0)