name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag_name:
description: 'Tag to release (e.g. v0.1.0)'
required: true
default: 'v0.1.0'
permissions:
contents: write
env:
BIN_NAME: rustdupe
PROJECT_NAME: rustdupe
jobs:
prepare:
name: Prepare Release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
clean_version: ${{ steps.get_version.outputs.clean_version }}
steps:
- uses: actions/checkout@v4
- name: Get version
id: get_version
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG_NAME="${{ github.event.inputs.tag_name }}"
else
TAG_NAME="${GITHUB_REF#refs/tags/}"
fi
echo "version=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "clean_version=${TAG_NAME#v}" >> $GITHUB_OUTPUT
- name: Create and Push Tag
if: github.event_name == 'workflow_dispatch'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag ${{ steps.get_version.outputs.version }} || echo "Tag already exists"
git push origin ${{ steps.get_version.outputs.version }} || echo "Tag already exists"
build:
name: Build - ${{ matrix.target }}
needs: prepare
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
suffix: ""
name: linux-x86_64
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
suffix: ""
name: linux-x86_64-musl
- target: x86_64-pc-windows-msvc
os: windows-latest
suffix: ".exe"
name: windows-x86_64
- target: x86_64-apple-darwin
os: macos-latest
suffix: ""
name: macos-x86_64
- target: aarch64-apple-darwin
os: macos-latest
suffix: ""
name: macos-arm64
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.85"
targets: ${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Package
shell: bash
run: |
VERSION=${{ needs.prepare.outputs.clean_version }}
ASSET_NAME="${PROJECT_NAME}-${VERSION}-${{ matrix.name }}"
BINARY_PATH="target/${{ matrix.target }}/release/${BIN_NAME}${{ matrix.suffix }}"
mkdir -p dist staging
# Copy binary to staging area
cp "$BINARY_PATH" "staging/${BIN_NAME}${{ matrix.suffix }}"
# Also copy raw binary to dist (some users prefer this)
cp "$BINARY_PATH" "dist/${ASSET_NAME}${{ matrix.suffix }}"
# Create archive
cd staging
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
# Windows: create .zip
7z a -tzip "../dist/${ASSET_NAME}.zip" "${BIN_NAME}${{ matrix.suffix }}"
else
# Unix: create .tar.gz
tar -czvf "../dist/${ASSET_NAME}.tar.gz" "${BIN_NAME}${{ matrix.suffix }}"
fi
cd ../dist
# Generate checksums for all files
for file in *; do
if [[ ! "$file" == *.sha256 ]]; then
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
sha256sum "$file" > "${file}.sha256"
else
shasum -a 256 "$file" > "${file}.sha256"
fi
fi
done
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.target }}
path: dist/*
release:
name: Create Release
needs: [build, prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: binaries-*
merge-multiple: true
- name: Generate Release Body
id: release_body
shell: bash
run: |
VERSION="${{ needs.prepare.outputs.clean_version }}"
# Create a nice header
echo "## Release ${{ needs.prepare.outputs.version }}" > release_notes.md
echo "" >> release_notes.md
# Add Installation section
echo "### Installation" >> release_notes.md
echo "" >> release_notes.md
echo "#### Linux" >> release_notes.md
echo '```bash' >> release_notes.md
echo "# Download (choose glibc or musl version)" >> release_notes.md
echo "curl -LO https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.version }}/rustdupe-${VERSION}-linux-x86_64.tar.gz" >> release_notes.md
echo "" >> release_notes.md
echo "# Extract and make executable" >> release_notes.md
echo "tar -xzf rustdupe-${VERSION}-linux-x86_64.tar.gz" >> release_notes.md
echo "chmod +x rustdupe" >> release_notes.md
echo "" >> release_notes.md
echo "# Run" >> release_notes.md
echo "./rustdupe --help" >> release_notes.md
echo '```' >> release_notes.md
echo "" >> release_notes.md
echo "#### macOS" >> release_notes.md
echo '```bash' >> release_notes.md
echo "# Download (use macos-arm64 for Apple Silicon, macos-x86_64 for Intel)" >> release_notes.md
echo "curl -LO https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.version }}/rustdupe-${VERSION}-macos-arm64.tar.gz" >> release_notes.md
echo "" >> release_notes.md
echo "# Extract and make executable" >> release_notes.md
echo "tar -xzf rustdupe-${VERSION}-macos-arm64.tar.gz" >> release_notes.md
echo "chmod +x rustdupe" >> release_notes.md
echo "" >> release_notes.md
echo "# Remove quarantine attribute (required for unsigned binaries)" >> release_notes.md
echo "xattr -d com.apple.quarantine rustdupe" >> release_notes.md
echo "" >> release_notes.md
echo "# Run" >> release_notes.md
echo "./rustdupe --help" >> release_notes.md
echo '```' >> release_notes.md
echo "" >> release_notes.md
echo "#### Windows" >> release_notes.md
echo '```powershell' >> release_notes.md
echo "# Download and extract" >> release_notes.md
echo "Invoke-WebRequest -Uri https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare.outputs.version }}/rustdupe-${VERSION}-windows-x86_64.zip -OutFile rustdupe.zip" >> release_notes.md
echo "Expand-Archive rustdupe.zip -DestinationPath ." >> release_notes.md
echo "" >> release_notes.md
echo "# Run" >> release_notes.md
echo ".\\rustdupe.exe --help" >> release_notes.md
echo '```' >> release_notes.md
echo "" >> release_notes.md
# Add "What's Changed" section
echo "### What's Changed" >> release_notes.md
# Try to get changes since last tag, or all if no tags
PREV_TAG=$(git describe --tags --abbrev=0 "${{ needs.prepare.outputs.version }}^" 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
git log --pretty=format:"* %s by @${{ github.actor }} in %h" >> release_notes.md
else
git log "${PREV_TAG}..${{ needs.prepare.outputs.version }}" --pretty=format:"* %s by @${{ github.actor }} in %h" >> release_notes.md
fi
echo "" >> release_notes.md
echo "" >> release_notes.md
# Add Asset table
echo "### Assets" >> release_notes.md
echo "| Filename | Type |" >> release_notes.md
echo "| -------- | ---- |" >> release_notes.md
for file in artifacts/*; do
filename=$(basename "$file")
if [[ "$filename" == *.sha256 ]]; then
echo "| $filename | Checksum |" >> release_notes.md
elif [[ "$filename" == *.exe ]]; then
echo "| **$filename** | Windows Binary |" >> release_notes.md
elif [[ "$filename" == *.zip ]]; then
echo "| **$filename** | Windows Archive |" >> release_notes.md
elif [[ "$filename" == *.tar.gz ]]; then
echo "| **$filename** | Unix Archive |" >> release_notes.md
else
echo "| **$filename** | Binary |" >> release_notes.md
fi
done
# Append Full Changelog link
echo "" >> release_notes.md
echo "**Full Changelog**: ${{ github.server_url }}/${{ github.repository }}/commits/${{ needs.prepare.outputs.version }}" >> release_notes.md
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.version }}
body_path: release_notes.md
files: artifacts/*
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}