name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
quality:
uses: ./.github/workflows/ci.yml
build:
name: Build ${{ matrix.target }}
needs: quality
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
asset_name: winx-linux-amd64.tar.gz
bundle: true
- os: macos-26
target: aarch64-apple-darwin
asset_name: winx-macos-arm64.tar.gz
bundle: true
- os: windows-latest
target: x86_64-pc-windows-msvc
asset_name: winx-windows-amd64.exe
bundle: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Setup Rust target
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Rust cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 with:
key: ${{ matrix.target }}-release
- name: Build Unix release bundle
if: matrix.bundle
run: cargo build --release --locked --target ${{ matrix.target }} --bins
- name: Build Windows standalone binary
if: ${{ !matrix.bundle }}
run: cargo build --release --locked --target ${{ matrix.target }} --bin winx-code-agent
- name: Stage Unix release bundle
if: matrix.bundle
shell: bash
run: |
set -euo pipefail
mkdir -p dist/stage
release_dir="target/${{ matrix.target }}/release"
cp "$release_dir/winx-code-agent" "$release_dir/winxd" "$release_dir/winx-guardian" dist/stage/
tar -C dist/stage -czf "dist/${{ matrix.asset_name }}" winx-code-agent winxd winx-guardian
- name: Stage Windows standalone binary
if: ${{ !matrix.bundle }}
shell: bash
run: |
set -euo pipefail
mkdir -p dist
cp "target/${{ matrix.target }}/release/winx-code-agent.exe" "dist/${{ matrix.asset_name }}"
- name: Generate artifact checksum
shell: bash
run: |
set -euo pipefail
cd dist
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${{ matrix.asset_name }}" > "${{ matrix.asset_name }}.sha256"
else
shasum -a 256 "${{ matrix.asset_name }}" > "${{ matrix.asset_name }}.sha256"
fi
- name: Attest build artifact provenance
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d with:
subject-path: dist/${{ matrix.asset_name }}
- name: Upload release artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: release-${{ matrix.target }}
path: |
dist/${{ matrix.asset_name }}
dist/${{ matrix.asset_name }}.sha256
if-no-files-found: error
retention-days: 5
sbom:
name: Generate CycloneDX SBOM
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Setup Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with:
toolchain: stable
- name: Rust cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 with:
key: sbom-tool
cache-bin: true
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx --locked --version 0.5.9
- name: Generate reproducible JSON SBOM
shell: bash
run: |
set -euo pipefail
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
cargo cyclonedx --format json --all-features
mkdir -p dist
sbom="$(find . -maxdepth 2 -type f \( -name '*.cdx.json' -o -name 'bom.json' \) -print -quit)"
test -n "$sbom"
# actions/attest requires bomFormat + specVersion + serialNumber, but
# cargo-cyclonedx omits serialNumber. Inject a deterministic UUIDv5
# derived from repo+commit so the SBOM stays reproducible.
serial="urn:uuid:$(uuidgen --sha1 --namespace @url --name "https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}")"
jq --arg serial "$serial" '.serialNumber = $serial' "$sbom" > dist/winx-code-agent.cdx.json
(cd dist && sha256sum winx-code-agent.cdx.json > winx-code-agent.cdx.json.sha256)
- name: Upload SBOM
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with:
name: release-sbom
path: |
dist/winx-code-agent.cdx.json
dist/winx-code-agent.cdx.json.sha256
if-no-files-found: error
retention-days: 5
publish:
name: Publish to crates.io
needs: [build, sbom]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Setup Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c with:
toolchain: stable
- name: Rust cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 with:
key: publish
- name: Verify tag matches Cargo.toml
shell: bash
run: |
set -euo pipefail
cargo_version="$(awk -F '"' '/^version = / { print $2; exit }' Cargo.toml)"
tag_version="${GITHUB_REF#refs/tags/v}"
if [[ "$cargo_version" != "$tag_version" ]]; then
echo "::error::Cargo.toml version ($cargo_version) != tag ($tag_version)"
exit 1
fi
echo "version=$cargo_version" >> "$GITHUB_ENV"
- name: Skip if version already exists on crates.io
id: check
shell: bash
run: |
status="$(curl -sf -A 'winx-ci/1.0' \
"https://crates.io/api/v1/crates/winx-code-agent/${{ env.version }}" \
-o /dev/null -w '%{http_code}' || true)"
if [[ "$status" == "200" ]]; then
echo "winx-code-agent ${{ env.version }} already exists on crates.io — skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Cargo publish
if: steps.check.outputs.skip != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: cargo publish --locked
release:
name: Publish GitHub Release
needs: [build, sbom, publish]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Download all release artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with:
path: dist
merge-multiple: true
- name: Verify individual checksums and generate aggregate checksum
shell: bash
run: |
set -euo pipefail
cd dist
for checksum in *.sha256; do
sha256sum --check "$checksum"
done
sha256sum winx-*.tar.gz winx-*.exe winx-code-agent.cdx.json > SHA256SUMS
- name: Attest release artifacts with their SBOM
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d with:
subject-path: |
dist/winx-*.tar.gz
dist/winx-*.exe
sbom-path: dist/winx-code-agent.cdx.json
- name: Publish release
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
generate_release_notes: true
fail_on_unmatched_files: true
files: dist/*