Cookbook · 2 of 3
Build and release
Turn a working package into signed artifacts anyone can verify. Two keys are involved and they do different jobs — that is the part worth reading before you copy the workflow.
Make the two keys, once
owfeed keygen -o author.pem # EC prime256v1 — signs the packages owfeed keygen --usign -o release.key # usign/ed25519 — signs the release manifest gh secret set OWFEED_AUTHOR_KEY < author.pem gh secret set OWFEED_RELEASE_KEY < release.key git add author.pub.pem release.pub && git commit -m "publish the public halves"
owfeed keygen refuses to write inside one without
--force. Note the key ids it prints: a feed will ask for them.Why two
| Key | Signs | Checked by |
|---|---|---|
EC (author.pem) | inside each .apk | anyone, at any time, against your public half |
usign (release.key) | the release manifest | a feed, once, when it ingests your release |
The usign signature is checked at ingest and then it is done — it never leaves the feed's CI. The EC signature travels inside the package all the way to the router, so somebody who does not trust the feed can still confirm you built what they installed. That is the one claim about a package that survives the feed serving it, and it is why feeds require it.
Build both release lines
owlab build --release 25.12.5 # apk, for 25.12 and later owlab build --release 24.10.8 # ipk, for 24.10 and earlier
Both land in dist/<arch>/. An architecture-independent package produces two
directories, and that is correct rather than a bug: apk requires noarch and rejects
all, opkg has never heard of noarch.
dist/ ├── noarch/my-app-1.0.0-r1.apk └── all/my-app_1.0.0-r1_all.ipk
Shipping only 25.12 abandons everyone who has not upgraded — routers stay on a release for years.
luci.mk minifies JS and CSS; owlab sync does not. Code that works
unminified and breaks minified is invisible until somebody builds a package.Sign, then write the manifest — in that order
owfeed sign --key env:OWFEED_AUTHOR_KEY owfeed release --repo "$GITHUB_REPOSITORY" --tag "$GITHUB_REF_NAME"
The order is not a preference. owfeed release records every
package's size and sha256, and signing appends bytes. Reverse them and the manifest describes files
that no longer exist.
owfeed release writes manifest.txt: what belongs to this release, each
file's size and hash, and which repository and tag it came from — signed with your usign key. It
records the repository because a signature proves who wrote something, never what it is
about. Without that line, a manifest lifted from another of your releases would verify
perfectly as this one.
GitHub Actions
.github/workflows/release.yml
name: release
on:
push: { tags: ["v*"] }
permissions: { contents: read }
jobs:
release:
runs-on: ubuntu-latest
permissions: { contents: write }
steps:
- uses: actions/checkout@v5
- uses: owfeed/owlab/setup@v0.5.1
- uses: owfeed/owfeed/setup@v0.5.0
- run: owlab build --release 25.12.5
- run: owlab build --release 24.10.8
- name: Sign the packages, then write the manifest
env:
OWFEED_AUTHOR_KEY: ${{ secrets.OWFEED_AUTHOR_KEY }}
OWFEED_RELEASE_KEY: ${{ secrets.OWFEED_RELEASE_KEY }}
run: |
owfeed sign --key env:OWFEED_AUTHOR_KEY
owfeed release
- run: gh release create "${{ github.ref_name }}" --generate-notes
dist/*/*.apk dist/*/*.ipk dist/manifest.txt*
env: { GH_TOKEN: ${{ github.token }} }
owfeed release reads the repository and tag from GITHUB_REPOSITORY and
GITHUB_REF_NAME on GitHub, and from CI_PROJECT_PATH and
CI_COMMIT_TAG on GitLab, so the flags are only for overriding them.
environment:.GitLab CI
release:
rules:
- if: $CI_COMMIT_TAG
script:
- wget -qO- https://github.com/owfeed/owlab/releases/download/v0.5.1/owlab_0.5.1_linux_amd64.tar.gz | tar xz
- wget -qO owfeed https://github.com/owfeed/owfeed/releases/latest/download/owfeed-linux-amd64
- chmod +x owfeed && export PATH="$PWD:$PATH"
- owlab build --release 25.12.5
- owlab build --release 24.10.8
- owfeed sign --key env:OWFEED_AUTHOR_KEY
- owfeed release
Check it before anyone else does
owfeed doctor --require-origin
Numbered findings, each saying what it costs and what to do. The ones that catch most first attempts:
| Finding | What it means |
|---|---|
arch: all | apk rejects it as uninstallable. Use LUCI_PKGARCH:=all and let the build translate it — that is what OpenWrt's own packaging does. |
| OWF207 | A /etc/config/* file is shipped but not in conffiles:, so sysupgrade replaces the user's settings with your defaults, silently, on every firmware upgrade. |
| Version rejected | After ~ apk reads a commit hash, so only hex. Write 1.0_beta1, not 1.0~beta. |
| Translations missing | LuCI reads compiled .lmo and ignores .po. Point i18n.from: at your catalogues. |