Files
firmware/.github/workflows/ci.yml
T
Oleksandr ShyshatskyiandClaude Opus 5 d05565284d Sections E, F, G: wrong values, stale claims, overclaims
E -- values and counts. The START_PRINT prologue has no M400 and an idle
timeout of 864000, not 1800000. The nozzle-clean temperature comes from the
material table FIRST, so NOZZLE=240 with no per-tool temp cleans at 220 --
documented backwards before. TOOLS is the paired 0:220,2:240 form. The wipe Z
is the raw eddy frame, not the homed one. The runout M117 strings are
"T<n> clog" / "T<n> out of filament", not E-codes. Grab-verify raises
E0051+tool, not E0143. Docks are at X~297; 250/280 are staging waypoints. The
grab sequence is documented in the order _grab actually emits it. ff_print
exposes seven status fields, not nine, and never emits TOOLS=. Package sizes
reconciled against the real artifacts (62MB built, ~93MB stock, ~370MB of CI
assets, ~60s not 90s). testing.md's sample output block matched no header the
code prints, so it is replaced with the real ones; its pytest row listed two
of six suites.

F -- stale claims. _FF_PREFLIGHT is not "missing". SDCARD_SET_CHANNEL is not
still issued (764af2a). The app and Moonraker do not coexist; the app is gone.
TOOLCHANGE_STATUS has nothing to compare against since the JSON reader was
removed. The JSON is re-imported every boot until a nozzle is saved, not once.
The 23-check render harness was deleted in 91604c2. PURGE now wipes by
default. 50-print-lifecycle.md gained the section on how the sequences are
actually entered now -- [ff_print], not the slicer. The dead "profiles"
concept is gone from three files, one of which printed it on every replica run.

G -- overclaims. The README no longer says a stock reflash puts the printer
back "exactly as it was": it says what is restored and what survives inert,
which is what test-recovery actually asserts. The random ssh password is
conditional on ROOT_PW_HASH being unset. verify.sh no longer warns "ssh will
not start" on every build for a dropbear we deliberately do not ship -- the
check now reads the way round it means. case-install.sh's read-only block says
it asserts nothing. An uncalibrated tool contributes z_adjust, not zero, in
three operator-facing places. offset_base does not cancel out in X/Y. The
safe-moves file's Z50 promise is scoped to its own lines, with the implicit
prepare's G1 Z10 spelled out.

48 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:48:43 +03:00

224 lines
10 KiB
YAML

