Reference
Commands and config
Look-up page. Current for owlab v0.5.1 and owfeed v0.5.0. The authoritative config reference is schema/v1.json, which is generated from the code and is what your editor reads.
owlab — develop and test
| Command | Does |
|---|---|
owlab up | Build and start the routers. --rebuild throws the old ones away. |
owlab down | Stop them. --purge also deletes images and VM disks. |
owlab sync | Copy your sources into the running routers and reload LuCI. --watch does it on every save. |
owlab build | Build a real .apk/.ipk with the OpenWrt SDK into dist/<arch>/. |
owlab test | Start, install, assert, tear down, exit 0 or 1. What CI runs. |
owlab install | Install packages on a running router, by file or by name. |
owlab shell / exec | Open a shell, or run one command. |
owlab logs | Boot and service log. -f follows. |
owlab open | Open LuCI in a browser. |
owlab status | What is running and on which ports. |
owlab releases | Compare your pinned releases against the download servers. |
owlab doctor | Check this machine: ports, ssh keys, line endings, QEMU, DNS. |
Every command takes router ids; with none, it acts on all of them. --config
(or -c) works anywhere in the line. test, status and
releases take --json.
owlab test assertions
owlab test --release 25.12.5 \ --install 'dist/*/my-app-*' \ --assert 'http 200 /cgi-bin/luci/admin/services/mine' \ --assert 'service mined' \ --assert 'uci mine.@mine[0].enabled' \ --assert 'package my-app' \ --assert 'file /usr/share/rpcd/acl.d/my-app.json' \ --assert 'exec pgrep mined | grep -q .'
| Kind | Passes when |
|---|---|
http <status> <path> | The page answers with that status and is not an error page. 2xx matches a class. |
service <name> | /etc/init.d/<name> running says so, or ubus does. |
package <name> | The router's package manager reports it installed. |
uci <path> | The value is set — how you check that a package's uci-defaults really ran. |
file <path> | The path exists on the router. |
exec <command> | The command exits 0. |
--feed URL with --feed-key FILE installs by name out of a signed
index instead of from a file. --keep leaves the routers up; --json
writes a report to stdout.
owfeed — sign, index, publish
| Command | Does |
|---|---|
owfeed init | Scaffold owfeed.yml in this directory. |
owfeed keygen | Create the feed's signing key. --usign makes a release-manifest key instead. |
owfeed lock | Derive the architecture matrix into owfeed.lock. --update prints the diff. |
owfeed build | Package every configured package into dist/<arch>/. Does not compile. |
owfeed sign | Add a signature inside each package. |
owfeed index | Fan the packages out per architecture and write a signed index in each. |
owfeed doctor | Check the built tree. --require-origin demands every package name its source repository; --author-keys DIR demands every package carry a signature by a key pinned in DIR. |
owfeed smoke | Install the built feed on a real OpenWrt image. --release 24.10 for the older line. |
owfeed verify | Check the published feed from outside, over its documented URL. |
owfeed publish | Gate the tree on those checks and hand it to the target. No override flag. |
owfeed release | Sign the built packages and write a signed manifest for a feed to consume. |
owfeed verify-artifact | Check someone else's release against their signature. |
owfeed install-snippet | Print the instructions your subscribers follow. --format sh for commands only. |
owfeed releases | What the download server publishes per line, and which format it takes. |
Global flags: -C DIR, --config PATH, --cache DIR,
--frozen-lock, --no-network, -v.
The usual order is build → sign → index → doctor. Each stage takes a directory
in and leaves a directory behind. Signing appends bytes, so indexing before signing produces an
index that no longer matches the files.
owlab.yaml
version: 1
project: # optional — only if sync needs telling
install: # source path → path on the router
my-package/htdocs: /www
build: ./build-css.sh # run before every sync
post_sync: | # run on the router after every sync
uci -q commit luci
theme: footstrap # select this theme after each sync
defaults: # merged into every router below
packages: ["+luci-app-sqm", "-ppp"]
fixtures: [all] # invent networks, clients, leases
extra_packages: # install from a URL, not a feed
- name: luci-app-example
apk: https://…/luci-app-example-1.4.0-r1.apk
ipk: https://…/luci-app-example-v1.4.0-r1-all.ipk
routers:
- id: owrt2512
release: "25.12.5" # pin exact point releases
distro: openwrt # or immortalwrt
fidelity: vm # basic (container) or vm (QEMU, real kernel)
ports: { http: 8080, ssh: 2222 }
memory: 512M
radios: 2 # vm only — mac80211_hwsim
Unknown keys are an error by design, so a typo fails at load time instead of being ignored.
owfeed.yml
version: 1
feed:
name: my-feed # seeds every derived filename
url: https://feed.example.org
maintainer: "You <you@example.org>"
releases:
- line: "25.12"
default: true # the line the install snippet advertises
format: apk
arches: auto # derived from downloads.openwrt.org
- line: "24.10"
format: ipk
signing:
key: env:OWFEED_SIGN_KEY # EC prime256v1, signs apk indexes and packages
usign-key: env:OWFEED_USIGN_KEY # usign, signs opkg indexes
sign-packages: true # false: sign only the index, not other people's files
author-keys: ./keys # every package must be signed by a key pinned here
keyring-package: true # ships -keyring, which carries the key to routers
also-sign: [] # during a rotation: the old key, so both signatures ride
publish:
- target: github-pages
packages:
- name: luci-app-mine
build: mkpkg # package a staged directory; owfeed does not compile
arch: noarch # never "all" — apk rejects it
version: 1.0.0-r1 # or version-from: makefile:./Makefile
files: ./staging/luci-app-mine
conffiles: ["/etc/config/mine"]
depends: ["luci-base"]
i18n: { from: ./po } # compiles .po into the .lmo LuCI reads
releases: ["25.12"] # omit to publish to every line
Full field list with descriptions: schema/v1.json. Add this line at the top of your config and your editor will complete and validate it:
# yaml-language-server: $schema=https://owfeed.org/schema/v1.json
Environment variables
| Variable | Used by | For |
|---|---|---|
OWFEED_SIGN_KEY | owfeed | The EC key that signs apk indexes and packages. |
OWFEED_USIGN_KEY | owfeed | The usign key that signs opkg indexes. Only a convention — signing.usign-key names whichever variable it likes, and there is no default. |
OWFEED_RELEASE_KEY | owfeed | The usign key that signs a release manifest. |
OWFEED_RELEASE_DATE | owfeed | Override the manifest timestamp for a reproducible build. |
OWLAB_CONFIG | owlab | Where owlab.yaml lives. |
OWLAB_PUBKEY | owlab | Which public key to install on routers. Default: every ~/.ssh/id_*.pub. |
OWLAB_ROOT_PASSWORD | owlab | Set a root password. Default is none. |
owfeed exit codes
CI can retry an outage. It must never retry a failed check, which is why 7 and 8 are different codes.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Internal error — a bug, or something the tool could not classify |
| 2 | Config error |
| 3 | Build failed |
| 4 | Key problem |
| 5 | Index failed |
| 6 | Publish failed |
| 7 | A check failed — do not retry |
| 8 | Upstream unreachable — safe to retry |
| 9 | owfeed.lock disagrees with upstream |
Glossary
- apk
- OpenWrt's package manager from 25.12. Reads one binary index per architecture and verifies
it against a key in
/etc/apk/keys. - opkg
- The package manager on 24.10 and earlier. Reads a text index and verifies it with usign. It does not understand any of apk's formats.
- feed
- A directory of packages and signed indexes, served over HTTPS. No server software, no database — anything that serves static files will do.
- index
- The file listing every package in a feed with its version and hash.
packages.adbfor apk,PackagesplusPackages.gzfor opkg. Trust lives here: the index is signed, and each package is checked against the hash the index recorded. - noarch / all
- Two words for a package that runs on any architecture. apk requires
noarchand rejectsallas uninstallable; opkg has only ever knownall. One build therefore writes bothdist/noarch/anddist/all/. - usign
- The ed25519 signing scheme OpenWrt already ships, so a router can verify a signature with nothing installed. Used for opkg indexes and release manifests. Keys are looked up by id, which is why the key file's name matters for opkg.
- conffiles
- The list of configuration files a package owns. Anything under
/etc/config/that is not declared gets replaced by your defaults on every firmware upgrade, silently. - .lmo
- The compiled translation format LuCI reads. Shipping
.pofiles does nothing — pointi18n.from:at them and they are compiled during the build. - sysupgrade
- A firmware upgrade that keeps a listed set of files.
/etc/apk/keys/*.pemand your repository list are not in that set by default — list them in/lib/upgrade/keep.d/<feed>, which sysupgrade reads alongside/etc/sysupgrade.conf. - dist/<arch>/
- The artifact contract: one directory per architecture, named the way downloads.openwrt.org spells it. The architecture is the directory, never the filename.
- -r<n>
- The package revision, always last in a version. After
~apk reads a commit hash, so only hex digits are allowed there — write1.0_beta1, not1.0~beta.