-
Notifications
You must be signed in to change notification settings - Fork 61
289 lines (269 loc) · 13 KB
/
Copy pathamyboard-pr-preview.yml
File metadata and controls
289 lines (269 loc) · 13 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
287
288
289
name: AMYboard PR preview
# Per-PR preview of the AMYboard web editor + flasher, with this PR's firmware
# bundled in, deployed to a unique URL (amyboard-pr-<N>.vercel.app). The bundled
# firmware means the preview flasher can ONLY flash this PR's build. Torn down
# by amyboard-pr-preview-cleanup.yml when the PR closes.
#
# Requires repo secret VERCEL_TOKEN (scope: bwhitmans-projects). The amyboard-pr
# Vercel project is deployed to as preview deployments and aliased per PR.
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'amy'
- 'tulip/amyboard/**'
# AMYboard firmware also compiles shared ESP32-S3 sources from here
# (e.g. tulip/esp32s3/usb.c), so changes there must rebuild the preview.
- 'tulip/esp32s3/**'
- 'tulip/amyboardweb/**'
# All of shared/ (py + C) is frozen into AMYboard firmware, so any change must
# rebuild the preview. It also produces the tulip-firmware artifact that
# tulip-pr-preview reuses for tulip.upgrade(pr=N).
- 'tulip/shared/**'
- '.github/workflows/amyboard-pr-preview.yml'
# Fired by shorepine/amy's amyboard-hwci-trigger.yml on an AMY PR: build this
# pipeline with the `amy` submodule pinned to client_payload.amy_sha, then chain
# to HW CI (which comments back on the AMY PR). See the AMY-PR plumbing below.
repository_dispatch:
types: [amy-pr]
# Manual fallback for an AMY *fork* PR (auto-dispatch is same-repo only): a
# maintainer supplies the fork's amy PR number + SHA after reviewing the diff.
workflow_dispatch:
inputs:
amy_pr:
description: "AMY PR number (for the preview alias + result comment)"
required: true
amy_sha:
description: "AMY commit SHA to pin the submodule to"
required: true
permissions:
contents: read
pull-requests: write
concurrency:
# Namespace by event so an AMY PR #N and a Tulip PR #N never cancel each other.
group: amyboard-pr-preview-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.client_payload.amy_pr || github.event.inputs.amy_pr }}
cancel-in-progress: true
jobs:
preview:
runs-on: ubuntu-latest
steps:
# Normalize the trigger: a Tulip PR (pull_request) vs an AMY PR (dispatched
# from shorepine/amy, or manual for a fork). Everything downstream keys off
# these outputs instead of github.event.pull_request.* directly.
- name: Resolve PR context (Tulip PR vs AMY PR)
id: ctx
uses: actions/github-script@v7
with:
script: |
const ev = context.eventName;
let isAmy = false, num, amySha = '', amyRepo = 'shorepine/amy';
if (ev === 'pull_request') {
num = context.payload.pull_request.number; // Tulip PR
} else if (ev === 'repository_dispatch') { // AMY PR (auto)
isAmy = true;
const cp = context.payload.client_payload || {};
num = cp.amy_pr; amySha = cp.amy_sha || ''; amyRepo = cp.amy_repo || amyRepo;
} else if (ev === 'workflow_dispatch') { // AMY fork PR (manual)
isAmy = true;
num = context.payload.inputs.amy_pr; amySha = context.payload.inputs.amy_sha || '';
}
if (isAmy && !amySha) { core.setFailed('AMY run requires an amy SHA'); return; }
core.setOutput('is_amy', String(isAmy));
core.setOutput('num', String(num));
core.setOutput('amy_sha', amySha);
core.setOutput('amy_repo', amyRepo);
core.setOutput('alias', `${isAmy ? 'amyboard-amypr' : 'amyboard-pr'}-${num}`);
core.info(`event=${ev} pr=${num} amy=${isAmy} sha=${amySha || '-'}`);
# Fail fast on a missing/mis-scoped token, before the ~15-min build.
- name: Verify Vercel token can access the team
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
if [ -z "$VERCEL_TOKEN" ]; then
echo "::error::VERCEL_TOKEN secret is not set — add it (scope bwhitmans-projects)."; exit 1
fi
npm i -g vercel@latest >/dev/null 2>&1
if ! vercel project ls --scope bwhitmans-projects --token "$VERCEL_TOKEN" >/dev/null 2>&1; then
echo "::error::VERCEL_TOKEN cannot access the 'bwhitmans-projects' team — recreate the token with that scope."; exit 1
fi
echo "Vercel token OK for bwhitmans-projects."
- uses: actions/checkout@v4
with:
submodules: recursive
# For an AMY PR, point the `amy` submodule at the PR's commit so the firmware
# is built from THIS PR's AMY code — the whole reason for the dispatch. The
# build reads amy sources by path (esp32_common.cmake → ../../amy/src).
#
# Checking out amy's working tree is NOT enough on its own: the esp-idf
# firmware step below runs `git submodule update` inside a root Docker
# container, which would reset amy back to the superproject's *recorded*
# gitlink (main's pin) — silently building main's amy instead of the PR's.
# So we also `git add amy` to record the PR SHA as the gitlink; a later
# `git submodule update` then keeps the PR commit.
- name: Pin amy submodule to the PR SHA (AMY runs only)
if: steps.ctx.outputs.is_amy == 'true'
env:
AMY_SHA: ${{ steps.ctx.outputs.amy_sha }}
run: |
set -euo pipefail
case "$AMY_SHA" in
""|*[!0-9a-fA-F]*) echo "::error::bad amy SHA: '$AMY_SHA'"; exit 1 ;;
esac
git -C amy fetch origin "$AMY_SHA"
git -C amy checkout --detach "$AMY_SHA"
echo "amy pinned to:"; git -C amy log -1 --oneline
# Record the new gitlink in the superproject index so the container's
# `git submodule update` can't revert amy to main's pin.
git add amy
# --- Firmware (.bin sets) ---
# Build AMYBOARD then TULIP4_R11 in ONE esp-idf container: the two targets
# share most sources, so the second build hits ccache for the common units
# (we enable IDF_CCACHE_ENABLE) instead of recompiling from scratch. The
# Tulip firmware is what the HW CI bench flashes onto the physical Tulip
# (it shares the AMYboard bench); see tulip/amyboard/hwci/tulip_hwci.py.
- name: Build AMYboard + Tulip firmware
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.4.1
target: esp32s3
path: tulip/amyboard
command: >-
export IDF_CCACHE_ENABLE=1 &&
python -m pip install littlefs-python &&
idf.py -DMICROPY_BOARD=AMYBOARD build &&
cd .. &&
python fs_create.py amyboard &&
cd esp32s3 &&
idf.py -DMICROPY_BOARD=TULIP4_R11 build &&
cd .. &&
python fs_create.py tulip
- name: Upload AMYboard firmware artifact
uses: actions/upload-artifact@v4
with:
name: amyboard-firmware
if-no-files-found: error
path: |
tulip/amyboard/dist/amyboard-firmware-AMYBOARD.bin
tulip/amyboard/dist/amyboard-full-AMYBOARD.bin
tulip/amyboard/dist/amyboard-sys.bin
# The HW CI workflow (chained off this one via workflow_run) downloads this
# to flash the physical Tulip — there's no Vercel preview for Tulip firmware.
- name: Upload Tulip firmware artifact
uses: actions/upload-artifact@v4
with:
name: tulip-firmware
if-no-files-found: error
path: |
tulip/esp32s3/dist/tulip-firmware-TULIP4_R11.bin
tulip/esp32s3/dist/tulip-full-TULIP4_R11.bin
tulip/esp32s3/dist/tulip-sys.bin
# The esp-idf firmware step above runs in a Docker container as root, so any
# file it created or rewrote in the bind-mounted workspace is now owned by
# root:root. The web build below runs as the (non-root) runner and, for AMY
# PRs, regenerates amy/amy/constants.py via `make web` (the pinned amy.h is
# newer than the committed patches.h). That write fails with "Permission
# denied" on the root-owned file — so reclaim ownership first.
- name: Reclaim workspace ownership after the root container build
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE"
# --- Web stage/ (amy + amyboard WASM + static) ---
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install web build deps
run: pip install numpy
- uses: mymindstorm/setup-emsdk@v14
with:
version: 4.0.22
- name: Build amyboardweb stage/
run: |
cd tulip/amyboardweb
python3 dev.py --build-only
# --- Bundle this PR's firmware into the site ---
- name: Bundle firmware into stage/
run: |
mkdir -p tulip/amyboardweb/stage/firmware
cp tulip/amyboard/dist/amyboard-full-AMYBOARD.bin \
tulip/amyboard/dist/amyboard-firmware-AMYBOARD.bin \
tulip/amyboard/dist/amyboard-sys.bin \
tulip/amyboardweb/stage/firmware/
# --- Deploy preview + stable per-PR alias ---
- name: Deploy preview to Vercel
id: deploy
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# amyboard-pr-<N> for Tulip PRs, amyboard-amypr-<N> for AMY PRs — one
# Vercel project, a distinct per-PR alias, so the two PR-number spaces
# can never collide on the same hostname.
ALIAS_HOST: ${{ steps.ctx.outputs.alias }}
run: |
if [ -z "$VERCEL_TOKEN" ]; then
echo "::error::VERCEL_TOKEN secret is not set — add it (scope bwhitmans-projects) to enable PR previews."
exit 1
fi
npm i -g vercel@latest >/dev/null 2>&1
cd tulip/amyboardweb
vercel link --yes --token "$VERCEL_TOKEN" --scope bwhitmans-projects --project amyboard-pr --cwd stage
url=$(vercel deploy stage --token "$VERCEL_TOKEN" --scope bwhitmans-projects --yes)
alias="${ALIAS_HOST}.vercel.app"
vercel alias set "$url" "$alias" --token "$VERCEL_TOKEN" --scope bwhitmans-projects
echo "url=https://$alias" >> "$GITHUB_OUTPUT"
# Thread the AMY-PR identity to the HW CI run. HW CI chains off this run via
# workflow_run and otherwise has no way to know which AMY PR built the
# firmware, where to fetch it, or where to comment — so hand it off here.
- name: Publish AMY context for HW CI
if: steps.ctx.outputs.is_amy == 'true'
env:
AMY_PR: ${{ steps.ctx.outputs.num }}
AMY_REPO: ${{ steps.ctx.outputs.amy_repo }}
AMY_SHA: ${{ steps.ctx.outputs.amy_sha }}
ALIAS_HOST: ${{ steps.ctx.outputs.alias }}
run: |
mkdir -p amy-ctx
python3 - <<'PY' > amy-ctx/ctx.json
import json, os
json.dump({
"amy_pr": int(os.environ["AMY_PR"]), # int() validates it's numeric
"amy_repo": os.environ["AMY_REPO"],
"amy_sha": os.environ["AMY_SHA"],
"alias": os.environ["ALIAS_HOST"],
}, open(1, "w"))
PY
cat amy-ctx/ctx.json
- name: Upload AMY context artifact
if: steps.ctx.outputs.is_amy == 'true'
uses: actions/upload-artifact@v4
with:
name: amy-ctx
path: amy-ctx/ctx.json
if-no-files-found: error
# --- Comment (upsert) the preview URL on the PR ---
# Tulip PRs only. AMY-PR runs get a single result comment posted on the AMY
# PR by the HW CI workflow instead (this run has no Tulip PR to comment on).
- name: Comment preview URL
if: success() && github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
PREVIEW_URL: ${{ steps.deploy.outputs.url }}
with:
script: |
const url = process.env.PREVIEW_URL;
const marker = '<!-- amyboard-pr-preview -->';
const body = [
marker,
'### 🔌 AMYboard PR preview',
'',
`**Editor + flasher:** ${url}/editor/`,
'',
"This preview bundles **this PR's firmware** — its flasher only flashes this build (not the release). Rebuilt on every push; removed when the PR closes.",
'',
'The hardware CI has been kicked off and should return within a few minutes, stand by!',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body });
}