mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 12:52:16 -05:00
The printer-sim job wrote its config.env from an UNQUOTED heredoc, and one
of the comments inside it says
# `make vendor` puts the pinned download. Setting them here would be
Those backticks are command substitution. The step ran `make vendor`,
captured its stdout, and pasted the fetch log into the config file it was
supposed to be writing -- so config.env line 6 came out as
>> fetch https://github.com/ghzserg/FF/releases/download/R/...
which sources as a redirect into a file named "fetch" plus a URL for a
command. Every run of that job died at 127 before it fetched anything.
The delimiter is quoted now and ROOT_PW_HASH loses the backslashes it only
needed to survive expansion. release.yml keeps its unquoted heredoc: it
interpolates $(case ...) and $PWD deliberately, and has no backticks inside.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S64uRMdZbj9xPMns2dqMG8
302 lines
15 KiB
YAML
302 lines
15 KiB
YAML
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
|