Skip to content

Releases: daniloaguiarbr/atomwrite

atomwrite v0.1.20 — 11 GAP-2026 closed, --lang → --locale rename, 542 tests, 4 ADRs

15 Jun 16:38

Choose a tag to compare

Highlights

  • 11 GAP-2026 closed (001-011) — see gaps.md for the full local audit
  • 542 tests in 47 suites (+27 from v0.1.19's 515 in 46 suites: 11 GAP-2026 closures + 16 intention-guard tests)
  • 4 ADRs added: ADR-0034, ADR-0035, ADR-0036, ADR-0037
  • BREAKING CHANGE in CLI surface: global --lang renamed to --locale
    • Env var ATOMWRITE_LANG stays stable
    • Rust field args.global.lang stays stable
    • Subcommand --lang (e.g. scope --lang rust, transform --lang python) now works as a freed alias for --language

GAP-2026 Closures (11/11)

GAP Subcommand Type ADR
001 count --by-size help-first drift
002 write --preserve-timestamps feature parity
003 scope --lang alias help-first + namespace 0037
004 write --line-ending crlf form-accept (crlf + cr-lf)
005b edit --partial semantics single vs multi-pair asymmetry 0036
006 diff --algorithm regression pinning
007 count --by-extension backup parsing edge case
008 read --head/--line/--lines API contract ambiguity
009 read mode discriminator API metadata
010 search --no-begin-end output noise
011 write intention guards defense-in-depth 0035

GAP-2026-011: Write Intention Guards (CRITICAL)

On 2026-06-15, atomwrite write was called without --append on c24-framework34.html (491,827 bytes). The file was truncated to a few bytes; ~127 lines of work (~9 KB) were lost. A 2026-06-14 23:49 manual cp backup existed but did not cover 2026-06-15 work.

v0.1.20 adds 5 layers of defense-in-depth (all opt-in, default off to preserve backward compat):

  • --require-backup (L2): aborts with InvalidInput (exit 65) if target exists and --backup is not set
  • --confirm (L3): prompts Overwrite <path> (<bytes> bytes)? [y/N] for files larger than 100 KB
  • --auto-rotate (L5): forces a rotation backup if target was modified within the last 24 hours
  • --risk-threshold <PERCENT> (default 50): L1 size-guard threshold; emits stderr warning (low/medium/high) when size delta exceeds the threshold
  • WriteOutput.risk_assessment field (L6 telemetry): carries original/new bytes, delta percentage, risk level, and which guard triggered

Migration

Breaking change in CLI surface: scripts using --lang <locale> must migrate to --locale <locale>.

# one-liner migration across bin/ and scripts/
rg -l '\-\-lang\b' bin/ scripts/ && sd -- '\-\-lang\b' '--locale' bin/ scripts/

Env var ATOMWRITE_LANG and the programatic args.global.lang field remain stable — CI matrices, container wrappers, and Rust consumers do not need changes.

Validation

  • cargo build --release — OK
  • cargo clippy --all-targets -- -D warnings — OK
  • 542 tests in 47 suites (up from 515 in 46 in v0.1.19; +27 new: 11 GAP-2026 closures + 16 intention-guard tests)
  • 4 new ADRs: 0034 (help-driven testing anti-pattern), 0035 (write intention guards), 0036 (edit --partial coverage), 0037 (global locale rename)
  • 11 GAP-2026 closed (001-011), 100% coverage of local audit gaps
  • Cross-compile verified on 3 Windows targets: x86_64-pc-windows-gnu, i686-pc-windows-gnu, x86_64-pc-windows-msvc

Resources

v0.1.19 — G121 path resolution, real S-expression, 7 exit code drifts

14 Jun 21:37

Choose a tag to compare

[0.1.19] - 2026-06-14

G121 — search and replace resolve root paths against the workspace

  • Bug (CWE-367 — TOCTOU/path-confusion)cmd_search and cmd_replace took the caller-supplied root paths, validated them with path_safety::validate_path, then fed the ORIGINAL (CWD-relative) path to ignore::WalkBuilder. validate_path returned the canonical absolute path but the per-call result was discarded. When CWD != --workspace, the walker either (a) silently walked the wrong tree if a same-named path existed under CWD, or (b) produced a JailViolation event per file walked. The G118 fix in write.rs:44 (ADR-0027) never propagated to these two commands because the per-entry validate_path inside the worker thread masked the missing pre-step resolution.
  • Fix — new path_resolution::resolve_paths_against_workspace helpercmd_search and cmd_replace now call this helper once at the top of the command (after global.resolve_workspace()()) and pass the canonical Vec<PathBuf> to build_walker. The pre-step for path in &args.paths { validate_path(path, &workspace)?; } loop in replace.rs is removed; search gets a parallel resolution call. Both build_walker signatures gain a canonical_paths: &[PathBuf] parameter; they no longer read &args.paths[0] directly.
  • Consequence — Search and replace now honor CWD != --workspace. A relative path like src/ passed with --workspace /path/to/ws walks /path/to/ws/src/ regardless of process CWD. Out-of-jail paths fail once with WORKSPACE_JAIL (exit 126) at command start instead of per-file inside the worker. See docs/decisions/0031-g121-path-resolution-helper.md.

G122 — Real S-expression matching in query subcommand

  • Bug (silent feature, documented but never implemented) — The query subcommand (v14 Tier 3, introduced in v0.1.12) always promised S-expression support. The docs in COOKBOOK.md, HOW_TO_USE.md and both bilingual SKILL files show examples like atomwrite --workspace . query src/main.rs --query "(function_item name: (identifier) @name)". In practice, cmd_query called walk_kind_filter which does wanted.iter().any(|w| w == &kind) — a literal STRING comparison with node.kind(). The whole string "(function_item name: (identifier) @name)" never matched any real node.kind(), so the S-expression feature never worked.
  • Fix — new QueryType enum + auto-classificationclassify_pattern(pattern) -> QueryType detects S-expression by the presence of (, ), or @. walk_sexpr compiles the pattern via tree_sitter::Query::new, executes via QueryCursor::matches, and emits query_match NDJSON with the new capture_name field for each @capture. cmd_query branches on the classification.
  • New direct dep tree-sitter = "0.26" — the language-pack re-exports Language but Query/QueryCursor/StreamingIterator require the tree-sitter crate itself.
  • Consequence — Patterns with (, ), or @ are routed to tree_sitter::Query::new. Parse errors (e.g. (unclosed) return exit 1 with invalid S-expression pattern: ... message (via anyhow::Context). The kind-filter path is preserved bit-by-bit: users passing --query function_item (without S-expression chars) keep getting the same results. See docs/decisions/0032-query-sexp-real-implementation.md.

Exit code documentation drift consolidation (ADR-0033)

  • Context — Phase D testing on 2026-06-14 ran 7 concrete binary-level probes against the v0.1.18 release and surfaced 7 places where the published docs diverged from the actual binary behavior. The 7 drifts are:
    1. STATE_DRIFT (82) absorbs CHECKSUM_VERIFY_FAILED (81) for --verify-checksum — both are conflict class, retryable. The 81-code is now historical (preserved only for the read path BLAKE3 mismatch on file content).
    2. --syntax-check returns SYNTAX_ERROR_DETECTED, NOT SYNTAX_ERROR — the rename happened in the v0.1.12 G72 tree-sitter rollout but docs were not updated.
    3. ORPHAN_JOURNAL (93) is consultive, NOT auto-detected — the gate is ATOMWRITE_WAL=1 OR --strict-atomic. The default write (v0.1.16 G119 WalPolicy::Auto) does not write a sidecar and therefore cannot detect orphans.
    4. BROKEN_PIPE (141) requires real SIGPIPE propagation — a simple head -1 pipe does NOT trigger it. The v0.1.4+ SIGPIPE restoration puts the default disposition back, so the signal is only raised when the downstream consumer actively closes the pipe mid-stream.
    5. Binary file reads return exit 0 with kind: binary metadata, NOT exit 65 — the v0.1.4 BINARY_FILE heuristic was changed to emit a structured envelope and exit 0. The 65-code path now only fires for read without --format raw AND with the binary heuristic bypassed.
    6. Missing positional argument returns ARGUMENT_PARSE_ERROR (exit 2), NOT INVALID_INPUT (65) — clap-level argument errors are reported as exit 2. The 65-code is reserved for runtime content validation (malformed TOML, invalid regex, empty stdin default).
    7. Missing --workspace defaults to CWD, NOT an error — --workspace is documented as a flag with a CWD default, not a required argument. WORKSPACE_JAIL (126) only fires when an absolute path resolves outside the effective jail.
  • Decision — Accept the binary behavior as canonical. Consolidate the docs in v0.1.19 to match. See docs/decisions/0033-v0-1-19-exit-code-naming-drift-consolidation.md.
  • Note on SYNTAX_ERROR legacy name — the v0.1.12 docs used SYNTAX_ERROR; the binary in v0.1.18 emits SYNTAX_ERROR_DETECTED. The historical name is preserved only in prose for grep-ability.

Validation

  • cargo build --release OK
  • cargo clippy --all-targets -- -D warnings OK
  • 515 tests passing in 46 suites (up from 502 in 44 suites in v0.1.18, +13 new)
  • 3 new ADRs: 0031 (path resolution helper), 0032 (query S-expression), 0033 (exit code drift consolidation)

[0.1.18] - 2026-06-14

v0.1.18 — G117, G118+R, G119, G120, 502 tests

14 Jun 08:24

Choose a tag to compare

[0.1.18] - 2026-06-14

G118 — replace pre-validates root paths against the workspace jail

  • cmd_replace resolve-first para todas as raízes — após global.resolve_workspace(), o comando itera sobre args.paths e chama path_safety::validate_path(path, &workspace)? para CADA raiz ANTES de construir o WalkBuilder. Falha rápida com WORKSPACE_JAIL (exit 126) na primeira violação. Comportamento legado per-entry (v0.1.12-v0.1.17) emitia um evento JailViolation por arquivo caminhado e o usuário via o diagnóstico enterrado sob N eventos. Agora replace /etc/passwd aborta em microssegundos com um único envelope de erro estruturado.
  • Convenção resolve-first agora universalwrite (ADR-0027), edit, copy, apply, move, rollback, set, del, case e agora replace todos validam o alvo contra o jail workspace ANTES de qualquer exists() ou read. Um único modelo mental para todos os comandos mutantes.
  • Teste de regressão atualizadoreplace_jail_violation_does_not_inflate_counter em tests/cli_v012_regressions.rs agora afirma exit 126 + envelope WORKSPACE_JAIL + arquivos inside/outside inalterados. O nome é preservado (a invariante subjacente — "jail violation não pode inflar o counter de substituições" — é mantida) mas o corpo da asserção mudou.
  • 2 novos testes integrados em tests/cli_replace.rs: replace_root_path_outside_workspace_exits_126 (caminho absoluto /etc/passwd) e replace_relative_dotdot_root_outside_workspace_exits_126 (caminho relativo ../escape).

G120 L3 — cobertura de teste para cross-validação

  • 2 novos testes integrados em tests/cli_write.rs:
    • g120_l3_append_empty_stdin_with_matching_checksum_succeeds — escreve arquivo seed, hasheia, roda write --append --allow-empty-stdin --expect-checksum <HASH> < /dev/null, afirma exit 0 + stdin_bytes_read: 0 + arquivo inalterado (no-op append preserva checksum).
    • g120_l3_append_empty_stdin_without_opt_in_rejects_at_l1 — sem --allow-empty-stdin, a guarda L1 dispara primeiro (exit 65) e o arquivo é preservado. Documenta que L3 é inalcançável sem opt-in explícita.

G117 follow-up — cobertura de edge cases

  • 3 novos testes integrados em tests/cli_edit.rs:
    • edit_unicode_old_new_exact_match — diacríticos UTF-8 (çãoAÇÃO) com casamento exato byte-a-byte. Documenta o contrato de single-pair (substitui apenas a PRIMEIRA ocorrência; multi-pair para múltiplas).
    • edit_crlf_line_endings_preserve_eol_after_replace — input com \r\n, replace, afirma preservação byte-a-byte (sem colapso para \n).
    • edit_multi_pair_same_old_appears_twice_applies_both — multi-par onde ambos os pares referenciam o mesmo token --old. Garante que o segundo par consome a versão pós-primeiro-substituição (proteção contra off-by-one).

ADR

  • docs/decisions/0030-v0-1-18-g118-replace-pre-validation-g120-l3-tests-g117-edge-cases.md — registra as 3 decisões, alternativas consideradas e gatilhos para revisitar.

Validation

  • cargo build --release OK
  • cargo clippy --all-targets -- -D warnings OK
  • Suíte completa de testes: 502 testes passando, 0 falhas, 0 regressões introduzidas pelas 3 mudanças
  • 2 flakes pré-existentes confirmados como não relacionados (signal_test::batch_interrupted_by_signal, tracing_test::span_captures_path_field + tracing_test::debug_level_includes_filter_info) — falhas idênticas no baseline git stash antes das mudanças

v0.1.14 — fix `write --line-ending auto` cross-platform divergence on new files

07 Jun 20:08

Choose a tag to compare

Summary

Resolves the write_creates_file_with_ndjson_output integration test failure on the windows-2025-vs2026 GitHub Actions runner. v0.1.13 produced different bytes_written values for the same stdin content on Linux/macOS (12 bytes) vs Windows (13 bytes) when the target file did not exist.

Root Cause

In src/commands/write.rs::normalize_line_endings, the Auto mode had a cfg!(windows) ? CrLf : Lf fallback for the new-file / unreadable-file branch. On Windows, the subsequent line_endings::normalize(..., CrLf) inserted a \r before every \n, inflating the byte count by 1. A secondary bug also stripped a byte from CRLF input on Linux/macOS via the same fallback path.

Fix

Both cfg!(windows) fallback branches have been removed. Auto on a new file is now a true no-op, matching the LineEnding::Auto docstring ("Preserve the dominant ending of the original file"). Behavior is deterministic across Linux, macOS, and Windows for the same stdin content. Detection on existing files and explicit --line-ending lf|cr-lf|cr modes are unchanged.

Regression Tests

Added in src/commands/write.rs::tests:

  • auto_on_new_file_preserves_lf_input
  • `auto_on_new_file_preserves_crlf_input"

Validation

  • cargo build --all-features (Linux): PASS
  • cargo fmt --check: PASS
  • cargo clippy --all-targets --all-features -- -D warnings: PASS
  • cargo test --all-features: 152 lib tests + integration suites + 3 doctests PASS
  • cargo check --target x86_64-pc-windows-gnu --lib with RUSTFLAGS=-Dwarnings: PASS, 0 errors, 0 warnings
  • 8/8 cli_write integration tests pass

Links

v0.1.13 — fix 4 RUSTFLAGS=-Dwarnings errors on windows-2025-vs2026

07 Jun 19:41

Choose a tag to compare

Summary

Resolves 4 compile errors on the windows-2025-vs2026 GitHub Actions runner under RUSTFLAGS=-Dwarnings. All four errors were unix-only symbols that become dead code when compiled for Windows; none were caught by the Linux CI matrix because Linux is cfg(unix).

Fixed

  • unused import: Duration in src/lock.rs:24std::time::Duration is gated behind #[cfg(unix)] since it is only consumed by the flock polling loop in try_acquire_loop.
  • unused variable: strict_atomic in src/atomic.rs:381 — annotated with #[cfg_attr(not(unix), allow(unused_variables))], mirroring the pattern already validated in src/signal.rs:15-17.
  • function copy_tempfile_to_target is never used in src/atomic.rs:604 — gated behind #[cfg(unix)] since the function is only invoked from the EXDEV fallback branch.
  • clippy::unnecessary_literal_unwrap in src/atomic.rs:195hardlink_nlink.unwrap_or(1) > 1 rewritten as hardlink_nlink.is_some_and(|n| n > 1) to avoid triggering clippy 1.94+ on the Windows None path.

Validation

  • cargo build --all-features (Linux): PASS
  • cargo clippy --all-targets --all-features -- -D warnings (Linux): PASS
  • cargo test --all-features: 150 lib tests + integration + doctests PASS
  • cargo check --target x86_64-pc-windows-gnu --lib with RUSTFLAGS=-Dwarnings: PASS, zero errors, zero warnings
  • cargo fmt --check: PASS

Links

atomwrite v0.1.12 — G72 Tree-sitter + G114 WAL + v14 Tier 3

07 Jun 14:52

Choose a tag to compare

Highlights

This release closes 20 technical gaps with 11,300+ lines of new code, tests, schemas, ADRs, and bilingual documentation.

Major Features

  • G72 — REAL tree-sitter syntax check (atomwrite write --syntax-check): validates content against 24 languages via tree_sitter_language_pack
  • G114 — WAL sidecar consultive: opt-in via ATOMWRITE_WAL=1 env or --strict-atomic flag, with recover_orphan_journals API
  • v14 Tier 3 — 6 new subcommands: set, get, del, case, query, outline

6 New Subcommands (ADDITIVE)

Subcommand Description
set Write a value to a TOML/JSON config file with dotted path
get Read a value from a TOML/JSON config file with dotted path
del Delete a key from a TOML/JSON config file
case Convert identifier case (snake, camel, pascal, kebab, screaming-snake)
query Tree-sitter S-expression query against a source file
outline Extract high-level structure from a source file

5 New Error Codes (ADDITIVE)

Code Variant Class Retryable
83 LockTimeout transient Yes
88 SyntaxError permanent No
91 ExdevFallbackDisabled precondition_failed No
92 CopyBackBlake3Failed conflict After re-read
93 OrphanJournal precondition_failed No

Quality Gates

  • 445 tests in 44 test suites (was 320 in v0.1.10, +29 in v0.1.11, +96 in v0.1.12)
  • 8 gates pass: fmt, clippy, build, test, doc, deny, audit, msrv
  • 3 cross-compile targets: x86_64-pc-windows-gnu, i686-pc-windows-gnu, x86_64-pc-windows-msvc
  • MSRV: Rust 1.88 stable
  • cargo deny: 4/4 OK (zero vulnerabilities)

New Modules

  • src/wal.rs — G114 WAL sidecar with consultive recovery
  • src/syntax_check.rs — G72 tree-sitter syntax validation
  • src/commands/{set,get,del,case,query,outline}.rs — v14 Tier 3 subcommands
  • src/lock.rs — G54 file lock with timeout
  • src/xattr_restore.rs — G39 extended attribute preservation

Documentation

  • 7 ADRs in docs/decisions/ (0019-0025)
  • 7 new JSON Schemas in docs/schemas/
  • 14 docs (7 EN + 7 PT-BR) updated with v0.1.12 sections
  • Skill files (skill/atomwrite-{en,pt}/SKILL.md) expanded from 35 to 46 sections

Dependencies

  • tree-sitter-language-pack = "1.8" (download + dynamic-loading, ~5-10MB footprint)

Breaking Changes

None. All 6 new subcommands and 5 new error variants are ADDITIVE. No existing subcommand was renamed or removed.

Installation

cargo install atomwrite --locked --version '^0.1.12'

Verification

atomwrite --version
# atomwrite 0.1.12

atomwrite --help
# 28 user-facing subcommands

atomwrite read src/main.rs | jaq -r '.checksum'
# BLAKE3 checksum

Migration from v0.1.11

No action required. The release is fully backward compatible. See CHANGELOG.md for the full list of changes.

Resources

v0.1.11

05 Jun 22:30

Choose a tag to compare

atomwrite v0.1.11### Fixed- Windows E0433 on windows-2025-vs2026libc::write(STDERR_FILENO, ...) was referenced from src/main.rs in a function compiled on every platform, but libc is declared only under [target.cfg(unix).dependencies]. The shutdown-message writer was moved to src/signal.rs and gated with #[cfg(unix)].- signal_test::shutdown_message_on_stderr no longer flakes on ubuntu-latest — Two independent failure modes addressed: 1. cmd_search short-circuits to Ok(()) when shutdown.is_shutdown() is true, so the main thread takes the Ok(()) branch and emits the shutdown banner. 2. install_handlers_early and install_handlers now share a single Arc<ShutdownSignal> instance, eliminating the race where the second instance flag remained false under signal-hook chain-of-handlers ordering.- Test uses ATOMWRITE_READY_FILE for race-free readiness detection — atomwrite writes its PID to the file as soon as install_handlers_early returns; the test polls the file with a 10s deadline before sending SIGINT.### Validation- 302/302 tests pass across 33 test suites (5 successive full-suite runs, 0 failures)- cargo fmt, cargo clippy --all-features --all-targets -- -D warnings, cargo build --release, RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features, cargo audit, cargo deny check all PASS

v0.1.10

05 Jun 20:32

Choose a tag to compare

Highlights

  • CI GitHub Actions fully green — The persistent ubuntu-latest signal_test::shutdown_message_on_stderr failure has been fixed by installing signal handlers as the very first initialization step in main(). The previous v0.1.8/v0.1.9 fixes (POSIX signal-safety, stderr flush) were correct in isolation but irrelevant: the child was being killed by SIGINT before any of that code could run.
  • GAP 20 resolved — Root cause: the SIGINT sent by signal_test was arriving BEFORE install_handlers() had a chance to register the signal_hook flag and low_level handlers. The default signal disposition (terminate via signal) was executing, killing the child with signal 2 and bypassing the entire graceful-shutdown path.

Fixed (GAP 20 - early signal handler installation)

  • signal_test::shutdown_message_on_stderr no longer fails on Linux CI — Added signal::install_handlers_early() which registers only the async-signal-safe signal_hook::flag::register for SIGINT and SIGTERM as the very first statement in main(), before any other initialization. This guarantees that any SIGINT/SIGTERM arriving from the first nanosecond of process startup is caught and sets the shutdown flag, which the search/batch loops check. The full install_handlers() (with counter tracking and OnceLock shared state) runs later and adds the double-Ctrl+C force-kill logic on top, without overwriting the early flag handlers. The previous v0.1.8/v0.1.9 fixes for POSIX signal-safety and stderr flush are still in place and necessary: the early install guarantees the flag is set, the main-thread writeln guarantees the message reaches the captured stderr pipe before exit.

  • src/main.rs shutdown message uses libc::write(STDERR_FILENO, ...) directly — Replaced writeln!(io::stderr().lock(), ...) with a helper function that calls libc::write(STDERR_FILENO, ...) to bypass any userspace buffering. The function is marked #[allow(unsafe_code)] because main.rs has #![deny(unsafe_code)]. The write is async-signal-safe per POSIX.1-2017 signal-safety(7) and goes straight to the kernel via fd 2, guaranteeing the bytes reach the captured stderr pipe before the process exits.

Validation

  • cargo build --all-features: PASS
  • cargo clippy --all-features --all-targets -- -D warnings: PASS
  • cargo test --all-features: 302/302 tests PASS (5/5 signal_test cases all pass)
  • cargo fmt -- --check: PASS
  • cargo audit: PASS (no vulnerabilities, no --ignore flag)
  • cargo deny check: PASS (advisories, bans, licenses, sources all OK)
  • 5/5 consecutive local runs of signal_test::shutdown_message_on_stderr PASS

Notes

  • v0.1.10 is a NON-BREAKING bug fix. No public API was modified.
  • The signal handler installation is now a two-phase process: minimal flag handlers at process start, full counter handlers after init. The OnceLock ensures only one full install takes effect.
  • See CHANGELOG.md for full details.

v0.1.9

05 Jun 19:45

Choose a tag to compare

Highlights

  • CI GitHub Actions fully green — Final flush fix for signal_test::shutdown_message_on_stderr ensures the shutdown message reaches the captured stderr pipe before the process exits on Linux/glibc.
  • POSIX signal-safety compliance — The shutdown message is emitted via io::stderr().lock() in the main thread, with the StderrLock guard guaranteeing buffer flush on Drop.

Fixed (GAP 17 follow-up)

  • signal_test::shutdown_message_on_stderr flushes via io::stderr().lock() — The first v0.1.8 fix moved eprintln! from the SIGINT/SIGTERM signal handlers to the main thread, but used writeln!(io::stderr(), ...) which is fully-buffered when stderr is redirected to a pipe (as in cargo test's Stdio::piped()). The buffer was never flushed before the process exited with the signal exit code, so the parent test saw an empty stderr. The fix uses io::stderr().lock() to acquire the StderrLock guard, which flushes the buffer on Drop. CI ubuntu-latest will confirm on push.

Validation

  • cargo build --all-features: PASS
  • cargo clippy --all-features --all-targets -- -D warnings: PASS
  • cargo test --all-features: 302/302 tests PASS (5/5 runs of signal_test::shutdown_message_on_stderr PASS)
  • cargo fmt -- --check: PASS
  • cargo audit: PASS
  • cargo deny check: PASS

Notes

  • v0.1.9 is a NON-BREAKING patch over v0.1.8. The only code change is writeln!(io::stderr(), ...)writeln!(io::stderr().lock(), ...) in src/main.rs.
  • See CHANGELOG.md for full details.

v0.1.8

05 Jun 19:26

Choose a tag to compare

Highlights

  • CI GitHub Actions fully green on all 3 OSes — Fixed two platform-specific race conditions that did not surface on macOS during local validation but blocked CI: ubuntu-latest (signal_test::shutdown_message_on_stderr exit 101) and windows-latest (atomic::create_backup_and_retention exit 1)
  • POSIX signal-safety complianceeprintln! removed from SIGINT/SIGTERM signal handlers per POSIX.1-2017 signal-safety(7). Shutdown message is now emitted by the main thread.
  • Windows backup fsync is now best-effort — New platform::fsync_file_best_effort logs a warning and continues when antivirus holds a read handle on %TEMP% files. Primary write path still uses strict fsync_file.
  • CI matrix pinned to windows-2025-vs2026 — Silences the windows-latest redirect NOTICE and prevents future runner changes from breaking the build.

Fixed (GAP 17 - signal handler POSIX safety)

  • signal_test::shutdown_message_on_stderr no longer fails on Linux CI — Removed eprintln! from the SIGINT and SIGTERM signal handlers. Per POSIX.1-2017 signal-safety(7), stdio functions are NOT async-signal-safe. Rust's std::io::stderr() uses a global Mutex that can deadlock or lose buffered output when the signal arrives while another thread holds the lock. On glibc Linux, the race was deterministic: stderr was empty. The shutdown message is now emitted by the main thread in src/main.rs when it observes is_shutdown() == true after atomwrite::run returns, which is the only async-signal-safe way to guarantee the message reaches the captured stderr pipe before the process exits. The Windows ctrlc path still emits the message inline because ctrlc handlers run in a normal thread.

Fixed (GAP 18 - Windows backup fsync)

  • atomic::tests::create_backup_and_retention no longer fails on Windows CI — Added platform::fsync_file_best_effort which logs a warning and continues. On Windows, antivirus products (Windows Defender, third-party AV) transiently hold a read handle on files in %TEMP% with FILE_SHARE_READ but without FILE_SHARE_WRITE, which causes FlushFileBuffers to return ERROR_ACCESS_DENIED (os error 5). Only the backup-durability fsync is best-effort; the primary write path still uses the strict fsync_file because the backup itself has already been created via fs::copy by the time fsync runs.

Fixed (CI matrix)

  • Pinned to windows-2025-vs2026 — The matrix entry for Windows was changed from windows-latest to windows-2025-vs2026 (its successor before the June 15 2026 GitHub-hosted runner migration). This silences the "windows-latest requests are being redirected" NOTICE and prevents unexpected runner changes from breaking the build.

Validation

  • cargo build --all-features: PASS
  • cargo clippy --all-features --all-targets -- -D warnings: PASS
  • cargo test --all-features: 302/302 tests PASS (all 5 signal_test cases pass; atomic::create_backup_and_retention passes)
  • cargo fmt -- --check: PASS
  • cargo audit: PASS (no vulnerabilities, no --ignore flag)
  • cargo deny check: PASS (advisories, bans, licenses, sources all OK)
  • cargo build --release: PASS
  • CI ubuntu-latest, macos-latest, windows-2025-vs2026: pending confirmation on push

Notes

  • v0.1.8 is a NON-BREAKING change. No public API was modified.
  • The signal-handler change is internal: external consumers that relied on the shutdown message appearing on stderr continue to see it; it is now emitted by the main thread instead of the signal handler.
  • The Windows backup fsync change is internal: backup files are still created and atomic; the only difference is that the durability flush for backup metadata is best-effort. If a future user reports data loss on backup, we can re-tighten the fsync.

See CHANGELOG.md for full details.