Skip to content

[Bug][#367]: Locate uses cross-filesystem rename when output and system temp are on different volumes #432

Description

@tt-a1i

Locate stages output in the system temp directory and fails when --out is on another filesystem

Scope and verification boundary

Identified in the unmerged feature work in PR #367, at head cc1b33a7a02351347842dad4ebe9c13278afc166, using an isolated snapshot. This report does not claim a regression in the released version or main branch.

The same-filesystem control below was executed and passed. Cross-filesystem execution has not been performed. The cross-filesystem failure is identified from the staging and rename implementation; no actual EXDEV log is claimed here.

Cause and impact

At this head:

  • archify/locate/cli.mjs:503 creates the staging directory under os.tmpdir().
  • Line 280 backs up existing output files into staging with fs.renameSync().
  • Line 285 commits candidates to --out with fs.renameSync().

Renaming across filesystem boundaries is unsupported. If --out is on a filesystem different from the system temp directory, both first-time generation and replacement of existing output encounter a cross-filesystem rename. The output path is otherwise a valid writable directory.

Environment

macOS, Node.js v22.23.2. The scripts below were verified with this runtime against the PR #367 snapshot identified above.

Reproduction procedure

From the repository root at the head above, save the script below as locate-exdev-repro.mjs. It creates an isolated Git repository and a fresh output directory, then removes both. The script can live outside the repository, but run it with the repository root as the current working directory.

  1. Run node locate-exdev-repro.mjs with LOCATE_OUT_PARENT unset for the same-filesystem control.
  2. Choose an already available, writable directory on another filesystem. No disk creation or mounting is needed.
  3. Set LOCATE_OUT_PARENT to that directory and run the same script, for example:
LOCATE_OUT_PARENT='/Volumes/ExistingExternalDisk/writable-directory' node locate-exdev-repro.mjs

The script must print same filesystem: false for step 3 to exercise the reported path. Different directory names on the same filesystem do not count as a cross-filesystem test.

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execFileSync, spawnSync } from 'node:child_process';

// Run from the repository root. Set LOCATE_OUT_PARENT to an existing writable
// directory on another filesystem to exercise the cross-filesystem path.
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-locate-exdev-repo-'));
const out = fs.mkdtempSync(path.join(
  process.env.LOCATE_OUT_PARENT || os.tmpdir(), 'archify-locate-exdev-out-',
));
try {
  const map = JSON.parse(fs.readFileSync(
    'archify/test/fixtures/bundle-checkout/ledger-flow.json', 'utf8',
  ));
  fs.writeFileSync(path.join(repo, 'map.json'), JSON.stringify(map));
  fs.writeFileSync(path.join(repo, 'map.ownership.json'), JSON.stringify({
    schema_version: 1, kind: 'ownership', map: 'map.json', excluded: [],
    components: map.nodes.map(node => ({ id: node.id, globs: [] })),
  }));
  const git = (...args) => execFileSync('git', ['-C', repo, ...args], { stdio: 'pipe' });
  git('init');
  git('add', '.');
  git('-c', 'user.name=Archify Repro', '-c', 'user.email=repro@example.test',
    '-c', 'commit.gpgsign=false', 'commit', '-m', 'fixture');
  console.log('same filesystem:', fs.statSync(os.tmpdir()).dev === fs.statSync(out).dev);
  const result = spawnSync(process.execPath, [
    'archify/bin/archify.mjs', 'locate', '--lint', 'HEAD',
    '--map', path.join(repo, 'map.json'), '--repo-root', repo,
    '--out', out, '--json',
  ], { encoding: 'utf8' });
  console.log('exit:', result.status);
  const receipt = JSON.parse(result.stdout);
  console.log(JSON.stringify({ ok: receipt.ok, error: receipt.error, diagnostics: receipt.diagnostics }, null, 2));
  console.log('output files:', JSON.stringify(fs.readdirSync(out)));
} finally {
  fs.rmSync(repo, { recursive: true, force: true });
  fs.rmSync(out, { recursive: true, force: true });
}

Actual same-filesystem result

Executed against the snapshot above, with LOCATE_OUT_PARENT unset:

same filesystem: true
exit: 0
{
  "ok": true
}
output files: ["locate.html","locate.receipt.json"]

Cross-filesystem expectation — not an observed log

With same filesystem: false, the rename path is expected to exit nonzero with a diagnostic containing EXDEV and rename, without successfully committing the output files. This remains to be confirmed on an actual second filesystem; the text here is a prediction from the implementation, not an execution transcript.

Expected behavior

Locate should support a writable output directory on another filesystem. Create staging beneath the resolved output directory, as the existing deliver and preview commands already do, so backups and candidate commits remain on the target filesystem.

Add coverage that verifies staging is target-adjacent, and include a real cross-filesystem run when a suitable environment is available.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions