Files
MeshTerm/.github/workflows/installers.yml
T
Jean-Pierre MartineauandClaude Opus 5.5 c5e7ce5a62 A release sends the Cardputer Zero package to M5Stack's app store
installers.yml gains a cardputer-zero job: build-deb.sh in a Debian 13
container on an arm64 runner, the same system as the Cardputer. The
release attaches the .deb with the other downloads and lists it in
SHA256SUMS, and its notes say where Cardputer users get MeshTerm.

github-release.yml gains a store job after the release. It runs
scripts/cardputer-zero/store-publish.py, which calls czdev publish with
two corrections: the manifest's email is johnputer@meshterm.net, not
the GitHub profile's, and the branch name stays the same between
czdev's two calls (for a fork, the second call named a branch that was
never pushed, and GitHub refused the PR). It also sends czdev's git
over HTTPS with the token, and it publishes nothing when the store has
the version or a PR for it is open, so a rerun opens no second PR.
AppBuilder is pinned to the commit whose functions it corrects.

The job needs the CARDPUTER_STORE_TOKEN secret (public_repo scope). The
release skill now checks the store PR and counts fifteen assets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 04:32:35 -04:00

237 lines
11 KiB
YAML

# Standalone builds — a single file you download and run, with no Python installed.
#
# For most people who want MeshTerm, this is the install. They have a radio and a
# terminal, not a Python toolchain.
#
# Frozen builds cannot be cross-compiled: each one is made on the system it targets, so
# the matrix is the point of this file rather than an optimisation. Everything here
# uploads artifacts and nothing publishes — attaching them to a release stays a separate,
# deliberate act.
name: Installers
on:
workflow_dispatch:
# Callable, so the release workflow builds these binaries rather than keeping a second
# copy of the matrix that would drift from this one.
workflow_call:
# Only when the packaging itself changes. A five-platform build takes the better part
# of half an hour, and a commit that touched nothing it freezes has nothing to learn
# from one; the release build is what proves the binaries.
push:
# `main` only. Without this, every Dependabot branch that touches this file — which
# is every action bump — starts a five-platform build of its own.
branches: [main]
paths:
- "packaging/**"
- ".github/workflows/installers.yml"
permissions:
contents: read
concurrency:
group: installers-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.os }}
# Windows gets an extra hour: a signed build waits for the maintainer to approve it on
# SignPath (see "Sign it" below), and that wait is spent inside this job.
timeout-minutes: ${{ matrix.os == 'windows-latest' && 90 || 25 }}
strategy:
fail-fast: false
matrix:
include:
- { os: windows-latest, label: windows-x64, ext: ".exe" }
- { os: macos-latest, label: macos-arm64, ext: "" }
# Apple silicon builds do not run on Intel Macs, and plenty of people still
# have one. `macos-15-intel` rather than `-26-intel` for the same reason the
# Python here is 3.10: built against an older system, runs on more machines.
# (Not `macos-13` — that image is retired, and a job asking for it does not
# fail, it queues forever and holds the whole run open behind it.)
- { os: macos-15-intel, label: macos-x64, ext: "" }
- { os: ubuntu-22.04, label: linux-x64, ext: "" }
# For the uConsole and other 64-bit ARM handhelds. The ARM runners are the
# public-repository ones; a private fork of this file may find the leg sits
# unclaimed, and can drop the line.
- { os: ubuntu-22.04-arm, label: linux-arm64, ext: "" }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# The Python the project builds with, bundled into every binary. How widely a
# Linux binary runs is set by the runner's glibc, which is why the Linux legs
# stay on ubuntu-22.04 rather than ubuntu-latest.
python-version: "3.14"
- run: pip install . pyinstaller
- name: Build
working-directory: packaging
run: pyinstaller --clean --noconfirm meshterm.spec
# `packaging/meshterm.spec` regenerates `THIRD-PARTY-NOTICES.txt` fresh on every
# leg's own build (see `packaging/notices.py`) and bundles it, along with LICENSE
# and NOTICE, inside that leg's own installer -- so each downloaded binary always
# carries an accurate copy of what it actually contains. This step is a separate
# convenience: the same three files land on the release page itself, next to the
# downloads, so nobody has to extract an installer just to read a license. One
# copy serves every platform there (LICENSE/NOTICE never vary, and the runtime
# dependency closures differ only in a handful of platform-specific packages), so
# it rides on this leg alone rather than being uploaded -- and attached to the
# release -- five identical-in-substance times over. linux-x64 was picked
# arbitrarily among the five; nothing here is Linux-specific.
- name: Upload the licences for the release page
if: matrix.label == 'linux-x64'
shell: bash
run: |
# `build/<specname>/` is PyInstaller's own workpath layout (see the spec's
# comment on `workpath`), not something worth this step knowing by heart
# twice -- copy it flat, next to LICENSE and NOTICE, so the artifact (and,
# via `find`, the release assets built from it) is three plain files.
cp packaging/build/meshterm/THIRD-PARTY-NOTICES.txt .
- uses: actions/upload-artifact@v4
if: matrix.label == 'linux-x64'
with:
name: meshterm-licenses
path: |
LICENSE
NOTICE
THIRD-PARTY-NOTICES.txt
if-no-files-found: error
- name: Check it actually runs
shell: bash
run: |
bin="packaging/dist/meshterm${{ matrix.ext }}"
chmod +x "$bin" || true
# Run it from somewhere else entirely, so a build that only works next to the
# source tree fails here rather than on someone's laptop.
cd "$(mktemp -d)"
"$GITHUB_WORKSPACE/$bin" --help > /dev/null
# Reads the asset files, so a bundle missing its pages fails here.
"$GITHUB_WORKSPACE/$bin" specimen > /dev/null
# And a tool-provided subcommand, which is the check that matters. Every tool is
# found at runtime by `pkgutil.iter_modules`, so a bundle that omits them still
# starts, still prints help, still draws the specimen — and has an empty menu
# with nothing in it but Quit. `--help` passing proves almost nothing on its own.
"$GITHUB_WORKSPACE/$bin" contacts --help > /dev/null
echo "runs, finds its assets, and its tools registered"
# The Windows binary is code-signed, so Windows has a publisher to recognise
# instead of an unknown file it has never seen before — SmartScreen's warning eases
# as that publisher's downloads accumulate, and antivirus engines, which flag
# unsigned PyInstaller one-file builds on sight, are markedly less trigger-happy.
# The certificate belongs to SignPath Foundation, who issue it free to open-source
# projects and sign on their own infrastructure; nothing secret exists on this
# runner, and there is no key here to leak. Its condition is that the artifact
# provably came from this repository's CI, which is why the binary is handed over
# as a GitHub artifact rather than uploaded: SignPath reads the workflow run behind
# that artifact id and refuses anything it cannot trace to an approved branch.
#
# Signed between the build's own smoke test and the naming step, so what gets
# verified is the build and what gets checksummed is the signed file — a checksum
# taken before signing would describe a binary nobody ever downloads.
#
# Both legs are skipped wholesale until the account exists (the variable is unset
# until then), so this file builds unsigned Windows binaries in the meantime rather
# than failing, and a fork with no SignPath project of its own is unaffected.
#
# SignPath Foundation's terms make a person approve every signing request, so each
# one waits on the maintainer. Only a release (a tag, arriving through the release
# workflow's call) or a run started by hand asks; a push to `main` that touched the
# packaging builds unsigned, rather than sending an approval request nobody wanted
# and failing the run when it went unanswered.
- name: Hand the Windows binary to SignPath
id: unsigned
if: >-
runner.os == 'Windows' && vars.SIGNPATH_ORGANIZATION_ID != '' &&
(github.ref_type == 'tag' || github.event_name == 'workflow_dispatch')
uses: actions/upload-artifact@v4
with:
# Deliberately not `meshterm-*`: the release workflow attaches every artifact
# matching that prefix, and this one is the unsigned draft.
name: unsigned-${{ matrix.label }}
path: packaging/dist/meshterm.exe
if-no-files-found: error
- name: Sign it
if: steps.unsigned.outcome == 'success'
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: meshterm
signing-policy-slug: release-signing
artifact-configuration-slug: exe
github-artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
# The signed file comes back where the unsigned one was built, under the same
# name, so every step after this one is untouched by any of it.
wait-for-completion: true
# An hour to approve it, where the action's own default is ten minutes: long
# enough to see SignPath's email and answer it from a phone, so a release
# doesn't fail because the maintainer stepped away. The job's timeout above
# leaves room for it.
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: packaging/dist
- name: Name it after the version and the platform
shell: bash
run: |
version=$(python -c "import meshterm; print(meshterm.__version__)")
out="meshterm-${version}-${{ matrix.label }}${{ matrix.ext }}"
mv "packaging/dist/meshterm${{ matrix.ext }}" "$out"
# A checksum beside the binary, so anyone can tell they got what we built.
if command -v sha256sum > /dev/null; then
sha256sum "$out" > "$out.sha256"
else
shasum -a 256 "$out" > "$out.sha256"
fi
cat "$out.sha256"
- uses: actions/upload-artifact@v4
with:
name: meshterm-${{ matrix.label }}
path: |
meshterm-*
if-no-files-found: error
# The Debian package for the Cardputer Zero's app store. It holds compiled code for arm64
# and Python 3.13, so it is built in Debian 13 on an arm64 runner: the same system as the
# Cardputer. The release attaches it with the binaries above, and its `store` job sends it
# to M5Stack's app store.
cardputer-zero:
name: cardputer-zero-deb
runs-on: ubuntu-24.04-arm
container: debian:trixie
timeout-minutes: 30
steps:
# gcc and the headers compile spidev, which has no arm64 wheel on PyPI.
- name: Install the build tools
run: |
apt-get update
apt-get install -y --no-install-recommends \
python3 python3-venv python3-dev gcc libc6-dev ca-certificates
- uses: actions/checkout@v4
- name: Build
run: sh scripts/cardputer-zero/build-deb.sh
- name: Checksum it
working-directory: dist
run: |
for deb in meshterm_*_arm64.deb; do sha256sum "$deb" > "$deb.sha256"; done
cat ./*.sha256
- uses: actions/upload-artifact@v4
with:
name: meshterm-cardputer-zero
path: |
dist/meshterm_*_arm64.deb
dist/meshterm_*_arm64.deb.sha256
if-no-files-found: error