Repository navigation
286 lines (269 loc) · 12.6 KB
/
Copy pathrelease.yml
File metadata and controls
286 lines (269 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
name: Build and Release Firmware
# Tag-driven release: push `vX.Y.Z` to build the firmware in the official
# ESP-IDF container and attach the artifacts to a GitHub Release. Manual
# dispatch is supported too for smoke-testing the CI without cutting a tag.
#
# Layout (2026-06 onwards):
# 0. `plan` job derives the version / release channel / board list from
# the tag (script/release_channels.py). Channels (2026-09):
# vX.Y.Z-alpha.N → alpha (cores3 + atoms3r only, GitHub pre-release)
# vX.Y.Z-beta.N → beta (all boards, pre-release)
# vX.Y.Z-rc.N → rc (all boards, pre-release)
# vX.Y.Z → stable (all boards, becomes "Latest")
# A tag that doesn't match this grammar fails the run before building.
# 1. `build` job runs once per board in parallel (matrix from `plan`),
# each shard does its own checkout + env init + ccache-warmed build +
# zip + artifact upload. Per-shard ccache cache survives across runs
# and pulls main branch hits even on a fresh tag, keeping compile time
# ~30 s after the first warm-up.
# 2. `release` job (needs: build) downloads the artifacts and attaches
# them to a single GitHub Release in one go. Only runs on tag pushes.
#
# Previously the build was one sequential job to avoid paying the Docker
# pull + apt install + IDF env setup 4× (was ~2 min × 4 in the old plain
# matrix). With matrix shards on separate VMs the env-init cost is paid
# 4× but happens in parallel — wall-clock impact is ≈ 1× pull. The
# ccache cache cuts the per-shard compile from ~3 min to ~30 s once warm,
# so the wall-clock floor becomes the env-init step (~90 s) rather than
# the compile, and we no longer benefit from sharing the env across the
# 4 boards inside one job.
on:
push:
tags:
- 'v*'
workflow_dispatch:
# softprops/action-gh-release needs to create a Release on the repo. Default
# token scopes are tight under modern GitHub orgs / repo settings, so grant
# `contents: write` here explicitly rather than relying on
# Settings → Actions → Workflow permissions.
permissions:
contents: write
jobs:
plan:
name: Plan (version / channel / boards)
runs-on: ubuntu-latest
outputs:
version: ${{ steps.plan.outputs.version }}
channel: ${{ steps.plan.outputs.channel }}
prerelease: ${{ steps.plan.outputs.prerelease }}
boards: ${{ steps.plan.outputs.boards }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: script
- id: plan
shell: bash
run: |
set -euo pipefail
if [ "${{ github.ref_type }}" = "tag" ]; then
VERSION=${GITHUB_REF#refs/tags/}
# Refuse typos like v0.15.0-alpha1 before spending 4 build VMs.
python3 script/release_channels.py --tag "$VERSION" validate
else
VERSION="dev-${GITHUB_SHA::8}"
fi
CHANNEL=$(python3 script/release_channels.py --tag "$VERSION" channel)
PRERELEASE=$(python3 script/release_channels.py --tag "$VERSION" prerelease)
BOARDS=$(python3 script/release_channels.py --tag "$VERSION" boards)
{
echo "version=$VERSION"
echo "channel=$CHANNEL"
echo "prerelease=$PRERELEASE"
echo "boards=$BOARDS"
} >> "$GITHUB_OUTPUT"
echo "Version: $VERSION channel: $CHANNEL prerelease: $PRERELEASE boards: $BOARDS"
build:
name: Build ${{ matrix.board }}
needs: plan
runs-on: ubuntu-latest
strategy:
# Don't kill sibling shards on one board's failure — we still want
# to know which other boards built OK so we can rerun only the bad
# one. The aggregating release job below depends on all of the
# artifacts though, so a partial success still won't publish.
fail-fast: false
matrix:
board: ${{ fromJSON(needs.plan.outputs.boards) }}
container:
# Pinned to v5.5 to match local development. v6.0 dropped the built-in
# `json` (cJSON) component in favour of an IDF component-manager
# dependency; our `conversation` component requires it directly and
# we don't ship a manifest entry for it yet.
image: espressif/idf:v5.5.5
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: 'recursive'
fetch-depth: 0 # full history so `git describe --tags` resolves
- name: Verify git version information
shell: bash
run: |
# Container's git refuses operations in a workspace it doesn't own;
# mark it safe before any git command.
git config --global --add safe.directory "$GITHUB_WORKSPACE"
echo "=== Git Information ==="
git describe --tags --abbrev=0 || echo "(no tags found)"
git rev-parse --short HEAD
git diff-index --quiet HEAD -- && echo "Working tree clean" || echo "Working tree dirty"
echo "Current ref: ${GITHUB_REF}"
- name: Install Node.js (avatar DSL compiler)
shell: bash
# components/avatar_vm/CMakeLists.txt invokes `node tools/avatar_dsl/cli.mjs`
# at build time to compile the default avatar face DSL into the
# bytecode that gets embedded into the firmware. The espressif/idf
# Debian image does not ship Node, so install it from the distro repo
# (the compiler is plain ESM that only needs a recent v18+).
run: |
apt-get update
apt-get install -y --no-install-recommends nodejs
node --version
- name: Apply M5Unified patches
shell: bash
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE/components/M5Unified"
tools/apply-m5-patches.sh
- name: ccache (compiler cache)
# Per-board cache key — the same .c source can compile differently
# for different boards (sdkconfig.defaults.<board> changes target /
# PSRAM mode / Kconfig macros), so we don't try to share .o files
# across shards. Restore-keys lets a new tag still pick up the
# most recent main-branch cache on cold start.
uses: hendrikmuhs/ccache-action@v1.2
with:
key: idf-v5.5-${{ matrix.board }}-${{ github.ref_name }}
restore-keys: |
idf-v5.5-${{ matrix.board }}-
max-size: 500M
variant: ccache
# Tell ESP-IDF's cmake module to actually invoke ccache. Without
# this env var ESP-IDF skips its compiler-launcher hook even if
# ccache is on PATH.
create-symlink: true
- name: Extract version
id: version
shell: bash
run: |
# Resolved once in the `plan` job so every shard and the release
# job agree on the string (it's also stamped into esp_app_desc).
VERSION="${{ needs.plan.outputs.version }}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (channel ${{ needs.plan.outputs.channel }})"
- name: Pin PROJECT_VER for the build
# `tools/apply-m5-patches.sh` modifies the M5Unified submodule's
# working tree, which makes ESP-IDF's internal
# `git describe --tags --dirty` (run during cmake) tack a -dirty
# suffix onto the version stamped into esp_app_desc. Writing the
# resolved version to version.txt at the project root takes priority
# over git_describe (see project.cmake's PROJECT_VER resolution
# order: version.txt > project(VERSION ...) > git describe) and
# gives us a clean, deterministic version string in CI builds.
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "$VERSION" > version.txt
echo "version.txt contents: $(cat version.txt)"
- name: Build firmware for ${{ matrix.board }}
shell: bash
env:
CI: true
VERSION: ${{ steps.version.outputs.version }}
BOARD: ${{ matrix.board }}
IDF_CCACHE_ENABLE: "1"
run: |
set -euo pipefail
. $IDF_PATH/export.sh
echo "=== ccache config ==="
ccache --version | head -1
ccache --show-config | grep -E "max_size|cache_dir|compiler_check" || true
echo "=== Cleaning build-$BOARD ==="
rm -rf "build-$BOARD"
echo "=== Setting target for $BOARD ==="
make set-target BOARD="$BOARD"
echo "=== Building $BOARD ==="
make build BOARD="$BOARD"
echo "=== Building Recovery for $BOARD (ADR-001) ==="
# Recovery は固定領域に置く小さな更新専用アプリ (recovery/)。ブートローダーは
# Main と同じ bootloader_components/main。version.txt は recovery/ にも置く。
cp version.txt recovery/version.txt
rm -rf "recovery/build-$BOARD"
make -C recovery set-target BOARD="$BOARD" IDF_PATH="$IDF_PATH"
make -C recovery build BOARD="$BOARD" IDF_PATH="$IDF_PATH"
echo "=== ccache stats ==="
ccache --show-stats || true
echo "=== Packing release for $BOARD (ADR-001 layout) ==="
# bootloader / 機体正本の標準表 / bootctl / recovery / exttab / Main と、
# Web flasher が読む flash_manifest.json、0x0 から書ける結合イメージ。
python3 script/pack_release.py --board "$BOARD" --version "$VERSION" --out "release/$BOARD"
cp README.md "release/$BOARD/" 2>/dev/null || true
echo "=== Firmware size for $BOARD ==="
python3 -m esp_idf_size "build-$BOARD/stackchan_idf.map" --format=text || true
(cd "release/$BOARD" && zip -r "../../firmware-${VERSION}-${BOARD}.zip" .)
ls -lh "firmware-${VERSION}-${BOARD}.zip"
- name: Upload firmware artifact (${{ matrix.board }})
uses: actions/upload-artifact@v4
with:
name: firmware-${{ steps.version.outputs.version }}-${{ matrix.board }}
path: firmware-${{ steps.version.outputs.version }}-${{ matrix.board }}.zip
retention-days: 90
release:
name: Create GitHub Release
needs: [plan, build]
# Only attach to a Release on actual tag pushes — manual dispatch runs
# just build the artifacts and stop. The matrix shards all upload
# their own artifact; this job aggregates them.
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history + tags: release_notes.py finds the previous tag for
# the "変更点 (vX.Y.Z から)" range and falls back to the commit list
# when docs/releases/<tag>.md is absent.
fetch-depth: 0
sparse-checkout: |
script
docs/releases
- name: Extract version / write release notes
id: version
shell: bash
run: |
VERSION="${{ needs.plan.outputs.version }}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Body = hand-written docs/releases/<tag>.md (主要変更点) or the
# commit list since the previous tag, plus the per-channel banner,
# the boards actually built and the standing install notes. No
# fixed feature list and no internal jargon — see the script.
python3 script/release_notes.py \
--tag "$VERSION" \
--boards '${{ needs.plan.outputs.boards }}' \
--repo "$GITHUB_REPOSITORY" \
--commit "$GITHUB_SHA" \
--build-date "$BUILD_DATE" \
--idf v5.5.5 > release_notes.md
echo "=== release_notes.md ==="
cat release_notes.md
- name: Download all firmware artifacts
uses: actions/download-artifact@v4
with:
# Glob over the per-board artifact names uploaded by the build
# matrix. merge-multiple flattens them into one directory.
pattern: firmware-${{ steps.version.outputs.version }}-*
merge-multiple: true
- name: Create / update Release
uses: softprops/action-gh-release@v2
with:
# Whatever boards this channel built (alpha = 2, others = 4).
files: firmware-${{ steps.version.outputs.version }}-*.zip
fail_on_unmatched_files: true
# Pre-release tags never become "Latest": the Releases page, the
# /releases/latest redirect and the pre-release badge all follow
# the channel derived from the tag.
prerelease: ${{ needs.plan.outputs.prerelease == 'true' }}
make_latest: ${{ needs.plan.outputs.prerelease != 'true' }}
body_path: release_notes.md
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}