From f6262cd2ec5d422305a4cf184277886c0b086c59 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:40:45 +0700 Subject: [PATCH 1/3] ci: make the binaries re-runnable, and give Windows a bash The 0.6.2 release shipped Linux and macOS archives and no zip. The build step had no shell: bash, and Windows runners default to PowerShell, where "$TARGET" expands to the empty string -- so cargo build --target was handed nothing. Every step is bash now, which is what the Package step already did and the Build step should have. The larger fix is the split. Publishing to crates.io happens once and cannot be repeated, so a failed artifact build inside release.yml leaves no way back except cutting another version. binaries.yml runs on a published release and on demand for any tag, so a leg that fails for a runner-image reason is re-run rather than re-released. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- .github/workflows/binaries.yml | 152 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 116 ------------------------- CHANGELOG.md | 13 +++ 3 files changed, 165 insertions(+), 116 deletions(-) create mode 100644 .github/workflows/binaries.yml diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml new file mode 100644 index 0000000..75bcaae --- /dev/null +++ b/.github/workflows/binaries.yml @@ -0,0 +1,152 @@ +name: binaries + +# Static, prebuilt binaries for every release, so that *using* mossaic does not +# require a Rust toolchain. +# +# Separate from release.yml on purpose. Publishing to crates.io happens once +# and cannot be repeated; building an artifact can fail for reasons that have +# nothing to do with the release -- a runner image change, a transient network +# -- and when it does the fix must not be "cut another version". This workflow +# is re-runnable against any existing tag. +# +# It fires when a release is published, and by hand for a tag whose artifacts +# need rebuilding. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: the release tag to build binaries for, e.g. v0.6.2 + required: true + +permissions: + contents: read + +env: + TAG: ${{ inputs.tag || github.event.release.tag_name }} + +jobs: + # Prebuilt binaries, so that using mossaic does not require a Rust toolchain. + # + # The reason is not convenience. The whole pitch for contributing a template + # is that it needs no Rust -- a template is a text file -- and that pitch is + # false if the only way to *see* what you drew is `cargo install`. Somebody + # who does not write Rust should not have to install a compiler to draw a + # picture. + # + # Linux targets are **musl**, so the binary is statically linked and runs on + # any distribution regardless of its glibc. Building against the runner's + # glibc would produce something that refuses to start on anything older than + # the runner, which for a download people keep for a year is the wrong + # trade. + # + # Every target builds on its own architecture rather than cross-compiling: + # GitHub gives public repositories arm64 runners, and a native build is one + # fewer thing that can be subtly wrong. + binaries: + name: ${{ matrix.target }} + runs-on: ${{ matrix.os }} + permissions: + contents: write # upload release assets + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-24.04-arm + - target: x86_64-apple-darwin + os: macos-13 + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + # The tag, not the branch: an artifact must be built from the commit + # the release names, whatever main has moved on to since. + ref: ${{ env.TAG }} + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 + with: + toolchain: stable + targets: ${{ matrix.target }} + + # --locked, because the point of a published binary is that it is the + # one the lockfile describes and the tests ran against. + # + # The target reaches the shell as an environment variable rather than + # interpolated into the script, which is how every other `run:` in this + # repository takes an input. + # `shell: bash` on every step, Build included. Windows runners default + # to PowerShell, where `"$TARGET"` expands to the empty string and + # `cargo build --target` is handed nothing -- which is how the first + # attempt at this failed. + - name: Build + shell: bash + env: + TARGET: ${{ matrix.target }} + run: cargo build --release --locked --target "$TARGET" + + - name: Package + shell: bash + env: + TARGET: ${{ matrix.target }} + run: | + set -euo pipefail + version="${TAG#v}" + stage="mossaic-${version}-${TARGET}" + mkdir "$stage" + suffix="" + case "$TARGET" in *windows*) suffix=".exe" ;; esac + for binary in mossaic mossaic-art mossaic-glyphs; do + cp "target/${TARGET}/release/${binary}${suffix}" "$stage/" + done + cp README.md LICENSE-MIT LICENSE-APACHE CHANGELOG.md "$stage/" + # The templates ship inside the binary, but a copy beside it is what + # someone starts from when drawing their own. + cp -r art/templates "$stage/templates" + + case "$TARGET" in + *windows*) + 7z a "${stage}.zip" "$stage" > /dev/null + archive="${stage}.zip" + ;; + *) + tar czf "${stage}.tar.gz" "$stage" + archive="${stage}.tar.gz" + ;; + esac + + # Checksums beside the archive: a download nobody can verify is a + # download nobody should run. + sums() { + if command -v sha256sum > /dev/null; then sha256sum "$1"; else shasum -a 256 "$1"; fi + } + sums "$archive" > "${archive}.sha256" + + # A second copy without the version in the name, because + # `releases/latest/download/` resolves by *exact* file name -- + # so a URL that is stable across releases cannot contain the version. + # The versioned name is what cargo-binstall and the Homebrew formula + # look for; the unversioned one is what a person copies out of the + # README and keeps working next release. + alias="${archive/-${version}-/-}" + cp "$archive" "$alias" + sums "$alias" > "${alias}.sha256" + + echo "ARCHIVE=$archive" >> "$GITHUB_ENV" + echo "ALIAS=$alias" >> "$GITHUB_ENV" + + - name: Attach to the release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "$TAG" \ + "$ARCHIVE" "${ARCHIVE}.sha256" \ + "$ALIAS" "${ALIAS}.sha256" --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9a6bfa..a989c29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,122 +116,6 @@ jobs: --notes-file "$RUNNER_TEMP/notes.md" \ --verify-tag - # Prebuilt binaries, so that using mossaic does not require a Rust toolchain. - # - # The reason is not convenience. The whole pitch for contributing a template - # is that it needs no Rust -- a template is a text file -- and that pitch is - # false if the only way to *see* what you drew is `cargo install`. Somebody - # who does not write Rust should not have to install a compiler to draw a - # picture. - # - # Linux targets are **musl**, so the binary is statically linked and runs on - # any distribution regardless of its glibc. Building against the runner's - # glibc would produce something that refuses to start on anything older than - # the runner, which for a download people keep for a year is the wrong - # trade. - # - # Every target builds on its own architecture rather than cross-compiling: - # GitHub gives public repositories arm64 runners, and a native build is one - # fewer thing that can be subtly wrong. - binaries: - name: ${{ matrix.target }} - needs: github-release - runs-on: ${{ matrix.os }} - permissions: - contents: write # upload release assets - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - - target: aarch64-unknown-linux-musl - os: ubuntu-24.04-arm - - target: x86_64-apple-darwin - os: macos-13 - - target: aarch64-apple-darwin - os: macos-latest - - target: x86_64-pc-windows-msvc - os: windows-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 - with: - toolchain: stable - targets: ${{ matrix.target }} - - # --locked, because the point of a published binary is that it is the - # one the lockfile describes and the tests ran against. - # - # The target reaches the shell as an environment variable rather than - # interpolated into the script, which is how every other `run:` in this - # repository takes an input. - - name: Build - env: - TARGET: ${{ matrix.target }} - run: cargo build --release --locked --target "$TARGET" - - - name: Package - shell: bash - env: - TARGET: ${{ matrix.target }} - run: | - set -euo pipefail - version="${GITHUB_REF_NAME#v}" - stage="mossaic-${version}-${TARGET}" - mkdir "$stage" - suffix="" - case "$TARGET" in *windows*) suffix=".exe" ;; esac - for binary in mossaic mossaic-art mossaic-glyphs; do - cp "target/${TARGET}/release/${binary}${suffix}" "$stage/" - done - cp README.md LICENSE-MIT LICENSE-APACHE CHANGELOG.md "$stage/" - # The templates ship inside the binary, but a copy beside it is what - # someone starts from when drawing their own. - cp -r art/templates "$stage/templates" - - case "$TARGET" in - *windows*) - 7z a "${stage}.zip" "$stage" > /dev/null - archive="${stage}.zip" - ;; - *) - tar czf "${stage}.tar.gz" "$stage" - archive="${stage}.tar.gz" - ;; - esac - - # Checksums beside the archive: a download nobody can verify is a - # download nobody should run. - sums() { - if command -v sha256sum > /dev/null; then sha256sum "$1"; else shasum -a 256 "$1"; fi - } - sums "$archive" > "${archive}.sha256" - - # A second copy without the version in the name, because - # `releases/latest/download/` resolves by *exact* file name -- - # so a URL that is stable across releases cannot contain the version. - # The versioned name is what cargo-binstall and the Homebrew formula - # look for; the unversioned one is what a person copies out of the - # README and keeps working next release. - alias="${archive/-${version}-/-}" - cp "$archive" "$alias" - sums "$alias" > "${alias}.sha256" - - echo "ARCHIVE=$archive" >> "$GITHUB_ENV" - echo "ALIAS=$alias" >> "$GITHUB_ENV" - - - name: Attach to the release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release upload "$GITHUB_REF_NAME" \ - "$ARCHIVE" "${ARCHIVE}.sha256" \ - "$ALIAS" "${ALIAS}.sha256" --clobber - # Tell the repository that tests this tool against a real subject that a # new version exists, so the deep run happens now rather than at its next # scheduled tick. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c94b80..19cb86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ listed under a **Changed** or **Removed** heading. ## [Unreleased] +### Fixed + +- **The Windows binary is built.** The 0.6.2 release shipped Linux and macOS + archives and no `.zip`: the build step had no `shell: bash`, and Windows + runners default to PowerShell, where `"$TARGET"` expands to the empty string + and `cargo build --target` is handed nothing. Every step is `bash` now. +- **Building the artifacts is re-runnable**, in `binaries.yml` rather than + inside `release.yml`. Publishing to crates.io happens once and cannot be + repeated; building an archive can fail for reasons that have nothing to do + with the release, and when it does the fix must not be "cut another + version". It fires on a published release, and by hand for any tag whose + artifacts need rebuilding. + ## [0.6.2] - 2026-08-23 ### Added From 7cc76c99134c8453433c04adfd250706c9bcbaf8 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:43:36 +0700 Subject: [PATCH 2/3] ci(binaries): a concurrency group, keyed on the tag zizmor audits at pedantic level and wants one. Keyed on the tag so two dispatches for the same release cannot race uploading the same asset names, and never cancelled in flight: a run interrupted between building and uploading leaves the release short an archive, which is the failure this workflow exists to make recoverable. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- .github/workflows/binaries.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 75bcaae..c7414e0 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -24,6 +24,14 @@ on: permissions: contents: read +# Keyed on the tag, so two dispatches for the same release cannot race each +# other uploading the same asset names. Never cancelled in flight: a run +# interrupted between building and uploading leaves the release short an +# archive, which is the failure this workflow exists to make recoverable. +concurrency: + group: binaries-${{ inputs.tag || github.event.release.tag_name }} + cancel-in-progress: false + env: TAG: ${{ inputs.tag || github.event.release.tag_name }} From ade1987a113c09240a2d3c88d20a6473a6d51a99 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:46:26 +0700 Subject: [PATCH 3/3] ci(binaries): rustup target add, not a toolchain action zizmor flags the toolchain action as superfluous, and it is right: the runners already ship stable Rust, so the only thing needed is the cross-target std. One fewer third-party action in the job that produces downloadable binaries is worth having on its own. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- .github/workflows/binaries.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index c7414e0..2335a3f 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -79,10 +79,15 @@ jobs: # The tag, not the branch: an artifact must be built from the commit # the release names, whatever main has moved on to since. ref: ${{ env.TAG }} - - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 - with: - toolchain: stable - targets: ${{ matrix.target }} + # `rustup target add` rather than a toolchain action: the runners already + # ship stable Rust, so the only thing actually needed here is the + # cross-target std -- and one fewer third-party action in a job that + # produces downloadable binaries is worth having on its own. + - name: Add the target's standard library + shell: bash + env: + TARGET: ${{ matrix.target }} + run: rustup target add "$TARGET" # --locked, because the point of a published binary is that it is the # one the lockfile describes and the tests ran against.