Skip to content

fix(runner): sequential fail_fast job count + README rewrite#11

Merged
leonardomso merged 16 commits into
masterfrom
leonardomso/accra
Apr 29, 2026
Merged

fix(runner): sequential fail_fast job count + README rewrite#11
leonardomso merged 16 commits into
masterfrom
leonardomso/accra

Conversation

@leonardomso

Copy link
Copy Markdown
Owner

Summary

Two changes: a bug fix in the sequential runner and a full README rewrite.

Bug fix: sequential fail_fast job count

The sequential executor skipped incrementing jobs_run when fail_fast triggered a break 'outer, jumping past the counter. A sequential hook with fail_fast = true that failed on its first job showed 0 run, 0 skipped in the summary -- even though the job clearly ran and failed.

The parallel executor already counted failed jobs correctly (incrementing before checking failure), making the two code paths inconsistent.

Fix: Move the increment after the commands loop and break the outer loop only after counting. Now both executors report the same way.

Before: FAIL -- 0 run, 0 skipped
After: FAIL -- 1 run, 0 skipped

README rewrite

The old README read like AI-generated marketing copy. The new one:

  • Opens with a one-liner that says what it is
  • Explains the problem in plain language: why worktree hooks break under parallel agents
  • Explains each feature in terms of the problem it solves, not as a feature checklist
  • Adds a "How it works" walkthrough that calls out the key insight (step 3: worktree dispatch)
  • Moves the comparison table to the end as reference
  • Removes generic Conductor mentions that were confusing without context
  • Shorter overall (300 lines vs 392), denser information per line

Also cleaned up Conductor references in intro and quickstart docs (the dedicated agents/conductor.mdx integration guide is untouched).

What changed

  • apps/betterhook/src/runner/executor.rs -- fix sequential fail_fast counting + update test assertion
  • README.md -- full rewrite
  • apps/docs/introduction.mdx -- remove generic Conductor mention
  • apps/docs/quickstart.mdx -- remove generic Conductor mention

Validation

cargo build --workspace        # pass
cargo test --workspace         # 608 tests pass
cargo clippy --workspace ...   # clean
cargo fmt --all -- --check     # clean

End-to-end tested all CLI commands against a real git repo (63 manual tests covering init, install, uninstall, run, explain, fix, status, doctor, cache, builtins, import, worktree isolation, DAG scheduling, fail_fast, streaming output, error handling).

The sequential executor's run_sequential skipped incrementing jobs_run
when fail_fast triggered, because break 'outer jumped past the counter.
The parallel executor already counted failed jobs correctly, making the
two paths inconsistent.

Move the increment after the commands loop and break the outer loop
only after counting, matching the parallel executor's behavior. Before
this fix, a sequential fail_fast run showed '0 run, 0 skipped' even
though a job clearly ran and failed.
Rewrite README.md from scratch with a human tone: lead with the
problem (worktree hooks breaking under parallel agents), explain
what betterhook does in terms of the problems it solves, add a
plain-language 'How it works' walkthrough, and move the comparison
table to the end as reference material.

Remove generic Conductor mentions from the README, introduction,
and quickstart pages. The dedicated agents/conductor.mdx integration
guide remains for users of that tool.
Release automation:
- release-plz workflow opens a release PR on every master push with
  auto-generated changelog and version bump from conventional commits
- release.yml now publishes both crates to crates.io after creating
  the GitHub release (library first, CLI after a 30s index delay)
- release-plz.toml configures per-crate changelog, tag, and publish
  settings (xtask excluded from publish)
- release.toml updated to enable crates.io publish

Dependency automation:
- Replace Dependabot with Renovate for Cargo and GitHub Actions deps
- Weekly grouped PRs with auto-merge on minor/patch updates
- renovate.json with semantic commit prefixes

Crates.io metadata:
- Add readme field to workspace and both crate Cargo.toml files
- Add exclude for tests/benches/fuzz in the library crate
- Fix CLI crate description (remove double-dash)
…rivilege permissions

