666ghj/MiroFish · error · StarHistoryError
output path changed during update
Error message
output path changed during update
What it means
Raised by _write_outputs during the atomic publish loop: for each output (history.json, star-history-light.svg, star-history-dark.svg) it re-resolves the destination with _safe_target and compares against the path captured before writing temp files. A mismatch means the workspace layout moved underneath the update — typically a symlink swap or directory replacement between the two resolutions — and the update aborts before os.replace to avoid writing through a redirected path.
Source
Thrown at scripts/star_history.py:1333
dir=target.parent,
prefix=f".{target.name}.",
suffix=".tmp",
delete=False,
) as handle:
handle.write(payloads[relative])
handle.flush()
os.fsync(handle.fileno())
temporary_paths[relative] = Path(handle.name)
os.chmod(temporary_paths[relative], 0o644)
_validate_svg(temporary_paths[LIGHT_SVG_RELATIVE].read_bytes())
_validate_svg(temporary_paths[DARK_SVG_RELATIVE].read_bytes())
json.loads(temporary_paths[STATE_RELATIVE].read_bytes())
for relative in OUTPUT_RELATIVES:
checked_target = _safe_target(workspace, relative, create_parent=False)
if checked_target != targets[relative]:
raise StarHistoryError("output path changed during update")
os.replace(temporary_paths[relative], targets[relative])
temporary_paths.pop(relative, None)
except OSError as exc:
raise StarHistoryError("could not atomically replace Star History outputs") from exc
finally:
for temporary in temporary_paths.values():
try:
temporary.unlink(missing_ok=True)
except OSError:
pass
check_workspace(workspace)
return True
def check_workspace(workspace: Path) -> None:
state = load_state(workspace, require_canonical=True)
expected = _output_payloads(state)View on GitHub (pinned to b5b53acc57)
Solutions
- Serialize runs: only one star-history update per checkout at a time (CI mutex, lockfile around the execute() entry point).
- Remove symlinks from the output paths — keep static/image/ and .github/star-history/ as real directories tracked by git.
- Retry the command once after the race clears; the write is idempotent (payloads are deterministic from history.json) so a clean retry is safe.
- If a deploy pipeline must swap directories, make it wait on the generator's completion marker rather than racing it.
Example fix
# before: two concurrent jobs race the same workspace
job1: python scripts/star_history.py update #
job2: python scripts/star_history.py update # path re-resolves differently -> raises
# after: serialize with a lock around execute()
with filelock.FileLock(".star-history.lock", timeout=600):
result = execute("update", github=gh, clock=clock, workspace=root) Defensive patterns
Strategy: retry
Validate before calling
from pathlib import Path
def outputs_are_stable(workspace: Path) -> bool:
# resolve twice; if paths move between resolutions, another actor is mutating the tree
first = {r: _safe_target(workspace, r, create_parent=False) for r in OUTPUT_RELATIVES}
second = {r: _safe_target(workspace, r, create_parent=False) for r in OUTPUT_RELATIVES}
return first == second and not any(p.is_symlink() for p in first.values()) Try / catch
for attempt in range(2):
try:
changed = _write_outputs(root, state)
break
except StarHistoryError as exc:
if "output path changed" not in str(exc) or attempt == 1:
raise
# race with another writer; release and retry once Prevention
- Serialize all star-history runs against one checkout with a lock.
- Keep output locations as plain git-tracked directories, never symlinks.
- Don't reorganize static/ or .github/star-history/ while a run is in flight.
When it happens
Trigger: A symlink at static/image/star-history-light.svg (or a parent dir like .github/star-history) is retargeted while the job runs; a concurrent process (another CI job, a deploy hook) rewrites/replaces directories during the seconds between target capture (line 1300) and re-check (line 1331); renaming the workspace root concurrently.
Common situations: Two CI jobs or scheduled runs executing simultaneously in one checkout; build tooling that swaps static/ via symlink for atomic site deploys; developers re-arranging directories while a long backfill runs; workspace on a network mount with path instability.
Related errors
- output directory cannot be a symbolic link
- output file cannot be a symbolic link
- could not atomically replace Star History outputs
- Graph {graph_id} is in use by active consumer(s): {', '.join
- 模拟已在运行或结束处理中: {simulation_id}
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/0b2ba2ae8b2f71e2.
Report an issue: GitHub.