Close the loop: build, sanity-check, and publish #1

Merged
schaefera merged 7 commits from worktree-build-publish-pipeline into master 2026-09-18 10:56:36 +00:00
Collaborator

Summary

  • Implements the last unimplemented pipeline stage: PKGBUILD generation + makepkg (builder.rs), a post-build version sanity check (sanity.rs), and repo-add publishing (publisher.rs), wired into a tier 1-3 auto-publish path and a new tier 4-6 review queue (pkgwatch review / pkgwatch review <name> --approve).
  • Publishing targets the existing, already-registered local pacman repo at ~/.local/share/pacman/custom ([custom] in /etc/pacman.conf) rather than one pkgwatch invents. Stops at repo-add — installing/upgrading (pacman -Syu/pacman -S) stays a manual, deliberate step.
  • Added Package::binary_name after discovering scaleway-cli's real binary is scw, not scaleway-cli (confirmed via pacman -Ql against the installed extra package) — without it the build would install alongside extra's package instead of shadowing it.
  • Every upstream-controlled string (version, asset name, download URL) is validated before touching generated shell content in the PKGBUILD template.

Test plan

  • cargo make ci clean (fmt, clippy, complexity, coverage, audit) — 69 tests
  • Verified for real end to end: uv (tier 2) auto-built and published against the real astral-sh/uv release with no human step
  • scaleway-cli (tier 4) queued for review; pkgwatch review scaleway-cli --approve re-verified, built, and published it against a scratch repo — confirmed the built package contains exactly usr/bin/scw
  • Both packages landed in the real custom repo's database; uv was subsequently installed via pacman -S custom/uv and confirmed working (uv --version reports 0.12.15, matching upstream, vs. extra's stale 0.12.10)
  • scaleway-cli's real-repo review is intentionally left pending — that's a human decision, not this PR's

🤖 Generated with Claude Code

## Summary - Implements the last unimplemented pipeline stage: PKGBUILD generation + `makepkg` (`builder.rs`), a post-build version sanity check (`sanity.rs`), and `repo-add` publishing (`publisher.rs`), wired into a tier 1-3 auto-publish path and a new tier 4-6 review queue (`pkgwatch review` / `pkgwatch review <name> --approve`). - Publishing targets the existing, already-registered local pacman repo at `~/.local/share/pacman/custom` (`[custom]` in `/etc/pacman.conf`) rather than one pkgwatch invents. Stops at `repo-add` — installing/upgrading (`pacman -Syu`/`pacman -S`) stays a manual, deliberate step. - Added `Package::binary_name` after discovering scaleway-cli's real binary is `scw`, not `scaleway-cli` (confirmed via `pacman -Ql` against the installed `extra` package) — without it the build would install alongside `extra`'s package instead of shadowing it. - Every upstream-controlled string (version, asset name, download URL) is validated before touching generated shell content in the PKGBUILD template. ## Test plan - [x] `cargo make ci` clean (fmt, clippy, complexity, coverage, audit) — 69 tests - [x] Verified for real end to end: `uv` (tier 2) auto-built and published against the real astral-sh/uv release with no human step - [x] `scaleway-cli` (tier 4) queued for review; `pkgwatch review scaleway-cli --approve` re-verified, built, and published it against a scratch repo — confirmed the built package contains exactly `usr/bin/scw` - [x] Both packages landed in the real `custom` repo's database; `uv` was subsequently installed via `pacman -S custom/uv` and confirmed working (`uv --version` reports `0.12.15`, matching upstream, vs. extra's stale `0.12.10`) - [ ] `scaleway-cli`'s real-repo review is intentionally left pending — that's a human decision, not this PR's 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-09-17 09:20:32 +00:00
Close the loop: build, sanity-check, and publish for the first time
All checks were successful
CI / build (pull_request) Successful in 13m14s
CI / test (pull_request) Successful in 3m35s
CI / coverage (pull_request) Successful in 9m23s
CI / audit (pull_request) Successful in 13s
68fa648010
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).

Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.

Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.

Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.

Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.

