name: Release
on:
push:
tags: ['v*']
permissions: read-all
env:
CARGO_TERM_COLOR: always
jobs:
preflight:
name: Verify tag matches Cargo.toml
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Compare tag with Cargo.toml version
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
tag_version="${TAG#v}"
echo "tag (no v): $tag_version"
echo "Cargo.toml: $cargo_version"
if [ "$cargo_version" != "$tag_version" ]; then
echo "::error::tag/Cargo.toml mismatch -- bump Cargo.toml to match the tag (RELEASING.md step 1)"
exit 1
fi
echo "version alignment: OK"
manpages:
name: Generate manpages
needs: preflight
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 with:
key: manpages
- name: Generate manpages
run: cargo run -p xtask --no-default-features --release --locked -- man --out man/man1
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: bzr-manpages
path: man/man1/*.1
if-no-files-found: error
build:
name: Build ${{ matrix.target }}
needs: manpages
runs-on: ${{ matrix.os }}
permissions:
contents: read
id-token: write attestations: write strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
use_cross: false
archive: tar.gz
pkg_deb: true
pkg_rpm: true
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
use_cross: true
archive: tar.gz
pkg_deb: true
pkg_rpm: true
- target: powerpc64le-unknown-linux-gnu
os: ubuntu-latest
use_cross: false
use_qemu: true
archive: tar.gz
pkg_deb: true
pkg_rpm: true
- target: s390x-unknown-linux-gnu
os: ubuntu-latest
use_cross: true
archive: tar.gz
pkg_deb: false
pkg_rpm: true
- target: aarch64-apple-darwin
os: macos-14
use_cross: false
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
use_cross: false
archive: zip
- target: aarch64-pc-windows-msvc
os: windows-latest
use_cross: false
archive: zip
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Install libdbus-1-dev (native Linux)
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 with:
key: ${{ matrix.target }}
- name: Set up QEMU
if: matrix.use_qemu
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
- name: Install cross
if: matrix.use_cross
run: cargo install cross --locked
- name: Static CRT link (Windows)
if: contains(matrix.target, 'windows-msvc')
shell: bash
run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV"
- name: Build
env:
PKG_CONFIG_ALLOW_CROSS: "1"
run: |
if [ "${{ matrix.use_qemu }}" = "true" ]; then
# Use the latest stable Rust image so the container's rustc tracks
# `dtolnay/rust-toolchain@stable` used by the rest of the matrix.
# A pinned older image (e.g. rust:1.85-bookworm) silently breaks
# whenever Cargo.toml's `rust-version` advances.
docker run --rm --platform linux/ppc64le \
-v "${{ github.workspace }}:/workspace" -w /workspace \
rust:bookworm \
bash -c "apt-get update && apt-get install -y libdbus-1-dev pkg-config && cargo build --release --locked --target ${{ matrix.target }}"
# The container ran as root and left target/ owned by root.
# Subsequent host-side steps (cargo deb / cargo generate-rpm /
# tarball staging / lintian) run as the runner user and would
# otherwise hit "Permission denied" on target/.
sudo chown -R "$(id -u):$(id -g)" target
elif [ "${{ matrix.use_cross }}" = "true" ]; then
cross build --release --locked --target ${{ matrix.target }}
else
cargo build --release --locked --target ${{ matrix.target }}
fi
shell: bash
- name: Download manpages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: bzr-manpages
path: man/man1
- name: Package (unix)
if: matrix.archive == 'tar.gz'
run: |
STAGING="bzr-${{ github.ref_name }}-${{ matrix.target }}"
mkdir "$STAGING"
cp "target/${{ matrix.target }}/release/bzr" "$STAGING/"
cp LICENSE README.md "$STAGING/"
mkdir -p "$STAGING/man/man1"
cp man/man1/*.1 "$STAGING/man/man1/"
tar czf "$STAGING.tar.gz" "$STAGING"
shell: bash
- name: Package (windows)
if: matrix.archive == 'zip'
run: |
$STAGING = "bzr-${{ github.ref_name }}-${{ matrix.target }}"
New-Item -ItemType Directory -Path $STAGING
Copy-Item "target\${{ matrix.target }}\release\bzr.exe" "$STAGING\"
Copy-Item LICENSE, README.md "$STAGING\"
New-Item -ItemType Directory -Path "$STAGING\man\man1"
Copy-Item "man\man1\*.1" "$STAGING\man\man1\"
Compress-Archive -Path $STAGING -DestinationPath "$STAGING.zip"
shell: pwsh
- name: Install cargo-deb
if: matrix.pkg_deb
uses: taiki-e/install-action@49ba71bf46962339e6e5c0a7a4ec3ed4c8af28ac
with:
tool: cargo-deb
- name: Install cargo-generate-rpm
if: matrix.pkg_rpm
uses: taiki-e/install-action@49ba71bf46962339e6e5c0a7a4ec3ed4c8af28ac
with:
tool: cargo-generate-rpm
- name: Build .deb
if: matrix.pkg_deb
run: cargo deb --no-build --no-strip --target ${{ matrix.target }}
shell: bash
- name: Build .rpm
if: matrix.pkg_rpm
shell: bash
run: |
# RPM's Version field disallows '-' (it separates the package's
# Version from its Release in NVR notation), so 0.2.0-rc6 is a
# hard error from cargo-generate-rpm. Translate the prerelease
# suffix to '~', which RPM accepts and which sorts correctly:
# 0.2.0~rc6 < 0.2.0 < 0.2.0~rc7. cargo-deb (run earlier in this
# job) accepts the hyphen form and is unaffected; this rewrite
# only persists for the rest of the cross-compile job in CI.
sed -i 's/^version = "\([^-"]*\)-\(.*\)"/version = "\1~\2"/' Cargo.toml
cargo generate-rpm --target ${{ matrix.target }}
- name: Stage packages alongside tarball
if: matrix.pkg_deb || matrix.pkg_rpm
run: |
set -euo pipefail
if [ "${{ matrix.pkg_deb }}" = "true" ]; then
cp target/${{ matrix.target }}/debian/*.deb .
fi
if [ "${{ matrix.pkg_rpm }}" = "true" ]; then
cp target/${{ matrix.target }}/generate-rpm/*.rpm .
fi
shell: bash
- name: Lint .deb (warn-only)
if: matrix.pkg_deb
continue-on-error: true
run: |
sudo apt-get update && sudo apt-get install -y lintian
lintian --no-tag-display-limit *.deb || true
shell: bash
- name: Lint .rpm (warn-only)
if: matrix.pkg_rpm
continue-on-error: true
run: |
sudo apt-get update && sudo apt-get install -y rpmlint
rpmlint *.rpm || true
shell: bash
- name: Install-test .deb (x86_64 only)
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.pkg_deb
run: |
docker run --rm -v "$PWD:/pkg" -w /pkg debian:stable bash -c '
apt-get update && apt-get install -y ./*.deb && bzr --version'
shell: bash
- name: Install-test .rpm (x86_64 only)
if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.pkg_rpm
run: |
docker run --rm -v "$PWD:/pkg" -w /pkg fedora:latest bash -c '
dnf install -y ./*.rpm && bzr --version'
shell: bash
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 with:
subject-path: |
bzr-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }}
*.deb
*.rpm
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: bzr-${{ matrix.target }}
path: |
bzr-${{ github.ref_name }}-${{ matrix.target }}.*
*.deb
*.rpm
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
path: artifacts
pattern: bzr-*-*
merge-multiple: true
- name: Stage installer scripts (with version baked in)
working-directory: artifacts
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
python3 - <<'PYEOF'
import os, pathlib
tag = os.environ["TAG"]
sh_marker = 'BZR_VERSION="${BZR_VERSION:-}"'
sh_replace = 'BZR_VERSION="${BZR_VERSION:-' + tag + '}"'
sh_in = pathlib.Path("../install.sh").read_text()
sh_out = sh_in.replace(sh_marker, sh_replace, 1)
assert sh_out != sh_in, f"install.sh marker line not found: {sh_marker!r}"
pathlib.Path("install.sh").write_text(sh_out)
ps_marker = "$BzrVersion = $env:BZR_VERSION"
ps_replace = (
"$BzrVersion = if ($env:BZR_VERSION) "
"{ $env:BZR_VERSION } else { '" + tag + "' }"
)
ps_in = pathlib.Path("../install.ps1").read_text()
ps_out = ps_in.replace(ps_marker, ps_replace, 1)
assert ps_out != ps_in, f"install.ps1 marker line not found: {ps_marker!r}"
pathlib.Path("install.ps1").write_text(ps_out)
PYEOF
chmod +x install.sh
- name: Generate SHA256SUMS
working-directory: artifacts
run: |
set -euo pipefail
# Hash every release artifact (tarballs, zips, .deb, .rpm).
# Sorted, LF-only output so reproducible across re-runs of the
# same set of inputs.
find . -maxdepth 1 -type f ! -name 'SHA256SUMS' -printf '%f\n' \
| sort \
| xargs -d '\n' sha256sum \
> SHA256SUMS
echo "Generated SHA256SUMS:"
cat SHA256SUMS
- name: Extract release notes from CHANGELOG.md
id: notes
shell: bash
run: |
set -euo pipefail
TAG="${{ github.ref_name }}"
VERSION="${TAG#v}"
NOTES_FILE="$(mktemp)"
awk -v v="$VERSION" '
$0 ~ "^## \\[" v "\\]" { capture = 1; next }
capture && /^## \[/ { exit }
capture { print }
' CHANGELOG.md > "$NOTES_FILE"
if ! [ -s "$NOTES_FILE" ]; then
echo "::error::No CHANGELOG.md entry found for [$VERSION]"
exit 1
fi
echo "notes_file=$NOTES_FILE" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
PRERELEASE=""
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
PRERELEASE="--prerelease"
fi
gh release create "$GITHUB_REF_NAME" \
--title "bzr $GITHUB_REF_NAME" \
--notes-file "${{ steps.notes.outputs.notes_file }}" \
$PRERELEASE \
artifacts/*
installer-smoke:
name: Installer smoke ${{ matrix.os }}
needs: release
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Smoke install.sh
if: matrix.os == 'ubuntu-latest'
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
export BZR_INSTALL_DIR="$HOME/.local/bin"
curl -fsSL "https://github.com/randomparity/bzr/releases/download/${TAG}/install.sh" | sh
installed_version="$("$BZR_INSTALL_DIR/bzr" --version 2>&1)"
expected="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
echo "installed: $installed_version"
echo "expected: $expected"
case "$installed_version" in
*"$expected"*) echo "binary version: OK" ;;
*) echo "binary mismatch: expected=$expected output=$installed_version" >&2; exit 1 ;;
esac
shell: bash
- name: Smoke install.ps1
if: matrix.os == 'windows-latest'
env:
TAG: ${{ github.ref_name }}
shell: pwsh
run: |
$env:BZR_INSTALL_DIR = Join-Path $env:LOCALAPPDATA 'Programs\bzr'
irm "https://github.com/randomparity/bzr/releases/download/$env:TAG/install.ps1" | iex
$installed = & (Join-Path $env:BZR_INSTALL_DIR 'bzr.exe') --version
$expected = (Select-String -Path Cargo.toml -Pattern '^version = "(.*)"' |
Select-Object -First 1).Matches[0].Groups[1].Value
Write-Host "installed: $installed"
Write-Host "expected: $expected"
if ($installed -notlike "*$expected*") {
Write-Error "binary mismatch: expected=$expected output=$installed"
exit 1
}
Write-Host "binary version: OK"
homebrew:
name: Bump randomparity/homebrew-tap
needs: release
if: ${{ !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout bzr (for template)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Checkout tap
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
repository: randomparity/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Render formula
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
BASE="https://github.com/randomparity/bzr/releases/download/${TAG}"
fetch_sha() {
curl --fail --silent --show-error --location "$1" | sha256sum | awk '{print $1}'
}
MAC_ARM_SHA=$(fetch_sha "${BASE}/bzr-${TAG}-aarch64-apple-darwin.tar.gz")
LINUX_ARM_SHA=$(fetch_sha "${BASE}/bzr-${TAG}-aarch64-unknown-linux-gnu.tar.gz")
LINUX_INTEL_SHA=$(fetch_sha "${BASE}/bzr-${TAG}-x86_64-unknown-linux-gnu.tar.gz")
SRC_SHA=$(fetch_sha "https://github.com/randomparity/bzr/archive/refs/tags/${TAG}.tar.gz")
mkdir -p tap/Formula
sed \
-e "s|{{VERSION}}|${VERSION}|g" \
-e "s|{{MAC_ARM_SHA}}|${MAC_ARM_SHA}|g" \
-e "s|{{LINUX_ARM_SHA}}|${LINUX_ARM_SHA}|g" \
-e "s|{{LINUX_INTEL_SHA}}|${LINUX_INTEL_SHA}|g" \
-e "s|{{SRC_SHA}}|${SRC_SHA}|g" \
homebrew/bzr.rb.template > tap/Formula/bzr.rb
- name: Commit and push
working-directory: tap
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/bzr.rb
if git diff --staged --quiet; then
echo "Formula already up-to-date for ${TAG}; nothing to commit."
exit 0
fi
git commit -m "bzr ${TAG#v}"
git push