-
-
Notifications
You must be signed in to change notification settings - Fork 229
324 lines (285 loc) · 14.5 KB
/
Copy pathupdate-rpi-imager-json.yml
File metadata and controls
324 lines (285 loc) · 14.5 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
#############################################################################
# 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 is GitHub's own definition of "latest": the
# most recent non-draft, non-prerelease release. Build releases
# are created as drafts (see build-images.yml's "release" job),
# so an unpublished draft is never picked up here by accident.
gh api "repos/${GITHUB_REPOSITORY}/releases/latest" > 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
# FPP release tags carry no 'v' prefix (10.0-beta, 9.5.3); asset
# filenames add it back (FPP-v10.0-beta-Pi.img.zip) -- see
# SD/build-image-pi.sh's OUT_IMG and build-images.yml's version step.
VERSION="$TAG"
PUBLISHED=$(jq -r '.published_at // .created_at' release.json | cut -d'T' -f1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_date=$PUBLISHED" >> "$GITHUB_OUTPUT"
echo "Resolved release: $TAG (published $PUBLISHED)"
- 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
NAME=$(jq -r --arg s "$SUFFIX" \
'.assets[] | select(.name | test("-" + $s + "\\.img\\.zip$")) | .name' \
release.json)
URL=$(jq -r --arg s "$SUFFIX" \
'.assets[] | select(.name | test("-" + $s + "\\.img\\.zip$")) | .browser_download_url' \
release.json)
if [ -z "$NAME" ] || [ "$NAME" = "null" ]; then
echo "No .img.zip asset for platform $SUFFIX in this release -- skipping."
continue
fi
echo "=== $SUFFIX: $NAME ==="
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}_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 }}
VERSION: ${{ steps.release.outputs.version }}
RELEASE_DATE: ${{ steps.release.outputs.release_date }}
Pi_url: ${{ steps.hashes.outputs.Pi_url }}
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_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_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_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)
version = os.environ["VERSION"]
release_date = os.environ["RELEASE_DATE"]
# 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
new_channel = channel_of(version)
new_family = family_of(version)
# 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+(.*)$")
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 = [], [], [], [], []
for suffix in PLATFORM_SUFFIXES:
url = os.environ.get(f"{suffix}_url", "")
if not url:
continue # no asset for this platform in the release
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)
else:
# First release ever seen in this channel for this platform
# (e.g. the very first *-beta build). Clone devices/icon/
# website/init_format from any existing same-platform entry
# so those don't have to be guessed; skip entirely if this
# platform has never been published under any channel.
template = same_platform[0] if same_platform else None
if template is None:
skipped.append(suffix)
continue
entry = dict(template)
entry["devices"] = list(template["devices"])
data["os_list"].append(entry)
added.append(suffix)
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", new_channel, "entry:", ", ".join(added) or "(none)")
print("Updated existing", new_channel, "entry:", ", ".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={new_channel}\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"