Files
firmware/.github/workflows/release.yml
Oleksandr ShyshatskyiandClaude Opus 5 ba01b8f9f0 The release e2e installed both models into one Creator5Pro replica
`for p in dist/*.tgz` fed the replica the Creator5 package first. That
machine's own app_startup.sh globs /mnt/Creator5Pro-*.tgz, found nothing,
and fell through to a normal boot -- so all 123 replica tests errored with
"could not install the package into a replica", about a package that was
built correctly and whose model gate was working exactly as designed.

The loop is older than the pytest lane and used to be fine: sim-install.py
resolved a machine per package. The replica lane takes PRINTER_IMAGE from
test.env, and there is one of those. So the end-to-end runs on the model
the replica IS, and a Creator5 end-to-end waits on a Creator5 replica image
that nothing publishes yet. Both models are still built and both are still
attached to the release; only the install test narrows.

upload-artifact moves above that step while I am here, so a run that
produced packages hands them over even when the lane fails. The release
gate is unchanged: Publish comes after, and a failed step still stops the
job before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S64uRMdZbj9xPMns2dqMG8
2026-08-30 14:54:16 +03:00

246 lines
12 KiB
YAML

name: release
# A release is blocked on the printer replica passing.
#
# The replica is the printer's real rootfs.squashfs, chrooted under
# qemu-mipsel, with /usr/prog installed by FlashForge's own updater. The
# installer under test runs on the printer's busybox, tar, md5sum and unTar --
# not on Debian stand-ins. If that fails, nothing ships.
#
# The stock FlashForge packages and the factory image are large but not
# secret: they are published at ghzserg/FF and this workflow fetches what is
# already public, exactly as ci.yml does. Every one is pinned by sha256 and
# the checksum is enforced, not advisory -- an unpinned download would let the
# archive change under us while the gates kept reporting green against a
# different firmware than they were written for.
#
# TAGS. `v<YYYYMMDD>-<city>`, e.g. v20260824-nova-kakhovka: the release date,
# and the name of a Ukrainian city under occupation. Only the date reaches the
# package filename -- the installer's version field is a date and nothing else
# -- so the city is carried by the tag and the release title.
on:
push:
tags: [ 'v*' ]
workflow_dispatch:
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch the stock firmware
# Same artefacts, same checksums as ci.yml. Enforced: a mismatch here
# means the thing we test against is not the thing we tested against
# last time, and that has to stop the release rather than colour it.
env:
STOCK_URL_CREATOR5PRO: https://github.com/ghzserg/FF/releases/download/R/Creator5Pro-1.9.7-1.2.9-20260810.tgz
STOCK_SHA_CREATOR5PRO: 1f3252feaae6d8fc72d7b0fd1ba832cc63d369659f8b2d2896efcf7adab2976b
STOCK_URL_CREATOR5: https://github.com/ghzserg/FF/releases/download/R/Creator5-1.9.7-1.2.9-20260810.tgz
STOCK_SHA_CREATOR5: 72009be183e74a4562050a927787ba686c6b558a075f00958892197ac52659cc
run: |
set -euo pipefail
mkdir -p stock
curl -fsSL "$STOCK_URL_CREATOR5PRO" -o stock/Creator5Pro-stock.tgz
curl -fsSL "$STOCK_URL_CREATOR5" -o stock/Creator5-stock.tgz
# Exactly two spaces before the path: sha256sum -c reads everything
# after them as the filename, so a column-aligned line looks for a
# file whose name begins with spaces and fails as "not found".
echo "$STOCK_SHA_CREATOR5PRO stock/Creator5Pro-stock.tgz" | sha256sum -c -
echo "$STOCK_SHA_CREATOR5 stock/Creator5-stock.tgz" | sha256sum -c -
ls -lh stock
- name: Write test.env (the replica)
env:
# THE replica: the printer's rootfs, its real /usr/prog and
# /usr/data, and the stock package already installed on top. The
# end-to-end test reinstalls the baseline itself when the package
# under test is for the other model, so this only ever changes how
# long a run takes, never what it proves. vars.PRINTER_IMAGE
# overrides it for a fork building its own.
#
# A VERSION TAG, NOT :latest. A release is blocked on this image, so
# it is as much a pinned input as the stock packages above -- and it
# is the only one whose contents nothing here checksums. A floating
# tag would let the substrate a release was proved on change without
# a commit, which is exactly what the sha256 lines exist to stop.
PRINTER_IMAGE: ${{ vars.PRINTER_IMAGE || 'monstrofil/creator5-printer:1.9.7-1.2.9-20260810' }}
run: |
set -euo pipefail
if [ -z "${PRINTER_IMAGE:-}" ]; then
echo "::error title=No replica::PRINTER_IMAGE is empty and there is no other way to get a replica."
exit 1
fi
echo "PRINTER_IMAGE=\"$PRINTER_IMAGE\"" > test.env
cat test.env
- name: Write config.env
run: |
cat > config.env <<CFG
MOD_NAME=anvil
# The tag is v<YYYYMMDD>-<city>; the package version is the date
# alone. FlashForge's installer reads this as the software version
# and it is compared as a number, so the codename stays in the tag.
# A manual run has no tag -- GITHUB_REF_NAME would be the branch --
# so leave it empty and let the build stamp today's UTC date.
# The optional trailing letter is the same-day re-release
# convention common.sh documents (20260824b): nothing on the printer
# reads it, so it only keeps the outer filenames distinct.
MOD_VER=$(case "$GITHUB_REF" in
refs/tags/*) echo "${GITHUB_REF_NAME#v}" | sed -n 's/^\([0-9]\{8\}[a-z]\{0,1\}\).*/\1/p' ;;
*) echo "" ;;
esac)
TARGET_MACHINE=Creator5Pro
STOCK_TGZ_CREATOR5PRO="$PWD/stock/Creator5Pro-stock.tgz"
STOCK_TGZ_CREATOR5="$PWD/stock/Creator5-stock.tgz"
STOCK_TGZ=""
# No Klipper source knob any more: pkgs/klipper builds from the
# commit pinned in versions.env and cross-compiles c_helper.so from
# those same sources. KLIPPER_FORK used to be settable here, and
# setting it to "" is what made v20260824 ship the stock 0.12-era
# klippy; a recipe names one source, so that cannot recur.
# The secret is INTENTIONALLY unset: a hash baked in here would be
# the same root password in every public release. Empty means each
# printer picks a random one on its first install and writes it to
# anvil-password.txt on the stick; updates keep it. The plumbing
# stays so a fork can bake one in deliberately.
ROOT_PW_HASH='${{ secrets.ROOT_PW_HASH }}'
FF_KEY='FFP0331&*%root'
CFG
# Before the suite, not after: the replica gates run ./bin/payload.sh
# directly and it expects vendor/ already populated. `make release`
# further down would have fetched them, but that runs after the gates.
#
# Cached because the set now includes the Klipper fork and the ~203MB
# MIPS toolchain that compiles its chelper; the key is the pin file, so
# a version bump refetches and anything else reuses.
- name: Cache the pinned vendor assets
uses: actions/cache@v4
with:
path: vendor
key: vendor-${{ hashFiles('versions.env') }}
- name: Fetch the pinned Mainsail, HelixScreen, Moonraker and Klipper assets
run: make vendor ASSET_ROOT=$PWD
# THE FEED FIRST. bin/payload.sh builds the payload by installing it, so
# it refuses to start without one, and the python recipes resolve their
# build dependencies out of it. This used to run unpack/patch/pack with
# no feed at all and could not have passed.
- name: Build the package feed
run: make packages ASSET_ROOT=$PWD
- name: Build the package the suite installs
run: make build ASSET_ROOT=$PWD
- name: Brick-safety suite, replica required
# No REQUIRE_PRINTER_SIM and no ALLOW_SKIP: qa has no skip mode, so a
# missing docker, image or package fails at the point that needs it.
#
# Through `make`, not around it. These steps used to hand-roll their
# own `docker run`, which meant two things: the release path exercised
# entry points no human uses -- so a bug in one of the Makefile's own
# runners could not be caught here -- and the container ran as root,
# leaving a work/ that the `make release` below, which runs as the
# runner user, could not write to.
run: make qa ASSET_ROOT=$PWD
- name: Build both models
run: make release ASSET_ROOT=$PWD
# BEFORE the end-to-end test, so a build that produced packages hands
# them over even when the lane below fails. This is not a weakening of
# the release gate: Publish runs after that step and the job stops if it
# fails, so a red run still cannot reach the release page.
- uses: actions/upload-artifact@v4
with:
name: firmware-packages
path: dist/*.tgz
retention-days: 30
- name: End-to-end update test on the package that will actually ship
# The suite above tested a package built from the same tree; this
# tests the exact file that gets attached to the release.
#
# ONE MODEL, NOT BOTH. Two models are built and both are attached, but
# there is one replica and it is a Creator5Pro -- its own app_startup.sh
# globs /mnt/Creator5Pro-*.tgz and refuses anything else, which is the
# model gate doing its job. Feeding it the Creator5 package produced 123
# errors that all said "could not install", and the package it was
# complaining about was correct.
#
# This loop used to run both because sim-install.py picked its own
# machine per package; the replica lane takes PRINTER_IMAGE from
# test.env and there is one of those. A Creator5 end-to-end needs a
# Creator5 replica image, which nothing publishes yet.
#
# REAL_PKG points the replica lane at one exact package instead of the
# newest thing in work/out -- 22 named results where sim-install.py
# gave one exit code. `make qa-replica` passes it through (see RUNSIM).
run: |
set -euo pipefail
p=$(ls dist/${TARGET_MACHINE}-*.tgz)
echo "=== $p ==="
REAL_PKG="$p" make qa-replica ASSET_ROOT=$PWD
env:
TARGET_MACHINE: Creator5Pro
- name: Assemble the release notes
if: startsWith(github.ref, 'refs/tags/')
# The fixed half is the same every time; the half that names the city
# is not. docs/releases/<tag>.md carries it, and is appended when it
# exists -- so the story of the city the release is named for lives in
# the repo at the tagged commit, not only in a hand-edited release
# page that nothing reproduces.
run: |
set -euo pipefail
cat > RELEASE_NOTES.md <<'NOTES'
Copy the file matching YOUR printer to the root of a FAT32 USB
stick and power the machine on. A `Creator5Pro-` package will not
install on a Creator 5 and vice versa.
Every package here was installed into a replica of the printer --
the real rootfs running under qemu-mipsel, with the package on a
real FAT filesystem that the machine's own app_startup.sh found
and installed -- and the machine was booted again afterwards and
checked to come up.
**If the screen stays dark**, the printer is still up: ssh and
Mainsail run from their own services, before and independently of
the UI, so reach it at `http://<printer-ip>/` or ssh in and read
`/usr/data/logs/helixscreen.log`. There is no automatic fallback --
`opkg remove anvil-helixscreen` to boot headless on
purpose. **To go back to stock**, put FlashForge's own package on a
USB stick and power on -- that path is tested on every release too.
---
NOTES
CITY="docs/releases/${GITHUB_REF_NAME}.md"
if [ -f "$CITY" ]; then
cat "$CITY" >> RELEASE_NOTES.md
else
echo "::warning::no $CITY -- publishing the generic line instead"
echo "Releases are named for a Ukrainian city under occupation." \
>> RELEASE_NOTES.md
fi
cat RELEASE_NOTES.md
- name: Publish
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: dist/*.tgz
# Every build so far is a pre-release: this replaces the stock UI on
# a machine whose only recovery path is a USB stick with the stock
# package on it. That is worth saying with the flag, not just in
# prose people scroll past.
prerelease: true
name: ${{ github.ref_name }}
body_path: RELEASE_NOTES.md