name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
tag:
description: "Tag to build artifacts for (e.g. v0.3.21). Must already exist."
required: true
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
verify:
name: verify build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo test --all-features
package-source:
name: package source + cargo .crate
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Build cargo package
run: cargo package --allow-dirty --no-verify
- name: Stage source artifacts
run: |
tag="${{ steps.tag.outputs.tag }}"
mkdir -p release
cp target/package/capa-*.crate "release/capa-${tag}.crate"
src_name="capa-${tag}-src"
git archive --format=tar.gz --prefix="${src_name}/" \
-o "release/${src_name}.tar.gz" HEAD
(cd release && \
for f in *.crate *.tar.gz; do
sha256sum "$f" > "$f.sha256"
done)
ls -la release
- name: Upload source artifacts
uses: actions/upload-artifact@v4
with:
name: capa-source-artifacts
path: |
release/*.crate
release/*.tar.gz
release/*.sha256
if-no-files-found: error
package-flirt-sigs:
name: package flirt-sigs tarball
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Build flirt-sigs tarball
run: |
tag="${{ steps.tag.outputs.tag }}"
mkdir -p release
if [ ! -d flirt-sigs ]; then
echo "flirt-sigs/ directory missing from checkout — refusing to ship empty bundle"
exit 1
fi
# Tar from the parent so the archive extracts to a clean
# `flirt-sigs/` directory at the user's chosen path.
tar -czf "release/flirt-sigs-${tag}.tar.gz" flirt-sigs/
(cd release && sha256sum "flirt-sigs-${tag}.tar.gz" > "flirt-sigs-${tag}.tar.gz.sha256")
ls -lh release
- name: Upload flirt-sigs artifact
uses: actions/upload-artifact@v4
with:
name: flirt-sigs
path: |
release/flirt-sigs-*.tar.gz
release/flirt-sigs-*.sha256
if-no-files-found: error
build-cli:
name: build capa_cli — ${{ matrix.target }}
needs: verify
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
cross: false
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
cross: true
- target: x86_64-apple-darwin
os: macos-latest
cross: false
- target: aarch64-apple-darwin
os: macos-latest
cross: false
- target: x86_64-pc-windows-msvc
os: windows-latest
cross: false
- target: aarch64-pc-windows-msvc
os: windows-latest
cross: false
steps:
- uses: actions/checkout@v4
- name: Install Rust + target
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install aarch64-linux cross toolchain
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
mkdir -p .cargo
cat >> .cargo/config.toml <<'EOF'
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
EOF
- name: Determine tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Build capa_cli (release)
run: cargo build --release --example capa_cli --target ${{ matrix.target }} --all-features
- name: Stage binary
shell: bash
run: |
tag="${{ steps.tag.outputs.tag }}"
target="${{ matrix.target }}"
mkdir -p release
# The example binary lives at target/<triple>/release/examples/<name>(.exe).
if [[ "$target" == *windows* ]]; then
src="target/${target}/release/examples/capa_cli.exe"
dst="release/capa_cli-${tag}-${target}.exe"
else
src="target/${target}/release/examples/capa_cli"
dst="release/capa_cli-${tag}-${target}"
fi
cp "$src" "$dst"
# SHA-256 — `sha256sum` exists on Linux/macOS, `certutil` on Windows.
if command -v sha256sum >/dev/null 2>&1; then
(cd release && sha256sum "$(basename "$dst")" > "$(basename "$dst").sha256")
else
(cd release && certutil -hashfile "$(basename "$dst")" SHA256 | \
grep -v ':' | head -1 | tr -d ' \r' > "$(basename "$dst").sha256")
fi
ls -la release
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: capa_cli-${{ matrix.target }}
path: |
release/capa_cli-*
release/capa_cli-*.sha256
if-no-files-found: error
github-release:
name: create GitHub release
needs: [package-source, package-flirt-sigs, build-cli]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Generate release notes
id: notes
run: |
tag="${{ steps.tag.outputs.tag }}"
previous=$(git describe --tags --abbrev=0 "${tag}^" 2>/dev/null || echo "")
{
echo "notes<<EOF"
if [ -n "$previous" ]; then
echo "## Changes since $previous"
echo
git log --pretty=format:"- %s (%h)" "${previous}..${tag}"
else
echo "## Initial release"
echo
git log --pretty=format:"- %s (%h)" "${tag}"
fi
echo
echo
echo "## Source"
echo
echo "- \`capa-${tag}.crate\` — the cargo package archive (same bytes as crates.io)"
echo "- \`capa-${tag}-src.tar.gz\` — plain git source tarball"
echo
echo "## CLI binaries"
echo
echo "Pre-built \`capa_cli\` binaries for each supported platform:"
echo
echo "| Platform | Binary |"
echo "|---------------------------|----------------------------------------------------|"
echo "| Linux x86_64 | \`capa_cli-${tag}-x86_64-unknown-linux-gnu\` |"
echo "| Linux aarch64 | \`capa_cli-${tag}-aarch64-unknown-linux-gnu\` |"
echo "| macOS Intel (x86_64) | \`capa_cli-${tag}-x86_64-apple-darwin\` |"
echo "| macOS Apple Silicon | \`capa_cli-${tag}-aarch64-apple-darwin\` |"
echo "| Windows x86_64 | \`capa_cli-${tag}-x86_64-pc-windows-msvc.exe\` |"
echo "| Windows aarch64 | \`capa_cli-${tag}-aarch64-pc-windows-msvc.exe\` |"
echo
echo "Every artifact ships with a matching \`*.sha256\` checksum."
echo
echo "## FLIRT signatures (optional, for library-function recognition)"
echo
echo "- \`flirt-sigs-${tag}.tar.gz\` — Mandiant FLARE + Maktm FLIRTDB \`.sig\` corpora (~25-35 MB compressed, ~70 MB extracted)"
echo
echo "Extract anywhere and pass \`--signatures /path/to/flirt-sigs\` to \`capa_cli\`. Optional — analysis works without it, but stripped MSVC binaries produce cleaner output when FLIRT is wired up. Signature content remains the work of Mandiant FLARE and Michael Kiros (Maktm); see \`flirt-sigs/README.md\` for licensing and credits."
echo
echo "Publishing to crates.io is performed manually with \`cargo publish\` and is not part of this workflow."
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Flatten artifact tree
run: |
mkdir -p release
# Each artifacts/<name>/ folder contains files we want at release/ top-level.
find artifacts -type f \( -name 'capa-*' -o -name 'capa_cli-*' -o -name 'flirt-sigs-*' \) -exec cp {} release/ \;
ls -la release
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
body: ${{ steps.notes.outputs.notes }}
files: release/*
draft: false
prerelease: ${{ contains(steps.tag.outputs.tag, '-') }}