packages feed

oglyad-0.1.1.0: README.md

# oglyad

repository metadata and health checker for [Terra](https://terra.fyralabs.com).
"огляд" is Ukrainian for "review" or "inspection".

```sh
git clone https://github.com/terrapkg/packages ../packages
oglyad -m ../packages            # warnings and errors
oglyad -m ../packages -v         # plus info findings
oglyad -m ../packages -u         # plus upstream version drift
oglyad -m ../packages --fedora   # plus overlap with Fedora
oglyad -m ../packages --only drift --format json
```

from a checkout, `cabal run oglyad -- <args>` and `cabal test`. it needs
`rpmspec` (rpm-build), `git`, and the `zstd`, `gzip` and `xz` commands.
upstream lookups use `GITHUB_TOKEN`, or `gh auth token` when that's unset.

## output and exit codes

`--format` picks `text` (default), `markdown` (made for GitHub job
summaries) or `json`. the json is a versioned envelope:

```json
{"schema": 1, "oglyad": "0.1.0.0", "generated": "...", "monorepo_head": "...",
 "summary": {"error": 0, "warn": 0, "info": 0, "ignored": 0},
 "findings": [{"check": "...", "package": "...", "path": "...", "severity": "...", "message": "..."}],
 "ignored": [...]}
```

`schema` changes on any breaking change to the json.

| exit | meaning |
|---|---|
| 0 | ran, nothing at or above `--fail-on` (default `error`, or `warn`, `info`, `never`) |
| 1 | ran, and found something at or above `--fail-on` |
| 2 | couldn't run: bad flags, unreadable ignore file, not a monorepo |

## ignoring known findings

some findings are intentional, like a bootstrap package sharing its real
package's name. there are two ways to silence one, and ignored findings
don't count toward the exit code.

an ignore file, passed with `--ignore-file`. every entry needs a reason, and
names one check plus a monorepo `path` or a `package` name:

```json
{
  "ignore": [
    {"check": "duplicate-name", "path": "anda/langs/zig/master-bootstrap",
     "reason": "bootstrap build of zig-master"}
  ]
}
```

or a label in the package's own `anda.hcl`:

```hcl
labels {
  oglyad_ignore = "duplicate-name, stale"
}
```

file entries that no longer match anything are reported as `unused-ignore`,
so exceptions don't outlive their reason. `--no-ignores` shows everything.
[`ignores.json`](ignores.json) is the file the nightly report uses.

## checks

| check | severity | what it means |
|---|---|---|
| `build-lag` | error / warn / info | never published, merged but not built, or the repo is newer than your checkout |
| `drift` | warn | upstream released something newer than the spec (`-u`) |
| `duplicate-name` | warn | two directories build the same package name into the same repo, so they overwrite each other |
| `stale` | info, warn with `-u` | last build is older than `--stale-days`, warn if upstream also moved on |
| `upstream-error` | warn | the upstream from update.rhai 404s or fails, so `anda update` is failing too |
| `fedora-overlap` | warn | Fedora ships the same source package (`--fedora`). subrepos are exempt |
| `no-autoupdate` | info | no update.rhai |
| `unparsed-rhai` | info | update.rhai does something oglyad can't follow, like custom scraping |
| `collect` | error | rpmspec failed, a repo couldn't be fetched, or an upstream rate-limited us (warn) |
| `unused-ignore` | info / warn | an ignore entry matches nothing, or an `oglyad_ignore` label names an unknown check |

## how it works

```
 ┌──────────── IO shell (Collect) ──┐      ┌──── pure core ────┐     ┌─ IO ─┐
 monorepo: rpmspec, git ──┐
 Terra repodata + subrepos├─► Snapshot ───► [Check] ─► [Finding] ─► Report
 Fedora repodata ─────────┤
 upstream APIs ───────────┘
```

checks are `Snapshot -> Package -> [Finding]` and never do IO, so each one is
tested with a fake `Snapshot` in `test/Spec.hs`.

- **specs** are evaluated with `rpmspec`
- **update.rhai** is lexed and pattern-matched, not run. it follows `let`
  bindings into `rpm.version(...)` / `rpm.global("ver", ...)` and recognizes
  the andax helpers (`gh`, `pypi`, `crates`, `gitlab`, ...)
- **upstream lookups** hit the same endpoints anda does, so oglyad sees what
  `anda update` would
- **versions** compare with a port of rpm's `rpmvercmp`, plus some forgiveness
  for respelled tags (`v1.2`, `1.2-rc1` vs `1.2~rc1`, `2026-07-27` vs `20260727`)

## caching

everything lives in `~/.cache/oglyad`.

| file | reused while |
|---|---|
| `specs.fcNN.json` | the package dir's files, the dist tag, and rpm's whole macro state (`rpm --showrc`) are unchanged |
| `commits.json` | HEAD hasn't moved |
| `repodata-*.json` | the repo's primary.xml checksum is unchanged |
| `upstream.json` | younger than `--cache-hours` (default 6), successes only. entries unused for 30 days are dropped |

a run with warm caches takes about 0.4s. a cold run takes about 5s, almost
all of it in rpmspec. `--fedora` adds about 8s and peaks around 200MB of
memory the first time, since Fedora's repodata is 260MB decompressed. after
that it's cached until Fedora publishes again.

## security

`rpmspec` runs the shell code in specs (`%(...)`, `%{lua:...}`), so running
oglyad on a checkout means running that checkout's code as you. child
processes get an allowlisted environment, so `GITHUB_TOKEN` and other
secrets in oglyad's environment never reach them, but code running as your
user can still read your files.

**don't run oglyad on untrusted code, like an unmerged pull request, with
credentials around.** the nightly report only checks out terrapkg/packages'
default branch, and its token is read-only and expires with the job.

the GitHub token is only ever sent to `api.github.com`, and is dropped if a
request gets redirected anywhere else.

## nightly report

[`report.yml`](.github/workflows/report.yml) runs every night in Terra's
builder image (`ghcr.io/terrapkg/builder:f45`), so specs evaluate with the
same macros Terra builds with. it builds `oglyad-git` with anda, checks
terrapkg/packages' default branch with `-u --fedora`, writes the markdown
report to the run's summary, and keeps the markdown and json for 30 days.
caches carry over between nights.

## packaging

built the same way as anda itself. [`anda.hcl`](anda.hcl) has two projects:

| project | spec | builds |
|---|---|---|
| `oglyad` | [`oglyad.spec`](oglyad.spec) | the `v<version>` release tag |
| `oglyad-git` | [`oglyad-git.spec`](oglyad-git.spec) | the checked-out commit, versioned `<version>^<date>.<commit>` |

```sh
anda build oglyad-git --package rpm -c terra-45-x86_64
```

both build in mock, offline, against Fedora's Haskell libraries with
`%ghc_bin_build`, run the test suite in `%check`, and ship one self-contained
binary. [`package-git.yml`](.github/workflows/package-git.yml) builds
`oglyad-git` on x86_64 and aarch64 for every push to main and every pull
request that touches the code.

## releasing

1. bump `version` in `oglyad.cabal` and `oglyad.spec`, and add a
   `CHANGELOG.md` entry
2. push a `v<version>` tag, e.g. `v0.1.0.0`

[`hackage.yml`](.github/workflows/hackage.yml) checks the tag matches the
cabal version, builds and tests the sdist, and publishes it to Hackage. the
same tag is what `oglyad.spec` downloads. to preview a release first, run the
workflow by hand, which uploads a Hackage package candidate instead.

it needs a `HACKAGE_TOKEN` secret (an API token from your Hackage account) in
a GitHub environment named `hackage`.

## formatting

code is formatted with [ormolu](https://github.com/tweag/ormolu) 0.9.0.0, and
CI fails on unformatted files. install the matching version from its
[release page](https://github.com/tweag/ormolu/releases/tag/0.9.0.0), then:

```sh
ormolu --mode inplace $(git ls-files '*.hs')
```

## license

GPL-3.0-or-later. see [LICENSE](LICENSE).