69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera added 1 commit 2026-09-17 09:30:45 +00:00
Check the custom repo is registered in pacman.conf before building
All checks were successful
CI / build (pull_request) Successful in 1m22s
CI / test (pull_request) Successful in 3m50s
CI / audit (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 9m2s
6305743e4d
PR feedback: the local repo isn't guaranteed to exist on every box this
runs on, so it shouldn't just be assumed. publisher::ensure_registered
checks /etc/pacman.conf for an active [<repo_name>] section before a
build even starts, failing fast with the exact snippet to add if it's
missing — instead of spending several seconds on a makepkg build that
would succeed and then publish into a repo pacman never syncs from.

The repo directory and its database file were already self-healing
(publish creates the dir if missing, repo-add creates the db on first
run) — the actual gap was the pacman.conf registration, which can't be
made self-healing without root, so this fails loud with instructions
instead of trying to write to /etc/pacman.conf itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera added 1 commit 2026-09-17 09:42:21 +00:00
Add ARCHITECTURE.md and apply it to this PR's code
All checks were successful
CI / build (pull_request) Successful in 54s
CI / test (pull_request) Successful in 4m6s
CI / audit (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 6m19s
3f30e0acc6
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.

Applied it to this PR's own code:

- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
  approve out of main.rs into a new pipeline.rs. main.rs's own main() had
  grown to 278 lines and zero tests by treating "it's just the entry
  point" as an excuse to skip separating logic from wiring; now main.rs
  is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
  pending-state -> what to do), replacing dispatch logic that was
  previously inlined into a function that also made the real network/
  build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
  each stating that module's one job in a sentence, per the "deep
  modules" principle the spec argues for.

Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.

Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera added 1 commit 2026-09-17 10:06:43 +00:00
Fix issues from code review: shell-escaping gap, exit code, and more
All checks were successful
CI / build (pull_request) Successful in 51s
CI / test (pull_request) Successful in 3m35s
CI / audit (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 6m57s
f55188e36f
A single-agent code review of this branch's diff (builder/pipeline/
publisher/sanity/hash + main/config/fetcher/state/verifier changes)
found six real issues, all fixed here:

- builder.rs: validate_shell_safe only rejected a literal single quote
  and newline, written for the single-quoted PKGBUILD fields. But
  asset_name (via install_source) and binary_name land in the install()
  line, which is necessarily double-quoted so ${srcdir}/${pkgdir} can
  expand — where $, backtick, and backslash are still live. Not
  currently exploitable (the one variable component, version, is already
  independently constrained by validate_pkgver's strict charset), but a
  latent gap relying on that coincidence rather than the validator
  actually covering its real use context. Widened the reject-list to
  cover both quoting styles, added regression tests including one at the
  generate_pkgbuild level. Corrected SPEC.md's "single-quoted" claim to
  match.
- pipeline.rs: a verification failure returned Ok(()) from
  process_package, so run_check never counted it as a failure and the
  process exited 0 even on a failed cryptographic/attestation check —
  exactly the event a monitoring setup (systemd OnFailure=, cron
  mail-on-error) most needs a non-zero exit to catch. Now bails, which
  run_check already treats as a package failure. Added an integration
  test against a mocked GitHub server exercising this exact path.
- builder.rs: find_built_package hardcoded the .pkg.tar.zst suffix, so a
  box with a different PKGEXT in makepkg.conf would report a false
  "makepkg failed" for a build that actually succeeded. Widened to match
  any .pkg.tar.* compression. Added direct unit tests (it had none).
- pipeline.rs: a newer tier 4-6 version silently overwrote a still-
  unreviewed older pending version with no indication anything was
  superseded. Now says so explicitly.
- hash.rs: builder/verifier each read a whole downloaded artifact into
  memory via std::fs::read just to hash it, doubling peak memory for no
  reason since the file's already on disk. Added sha256_hex_file,
  streamed in fixed-size chunks; both callers switched to it.
- Deduplicated two near-identical test-only "write an executable shell
  script" helpers (publisher.rs, sanity.rs) into a shared
  src/test_support.rs.

75 tests (was 63), cargo make ci clean. Re-verified end to end against
the real astral-sh/uv release after all six fixes — build, sanity check,
and publish into a scratch repo all still succeed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera added 1 commit 2026-09-17 11:22:43 +00:00
Move high-level docs into docs/, matching doubleo7's convention
All checks were successful
CI / build (pull_request) Successful in 48s
CI / test (pull_request) Successful in 2m1s
CI / audit (pull_request) Successful in 9s
CI / coverage (pull_request) Successful in 3m53s
3e87282b21
SPEC.md and ARCHITECTURE.md were sitting at the repo root alongside
Cargo.toml/Makefile.toml/packages.d — moved both into docs/ (doubleo7
already does this for its own supplementary docs, so this matches an
existing convention in the fleet rather than inventing a new one).

Updated every doc-comment cross-reference across src/*.rs and
Makefile.toml (23 references) to the new docs/SPEC.md / docs/
ARCHITECTURE.md paths. The two files' own cross-references to each other
didn't need changing — they're still same-directory relative references.

Also updated the project reference memory pointing at ARCHITECTURE.md's
location, so it doesn't go stale pointing at a path that no longer exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera added 1 commit 2026-09-18 10:38:07 +00:00
Merge branch 'master' into worktree-build-publish-pipeline
All checks were successful
CI / build (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 3m58s
CI / audit (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 8m6s
14e1cee36f
schaefera added 1 commit 2026-09-18 10:46:36 +00:00
Add a third builder shape for archives with no wrapping directory
All checks were successful
CI / build (pull_request) Successful in 1m13s
CI / test (pull_request) Successful in 3m46s
CI / audit (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 7m27s
0044eda532
Merging master's claude-code.toml onto this branch surfaced a real gap:
builder.rs only knew "bare binary download" and "tarball extracting into
a same-named directory" (uv). claude-code's tarball extracts a bare
`claude` file with no wrapping directory, and that inner filename
doesn't match the package name either — makepkg's package() failed with
"cannot stat .../claude-linux-x64/claude-code" (confirmed by actually
running the build).

Add Package::archive_binary_path, an explicit override for the
in-archive path builder.rs installs from, used verbatim when present
instead of the stem/binary_name convention. Set binary_name = "claude"
too, matching the box's actual command name (/opt/claude-code/bin/claude)
rather than the claude-code package name. Also added a sanity_check
block (claude --version), matching uv's pattern, confirmed against the
real built binary's output ("2.1.276 (Claude Code)").

Verified end to end against the real repo with PKGWATCH_REPO_DIR
pointed at a scratch dir: check -> fetch -> verify -> review --approve
-> build -> sanity-check -> publish all pass for claude-code v2.1.276.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
schaefera merged commit d96b7a32f6 into master 2026-09-18 10:56:36 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: schaefera/pkgwatch#1
No description provided.