name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: build (${{ matrix.target.name }})
runs-on: ${{ matrix.target.os }}
timeout-minutes: 90
env:
GRR_CLIENT_ID: ${{ secrets.GRR_CLIENT_ID }}
GRR_CLIENT_SECRET: ${{ secrets.GRR_CLIENT_SECRET }}
GRR_DEVICE_CLIENT_ID: ${{ secrets.GRR_DEVICE_CLIENT_ID }}
GRR_DEVICE_CLIENT_SECRET: ${{ secrets.GRR_DEVICE_CLIENT_SECRET }}
UPX_VERSION: "5.2.1"
strategy:
fail-fast: false
matrix:
target:
- name: windows-x86_64
os: windows-latest
artifact: grr.exe
triple: x86_64-pc-windows-msvc
- name: windows-aarch64
os: windows-11-arm
artifact: grr.exe
triple: aarch64-pc-windows-msvc
- name: linux-x86_64
os: ubuntu-26.04
artifact: grr
triple: x86_64-unknown-linux-gnu
- name: linux-aarch64
os: ubuntu-26.04-arm
artifact: grr
triple: aarch64-unknown-linux-gnu
- name: macos-aarch64
os: macos-latest
artifact: grr
triple: aarch64-apple-darwin
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 10
with:
components: rust-src
- uses: Swatinem/rust-cache@v2
timeout-minutes: 10
with:
key: ${{ matrix.target.triple }}
- name: Build release binary
timeout-minutes: 45
run: cargo build --release --target ${{ matrix.target.triple }}
- name: Install UPX (linux)
if: runner.os == 'Linux'
timeout-minutes: 5
run: |
case "$(uname -m)" in
x86_64) UPX_ARCH="amd64_linux" ;;
aarch64) UPX_ARCH="arm64_linux" ;;
*) echo "::error::unsupported linux arch $(uname -m)"; exit 1 ;;
esac
curl -fsSL -o upx.tar.xz \
"https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-${UPX_ARCH}.tar.xz"
tar -xJf upx.tar.xz
sudo install -m 0755 "upx-${UPX_VERSION}-${UPX_ARCH}/upx" /usr/local/bin/upx
upx --version | head -1
- name: Install UPX (macos)
if: runner.os == 'macOS'
timeout-minutes: 10
run: |
brew install upx
upx --version | head -1
- name: Install UPX (windows)
if: runner.os == 'Windows'
timeout-minutes: 5
shell: pwsh
run: |
$zip = "upx-$env:UPX_VERSION-win64.zip"
curl.exe -fsSL -o $zip "https://github.com/upx/upx/releases/download/v$env:UPX_VERSION/$zip"
Expand-Archive -Path $zip -DestinationPath upx-dist -Force
$upx = (Get-ChildItem -Recurse upx-dist -Filter upx.exe | Select-Object -First 1).FullName
# Appending to GITHUB_PATH is the supported way to extend PATH; it
# takes effect for LATER steps, so the Pack step below is where
# `upx` resolves (and fails loudly if it does not). The deprecated
# `::add-path::` command is disabled outright, and GITHUB_ENV
# entries must be KEY=VALUE (a bare path is rejected).
"$((Split-Path $upx))" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Pack binary (UPX --best --lzma)
shell: bash
timeout-minutes: 10
run: |
set -o pipefail
BIN="target/${{ matrix.target.triple }}/release/${{ matrix.target.artifact }}"
size() { stat -c %s "$1" 2>/dev/null || stat -f %z "$1"; }
BEFORE="$(size "$BIN")"
# Never pack in place: UPX's win64 packer segfaults (exit 139) on
# the ARM64 PE and can leave a corrupted binary behind. Pack a
# copy, require exit 0 AND that the result still executes, and
# only then replace the original. Anything else ships unpacked.
WORK="$BIN.packed"
cp "$BIN" "$WORK"
# --lzma measured 20% smaller than --best alone on the real binary
# (4,495,508 -> 3,584,844 bytes, upx 5.2.1); --brute packs to the
# identical size and is not worth its minutes. Formats that cannot
# do LZMA fall back to plain --best from a fresh copy, since a
# failed pack attempt must not be repacked on top of itself.
#
# No `| tee`: a pipeline reports the LAST command's status, so
# `upx ... | tee log` without pipefail made this condition test
# tee — every failed --lzma pack looked like success, the --best
# fallback below was dead code, and the build reported "packed"
# with equal before/after bytes. upx's output already lands in
# the step log.
if ! upx --best --lzma "$WORK"; then
cp "$BIN" "$WORK"
if upx --best "$WORK"; then
echo "::notice::--lzma unsupported for ${{ matrix.target.name }}; packed with --best"
else
echo "::warning::UPX failed on ${{ matrix.target.name }}; shipping unpacked"
rm -f "$WORK"
fi
fi
if [ -f "$WORK" ]; then
if "$WORK" --version >/dev/null 2>&1; then
AFTER="$(size "$WORK")"
mv "$WORK" "$BIN"
echo "packed ${{ matrix.target.name }}: $BEFORE -> $AFTER bytes"
else
echo "::warning::UPX packed ${{ matrix.target.name }} but the result does not run; shipping unpacked"
rm -f "$WORK"
fi
fi
"$BIN" --version
- uses: actions/upload-artifact@v7
timeout-minutes: 5
with:
name: grr-${{ github.ref_name }}-${{ matrix.target.name }}
path: target/${{ matrix.target.triple }}/release/${{ matrix.target.artifact }}
if-no-files-found: error
archive:
name: archive
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/download-artifact@v8
timeout-minutes: 5
with:
path: dist
merge-multiple: false
- name: Stage binaries
timeout-minutes: 5
run: |
set -euo pipefail
mkdir -p stage
# merge-multiple=false keeps one directory per artifact, named
# `grr-<ref>-<target>`. The target list is spelled out here instead of
# being recovered by stripping the version out of that directory name:
# a prerelease tag (`v0.7.0-rc.1`) makes the strip ambiguous and
# produced names like `rc.1-linux-x86_64`, so every archive step below
# then looked for a stage file that did not exist and the release
# failed. Matching on the known target suffix is tag-independent.
for name in linux-x86_64 linux-aarch64 macos-aarch64 windows-x86_64 windows-aarch64; do
bin=$(find dist -type f \( -name grr -o -name grr.exe \) -path "*/grr-*-$name/*" -print -quit)
if [ -z "$bin" ]; then
echo "::error::no downloaded artifact for $name"
exit 1
fi
# Stage under the PRODUCT name, one directory per target so the
# four unix binaries cannot clobber each other. Archives shipping
# `<target>` members broke real consumers: winget's
# NestedInstallerFiles promised grr.exe, found nothing, and the
# first submission looped on "issue with installing the
# application correctly" (live example: winget-pkgs PR #441361);
# every human extracting a tarball had to rename the file by hand.
mkdir -p "stage/$name"
case "$name" in
windows-*) cp "$bin" "stage/$name/grr.exe" ;;
*) cp "$bin" "stage/$name/grr" ;;
esac
done
ls -l stage/*
- name: Build archives
timeout-minutes: 10
run: |
set -euo pipefail
ref="${{ github.ref_name }}"
# zstd -22 is the maximum level; -T0 uses all cores. Decompression
# stays fast, which matters more than the last few percent.
for name in linux-x86_64 linux-aarch64 macos-aarch64; do
tar -cf - -C "stage/$name" grr | zstd -22 -T0 -q -o "grr-$ref-$name.tar.zst"
done
# Windows keeps .zip: Explorer's built-in extractor only reads
# deflate, so lzma2-in-zip would be unopenable without 7-Zip.
# -mx=9 is max deflate.
for name in windows-x86_64 windows-aarch64; do
7z a -m0=deflate -mx=9 -bso0 -bsp0 "grr-$ref-$name.zip" "./stage/$name/grr.exe" >/dev/null
done
sha256sum grr-* > SHA256SUMS
ls -l
- uses: actions/upload-artifact@v7
timeout-minutes: 5
with:
name: release-assets
path: |
grr-${{ github.ref_name }}-*.tar.zst
grr-${{ github.ref_name }}-*.zip
SHA256SUMS
release:
name: create release
needs: archive
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
actions: write
steps:
- uses: actions/download-artifact@v8
timeout-minutes: 5
with:
name: release-assets
path: assets
- name: Create GitHub release
uses: softprops/action-gh-release@v3
timeout-minutes: 10
with:
generate_release_notes: true
files: |
assets/grr-${{ github.ref_name }}-windows-x86_64.zip
assets/grr-${{ github.ref_name }}-windows-aarch64.zip
assets/grr-${{ github.ref_name }}-linux-x86_64.tar.zst
assets/grr-${{ github.ref_name }}-linux-aarch64.tar.zst
assets/grr-${{ github.ref_name }}-macos-aarch64.tar.zst
assets/SHA256SUMS
- name: Dispatch the release watchers
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
timeout-minutes: 5
run: |
for wf in demo.yml benchmark.yml stats.yml verify-delivery.yml; do
echo "dispatching $wf"
gh workflow run "$wf" --ref main || echo "::warning::$wf dispatch failed"
done
echo "dispatching changelog.yml (verify=true)"
gh workflow run changelog.yml --ref main -f verify=true \
|| echo "::warning::changelog.yml dispatch failed"
winget:
name: generate the winget manifests
needs: release
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: actions/setup-node@v7
timeout-minutes: 5
with:
node-version: "26"
- name: Generate and attach the winget submission
timeout-minutes: 8
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
node scripts/generate-winget.ts "${{ github.ref_name }}" --out winget-manifests
tar -czf "grr-${{ github.ref_name }}-winget-manifests.tar.gz" -C winget-manifests .
gh release upload "${{ github.ref_name }}" \
"grr-${{ github.ref_name }}-winget-manifests.tar.gz" --clobber
echo "attached grr-${{ github.ref_name }}-winget-manifests.tar.gz"