Release FrameOS #463
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |