name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
NODE_VERSION: "22.12.0"
jobs:
preflight:
name: Release Preflight
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
VSCE_PAT: ${{ secrets.VSCE_PAT }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Verify release secrets
run: |
missing=0
for name in NPM_TOKEN CARGO_REGISTRY_TOKEN VSCE_PAT; do
if [ -z "${!name:-}" ]; then
echo "::error::Missing required release secret: ${name}"
missing=1
fi
done
exit "${missing}"
- name: Verify tag matches package versions
run: |
tag_version="${GITHUB_REF_NAME#v}"
cargo_version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
npm_version=$(node -p "require('./packages/npm/package.json').version")
vscode_version=$(node -p "require('./vscode-extension/package.json').version")
vscode_lock_version=$(node -p "require('./vscode-extension/package-lock.json').version")
for entry in \
"Cargo.toml:${cargo_version}" \
"packages/npm/package.json:${npm_version}" \
"vscode-extension/package.json:${vscode_version}" \
"vscode-extension/package-lock.json:${vscode_lock_version}"
do
file="${entry%%:*}"
version="${entry#*:}"
if [ "${version}" != "${tag_version}" ]; then
echo "::error::${file} version ${version} does not match tag ${tag_version}"
exit 1
fi
done
build:
name: Build ${{ matrix.target }}
needs: preflight
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
artifact: foxguard-linux-x86_64
use_cross: false
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
artifact: foxguard-linux-aarch64
use_cross: true
- target: x86_64-apple-darwin
os: macos-latest
artifact: foxguard-macos-x86_64
- target: aarch64-apple-darwin
os: macos-latest
artifact: foxguard-macos-aarch64
- target: x86_64-pc-windows-msvc
os: windows-latest
artifact: foxguard-windows-x86_64.exe
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools (x86_64)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Install cross (aarch64)
if: matrix.use_cross == true
run: cargo install cross --git https://github.com/cross-rs/cross
- uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-release-
- name: Build
run: |
if [ "${{ matrix.use_cross }}" = "true" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
shell: bash
- name: Rename binary (unix)
if: runner.os != 'Windows'
run: cp target/${{ matrix.target }}/release/foxguard ${{ matrix.artifact }}
- name: Rename binary (windows)
if: runner.os == 'Windows'
run: cp target/${{ matrix.target }}/release/foxguard.exe ${{ matrix.artifact }}
- uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
github-release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v8
with:
path: artifacts
- name: Collect binaries
run: |
mkdir -p release
find artifacts -type f -exec cp {} release/ \;
ls -la release/
- name: Generate checksums
run: |
cd release
sha256sum * > checksums.txt
echo "--- checksums.txt ---"
cat checksums.txt
- name: Attest release binaries
uses: actions/attest-build-provenance@v2
with:
subject-checksums: release/checksums.txt
- name: Attest checksum manifest
uses: actions/attest-build-provenance@v2
with:
subject-path: release/checksums.txt
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTES_${{ github.ref_name }}.md
files: release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-crates:
name: Publish crates.io
needs: github-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: Publish crate
run: |
set +e
output=$(cargo publish --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" 2>&1)
status=$?
set -e
echo "${output}"
if [ "${status}" -ne 0 ]; then
crate_name=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
crate_version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
published_version=$(curl -fsSL "https://crates.io/api/v1/crates/${crate_name}" | jq -r '.crate.max_version // ""' 2>/dev/null || echo "")
if [ "${published_version}" = "${crate_version}" ]; then
echo "crates.io already has ${crate_name} ${crate_version}; treating rerun as success"
exit 0
fi
exit "${status}"
fi
publish-npm:
name: Publish npm
needs: github-release
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/npm
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: "https://registry.npmjs.org"
- name: Publish npm package
run: |
set +e
output=$(npm publish --access public 2>&1)
status=$?
set -e
echo "${output}"
if [ "${status}" -ne 0 ]; then
package_name=$(node -p "require('./package.json').name")
package_version=$(node -p "require('./package.json').version")
published_version=$(npm view "${package_name}" version 2>/dev/null || true)
if [ "${published_version}" = "${package_version}" ]; then
echo "npm already has ${package_name} ${package_version}; treating rerun as success"
exit 0
fi
exit "${status}"
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-vscode:
name: Publish VS Code Extension
needs: github-release
runs-on: ubuntu-latest
defaults:
run:
working-directory: vscode-extension
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: vscode-extension/package-lock.json
- run: npm ci
- run: npm run compile
- name: Publish extension
run: |
set +e
output=$(npx @vscode/vsce publish -p "${VSCE_PAT}" 2>&1)
status=$?
echo "${output}"
if [ "${status}" -ne 0 ] && ! echo "${output}" | grep -q "already exists"; then
exit "${status}"
fi
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
- name: Verify Marketplace propagation
run: VSCODE_MARKETPLACE_VERSION="${GITHUB_REF_NAME#v}" node ../scripts/verify-vscode-marketplace.mjs
publish-ghcr-github-app:
name: Publish foxguard-github-app image (GHCR)
needs: github-release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- name: Check Dockerfile presence
id: check
run: |
if [ -f Dockerfile.github-app ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "Dockerfile.github-app not present at this tag — skipping image publish."
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- uses: docker/setup-qemu-action@v3
if: steps.check.outputs.exists == 'true'
- uses: docker/setup-buildx-action@v3
if: steps.check.outputs.exists == 'true'
- uses: docker/login-action@v3
if: steps.check.outputs.exists == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
if: steps.check.outputs.exists == 'true'
with:
context: .
file: Dockerfile.github-app
load: true
push: false
tags: foxguard-github-app:release-smoke
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke test GitHub App container
if: steps.check.outputs.exists == 'true'
run: scripts/smoke-github-app-image.sh foxguard-github-app:release-smoke
- uses: docker/build-push-action@v6
if: steps.check.outputs.exists == 'true'
with:
context: .
file: Dockerfile.github-app
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/0sec-labs/foxguard-github-app:${{ github.ref_name }}
ghcr.io/0sec-labs/foxguard-github-app:latest
cache-from: type=gha
cache-to: type=gha,mode=max