Security:
- Pin every GitHub Action to its full SHA digest with a version
  comment (checkout, rust-toolchain, rust-cache, upload-artifact,
  download-artifact, action-gh-release, release-plz/action). Prevents
  supply-chain attacks via mutable tags.
- Add cargo-deny to CI: checks RustSec advisories for known vulns,
  enforces an allow-list of licenses (MIT, Apache-2.0, BSD, ISC,
  Unicode), blocks wildcard version requirements, and rejects deps
  from unknown registries or git sources.
- Scope workflow permissions to least-privilege: CI gets read-only
  contents, release-plz gets per-job pull-requests:write or
  contents:write, release gets contents:write only.
- Add CODEOWNERS file.

Release pipeline:
- Split release-plz into two jobs: release-pr (opens version-bump PR)
  and release (tags + publishes to crates.io on merge). Previously
  only release-pr ran, so tags and crates.io publish never happened.
- Tar and gzip release binaries (.tar.gz) instead of uploading raw
  executables. Checksums cover the archives.
- Remove the manual crates.io publish job from release.yml since
  release-plz now handles it.

Renovate:
- Enable vulnerability alerts via :enableVulnerabilityAlerts preset.
- Enable helpers:pinGitHubActionDigests so Renovate auto-updates
  action SHA pins going forward.
- Add weekly Cargo.lock refresh via lockFileMaintenance rule.
Add an npm publish job to release.yml that runs after the GitHub
Release is created. On every tag push:
1. Build job compiles binaries for 4 targets
2. Release job creates the GitHub Release with tar.gz archives
3. npm job syncs the version from the git tag and publishes the
   betterhook package to the npm registry

The npm package is a thin wrapper (packaging/npm/) that downloads
the correct platform binary from GitHub Releases on postinstall.

Changes to the npm wrapper:
- install.js now downloads .tar.gz archives (matching the new release
  format) and extracts them with tar
- Native binary stored as bin/betterhook-native to avoid overwriting
  the JS shim at bin/betterhook
- JS shim checks for the native binary and gives a clear error if
  it is missing
- setup-node action pinned to SHA
Replace the NPM_TOKEN secret with OIDC-based trusted publishing.
GitHub Actions proves its identity directly to npm via an id-token,
so no long-lived secrets are stored or rotated.

Changes:
- Add id-token: write permission to the npm job
- Add contents: read (least privilege)
- Update npm to latest for OIDC support (requires npm >= 11.5.1)
- Publish with --provenance flag for supply chain attestation
- Remove NODE_AUTH_TOKEN / NPM_TOKEN entirely
Four action SHAs were invalid (not found in their repos):
- Swatinem/rust-cache: fixed to e18b497 (v2.9.1)
- actions/upload-artifact: fixed to 043fb46 (v7.0.1)
- softprops/action-gh-release: fixed to 3bb1273 (v2.2.1)
- release-plz/action: fixed to 1528104 (v0.5)

All SHAs verified against their respective repos via the GitHub API.
Three CI failures:

1. security-audit: deny.toml used the deprecated 'unlicensed' key
   (removed in cargo-deny 0.16+). Removed it; modern cargo-deny
   denies unlicensed crates by default.

2. msrv-check (Linux): installed_wrapper_handles_empty_git_dir_env
   panicked with ETXTBSY. The wrapper file was written with
   std::fs::write then immediately exec'd. On Linux under load the
   kernel can return 'Text file busy' if the write fd is not fully
   synced. Fixed by using File::create + sync_all + explicit drop
   before chmod+exec.

3. check-macos-latest: exclude_filter_blocks_events panicked because
   macOS FSEvents delivered the directory-creation event before the
   watcher started filtering. Added a settle delay after mkdir and
   a registration delay after starting the watcher.
- Add CC0-1.0 to the license allow list (used by notify v8.2.0,
  a permissive public-domain-equivalent license)
- Ignore RUSTSEC-2025-0141 (bincode unmaintained). bincode 2.0.1 is
  feature-complete for our daemon wire protocol. Will evaluate
  postcard or bitcode when the protocol changes.
