Tag-driven firmware CI pipeline for Raspberry Pi Pico (Demo Project)
firm-CI is a demonstration-only IoT DevOps project that showcases how to build, version, and publish firmware automatically and deterministically using GitHub Actions, Docker, and a self-hosted runner.
The pipeline generates Raspberry Pi Pico firmware only when a Git tag is pushed, ensuring clean release semantics and traceability between source, build environment, and output artifact.
This repository is intentionally minimal in firmware logic (a Pico SDK blink example) and maximal in CI/CD clarity.
- Tag-triggered firmware releases (no tag → no firmware)
- Deterministic builds via Dockerized toolchains
- Self-hosted CI runner usage for cost and control
- Firmware artifact versioning tied to Git tags
- Pushing generated
.uf2firmware back into the repository - Separation of source code, SDK, and build output
- Reproducibility over convenience
This is not production-ready firmware infrastructure. It is a proof-of-competence CI pipeline.
- Target boards
- Raspberry Pi Pico
- Firmware source
- Located in
src/ - Uses
blink_simple.cfrom Pico SDK examples (demo purpose)
- Located in
- Language
- Mixed C / C++
The firmware logic is intentionally trivial.
- Source-of-truth branch:
main - Trigger condition: push of a Git tag matching
v* - No tag = no build
- Tags must be created on
main
This avoids:
- Accidental firmware builds
- CI noise
- Ambiguous release state
Developer
|
|-- git tag vX.Y.Z
|-- git push --tags
v
GitHub Actions
|
|-- Self-hosted Runner (Arch Linux)
|
|-- Checkout firmware repo (main)
|-- Checkout pico-sdk (external)
|
|-- Docker image build (only if Dockerfile changed)
|
|-- Dockerized firmware build
|
|-- Version .uf2 using Git tag
|
|-- Commit firmware back to main
v
build/*.uf2 (versioned)
- Chaching of repository and docker-images(toolchains). This is network heavy.
- Reduce time to download toolchain, clone pico-sdk repo.
- Lower cost than hosted runners for longer/complex firmware builds
- Full Docker control
- Predictable environment
- No hosted-runner time limits
- Suitable for embedded workflows
Runner OS: Arch Linux
Motivation: Cost, speed optimization and control
Docker is used for both:
-
Toolchain pinning
- Pico SDK
- CMake
- Ninja
-
Reproducible builds
- Same inputs → same outputs
- Host OS becomes irrelevant
Docker images are rebuilt only when the Dockerfile changes, avoiding unnecessary churn.
- Output format:
.uf2 - Location:
build/ - Versioning scheme:
firmware-name-vX.Y.Z.uf2 - Version source: Git tag (
github.ref_name)
Artifacts are committed back to main intentionally for demonstration purposes.
Yes, committing binaries is usually discouraged.
Here, it is deliberate to show traceability and CI authority.
| Tag | Firmware File | Source Commit | Build Environment |
|---|---|---|---|
v1.0.0 |
blink-v1.0.0.uf2 |
Tag commit | Docker image |
v1.1.0 |
blink-v1.1.0.uf2 |
Tag commit | Same image |
Every firmware artifact is:
- Traceable to a Git tag
- Traceable to source code
- Built in a known container
These are representative outputs of the pipeline behavior.
- One firmware per Git tag
- Zero firmware without a tag
- No overwrites
- 1:1 mapping between tag and
.uf2 - Artifact name embeds version explicitly
- Docker image rebuilt only when Dockerfile changes
- Firmware builds reuse cached image
- Typical reuse rate: >90% across tags
# ensure main is clean git checkout main git pull
# create version tag git tag v1.0.0
# push tag git push origin v1.0.0
That’s it.
No manual build steps.
No UI clicks.
No ambiguity.
- Binary artifacts committed to repository
- No GitHub Releases integration
- No semantic version enforcement
- No hardware-in-the-loop testing
- No rollback automation
These are out of scope for a demo pipeline.
This repository can be reused as a template if you:
- Replace firmware logic in
src/ - Adjust artifact handling (e.g. GitHub Releases)
- Harden branch protections
- Add test stages