pkgwatch/docs/SPEC.md

522 lines
29 KiB
Markdown
Raw Normal View History

# pkgwatch — declarative package-update watcher/publisher (working name)
Status: design draft, pre-PoC. Captures the design discussion as of 2026-09-11.
Add ARCHITECTURE.md and apply it to this PR's code 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>
2026-09-17 09:42:18 +00:00
This is the *product* design — what pkgwatch does and why. For how the
code implementing it is organized (module boundaries, testability
conventions, what CI does and doesn't enforce), see `ARCHITECTURE.md`.
## Problem
Software not packaged by the distro (Arch/Manjaro here) usually gets installed
one of a few ways:
- `curl | sh` from the vendor's own install script — the classic "trust me,
bro." The script and any checksum it embeds share a trust boundary, so it
verifies nothing beyond transport corruption.
- Manual download + manual checksum/signature verification, redone by hand
every time you want to update. Tedious enough that people stop doing it.
- A distro package (pacman `extra`, AUR) — trustworthy, but version-lagged
behind upstream, and someone else has to maintain the PKGBUILD.
There's no local, low-effort way to say "here's how to fetch and verify
package X" once, and have that declaration stay live — checked periodically,
re-verified on every new upstream release, and fed into a normal
`pacman`-based workflow without hand-editing a PKGBUILD each time.
## Scope
This is a personal tool for a small, curated list of non-critical packages
— not a general-purpose supply-chain-security framework. The concrete
motivating case: software (especially fast-moving AI/ML tooling) that
Manjaro's `extra` repo lags weeks behind upstream on, where raw AUR or a
vendor's curl|sh script are the only faster alternatives today.
**Goal**: a middle ground — fresher than Manjaro's lag, with real
verification where the vendor actually offers something to check, safer
than blindly piping an install script to `sh`.
**Non-goals**:
- Defending against a fully compromised vendor signing/release pipeline.
If upstream's CI or signing key is itself compromised, pkgwatch cannot
and does not try to catch that. Tiers 13 (below) raise the bar from
"trust the domain" to "trust the vendor's actual release process"; they
are not a guarantee against that process being subverted.
- Defending against a malicious downgrade specifically. A compromised
version-check source reporting an older version as "latest" is a
downstream/distro-security problem, out of scope here. (See "post-build
version check" below for a related but distinct sanity check — it is
not a security control.)
- Supporting an adversarial or multi-user config. The tracked-package list
is curated by the one person running the daemon on their own machine;
config/state file integrity relies on normal filesystem permissions, not
a hardened trust boundary. Key-pinning friction on rotation (see tiers
below) is acceptable, even desirable, regardless of list size — it's
about who's allowed to add a trust decision, not about volume.
This is a narrower non-goal than "doesn't need to scale" — see **Scaling to
many packages**, below. If pkgwatch actually solves the release-cadence
problem, the natural outcome is tracking many packages, not a handful, and
the design should hold up under that.
## Vision
A Rust binary, run as a systemd service (service + timer, periodic not
persistent), that:
1. Reads a declarative config of tracked packages — where to check for new
versions, how to fetch the artifact, and how to verify it.
2. On each run, checks each tracked package for a new upstream version.
3. If a new version is found, fetches the artifact and runs the verification
method declared for that package.
4. If verification succeeds (per the package's trust tier — see below),
updates/generates a local PKGBUILD (bump `pkgver`, refresh
`sha256sums`/signature reference) and rebuilds it into a local pacman
repo via `makepkg` + `repo-add`.
5. The next `pacman -Syu` (with the local repo configured) picks up the new
version normally — no separate tooling needed on the consuming side.
This is conceptually `nvchecker` (version checking) + `updpkgsums` (checksum
refresh) + `repo-add` (local repo publishing), fused into one daemon with a
single declarative source of truth, plus an explicit, surfaced trust model
that those tools don't provide.
## Verification trust tiers
Per the Scope above, the goal is to raise the bar above curl|sh where the
vendor gives us something to check — not to build airtight supply-chain
defense. The central design problem within that goal: from the install UX,
a cryptographically strong verification and a "trust-me-bro" same-domain
checksum look identical. The tool's job is to make that difference legible
instead of laundering every package into an undifferentiated "verified"
bucket.
Tiers, strongest to weakest:
1. **Pinned-key signature** — GPG/minisign/sigstore-cosign, where the
signing key's fingerprint is pinned in *our* config (not fetched fresh
from the vendor each time). Proves authorship, independent of the
artifact's own hosting.
2. **Build provenance attestation** — GitHub/GitLab attestations, SLSA
provenance. Ties the artifact to a specific CI run and source commit.
Strong, but only as trustworthy as that CI pipeline.
3. **Registry-native signing** — crates.io, PyPI trusted publishing, npm
provenance. Similar strength, scoped to that registry's trust model.
4. **Same-origin checksum file** — a `.sha256`/`.sha256sum` served next to
the artifact by the vendor. Proves transport integrity only. If the
vendor's server or account is compromised, the attacker controls the
artifact and the "verification" in the same move.
5. **Checksum embedded in an install script** — the classic curl|sh case.
The verifier and the thing being verified share a trust boundary.
6. **Nothing** — bare TLS to a domain, no checksum or signature at all.
### Automation posture per tier
This is the load-bearing decision, not just a cosmetic label:
- **Tiers 13**: a passing verification is a real trust signal. Safe to
auto-bump, auto-verify, auto-publish unattended.
- **Tiers 46**: a passing "verification" only proves internal consistency
of one origin (the checksum and the artifact agree), which tells you
nothing about whether that origin was compromised. For these tiers the
tool should **not** treat a pass as "verified, ship it." Instead: treat a
version/hash *change* as a flag-for-human-review event. The value pkgwatch
adds at these tiers is diff-and-alert (notice something changed, surface
the new hash for a human to look at), not verify-and-trust.
The post-build version sanity check (see Architecture below) runs
regardless of tier — it's a correctness gate on the build itself, not part
of the trust-tier judgment, and doesn't change this tiering.
### Surfacing trust, not just gating on it
- Every tracked package carries an explicit tier + one-line justification
(e.g. "minisign, key pinned 2024-03" vs. "same-domain sha256, no
independent signer") in a metadata file alongside the generated PKGBUILD —
something `repo-add`/pacman don't touch, but that a human or `pkgwatch
audit` can read.
- `pkgwatch audit` (or similar) lists all tracked packages sorted
worst-tier-first, so weak links don't hide among strong ones in a repo
that otherwise looks uniformly trustworthy.
## Scaling to many packages
At a handful of tracked packages, several design choices above are
invisible non-issues. At dozens, they become real:
- **Review-queue fatigue.** Most real-world packages will land in tiers
46 (bare GitHub release, no signing, is the common case, not the
exception). If every release of every tracked package produces one
review-and-approve event, the queue turns into something rubber-stamped
to clear it — which degrades the already-weak tier 46 review into pure
theater, worse than the single-package case. Mitigation: batch same-tier,
low-signal changes (patch-version bump, no maintainer/key change) into a
digest, and reserve individual review prompts for changes that look more
significant (new signing key, jump of more than one minor version, new
maintainer/publisher identity where that's knowable).
- **Per-source check cadence, not one global interval.** The original
motivating problem — some software moves weekly, some quarterly — argues
against checking everything on the same timer tick. See "Check method"
below for the GitHub-specific answer; other source types likely want an
explicit fast/normal/slow interval field per package rather than one
daemon-wide interval.
- **Config as a directory, not one file.** `packages.d/*.toml` (one file
per package), loaded as a directory — same convention as
sudoers.d/systemd drop-ins — scales better than a single growing TOML
file: easier to add/remove/diff one package, plays nicer with putting
the config itself under version control.
- **`pkgwatch audit` becomes core, not peripheral.** At 3 packages you
remember the trust tiers by heart. At 30 you don't. The audit/surfacing
command (see "Surfacing trust," above) is what keeps the weak tiers safe
to have around at all once the list is too big to hold in your head.
- **Local repo retention.** `makepkg`/`repo-add` output accumulates. Needs
a "keep last N versions per package" prune step, or disk fills quietly
over time.
- **Rate limits become real.** Enough GitHub-sourced packages checked on
the same schedule can hit unauthenticated API limits — argues for an
optional auth token in config, and/or staggering check times across
packages rather than firing every check on the same tick.
- **Template reuse matters more.** Already an open schema question below,
but at scale "a small fixed set of parameterized PKGBUILD shapes" clearly
wins over "bespoke template per package" on maintenance-burden grounds
alone, not just taste.
### Check method: polling vs. push (GitHub specifically)
True server-initiated push isn't available for repos you don't own —
GitHub webhooks require admin access on the repo being watched, which
rules them out for upstream projects you're only consuming. A third-party
relay (e.g. newreleases.io) could convert this into a webhook on your end,
but that requires a publicly reachable HTTPS receiver on this box, which
cuts against the existing WireGuard-only/no-public-SSH posture for real
inbound exposure and a purely cosmetic latency win — refreshing a local
pacman repo doesn't need sub-minute notification.
The practical middle ground, outbound-only:
- **`github-atom` check method**: poll `https://github.com/<owner>/<repo>/releases.atom`.
Public, unauthenticated, and (worth reconfirming at implementation time)
historically not counted against the REST API rate limit — cheap enough
to poll every few minutes, getting close to push-latency for the
GitHub-hosted slice of tracked packages without any inbound exposure.
- **`github-api` check method**: for anything the Atom feed doesn't cover
(asset-level metadata, attestations), use conditional GETs
(`If-None-Match`/ETag) against `api.github.com` — a `304 Not Modified`
historically didn't consume rate-limit quota either, so frequent polling
stays cheap even on the real API.
- Non-GitHub sources still need the per-package interval field above;
there's no equivalent free-to-poll feed for most of them.
## Config schema (draft)
```toml
[package.uv]
source = "github-release"
repo = "astral-sh/uv"
asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz"
check_method = "github-atom" # or "github-api"; see Scaling > Check method
check_interval = "5m" # per-package, not a global daemon interval
# Verified 2026-09-11 against the real repo: uv publishes GitHub
# build-provenance attestations (sigstore bundle) for every release asset
# — tier 2, not the tier-4 same-origin-sha256 originally guessed here.
# Checked via `gh attestation verify` rather than reimplementing sigstore
# verification in Rust. Tier is *derived* from `method`, not stored
# separately — the PoC found that storing both invites a tier/method
# mismatch that would mean nothing (see `Verification::tier()` in the PoC).
[package.uv.verification]
method = "github-attestation"
# Post-build sanity check — correctness only, not a security control.
# Runs the built binary and confirms it reports the version pkgwatch
# believes it just built; mismatch blocks publish.
[package.uv.sanity_check]
command = "uv --version"
version_regex = 'uv (\d+\.\d+\.\d+)'
# Tier 4 example — same-origin checksum only, proves transport integrity,
# not authorship (this is what the uv example above was, until checked):
[package.otherpkg]
source = "github-release"
repo = "someorg/otherpkg"
asset_pattern = "otherpkg-x86_64-unknown-linux-gnu.tar.gz"
[package.otherpkg.verification]
method = "same-origin-sha256"
checksum_asset_pattern = "otherpkg-x86_64-unknown-linux-gnu.tar.gz.sha256"
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
# binary_name: only needed when the installed binary's name differs from
# the pacman package name — e.g. real-world case, scaleway-cli's package
# is named scaleway-cli but its actual binary is `scw` (see
# packages.d/scaleway-cli.toml). Defaults to the package name.
binary_name = "otherbin"
# Tier 1 example — not yet implemented in the PoC (only
# same-origin-sha256 and github-attestation exist so far):
[package.somepkg]
source = "url-with-version-regex"
url = "https://example.com/downloads/"
version_regex = 'somepkg-(\d+\.\d+\.\d+)\.tar\.gz'
[package.somepkg.verification]
method = "minisign"
pinned_key = "RWQ...base64pubkey..."
```
**PoC status** (see `src/`, `packages.d/uv.toml`, `packages.d/scaleway-cli.toml`):
implements `repo`, `asset_pattern`, and `verification.method`
(`same-origin-sha256` | `github-attestation` only), loaded from
`packages.d/*.toml`. Confirmed working end to end against two real repos:
- `astral-sh/uv``github-atom` feed → fetch → `gh attestation verify`
(tier 2) → state persisted so a second run reports "up to date."
- `scaleway/scaleway-cli` — first live exercise of `same-origin-sha256`
(tier 4). Verified upstream ships no build-provenance attestations
(`attestations` API 404s), so this is genuinely tier 4, not an
under-verified tier 2. Surfaced two schema/implementation gaps beyond
what uv exercised, both now handled:
- Release asset names embed the version
(`scaleway-cli_2.62.0_linux_amd64`), unlike uv's static names.
`asset_pattern`/`checksum_asset_pattern` now support a `{version}`
placeholder, substituted via `checker::version_from_tag` (which also
strips a tag's leading `v`, since scaleway-cli tags `vX.Y.Z` but
filenames use the bare version).
- Checksums ship as one combined `SHA256SUMS` (one line per platform
asset) rather than a per-asset file like uv's — the verifier now
matches the line by filename instead of assuming a single-hash file.
- Separately, scaleway-cli's Atom feed lists a `vX.Y.Z-dbg1` tag newest,
with no real Release object behind it (`releases/tags/<tag>` 404s) —
`checker::latest_github_release` now confirms each feed candidate
against the releases API in feed order rather than trusting the first
entry outright.
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
`sanity_check` and `binary_name` are now real, implemented fields (see
Builder/Sanity checker above) — added `packages.d/uv.toml`'s and
`packages.d/scaleway-cli.toml`'s own `sanity_check` blocks, and
scaleway-cli's `binary_name = "scw"`. `source`, `check_method`, and
`check_interval` are still schema sketch, not yet read by the code — the
PoC only knows how to check GitHub-release sources, on a single one-shot
run rather than a scheduled loop.
Build/publish/review-queue (`makepkg`, `repo-add`, tier 46 human review)
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
are now implemented too — see Builder/Sanity checker/Publisher/Reviewer
queue above and the Status entry below for the first full end-to-end run.
Tier 4-6 packages still don't auto-publish (by design, see Verification
trust tiers > Automation posture per tier); they queue for
`pkgwatch review <name> --approve`.
Open questions on the schema:
- How much of `nvchecker`'s source-type taxonomy (github, gitlab, pypi,
crates.io, regex, htmlparser, ...) to reimplement vs. shell out to
`nvchecker` itself for the version-check step and own only the
verification + publish pipeline.
- PKGBUILD generation: full Jinja-style templates per package vs. a small
fixed set of PKGBUILD "shapes" (single binary tarball, cargo-install,
etc.) parameterized by the config.
- Where the local pacman repo lives and how it's registered in
`pacman.conf` (one-time manual setup step vs. something pkgwatch manages).
- Failure/alerting channel for tier 46 change events — log only, or a
notification hook (this box already has a wofi/Mako notification setup —
see `project_wofi_notification_picker` in Claude's memory).
- ~~Config layout: single TOML vs. `packages.d/*.toml` directory~~ —
resolved: PoC loads `packages.d/*.toml` directly.
- Digest/batching rules for low-signal tier 46 changes (see Scaling,
above) — what counts as "low-signal" needs a concrete definition, not
just "not a major version bump."
## Architecture sketch
- **Config loader**: parses `packages.d/*.toml` into an in-memory package
list. *(Implemented — `src/config.rs`.)*
- **Paths**: where config, state and work files live, resolved by
`src/paths.rs` per the XDG base-directory spec rather than the current
working directory, so an installed binary behaves the same wherever it's
launched from. *(Implemented.)*
| What | Default | XDG variable | Override |
|---|---|---|---|
| Package declarations (`packages.d/*.toml`) | `~/.config/pkgwatch/packages.d` | `XDG_CONFIG_HOME` | `PKGWATCH_CONFIG_DIR` (the dir *containing* `packages.d`) |
| Last-published / pending versions | `~/.local/state/pkgwatch` | `XDG_STATE_HOME` | `PKGWATCH_STATE_DIR` |
| Downloads and build trees (safe to delete) | `~/.cache/pkgwatch` | `XDG_CACHE_HOME` | `PKGWATCH_WORK_DIR` |
The overrides are used verbatim (no `pkgwatch/` suffix) and exist for
dry runs against scratch directories, like `PKGWATCH_REPO_DIR` does for
the pacman repo. Migrating from the old cwd-relative layout: move
`state/` to the state dir and copy or symlink `packages.d/` into the
config dir; `work/` is cache and can simply be dropped.
- **Checker**: per source type, resolves "what's the latest version" —
likely reuses `nvchecker`'s logic/sources conceptually for non-GitHub
sources eventually. For GitHub sources, prefers the `github-atom` feed
(see Scaling > Check method) over unconditional REST polling.
*(Implemented for GitHub only — `src/checker.rs` regex-matches the first
`releases/tag/<tag>` link in the feed rather than doing a full XML parse;
fine while the feed's newest-entry-first shape holds, revisit if that
ever changes. `check_interval`/per-package cadence not wired up yet —
the PoC is a single one-shot run, not a scheduled loop.)*
- **Fetcher**: downloads the artifact (and any checksum/signature/
attestation companion) for a resolved version. *(Implemented —
`src/fetcher.rs`, via the GitHub releases API; exact asset-name match,
not a glob.)*
- **Verifier**: tier-specific verification implementations, dispatched via
a `Verification` enum matched on `method` (an internally-tagged serde
enum) rather than a trait — simpler while there are only two methods;
revisit as a trait if the method count grows. Returns a tier + pass/fail
+ justification string; tier is derived from `method`, never configured
separately. *(Implemented for `same-origin-sha256` and
`github-attestation``src/verifier.rs`. The latter shells out to `gh
attestation verify` rather than reimplementing sigstore verification.)*
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
- **Builder**: for tiers 13 on pass, generates a PKGBUILD (strict
validation on every upstream-controlled string — version, asset name,
Fix issues from code review: shell-escaping gap, exit code, and more 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>
2026-09-17 10:06:40 +00:00
download URL — before it touches generated shell content; most fields
are single-quoted, but the `install()` line necessarily uses double
quotes so `${srcdir}`/`${pkgdir}` expand, so the validation rejects `'`,
newline, `$`, backtick, *and* backslash — safe for either quoting style
rather than assuming a value only ever lands in one of them — never
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
unescaped interpolation) and runs `makepkg`. *(Implemented —
`src/builder.rs`. One fixed "prebuilt binary" PKGBUILD shape covers both
tracked packages so far: a bare-binary download (scaleway-cli) and a
tarball extracting to a same-named directory (uv) — see Scaling >
Template reuse. `Package.binary_name` (config.rs) covers the case where
the installed binary's name differs from the pacman package name, which
turned out to matter immediately: scaleway-cli's real binary is `scw`,
not `scaleway-cli` — confirmed by inspecting the currently-installed
`extra` package with `pacman -Ql`, not guessable from the repo name.
Without it the build would install alongside `extra`'s package instead
of shadowing it.)*
- **Sanity checker**: after a successful build, runs the package's
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
declared `sanity_check.command` — with the freshly built package's
`usr/bin` prepended to `PATH`, so it exercises what was just built
rather than whatever's already installed system-wide — and confirms the
reported version matches what pkgwatch believes it just built. Mismatch
= fail loud, do not publish. This is a correctness check, not a security
control — it catches checker bugs and mangled/wrong-artifact downloads,
not malicious releases. *(Implemented — `src/sanity.rs`.)*
- **Publisher**: copies the built package into the local repo directory
and runs `repo-add`, only after the sanity check passes. *(Implemented —
`src/publisher.rs`. Targets an existing, already-registered local pacman
repo rather than one pkgwatch creates — this box already has one at
`~/.local/share/pacman/custom`, registered as `[custom]` in
`/etc/pacman.conf` (`SigLevel = Optional TrustAll`) and already in use
for a hand-packaged AppImage. But that repo directory/registration isn't
guaranteed to exist on every box this ever runs on, so it isn't just
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, rather than wasting a
`makepkg` build on a repo pacman will never sync from. The repo
*directory* and its database file, by contrast, are fully self-healing —
`publish` creates the directory if missing and `repo-add` creates the
database on its first run. What's deliberately not automatic, and can't
safely be: writing the `[section]` into `/etc/pacman.conf` itself — that
needs root, which this process doesn't have and shouldn't grab for
itself. Similarly, publish deliberately stops at `repo-add`: getting the
new version onto the running system is a separate, deliberate
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
`pacman -Syu`/`pacman -S <pkg>` step left to the operator, not run
automatically.)*
- **Reviewer queue**: for tiers 46, records the detected change instead of
auto-building; a separate `pkgwatch review` command lets a human
approve/reject, which then triggers the build → sanity-check → publish
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
steps above. *(Implemented — `state::{load,save,clear}_pending_version`
plus the `review`/`review <name> --approve` subcommands in `src/main.rs`.
Tracked separately from the last-published-version state: approving one
release doesn't mean future ones auto-publish. `--approve` re-verifies
before building rather than trusting a possibly-stale flag from an
earlier run. No reject/dismiss command yet — see Status below.)*
- **Scheduling**: systemd `.service` (oneshot) + `.timer` running it
periodically, matching the pattern already used for other periodic tasks
on this box. *(Implemented — user-level units under `systemd/`, run 10s
after login and then hourly, non-persistent. Install from the main
checkout:*
```sh
cargo build --release && mkdir -p ~/.config/systemd/user &&
cp systemd/* ~/.config/systemd/user/ && systemctl --user daemon-reload &&
systemctl --user enable --now pkgwatch.timer
```
*`pkgwatch.service` runs the release binary from the checkout and sets
no `WorkingDirectory`: config, state and work dirs come from the XDG
paths above, so the service needs `~/.config/pkgwatch/packages.d` set
up first — see the migration note under Paths.)*
- **Notifications**: `notifier.rs` sends a desktop notification
(`notify-send`) when a tier 4-6 release is newly queued for review or a
tier 1-3 release is published; both are best-effort and never fail a
run. A non-zero exit (verification/build/network failure) triggers
`pkgwatch-failure.service` via `OnFailure=`. Approving via
`pkgwatch review --approve` doesn't notify — the operator is already at
the terminal.
## Prior art / reference points
- `nvchecker` — version-check-only, no verification or publish step.
- `updpkgsums` (pacman-contrib/devtools) — checksum refresh only, manual
trigger.
- `aurutils` — local repo + AUR build automation, but AUR itself carries no
stronger verification guarantee than what each PKGBUILD maintainer does.
- `repology` — cross-distro version tracking, no verification/publish.
- GitHub artifact attestations (`gh attestation verify`) — tier 2 building
block for GitHub-hosted releases.
## Status / next steps
- [x] Scope decided: personal middle-ground tool for a curated package
list, not a general supply-chain-security framework (see Scope
above). Downgrade attacks and compromised-vendor-pipeline defense
are explicit non-goals; per-user scale to many packages is *not* a
non-goal (see Scaling to many packages, above).
- [x] Check method decided for GitHub sources: `github-atom`/conditional
`github-api` polling, outbound-only. Inbound webhooks explicitly
rejected — no repo-admin access on upstreams, and a relay-based
alternative would require a public receiver this box's networking
posture deliberately avoids.
- [x] `packages.d/*.toml` config layout implemented (`src/config.rs`).
- [x] First PoC iteration, working end to end against the real
`astral-sh/uv` repo: `github-atom` check → GitHub-API fetch →
`github-attestation` (tier 2) verify via `gh attestation verify`
state persisted so re-runs report "up to date." Confirmed uv
actually ships attestations, correcting the spec's original tier-4
guess for it. Run: `cargo run` from the project root.
- [x] `same-origin-sha256` exercised against a real package:
`scaleway/scaleway-cli`, tracked via `packages.d/scaleway-cli.toml`
(added because Manjaro's `extra` scaleway-cli lags upstream). Tier 4
confirmed correct — no build-provenance attestations upstream.
Required adding `{version}`-placeholder support to `asset_pattern`/
`checksum_asset_pattern`, filename-matched parsing of combined
multi-asset checksum files, and having `latest_github_release`
confirm each Atom-feed candidate against the releases API (this
repo's newest feed entry, a `-dbg1` tag, has no real Release behind
it). Still just flags for human review, same as any tier 4-6 pass —
not auto-installed; see the unchecked build/publish item below.
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
- [x] Full pipeline closed end to end for the first time: check → fetch →
verify → build → sanity-check → publish, against two real packages.
`uv` (tier 2) auto-built and published on the first run with no
human step. `scaleway-cli` (tier 4) queued for review, then
`pkgwatch review scaleway-cli --approve` re-verified, built, and
published it — confirmed the built package installs as
`/usr/bin/scw`, actually shadowing `extra`'s package rather than
installing alongside it under the wrong name. Both landed in the
real `~/.local/share/pacman/custom` repo's database
(`custom.db.tar.gz`), ready for `sudo pacman -Syu`/`sudo pacman -S`
— not run automatically. See Builder/Sanity checker/Publisher/
Reviewer queue above for what each piece does.
One cosmetic wrinkle, not a correctness issue: `makepkg` printed
`libfakeroot internal error: payload not recognized!` while
packaging scaleway-cli's large Go binary, but still produced a
correct package (verified: exactly `usr/bin/scw` plus standard
metadata) — looks like an environment quirk in this sandbox's
fakeroot, not something pkgwatch caused; revisit if a real build
ever actually fails on it.
- [ ] Not yet implemented: `pkgwatch review <name> --reject` (a pending
review can only be approved or left pending, not dismissed),
per-package `check_interval` (the timer is a fixed hourly tick),
non-GitHub sources, `minisign`/tier-1
Close the loop: build, sanity-check, and publish for the first time 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>
2026-09-17 09:13:13 +00:00
method, retention/pruning of old versions in the local repo (see
Scaling > Local repo retention), staggering/auth for GitHub API
rate limits at higher package counts.
- [ ] Refine config schema further (see open questions above).
- [ ] Decide version-check strategy for non-GitHub sources: shell out to
`nvchecker` vs. own implementation.
- [ ] Decide on project home: local-only for now, or push to
code.austinschaefer.com (Forgejo) once the spec settles.