-
-
Notifications
You must be signed in to change notification settings - Fork 229
391 lines (351 loc) · 18.6 KB
/
Copy pathupdate-rpi-imager-json.yml
File metadata and controls
391 lines (351 loc) · 18.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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#############################################################################
# Update rpi-imager/rpi-imager_falcon_player.json from a published release.
#
# Manual only (workflow_dispatch). Downloads each platform's .img.zip asset
# from a GitHub release, computes the four hash/size fields Raspberry Pi
# Imager needs (compressed zip size+sha256, and the raw .img's extracted
# size+sha256), updates the matching entry in the JSON, and opens a PR --
# it never pushes straight to master.
#
# Only platforms that already have an entry in the JSON are updated (Pi,
# BBB/PB, BB64/PB2 as of this writing). A release asset with no matching
# existing entry (e.g. Pi64, which isn't published to rpi-imager today) is
# skipped with a warning rather than guessing at a new entry's "devices"
# list, icon, etc.
#############################################################################
name: Update RPi Imager JSON
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to use (blank = latest published release)'
required: false
default: ''
dry_run:
description: 'Compute and show the diff but do not open a PR'
type: boolean
required: false
default: false
concurrency:
group: update-rpi-imager-json
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout master
uses: actions/checkout@v5
with:
ref: master
- name: Resolve release
id: release
run: |
if [ -n "${{ inputs.tag }}" ]; then
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${{ inputs.tag }}" > release.json
else
# /releases/latest excludes prereleases entirely by GitHub's own
# definition -- that hid 10.0-beta (published, prerelease=true)
# behind the older 9.5 stable release. List all releases instead,
# drop drafts (build releases start as drafts -- see
# build-images.yml's "release" job -- so an unpublished one is
# still never picked up by accident), and take whichever
# published release is newest, prerelease or not.
gh api "repos/${GITHUB_REPOSITORY}/releases" --paginate \
| jq -s 'add | map(select(.draft == false)) | sort_by(.published_at) | last' > release.json
fi
TAG=$(jq -r '.tag_name' release.json)
if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then
echo "::error::Could not resolve a release (tag input: '${{ inputs.tag }}')." >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Resolved release: $TAG"
# NOTE: this repo reuses one release/tag across patch bumps -- the
# "9.5" release carries FPP-v9.5-Pi.img.zip, FPP-v9.5.1-*, ...,
# FPP-v9.5.3-*, uploaded on different dates, all under tag_name
# "9.5". So the per-platform VERSION and release date must come
# from the *matched asset itself*, never from this release object's
# tag_name/published_at -- using those here previously overwrote
# correct "9.5.3" entries back down to "9.5" (PR #2772).
- name: Download platform images and compute hashes
id: hashes
run: |
set -euo pipefail
mkdir -p work
# Suffix must match the trailing "-<suffix>.img.zip" of the asset
# filename exactly (a plain substring match would let "Pi" match
# "Pi64" too).
for SUFFIX in Pi Pi64 BB64 BBB; do
MATCH=$(jq -c --arg s "$SUFFIX" \
'[.assets[] | select(.name | test("-" + $s + "\\.img\\.zip$"))] | sort_by(.updated_at) | last // empty' \
release.json)
if [ -z "$MATCH" ]; then
echo "No .img.zip asset for platform $SUFFIX in this release -- skipping."
continue
fi
NAME=$(jq -r '.name' <<<"$MATCH")
URL=$(jq -r '.browser_download_url' <<<"$MATCH")
# Per-asset, not per-release: this repo re-uploads patch bumps
# (9.5, 9.5.1, ..., 9.5.3) onto the SAME release/tag over time,
# so the release object's own tag_name/published_at are stale
# for anything past the first upload (see the note above).
ASSET_DATE=$(jq -r '(.updated_at // .created_at)' <<<"$MATCH" | cut -d'T' -f1)
# Asset filenames are FPP-v<VERSION>-<SUFFIX>.img.zip (see
# SD/build-image-pi.sh's OUT_IMG); strip the fixed prefix/suffix
# to recover VERSION even though it can differ from the release
# tag_name (FPP-v9.5.3-Pi.img.zip on tag "9.5").
ASSET_VERSION="${NAME#FPP-v}"
ASSET_VERSION="${ASSET_VERSION%-${SUFFIX}.img.zip}"
echo "=== $SUFFIX: $NAME (version $ASSET_VERSION, $ASSET_DATE) ==="
ZIP="work/$NAME"
curl -fL --retry 3 -o "$ZIP" "$URL"
ZIP_SIZE=$(stat -c%s "$ZIP")
ZIP_SHA=$(sha256sum "$ZIP" | awk '{print $1}')
IMG_DIR="work/extract-$SUFFIX"
rm -rf "$IMG_DIR"
mkdir -p "$IMG_DIR"
unzip -q "$ZIP" -d "$IMG_DIR"
IMG_FILE=$(find "$IMG_DIR" -maxdepth 1 -name '*.img' | head -n1)
if [ -z "$IMG_FILE" ]; then
echo "::error::No .img found inside $NAME" >&2
exit 1
fi
IMG_SIZE=$(stat -c%s "$IMG_FILE")
IMG_SHA=$(sha256sum "$IMG_FILE" | awk '{print $1}')
# Free the extracted .img right away -- these are multi-GB and
# four platforms in a row will exhaust the runner's disk otherwise.
rm -rf "$IMG_DIR"
rm -f "$ZIP"
{
echo "${SUFFIX}_url=$URL"
echo "${SUFFIX}_version=$ASSET_VERSION"
echo "${SUFFIX}_release_date=$ASSET_DATE"
echo "${SUFFIX}_image_download_size=$ZIP_SIZE"
echo "${SUFFIX}_image_download_sha256=$ZIP_SHA"
echo "${SUFFIX}_extract_size=$IMG_SIZE"
echo "${SUFFIX}_extract_sha256=$IMG_SHA"
} >> "$GITHUB_OUTPUT"
done
- name: Update JSON
id: update
env:
TAG: ${{ steps.release.outputs.tag }}
Pi_url: ${{ steps.hashes.outputs.Pi_url }}
Pi_version: ${{ steps.hashes.outputs.Pi_version }}
Pi_release_date: ${{ steps.hashes.outputs.Pi_release_date }}
Pi_image_download_size: ${{ steps.hashes.outputs.Pi_image_download_size }}
Pi_image_download_sha256: ${{ steps.hashes.outputs.Pi_image_download_sha256 }}
Pi_extract_size: ${{ steps.hashes.outputs.Pi_extract_size }}
Pi_extract_sha256: ${{ steps.hashes.outputs.Pi_extract_sha256 }}
Pi64_url: ${{ steps.hashes.outputs.Pi64_url }}
Pi64_version: ${{ steps.hashes.outputs.Pi64_version }}
Pi64_release_date: ${{ steps.hashes.outputs.Pi64_release_date }}
Pi64_image_download_size: ${{ steps.hashes.outputs.Pi64_image_download_size }}
Pi64_image_download_sha256: ${{ steps.hashes.outputs.Pi64_image_download_sha256 }}
Pi64_extract_size: ${{ steps.hashes.outputs.Pi64_extract_size }}
Pi64_extract_sha256: ${{ steps.hashes.outputs.Pi64_extract_sha256 }}
BB64_url: ${{ steps.hashes.outputs.BB64_url }}
BB64_version: ${{ steps.hashes.outputs.BB64_version }}
BB64_release_date: ${{ steps.hashes.outputs.BB64_release_date }}
BB64_image_download_size: ${{ steps.hashes.outputs.BB64_image_download_size }}
BB64_image_download_sha256: ${{ steps.hashes.outputs.BB64_image_download_sha256 }}
BB64_extract_size: ${{ steps.hashes.outputs.BB64_extract_size }}
BB64_extract_sha256: ${{ steps.hashes.outputs.BB64_extract_sha256 }}
BBB_url: ${{ steps.hashes.outputs.BBB_url }}
BBB_version: ${{ steps.hashes.outputs.BBB_version }}
BBB_release_date: ${{ steps.hashes.outputs.BBB_release_date }}
BBB_image_download_size: ${{ steps.hashes.outputs.BBB_image_download_size }}
BBB_image_download_sha256: ${{ steps.hashes.outputs.BBB_image_download_sha256 }}
BBB_extract_size: ${{ steps.hashes.outputs.BBB_extract_size }}
BBB_extract_sha256: ${{ steps.hashes.outputs.BBB_extract_sha256 }}
run: |
python3 <<'PY'
import json
import os
import re
path = "rpi-imager/rpi-imager_falcon_player.json"
with open(path) as f:
data = json.load(f)
# FPP tags are plain numeric for stable releases (9.5.3, 9.6) and
# carry a letter suffix for anything else (10.0-beta, 10.0-beta2,
# 10.0-rc1) -- see build-images.yml's tag-shape comment. Use that
# to sort entries into (at most) one "stable" slot and one
# "prerelease" slot per platform, so e.g. releasing 10.0-beta2
# replaces 10.0-beta1 in place, and releasing 9.6 replaces 9.5 --
# while a prerelease never touches the stable entry and vice versa.
def channel_of(v):
return "prerelease" if re.search(r"[A-Za-z]", v) else "stable"
# major.minor family, e.g. "10.0-beta2" / "10.0-rc1" / "10.0" -> "10.0",
# "9.5.3" -> "9.5". Used so a stable GA release (10.0) supersedes every
# prerelease of that same family (10.0-beta, 10.0-beta2, 10.0-rc1, ...).
def family_of(v):
m = re.match(r"^(\d+\.\d+)", v)
return m.group(1) if m else v
# Longest suffix first: "Pi64" must be checked before "Pi", since
# "Pi" is a substring of "Pi64" and would otherwise match both.
PLATFORM_SUFFIXES = ("Pi64", "Pi", "BB64", "BBB")
name_re = re.compile(r"^FPP\s+v(\S+)\s+(.*)$")
# A platform with no existing JSON entry at all is skipped by
# default (see below) rather than guessing its "devices" list --
# guessing that field has bitten this file before (2024's
# "pi4-64bit" experiment was reverted a month later for possibly
# breaking flashing for users). Pi64 is a deliberate, verified
# exception: cross-checked against both the official rpi-imager
# schema example (doc/os-sublist-example.json) and the live
# production os_list, which use "<board>-64bit" tags (no suffix on
# pi1/pi2 entries since those chips are 32-bit only) -- e.g.
# Raspberry Pi OS (64-bit) ships devices: ["pi5-64bit", "pi4-64bit",
# "pi3-64bit"]. Mirrors this file's existing "Pi" entry's
# "-32bit"-suffixed convention exactly.
NEW_PLATFORM_TEMPLATES = {
"Pi64": {
"label": "Pi64",
"description": "Falcon Player for 64-bit capable RPi models",
"devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit"],
},
}
def entry_platform(o):
for suffix in PLATFORM_SUFFIXES:
if re.search(rf"-{re.escape(suffix)}\.img\.zip$", o["url"]):
return suffix
return None
def entry_channel(o):
m = name_re.match(o.get("name", ""))
return channel_of(m.group(1)) if m else "stable"
added, updated, removed_dupes, superseded, skipped = [], [], [], [], []
channels_seen = set()
for suffix in PLATFORM_SUFFIXES:
url = os.environ.get(f"{suffix}_url", "")
if not url:
continue # no asset for this platform in the release
# Per-platform, not per-release: see the note by the "Resolve
# release" step -- a single release/tag can carry several
# differently-versioned assets uploaded over time.
version = os.environ[f"{suffix}_version"]
release_date = os.environ[f"{suffix}_release_date"]
new_channel = channel_of(version)
new_family = family_of(version)
channels_seen.add(new_channel)
same_platform = [o for o in data["os_list"] if entry_platform(o) == suffix]
same_slot = [o for o in same_platform if entry_channel(o) == new_channel]
if new_channel == "stable":
# A stable release supersedes every prerelease build of the
# same major.minor family: 10.0 GA removes 10.0-beta,
# 10.0-beta2, 10.0-rc1, etc. for this platform. Prereleases
# of an *unrelated* family (e.g. an 11.0-beta already in
# flight when 10.0 ships) are left alone.
for stale in [o for o in same_platform
if entry_channel(o) == "prerelease"
and family_of(name_re.match(o.get("name", "")).group(1)) == new_family]:
data["os_list"].remove(stale)
superseded.append(stale.get("name", suffix))
if same_slot:
entry = same_slot[0]
for dupe in same_slot[1:]:
# Shouldn't normally happen -- more than one existing
# entry in the same (platform, channel) slot. Collapse
# to the first and drop the rest rather than leaving
# ambiguous duplicates behind.
data["os_list"].remove(dupe)
removed_dupes.append(dupe.get("name", suffix))
updated.append(suffix)
elif same_platform:
# First release ever seen in this channel for this platform
# (e.g. the very first *-beta build). Clone devices/icon/
# website/init_format from the existing same-platform entry
# so those don't have to be guessed.
template = same_platform[0]
entry = dict(template)
entry["devices"] = list(template["devices"])
data["os_list"].append(entry)
added.append(suffix)
elif suffix in NEW_PLATFORM_TEMPLATES:
# Platform has never had a JSON entry in any channel, but
# it's on the verified allowlist above -- build a fresh
# entry from a same-repo sibling for the generic fields
# (icon/website/init_format never vary between platforms
# here) plus the verified description/devices.
spec = NEW_PLATFORM_TEMPLATES[suffix]
sibling = data["os_list"][0] if data["os_list"] else {}
entry = {
"name": f"FPP v{version} {spec['label']}",
"description": spec["description"],
"url": url,
"icon": sibling.get("icon", ""),
"init_format": sibling.get("init_format", "none"),
"website": sibling.get("website", "https://www.falconplayer.com/"),
"devices": list(spec["devices"]),
}
data["os_list"].append(entry)
added.append(suffix)
else:
# Genuinely unknown platform with no template and no
# verified device list -- skip rather than guess.
skipped.append(suffix)
continue
m = name_re.match(entry.get("name", ""))
label = m.group(2) if m else suffix
entry["name"] = f"FPP v{version} {label}"
entry["url"] = url
entry["image_download_size"] = int(os.environ[f"{suffix}_image_download_size"])
entry["image_download_sha256"] = os.environ[f"{suffix}_image_download_sha256"]
entry["extract_size"] = int(os.environ[f"{suffix}_extract_size"])
entry["extract_sha256"] = os.environ[f"{suffix}_extract_sha256"]
entry["release_date"] = release_date
with open(path, "w") as f:
f.write(json.dumps(data, indent=4) + "\n")
print("Added new entries:", ", ".join(added) or "(none)")
print("Updated existing entries:", ", ".join(updated) or "(none)")
if superseded:
print("Removed superseded prerelease entries:", ", ".join(superseded))
if removed_dupes:
print("Removed duplicate stale entries:", ", ".join(removed_dupes))
if skipped:
print("Skipped (platform never published, no template entry to clone):", ", ".join(skipped))
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"added={','.join(added)}\n")
f.write(f"updated={','.join(updated)}\n")
f.write(f"superseded={','.join(superseded)}\n")
f.write(f"changed={'1' if (added or updated or superseded) else ''}\n")
f.write(f"channel={'+'.join(sorted(channels_seen))}\n")
PY
- name: Show diff
run: git diff -- rpi-imager/rpi-imager_falcon_player.json
- name: Open PR
if: ${{ inputs.dry_run != true && steps.update.outputs.changed != '' }}
env:
TAG: ${{ steps.release.outputs.tag }}
CHANNEL: ${{ steps.update.outputs.channel }}
ADDED: ${{ steps.update.outputs.added }}
UPDATED: ${{ steps.update.outputs.updated }}
SUPERSEDED: ${{ steps.update.outputs.superseded }}
run: |
if git diff --quiet -- rpi-imager/rpi-imager_falcon_player.json; then
echo "No changes -- JSON already up to date with $TAG."
exit 0
fi
BRANCH="update-rpi-imager-json-${TAG}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add rpi-imager/rpi-imager_falcon_player.json
git commit -m "chore(rpi-imager): update to release ${TAG}"
git push -f origin "$BRANCH"
BODY="Automated update of \`rpi-imager_falcon_player.json\` to release [${TAG}](https://github.com/${GITHUB_REPOSITORY}/releases/tag/${TAG}) (${CHANNEL} channel)."
[ -n "$ADDED" ] && BODY="${BODY}
- Added new entries: ${ADDED}"
[ -n "$UPDATED" ] && BODY="${BODY}
- Replaced previous ${CHANNEL} entries in place: ${UPDATED}"
[ -n "$SUPERSEDED" ] && BODY="${BODY}
- Removed superseded prerelease entries (same major.minor family): ${SUPERSEDED}"
gh pr create \
--title "chore(rpi-imager): update to release ${TAG}" \
--body "$BODY" \
--base master \
--head "$BRANCH" \
|| echo "PR create failed or already exists for $BRANCH"