Notcurses-Native.git | .github/workflows/ | _release-publish.yml edit


# 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