Skip to content

Merge pull request #24 from kexi/fix/nix-cargo-hash #7

Merge pull request #24 from kexi/fix/nix-cargo-hash

Merge pull request #24 from kexi/fix/nix-cargo-hash #7

Workflow file for this run

# Release automation: build prebuilt pitty binaries and publish them to a
# GitHub Release, then move the floating major/minor tags (`v1`, `v1.2`, ...)
# and publish matching asset sets so action users can pin either stability level
# while still getting the prebuilt fast path. After the one-time Marketplace
# publication has been done in the GitHub UI, these published GitHub Releases are
# the Marketplace update path; GitHub does not expose a separate Marketplace
# publish API for workflows.
#
# Trigger: this workflow wakes up on any `v*` tag push but only publishes for a
# full patch tag (`vMAJOR.MINOR.PATCH`, e.g. `v1.2.0`). Floating tag moves (`v1`
# and `v1.2`) re-trigger the workflow, and the parse job deliberately turns all
# release/upload jobs into no-ops for those refs. A release job has
# `contents: write` and the GITHUB_TOKEN in scope; restricting it to tag pushes
# from this repo keeps that token away from untrusted fork code (a fork can open
# a PR but cannot push a tag here), which is why there is deliberately no
# `pull_request`/`pull_request_target` trigger.
#
# Asset-name contract (the load-bearing detail): action.yml downloads
# pitty-${ref}-${RUNNER_OS}-${RUNNER_ARCH}.tar.gz
# so the archive names produced here MUST use GitHub's runner OS/arch labels,
# not `uname` values or Rust target triples. In particular Apple Silicon is
# `macOS-ARM64` (RUNNER_OS/RUNNER_ARCH), even though we build the
# `aarch64-apple-darwin` triple; Windows x64 is `Windows-X64`, even though the
# executable inside the archive is `pitty.exe`. A mismatch makes the action's
# fast path 404 and silently fall back to a slow `cargo install`, so the
# `release_asset_name_contract.rs` test pins both sides to one source of truth.
#
# Action pinning: every action is pinned to an immutable commit SHA (with a
# version comment), and ci.yml gates those pins/comments with pinact. This
# workflow has a write-scoped GITHUB_TOKEN in scope, so a compromised mutable tag
# here could publish tampered release assets. Bump the SHAs deliberately.
name: Release
on:
push:
tags:
- "v*"
# Least privilege: the only write this workflow needs is creating the GitHub
# Release, moving version tags, and uploading assets (`contents: write`). It does
# not need packages, id-token, or any other scope.
permissions:
contents: write
jobs:
# Normalize the pushed ref once, then make every publishing job depend on this
# single decision. Why not keep a simple expression guard: GitHub Actions has
# no regex operator, and `contains(github.ref_name, '.')` would also match the
# floating minor tag (`v1.2`) that this workflow force-pushes.
release-version:
name: parse release tag
runs-on: ubuntu-latest
outputs:
should_publish: ${{ steps.parse.outputs.should_publish }}
major: ${{ steps.parse.outputs.major }}
minor: ${{ steps.parse.outputs.minor }}
steps:
- name: Parse release ref
id: parse
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
if [[ "$tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
{
echo "should_publish=true"
echo "major=v${BASH_REMATCH[1]}"
echo "minor=v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
} >> "$GITHUB_OUTPUT"
echo "Release tag ${tag}; floating refs are v${BASH_REMATCH[1]} and v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}."
else
{
echo "should_publish=false"
echo "major="
echo "minor="
} >> "$GITHUB_OUTPUT"
echo "Skipping ${tag}; release automation only publishes vMAJOR.MINOR.PATCH tags."
fi
# Create the GitHub Release for the pushed patch tag FIRST.
# upload-rust-binary-action only attaches assets to an existing release — if
# none exists it retries and then fails with "release not found". The four
# parallel upload jobs would all race that failure, so one job creates the
# release up front and they depend on it. If the release already exists, edit
# it back to a published/latest release so reruns preserve the Marketplace-
# visible patch version instead of letting floating releases become Latest.
create-release:
name: create release
needs: [release-version]
if: ${{ needs.release-version.outputs.should_publish == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Create or refresh the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
if gh release view "$tag" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "Release $tag already exists; marking it published and Latest."
gh release edit "$tag" --repo "${GITHUB_REPOSITORY}" \
--draft=false --prerelease=false --latest
else
echo "Creating release $tag"
gh release create "$tag" --repo "${GITHUB_REPOSITORY}" \
--title "$tag" --generate-notes --latest --verify-tag
fi
# Build one prebuilt binary per OS x arch and upload it under the pushed patch
# tag (e.g. v1.2.0). Separate jobs below rebuild the same targets for the
# floating major/minor releases because upload-rust-binary-action's `ref`
# input selects which release/tag the asset attaches to and feeds the `$tag`
# template variable.
upload-assets:
name: ${{ matrix.os_name }}-${{ matrix.arch_name }} (${{ matrix.target }})
needs: [release-version, create-release]
if: ${{ needs.release-version.outputs.should_publish == 'true' }}
strategy:
# Build every target even if one fails, so a single broken cross build
# does not hide the status of the others.
fail-fast: false
matrix:
include:
# Linux x64: RUNNER_OS = Linux, RUNNER_ARCH = X64. Native build.
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: X64
# Linux ARM64: RUNNER_ARCH = ARM64 on a Linux ARM host. Cross-built
# from an x64 runner via upload-rust-binary-action's `build-tool:
# cross` (which invokes the `cross` tool under the hood; no separate
# cross action is used).
- target: aarch64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: ARM64
# Apple Silicon: RUNNER_OS = macOS, RUNNER_ARCH = ARM64, even though
# the Rust triple is aarch64-apple-darwin. The archive must be named
# with runner labels to match action.yml.
#
# No macOS Intel (x86_64-apple-darwin) target: GitHub's macos-13 Intel
# runners are unreliably scheduled (queued indefinitely), which blocked
# the whole release. Intel Macs fall back to `cargo install` in the
# composite action instead. Re-add a `- target: x86_64-apple-darwin`
# entry (and the RUNNER_PAIRS row in the contract test) if a dependable
# Intel runner returns.
- target: aarch64-apple-darwin
runner: macos-14
os_name: macOS
arch_name: ARM64
# Windows x64: the archive is still `.tar.gz` for one action-side
# extraction path; the binary inside is `pitty.exe`.
- target: x86_64-pc-windows-msvc
runner: windows-latest
os_name: Windows
arch_name: X64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Publish the asset under the pushed tag (e.g. v1.2.0). `archive` is a
# literal name embedding the GitHub runner OS/arch labels (not $target) so
# it matches action.yml's download URL exactly; `leading-dir: false` places
# the binary at the tarball root so the action's `tar -xzf -C
# $HOME/.local/bin` yields `$HOME/.local/bin/pitty` or `pitty.exe`.
# Why not rely on the action default: a future upload-rust-binary-action
# release could flip the default to a leading `pitty-.../` directory,
# which would make action.yml's chmod target hit "No such file"; pinning
# it here fixes the tarball layout regardless.
- name: Upload assets for the release tag
uses: taiki-e/upload-rust-binary-action@f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6 # v1.30.2
with:
bin: pitty
target: ${{ matrix.target }}
# Cross-build Linux aarch64 via `cross`; native build for the rest.
build-tool: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'cross' || 'cargo' }}
archive: pitty-${{ github.ref_name }}-${{ matrix.os_name }}-${{ matrix.arch_name }}
leading-dir: false
tar: all
zip: none
checksum: sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Move both floating semver tags to this release commit and create matching
# non-Latest GitHub Releases so their prebuilt assets have release targets.
# `v1` supports the default `uses: kexi/pitty@v1`; `v1.2` lets users pin to a
# minor line per GitHub's action-versioning recommendation.
move-floating-tags:
name: move floating tags and publish floating releases
needs: [release-version, upload-assets]
if: ${{ needs.release-version.outputs.should_publish == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Full history so the tag moves point at real commit objects.
fetch-depth: 0
# Force-move the floating major/minor tags to the release commit so
# `uses: kexi/pitty@v1`, `uses: kexi/pitty@v1.2`, and the cargo fallback
# all resolve to the same released source. The push uses the workflow
# GITHUB_TOKEN (contents: write), so no PAT is needed.
- name: Move floating tags to the release commit
env:
MAJOR_REF: ${{ needs.release-version.outputs.major }}
MINOR_REF: ${{ needs.release-version.outputs.minor }}
run: |
set -euo pipefail
for ref in "$MAJOR_REF" "$MINOR_REF"; do
echo "Moving floating tag ${ref} to ${GITHUB_SHA}"
git tag -f "${ref}" "${GITHUB_SHA}"
done
git push -f origin "refs/tags/${MAJOR_REF}" "refs/tags/${MINOR_REF}"
# Create (or refresh) the floating releases so the asset upload jobs have
# releases to attach to. If a floating release exists from a prior patch,
# delete it first so its assets/notes are regenerated for the new commit;
# the moved tags are preserved (we only delete releases, not tags).
- name: Create floating releases
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAJOR_REF: ${{ needs.release-version.outputs.major }}
MINOR_REF: ${{ needs.release-version.outputs.minor }}
run: |
set -euo pipefail
for ref in "$MAJOR_REF" "$MINOR_REF"; do
if gh release view "$ref" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
# `gh release delete` keeps the tag by default (only --cleanup-tag
# removes it), so the freshly force-moved tag is preserved.
echo "Deleting stale $ref release (keeping the tag) before recreating"
gh release delete "$ref" --repo "${GITHUB_REPOSITORY}" --yes
fi
echo "Creating $ref release at ${GITHUB_SHA}"
gh release create "$ref" --repo "${GITHUB_REPOSITORY}" \
--title "$ref" --latest=false \
--notes "Floating release. Tracks the latest ${ref}.x compatible patch; assets mirror the latest tagged release." \
--verify-tag
done
# Build the same targets again, but attach the archives to the floating major
# release under major-named files. A separate matrix job (rather than reusing
# upload-assets) because `ref` must point at the floating tag, which only
# exists after move-floating-tags has run.
upload-major-assets:
name: floating major ${{ matrix.os_name }}-${{ matrix.arch_name }} (${{ matrix.target }})
needs: [release-version, move-floating-tags]
if: ${{ needs.release-version.outputs.should_publish == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: X64
- target: aarch64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: ARM64
# No macOS Intel target — see the upload-assets matrix for why.
- target: aarch64-apple-darwin
runner: macos-14
os_name: macOS
arch_name: ARM64
- target: x86_64-pc-windows-msvc
runner: windows-latest
os_name: Windows
arch_name: X64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# `ref` attaches these assets to the floating major release and the
# literal `archive` name embeds that same major ref plus runner OS/arch
# labels, matching the action's default `version: v1` download URL.
- name: Upload assets for the floating major tag
uses: taiki-e/upload-rust-binary-action@f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6 # v1.30.2
with:
bin: pitty
target: ${{ matrix.target }}
build-tool: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'cross' || 'cargo' }}
ref: refs/tags/${{ needs.release-version.outputs.major }}
archive: pitty-${{ needs.release-version.outputs.major }}-${{ matrix.os_name }}-${{ matrix.arch_name }}
leading-dir: false
tar: all
zip: none
checksum: sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Build the same targets for the floating minor release (`v1.2`, `v2.0`, ...).
# This gives Marketplace/Actions users the standard "stay within this minor
# line" pin without falling back to a source build.
upload-minor-assets:
name: floating minor ${{ matrix.os_name }}-${{ matrix.arch_name }} (${{ matrix.target }})
needs: [release-version, move-floating-tags]
if: ${{ needs.release-version.outputs.should_publish == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: X64
- target: aarch64-unknown-linux-gnu
runner: ubuntu-latest
os_name: Linux
arch_name: ARM64
# No macOS Intel target — see the upload-assets matrix for why.
- target: aarch64-apple-darwin
runner: macos-14
os_name: macOS
arch_name: ARM64
- target: x86_64-pc-windows-msvc
runner: windows-latest
os_name: Windows
arch_name: X64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# `ref` attaches these assets to the floating minor release and the
# literal `archive` name embeds that same minor ref plus runner OS/arch
# labels, so `with: version: v1.2` gets the prebuilt fast path too.
- name: Upload assets for the floating minor tag
uses: taiki-e/upload-rust-binary-action@f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6 # v1.30.2
with:
bin: pitty
target: ${{ matrix.target }}
build-tool: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && 'cross' || 'cargo' }}
ref: refs/tags/${{ needs.release-version.outputs.minor }}
archive: pitty-${{ needs.release-version.outputs.minor }}-${{ matrix.os_name }}-${{ matrix.arch_name }}
leading-dir: false
tar: all
zip: none
checksum: sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}