RPM packaging recipes for public CLI / infra tools, published to Copr and kept
current automatically via Packit + per-tool upstream watchers.
Install a tool with dnf copr enable gmipf/devtools then dnf install <tool>.
Only public upstream tools live here. Nothing private or closed-source.
The directory tree encodes only what produces files — format → app. Everything volatile (which Copr project, trigger branch, watcher schedule) lives in config, never in the path. That's what keeps the repo sane once it holds many apps.
.
├── .packit.yaml # one packages: block per app (paths: + specfile_path)
├── packaging.conf # tool registry consumed by scripts/build-tool.sh
├── scripts/build-tool.sh # safe manual Copr build (NEVRA auto-bump + branch-scoped trigger)
├── scripts/copr-reconcile.py # safety net: re-pushes a trigger whose build never reached Copr
└── fedora/ # format level (sibling debian/, arch/ … added on demand)
└── <app>/ # self-contained: <app>.spec + flat sources + manpage/icon/state
| Axis | Lives in | Example |
|---|---|---|
| Distro / format | path top level | fedora/ · later debian/ |
| App | path under format | fedora/tea/ |
| Copr project (routing) | packaging.conf 4th field |
default gmipf/devtools, overridable per app |
A second distro for the same app is a sibling recipe dir (debian/<app>/), not a move.
Re-homing an app to a thematic Copr (gmipf/<theme>) is a one-line config change — no
files move.
Four additions, nothing else changes:
fedora/<app>/<app>.spec(+ flat sources, manpage, watcher state file).- A
packages:block in.packit.yaml(paths: fedora/<app>/,specfile_path: <app>.spec, aget-current-versionaction that reads the spec and acreate-archiveaction that matches the vendored Source0 by its form, never by its version) and acopr_buildjob withbranch: ^build-<app>$,update_release: falseandowner/project.scripts/test-copr-reconcile.pychecks that it routes exactly likepackaging.confand that Packit leaves the NEVRA alone;scripts/test-packit-config.pyruns both actions through a simulated upstream bump. - One line in
packaging.conf:TOOLS[<app>]="<copr-package>|fedora/<app>/<app>.spec|build-<app>[|<copr-project>]". - An upstream-release watcher workflow.
Upstream releases trigger Copr builds automatically. To fire a build by hand:
scripts/build-tool.sh <app>It refuses to republish an identical NEVRA (which would break dnf clients), auto-bumping
the bare-N Release if needed, and routes through the per-tool build-<app> branch so
only that one package rebuilds.
A watcher pushes the bumped spec to main first and the trigger branch second. If only the
second push fails, the next watcher run sees nothing to do, so every push is retried
(scripts/push-retry.sh), and every six hours copr-reconcile.yml compares each spec on
main with Copr: a version Copr never received gets its trigger re-pushed. It never
re-pushes a version Copr has seen in any state, so the same NEVRA is never built twice. A
failed build, Copr ahead of main, or an unanswered API turns the run red instead.
Run scripts/copr-reconcile.py for the same report locally (--heal only in CI).
scripts/lint.sh holds every lint rule (whitespace, shellcheck, ruff, yamllint, actionlint,
packit validate, rpmlint with a declared list of expected findings) and runs the offline
test suites (scripts/test-*.py). CI runs it with every tool installed (check-repo.yml,
on push, on pull requests and daily); locally it runs as a pre-commit hook, skipping tools
that are not installed:
git config core.hooksPath .githooksEvery spec has a %check that runs the binary it ships: it must report exactly the packaged
version, and the man page and completions rendered from it must not be empty.
scripts/rpmbuild-check.sh builds every spec that way in CI (check-repo.yml, job
rpmbuild), since a push to main builds nothing on Copr; the script's header shows the
docker run line for a local run.
A check that cannot fail looks exactly like one that passes. scripts/selftest.py plants a
defect for every check above (every lint rule, every test-suite decision, the hook, each
spec's %check with --rpmbuild) in a throwaway copy and requires each to turn red for
its own reason; CI runs it in both jobs.
What only the real services can show (how Packit routes a push, what Copr runs, what the
reconciler does on GitHub) is in proofs/ledger.tsv: observed once, with the evidence in
proofs/ and a fingerprint of the files it rests on. scripts/test-proofs.py (run by
lint.sh) turns red when such a file changes while the entry still claims the old
observation.
| Tool | Description | Status |
|---|---|---|
| tea | Gitea / Forgejo CLI | live |
| syft | Anchore SBOM generator (SPDX / CycloneDX) | live |
| grype | Anchore vulnerability scanner (images / filesystems / SBOMs) | live |
| grant | Anchore license checker (images / SBOMs / filesystems) | live |