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

CommandDoes
owlab upBuild and start the routers. --rebuild throws the old ones away.
owlab downStop them. --purge also deletes images and VM disks.
owlab syncCopy your sources into the running routers and reload LuCI. --watch does it on every save.
owlab buildBuild a real .apk/.ipk with the OpenWrt SDK into dist/<arch>/.
owlab testStart, install, assert, tear down, exit 0 or 1. What CI runs.
owlab installInstall packages on a running router, by file or by name.
owlab shell / execOpen a shell, or run one command.
owlab logsBoot and service log. -f follows.
owlab openOpen LuCI in a browser.
owlab statusWhat is running and on which ports.
owlab releasesCompare your pinned releases against the download servers.
owlab doctorCheck 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 .'
KindPasses 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

CommandDoes
owfeed initScaffold owfeed.yml in this directory.
owfeed keygenCreate the feed's signing key. --usign makes a release-manifest key instead.
owfeed lockDerive the architecture matrix into owfeed.lock. --update prints the diff.
owfeed buildPackage every configured package into dist/<arch>/. Does not compile.
owfeed signAdd a signature inside each package.
owfeed indexFan the packages out per architecture and write a signed index in each.
owfeed doctorCheck 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 smokeInstall the built feed on a real OpenWrt image. --release 24.10 for the older line.
owfeed verifyCheck the published feed from outside, over its documented URL.
owfeed publishGate the tree on those checks and hand it to the target. No override flag.
owfeed releaseSign the built packages and write a signed manifest for a feed to consume.
owfeed verify-artifactCheck someone else's release against their signature.
owfeed install-snippetPrint the instructions your subscribers follow. --format sh for commands only.
owfeed releasesWhat 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

VariableUsed byFor
OWFEED_SIGN_KEYowfeedThe EC key that signs apk indexes and packages.
OWFEED_USIGN_KEYowfeedThe usign key that signs opkg indexes. Only a convention — signing.usign-key names whichever variable it likes, and there is no default.
OWFEED_RELEASE_KEYowfeedThe usign key that signs a release manifest.
OWFEED_RELEASE_DATEowfeedOverride the manifest timestamp for a reproducible build.
OWLAB_CONFIGowlabWhere owlab.yaml lives.
OWLAB_PUBKEYowlabWhich public key to install on routers. Default: every ~/.ssh/id_*.pub.
OWLAB_ROOT_PASSWORDowlabSet 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.

CodeMeaning
0Success
1Internal error — a bug, or something the tool could not classify
2Config error
3Build failed
4Key problem
5Index failed
6Publish failed
7A check failed — do not retry
8Upstream unreachable — safe to retry
9owfeed.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.adb for apk, Packages plus Packages.gz for 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 noarch and rejects all as uninstallable; opkg has only ever known all. One build therefore writes both dist/noarch/ and dist/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 .po files does nothing — point i18n.from: at them and they are compiled during the build.
sysupgrade
A firmware upgrade that keeps a listed set of files. /etc/apk/keys/*.pem and 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 — write 1.0_beta1, not 1.0~beta.