Skip to content

Release

Release #20

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
ref:
description: Git tag to release, for example v1.2.3
required: true
type: string
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
source-gates:
name: Release source gates
runs-on: ubuntu-latest
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }}
RUSTDOCFLAGS: -D warnings
RUST_MIN_STACK: "8388608"
RUST_TEST_THREADS: "1"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_REF }}
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Fetch locked dependencies
run: cargo fetch --locked
- name: cargo fmt
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: cargo test
run: cargo test --workspace --locked --no-fail-fast
- name: rustdoc with warnings denied
run: cargo doc --workspace --locked --no-deps
- name: Source boundary gates
shell: bash
run: |
scripts/no-network-in-runtime.sh
scripts/check-platform-neutrality.sh
scripts/no-debug-assert-side-effects.sh
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
platform-label: linux-x86_64
binary: rmux
archive: tar.gz
- os: macos-15-intel
target: x86_64-apple-darwin
platform-label: macos-x86_64
binary: rmux
archive: tar.gz
- os: macos-15
target: aarch64-apple-darwin
platform-label: macos-aarch64
binary: rmux
archive: tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
platform-label: windows-x86_64
binary: rmux.exe
archive: zip
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }}
TARGET: ${{ matrix.target }}
PLATFORM_LABEL: ${{ matrix.platform-label }}
BINARY: ${{ matrix.binary }}
BUILD_PROFILE: release
PROFILE_DIR: release
steps:
- name: Validate release ref
shell: bash
run: |
if [[ ! "$RELEASE_REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release ref: $RELEASE_REF" >&2
exit 1
fi
echo "TAG=$RELEASE_REF" >> "$GITHUB_ENV"
echo "VERSION=${RELEASE_REF#v}" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_REF }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Fetch locked dependencies
run: cargo fetch --locked
- name: Build rmux
shell: bash
run: cargo build --package rmux --locked --profile "$BUILD_PROFILE" --target "$TARGET"
- name: Inspect release binary
shell: bash
run: |
bin="target/$TARGET/$PROFILE_DIR/$BINARY"
test -x "$bin"
ls -lh "$bin"
if [[ "$RUNNER_OS" != "Windows" ]]; then
file "$bin"
fi
- name: Re-sign macOS binary
if: runner.os == 'macOS'
shell: bash
run: |
bin="target/$TARGET/$PROFILE_DIR/$BINARY"
codesign --force --sign - "$bin"
codesign --verify --verbose=2 "$bin"
- name: Smoke binary
shell: bash
run: |
bin="target/$TARGET/$PROFILE_DIR/$BINARY"
"$bin" -V | tee version.txt
grep -F "rmux $VERSION" version.txt
- name: Install Linux package tooling
if: matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y cpio rpm createrepo-c
- name: Windows runtime smoke
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$bin = "target/$env:TARGET/$env:PROFILE_DIR/$env:BINARY"
& $bin -V
& $bin list-commands | Out-Null
- name: Create Unix archive
if: matrix.archive == 'tar.gz'
shell: bash
run: |
rm -rf dist
if [[ "$RUNNER_OS" == "macOS" ]]; then
export RMUX_PACKAGE_CODESIGN_ADHOC=1
fi
scripts/package-unix.sh \
--configuration "$BUILD_PROFILE" \
--target "$TARGET" \
--output-dir dist
archive="$(ls dist/rmux-*.tar.gz | head -n 1)"
scripts/verify-package.sh "$archive" --checksums dist/SHA256SUMS.txt --run-binary --require-release-artifact
- name: Create Linux distro packages
if: matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
scripts/package-debian.sh \
--configuration "$BUILD_PROFILE" \
--target "$TARGET" \
--output-dir dist \
--skip-build \
--reuse-release-binary
deb="$(ls dist/rmux_*.deb | head -n 1)"
scripts/verify-debian-package.sh "$deb" --checksums dist/SHA256SUMS.txt --run-binary --require-release-artifact
scripts/package-rpm.sh \
--configuration "$BUILD_PROFILE" \
--target "$TARGET" \
--output-dir dist \
--skip-build \
--reuse-release-binary
rpm="$(ls dist/rmux-*.rpm | head -n 1)"
scripts/verify-rpm-package.sh "$rpm" --checksums dist/SHA256SUMS.txt --run-binary --require-release-artifact
- name: Create Windows archive
if: matrix.archive == 'zip'
shell: pwsh
run: |
./scripts/package-windows.ps1 `
-Configuration "$env:BUILD_PROFILE" `
-Target "$env:TARGET" `
-OutputDir dist `
-PlatformLabel "$env:PLATFORM_LABEL"
$archive = Get-ChildItem -Path dist -Filter "rmux-*.zip" | Select-Object -First 1
if ($null -eq $archive) {
throw "Windows package archive was not generated"
}
./scripts/verify-package-windows.ps1 `
$archive.FullName `
-Checksums dist/SHA256SUMS.txt `
-RunBinary `
-RunDaemonSmoke
- uses: actions/upload-artifact@v4
with:
name: rmux-${{ env.RELEASE_REF }}-${{ matrix.target }}
path: |
dist/*.tar.gz
dist/*.zip
dist/*.deb
dist/*.rpm
if-no-files-found: error
publish:
name: Publish GitHub release assets
runs-on: ubuntu-latest
environment: release
needs:
- source-gates
- build
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_REF }}
- uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: Generate checksums
shell: bash
run: |
cd release-assets
shopt -s nullglob
assets=( *.tar.gz *.zip *.deb *.rpm )
if (( ${#assets[@]} == 0 )); then
echo "No release archives found" >&2
exit 1
fi
printf '%s\0' "${assets[@]}" | sort -z | xargs -0 sha256sum > SHA256SUMS
- name: Install repository metadata tooling
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y createrepo-c gnupg rpm
- name: Import Linux repository signing keys
shell: bash
env:
RMUX_APT_GPG_PRIVATE_KEY: ${{ secrets.RMUX_APT_GPG_PRIVATE_KEY }}
RMUX_APT_GPG_KEY: ${{ secrets.RMUX_APT_GPG_KEY }}
RMUX_RPM_GPG_PRIVATE_KEY: ${{ secrets.RMUX_RPM_GPG_PRIVATE_KEY }}
RMUX_RPM_GPG_KEY: ${{ secrets.RMUX_RPM_GPG_KEY }}
RMUX_RPM_REPO_GPG_PRIVATE_KEY: ${{ secrets.RMUX_RPM_REPO_GPG_PRIVATE_KEY }}
RMUX_RPM_REPO_GPG_KEY: ${{ secrets.RMUX_RPM_REPO_GPG_KEY }}
run: |
missing=0
for name in RMUX_APT_GPG_PRIVATE_KEY RMUX_APT_GPG_KEY RMUX_RPM_GPG_PRIVATE_KEY RMUX_RPM_GPG_KEY; do
if [ -z "${!name:-}" ]; then
echo "Missing required secret: $name" >&2
missing=1
fi
done
rpm_repo_key="${RMUX_RPM_REPO_GPG_KEY:-$RMUX_RPM_GPG_KEY}"
if [ "$rpm_repo_key" != "$RMUX_RPM_GPG_KEY" ] && [ -z "${RMUX_RPM_REPO_GPG_PRIVATE_KEY:-}" ]; then
echo "Missing required secret: RMUX_RPM_REPO_GPG_PRIVATE_KEY" >&2
missing=1
fi
[ "$missing" -eq 0 ] || exit 1
install -m 700 -d ~/.gnupg
printf '%s\n' "$RMUX_APT_GPG_PRIVATE_KEY" | gpg --batch --import
printf '%s\n' "$RMUX_RPM_GPG_PRIVATE_KEY" | gpg --batch --import
if [ -n "${RMUX_RPM_REPO_GPG_PRIVATE_KEY:-}" ]; then
printf '%s\n' "$RMUX_RPM_REPO_GPG_PRIVATE_KEY" | gpg --batch --import
fi
gpg --batch --list-secret-keys "$RMUX_APT_GPG_KEY" >/dev/null
gpg --batch --list-secret-keys "$RMUX_RPM_GPG_KEY" >/dev/null
gpg --batch --list-secret-keys "$rpm_repo_key" >/dev/null
- name: Generate Homebrew formula
shell: bash
run: |
version="${RELEASE_REF#v}"
mkdir -p target/homebrew/Formula
scripts/generate-homebrew-formula.sh \
--version "$version" \
--checksums release-assets/SHA256SUMS \
--output target/homebrew/Formula/rmux.rb
if command -v ruby >/dev/null 2>&1; then
ruby -c target/homebrew/Formula/rmux.rb
fi
- name: Generate package manager metadata
shell: bash
run: |
version="${RELEASE_REF#v}"
mkdir -p \
target/package-managers/homebrew/Formula \
target/package-managers/winget \
target/package-managers/scoop \
target/package-managers/chocolatey
cp target/homebrew/Formula/rmux.rb target/package-managers/homebrew/Formula/rmux.rb
scripts/generate-winget-manifest.sh \
--version "$version" \
--checksums release-assets/SHA256SUMS \
--output target/package-managers/winget/Helvesec.RMUX.yaml
sed -i \
-e 's/winget-manifest\.singleton\.1\.12\.0\.schema\.json/winget-manifest.singleton.1.10.0.schema.json/' \
-e 's/ManifestVersion: "1\.12\.0"/ManifestVersion: "1.10.0"/' \
target/package-managers/winget/Helvesec.RMUX.yaml
scripts/generate-scoop-manifest.sh \
--version "$version" \
--checksums release-assets/SHA256SUMS \
--output target/package-managers/scoop/rmux.json
scripts/generate-chocolatey-package.sh \
--version "$version" \
--checksums release-assets/SHA256SUMS \
--output-dir target/package-managers/chocolatey/rmux
if command -v ruby >/dev/null 2>&1; then
ruby -rjson -e 'JSON.parse(File.read(ARGV.fetch(0)))' target/package-managers/scoop/rmux.json
fi
- name: Validate WinGet metadata
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = $env:RELEASE_REF.Substring(1)
./scripts/validate-winget-manifest.ps1 `
-Manifest "target/package-managers/winget/Helvesec.RMUX.yaml" `
-Version $version `
-Checksums "release-assets/SHA256SUMS" `
-Repository $env:GITHUB_REPOSITORY
- uses: actions/upload-artifact@v4
with:
name: package-manager-metadata-${{ env.RELEASE_REF }}
path: target/package-managers
if-no-files-found: error
- name: Generate Linux package repositories
shell: bash
env:
RMUX_APT_GPG_KEY: ${{ secrets.RMUX_APT_GPG_KEY }}
RMUX_RPM_GPG_KEY: ${{ secrets.RMUX_RPM_GPG_KEY }}
RMUX_RPM_REPO_GPG_KEY: ${{ secrets.RMUX_RPM_REPO_GPG_KEY }}
run: |
: "${RMUX_APT_GPG_KEY:?missing RMUX_APT_GPG_KEY}"
: "${RMUX_RPM_GPG_KEY:?missing RMUX_RPM_GPG_KEY}"
rpm_repo_key="${RMUX_RPM_REPO_GPG_KEY:-$RMUX_RPM_GPG_KEY}"
mkdir -p target/package-repositories
scripts/generate-apt-repository.sh \
--input-dir release-assets \
--output-dir target/package-repositories/debian \
--suite stable \
--component main \
--architecture amd64 \
--signing-key "$RMUX_APT_GPG_KEY"
scripts/generate-rpm-repository.sh \
--input-dir release-assets \
--output-dir target/package-repositories/rpm \
--baseurl https://packages.rmux.io/rpm \
--rpm-signing-key "$RMUX_RPM_GPG_KEY" \
--repo-signing-key "$rpm_repo_key"
gpg --armor --export "$RMUX_APT_GPG_KEY" > target/package-repositories/debian/rmux.asc
gpg --armor --export "$RMUX_RPM_GPG_KEY" > target/package-repositories/rpm/RPM-GPG-KEY-rmux
test -f target/package-repositories/debian/rmux.asc
test -f target/package-repositories/debian/dists/stable/Release
test -f target/package-repositories/debian/dists/stable/InRelease
test -f target/package-repositories/debian/dists/stable/Release.gpg
test -f target/package-repositories/debian/dists/stable/main/binary-amd64/Packages.gz
test -f target/package-repositories/rpm/RPM-GPG-KEY-rmux
test -f target/package-repositories/rpm/repodata/repomd.xml
test -f target/package-repositories/rpm/repodata/repomd.xml.asc
test -f target/package-repositories/rpm/rmux.repo
grep -qx 'gpgcheck=1' target/package-repositories/rpm/rmux.repo
grep -qx 'repo_gpgcheck=1' target/package-repositories/rpm/rmux.repo
- uses: actions/upload-artifact@v4
with:
name: linux-package-repositories-${{ env.RELEASE_REF }}
path: target/package-repositories
if-no-files-found: error
- name: Create or update release
shell: bash
run: |
tag="$RELEASE_REF"
version="${tag#v}"
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$tag" \
--repo "$GITHUB_REPOSITORY" \
--title "$tag" \
--draft \
--notes "RMUX $version binary release."
fi
shopt -s nullglob
assets=(
release-assets/*.tar.gz
release-assets/*.zip
release-assets/*.deb
release-assets/*.rpm
release-assets/SHA256SUMS
)
if (( ${#assets[@]} == 0 )); then
echo "No release assets found" >&2
exit 1
fi
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" "${assets[@]}" --clobber
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --draft=false
- name: Deploy Linux package repositories
shell: bash
env:
RMUX_PACKAGE_REPO_TOKEN: ${{ secrets.RMUX_PACKAGE_REPO_TOKEN }}
RMUX_PACKAGE_REPO: Helvesec/rmux-packages
RMUX_PACKAGE_REPO_BRANCH: main
run: |
: "${RMUX_PACKAGE_REPO_TOKEN:?missing RMUX_PACKAGE_REPO_TOKEN}"
: "${RMUX_PACKAGE_REPO:?missing RMUX_PACKAGE_REPO}"
: "${RMUX_PACKAGE_REPO_BRANCH:?missing RMUX_PACKAGE_REPO_BRANCH}"
test -d target/package-repositories/debian
test -d target/package-repositories/rpm
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
package_repo_url="https://x-access-token:${RMUX_PACKAGE_REPO_TOKEN}@github.com/${RMUX_PACKAGE_REPO}.git"
git clone --depth 1 --branch "$RMUX_PACKAGE_REPO_BRANCH" "$package_repo_url" "$work"
rm -rf "$work/debian" "$work/rpm"
cp -a target/package-repositories/debian "$work/debian"
cp -a target/package-repositories/rpm "$work/rpm"
cat > "$work/index.html" <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RMUX Packages</title>
</head>
<body>
<h1>RMUX Packages</h1>
<ul>
<li><a href="/debian/dists/stable/InRelease">APT repository</a></li>
<li><a href="/rpm/rmux.repo">RPM repository</a></li>
</ul>
</body>
</html>
EOF
cat > "$work/_headers" <<'EOF'
/debian/dists/*
Cache-Control: public, max-age=300
/debian/rmux.asc
Cache-Control: public, max-age=3600
/debian/pool/*
Cache-Control: public, max-age=31536000, immutable
/rpm/repodata/*
Cache-Control: public, max-age=300
/rpm/RPM-GPG-KEY-rmux
Cache-Control: public, max-age=3600
/rpm/rmux.repo
Cache-Control: public, max-age=300
/rpm/*.rpm
Cache-Control: public, max-age=31536000, immutable
EOF
cd "$work"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if [ -z "$(git status --porcelain)" ]; then
echo "Linux package repository already points to $RELEASE_REF."
exit 0
fi
git add _headers index.html debian rpm
git commit -m "packages: publish rmux $RELEASE_REF"
git push "$package_repo_url" "HEAD:$RMUX_PACKAGE_REPO_BRANCH"
validate-winget:
name: Validate WinGet manifest
runs-on: windows-latest
needs:
- publish
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }}
steps:
- name: Validate release ref
shell: pwsh
run: |
if ($env:RELEASE_REF -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid release ref: $env:RELEASE_REF"
}
"VERSION=$($env:RELEASE_REF.Substring(1))" | Out-File -FilePath $env:GITHUB_ENV -Append
- uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_REF }}
- uses: actions/download-artifact@v4
with:
name: package-manager-metadata-${{ env.RELEASE_REF }}
path: package-manager-metadata
- name: Download release checksums
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path release-assets | Out-Null
gh release download $env:RELEASE_REF `
--repo $env:GITHUB_REPOSITORY `
--pattern SHA256SUMS `
--dir release-assets `
--clobber
- name: Validate generated WinGet manifest
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$manifest = "package-manager-metadata\winget\Helvesec.RMUX.yaml"
.\scripts\validate-winget-manifest.ps1 `
-Manifest $manifest `
-Version $env:VERSION `
-Checksums "release-assets\SHA256SUMS" `
-Repository $env:GITHUB_REPOSITORY
- name: Run winget validate
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$manifest = "package-manager-metadata\winget\Helvesec.RMUX.yaml"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
throw "winget is required on the Windows release runner so the generated manifest is validated by the real WinGet CLI."
}
winget --version
winget validate --manifest $manifest --disable-interactivity
publish-chocolatey:
name: Publish Chocolatey package
runs-on: windows-latest
needs:
- publish
environment: release
env:
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }}
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
steps:
- name: Validate release ref
shell: pwsh
run: |
if ($env:RELEASE_REF -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid release ref: $env:RELEASE_REF"
}
"VERSION=$($env:RELEASE_REF.Substring(1))" | Out-File -FilePath $env:GITHUB_ENV -Append
- uses: actions/download-artifact@v4
with:
name: package-manager-metadata-${{ env.RELEASE_REF }}
path: package-manager-metadata
- name: Validate Chocolatey prerequisites
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:CHOCOLATEY_API_KEY)) {
throw "Missing required GitHub Actions secret: CHOCOLATEY_API_KEY"
}
choco --version
- name: Pack and smoke-test Chocolatey package
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$version = $env:VERSION
$packageRoot = (Resolve-Path "package-manager-metadata\chocolatey\rmux").Path
$nuspecPath = Join-Path $packageRoot "rmux.nuspec"
$installScript = Join-Path $packageRoot "tools\chocolateyInstall.ps1"
$uninstallScript = Join-Path $packageRoot "tools\chocolateyUninstall.ps1"
foreach ($required in @($nuspecPath, $installScript, $uninstallScript)) {
if (-not (Test-Path $required)) {
throw "Missing generated Chocolatey file: $required"
}
}
[xml]$nuspec = Get-Content $nuspecPath
if ($nuspec.package.metadata.id -ne "rmux") {
throw "Unexpected Chocolatey package id: $($nuspec.package.metadata.id)"
}
if ($nuspec.package.metadata.version -ne $version) {
throw "Unexpected Chocolatey package version: $($nuspec.package.metadata.version)"
}
Push-Location $packageRoot
try {
choco pack .\rmux.nuspec --outputdirectory .
} finally {
Pop-Location
}
$package = Join-Path $packageRoot "rmux.$version.nupkg"
if (-not (Test-Path $package)) {
throw "Chocolatey package was not generated: $package"
}
choco install rmux `
--source $packageRoot `
--version $version `
--yes `
--no-progress `
--force
$versionOutput = (& rmux -V).Trim()
if ($versionOutput -ne "rmux $version") {
throw "Unexpected rmux version from Chocolatey install: $versionOutput"
}
choco uninstall rmux --yes --no-progress
"CHOCOLATEY_PACKAGE=$package" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Push Chocolatey package
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
if (-not (Test-Path $env:CHOCOLATEY_PACKAGE)) {
throw "Chocolatey package not found: $env:CHOCOLATEY_PACKAGE"
}
$version = $env:VERSION
$query = "https://community.chocolatey.org/api/v2/FindPackagesById()?id='rmux'"
$response = Invoke-WebRequest -Uri $query -Method Get
[xml]$feed = $response.Content
$namespaceManager = [System.Xml.XmlNamespaceManager]::new($feed.NameTable)
$namespaceManager.AddNamespace("atom", "http://www.w3.org/2005/Atom")
$namespaceManager.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")
$namespaceManager.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
$existingVersion = $feed.SelectSingleNode("//atom:entry/m:properties[d:Version='$version']/d:Version", $namespaceManager)
if ($existingVersion) {
Write-Host "Chocolatey package rmux $version already exists; skipping push."
exit 0
}
choco push $env:CHOCOLATEY_PACKAGE `
--source "https://push.chocolatey.org/" `
--api-key $env:CHOCOLATEY_API_KEY `
--yes `
--no-progress