Move the ReleaseSource trait, GithubEndpoints and ForgejoEndpoints under
src/source/, each in its own file (release_source.rs, github.rs,
forgejo.rs). The submodules are private; mod.rs re-exports their types and
holds for_package, so the rest of the crate imports from crate::source and
never names a host's file. checker.rs keeps only version_from_tag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the Endpoints enum and checker's per-host dispatch: checker.rs now
holds only the ReleaseSource trait (latest release + API root), each host
implements it in its own module (github.rs, forgejo.rs), and source.rs is a
factory returning a Box<dyn ReleaseSource> per package. Adding a host no
longer touches existing ones, and the pipeline only sees the trait.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix stale ARCHITECTURE/config docs, soften source.rs's overclaim, trim the
SPEC's crate paragraph (no brittle counts), and merge the duplicate
same-origin Package test helper into test_support.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Packages can now declare source = "forgejo-release" plus a base_url and be
checked, fetched and verified against a Forgejo instance's releases API,
alongside the existing GitHub source. This is what lets pkgwatch track its
own releases from the self-hosted Forgejo.
- config: Source enum (github-release default, forgejo-release) + base_url,
validated once at load (base_url pairing, http(s) scheme, and no
github-attestation on a Forgejo source).
- source: new module mapping a package to its Endpoints.
- checker: latest_forgejo_release, one call to releases/latest; latest_release
dispatches per source.
- fetcher/verifier: take the releases API root instead of GithubEndpoints,
since GitHub and Forgejo serve the same releases/tags/<tag> shape.
- pipeline: endpoints are resolved per package.
- docs: SPEC documents the source key and why the HTTP is hand-rolled rather
than an API-client crate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ignore relative XDG_* values and reject a relative HOME, per the XDG
spec; add a hint to the missing-config error; tighten docs and comments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An installed pkgwatch has no checkout to run from, so packages.d/,
state/ and work/ can no longer be relative to the working directory.
New paths module resolves them per the XDG base-directory spec, with
PKGWATCH_{CONFIG,STATE,WORK}_DIR overrides for dry runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hourly user-level pkgwatch.timer/.service, an OnFailure= notifier, and a
notifier module that sends notify-send alerts when a tier 4-6 release is
queued for review or a tier 1-3 release is published.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>