Skip to content

Release

Release #7

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
bump:
description: Version bump to release
type: choice
required: true
options:
- patch
- minor
- major
- current
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
jobs:
prepare:
name: Prepare release
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
outputs:
crate_name: ${{ steps.version.outputs.crate_name }}
next_version: ${{ steps.version.outputs.next_version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: true
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build
run: cargo build --all-targets --all-features
- name: Test
run: cargo test --all-features
- name: Require 90% line coverage in every source file
run: .github/scripts/check-file-coverage.sh 90 target/coverage.json
- name: Build documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --no-deps --all-features
- name: Compute next version
id: version
shell: bash
run: |
set -euo pipefail
metadata="$(cargo metadata --format-version 1 --no-deps)"
crate_name="$(jq -r '.packages[0].name' <<< "$metadata")"
current_version="$(jq -r '.packages[0].version' <<< "$metadata")"
if [[ -z "$crate_name" || "$crate_name" == "null" ]]; then
echo "Could not resolve the crate name" >&2
exit 1
fi
if [[ -z "$current_version" || "$current_version" == "null" ]]; then
echo "Could not resolve the current crate version" >&2
exit 1
fi
IFS=. read -r major minor patch <<< "$current_version"
case "${{ inputs.bump }}" in
current)
;;
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
echo "Unsupported bump: ${{ inputs.bump }}" >&2
exit 1
;;
esac
next_version="${major}.${minor}.${patch}"
tag="v${next_version}"
git fetch --tags origin
if git rev-parse --verify --quiet "refs/tags/${tag}"; then
if [[ "${{ inputs.bump }}" != "current" ]]; then
echo "Tag ${tag} already exists" >&2
exit 1
fi
tagged_version="$(
git show "${tag}:Cargo.toml" \
| sed -n 's/^version = "\([^"]*\)"/\1/p' \
| head -n 1
)"
if [[ "$tagged_version" != "$current_version" ]]; then
echo "Tag ${tag} does not contain version ${current_version}" >&2
exit 1
fi
elif [[ "${{ inputs.bump }}" == "current" ]]; then
echo "Tag ${tag} does not exist; choose a semantic version bump" >&2
exit 1
fi
{
echo "crate_name=${crate_name}"
echo "current_version=${current_version}"
echo "next_version=${next_version}"
echo "tag=${tag}"
} >> "$GITHUB_OUTPUT"
- name: Update crate version
if: ${{ inputs.bump != 'current' }}
env:
CRATE_NAME: ${{ steps.version.outputs.crate_name }}
NEXT_VERSION: ${{ steps.version.outputs.next_version }}
run: |
set -euo pipefail
perl -0pi -e 's/(\[package\][\s\S]*?\nversion = ")[^"]+(")/$1$ENV{NEXT_VERSION}$2/' Cargo.toml
cargo update -p "$CRATE_NAME" --precise "$NEXT_VERSION"
- name: Commit version bump and tag
if: ${{ inputs.bump != 'current' }}
env:
RELEASE_TAG: ${{ steps.version.outputs.tag }}
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 Cargo.toml Cargo.lock
git commit -m "Release ${RELEASE_TAG}"
git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG}"
git push origin "HEAD:${GITHUB_REF_NAME}"
git push origin "${RELEASE_TAG}"
native-bundles:
name: Rust module bundle (${{ matrix.id }})
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- id: ubuntu-22.04-x86_64
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- id: ubuntu-22.04-arm64
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
- id: ubuntu-24.04-x86_64
os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
- id: ubuntu-24.04-arm64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- id: macos-15-x86_64
os: macos-15-intel
target: x86_64-apple-darwin
- id: macos-15-arm64
os: macos-15
target: aarch64-apple-darwin
- id: macos-26-x86_64
os: macos-26-intel
target: x86_64-apple-darwin
- id: macos-26-arm64
os: macos-26
target: aarch64-apple-darwin
- id: windows-2022-x86_64
os: windows-2022
target: x86_64-pc-windows-msvc
- id: windows-2025-x86_64
os: windows-2025
target: x86_64-pc-windows-msvc
- id: windows-11-arm64
os: windows-11-arm
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
persist-credentials: false
submodules: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Verify native Rust target
shell: bash
env:
EXPECTED_TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
actual_target="$(rustc -vV | sed -n 's/^host: //p')"
if [[ "$actual_target" != "$EXPECTED_TARGET" ]]; then
echo "expected ${EXPECTED_TARGET}, got ${actual_target}" >&2
exit 1
fi
- name: Build installable module
run: cargo build --locked --release --lib
- name: Verify Unix module through TinyBus loader
if: ${{ runner.os != 'Windows' }}
shell: bash
env:
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
run: |
set -euo pipefail
library_name="${CRATE_NAME//-/_}"
case "$RUNNER_OS" in
Linux) module="target/release/lib${library_name}.so" ;;
macOS) module="target/release/lib${library_name}.dylib" ;;
*) echo "unsupported Unix runner: ${RUNNER_OS}" >&2; exit 1 ;;
esac
cargo run --locked --example verify_module -- "$module"
- name: Verify Windows module through TinyBus loader
if: ${{ runner.os == 'Windows' }}
shell: pwsh
env:
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
run: |
$ErrorActionPreference = 'Stop'
$libraryName = $env:CRATE_NAME.Replace('-', '_')
$module = "target/release/$libraryName.dll"
$verifyRoot = Join-Path $env:RUNNER_TEMP 'tinyhosts-module-verify'
New-Item -ItemType Directory -Force $verifyRoot | Out-Null
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$security = [System.Security.AccessControl.DirectorySecurity]::new()
$security.SetOwner($identity.User)
$security.SetAccessRuleProtection($true, $false)
$rights = [System.Security.AccessControl.FileSystemRights]::FullControl
$inheritance = [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
$propagation = [System.Security.AccessControl.PropagationFlags]::None
$access = [System.Security.AccessControl.AccessControlType]::Allow
foreach ($sidValue in @(
$identity.User.Value,
'S-1-5-18',
'S-1-5-32-544'
)) {
$sid = [System.Security.Principal.SecurityIdentifier]::new($sidValue)
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$sid,
$rights,
$inheritance,
$propagation,
$access
)
[void]$security.AddAccessRule($rule)
}
Set-Acl -LiteralPath $verifyRoot -AclObject $security
$verifiedModule = Join-Path $verifyRoot "$libraryName.dll"
Copy-Item -LiteralPath $module -Destination $verifiedModule
cargo run --locked --example verify_module -- $verifiedModule
- name: Assemble Unix module package
if: ${{ runner.os != 'Windows' }}
id: unix_package
shell: bash
env:
BUNDLE_ID: ${{ matrix.id }}
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
VERSION: ${{ needs.prepare.outputs.next_version }}
run: |
set -euo pipefail
library_name="${CRATE_NAME//-/_}"
case "$RUNNER_OS" in
Linux) module="target/release/lib${library_name}.so" ;;
macOS) module="target/release/lib${library_name}.dylib" ;;
*) echo "unsupported Unix runner: ${RUNNER_OS}" >&2; exit 1 ;;
esac
if [[ ! -f "$module" ]]; then
echo "module artifact is missing: ${module}" >&2
exit 1
fi
package_name="${CRATE_NAME}-${VERSION}-${BUNDLE_ID}"
package_root="dist/${package_name}"
mkdir -p "$package_root"
install -m 755 "$module" "$package_root/"
install -m 644 LICENSE MODULE.md "$package_root/"
module_name="$(basename "$module")"
module_hash="$(shasum -a 256 "$package_root/$module_name" | awk '{print $1}')"
printf '"%s" = "%s"\n' "$module_name" "$module_hash" \
> "$package_root/modules.toml"
tar -C "$package_root" -czf "dist/${package_name}.tar.gz" .
echo "archive=dist/${package_name}.tar.gz" >> "$GITHUB_OUTPUT"
- name: Assemble Windows module package
if: ${{ runner.os == 'Windows' }}
id: windows_package
shell: pwsh
env:
BUNDLE_ID: ${{ matrix.id }}
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
VERSION: ${{ needs.prepare.outputs.next_version }}
run: |
$ErrorActionPreference = 'Stop'
$libraryName = $env:CRATE_NAME.Replace('-', '_')
$module = "target/release/$libraryName.dll"
if (-not (Test-Path -LiteralPath $module -PathType Leaf)) {
throw "module artifact is missing: $module"
}
$packageName = "$env:CRATE_NAME-$env:VERSION-$env:BUNDLE_ID"
$packageRoot = "dist/$packageName"
New-Item -ItemType Directory -Force $packageRoot | Out-Null
Copy-Item -LiteralPath $module, 'LICENSE', 'MODULE.md' -Destination $packageRoot
$moduleName = Split-Path -Leaf $module
$hash = (Get-FileHash -LiteralPath "$packageRoot/$moduleName" -Algorithm SHA256).Hash.ToLowerInvariant()
$utf8 = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText(
"$packageRoot/modules.toml",
('"{0}" = "{1}"' -f $moduleName, $hash) + [Environment]::NewLine,
$utf8
)
$archive = "dist/$packageName.zip"
Compress-Archive -Path "$packageRoot/*" -DestinationPath $archive
"archive=$archive" >> $env:GITHUB_OUTPUT
- name: Upload Unix package
if: ${{ runner.os != 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: ${{ needs.prepare.outputs.crate_name }}-${{ matrix.id }}
path: ${{ steps.unix_package.outputs.archive }}
if-no-files-found: error
- name: Upload Windows package
if: ${{ runner.os == 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: ${{ needs.prepare.outputs.crate_name }}-${{ matrix.id }}
path: ${{ steps.windows_package.outputs.archive }}
if-no-files-found: error
distro-bundles:
name: Rust module bundle (${{ matrix.id }})
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- id: fedora-43-x86_64
os: ubuntu-24.04
container: fedora:43
target: x86_64-unknown-linux-gnu
family: fedora
- id: fedora-43-arm64
os: ubuntu-24.04-arm
container: fedora:43
target: aarch64-unknown-linux-gnu
family: fedora
- id: fedora-44-x86_64
os: ubuntu-24.04
container: fedora:44
target: x86_64-unknown-linux-gnu
family: fedora
- id: fedora-44-arm64
os: ubuntu-24.04-arm
container: fedora:44
target: aarch64-unknown-linux-gnu
family: fedora
- id: archlinux-rolling-x86_64
os: ubuntu-24.04
container: archlinux:base-devel
target: x86_64-unknown-linux-gnu
family: archlinux
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
steps:
- name: Install Fedora build tools
if: ${{ matrix.family == 'fedora' }}
run: dnf install -y gcc git gzip make perl tar
- name: Install Arch Linux build tools
if: ${{ matrix.family == 'archlinux' }}
run: pacman -Syu --noconfirm base-devel git
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
persist-credentials: false
submodules: true
- uses: dtolnay/rust-toolchain@stable
- name: Verify native Rust target
env:
EXPECTED_TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
actual_target="$(rustc -vV | sed -n 's/^host: //p')"
if [[ "$actual_target" != "$EXPECTED_TARGET" ]]; then
echo "expected ${EXPECTED_TARGET}, got ${actual_target}" >&2
exit 1
fi
- name: Build installable module
run: cargo build --locked --release --lib
- name: Verify module through TinyBus loader
env:
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
run: |
set -euo pipefail
library_name="${CRATE_NAME//-/_}"
verify_root="/opt/${CRATE_NAME}-module-verify"
install -d -m 700 "$verify_root"
install -m 755 "target/release/lib${library_name}.so" "$verify_root/"
cargo run --locked --example verify_module -- \
"$verify_root/lib${library_name}.so"
- name: Assemble distribution module package
id: package
env:
BUNDLE_ID: ${{ matrix.id }}
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
VERSION: ${{ needs.prepare.outputs.next_version }}
run: |
set -euo pipefail
library_name="${CRATE_NAME//-/_}"
module="target/release/lib${library_name}.so"
if [[ ! -f "$module" ]]; then
echo "module artifact is missing: ${module}" >&2
exit 1
fi
package_name="${CRATE_NAME}-${VERSION}-${BUNDLE_ID}"
package_root="dist/${package_name}"
mkdir -p "$package_root"
install -m 755 "$module" "$package_root/"
install -m 644 LICENSE MODULE.md "$package_root/"
module_name="$(basename "$module")"
module_hash="$(sha256sum "$package_root/$module_name" | awk '{print $1}')"
printf '"%s" = "%s"\n' "$module_name" "$module_hash" \
> "$package_root/modules.toml"
tar -C "$package_root" -czf "dist/${package_name}.tar.gz" .
echo "archive=dist/${package_name}.tar.gz" >> "$GITHUB_OUTPUT"
- name: Upload distribution package
uses: actions/upload-artifact@v7
with:
name: ${{ needs.prepare.outputs.crate_name }}-${{ matrix.id }}
path: ${{ steps.package.outputs.archive }}
if-no-files-found: error
github-release:
name: Create GitHub release
needs:
- prepare
- native-bundles
- distro-bundles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.prepare.outputs.tag }}
persist-credentials: false
submodules: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/download-artifact@v8
with:
pattern: '*'
path: release-assets
merge-multiple: true
- name: Create release checksum manifest with TinyBus
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
assets=(release-assets/*.tar.gz release-assets/*.zip)
if [[ ${#assets[@]} -ne 16 ]]; then
echo "expected 16 module archives, found ${#assets[@]}" >&2
exit 1
fi
checksum_args=()
for asset in "${assets[@]}"; do
checksum_args+=(--path "$asset")
done
cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \
--package tinybus --all-features --bin tinybus -- \
modules checksum "${checksum_args[@]}" \
--output release-assets/checksum.toml
- name: Create immutable release with module packages
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
if gh release view "$RELEASE_TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then
echo "Release ${RELEASE_TAG} already exists; immutable assets are unchanged."
else
gh release create "$RELEASE_TAG" release-assets/* \
--repo "$REPOSITORY" \
--verify-tag \
--title "$RELEASE_TAG" \
--generate-notes
fi
- name: Verify the published module through TinyBus
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
REPOSITORY: ${{ github.repository }}
CRATE_NAME: ${{ needs.prepare.outputs.crate_name }}
VERSION: ${{ needs.prepare.outputs.next_version }}
run: |
set -euo pipefail
archive="${CRATE_NAME}-${VERSION}-ubuntu-24.04-x86_64.tar.gz"
release_url="https://github.com/${REPOSITORY}/releases/tag/${RELEASE_TAG}"
sha256="$(
sed -n "s/^\"${archive}\" = \"\([0-9a-f]\{64\}\)\"$/\1/p" \
release-assets/checksum.toml
)"
if [[ -z "$sha256" ]]; then
echo "checksum missing for ${archive}" >&2
exit 1
fi
cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \
--package tinybus --all-features --example github_module_host -- \
"$release_url" "$archive" "$sha256"
cargo run --locked --example verify_github_release -- \
"$release_url" "$archive" "$sha256"