name: Release
on:
push:
tags:
- "v*"
env:
CARGO_TERM_COLOR: always
BIN_NAME: ghr
CRATE_NAME: ghr-cli
permissions:
contents: write
jobs:
verify:
name: Verify release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Verify tag matches Cargo.toml
id: version
run: |
crate_version="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == env.CRATE_NAME) | .version')"
tag_version="${GITHUB_REF_NAME#v}"
if [ "$crate_version" != "$tag_version" ]; then
echo "tag $GITHUB_REF_NAME does not match crate version $crate_version" >&2
exit 1
fi
echo "version=$crate_version" >> "$GITHUB_OUTPUT"
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check all targets
run: cargo check --locked --all-targets
- name: Clippy
run: cargo clippy --locked --all-targets -- -D warnings
- name: Test
run: cargo test --locked
- name: Package crate
run: cargo package --locked
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
needs:
- verify
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: tar
exe_suffix: ""
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
archive: tar
exe_suffix: ""
- os: macos-15-intel
target: x86_64-apple-darwin
archive: tar
exe_suffix: ""
- os: macos-15
target: aarch64-apple-darwin
archive: tar
exe_suffix: ""
- os: windows-latest
target: x86_64-pc-windows-msvc
archive: zip
exe_suffix: ".exe"
- os: windows-11-arm
target: aarch64-pc-windows-msvc
archive: zip
exe_suffix: ".exe"
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Build binary
run: cargo build --locked --release --target "${{ matrix.target }}"
- name: Package tar archive
if: matrix.archive == 'tar'
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
asset="${BIN_NAME}-${GITHUB_REF_NAME}-${TARGET}"
staging="dist/${asset}"
binary_path="target/${TARGET}/release/${BIN_NAME}"
mkdir -p "$staging"
cp "$binary_path" "$staging/"
cp README.md LICENSE "$staging/"
archive_path="dist/${asset}.tar.gz"
tar -C dist -czf "$archive_path" "$asset"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$archive_path" > "${archive_path}.sha256"
else
shasum -a 256 "$archive_path" > "${archive_path}.sha256"
fi
rm -rf "$staging"
- name: Package zip archive
if: matrix.archive == 'zip'
shell: pwsh
env:
TARGET: ${{ matrix.target }}
EXE_SUFFIX: ${{ matrix.exe_suffix }}
run: |
$asset = "${env:BIN_NAME}-${env:GITHUB_REF_NAME}-${env:TARGET}"
$staging = "dist/${asset}"
$binaryPath = "target/${env:TARGET}/release/${env:BIN_NAME}${env:EXE_SUFFIX}"
$archivePath = "dist/${asset}.zip"
New-Item -ItemType Directory -Force -Path $staging | Out-Null
Copy-Item $binaryPath $staging
Copy-Item README.md, LICENSE $staging
Compress-Archive -Force -Path "${staging}/*" -DestinationPath $archivePath
$hash = Get-FileHash -Algorithm SHA256 $archivePath
"$($hash.Hash.ToLowerInvariant()) $archivePath" | Set-Content -NoNewline "${archivePath}.sha256"
Remove-Item -Recurse -Force $staging
- name: Upload release artifact
uses: actions/upload-artifact@v7
with:
name: ghr-${{ matrix.target }}
path: dist/*
if-no-files-found: error
publish:
name: Publish crate and GitHub release
runs-on: ubuntu-latest
needs:
- verify
- build
environment: crates-io
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: Publish to crates.io
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
crate_url="https://crates.io/api/v1/crates/${CRATE_NAME}/${version}"
if curl -fsS -H "User-Agent: ${GITHUB_REPOSITORY} release workflow" "$crate_url" >/dev/null; then
echo "${CRATE_NAME} ${version} is already published; skipping cargo publish"
else
publish_log="$(mktemp)"
if cargo publish --locked --token "$CARGO_REGISTRY_TOKEN" 2>"$publish_log"; then
cat "$publish_log"
elif grep -Eiq "already (uploaded|exists)|version .* is already uploaded" "$publish_log"; then
cat "$publish_log"
echo "${CRATE_NAME} ${version} is already published; continuing"
else
cat "$publish_log" >&2
exit 1
fi
fi
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Download release artifacts
uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Publish GitHub release
run: |
notes_file="$(mktemp)"
release_notes=".github/release-notes/${GITHUB_REF_NAME}.md"
if [ -f "$release_notes" ]; then
cat "$release_notes" > "$notes_file"
printf '\n\n' >> "$notes_file"
fi
printf 'Published to crates.io: https://crates.io/crates/%s/%s\n' "$CRATE_NAME" "${GITHUB_REF_NAME#v}" >> "$notes_file"
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release edit "$GITHUB_REF_NAME" \
--title "ghr $GITHUB_REF_NAME" \
--notes-file "$notes_file"
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
else
gh release create "$GITHUB_REF_NAME" dist/* \
--verify-tag \
--title "ghr $GITHUB_REF_NAME" \
--notes-file "$notes_file"
fi
env:
GH_TOKEN: ${{ github.token }}