Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
537 changes: 537 additions & 0 deletions .github/workflows/build-armv6.yml

Large diffs are not rendered by default.

322 changes: 59 additions & 263 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: CI
# if every branch push produces them -- not just the ones that have a pull request open. The
# cost is that a PR raised from a branch in this repo builds twice.
#
# Except tags, which belong to release.yml. Both workflows call the same build, and the
# `concurrency` group below is keyed by workflow name, so nothing would collapse the pair: a
# `v*` push would otherwise run the matrix twice over and report two statuses for one tag.
# Except tags, which belong to release.yml. Both workflows call build.yml, and the `concurrency`
# group below is keyed by workflow name, so nothing would collapse the pair: a `v*` push would
# otherwise run the matrix twice over and report two statuses for one tag.
#
# The `branches` filter is what excludes them, and the rule behind it cuts both ways. A `push`
# naming only branch filters does not fire for tags at all -- that is the whole of how a tag is
Expand Down Expand Up @@ -47,9 +47,9 @@ jobs:
# Stays here rather than moving into build.yml with the rest, and the asymmetry is worth being
# honest about now that build_macos_pkg.sh shapes a released artifact: what this job adds is
# *lint*, and a tag push runs release.yml alone, so it is lint that a tag does not get. What a
# tag does get is four of them being run -- smoke_test.sh on every publishing leg,
# build_arm32.sh on the cross-compiled 32-bit ARM one, build_armv6_container.sh on the
# containerised one, and build_macos_pkg.sh on the macOS one, whose .pkg is then installed and
# tag does get is four of them being run -- smoke_test.sh on every publishing build,
# build_arm32.sh on the cross-compiled 32-bit ARM leg, build_armv6_container.sh throughout
# build-armv6.yml, and build_macos_pkg.sh on the macOS leg, whose .pkg is then installed and
# its receipt asserted -- so a release is gated on the scripts working whether or not it is
# gated on their style.
shellcheck:
Expand All @@ -63,7 +63,14 @@ jobs:
- name: Lint the shell scripts
run: shellcheck scripts/*.sh

# The matrix itself lives in build.yml, which release.yml calls too, so a change to how this
# project builds reaches both paths rather than one of them.
# The matrix itself lives in build.yml, which release.yml calls too, so a change to how these
# five legs build reaches both paths rather than one of them.
#
# There is a sixth archive and it is deliberately not here. build-armv6.yml builds linux-armv6
# inside an emulated Raspbian container -- 23 minutes against the two or three these legs take
# -- so it carries its own triggers rather than running on every push: `main` after a merge, a
# narrow path filter on pull requests, and a dispatch. release.yml calls it beside build.yml and
# blocks the release on both, so what a tag is gated on is a superset of this. That file's
# header sets the trade out in full.
build:
uses: ./.github/workflows/build.yml
49 changes: 36 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ on:
# would be free to cancel this run midway through uploading assets, which is the exact partial
# state the release below is arranged to make impossible. A superseded tag push is not a thing
# that happens anyway: a tag is pushed once.
#
# The single trigger above is load-bearing for a second reason, in another file. build-armv6.yml
# does carry a `concurrency` block, and what stops it reaching into a run started here is that its
# `cancel-in-progress` is false on a tag ref -- which holds only while every run of this workflow
# has one. Giving this workflow a `workflow_dispatch` or any branch trigger would hand a Release
# run a branch ref and quietly re-open the window this comment exists to keep shut.

permissions:
contents: read
Expand Down Expand Up @@ -111,15 +117,29 @@ jobs:
}
echo "no release claims $TAG yet"

# The same build ci.yml runs, matrix and assertions and all. `needs: preflight` is what keeps
# a bad tag from starting it.
# The same five legs ci.yml runs, matrix and assertions and all. `needs: preflight` is what
# keeps a bad tag from starting it.
build:
needs: preflight
uses: ./.github/workflows/build.yml

# The sixth archive, and the one thing a tag builds that a push does not. It is a workflow of
# its own because it emulates every compile and costs 23 minutes against the two or three the
# legs above take, so it carries triggers that keep it off the per-push path -- `main` after a
# merge, a narrow path filter on pull requests, and a dispatch. build-armv6.yml's header sets
# that trade out in full.
#
# Called here rather than left to those triggers, and blocking rather than advisory: `release`
# needs it below, so an ARMv6 build that goes red fails the release. The seam that leaves is
# that a break can reach a tag having gone unnoticed since the last merge to main; what it
# cannot do is publish a release the archive is missing from.
build-armv6:
needs: preflight
uses: ./.github/workflows/build-armv6.yml

release:
name: release
needs: [preflight, build]
needs: [preflight, build, build-armv6]
runs-on: ubuntu-24.04
timeout-minutes: 10
# Scoped to this job alone, the workflow default above staying read-only. The only other
Expand All @@ -137,21 +157,24 @@ jobs:
steps:
# `merge-multiple`, or each artifact arrives in a directory named after itself and every
# path below gains a redundant level. No `name:` or `pattern:`, so this asks for whatever
# the build produced -- which is the point: the assertion that follows is what says
# whether that was the right set, and a pattern here would pre-filter the evidence.
- name: Download what the build produced
# the two builds produced between them -- which is the point: the assertion that follows is
# what says whether that was the right set, and a pattern here would pre-filter the
# evidence.
- name: Download what the builds produced
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
merge-multiple: true

# The check that makes the release atomic on the way in. `needs: build` already covers a
# leg that went red; what it does not cover is a leg that went *green while publishing
# nothing* -- `Upload` is `if: matrix.publish`, and a leg that lost that key would skip
# the upload and still succeed. `if-no-files-found: error` catches an empty upload, not an
# absent one, and download-artifact matching nothing is not an error either: it is an
# empty directory. So the set is named here and diffed, in the shape build.yml already
# uses on the payload.
# The check that makes the release atomic on the way in. The two `needs` above already cover
# a build that went red; what they do not cover is one that went *green while publishing
# nothing* -- build.yml's `Upload` is `if: matrix.publish`, and a leg that lost that key
# would skip the upload and still succeed. Nor do they cover a `needs:` that stopped naming
# one of the two builds, which is the failure two build jobs make possible where one did
# not. `if-no-files-found: error` catches an empty upload, not an absent one, and
# download-artifact matching nothing is not an error either: it is an empty directory. So
# the set is named here and diffed, in the shape build.yml already uses on the payload --
# which is what makes an archive nobody built a diff rather than a quietly smaller release.
#
# Only the set, not the contents: every archive had its own file list diffed twice inside
# the build, and the .pkg was installed there and its receipt diffed as well, so re-opening
Expand Down
58 changes: 41 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,9 +1326,8 @@ path. CI runs it on every platform leg; run it yourself against any build.
## CI

Every branch push and pull request builds on `ubuntu-24.04`, `ubuntu-24.04-arm`
and `macos-14`, a fourth leg cross-compiled for ARMv7 on `ubuntu-24.04`, a fifth
built for ARMv6 inside an emulated Raspbian container on the same runner, and a
sixth configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles
and `macos-14`, a fourth leg cross-compiled for ARMv7 on `ubuntu-24.04`, and a
fifth configured `-DSENDSPIN_CLI_WITH_MDNS=OFF` — which compiles
`src/mdns_null.cpp` in place of `src/mdns_dnssd.cpp`, so that configuration is
built rather than assumed. Every leg builds with `-DSENDSPIN_CLI_WERROR=ON` and
runs the unit suite, and each asserts from its own configure output that it found
Expand All @@ -1343,20 +1342,35 @@ under `qemu-user`, and the linked binary's own ELF build attributes are asserted
say ARMv7, hard-float EABI before an archive is made — which is what catches a
dependency quietly compiled for something else.

The ARMv6 leg cannot be that, and the difference is the toolchain rather than the
The ARMv6 build cannot be that, and the difference is the toolchain rather than the
runner: Debian and Ubuntu `armhf` are an ARMv7-A port, so a cross toolchain's own
`crt1.o` and `libgcc.a` are ARMv7 and end up in the binary whatever `-march` said.
Raspbian's are genuinely ARMv6, so that leg runs an ordinary native build inside a
Raspbian's are genuinely ARMv6, so it runs an ordinary native build inside a
digest-pinned Raspbian container under `qemu-user` — with its suite and smoke test
under `QEMU_CPU=arm1176`, so the emulator is no more permissive than an ARM1176 —
and asserts ARMv6, hard-float EABI off the finished binary the same way. It holds
the warning line with one exemption: Raspbian's gcc 12 has the same `-Wrestrict`
false positive the `pipewire-minimum` job documents, so that leg passes
false positive the `pipewire-minimum` job documents, so it passes
`-Wno-error=restrict` and leaves `-Werror` standing over everything else.

The matrix lives in `.github/workflows/build.yml`, which both `ci.yml` and
`release.yml` call, so a release is built and gated exactly the way a push is.
`ci.yml` ignores tags for that reason — otherwise a tag would build twice.
Emulating every compile is what that costs, and it measures 23 minutes against the
two or three every other leg takes — so ARMv6 is not in the matrix. It has a
workflow of its own, `.github/workflows/build-armv6.yml`, which runs on a push to
`main`, on a pull request touching that workflow, `scripts/build_armv6_container.sh`
or `CMakeLists.txt`, on a manual dispatch, and on a tag. Not on every branch push,
which is the whole point: a push no longer waits half an hour on it.

The five-leg matrix lives in `.github/workflows/build.yml`, and both `ci.yml` and
`release.yml` call it. `ci.yml` ignores tags — otherwise a tag would build twice.

A tag therefore builds one thing a push does not, which is the trade this split
buys and worth stating rather than leaving to be discovered. What limits it is that
`release.yml` calls the ARMv6 workflow beside the matrix and blocks the release on
both, so an ARMv6 break can delay a tag but cannot publish a release the archive is
missing from — and the tip of `main` gets an ARMv6 build within half an hour of a
merge either way. A pull request that expects to break ARMv6 alone, which in
practice means the `-Wrestrict` and `-latomic` classes rather than anything in the
workflow files, can be opted in with a manual dispatch.

To try a commit without building it, open its run under the repository's Actions
tab and take `sendspin-cli-<version>-<os>-<arch>` from the run summary. Inside is a
Expand All @@ -1376,17 +1390,27 @@ unpacking it at `/`. Or run it where you unpacked it, at
of `/usr/local` leaves the unit naming a path with nothing at it. The macOS leg
publishes a second artifact beside that tarball,
`sendspin-cli-<version>-macos-arm64-installer`, holding the
[`.pkg`](#the-macos-installer-pkg) described below. These are per-commit builds
kept for 14 days. For something that does not expire, take a
[release](../../releases) instead.
[`.pkg`](#the-macos-installer-pkg) described below.

Five of the six archives are built on every commit that way. The sixth,
`linux-armv6`, is on its own workflow's runs instead — a push to `main`, a pull
request touching one of the three paths above, a manual dispatch, or a tag — so on
an arbitrary branch commit there is no ARMv6 tarball to take. Dispatch the ARMv6
workflow against that branch if you need one.

All of these are kept for 14 days. For something that does not expire, take a
[release](../../releases) instead, which carries all six.

## Releases

Pushing a `vMAJOR.MINOR.PATCH` tag builds the same matrix and publishes the five
platform archives and the macOS installer `.pkg`, plus a `SHA256SUMS` covering all
six, as a GitHub Release. The workflow triggers on `v*` but refuses anything else
that matches — a prerelease like `v0.2.0-rc1` is rejected rather than quietly
published as the latest release, until somebody decides what it should mean.
Pushing a `vMAJOR.MINOR.PATCH` tag builds the same matrix, and the ARMv6 workflow
beside it, and publishes the five platform archives and the macOS installer `.pkg`,
plus a `SHA256SUMS` covering all six, as a GitHub Release. Both builds have to go
green: the release job needs them both, so a red ARMv6 build fails the release
rather than publishing without its archive. The workflow triggers on `v*` but
refuses anything else that matches — a prerelease like `v0.2.0-rc1` is rejected
rather than quietly published as the latest release, until somebody decides what it
should mean.
Nothing else publishes, and the workflow never creates a tag: a release exists
because a human tagged a commit
whose version `CMakeLists.txt` already agreed with. It is attached whole or not at
Expand Down
Loading
Loading