# Reusable workflow: collect every uploaded artefact from the
# parallel build lanes, fetch and verify the third-party source
# tarballs those artefacts were built from, assemble a single
# `checksums.txt` over both, validate that the pushed tag matches
# BINARY_TAG, and publish to a GitHub release.
#
# Started life as a straight extraction of build-binaries.yml's
# `release` job, so that the top-level release.yml could stay focused
# on orchestration. It has since grown two responsibilities that are
# not just plumbing, and both are release gates rather than
# conveniences:
#
# * it publishes only on a pushed `binaries-*` tag, never on a
# workflow_dispatch (see the `if:` below);
# * it will not publish at all unless every source tarball named in
# resources/third-party.json downloads and hashes to the recorded
# SHA-256 — that is the corresponding source for the LGPL
# libraries inside the archives, and attaching source that does
# not match the binaries would be worse than attaching none.
name: _release-publish
on:
workflow_call:
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
# Tag-triggered runs only. A workflow_dispatch used to publish too —
# useful when dispatch was the release path, but it means "validate
# the recipe on a branch" and "overwrite the published archives that
# every installed dist checksum-verifies against" are one click
# apart: a dispatch while BINARY_TAG still names the LIVE tag would
# clobber its assets and break every existing install's download.
# Dispatch now builds and smokes all eight lanes and stops, the same
# contract as the app release workflows downstream; only pushing the
# binaries-* tag itself publishes.
if: startsWith(github.ref, 'refs/tags/binaries-')
steps:
- uses: actions/checkout@v6
- name: Download all artefacts
uses: actions/download-artifact@v7
with:
path: downloaded/
# Corresponding source for the copyleft libraries in the packs.
# FFmpeg (LGPL-2.1), GNU libunistring (LGPL-3.0) and, on the
# Windows packs, GNU libiconv (LGPL-2.1) all oblige us to be
# able to hand a recipient the source for the exact binary they
# got; the other four are attached so that "which source built
# this?" has one kind of answer for every component rather than
# two. The pinned URL + SHA-256 pair lives in
# resources/third-party.json and is the same pair the build
# scripts fetch, so what gets attached here is byte-identical to
# what the lanes compiled.
#
# Verification is a hard gate, not a nicety: attaching a tarball
# that does not hash to the recorded value would mean publishing
# "here is the source for these binaries" next to source that
# is not. If this fires, re-verify the upstream tarball by hand
# and update the manifest — do not delete the check.
- name: Download + verify third-party source tarballs
shell: bash
run: |
set -euo pipefail
mkdir -p release
fail=0
# Process substitution rather than `jq | while`: a pipeline
# runs its right-hand side in a subshell, so a failure
# detected inside the loop could not affect $fail out here.
#
# The asset name comes from the manifest, not from
# `basename "$url"`. GitHub serves tag archives as
# `v1.16.0.tar.gz` — which tells a downloader nothing about
# what it contains and would collide with any other
# component pinned at the same version number.
while IFS=$'\t' read -r id url sha name; do
file="release/$name"
echo "--- $id ---"
if ! curl -fSL --retry 5 --retry-delay 10 -o "$file" "$url"; then
echo "::error::could not download $id source from $url"
fail=1
continue
fi
actual=$(sha256sum "$file" | awk '{print $1}')
if [[ "$actual" != "$sha" ]]; then
echo "::error::$id source tarball hash mismatch"
echo " url: $url"
echo " expected: $sha"
echo " actual: $actual"
fail=1
continue
fi
echo "ok: $name sha256 $actual"
done < <(
jq -r '
.components[]
| select(.source["attach-to-release"] == true)
| [ .id, .source.url, .source.sha256, .source.filename ]
| @tsv
' resources/third-party.json
)
if (( fail != 0 )); then
echo "❌ One or more third-party source tarballs could not be"
echo " fetched or did not match resources/third-party.json."
echo " Publishing would attach source that does not correspond"
echo " to the binaries in these archives."
exit 1
fi
echo "✅ All attachable source tarballs verified against the manifest."
- name: Flatten + generate combined checksums.txt + resolve tag
id: prep
shell: bash
run: |
set -euxo pipefail
mkdir -p release
find downloaded -type f \( -name 'notcurses-*.tar.gz' \
-o -name 'notcurses-*.zip' \) \
-exec cp {} release/ \;
# checksums.txt covers everything in release/ — the eight
# platform archives AND the source tarballs staged by the
# step above — so a recipient can verify the source they
# were given as easily as the binaries. Build.rakumod looks
# entries up by artefact name, so the extra lines are inert
# to it.
cd release
sha256sum * | sort > ../checksums.txt
cd ..
echo "--- checksums.txt ---"
cat checksums.txt
file_tag=$(cat BINARY_TAG | tr -d '[:space:]')
if [[ -z "$file_tag" ]]; then
echo "❌ BINARY_TAG empty / missing."
exit 1
fi
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
pushed=${GITHUB_REF#refs/tags/}
if [[ "$pushed" != "$file_tag" ]]; then
echo "❌ Pushed tag '$pushed' doesn't match BINARY_TAG '$file_tag'."
exit 1
fi
fi
echo "tag=$file_tag" >> "$GITHUB_OUTPUT"
- name: Create / update GitHub release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.prep.outputs.tag }}
name: ${{ steps.prep.outputs.tag }}
files: |
release/*
checksums.txt
body: |
Prebuilt notcurses binaries for `${{ steps.prep.outputs.tag }}`.
Each archive contains `libnotcurses`, `libnotcurses-core`,
`libnotcurses-ffi`, plus ffmpeg sibling dylibs/sos/DLLs
that notcurses dyn-links — all with rpaths relocated so
they load siblings from the archive's own directory — and a
`THIRD-PARTY.md` + `LICENSES/` licensing kit describing
everything in that particular archive.
Linux glibc archives are built in
`quay.io/pypa/manylinux_2_28_*` containers (glibc 2.28
floor — RHEL 8+, Ubuntu 18.10+, Debian 10+, i.e. every
glibc distro still under maintenance).
Linux musl archives are built in `alpine:3.20` containers
(musl 1.2.5 build, 1.20+ runtime — Alpine 3.13+, Postmarket
OS, Void, Adelie).
macOS arm64 targets macOS 11.0 (Big Sur).
macOS x86_64 targets macOS 10.15 (Catalina) and is built
under Rosetta 2 on an arm64 GHA runner.
Windows x86_64 is built in MSYS2 UCRT64; Windows arm64 in
MSYS2 CLANGARM64.
## Licensing
These archives redistribute third-party binaries. Every one
of them is inventoried in
[`resources/third-party.json`](https://github.com/m-doughty/Notcurses-Native/blob/main/resources/third-party.json),
and each archive carries the subset relevant to it as
`THIRD-PARTY.md` plus full licence texts in `LICENSES/`.
Copyleft, and therefore attached as source below:
* **FFmpeg** (LGPL-2.1-or-later) — a decoder-only build
with neither `--enable-gpl` nor `--enable-nonfree`, so
none of FFmpeg's GPL-only components are compiled in.
* **GNU libunistring** (LGPL-3.0-or-later OR
GPL-2.0-or-later; conveyed under the LGPL).
* **GNU libiconv** (LGPL-2.1-or-later) — Windows archives
only. Linux and macOS take iconv from their C library
and carry no copy of it.
Permissive: **notcurses** (Apache-2.0), **dav1d** (BSD-2),
**libvpx** and **Opus** (BSD-3), **libdeflate** (MIT),
**ncursesw** (X11-style), **zlib** (Zlib), and on Windows
the MSYS2 toolchain runtimes (MIT /
GPL-3.0-with-GCC-exception / Apache-2.0-with-LLVM-exception,
as listed per-archive).
The `*.tar.gz` / `*.tar.xz` / `*.tar.bz2` assets on this
release are the exact upstream source tarballs these
binaries were built from — same URLs, same SHA-256s, as
recorded in the manifest and verified at publish time. They
are the corresponding source for the copyleft libraries
above; the permissive ones are attached too so that every
component has one kind of answer to "what built this?".
Platform C runtimes (glibc, musl, Apple's `/usr/lib`,
Windows' own DLLs) are dynamically linked, never bundled,
and so are not redistributed here.
## Verifying
```
sha256sum -c checksums.txt
```
`checksums.txt` covers both the platform archives and the
attached source tarballs.
## After release
Copy `checksums.txt` contents into `resources/checksums.txt`
in the source tree + commit, then publish the next Raku
dist version.
draft: false
prerelease: false