{"record":{"id":"e7bff2fba701fb24","repo":"astrid-runtime/astrid","slug":"failed-to-replace-signed-capsule-archive-error","errorCode":null,"errorMessage":"failed to replace signed capsule archive: {error}","messagePattern":"failed to replace signed capsule archive: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-build/src/artifact.rs","lineNumber":430,"sourceCode":"                0o755\n            } else {\n                0o644\n            };\n            let mut header = deterministic_header(entry.size(), mode, entry_type);\n            target\n                .append_data(&mut header, &path, &mut entry)\n                .with_context(|| format!(\"failed to copy capsule entry '{path}'\"))?;\n        }\n        let mut header =\n            deterministic_header(envelope.len() as u64, 0o644, tar::EntryType::Regular);\n        target.append_data(&mut header, PROVENANCE_FILE, envelope)?;\n        let encoder = target.into_inner()?;\n        encoder.finish()?;\n    }\n    staged.as_file_mut().sync_all()?;\n    staged\n        .persist(archive_path)\n        .map_err(|error| anyhow::anyhow!(\"failed to replace signed capsule archive: {error}\"))?;\n    Ok(())\n}\n\n/// Construct a metadata-stable GNU tar header for synthesized or rewritten\n/// entries. Content and executable intent remain significant; host ownership\n/// and wall-clock timestamps do not.\nfn deterministic_header(size: u64, mode: u32, entry_type: tar::EntryType) -> tar::Header {\n    let mut header = tar::Header::new_gnu();\n    header.set_size(size);\n    header.set_mode(mode);\n    header.set_uid(0);\n    header.set_gid(0);\n    header.set_mtime(tar::DETERMINISTIC_TIMESTAMP);\n    header.set_entry_type(entry_type);\n    header.set_cksum();\n    header\n}\n","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-build/src/artifact.rs#L412-L448","documentation":"rewrite_with_provenance rebuilds a signed capsule archive into a staged temporary file and then atomically persists it over the original archive path with persist(). If the OS refuses to replace the target (persist fails), the library wraps the persist error with this message so the caller knows the signed archive was built but could not be swapped into place.","triggerScenarios":"Calling sign_archive (which calls rewrite_with_provenance) when tempfile::persist fails to rename the staged file onto archive_path — target path locked/open by another process, permission denied on the destination or its directory, cross-filesystem rename, or destination removed/immutable.","commonSituations":"Antivirus, backup, or another build process holding the archive open (common on Windows); read-only output directory or CI cache permissions; archive_path on a different mount than the staging directory; the file marked read-only from a previous artifact sync.","solutions":["Check the wrapped inner error: fix permissions on archive_path and its parent directory (writable by the build user).","Close other processes holding the archive open (editors, AV scanners, prior build steps) and retry.","Ensure the staged temp file and archive_path are on the same filesystem so the atomic rename can succeed.","Remove read-only/immutable flags from the existing archive before signing."],"exampleFix":"// before: destination read-only, persist fails\n// -rw-r--r-- root capsule.tar.zst (owned by root, build runs as ci)\n// after: ensure ownership/permissions before signing\nchown ci:ci capsule.tar.zst && chmod u+w capsule.tar.zst\ncargo build --release","handlingStrategy":"fallback","validationCode":"// Pre-flight: destination must be writable and not read-only before signing\nlet meta = std::fs::metadata(archive_path)?;\nif meta.permissions().readonly() {\n    let mut perms = meta.permissions();\n    perms.set_permissions(perms.mode() & !0o444 | 0o200);\n    std::fs::set_permissions(archive_path, perms)?;\n}","typeGuard":null,"tryCatchPattern":"match sign_archive(&archive_path, &key).await {\n    Ok(()) => {},\n    Err(e) if e.to_string().contains(\"failed to replace signed capsule archive\") => {\n        // fallback: write to a sibling path so the build still produces the artifact\n        let alt = archive_path.with_extension(\"tar.zst.new\");\n        rewrite_with_provenance(&alt, provenance).await?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep the staged temp file and the destination on the same filesystem.","Ensure build users have write permission on the artifact directory.","Exclude artifact paths from AV/indexers that hold files open during builds.","Clear read-only flags inherited from artifact sync tools before signing."],"tags":["filesystem","file-write","build","artifacts"],"backgroundTag":"file-write-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}