Files
omarchy-pkgs/README.md
T
Emir Beganovic 9b7daaf564 Rewrite the README around the pipeline as it runs today
The README described the repository host as the publisher: bin/repo
release as the main workflow, five-minute systemd timers, a signing key
that lives on the host and nowhere else, aarch64 under emulation. Since
September 18 packages are built by build-pr.yml and published by
publish.yml, and neither workflow was mentioned. ci/README.md still opened
with "Status: spike. Nothing here publishes."

The README now covers, in order: contributing a package, how a package
reaches users, where it is published, the maintainer tasks (republish,
build, retire, check that a version is live, read the publish log), what
to do when a publish fails, who can build and merge, the three scheduled
update workflows, package metadata, releases, and the rules that keep two
publishers from colliding.

Text about the old host pipeline moved to docs/repository-host.md, marked
abandoned. The rest moved to docs/ largely unchanged:
- docs/releases.md: the release train and versioning rules
- docs/rebuild-triggers.md: rebuild_on and bin/sync-rebuilds
- docs/builds.md: dependency order, isolation, builder images
- docs/upstream-sources.md: gains the provider and hook reference

ci/README.md is rewritten as the operating guide for the x86_64 builder
pool. Statements the move would have carried over wrong are corrected,
among them the signing key's location, AUR sync, and which architecture
list CI reads. bin/sync-upstream's help now points at the docs page that
holds the provider schemas.
2026-10-09 02:08:30 +02:00

512 lines
24 KiB
Markdown

