SignPath Foundation's terms make a person approve every signing request, so the plan's automatic approval doesn't exist. The action gave up after ten minutes, which would fail a release whenever the maintainer stepped away; it now waits an hour, and the Windows job's timeout grows to make room. Only a tag build or a run started by hand asks to be signed: a push to main that touched the packaging builds unsigned instead of sending an approval request nobody wanted. Still inert until the SignPath variable exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
201 lines
9.9 KiB
YAML
201 lines
9.9 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
|