1- # Publish a firmware release to the OTA update server and browser flasher.
2- #
3- # Triggered ONLY when a GitHub release is published (not on pushes or tag
4- # creation). Builds the tracker for every supported target plus the bridge
5- # from the tagged source, verifies the tag matches version.h, attaches the
6- # app binaries to the release, and deploys the full site to Cloudflare Pages
7- # (updates.crowpilot.in):
8- #
9- # / browser flasher (ESP Web Tools)
10- # /flash/manifest.json legacy manifest (C3 tracker + C6 bridge)
11- # /flash/crowmotion.json CrowMotion manifest (C3/S3/C6 builds)
12- # /flash/crowlink.json CrowLink manifest (C6)
13- # /flash/*-merged*.bin complete images (bootloader+partitions+app)
14- # /crowmotion/latest.json legacy OTA manifest (fielded C3 devices)
15- # /crowmotion/latest-<target>.json per-target OTA manifests (0.1.3+)
16- # /crowmotion/crowmotion-X.Y.Z[-<target>].bin OTA app images
17- #
18- # Each Pages deploy replaces the site, so only the newest binaries live on
19- # the update host; all historical binaries remain on GitHub Releases.
1+ # Build and publish a signed ESP32-C3 CrowMotion release.
202name : release-ota
213
224on :
235 release :
246 types : [published]
257
268permissions :
27- contents : write # upload the built binaries to the release
9+ contents : write
2810
2911jobs :
3012 publish :
3113 runs-on : ubuntu-latest
32- # The release tag is attacker-influenceable text; never interpolate it
33- # into a run: script directly (that is a shell-injection vector). Bind it
34- # to an env var, validate the format once, and reference "$TAG" quoted.
3514 env :
3615 TAG : ${{ github.event.release.tag_name }}
3716 steps :
@@ -46,219 +25,61 @@ jobs:
4625 with :
4726 ref : ${{ github.event.release.tag_name }}
4827
49- - name : Verify tag matches version.h
28+ - name : Verify tag matches firmware version
5029 run : |
5130 VER="${TAG#v}"
5231 SRC=$(sed -n 's/#define CROWMOTION_VERSION "\(.*\)"/\1/p' main/version.h)
53- echo "release tag: $VER version.h: $SRC"
54- if [ "$VER" != "$SRC" ]; then
55- echo "::error::version.h ($SRC) does not match the release tag ($VER). Bump version.h before releasing."
56- exit 1
57- fi
32+ test "$VER" = "$SRC" || { echo "::error::version.h ($SRC) does not match $VER"; exit 1; }
5833
59- - name : Sign-enable the tracker build (M13)
34+ - name : Enable signed OTA images
6035 env :
6136 OTA_SIGNING_KEY_B64 : ${{ secrets.OTA_SIGNING_KEY_B64 }}
6237 run : |
63- if [ -z "$OTA_SIGNING_KEY_B64" ]; then
64- echo "::error::OTA_SIGNING_KEY_B64 secret is missing; cannot sign the release"; exit 1
65- fi
38+ test -n "$OTA_SIGNING_KEY_B64" || { echo "::error::OTA_SIGNING_KEY_B64 is missing"; exit 1; }
6639 echo "$OTA_SIGNING_KEY_B64" | base64 -d > ota_signing_key.pem
67- # Layer the CI-only signing overlay onto the shared defaults so every
68- # tracker target build produces an RSA-3072 signed image. Local/dev
69- # builds do not run this, so they stay unsigned and need no key. Only
70- # affects the root (tracker) project; the bridge uses bridge/sdkconfig.
7140 cat sdkconfig.ci.signed >> sdkconfig.defaults
72- echo "signing enabled for the tracker builds"
7341
74- # The tracker builds run sequentially in the same workspace: stash each
75- # target's outputs, then wipe build/ and sdkconfig so the next set-target
76- # starts clean from the (sign-enabled) defaults.
77- - name : Build tracker (esp32c3)
42+ - name : Build ESP32-C3 firmware
7843 uses : espressif/esp-idf-ci-action@e6f5c74232b1ccd4c97ed641f1e48553853f1fd5 # v1.2.0
7944 with :
8045 esp_idf_version : v5.5.4
8146 target : esp32c3
8247
83- - name : Stash esp32c3 outputs
84- run : |
85- mkdir -p out/esp32c3
86- cp build/crowmotion.bin build/bootloader/bootloader.bin \
87- build/partition_table/partition-table.bin build/ota_data_initial.bin \
88- out/esp32c3/
89- sudo rm -rf build sdkconfig
90-
91- - name : Build tracker (esp32s3)
92- uses : espressif/esp-idf-ci-action@e6f5c74232b1ccd4c97ed641f1e48553853f1fd5 # v1.2.0
93- with :
94- esp_idf_version : v5.5.4
95- target : esp32s3
96-
97- - name : Stash esp32s3 outputs
98- run : |
99- mkdir -p out/esp32s3
100- cp build/crowmotion.bin build/bootloader/bootloader.bin \
101- build/partition_table/partition-table.bin build/ota_data_initial.bin \
102- out/esp32s3/
103- sudo rm -rf build sdkconfig
104-
105- - name : Build tracker (esp32c6)
106- uses : espressif/esp-idf-ci-action@e6f5c74232b1ccd4c97ed641f1e48553853f1fd5 # v1.2.0
107- with :
108- esp_idf_version : v5.5.4
109- target : esp32c6
110-
111- - name : Stash esp32c6 outputs
112- run : |
113- mkdir -p out/esp32c6
114- cp build/crowmotion.bin build/bootloader/bootloader.bin \
115- build/partition_table/partition-table.bin build/ota_data_initial.bin \
116- out/esp32c6/
117-
118- - name : Build CrowLink bridge (esp32c6)
119- uses : espressif/esp-idf-ci-action@e6f5c74232b1ccd4c97ed641f1e48553853f1fd5 # v1.2.0
120- with :
121- esp_idf_version : v5.5.4
122- target : esp32c6
123- path : bridge
124-
125- - name : Stage site (flasher + OTA)
48+ - name : Verify signature and stage release
12649 run : |
12750 VER="${TAG#v}"
12851 pip install --quiet "esptool==4.8.1"
129-
130- # Fail the release unless every tracker image is actually signed with
131- # our key (guards against a misbuild shipping an unverifiable image
132- # that would then reject every future OTA on 0.1.3+ devices).
133- for T in esp32c3 esp32s3 esp32c6; do
134- espsecure.py verify_signature --version 2 --keyfile ota_signing_key.pem \
135- "out/$T/crowmotion.bin" | tee /tmp/verify.txt
136- grep -q "verification successful" /tmp/verify.txt \
137- || { echo "::error::$T tracker image is not validly signed"; exit 1; }
138- done
52+ espsecure.py verify_signature --version 2 --keyfile ota_signing_key.pem build/crowmotion.bin | tee /tmp/verify.txt
53+ grep -q "verification successful" /tmp/verify.txt || { echo "::error::firmware signature verification failed"; exit 1; }
13954 rm -f ota_signing_key.pem
140-
14155 mkdir -p site/flash site/crowmotion
142- # Copy the whole flasher (index.html + vendored esp-web-tools);
143- # nothing is loaded from a third-party CDN.
14456 cp -R flasher/. site/
145-
146- # CORS: let the flasher embedded on crowpilot.in fetch the
147- # manifest and images cross-origin (Cloudflare Pages _headers).
14857 printf '/flash/*\n Access-Control-Allow-Origin: *\n' > site/_headers
149-
150- # Complete flash images for the browser flasher (offset 0), one per
151- # tracker target plus the bridge.
152- for T in esp32c3 esp32s3 esp32c6; do
153- esptool.py --chip "$T" merge_bin \
154- -o "site/flash/crowmotion-merged-$T.bin" \
155- --flash_mode dio --flash_size 4MB \
156- 0x0 "out/$T/bootloader.bin" \
157- 0x8000 "out/$T/partition-table.bin" \
158- 0xf000 "out/$T/ota_data_initial.bin" \
159- 0x20000 "out/$T/crowmotion.bin"
160- done
161- esptool.py --chip esp32c6 merge_bin \
162- -o site/flash/crowlink-merged.bin \
163- --flash_mode dio --flash_size 2MB \
164- 0x0 bridge/build/bootloader/bootloader.bin \
165- 0x8000 bridge/build/partition_table/partition-table.bin \
166- 0x10000 bridge/build/crowlink.bin
167-
168- # Legacy names, kept so the currently-published flasher page and
169- # product pages keep working until they switch to the per-product
170- # manifests below.
171- cp site/flash/crowmotion-merged-esp32c3.bin site/flash/crowmotion-merged.bin
172- cat > site/flash/manifest.json <<EOF
173- {
174- "name": "CrowMotion / CrowLink",
175- "version": "${VER}",
176- "new_install_prompt_erase": true,
177- "builds": [
178- {
179- "chipFamily": "ESP32-C3",
180- "parts": [ { "path": "crowmotion-merged.bin", "offset": 0 } ]
181- },
182- {
183- "chipFamily": "ESP32-C6",
184- "parts": [ { "path": "crowlink-merged.bin", "offset": 0 } ]
185- }
186- ]
187- }
188- EOF
189-
190- # Per-product manifests: CrowMotion picks the image for whichever
191- # supported chip is plugged in; CrowLink is its own button.
192- cat > site/flash/crowmotion.json <<EOF
193- {
194- "name": "CrowMotion",
195- "version": "${VER}",
196- "new_install_prompt_erase": true,
197- "builds": [
198- {
199- "chipFamily": "ESP32-C3",
200- "parts": [ { "path": "crowmotion-merged-esp32c3.bin", "offset": 0 } ]
201- },
202- {
203- "chipFamily": "ESP32-S3",
204- "parts": [ { "path": "crowmotion-merged-esp32s3.bin", "offset": 0 } ]
205- },
206- {
207- "chipFamily": "ESP32-C6",
208- "parts": [ { "path": "crowmotion-merged-esp32c6.bin", "offset": 0 } ]
209- }
210- ]
211- }
212- EOF
213- cat > site/flash/crowlink.json <<EOF
214- {
215- "name": "CrowLink",
216- "version": "${VER}",
217- "new_install_prompt_erase": true,
218- "builds": [
219- {
220- "chipFamily": "ESP32-C6",
221- "parts": [ { "path": "crowlink-merged.bin", "offset": 0 } ]
222- }
223- ]
224- }
225- EOF
226-
227- # OTA: per-target app images + manifests (0.1.3+ devices fetch
228- # latest-<target>.json), plus the legacy pair for fielded C3 devices
229- # running 0.1.2 or older.
230- for T in esp32c3 esp32s3 esp32c6; do
231- cp "out/$T/crowmotion.bin" "site/crowmotion/crowmotion-${VER}-$T.bin"
232- printf '{"version":"%s","url":"https://updates.crowpilot.in/crowmotion/crowmotion-%s-%s.bin"}\n' \
233- "$VER" "$VER" "$T" > "site/crowmotion/latest-$T.json"
234- done
235- cp out/esp32c3/crowmotion.bin "site/crowmotion/crowmotion-${VER}.bin"
58+ esptool.py --chip esp32c3 merge_bin -o site/flash/crowmotion-merged.bin \
59+ --flash_mode dio --flash_size 4MB \
60+ 0x0 build/bootloader/bootloader.bin \
61+ 0x8000 build/partition_table/partition-table.bin \
62+ 0xf000 build/ota_data_initial.bin \
63+ 0x20000 build/crowmotion.bin
64+ cp build/crowmotion.bin "site/crowmotion/crowmotion-${VER}.bin"
23665 printf '{"version":"%s","url":"https://updates.crowpilot.in/crowmotion/crowmotion-%s.bin"}\n' \
23766 "$VER" "$VER" > site/crowmotion/latest.json
238- echo "manifests:" && cat site/crowmotion/latest.json site/crowmotion/latest-*.json
67+ cp site/crowmotion/latest.json site/crowmotion/latest-esp32c3.json
68+ cat > site/flash/manifest.json <<EOF
69+ {"name":"CrowMotion","version":"${VER}","new_install_prompt_erase":true,"builds":[{"chipFamily":"ESP32-C3","parts":[{"path":"crowmotion-merged.bin","offset":0}]}]}
70+ EOF
23971
240- - name : Attach binaries to the release
72+ - name : Attach binaries to release
24173 env :
24274 GH_TOKEN : ${{ github.token }}
24375 run : |
24476 VER="${TAG#v}"
245- cp bridge/build/crowlink.bin "crowlink-${VER}.bin"
246- gh release upload "$TAG" \
247- "site/crowmotion/crowmotion-${VER}-esp32c3.bin" \
248- "site/crowmotion/crowmotion-${VER}-esp32s3.bin" \
249- "site/crowmotion/crowmotion-${VER}-esp32c6.bin" \
250- "crowlink-${VER}.bin" \
251- "site/flash/crowmotion-merged-esp32c3.bin" \
252- "site/flash/crowmotion-merged-esp32s3.bin" \
253- "site/flash/crowmotion-merged-esp32c6.bin" \
254- "site/flash/crowlink-merged.bin" --clobber
77+ gh release upload "$TAG" "site/crowmotion/crowmotion-${VER}.bin" \
78+ site/flash/crowmotion-merged.bin --clobber
25579
256- - name : Deploy to Cloudflare Pages
80+ - name : Deploy update site
25781 uses : cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
25882 with :
25983 apiToken : ${{ secrets.CLOUDFLARE_API_TOKEN }}
26084 accountId : 2fca98f9bd732794c53117b67c5b4593
261- # --branch main forces a production deployment; the release event
262- # checks out a detached tag, which wrangler would otherwise treat
263- # as a preview branch ("HEAD") and never update the live site.
26485 command : pages deploy site --project-name crowmotion-updates --branch main
0 commit comments