name: ci
# The stock FlashForge firmware is proprietary and ~93MB, so it is never in
# the repo. The `test` job runs against a synthetic fixture that reproduces
# the package STRUCTURE (see test/integration/make-stock-fixture.sh) -- enough
# for the static and packaging checks, but NOT enough to prove a package will
# not brick a printer.
#
# That proof needs the printer's real rootfs, and so it needs the stock
# package. `printer-sim` below now always runs, because the packages it needs
# are publicly mirrored (see env: below) rather than hidden behind a secret
# nobody had set. It used to be gated on STOCK_URL_CREATOR5PRO, which meant
# that for its entire existence it never ran once: a grey dash beside two
# green ticks, reading as "CI passed" while the only gates that decide
# whether a package bricks a printer were never executed.
on:
push:
branches: [ main, master ]
pull_request:
workflow_dispatch:
# The stock packages and the factory image, from ghzserg/FF -- a public
# archive of FlashForge firmware that this repo already points at in
# docs/printer-replica.md. Nothing proprietary is redistributed from here; the
# workflow fetches what is already published elsewhere.
#
# Every one is pinned by sha256, and the checksum is enforced, not advisory.
# An unpinned download would let the archive change under us and the suite
# would keep reporting green while testing a different firmware than the one
# these gates were written against -- the same class of silent drift as a gate
# that skips without saying so. Verified byte-for-byte against the maintainer's
# local copies on 2026-08-24.
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
FACTORY_URL: https://github.com/ghzserg/FF/releases/download/R/Creator5Pro-factory.tar.xz
FACTORY_SHA: 45657924298a41b5cd48c0f4412b4b2fb67df624af666b494d6bcea969cc5b7b
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
openssl xz-utils unzip binutils file squashfs-tools python3-pytest python3-jinja2
# Syntax and the brick lint are the first thing run-tests.py does, so
# they are not repeated here.
- name: shellcheck
run: |
sudo apt-get install -y shellcheck
# Errors are a gate. These used to end in `|| true`, which meant
# shellcheck could report anything it liked and the job stayed green.
# payload scripts run under busybox ash on the printer.
shellcheck -s sh -S error payload/*.sh payload/init.d/S* payload/firmwareExe
shellcheck -s bash -S error bin/*.sh test/integration/*.sh
# Warnings are advisory: printed, not enforced.
shellcheck -s sh -S warning payload/*.sh payload/init.d/S* payload/firmwareExe || true
# This job has no stock package on purpose, so exactly two gates cannot
# run: the tests that read the real rootfs, and the replica itself. The
# suite refuses to call a gate it did not run a pass, so they have to be
# accepted explicitly -- BY NAME. `ALLOW_SKIP=1` would accept them and
# also every future skip, which would make this job green while it
# quietly stopped testing things. Naming them means a third skip fails
# here, and both are still printed and annotated on every run.
#
# The job that must not skip at all is printer-sim below, and
# release.yml; both set REQUIRE_PRINTER_SIM=1 and no ALLOW_SKIP.
- name: Full test suite (fixture-based)
run: ALLOW_SKIP="pytest,the printer replica" ./test/run-tests.py
- name: Upload built fixture packages
if: always()
uses: actions/upload-artifact@v4
with:
name: fixture-packages
path: work/out/*.tgz
if-no-files-found: ignore
retention-days: 7
# The real check: install the package into a replica of the printer -- the
# actual rootfs.squashfs under qemu-mipsel, on the printer's own busybox --
# and assert the machine still boots. No `if:` guard: this job is the point
# of the repo, and a version of it that can quietly not run is worse than
# not having it.
printer-sim:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ~370MB of somebody else's release assets per run (two ~93MB stock
# packages and the 182MB factory image), and they never
# change -- the cache key is their checksums, so a new pin misses and
# re-fetches, and an unchanged pin costs the archive nothing.
- name: Cache the stock firmware
uses: actions/cache@v4
with:
path: stock
key: stock-${{ env.STOCK_SHA_CREATOR5PRO }}-${{ env.STOCK_SHA_CREATOR5 }}-${{ env.FACTORY_SHA }}
- name: Fetch the stock firmware
env:
# Secrets override the public pins, for a fork testing its own
# firmware. Override the URL and you must override the checksum too,
# or the fetch refuses to run -- an unverified download is exactly
# what this is meant to prevent.
SECRET_PRO: ${{ secrets.STOCK_URL_CREATOR5PRO }}
SECRET_PRO_SHA: ${{ secrets.STOCK_SHA256_CREATOR5PRO }}
SECRET_STD: ${{ secrets.STOCK_URL_CREATOR5 }}
SECRET_STD_SHA: ${{ secrets.STOCK_SHA256_CREATOR5 }}
run: |
set -euo pipefail
mkdir -p stock
get() { # name url sha dest
if [ -z "$2" ]; then
echo "::error::no URL for $1"; exit 1
fi
if [ -z "$3" ]; then
echo "::error title=Unverified firmware download::$1 has a URL but no sha256. Set the matching STOCK_SHA256_* secret; an unpinned firmware would let this suite report green against bytes nobody checked."
exit 1
fi
if [ -f "$4" ] && echo "$3 $4" | sha256sum -c - >/dev/null 2>&1; then
echo " $1: cache hit, checksum ok"
return
fi
echo " $1: fetching $2"
curl -fsSL "$2" -o "$4"
echo "$3 $4" | sha256sum -c -
}
# Written out rather than nested in one expansion: a URL and its
# checksum must come from the SAME source, and a clever ${x:+y}${x:-z}
# pair silently pastes the URL in where the checksum belongs.
PRO_URL="${SECRET_PRO:-$STOCK_URL_CREATOR5PRO}"
if [ -n "${SECRET_PRO:-}" ]; then PRO_SHA="${SECRET_PRO_SHA:-}"
else PRO_SHA="$STOCK_SHA_CREATOR5PRO"; fi
STD_URL="${SECRET_STD:-$STOCK_URL_CREATOR5}"
if [ -n "${SECRET_STD:-}" ]; then STD_SHA="${SECRET_STD_SHA:-}"
else STD_SHA="$STOCK_SHA_CREATOR5"; fi
get Creator5Pro "$PRO_URL" "$PRO_SHA" stock/Creator5Pro-stock.tgz
get Creator5 "$STD_URL" "$STD_SHA" stock/Creator5-stock.tgz
- name: Write test.env (the replica's /usr/prog)
env:
SECRET_FACTORY: ${{ secrets.FACTORY_URL }}
SECRET_FACTORY_SHA: ${{ secrets.FACTORY_SHA256 }}
# Optional repository variable: a prebuilt replica image, e.g.
# monstrofil/creator5-printer:latest. It carries the factory
# partitions AND the stock package already installed, which is about
# a minute of setup saved on every replica case.
PRINTER_IMAGE: ${{ vars.PRINTER_IMAGE }}
run: |
set -euo pipefail
: > test.env
URL="${SECRET_FACTORY:-$FACTORY_URL}"
if [ -n "${SECRET_FACTORY:-}" ]; then SHA="${SECRET_FACTORY_SHA:-}"
else SHA="$FACTORY_SHA"; fi
if [ -z "$SHA" ]; then
echo "::error title=Unverified factory image::FACTORY_URL is set without FACTORY_SHA256."
exit 1
fi
if [ -f stock/factory.tar.xz ] && \
echo "$SHA stock/factory.tar.xz" | sha256sum -c - >/dev/null 2>&1; then
echo " factory image: cache hit, checksum ok"
else
curl -fsSL "$URL" -o stock/factory.tar.xz
echo "$SHA stock/factory.tar.xz" | sha256sum -c -
fi
echo "PROG_DUMP=\"$PWD/stock/factory.tar.xz\"" >> test.env
[ -z "${PRINTER_IMAGE:-}" ] || echo "PRINTER_IMAGE=\"$PRINTER_IMAGE\"" >> test.env
cat test.env
- name: Write config.env
run: |
cat > config.env <<CFG
MOD_NAME=anvil
MOD_VER=ci
SW_VER=""
TARGET_MACHINE=Creator5Pro
STOCK_TGZ_CREATOR5PRO="$PWD/stock/Creator5Pro-stock.tgz"
STOCK_TGZ_CREATOR5="$PWD/stock/Creator5-stock.tgz"
STOCK_TGZ=""
KLIPPER_FORK=""
ROOT_PW_HASH='\$6\$ci\$abcdefghijklmnopqrstuvwxyz'
FF_KEY='FFP0331&*%root'
CFG
# The replica gates run ./bin/patch.sh, which needs the real Mainsail
# zip and HelixScreen tarball in vendor/ -- patch.sh does not fetch them
# itself, bin/build.sh does, and the gates do not go through build.sh.
# Nothing here noticed until this job ran for the first time, because
# every local run already had a populated vendor/. Both are public,
# sha256-pinned in versions.env, and re-download only on a version bump.
- name: Fetch the pinned Mainsail and HelixScreen assets
run: make vendor ASSET_ROOT=$PWD
- name: Extract the printer rootfs
run: make rootfs ASSET_ROOT=$PWD
# The replica starts sibling containers through the docker socket and
# needs loop devices for the simulated USB stick; both work on a stock
# ubuntu-latest runner.
- name: Brick-safety suite on the printer replica
run: |
docker build -q -t creator5-fw-build -f docker/Dockerfile.build docker
docker run --rm \
-v "$PWD":"$PWD" -w "$PWD" \
-v /var/run/docker.sock:/var/run/docker.sock \
-e REQUIRE_PRINTER_SIM=1 \
creator5-fw-build ./test/run-tests.py