Shell completions:
- Add clap_complete dependency and a 'betterhook completions <shell>'
  subcommand that generates completions for bash, zsh, fish, elvish,
  and powershell
- Add a completions job to the release workflow that builds the CLI,
  generates bash/zsh/fish completions, and includes them in the
  GitHub Release as betterhook-completions.tar.gz

README install instructions:
- Add a proper Install section with three methods: cargo install from
  crates.io, npm install -g, and from source
- Add a Shell completions subsection with copy-paste commands for
  bash, zsh, and fish, plus a note about pre-built completions in
  GitHub Releases
- Add the completions command to the commands table
TTY path:
- Replace println!/eprintln! (lock-per-call) with explicit
  stdout().lock() / stderr().lock() + write!/writeln!. Each event
  now acquires the stdio lock once instead of once per format arg.
- Write colored fields directly via Display into the locked writer
  instead of .to_string() intermediates. Removes one heap allocation
  per colored field per event.
- Eliminate format!() intermediates for diagnostic locations and
  severity strings by writing fields sequentially.

JSON path:
- Replace serde_json::to_string + println! with
  serde_json::to_writer directly into locked stdout. Removes the
  intermediate String allocation on every NDJSON event.

Under 8 parallel jobs each producing hundreds of lines, this cuts
the per-line overhead from ~3 allocations + 2 lock cycles to
0 allocations + 1 lock cycle.
Visual improvements:
- Add a hook header line ('pre-commit | 3 jobs, parallel') so users
  know which hook is running and its execution mode
- Move the command to a separate indented line below the job name,
  dimmed, so the job name stands out
- Indent output lines with 4 spaces for visual grouping under their
  job's start marker
- Hide 'exit 0' on success (noise). Only show exit code on failure
  where it's actionable
- Human-readable durations: '42ms', '1.5s', '2m 30s' instead of
  raw milliseconds
- Replace '∘' skip marker with '○' (more visible at small sizes)
- Show 'cached (N files)' instead of 'cache hit (N files)' for
  brevity
- Cleaner summary: 'PASS  3 run  212ms' on success, 'FAIL  1 run'
  on failure. Skip count only shown when nonzero
- Add blank lines around the header and summary for breathing room

New event:
- OutputEvent::HookStarted { hook, jobs, parallel } emitted at the
  start of run_hook, before job resolution. Gives the TTY writer
  enough context to render the header line.
P1 fixes:
- release-plz release job: add pull-requests: read permission so
  release-plz can inspect PR metadata when tagging
- CLI Cargo.toml: add version = "0.0.2" alongside path dep so
  cargo publish accepts the crate (path-only deps are rejected)
- npm install.js: enforce HTTPS-only redirects with a max redirect
  count, resolve relative Location headers via URL constructor,
  replace execSync tar with in-process extraction using Node zlib
  (no external tar binary dependency)

P2 fixes:
- CI cargo-deny: run all checks (advisories, licenses, bans,
  sources) not just advisories+licenses. The bans and sources
  policies in deny.toml were never evaluated.
- Renovate: add top-level lockFileMaintenance.enabled block. The
  packageRule alone does not enable the feature.

P3 fixes:
- README: remove duplicate 'Your next git commit' sentence in the
  quickstart section
bitflags (v1 + v2) and unicode-width (v0.1 + v0.2) are pulled in
by notify and miette respectively. We cannot eliminate these
duplicates without dropping the dependencies. Changed
multiple-versions from 'warn' to 'allow' so the bans check passes.
xtask (publish = false) depends on betterhook via path only without
a version constraint. cargo-deny flags this as a wildcard. Set
allow-wildcard-paths = true so private crates can use versionless
path deps without triggering the wildcard lint.
@leonardomso leonardomso merged commit 7180ea5 into master Apr 29, 2026
5 checks passed
@leonardomso leonardomso deleted the leonardomso/accra branch April 29, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant