Skip to content

♻️ refactor(repo): cut two dependencies, one triplicated delegate, and the seams with no production side - #73

Merged
Misery7100 merged 6 commits into
mainfrom
refactor/ponytail-audit
Aug 28, 2026
Merged

♻️ refactor(repo): cut two dependencies, one triplicated delegate, and the seams with no production side#73
Misery7100 merged 6 commits into
mainfrom
refactor/ponytail-audit

Conversation

@Misery7100

@Misery7100 Misery7100 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

A repo-wide audit for over-engineering, applied. Two dependencies go back to the
standard library, twenty-seven identical forwarding methods across three backup
transports become one delegate, and the exported seams that no production path
ever reached are closed. No behaviour changes.

Closes #
RFC: none — no design decision changes. RFC 0005 names renameio/v2 in a dated
"what is present" snapshot as one of three reasons the language version is
1.25.0; x/term and x/sys still require it, so the conclusion holds. That
record is left as written rather than edited after the fact.

Type

  • Feature
  • Fix
  • Refactor (no behaviour change)
  • Docs
  • Chore / CI / dependencies
  • Breaking change

What changed

gofrs/flocksyscall.Flock one non-blocking attempt; the cancellable poll was already written here
renameio/v2os.CreateTemp + rename its one documented gap — fsyncs the file, not the directory — was already compensated at the call site
blob.Delegate localdir/sftp/s3 keep only the three methods with a reason to wrap a call
Scripted, Collectortest/fakes 216 lines of test double out of the shipped tree
5 seams unexported behind export_test.go sopsage ×2, systemd, health waiter
compose.WithDockerBinary, Registry.Available, atomicfs.ReadFileIn deleted no production caller ever existed
slices.Sorted, slog.DiscardHandler, ui.Truncate three hand-rolled stdlib copies

−280 lines from internal/ + cmd/, −2 direct dependencies.

Two of these are worth a reviewer's eye rather than a skim:

atomicfs.ReadFileIn was the only thing asserting root containment. Its
test was the sole place cleanRel's escape refusal was checked — through a
function nothing outside that test called — while WriteFileIn, which renders a
secret into a release directory, had no such test. The test now drives
WriteFileIn. Sabotaged cleanRel to confirm it fails, and it fails naming
ErrPathEscape rather than a syscall error, which is the distinction cleanRel
exists to make.

events.StepOutput was a constructor only tests called while root.go built
the same event as a struct literal. The literal now calls the constructor, so
the shape the tests assert on is the shape production emits.

Design

  • Behaviour matches the accepted RFC
  • The RFC is amended in this PR, because the design changed

Lifecycle invariants

No steps added or changed; the plan, journal and recovery paths are untouched.

  • Every new step appears in --dry-run plan output — n/a, no new steps
  • Every new step is journaled and verifiable after the fact — n/a
  • Failure mid-step leaves a recoverable installation; re-running converges — unchanged
  • An undo path exists — unchanged

Compatibility

  • Manifest schema (schemas/) unchanged
  • Schema changed
  • Hook ABI unchanged, or versioned and documented
  • New or changed exit codes are in the reference docs — none

Secrets & safety

  • No secret value reaches stdout, stderr, the journal, or an error message
  • Destructive paths confirm first, or take a backup first — unchanged
  • Any new external tool or version requirement is declared — none added

Both rewritten primitives are on secret-handling paths, so specifically:
WriteFile still chmods before the rename (os.CreateTemp opens at 0600, so the
window is narrower than the final mode until this widens it) and now fsyncs
before the rename rather than after — a rename landing ahead of its contents
would leave the old name pointing at zeros. The lock releases with an explicit
LOCK_UN before the close, so a forked descriptor cannot outlive the release.

Verification

  • just ci green
  • just demo, just demo-plan, just demo-recovery still pass
  • The acceptance run against real Docker exercises this path

What I ran and what it proved:

Every gate in the ci recipe, run individually: fmt-check, vet,
darwin-check (amd64 + arm64 build and vet), lint (golangci-lint, 0 issues),
shellcheck, runtime-check (17 known mentions, 0 runtime branches),
docs-check (41 pages, 56 checks, no drift), log-check, the strict contract
run (no suite skipped), test-race, and coverage-gate — 87.0% against an 84%
floor. go vet -tags docker ./... too, since a deletion that only breaks a
tagged file passes an untagged gate. The acceptance scenario passes against real
Docker, which exercises the rewritten atomicfs.WriteFile and the lock on every
step; it does not exercise the backup-target delegate.

