{"record":{"id":"4a1d38b79b935326","repo":"666ghj/MiroFish","slug":"could-not-atomically-replace-star-history-outputs","errorCode":null,"errorMessage":"could not atomically replace Star History outputs","messagePattern":"could not atomically replace Star History outputs","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":1337,"sourceCode":"            ) as handle:\n                handle.write(payloads[relative])\n                handle.flush()\n                os.fsync(handle.fileno())\n                temporary_paths[relative] = Path(handle.name)\n            os.chmod(temporary_paths[relative], 0o644)\n\n        _validate_svg(temporary_paths[LIGHT_SVG_RELATIVE].read_bytes())\n        _validate_svg(temporary_paths[DARK_SVG_RELATIVE].read_bytes())\n        json.loads(temporary_paths[STATE_RELATIVE].read_bytes())\n\n        for relative in OUTPUT_RELATIVES:\n            checked_target = _safe_target(workspace, relative, create_parent=False)\n            if checked_target != targets[relative]:\n                raise StarHistoryError(\"output path changed during update\")\n            os.replace(temporary_paths[relative], targets[relative])\n            temporary_paths.pop(relative, None)\n    except OSError as exc:\n        raise StarHistoryError(\"could not atomically replace Star History outputs\") from exc\n    finally:\n        for temporary in temporary_paths.values():\n            try:\n                temporary.unlink(missing_ok=True)\n            except OSError:\n                pass\n\n    check_workspace(workspace)\n    return True\n\n\ndef check_workspace(workspace: Path) -> None:\n    state = load_state(workspace, require_canonical=True)\n    expected = _output_payloads(state)\n    for relative in OUTPUT_RELATIVES[1:]:\n        target = _safe_target(workspace, relative, create_parent=False)\n        actual = _read_limited(target, MAX_STATE_BYTES, str(relative))\n        _validate_svg(actual)","sourceCodeStart":1319,"sourceCodeEnd":1355,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L1319-L1355","documentation":"Raised by _write_outputs when any OSError escapes the try block that writes temp files (NamedTemporaryFile + fsync + chmod), validates them, and os.replace()s them onto the three output paths. The original OSError is chained as __cause__. The finally-block unlinks leftover temp files, so a failure leaves the existing outputs untouched — the atomic design means nothing is half-written.","triggerScenarios":"Read-only workspace or missing write permission on static/image/; disk full or inode exhaustion when creating the temp files; fsync failing on exotic filesystems; os.replace failing because the destination is a directory or on a different mount (shouldn't happen since temps are created in the same dir); a parent directory (e.g. .github/star-history) deleted mid-run.","commonSituations":"CI running as a user without write access to the checkout; Docker容器 with a read-only bind mount for static/; disk quota exceeded during backfill; a pre-push hook or file watcher holding locks on Windows; SELinux denying writes despite rwx bits.","solutions":["Inspect exc.__cause__ — the underlying OSError carries errno (ENOSPC, EACCES, EROFS, ENOENT) that identifies the real problem.","Ensure the running user can create files in .github/star-history/ and static/image/ (the temp files are written into those same directories).","Free space / raise the disk quota if ENOSPC; check the mount is not read-only in containers.","Re-run the command — generation is deterministic from history.json, so the retry produces identical payloads with no partial state."],"exampleFix":"# before: read-only mount in docker-compose.yml\nvolumes:\n  - ./static:/app/static:ro   # EROFS -> 'could not atomically replace...'\n\n# after: writable mount (or write outside and copy in CI)\nvolumes:\n  - ./static:/app/static","handlingStrategy":"try-catch","validationCode":"import os\n\nfor parent in {t.parent for t in targets.values()}:\n    assert parent.is_dir(), f\"missing output dir {parent}\"\n    assert os.access(parent, os.W_OK), f\"no write permission on {parent}\"\n\nusage = shutil.disk_usage(targets[0].parent)\nassert usage.free > 10 * 1024 * 1024, \"insufficient disk space for atomic write\"","typeGuard":null,"tryCatchPattern":"try:\n    changed = _write_outputs(root, state)\nexcept StarHistoryError as exc:\n    cause = exc.__cause__\n    if isinstance(cause, OSError):\n        logging.error(\"atomic replace failed: errno=%s path=%s\", cause.errno, cause.filename)\n        # EACCES/EROFS -> fix mount/permissions; ENOSPC -> free space; then re-run\n    raise","preventionTips":["Run the generator as a user with write access to .github/star-history/ and static/image/.","Keep writable mounts writable in container configs (no :ro on output paths).","Writes are atomic and deterministic — after fixing the underlying OSError, a plain retry is safe."],"tags":["filesystem","permissions","io","atomic-write"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}