Skip to content

Update RPi Imager JSON #6

Update RPi Imager JSON

Update RPi Imager JSON #6

#############################################################################
# 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"