# Omarchy Package Repository
Recipes and tooling for the packages Omarchy ships from
`https://pkgs.omarchy.org/<channel>/<arch>`.
- **Channels:** `edge` → `rc` → `stable`. Packages only move forward.
- **Architectures:** `x86_64` and `aarch64`.
- **Recipes:** one directory per package in `pkgbuilds/<package>/`, with a
`PKGBUILD` and `.omarchy/package.json`.
## Contribute a package
Everything a package PR needs is in `pkgbuilds/<package>/`. No secrets or
access are required. To work locally you need:
- `git`, `bash`, `jq`, `curl` and a `tar` that reads zstd
- Docker or Podman that your user can run, to build. With Docker, a build
calls `sudo` to fix the ownership of its output.
- on Arch, to check an upstream declaration: Python 3.11+, `vercmp`, `bsdtar`
Questions and problems: open an issue on this repository.
### Add a new package
1. Fork the repository and branch from `master`.
2. Create the recipe:
```bash
bin/add-package my-package --local --scaffold # Starter PKGBUILD and metadata
bin/add-package my-package --source aur # Or import an AUR recipe once
```
An imported recipe is Omarchy's from then on; there is no AUR sync.
3. Write `pkgbuilds/my-package/PKGBUILD` to Arch packaging standards.
- List every architecture it supports in `arch=()`: `x86_64`, `aarch64`,
or `any`. CI builds each one.
- Pin every git source with `#commit=<sha>` or `#tag=<tag>`. An unpinned
source is rejected; `tests/pinned-sources.sh` checks it locally.
- Fill the checksums: run `updpkgsums` (from `pacman-contrib`) in the
package directory. For a git source it leaves a clone of the source
there; delete it before committing.
4. Declare where new releases come from in
`pkgbuilds/my-package/.omarchy/package.json`, so updates arrive without
another PR. See [docs/upstream-sources.md](docs/upstream-sources.md).
```json
{
"source": "local",
"upstream": { "watch": { "github": "owner/project", "pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)" } }
}
```
Use this `watch` form for a new package. The pattern must match the whole
tag, so drop the `v` for a project that tags `1.2.3`. A `github` watch
reads GitHub Releases; for a project that only pushes tags, watch the tags:
```json
"upstream": { "watch": { "git_tags": "https://github.com/owner/project.git", "pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)" } }
```
Check that it finds the current release:
```bash
python helpers/upstream-watch.py check pkgbuilds/my-package
```
A working watch on a current recipe prints
`{"status": "skipped", ..., "reason": "already current"}`.
If the package should not be updated automatically, set `"sync": false`
and say why in the PR.
5. Build it:
```bash
bin/build --package my-package --dry-run # Show the plan
bin/build --package my-package # Build for x86_64
bin/build --package my-package --arch aarch64 # Emulated on an x86_64 machine; slow
```
`--dry-run` only shows the plan; it does not validate the recipe. The
package lands unsigned in `build-output/edge/<arch>/`. Install it with
`pacman -U` to try it. CI builds aarch64 natively, so a local aarch64
build is optional.
6. Open a PR against `master` with one package in it. Say what the package
is, and how you tested it.
### Update or fix an existing package
- **New upstream version:** for a package with an `upstream` declaration, a
bot proposes the update on its next scheduled run, or later if the package
sets `min_release_age`; you don't need to. Otherwise set `pkgver`, run
`updpkgsums`, and reset `pkgrel` to 1.
- **Packaging change at the same version:** bump `pkgrel`. A PR that changes
a recipe without moving its version builds nothing and ships nothing; when
the `PKGBUILD` itself changed, the PR's build check carries a "Change will
not ship" warning. (A `-git` package whose version carries the commit hash
is also rebuilt when its pinned commit changes.)
- Never set `epoch` without agreeing it in the PR first; it is permanent.
### What happens to your PR
1. **It waits for build approval** unless you are a collaborator or listed
in `.github/VOUCHED.td`. A maintainer starts the builds by labelling the
PR `build-approved`. Until then it shows **Awaiting build approval**, or,
for your first PR here, GitHub's own "workflows awaiting approval".
Builds cost machines, so they run for trusted authors only.
2. **CI builds the package** for each architecture and runs the tests. The
required checks are `result`, `self-tests` and `build-isolation`.
3. **It merges itself when green**, if it changes only package files.
4. **It is published to `edge`** within minutes of merging, and reaches
`rc` and `stable` with the next release. Say so in the PR if you think the
package should reach `stable` immediately (the fast ring).
Keep a package PR to these files, or it needs a maintainer to merge it by
hand:
- anything under `pkgbuilds/`
- a new test under `tests/`, and the one line in `.github/workflows/test.yml`
that runs it
Changes to `bin/`, `helpers/`, `build/` or the workflows go in a separate PR,
first: a PR's packages are built with `master`'s tooling, not its own.
## How a package reaches users
```
PR touching pkgbuilds/<package>/
└─ build-pr.yml one build per package and architecture, unsigned
merge to master
└─ publish.yml collect the builds, sign, add to the channel
databases, upload
pkgs.omarchy.org served from the R2 bucket omarchy-pkgs through a CDN
```
To ship a change: open a PR, wait for green, merge. Nothing else is needed.
1. **Build.** `build-pr.yml` builds every package directory the PR touches,
once, against `edge`. x86_64 builds on an ephemeral DigitalOcean droplet
(runner label `omarchy-builder`), aarch64 on GitHub's `ubuntu-24.04-arm`.
Each build is kept as a workflow artifact for 7 days, named
`<package>-<arch>-<git tree hash of the package directory>`.
2. **Merge.** Branch protection requires `result`, `self-tests` and
`build-isolation`. Don't merge around a red check.
3. **Publish.** `publish.yml` runs on every push to `master` that touches
`pkgbuilds/**`. It finds the artifact for exactly the merged tree, signs
it, and publishes it to every channel and architecture the package ships
to. When no artifact exists it builds one first, in a separate job. It
reports in a comment on the merged PR and appends to the
[publish log](#read-the-publish-log).
A package is built once and that one file is what every channel serves.
**A published filename never changes its bytes.** To ship a different build
of the same version, bump `pkgrel`.
## Where a package is published
`bin/build-matrix` decides, from `.omarchy/package.json`:
| Package | Published on merge to |
|---|---|
| Default | `edge` |
| `"release_ring": "fast"` | `edge`, `rc` and `stable` |
| `"channels": [...]` | never outside the listed channels; the other rows still apply within them |
| `"pinned": true` (`omarchy`, `omarchy-settings`) | `edge`; `rc` and `stable` only through a release |
A package is published for each architecture in its `arch=()`. An `arch=any`
package builds once and goes into both architectures' databases.
Everything not on the fast ring reaches `rc` and `stable` by promotion: see
[Releases](#releases).
## Maintainer tasks
### Republish a package
Use this when a publish run failed or was cancelled, or a package is missing
from a channel:
```bash
gh workflow run publish.yml -R omacom/omarchy-pkgs -f packages="<package> ..."
```
It publishes `master`'s version of each named package, reusing a build
artifact when one exists and building otherwise. With no artifact, it skips a
package that `edge` already holds at that version for that architecture; bump
`pkgrel` to publish that one again. Spell the names as the directories under
`pkgbuilds/`: an unknown name is ignored and the run still passes.
For a run that failed, see [When a publish fails](#when-a-publish-fails).
### Build a package without publishing
```bash
gh workflow run build-pr.yml -R omacom/omarchy-pkgs -f packages="<package> ..."
bin/build --package <package> # Locally; needs Docker or Podman
bin/build --package <package> --arch aarch64 # Emulated on an x86_64 machine
bin/build --package <package> --dry-run # Show the plan only
```
Both build only a version that `edge` does not hold yet. To build a published
version again locally, plan without the channel:
```bash
OMARCHY_PUBLISHED_REPO_URL= bin/build --package <package>
```
That also drops this repository as a source of dependencies, so it only works
for a package that depends on nothing else built here.
Local output is unsigned, in `build-output/edge/<arch>/`. Don't publish from
a local checkout.
### Retire a package
1. Delete `pkgbuilds/<package>/` in a PR and merge it.
2. Remove it from the channel databases:
```bash
gh workflow run unpublish.yml -R omacom/omarchy-pkgs \
-f packages="<pkgbase> ..." -f channels="edge rc stable"
```
It removes every entry built from those pkgbases, split and `-debug`
packages included, from both architectures. It refuses a package that
still has a recipe on `master`. Package files stay in the bucket: nothing
references them once the entries are gone.
### Check that a version is live
What the recipe says, from an up-to-date checkout of `master` (`<recipe>` is
the directory name under `pkgbuilds/`):
```bash
bin/repo list --json | jq -r '.[] | select(.name == "<recipe>") | .version'
bin/repo list # Every recipe, as a table
```
What each channel serves. A database lists every package as
`<name>-<version>/`:
```bash
for channel in edge rc stable; do for arch in x86_64 aarch64; do
db=$(curl -fsSL "https://pkgs.omarchy.org/$channel/$arch/omarchy.db?$RANDOM" | bsdtar -tf - 2>/dev/null)
if [[ -z $db ]]; then found="COULD NOT READ THE DATABASE"
else found=$(awk -F/ -v p="<package>" '{n=$1; sub(/-[^-]+-[^-]+$/, "", n)} n==p && $2=="" {print $1}' <<<"$db"); fi
printf '%-7s %-8s %s\n' "$channel" "$arch" "${found:-not in this channel}"
done; done
```
Compare the result against
[Where a package is published](#where-a-package-is-published) before calling
a missing entry wrong. `<package>` here is the package name, which for a
split package is not always the recipe directory.
List a whole channel:
```bash
curl -fsSL https://pkgs.omarchy.org/stable/aarch64/omarchy.db | bsdtar -tf - | grep '/$'
```
### Read the publish log
A publish run appends one JSON line to
`https://pkgs.omarchy.org/publish-log.jsonl` when it finishes, newest last.
A rerun adds another line for the same run. The log records finished runs;
for a cancelled one, [check the channel](#check-that-a-version-is-live).
| Field | Meaning |
|---|---|
| `time`, `commit`, `run` | When, which `master` commit, and the workflow run URL |
| `event` | `push` for a merge, `workflow_dispatch` for a manual run |
| `target` | `live`, or the scratch prefix of a proof run |
| `plan` | Every package, architecture and channel the run set out to publish |
| `sources` | Per package and architecture, where its files came from. Stops at the first one that failed |
| `slots` | Per channel and architecture attempted: `status`, and `packages` as file names without `.pkg.tar.zst` |
`sources[].source` is one of:
| Value | Meaning |
|---|---|
| `pr-artifact` | Used an existing build artifact, from the PR or an earlier run |
| `rebuild` | No artifact existed; built during this run |
| `already-published` | `edge` already held this version; nothing done |
| `build-failed` | No artifact: the build failed, or the artifact expired |
| `artifact-download-failed` | The artifact exists but could not be fetched or unpacked |
| `built`, `native-rebuild`, `native-build-failed` | Written by earlier versions of the workflow: built during the run, or failed to |
`slots[].status` is `published` or `failed`. An empty `slots` means the run
wrote no channel.
```bash
log=https://pkgs.omarchy.org/publish-log.jsonl
# The last five runs
curl -fsSL $log | tail -5 |
jq -c '{time, run, slots: [.slots[] | "\(.mirror)/\(.arch) \(.status)"]}'
# Every run that published a package, and where
curl -fsSL $log | jq -c --arg p "<package>" '
[.slots[] | select(.status == "published" and any(.packages[]; sub("-[^-]+-[^-]+-[^-]+$"; "") == $p))
| "\(.mirror)/\(.arch)"] as $where
| select($where | length > 0) | {time, run, published: $where}'
# Every run where something failed
curl -fsSL $log | jq -c '
select(any(.sources[]; .source | test("failed")) or any(.slots[]; .status == "failed"))
| {time, run, failed: [.sources[] | select(.source | test("failed")) | "\(.package) \(.arch) \(.source)"]}'
```
A merge also gets a summary of its record as a comment on the PR. The record
itself is kept as an artifact of the run for 90 days: `gh run download <run-id> -n publish-record-<run-id>`.
## When a publish fails
One package without a usable build stops the run before any channel is
written, for every package in that merge.
Find the run and read the failure:
```bash
gh run list -R omacom/omarchy-pkgs --workflow publish.yml --limit 10
gh run view <run-id> -R omacom/omarchy-pkgs # Which job failed
gh run view <run-id> -R omacom/omarchy-pkgs --log-failed # Its log
```
Then act on which job failed:
| Failed job | Log says | Do |
|---|---|---|
| `rebuild` | `Makepkg failed for <package>` | The recipe does not build. Fix it in a PR. Meanwhile publish the other packages of that merge: `gh workflow run publish.yml -f packages="<the healthy ones>"` |
| `rebuild` | an error before the package build starts: the builder image, a mirror, the network | Not the recipe. Rerun: `gh run rerun <run-id> --failed` |
| `publish`, step "Collect artifacts" | `no artifact from the rebuild job` | Either that package's build failed (see the `rebuild` job), or its artifact expired mid-run. For the second, rerun every job: `gh run rerun <run-id>` |
| `publish`, step "Collect artifacts" | the artifact could not be downloaded or unpacked | Rerun: `gh run rerun <run-id> --failed`. If it fails again the artifact is bad: bump `pkgrel` in a PR to get a fresh build |
| `publish`, step "Publish" | `Already published with DIFFERENT bytes, refusing to overwrite` | That version is already out with other contents. Bump `pkgrel` in a PR |
| `publish`, step "Publish" | `Upload incomplete`, or an rclone or gpg error | Storage or signing trouble. Rerun: `gh run rerun <run-id> --failed` |
| `publish` shows cancelled, and nobody cancelled it | | GitHub keeps one publish waiting at a time; when several merges land close together it cancels the ones in between. Republish their packages with a dispatch |
**The Publish step writes channels in a fixed order:** `edge` then `rc` then
`stable`, x86_64 before aarch64. It stops at the first one that fails, so the
earlier ones are complete, and running it again finishes the rest: a file
already uploaded with the same bytes is only added to the database.
- Rerun it while the build artifact exists: 7 days from when it was built.
- After that, bump `pkgrel` in a PR to finish the remaining channels.
A run cancelled before the Publish step wrote no channel: republish its
packages with a dispatch.
- Rerun (`gh run rerun`) repeats that merge's commit.
- Dispatch (`gh workflow run publish.yml -f packages=...`) publishes
`master`'s current version of the named packages.
Afterwards, [check that the version is live](#check-that-a-version-is-live).
## Who can build and merge
Builds cost machines, so `build-pr.yml` runs them only for trusted authors:
- repository collaborators and bots
- authors listed in `.github/VOUCHED.td`
- any PR a maintainer labels `build-approved` (that PR only, including later
commits): `gh pr edit <number> -R omacom/omarchy-pkgs --add-label build-approved`
`VOUCHED.td` takes one `github:<username>` per line; `-github:<username>
<reason>` denounces an author.
An unknown author's PR shows **Awaiting build approval** and stays blocked
until one of those applies. An author denounced in `VOUCHED.td` cannot be
overridden by the label.
A trusted PR also merges itself. `auto-merge-pr.yml` enables auto-merge when
the PR is open, not a draft, and changes only packages: files under
`pkgbuilds/`, new files under `tests/`, and `test.yml` lines that only add a
`./tests/<name>.sh` call. Anything touching workflows, scripts, build tooling
or an existing test needs a maintainer to merge it.
- Remove `build-approved` to withdraw the auto-merge it armed for an unknown
author. Runs it already released keep going.
- If the builds do not start a few minutes after labelling, remove the label
and apply it again.
- Evaluate a PR by hand: `gh workflow run auto-merge-pr.yml -f pr=<number>`.
## Automatic updates
Three scheduled workflows open PRs. Each takes a `packages` input for a manual
run on named packages; for the two sync workflows that run opens its own PR,
on `auto/sync-upstream-<packages>` or `auto/sync-rebuilds-<packages>`.
| Workflow | Runs | Opens | Merges |
|---|---|---|---|
| `sync-upstream.yml` | every 6 hours | one PR on `auto/sync-upstream` with every new upstream release | **a maintainer merges it** |
| `sync-rebuilds.yml` | every 6 hours | one PR on `auto/sync-rebuilds` bumping `pkgrel` where a `rebuild_on` dependency moved | itself, when green |
| `track-branches.yml` | every 2 hours | one PR on `auto/track-branches` pinning `"auto_merge": true` packages to their branch tip | itself, when green |
- **Each PR is a batch.** One package that fails to build keeps the whole PR
red and unmerged. Fix that package on `master`. For the two sync workflows,
a run with `packages` naming the healthy ones gives them their own PR. The
branch tracker has one PR only; its next run replaces it.
- **A package with no upstream declaration or hook gets no upstream updates.** That
includes the kernels (`linux-omarchy*`, `linux-ptl`, `linux-aurora`): bump
them by PR.
- A failed run of any of the three posts to Basecamp when
`BASECAMP_CHATBOT_URL` is set.
- Every self-merging path (`sync-rebuilds.yml`, `track-branches.yml` and
`auto-merge-pr.yml`) needs
the `PKGS_BOT_TOKEN` secret: a personal access token with Contents and Pull
requests write access to this repository, owned by an account trusted to
build. A merge made with the built-in `GITHUB_TOKEN` does not start
`publish.yml`.
Details: [docs/upstream-sources.md](docs/upstream-sources.md),
[docs/rebuild-triggers.md](docs/rebuild-triggers.md).
## Package metadata
`pkgbuilds/<package>/.omarchy/package.json`:
```json
{ "source": "local" }
{ "source": "local", "release_ring": "fast" }
{ "source": "local", "upstream": { "watch": { "github": "abenz1267/walker", "pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)" } } }
```
| Field | Meaning |
|---|---|
| `source` | Always `local`. |
| `upstream` | Where releases come from. Mutually exclusive with an `.omarchy/upstream.sh` hook. |
| `min_release_age` | Hold a new upstream release back this long (`"24h"`, `"2d"`). A release whose age cannot be proven fails the sync. |
| `auto_merge` | `true` moves the package's updates from the reviewed sync PR to `track-branches.yml`. For packages that follow a moving branch. Needs an upstream declaration. |
| `release_ring` | `fast`: publish to `rc` and `stable` on merge, not only `edge`. Takes effect with the package's next version: bump `pkgrel` in the same PR. |
| `channels` | The only channels the package may be published to. `omarchy-dev` is held to `["edge"]` this way. |
| `pinned` | Version is set per release on the `rc` branch. Used by `omarchy` and `omarchy-settings`. |
| `rebuild_on` | Packages whose version change forces a rebuild of this one. |
| `rebuilt_against` | Written by `bin/sync-rebuilds`. Don't edit. |
| `skip_build` | Left the package out of the old host's unscoped builds. CI ignores it: a PR that touches the package builds and publishes it. |
| `sync` | `false` holds the package out of upstream updates, with or without an `upstream` declaration. |
| `origin` | Where an imported recipe came from. Informational. |
## Releases
Cutting an Omarchy release and promoting a channel are **not** in the GitHub
workflows. They are done with `bin/omarchy-release` and `bin/repo advance`,
which were written for the old repository host, now abandoned, and work on a
local copy of the published tree.
Read [docs/releases.md](docs/releases.md) before cutting a release or
promoting a channel.
## Rules
- **Publish through `publish.yml` only.** Don't run `bin/repo release`,
`sync`, `push` or `deploy`. A second publisher overwrites the channel
database the first one wrote. The release train is the one exception: see
[docs/releases.md](docs/releases.md).
- **Don't bring the old host's release timers back.** They run
`bin/repo release` every five minutes. Run `bin/setup` with
`--skip-timers`.
- **Never reuse a published filename for different bytes.** The CDN caches
package files by name and keeps serving the old ones. Bump `pkgrel`.
- **Don't merge a package PR before its builds are green.** `publish.yml`
then has to build the missing packages, and one that fails stops the whole
merge from publishing.
- **Don't cancel a publish run.** If one is cancelled, republish its packages
with a dispatch.
- **Check after a manual publish.** Confirm the package and version are in
the channel database for each architecture it ships to.
## Layout
```
pkgbuilds/<package>/ Recipe: PKGBUILD and .omarchy/package.json
bin/ Tooling the workflows and local work call
build/ Builder image and the in-container build script
helpers/ Shell and Python shared by bin/ and the workflows
ci/ x86_64 builder pool: controller and runner setup
systemd/ Release timers for the repository host (abandoned)
tests/ Tests; test.yml runs them on every PR
docs/ Reference
.github/workflows/ The pipeline
.github/VOUCHED.td Authors trusted to build
```
| Workflow | Trigger | Does |
|---|---|---|
| `build-pr.yml` | PR, dispatch | Builds changed packages; reports `result` |
| `test.yml` | PR, dispatch | `self-tests` and `build-isolation` |
| `publish.yml` | push to `master`, dispatch | Signs and publishes |
| `unpublish.yml` | dispatch | Removes retired packages from channel databases |
| `auto-merge-pr.yml` | PR events, dispatch | Enables auto-merge on trusted package PRs |
| `approve-pr.yml` | PR events | Releases GitHub's hold on a `build-approved` PR's runs |
| `sync-upstream.yml` | every 6 hours, dispatch | Upstream release PR |
| `sync-rebuilds.yml` | every 6 hours, dispatch | Dependency rebuild PR |
| `track-branches.yml` | every 2 hours, dispatch | Branch tip PR |
| `builder-images.yml` | daily, changes to the image's inputs (push and PR), dispatch | Builds and tests the builder image; publishes it from `master` |
Secrets: the `publish` environment (usable from `master` only) holds the
signing key and bucket credentials: `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE`,
`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`. Its variable
`OMARCHY_PUBLISH_PREFIX` redirects publish and unpublish to a scratch prefix
in the bucket for a proof run; it must be empty for live publishing.
Repository secrets: `PKGS_BOT_TOKEN`, `BASECAMP_CHATBOT_URL`.
## More
- [ci/README.md](ci/README.md): the x86_64 builder pool and how to operate it
- [docs/builds.md](docs/builds.md): dependency order, isolation, builder images
- [docs/upstream-sources.md](docs/upstream-sources.md): declaring upstream releases
- [docs/rebuild-triggers.md](docs/rebuild-triggers.md): rebuilding when a dependency moves
- [docs/releases.md](docs/releases.md): cutting a release, versioning rules
- [docs/repository-host.md](docs/repository-host.md): the old host pipeline (abandoned)