Skip to content

Release FrameOS

Release FrameOS #463

name: Release FrameOS
on:
workflow_dispatch:
inputs:
next_version:
description: 'Optional release version override, for example 2026.6.0'
required: false
type: string
permissions:
actions: write
contents: write
jobs:
# Runner sizing: every job runs on Depot. The bookkeeping jobs (version
# bump, npm dispatch, release notes, matrix, addon repo, GitHub release) are
# I/O bound and take the default Depot size; only the two jobs that actually
# compute (Buildroot images, ESP32 firmware) take 16 cores. The one
# exception is the armhf leg of build-prebuilt-cross, which stays on
# GitHub's ARM runners — see backend/bin/cross for the measurements.
update-versions:
runs-on: depot-ubuntu-24.04
outputs:
release_sha: ${{ steps.release-ref.outputs.release_sha }}
docker_version: ${{ steps.release-ref.outputs.docker_version }}
release_tag: ${{ steps.release-ref.outputs.release_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Recompute component versions
env:
NEXT_VERSION: ${{ inputs.next_version }}
run: |
args=(--force-project docker)
if [ -n "$NEXT_VERSION" ]; then
args+=(--force-project frameos --next-version "$NEXT_VERSION")
fi
python3 tools/update_versions.py "${args[@]}"
- name: Verify version changes
run: |
if git diff --quiet -- versions.json; then
echo "Release did not change versions.json"
exit 1
fi
echo "Version changes detected"
- name: Commit updated versions
run: |
VERSION=$(python3 -c "import json; print(json.load(open('versions.json'))['docker'].split('+', 1)[0])")
git config user.name "frameos-bot"
git config user.email "git@frameos.net"
# update_versions.py syncs the wasm package to the FrameOS runtime
# version and the editor package to its independent frontend hash.
git add versions.json frameos/wasm/package.json frameos/editor/package.json
git commit -m "chore: version ${VERSION}"
git push origin HEAD:main
- name: Select release commit
id: release-ref
run: |
VERSION=$(python3 -c "import json; print(json.load(open('versions.json'))['docker'].split('+', 1)[0])")
echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "docker_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_tag=v$VERSION" >> "$GITHUB_OUTPUT"
publish-npm:
name: Publish FrameOS npm packages
needs: update-versions
runs-on: depot-ubuntu-24.04
steps:
# npm auth uses trusted publishing (OIDC), which matches on the
# top-level workflow file — so publishing lives in npm-publish.yml and
# is dispatched here rather than run inline. That workflow is also
# manually dispatchable to re-publish without bumping versions.
- name: Dispatch npm publish workflow
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ needs.update-versions.outputs.release_sha }}
run: |
gh workflow run npm-publish.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f ref="$RELEASE_SHA"
release-notes:
name: Generate Release Notes
needs: update-versions
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout release commit
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
fetch-depth: 0
- name: Generate release notes
env:
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
RELEASE_TAG: ${{ needs.update-versions.outputs.release_tag }}
RELEASE_SHA: ${{ needs.update-versions.outputs.release_sha }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
python3 tools/generate_release_notes.py \
--version "$DOCKER_VERSION" \
--release-tag "$RELEASE_TAG" \
--head "$RELEASE_SHA" \
--output release-notes.md
- name: Upload release notes
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ needs.update-versions.outputs.docker_version }}
path: release-notes.md
if-no-files-found: error
determine-release-targets:
needs: update-versions
runs-on: depot-ubuntu-24.04
outputs:
targets: ${{ steps.set-matrix.outputs.targets }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
- id: set-matrix
run: |
MATRIX=$(python3 backend/bin/cross matrix)
echo "targets=$MATRIX" >> "$GITHUB_OUTPUT"
build-prebuilt-cross:
name: Build Prebuilt Cross (${{ matrix.slug }})
needs: [update-versions, determine-release-targets]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.determine-release-targets.outputs.targets) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: pnpm/action-setup@v4
with:
version: 10.27.0
run_install: false
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
cache-dependency-path: pnpm-lock.yaml
# Cache pip's wheel cache: installing backend requirements from a warm
# cache saves ~35-45s on each of the 11 matrix jobs.
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: backend/requirements.txt
- name: Install backend dependencies
run: |
pip install -r backend/requirements.txt
- name: Install Nim (prebuilt)
env:
NIM_VERSION: 2.2.4
run: |
set -euo pipefail
distro=$(lsb_release -si | tr '[:upper:]' '[:lower:]')
version=$(lsb_release -sr)
case "$(uname -m)" in
aarch64) host_arch=arm64 ;;
armv7l|armv6l) host_arch=armhf ;;
x86_64) host_arch=amd64 ;;
*) echo "Unsupported runner architecture: $(uname -m)" >&2; exit 1 ;;
esac
nim_slug="${distro}-${version}-${host_arch}"
echo "$nim_slug"
mkdir -p "$HOME/.nim"
curl -L "https://archive.frameos.net/prebuilt-deps/${nim_slug}/nim-${NIM_VERSION}.tar.gz" -o /tmp/nim.tar.gz
tar -xzf /tmp/nim.tar.gz -C "$HOME/.nim"
rm -rf "$HOME/.nim/nim-${NIM_VERSION}/nim/bin"
mv "$HOME/.nim/nim-${NIM_VERSION}/nim/"* "$HOME/.nim/nim-${NIM_VERSION}/"
echo "$HOME/.nim/nim-${NIM_VERSION}/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.nim/nim-${NIM_VERSION}/bin:$PATH"
nim --version
- name: Build release artifacts for ${{ matrix.slug }}
run: |
cd frameos
make release-${{ matrix.slug }}
- name: Package release artifacts
env:
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
TARGET_SLUG: ${{ matrix.slug }}
run: |
mkdir -p release-assets
archive_root="frameos-${DOCKER_VERSION}-${TARGET_SLUG}"
mkdir -p "release-assets/${archive_root}"
cp -a "frameos/build/prebuilt-cross/${TARGET_SLUG}/." "release-assets/${archive_root}/"
tar -czf "release-assets/${archive_root}.tar.gz" \
-C release-assets \
"${archive_root}"
rm -rf "release-assets/${archive_root}"
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: frameos-${{ needs.update-versions.outputs.docker_version }}-${{ matrix.slug }}
path: release-assets/*.tar.gz
if-no-files-found: error
build-buildroot-release-image:
name: Build Buildroot Release Image
needs: [update-versions, build-prebuilt-cross]
runs-on: depot-ubuntu-24.04-16
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
- name: Download prebuilt cross artifacts
uses: actions/download-artifact@v4
with:
pattern: frameos-${{ needs.update-versions.outputs.docker_version }}-*
path: release-assets
merge-multiple: true
# Same warm pip cache as the cross matrix: this job's requirements
# install spent ~31s of its ~150s wall clock building cffi from sdist.
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: backend/requirements.txt
- name: Install backend dependencies
run: |
pip install -r backend/requirements.txt
- name: Build Buildroot release images
env:
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
run: |
set -euo pipefail
# Build a release image for every enabled platform that has a
# cached Buildroot base image in the checked-in manifest.
platforms=$(python3 - <<'PY'
import contextlib
import json
from pathlib import Path
import sys
sys.path.insert(0, "backend")
# Importing app.tasks pulls in the whole backend, which may emit
# diagnostics at import time (e.g. SQLite warnings). Keep stdout
# clean: it is captured as the platform list below.
with contextlib.redirect_stdout(sys.stderr):
from app.tasks.buildroot_platforms import enabled_buildroot_platforms
manifest = json.loads(Path("tools/buildroot-images/manifest.json").read_text(encoding="utf-8"))
available = {entry.get("platform") for entry in manifest.get("entries", [])}
for platform in enabled_buildroot_platforms():
if platform.key in available:
print(platform.key)
else:
print(f"Skipping {platform.key}: no cached Buildroot base image in manifest", file=sys.stderr)
PY
)
if [ -z "$platforms" ]; then
echo "No Buildroot platforms with cached base images found." >&2
exit 1
fi
# One platform per background job. Each build works in its own
# tempdir and writes its own frameos-<version>-<platform>-* files,
# so the only shared state is the read-mostly Buildroot base image
# cache (one distinct file per platform). Sequentially the two
# platforms cost 55s + 38s; in parallel the step is as long as the
# slowest one. Output is captured per platform and replayed in
# order afterwards so the log stays readable.
pids=()
names=()
for platform in $platforms; do
echo "Building release image for ${platform}"
python3 tools/buildroot-images/buildroot_images.py \
--platform "$platform" \
release-image \
--prebuilt-cross-dir release-assets \
--release-assets-dir release-assets \
--version "$DOCKER_VERSION" \
> "buildroot-release-${platform}.log" 2>&1 &
pids+=("$!")
names+=("$platform")
done
status=0
for index in "${!pids[@]}"; do
if ! wait "${pids[$index]}"; then
echo "Buildroot release image failed for ${names[$index]}" >&2
status=1
fi
done
for platform in "${names[@]}"; do
echo "::group::Buildroot release image ${platform}"
cat "buildroot-release-${platform}.log"
echo "::endgroup::"
done
if [ "$status" -ne 0 ]; then
exit 1
fi
ls -lh release-assets/*.img.gz release-assets/*.metadata.json
- name: Upload Buildroot release images
uses: actions/upload-artifact@v4
with:
name: frameos-${{ needs.update-versions.outputs.docker_version }}-buildroot-images
path: |
release-assets/*.img.gz
release-assets/*.metadata.json
if-no-files-found: error
# Prebuilt ESP32 firmware with no baked credentials: WiFi and cloud config
# are provisioned after flashing via the captive portal, the serial console,
# or the cloud browser flasher (cloud_url + claim_token over the USB API —
# docs/cloud-frames.md). Every supported Waveshare panel driver is compiled
# in and the active panel is chosen at runtime (`set panel <key>` on the
# serial console or the setup portal dropdown); FRAMEOS_SELECTED_PANEL only
# sets the boot-time default, kept at EPD_7in5_V2 for backward compat.
# The binary is published as -generic.bin; an identical copy keeps the old
# -epd7in5v2.bin name for one release cycle so cloud deployments that still
# reference it keep working — drop the copy after the cloud side reads
# -generic.bin from release metadata.
build-esp32-generic-firmware:
name: Build ESP32 firmware (generic, all panels)
needs: [update-versions]
runs-on: depot-ubuntu-24.04-16
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
# Depot keeps a persistent layer cache per project, so the esp32-ci
# target (ESP-IDF toolchain + Nim) is only rebuilt when its layers
# actually change. load: true pulls the image into the local Docker
# daemon for the `docker run` below.
#
# This step must stay byte-identical in its cache-relevant inputs
# (project, context, target, build-args, platforms) to the esp32-ci
# build in e2e-docker.yml, which runs on every push to main. esp32-ci
# pulls in the whole app-builder stage, so that job is what keeps the
# release build's toolchain layers warm — diverge them and every
# release pays for a cold ESP-IDF/Nim/pnpm/emsdk build again.
- name: Build ESP32 CI Docker image
uses: depot/build-push-action@v1
with:
project: rsbh2wrlkj
token: ${{ secrets.DEPOT_TOKEN }}
context: .
target: esp32-ci
tags: frameos-esp32-ci
load: true
push: false
- name: Build firmware
timeout-minutes: 60
env:
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
run: |
set -euo pipefail
mkdir -p release-assets
# DOCKER_VERSION is passed into the container instead of spliced into
# the inner single-quoted script, so the value is never re-parsed by
# the container's shell.
docker run --rm \
-e FRAMEOS_SELECTED_PANEL=EPD_7in5_V2 \
-e DOCKER_VERSION \
-v "$PWD/release-assets:/release-assets" \
frameos-esp32-ci \
bash -lc '
set -euo pipefail
bash embedded/esp32/ci_build_image.sh
cp embedded/esp32/build-ci/merged-binary.bin \
"/release-assets/frameos-$DOCKER_VERSION-esp32-s3-generic.bin"
# Transitional copy under the pre-runtime-panel-selection name;
# remove once no deployed cloud references it.
cp embedded/esp32/build-ci/merged-binary.bin \
"/release-assets/frameos-$DOCKER_VERSION-esp32-s3-epd7in5v2.bin"
# ESP32-C3 thin-client firmware (TRMNL OG/BWRY, XTEINK X4):
# 4MB no-OTA layout, no on-device Nim renderer.
FRAMEOS_ESP32_PLATFORM=esp32-c3 \
FRAMEOS_ESP32_BUILD_DIR=build-ci-c3 \
bash embedded/esp32/ci_build_image.sh
cp embedded/esp32/build-ci-c3/merged-binary.bin \
"/release-assets/frameos-$DOCKER_VERSION-esp32-c3-generic.bin"
'
ls -lh release-assets/
# Detached minisign-compatible signatures (<asset>.minisig) next to
# every firmware image. The device verifies the Ed25519/BLAKE2b-512
# signature against the public key baked into the firmware BEFORE
# switching boot slots, and the cloud's OTA manifest route answers
# 409 unsigned_release for a release without them — so a missing
# signing secret must fail the release here, loudly: an unsigned
# release is worse than a failed one.
- name: Sign ESP32 firmware
env:
FRAMEOS_FIRMWARE_SIGNING_KEY: ${{ secrets.FRAMEOS_FIRMWARE_SIGNING_KEY }}
run: |
set -euo pipefail
if [ -z "${FRAMEOS_FIRMWARE_SIGNING_KEY:-}" ]; then
echo "::error::FRAMEOS_FIRMWARE_SIGNING_KEY secret is not set — refusing to publish unsigned firmware"
exit 1
fi
# Ubuntu 24.04 pip is PEP 668 "externally managed"; the fallback
# keeps this working if the runner image ever stops being.
python3 -m pip install --quiet --break-system-packages cryptography \
|| python3 -m pip install --quiet cryptography
keyfile=$(mktemp)
# The secret is the small text file sign_firmware.py keygen wrote:
# a comment line + base64(ED + keyid8 + seed32).
printf '%s\n' "$FRAMEOS_FIRMWARE_SIGNING_KEY" > "$keyfile"
status=0
python3 tools/sign_firmware.py sign --secret "$keyfile" \
release-assets/frameos-*.bin || status=$?
shred -u "$keyfile"
[ "$status" -eq 0 ] || exit "$status"
# The CI secret must be the counterpart of the committed public key
# (the one baked into shipped firmware) — a rotated-but-not-updated
# secret would sign images no device accepts.
for bin in release-assets/frameos-*.bin; do
python3 tools/sign_firmware.py verify \
--public release-assets/firmware-signing.pub "$bin"
done
ls -lh release-assets/
- name: Upload ESP32 firmware
uses: actions/upload-artifact@v4
with:
# The github-release job downloads artifacts by the
# frameos-<version>-* name pattern, so this name must keep that
# prefix.
name: frameos-${{ needs.update-versions.outputs.docker_version }}-esp32-generic
path: |
release-assets/*-esp32-s3-generic.bin
release-assets/*-esp32-s3-generic.bin.minisig
release-assets/*-esp32-s3-epd7in5v2.bin
release-assets/*-esp32-s3-epd7in5v2.bin.minisig
release-assets/*-esp32-c3-generic.bin
release-assets/*-esp32-c3-generic.bin.minisig
if-no-files-found: error
# Prebuilt thin-client firmware for Raspberry Pi Pico W / Pico 2 W (the
# Pimoroni Inky Frame family). Generic on purpose: UF2s flash over BOOTSEL
# drag-drop and provision over the USB serial console, the backend never
# builds them per-frame.
build-pico-firmware:
name: Build Pico W / Pico 2 W firmware
needs: [update-versions]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
- name: Install pico toolchain
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake ninja-build gcc-arm-none-eabi libnewlib-arm-none-eabi \
libstdc++-arm-none-eabi-newlib
- name: Cache pico-sdk
id: pico-sdk-cache
uses: actions/cache@v4
with:
path: ~/pico-sdk
key: pico-sdk-2.1.1
- name: Fetch pico-sdk
if: steps.pico-sdk-cache.outputs.cache-hit != 'true'
run: |
git clone --depth 1 --branch 2.1.1 https://github.com/raspberrypi/pico-sdk.git ~/pico-sdk
cd ~/pico-sdk
git submodule update --init --depth 1 lib/cyw43-driver lib/lwip lib/mbedtls lib/tinyusb
- name: Build firmware
env:
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
run: |
set -euo pipefail
mkdir -p release-assets
cd embedded/pico
for board in pico_w pico2_w; do
cmake -B "build-$board" -DPICO_BOARD=$board \
-DPICO_SDK_PATH=$HOME/pico-sdk \
-DFRAMEOS_VERSION="$DOCKER_VERSION" -G Ninja
cmake --build "build-$board"
done
cp build-pico_w/frameos_pico.uf2 \
"../../release-assets/frameos-$DOCKER_VERSION-pico-w.uf2"
cp build-pico2_w/frameos_pico.uf2 \
"../../release-assets/frameos-$DOCKER_VERSION-pico-2w.uf2"
ls -lh ../../release-assets/
- name: Upload Pico firmware
uses: actions/upload-artifact@v4
with:
# github-release downloads artifacts by the frameos-<version>-*
# name pattern, so this name must keep that prefix.
name: frameos-${{ needs.update-versions.outputs.docker_version }}-pico
path: |
release-assets/*-pico-w.uf2
release-assets/*-pico-2w.uf2
if-no-files-found: error
# Depot builds both platforms remotely with its own persistent layer cache,
# so there is no separate warm-up build job: push-multiarch is the only
# image build and pushes the multi-arch manifest directly.
push-multiarch:
needs: [update-versions, build-prebuilt-cross, build-buildroot-release-image]
# The image itself is built on Depot's remote builders, so this runner only
# orchestrates: the default size is plenty.
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
# No QEMU or local buildx setup: depot/build-push-action runs the build
# on Depot's remote amd64 and arm64 builders, so nothing is emulated or
# built on this runner. Only the registry login is still needed, because
# Depot pushes with this runner's Docker credentials.
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract branch name
shell: bash
run: echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Short SHA
shell: bash
run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Read Docker version tags
shell: bash
run: |
DOCKER_VERSION=$(python3 -c "import json; print(json.load(open('versions.json'))['docker'].split('+', 1)[0])")
echo "DOCKER_VERSION=$DOCKER_VERSION" >> $GITHUB_ENV
echo "DOCKER_VERSION_MINOR=${DOCKER_VERSION%.*}" >> $GITHUB_ENV
echo "DOCKER_VERSION_MAJOR=${DOCKER_VERSION%%.*}" >> $GITHUB_ENV
- name: Set up Depot CLI
uses: depot/setup-action@v1
# Push all built platforms as a multi-arch image
- name: Push multi-arch image
uses: depot/build-push-action@v1
with:
project: rsbh2wrlkj
token: ${{ secrets.DEPOT_TOKEN }}
context: .
push: true
platforms: linux/amd64,linux/arm64/v8
tags: |
frameos/frameos:latest
frameos/frameos:${{ env.DOCKER_VERSION_MAJOR }}
frameos/frameos:${{ env.DOCKER_VERSION_MINOR }}
frameos/frameos:${{ env.DOCKER_VERSION }}
update-addon-repo:
name: Update Home Assistant Addon
needs: [update-versions, push-multiarch, release-notes]
runs-on: depot-ubuntu-24.04
steps:
- name: Checkout frameos
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
path: frameos
- name: Checkout frameos-home-assistant-addon
uses: actions/checkout@v4
with:
repository: frameos/frameos-home-assistant-addon
token: ${{ secrets.ACTIONS_WRITE_TOKEN }}
ref: main
path: home-assistant-addon
- name: Download release notes
uses: actions/download-artifact@v4
with:
name: release-notes-${{ needs.update-versions.outputs.docker_version }}
- name: Read Docker version
shell: bash
run: |
DOCKER_VERSION=$(python3 -c "import json; print(json.load(open('frameos/versions.json'))['docker'].split('+', 1)[0])")
echo "DOCKER_VERSION=$DOCKER_VERSION" >> $GITHUB_ENV
- name: Update version in config.yaml
run: |
cd home-assistant-addon/frameos
echo "Updating version in config.yaml to ${{ env.DOCKER_VERSION }}"
sed -i "s/^version: .*/version: ${{ env.DOCKER_VERSION }}/" config.yaml
- name: Prepend release notes to the addon changelog
run: |
python3 - <<'EOF'
import datetime
import os
import re
from pathlib import Path
version = os.environ["DOCKER_VERSION"]
date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
notes = Path("release-notes.md").read_text()
# Keep version headers at the top level: demote "## New features" -> "###"
notes = re.sub(r"^(##+)", r"#\1", notes, flags=re.M)
# The "Docker images" block is appended later for the GitHub release only
notes = re.sub(r"\n?### Docker images\n(?:.*\n?)*?(?=\n### |\Z)", "", notes).strip()
section = f"## {version} ({date})\n\n{notes}\n"
path = Path("home-assistant-addon/frameos/CHANGELOG.md")
header = "# Changelog\n\n"
changelog = path.read_text() if path.exists() else header
if f"\n## {version} " in changelog:
print(f"Changelog already contains {version}, skipping")
else:
first_entry = changelog.find("\n## ")
if first_entry == -1:
changelog = changelog.rstrip() + "\n\n" + section
else:
changelog = changelog[: first_entry + 1] + section + "\n" + changelog[first_entry + 1 :]
path.write_text(changelog)
print(f"Added changelog entry for {version}")
EOF
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
message: 'Update FrameOS version to ${{ env.DOCKER_VERSION }}'
add: '.'
cwd: home-assistant-addon
push: true
author_name: FrameOS Bot
author_email: git@frameos.net
github-release:
name: Create GitHub Release
needs: [update-versions, push-multiarch, build-prebuilt-cross, build-buildroot-release-image, build-esp32-generic-firmware, build-pico-firmware, update-addon-repo, release-notes]
runs-on: depot-ubuntu-24.04
steps:
# Shallow: this job only needs a working directory for `gh` and the
# downloaded artifacts. Release notes come from an artifact and the tag
# is created from RELEASE_SHA, so no history is read here.
- name: Checkout release commit
uses: actions/checkout@v4
with:
ref: ${{ needs.update-versions.outputs.release_sha }}
fetch-depth: 1
- name: Download prebuilt cross artifacts
uses: actions/download-artifact@v4
with:
pattern: frameos-${{ needs.update-versions.outputs.docker_version }}-*
path: release-assets
merge-multiple: true
- name: Download release notes
uses: actions/download-artifact@v4
with:
name: release-notes-${{ needs.update-versions.outputs.docker_version }}
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
RELEASE_TAG: ${{ needs.update-versions.outputs.release_tag }}
RELEASE_SHA: ${{ needs.update-versions.outputs.release_sha }}
run: |
ls -lh release-assets
cat >> release-notes.md <<EOF
## Docker images
- [frameos/frameos:latest](https://hub.docker.com/r/frameos/frameos/tags?name=latest)
- [frameos/frameos:${DOCKER_VERSION}](https://hub.docker.com/r/frameos/frameos/tags?name=${DOCKER_VERSION})
- [frameos/frameos:${DOCKER_VERSION%.*}](https://hub.docker.com/r/frameos/frameos/tags?name=${DOCKER_VERSION%.*})
- [frameos/frameos:${DOCKER_VERSION%%.*}](https://hub.docker.com/r/frameos/frameos/tags?name=${DOCKER_VERSION%%.*})
EOF
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release edit "$RELEASE_TAG" \
--title "FrameOS ${DOCKER_VERSION}" \
--notes-file release-notes.md
gh release upload "$RELEASE_TAG" release-assets/* --clobber
else
gh release create "$RELEASE_TAG" release-assets/* \
--target "$RELEASE_SHA" \
--title "FrameOS ${DOCKER_VERSION}" \
--notes-file release-notes.md
fi
- name: Dispatch Discord notification
env:
GH_TOKEN: ${{ github.token }}
DOCKER_VERSION: ${{ needs.update-versions.outputs.docker_version }}
RELEASE_TAG: ${{ needs.update-versions.outputs.release_tag }}
RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/${{ needs.update-versions.outputs.release_tag }}
run: |
gh workflow run discord-release.yml \
--ref main \
-f release_name="FrameOS ${DOCKER_VERSION}" \
-f release_tag="$RELEASE_TAG" \
-f release_url="$RELEASE_URL"