The container lane does not pass on my machine, and does not pass on main
either.
Nine failures, all 120s timeouts, two environmental causes:
test/installer needs a daemon sharing the host network namespace and says so
itself; the five minisign suites apk add minisign inside alpine:3.20, and
this host's containers cannot reach the Alpine CDN (Permission denied fetching
the index, reproduced with a bare docker run). apk fails, &&
short-circuits, and require.NoError then prints "the real minisign rejected our
signature" for what is a failed package install. I confirmed by running two of
them on main: identical failures, identical timings. So the S3, SFTP and
volume-capture suites that would exercise the new delegate against real
containers are unverified locally — CI is the first place they run.

Risk & rollback

The two rewritten primitives are the ones a bad merge would hurt most: every
atomic write and the lock that stops two concurrent deployments. A defect in
WriteFile corrupts state files on a crash rather than immediately, and a
defect in the lock allows two mutating operations at once. Both are covered by
existing unit tests and the acceptance run, but neither failure mode is one a
green suite proves absent.

Rollback is git revert of the five commits and go mod tidy; nothing on disk
changes format, so an operator on the old binary reads everything this one wrote.


CodeAnt-AI Description

Reduce runtime dependencies while preserving backup, locking, and file-safety behavior

What Changed

  • Backup targets now share the same operation handling while retaining transport-specific behavior, including local directory creation rules and S3 bucket checks.
  • Atomic file writes and deployment locks now use the standard library without changing their durability, exclusivity, or cancellation behavior.
  • Test-only helpers and configuration seams are kept out of production APIs, while tests now verify path containment on the writes used in production.
  • Terminal truncation uses the shared UI behavior, and event publishing uses the standard event format.

Impact

✅ Fewer production dependencies
✅ Cancellable deployment lock waits
✅ Safer atomic writes and root-contained file writes

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.


Summary by cubic

Refactors the repo to cut two dependencies and ~280 lines: gofrs/flock and renameio/v2 go back to the standard library, three backup transports share one delegate, and test-only seams are closed. It also fixes a regression the lock rewrite introduced — with a free lock, a cancelled --wait acquisition took the lock and went on to mutate the installation.

Attention areas

  • The rewritten atomic write syncs before rename and the lock releases with an explicit LOCK_UN before close; both sit on secret-handling paths.
  • The lock regression only showed when the lock was free; the fix checks the context before the first attempt, and the new test leaves the lock free.
  • The root-containment assertion moved from the unused ReadFileIn to WriteFileIn, which is what production actually calls.
  • The container lane fails locally on both this branch and main, so S3, SFTP, and volume-capture suites are unverified until CI runs them.

Written for commit bb2927a. Summary will update on new commits.

Review in cubic

Misery7100 and others added 5 commits August 27, 2026 22:04
… the standard library

gofrs/flock wrapped one non-blocking flock(2) attempt, and the polling that
makes waiting cancellable was already written here rather than taken from it.
renameio wrapped a temporary file, a chmod and a rename, and its one documented
gap -- it fsyncs the file but not the directory -- was already compensated for
at the call site.

Both are now what they wrap. The lock keeps its own descriptor and releases it
explicitly before the close, so a forked descriptor cannot outlive the release.
The atomic write syncs before the rename rather than after, which is the
ordering renameio was providing.

Also: slices.Sorted for a hand-written insertion sort, slog.DiscardHandler for
a hand-written one, and ui.Truncate for the tty renderer's own copy -- which
was not ANSI-aware, so it would have cut a styled cell mid-escape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts had written out identically

Every store-backed backup target implemented Push, List, Fetch, FetchFile,
Verify, Remove, PutObject, ObjectKeys and GetObject the same way: open the
store, call the function in blob that knows what a backup is. Three copies of
twenty-seven methods, identical right up until one of them would not have been.

What actually differs is how the store is opened, so that is what an adapter
supplies now. Two openers rather than one, because the halves differ for a
reason that is not reading versus writing: S3 probes the bucket for the backup
half and deliberately skips the probe for the object half, where a missing
prefix is the ordinary state before the first publish.

Embedded rather than called, so the three methods with a reason to wrap a call
keep it: localdir refuses to push a directory onto itself before its root is
created, localdir and sftp each tidy an emptied directory afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scripted is a Runner whose replies are written in advance, and Collector is a
Sink that records what an operation announced. Neither is reachable from any
main; both sat in internal/infra/exec and internal/events, where they read as
production code. Collector's own comment claimed the JSON presenter as a user,
which stopped being true without the comment noticing.

They now live in test/fakes with the other doubles. Nothing shipped changes --
the linker had already dropped both -- but the packages that hold the real
Runner and the real Bus no longer also hold a fake one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five options and two helpers were exported API that no production path reached.
An option only a test passes is an option no test exercises as production
leaves it, which is the shape a defect hides in.

