kodeOS internals
Release pipeline
How a version tag becomes a kodeOS release, built by hand from a clean clone of the tag: the order of the three repositories, the checks, the C5 image, the welcome app, the factory pack and the publish.
What it is
A release is a numbered kodeOS that people can download. A maintainer makes
one by hand. They push a git tag named vMAJOR.MINOR.PATCH on main, clone
that tag into an empty folder, run the checks, build the C5 image, kodeOS and
the welcome app, and assemble the release files. They try the factory pack on
a Kode Dot, and publish the files as a GitHub release with gh release create.
Four files make a release:
- a factory pack: a zip with the six images and a flashing script;
- the standalone
kodeos-vX.binfor updates; SHA256SUMS.txt;BUILD_INFO.txt, the record of what the release was built from.
The clean clone is the point. A build in a working tree can pick up a file
nobody committed, a submodule left on another commit or a dependencies.lock
from last week. A clone of the tag holds only what the tag names.
The checks a change goes through before it is merged are on
internals/test-pipeline.
How it is connected
flowchart LR c5["C5 firmware<br/>tag, when it changed"] --> sdk["SDK<br/>tag"] sdk --> pins["kodeOS: kgui pins,<br/>PROJECT_VER, submodule"] pins --> tag["kodeOS tag"] tag --> clone["clean clone<br/>of the tag"] clone --> build["checks, build_c5.sh,<br/>idf.py, build_welcome.sh"] build --> dist["assemble_release.sh<br/>BUILD_INFO.txt"] dist --> try["flash a Kode Dot"] try --> pub["gh release create"] class c5,sdk,tag ext class dist store class try chip
Three repositories release, always in this order, because each pins the one
before it: the C5's firmware (when it changed), the SDK, which pins the C5 as
its c5 submodule, and kodeOS, which pins the SDK as its sdk submodule and
by tag in kgui's manifest. The SDK tag and the kodeOS tag are two separate
numbers.
An annotated tag is its own git object and is not the commit. Resolve a tag to
its commit before you trust it: git rev-parse TAG^{commit}. A pin resolved
against the wrong object builds code nobody reviewed.
dependencies.lock is not in the repository, so the tag named in kgui's
manifest alone decides what a clean clone builds. If that tag does not exist,
the build fails.
Specifications
The order of a kodeOS release
- If the C5's firmware changed, release it first (below). Then move the SDK's
c5submodule to that commit. - Release the SDK (below): tag
vAA.BB.CCon itsmain. - In kodeOS, on a branch, set the four
version:pins ofcomponents/kgui/idf_component.yml(kodediy/kui,kodediy/kode,kodediy/kbsp,kodediy/kdrv) to that tag. They are the only per-release manifest edit in the repository. - Set
PROJECT_VERin the rootCMakeLists.txtto the new kodeOS number, and move thesdksubmodule to the commit the SDK tag names. - Run the checks (
internals/test-pipeline), open the pull request and merge it. - Tag
vXX.YY.ZZon the merged commit ofmainand push the tag. - Build the release from a clean clone of the tag, try it and publish it (below).
The build from a clean clone
From an empty folder. Each command stops the release when it fails.
| Step | Command | What it proves |
|---|---|---|
| Clone | git clone --recurse-submodules --branch vXX.YY.ZZ git@github.com:kodediy/KodeOS.git | The tree is exactly the tag's |
| Version | tools/validate_release_tag.sh vXX.YY.ZZ prints XX.YY.ZZ | The tag is well formed. The printed string is PROJECT_VER and names the zip and the bin |
On main | git fetch origin main then tools/check_on_main.sh HEAD origin/main | The tag's commit was merged |
| The SDK pin | kgui's four version: lines name one tag, and git -C sdk rev-parse HEAD equals git -C sdk rev-parse vAA.BB.CC^{commit} | kgui and the submodule name the same tree |
The C5 on its main | In sdk/c5/: git fetch origin main then ../../tools/check_on_main.sh <the commit the SDK pins> origin/main, the commit from git -C sdk ls-tree HEAD c5 | The C5 image comes from a merged commit |
| The checks | kodeOS's steps on internals/test-pipeline | The tag passes what every merge passes |
| The C5 image | With ESP-IDF v6.0.2 loaded: tools/build_c5.sh | Writes storage_image/c5_slave.bin and its stamp |
| kodeOS | With ESP-IDF v6.1 loaded: idf.py -DPROJECT_VER=XX.YY.ZZ build | The OS, with the C5 image in its FAT |
| The welcome app | In the same shell: tools/build_welcome.sh | Writes apps_image/<project>.bin and its stamp |
| Assemble | tools/assemble_release.sh XX.YY.ZZ | Writes the zip, the bin and SHA256SUMS.txt in dist/ |
| Record | BUILD_INFO.txt in dist/, below | What the release was built from |
The C5 image comes before kodeOS's build, because kodeOS's build puts it in the FAT image.
The scripts
| Script | What it does | Exit |
|---|---|---|
tools/validate_release_tag.sh TAG | Accepts ^v[0-9]+\.[0-9]+\.[0-9]+$ and prints the tag without the v. It is the only place the tag format is defined | 0 valid, 1 malformed |
tools/check_on_main.sh COMMIT REF | Succeeds when COMMIT is an ancestor of REF. The caller fetches REF first | 0 yes, 1 no |
tools/check_os_size.sh kodeos.bin | Prints name n of ceiling bytes, left (pct%) against the smaller of the 4 MB slot and the mailbox's image space (KAPPS_PENDING_INTENT_OFF in kapps_fmt.h), and warns once under 10 % is left | 0 under, 1 past the ceiling or unreadable |
tools/build_c5.sh | set-target esp32c5 and a build of sdk/c5/ into build_c5/, copies kode_c5.bin to storage_image/c5_slave.bin, and writes the stamp storage_image/c5_slave.commit as <commit> <sha256>, with -dirty after the commit when the C5 tree has local changes. Warns when the commit is not the one the SDK pins | 0 built, 1 submodule missing, IDF_PATH unset or no image |
tools/build_welcome.sh | Reads apps_image/welcome.pin (<repository> <app folder> <commit>), clones that repository into build_welcome/src once, builds the folder at the pinned commit, and writes apps_image/<project>.bin and its stamp <project>.commit | 0 built, 1 no pin, the source not at the pin, or a failed build |
tools/assemble_release.sh VERSION [BUILD [OUT [APPS]]] | Turns a finished build into the release files. Defaults: build, dist, apps_image | 0 done, 1 an input is missing or a check failed |
build_c5.sh needs ESP-IDF v6.0.2, because the C5 firmware builds with it.
Host and slave must run the same esp-hosted version, so building from the
pinned commit is what keeps the pair true. If idf.py is a shell function
(some installers define it so), both build scripts run $IDF_PATH/tools/idf.py.
The welcome app lives in kode-app-onboarding, a private repository, so
build_welcome.sh needs a key with read access to it. A fork points
welcome.pin at a welcome app of its own.
What assemble_release.sh checks and writes
| Step | Detail |
|---|---|
| Inputs | bootloader/bootloader.bin, partition_table/partition-table.bin, ota_data_initial.bin, kodeos.bin, storage.bin from the build, and flash.sh, FACTORY_README.txt, THIRD_PARTY_NOTICES.txt from tools/release/. The first missing one stops the script with one message |
| Size | check_os_size.sh on kodeos.bin |
| FAT contents | build/storage_stage must hold exactly kode/ and c5_slave.bin |
| C5 stamp | The commit in build/c5_slave.commit must equal what git -C sdk ls-tree HEAD c5 pins, and the SHA-256 must match the c5_slave.bin in the FAT |
| Welcome app | The image named by CONFIG_KODEOS_WELCOME_APP must be in apps_image/ with its stamp, built from the commit welcome.pin names, and match its stamp's SHA-256 |
apps.bin | tools/mkapps.py builds the apps region with the welcome app installed and no boot request. flash.sh must write it at the region's base in partitions.csv |
kodeos-factory-vX.zip | The six images, flash.sh, README.txt and THIRD_PARTY_NOTICES.txt: nine entries. flash.sh keeps its executable bit |
kodeos-vX.bin | kodeos.bin byte for byte, for install ... dst=os or the mailbox |
SHA256SUMS.txt | sha256sum over the zip and the bin |
Files are copied by name from an explicit list, so a key or a stray file in the folder cannot reach a release file.
BUILD_INFO.txt
Written in the clone's root after the assembly:
{
echo "kodeOS XX.YY.ZZ"
echo "kodeos commit $(git rev-parse HEAD)"
echo "sdk commit $(git -C sdk rev-parse HEAD)"
echo "c5 stamp $(cat build/c5_slave.commit)"
echo "welcome pin $(cat apps_image/welcome.pin)"
echo "welcome stamp $(cat apps_image/*.commit)"
echo "ESP-IDF $(python3 -c 'import json; print(json.load(open("build/project_description.json"))["git_revision"])')"
echo
echo "--- dependencies.lock"
cat dependencies.lock
} > dist/BUILD_INFO.txtThe resolved dependencies.lock is the only record of which component
versions the release was built with, because the repository keeps none.
The factory pack
flash.sh PORT needs Python 3 with esptool 5 or newer and checks the version
before it touches the board. It makes one call:
esptool --chip esp32p4 -p PORT write-flash --erase-all with these images.
| Offset | Image |
|---|---|
0x2000 | bootloader.bin |
0x8000 | partition-table.bin |
0x1B000 | ota_data_initial.bin (blank otadata) |
0x20000 | kodeos.bin |
0xC40000 | apps.bin (the apps region with the welcome app) |
0x1800000 | storage.bin (Kody's pack and the C5 image) |
The erase removes everything on the chip: settings, remembered networks, BLE
bonds, installed apps, the mailbox and every file on the internal volume.
Afterwards otadata is blank, so the board boots factory, and the apps region
holds the welcome app with no boot request. boot_info answers
running=factory and otadata=blank. See internals/boot-model.
Trying it on a Kode Dot
Before publishing, flash a unit with the pack the release will carry and read it back over the cable.
| Block | Checks |
|---|---|
| Files | cd dist && sha256sum -c SHA256SUMS.txt reports OK twice |
| Flash | Unzip the factory pack and run its own ./flash.sh PORT |
| Boot | info names a running slot on the tag's version and coredump=none; boot_info's reason is not unexpected |
| Self-test | test component=main: pmic, gauge, ioexp, clocks, imu, mag all pass |
| Memory | ps: int_low of at least 64 KB, alloc_fail=0, log_dropped=0 |
Publishing
| Repository | Command | Title | Files |
|---|---|---|---|
| kodeOS | gh release create vXX.YY.ZZ --repo kodediy/KodeOS --verify-tag --title "kodeOS vXX.YY.ZZ" --generate-notes dist/kodeos-factory-vXX.YY.ZZ.zip dist/kodeos-vXX.YY.ZZ.bin dist/SHA256SUMS.txt dist/BUILD_INFO.txt | kodeOS vXX.YY.ZZ | The four release files |
| SDK | gh release create vAA.BB.CC --repo kodediy/kode-sdk --verify-tag --title "kode-sdk vAA.BB.CC" --notes-file notes.md --latest | kode-sdk vAA.BB.CC | None. The notes are the tag's first message line, then the notes GitHub generates (gh api repos/kodediy/kode-sdk/releases/generate-notes -f tag_name=vAA.BB.CC --jq .body) |
| C5 firmware | gh release create vC --repo kodediy/kode_slave_c5 --verify-tag --title "kode_c5 vC" --generate-notes dist/* | kode_c5 vC | Below |
--verify-tag refuses a tag that is not on GitHub, so a release never names a
tag that exists only on one computer.
The C5 firmware's release
Only an admin can push a v* tag on kode_slave_c5 (the ruleset "Release
tags: admins only"). From a clean clone of the tag, on its main:
- With ESP-IDF v6.0.2:
idf.py -B build_kode set-target esp32c5 buildandtools/check_image.sh build_kode. Copykode_c5.bin, the bootloader, the partition table,ota_data_initial.binandflasher_args.jsonintodist/, and writedist/c5_slave.commitas<commit> <sha256 of kode_c5.bin>, the formatbuild_c5.shwrites in kodeOS. - With ESP-IDF v6.1: the closed build into
build_closedandtools/check_image.sh build_closed closed(internals/test-pipeline). SIGN_TOKEN=<token> tools/sign_closed.py build_closed kode_c5-closed. The factory's signing service holds the two Secure Boot keys and signs the bootloader and the app; the script checks both signatures against the keys inpki/before it writes anything. It needsespsecureon the path.- Zip the folder as
dist/kode_c5-closed-vC.zip, copy itskode_c5.bintodist/kode_c5-closed.bin, and writeSHA256SUMS.txtoverdist/. - Publish with the command above.
kodeOS builds the C5 image itself, from the commit the SDK pins, and does not read this release. The release is the record of what that commit produces, and the production line takes the closed image from it: the line's station writes it to a virgin C5, and its first boot closes the C5 for good.
Gotchas
Example code
A dry run of a release on your own computer, from a clean clone of main,
with no tag. It builds version 00.00.00, and the pin check may find the
submodule ahead of the last tag, which is normal between two releases.
- Check the tag format:
tools/validate_release_tag.sh v0.1.0prints0.1.0.v1.2.3-rc1prints an error and exits 1. - Load ESP-IDF v6.0.2 and build the C5 image:
tools/build_c5.sh. It prints the commit and size, and writesstorage_image/c5_slave.commit. - Load ESP-IDF v6.1 and build kodeOS:
idf.py -DPROJECT_VER=00.00.00 build. - Build the welcome app:
tools/build_welcome.sh. - Assemble:
tools/assemble_release.sh 00.00.00. It lists the zip, the bin andSHA256SUMS.txtindist/. - Check the sums:
cd dist && sha256sum -c SHA256SUMS.txtreportsOKtwice. - Flash a test unit from the pack: unzip it and run
./flash.sh PORT. The last lines tell you to runboot_infoand look forrunning=factory.
In the SDK
tools/assemble_release.sh: the release files, withtools/releaseholding the pack's templates.tools/build_welcome.shandapps_image: the welcome app's pin and build.components/kdrv/apps:kapps_fmt.h, where the mailbox ceiling the size check reads is defined.