name: ci # The stock FlashForge firmware is proprietary and ~93MB, so it is never in # the repo. The `test` job therefore runs only what needs no firmware at all: # qa/static and the host-side pytest tree. That is 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 and pinned in versions.env 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 are no longer fetched here. They are pinned by name and # sha256 in versions.env like every other third-party source, and # bin/fetch-assets.sh downloads them into vendor/ -- so `make vendor` below # gets them the same way a person's checkout does, and this workflow no longer # carries a second spelling of "fetch the firmware, check the hash" that could # drift from the one humans run. # # The secrets that used to override the URLs (STOCK_URL_CREATOR5PRO and # friends, for a fork testing its own firmware) are gone with them. A fork # re-pins versions.env instead, which works locally too -- the secrets only # ever worked in CI, and the local build had no equivalent at all. # # The factory image used to be fetched here too, for PROG_DUMP. Both are gone: # the replica is PRINTER_IMAGE and nothing else, and that image carries the # real /usr/prog already. PRINTER_IMAGE is pinned to a version tag for the # same reason the sha256s are -- it is now the whole substrate the # brick-safety gates run on, so it must not move on its own. jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # gcc/pkg-config/perl/zlib1g-dev/patch/make are for the SAME reason # docker/Dockerfile.build carries them: cross-building CPython 3.13 # needs a HOST build-python of its own to freeze modules and # byte-compile the stdlib, and that one needs a host C compiler, # pkg-config (CPython's configure probes libffi through it), perl # (OpenSSL's Configure is a perl program), zlib1g-dev (the build-python # needs a working zlib module or ensurepip cannot even unpack pip's # bundled wheel) and patch (lmdb's setup.py shells out to # /usr/bin/patch by absolute path). See that file's own comment for the # long version. - 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 python3-pyflakes \ gcc pkg-config perl zlib1g-dev patch make # The shell parse and the bashism pass live in qa/static/test_shell_syntax.py # now; this step is the repo-wide shellcheck, which is a wider net. - name: shellcheck run: | set -euo pipefail sudo apt-get install -y shellcheck # FOUND, NOT GLOBBED, and the count is asserted. The list here used # to be four literal globs, two of which stopped matching anything: # pkgs/*/payload/init.d/S* went when s6-rc replaced the init # scripts, and pkgs/*/prog/ moved under payload/. shellcheck exits 2 # on a path it cannot open, so this step failed on every run -- and # had it not, the opposite failure was worse: a glob that quietly # matches nothing checks nothing and still reports green. mapfile -t PRINTER_SH < <( find pkgs -path '*/payload/*' \ \( -name '*.sh' -o -name firmwareExe \) | sort) PRINTER_SH+=(installer/runFirmwareExe.sh) if [ "${#PRINTER_SH[@]}" -lt 5 ]; then echo "::error title=Nothing to check::found ${#PRINTER_SH[@]} on-printer scripts; the find pattern is stale." exit 1 fi printf 'checking: %s\n' "${PRINTER_SH[@]}" # 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 "${PRINTER_SH[@]}" shellcheck -s bash -S error bin/*.sh tools/replica/*.sh # Warnings are advisory: printed, not enforced. shellcheck -s sh -S warning "${PRINTER_SH[@]}" || true # s6 and CPython 3.13 have no BUILD_ flag -- every package ships them # (see bin/fetch-assets.sh) -- so this job builds them for real rather # than stubbing around them the way it stubs Mainsail, HelixScreen and # the Klipper fork. Cached on versions.env: an unchanged pin never # re-fetches the ~203MB Ingenic toolchain and never recompiles either # tree. Both are recipes under work/pkg now -- four for s6, nineteen for # CPython and its site-packages -- and every one carries pkg_stamp's key. # # work/pkg is every recipe under pkgs/ at once -- one cache entry instead # of one per package, because pkgs/lib.sh derives the output directory # from the recipe name rather than naming each one in bin/common.sh. A # new recipe therefore needs no edit here, which is the same property # that makes adding a package one directory. work/packages is the feed # built out of those trees, cached beside them for the same reason. - name: Cache the s6 + CPython 3.13 build inputs and outputs uses: actions/cache@v4 with: path: | vendor work/pkg work/packages key: fixture-py-${{ hashFiles('versions.env') }} - name: Fetch the s6 + CPython 3.13 build inputs run: | set -euo pipefail # This config.env exists purely to let fetch-assets.sh see a # config at all; it skips Mainsail and HelixScreen (still gated on # their own BUILD_ flags, unset here) and fetches what s6, CPython # and the Klipper fork need, none of which has a BUILD_ flag of its # own any more -- BUILD_KLIPPER is gone and the fork is the only # Klipper, so its 27MB tarball comes down here with the rest. This # config.env is KEPT rather than removed: the packaging step below # reads it too, and unlike payload.sh, bin/build-packages.sh needs no # stock package -- which is the whole reason the feed can be built # in this job at all. # # s6 and CPython are not cross-built by payload.sh any more; there are # no sections 5b/5c. They are recipes under pkgs/, built by # build-packages.sh like every other one. cat > config.env <<'CFG' BUILD_MAINSAIL=0 BUILD_HELIX=0 BUILD_MOONRAKER=0 CFG ./bin/fetch-assets.sh # BEFORE the static lane, because a good part of what that lane asks is # about .ipk files, and those questions do not fail on a checkout with # no feed -- they return, quietly, and the lane reports a pass having # never opened a package. Building the feed here is what makes that # answer mean something, and it needs no stock firmware to do it. - name: Build the package feed run: ./bin/build-packages.sh # The qa suite's static lane: parses, bashisms, names, recipe layout, # and -- now that the feed above exists -- what is inside the .ipk files. # # No strictness flag: a missing shellcheck or pyflakes fails on its own, # because there is no configuration in which running this lane without # them is a complete run. Only the static lane here -- this job has no # stock firmware on purpose, and printer-sim below is where the replica # is proved. - name: qa -- static lane run: python3 -m pytest ./qa/static -q # 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 - name: Write test.env (the replica) env: # THE replica. It carries the printer's rootfs, its real /usr/prog # and /usr/data, and the stock package already installed on top of # them -- about a minute of setup saved on EVERY replica case, which # over a full suite is the difference between this job and a coffee # break. vars.PRINTER_IMAGE overrides it for a fork building its own. # # A VERSION TAG, NOT :latest. This image is now the entire substrate # the brick-safety gates run on -- there is no local fallback left # to build one from a stock package -- so a floating tag would let # it change under a green suite. That is precisely the drift the # sha256 pins above exist to prevent, and it would be worse here, # because nothing in the run would even record what was tested. 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 MOD_VER=ci TARGET_MACHINE=Creator5Pro # No stock paths: bin/common.sh defaults them into vendor/, where # `make vendor` puts the pinned download. Setting them here would be # the override that suppresses the checksum. STOCK_TGZ="" # No Klipper source knob any more: pkgs/klipper builds from the # commit pinned in versions.env and nothing else. The variable that # used to live here, KLIPPER_FORK, is what let v20260824 ship the # stock klippy tree by being set to "" in this very file. ROOT_PW_HASH='$6$ci$abcdefghijklmnopqrstuvwxyz' FF_KEY='FFP0331&*%root' CFG # The replica gates run ./bin/payload.sh, which needs the real Mainsail # zip and HelixScreen tarball in vendor/ -- payload.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/. All are public, # sha256-pinned in versions.env, and re-download only on a version bump. # # Cached because the set now includes the Klipper fork and the ~203MB # MIPS toolchain that compiles its chelper -- this job runs on every # push, and the toolchain host should not pay for that. - name: Cache the pinned vendor assets uses: actions/cache@v4 with: path: vendor key: vendor-${{ hashFiles('versions.env') }} # `make vendor` is --all --stock, which now also brings down both models' # stock firmware (~186MB) -- this job builds Creator5Pro but the qa lane # reads both baselines. Same cache, keyed on versions.env, so the pins and # the firmware they name move together and an unchanged pin costs nothing. # The `test` job above calls bin/fetch-assets.sh bare and so still gets # no firmware at all, which is what keeps its feed build honest. - name: Fetch the pinned vendor assets and the stock firmware run: make vendor ASSET_ROOT=$PWD # THE FEED, and it is not optional on the release path. bin/payload.sh # builds the payload by INSTALLING the feed, so it refuses to start # without one, and the python recipes resolve PKG_BUILD_DEPENDS out of # work/packages. These two steps ran as one unpack/patch/pack with no # feed for a while, which could not have passed. # # Unlike the build below, this needs no stock firmware -- which is most # of the point: packaging has to be runnable on a bare checkout, or the # gate only runs where the proprietary package is. - name: Build the package feed run: make packages ASSET_ROOT=$PWD # BUILD THE PACKAGE THE SUITE INSTALLS. The qa replica lane installs for # real -- the printer's own app_startup.sh, off a FAT stick -- so it # needs a package in work/out, and it never builds one on demand: that # would turn "the suite is slow today" into a mystery. # # It also supplies the cross-built s6 and CPython, because both ship # INSIDE the package; there is no separate build tree to track. # # Through `make`, not around it. `make build` runs bin/build.sh on the # RUNBUILD lane -- as the runner user, with the docker socket and the # socket's own gid -- because bin/payload.sh assembles the payload inside # the replica. Hand-rolling the `docker run` here meant CI exercised an # entry point no human uses, so a mistake in one of the Makefile's own # runners could never turn this job red. - name: Build the package run: make build ASSET_ROOT=$PWD # The brick-safety gate. No ALLOW_SKIP and no REQUIRE_PRINTER_SIM: qa has # no skip mode at all, so a missing docker, daemon, image or package # fails at the point that needs it rather than being reported as a gap # somebody has to remember to accept. # # 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: qa suite on the printer replica run: make qa ASSET_ROOT=$PWD # The docs make claims about a machine that can be left unbootable, and the # pages cross-link each other and the payload sources heavily -- 30+ links # between README.md, docs/*.md and docs/notes/. This is the cheap half of # keeping them true: --strict fails on a link whose target has moved or been # deleted, so a rename in payload/ cannot quietly leave a doc pointing at # nothing. It cannot check that the prose is still right; only a reader can. # # Read the Docs builds the same mkdocs.yml with fail_on_warning, so this job # going red is also the warning that the published site is about to fail. docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" # what .readthedocs.yaml builds with - name: Install the docs toolchain run: pip install -r docs/requirements.txt - name: Build the site run: mkdocs build --strict