Unexported behind an export_test.go, where the test lives in the same
directory: sopsage's sops path and clock, systemd's systemctl path, the health
waiter's clock. WithUnitDir stays exported -- the suites in test/ install real
units into a temporary directory and cannot reach an export_test.go -- and so
do the https, oci and imagepack seams, for the same reason.

Deleted outright: compose.WithDockerBinary, whose two callers wanted the
default anyway, and Registry.Available, which no caller ever wanted -- every
one of them wanted the error Lookup returns.

atomicfs.ReadFileIn goes too, and its test moves to WriteFileIn. That test was
the only place the root-containment refusal was asserted, and it was asserting
it through a function nothing calls, while the function that renders a secret
into a release directory had no such test. Same rule, now guarded where it runs.

events.StepOutput was a constructor only tests called, while the one place that
publishes the event built it as a struct literal. The literal now calls the
constructor, so the shape the tests assert on is the shape production emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs-check caught the rename: the filesystem-containment table pointed at
TestReadFileInReadsAndRefuses, which went with the function it was testing.
The claim is the same one, asserted through WriteFileIn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codeant-ai

codeant-ai Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 69a13a5 Aug 27, 2026 · 20:52 20:56

@codeant-ai

codeant-ai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 27, 2026
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a1d68b3f-70c4-4f96-93ee-611d4394fd1e

📥 Commits

Reviewing files that changed from the base of the PR and between 69a13a5 and bb2927a.

📒 Files selected for processing (2)
  • internal/infra/lock/lock.go
  • internal/infra/lock/lock_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change centralizes blob target operations, narrows production APIs to test-only seams, migrates tests to shared fakes, replaces filesystem and lock implementations, and simplifies CLI, event, logging, and TTY helpers.

Changes

Adapter and infrastructure refactor

Layer / File(s) Summary
Test-only seams and shared fakes
internal/adapters/health/*, internal/adapters/secrets/sopsage/*, internal/adapters/supervisor/systemd/*, internal/adapters/runtime/compose/*, internal/infra/tools/*, internal/lifecycle/engine/*, internal/ui/tty/*
Production option constructors are unexported. Test-only exports provide clock, binary-path, systemctl, watch-model, and event-message access. Tests use test/fakes runners and collectors.
Blob target delegation
internal/adapters/target/blob/*, internal/adapters/target/localdir/*, internal/adapters/target/s3/*, internal/adapters/target/sftp/*
blob.Delegate now handles backup and object operations. Local, S3, and SFTP targets embed and configure the delegate.
Atomic file writes
internal/infra/atomicfs/*
WriteFile now manages temporary-file creation, synchronization, rename, and cleanup directly. ReadFileIn and its test are removed.
File locking
internal/infra/lock/*
Lock acquisition now uses syscall.Flock, polling, context cancellation, and explicit unlock handling.
CLI and presentation cleanup
internal/cli/*, internal/events/*, internal/infra/logging/*, internal/ui/tty/*
Completion sorting uses standard helpers. Step output uses its constructor. The event collector and custom discard handler are removed. TTY truncation uses ui.Truncate.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to bb292

This refactor replaces two dependencies with standard-library implementations and consolidates backup delegation without a demonstrated behavior change; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 63 functions across 26 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main refactor: removing two dependencies, consolidating duplicated delegation, and closing production-unused seams.
Description check ✅ Passed The description is detailed and covers the required summary, type, design, lifecycle, compatibility, safety, verification, risks, and rollback. It also records known verification limits and explains w…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed and covers the required summary, type, design, lifecycle, compatibility, safety, verification, risks, and rollback. It also records known verification limits and explains why they are environmental.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/ponytail-audit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread internal/infra/lock/lock.go
Comment thread internal/cli/root.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 27, 2026
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

The dependency this replaced checked the context before its first attempt, in
tryCtx, and the replacement only checked it in the poll. With the lock held that
is invisible -- the waiter reaches the select and is answered there, which is
why the existing cancellation test passed either way. With the lock free the
first attempt succeeds, so a command the operator had already interrupted took
the deployment lock and went on to mutate the installation.

The waiting path only, which is the parity that was lost. A non-waiting
acquisition never consulted the context and still does not: one syscall that
either succeeds or names the holder, with nothing to interrupt.

The test leaves the lock free, which is the arrangement no existing test had.
It fails on this branch before this commit and passes on main, which is what
makes it a regression rather than a gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Misery7100

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Misery7100
Misery7100 merged commit 1996f47 into main Aug 28, 2026
17 checks passed
@Misery7100
Misery7100 deleted the refactor/ponytail-audit branch August 28, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant