name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
permissions:
contents: write
id-token: write
jobs:
validate-release-metadata:
name: Validate release metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Validate release metadata (tag)
if: startsWith(github.ref, 'refs/tags/v')
run: |
python3 .github/scripts/validate_release_metadata.py \
--mode release_tag \
--tag "${GITHUB_REF_NAME}"
- name: Validate release metadata (manual dispatch)
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: |
python3 .github/scripts/validate_release_metadata.py \
--mode push_main
build-release:
name: Build ${{ matrix.platform }}
needs: validate-release-metadata
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x64-gnu
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
cross: false
archive: tar.gz
binary: x0xd
- platform: linux-x64-musl
os: ubuntu-latest
target: x86_64-unknown-linux-musl
cross: true
archive: tar.gz
binary: x0xd
- platform: linux-arm64-gnu
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
cross: true
archive: tar.gz
binary: x0xd
- platform: macos-x64
os: macos-14
target: x86_64-apple-darwin
cross: false
archive: tar.gz
binary: x0xd
- platform: macos-arm64
os: macos-14
target: aarch64-apple-darwin
cross: false
archive: tar.gz
binary: x0xd
- platform: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
cross: false
archive: zip
binary: x0xd.exe
steps:
- uses: actions/checkout@v5
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo registry
uses: actions/cache@v5
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-${{ matrix.target }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v5
with:
path: ~/.cargo/git
key: ${{ runner.os }}-${{ matrix.target }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
- name: Cache target directory
uses: actions/cache@v5
with:
path: target
key: ${{ runner.os }}-${{ matrix.target }}-target-release-${{ hashFiles('**/Cargo.lock') }}
- name: Install cross
if: matrix.cross
run: cargo install cross --locked
- name: Build (native)
if: ${{ !matrix.cross }}
run: cargo build --release --target ${{ matrix.target }} --bin x0xd --bin x0x
- name: Build (cross)
if: matrix.cross
run: cross build --release --target ${{ matrix.target }} --bin x0xd --bin x0x
- name: Import macOS signing certificate
if: startsWith(matrix.platform, 'macos')
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo "$MACOS_CERTIFICATE" | base64 --decode -o "$CERTIFICATE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
- name: Sign macOS binaries
if: startsWith(matrix.platform, 'macos')
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
for bin in x0xd x0x; do
BINARY="target/${{ matrix.target }}/release/${bin}"
if [ -f "$BINARY" ]; then
codesign --force --options runtime --sign "$MACOS_SIGNING_IDENTITY" --timestamp "$BINARY"
codesign --verify --deep --strict "$BINARY"
echo "Signed ${bin} successfully"
fi
done
- name: Notarize macOS binaries
if: startsWith(matrix.platform, 'macos')
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
run: |
for bin in x0xd x0x; do
BINARY="target/${{ matrix.target }}/release/${bin}"
if [ -f "$BINARY" ]; then
ditto -c -k --keepParent "$BINARY" "$RUNNER_TEMP/notarize-${bin}.zip"
xcrun notarytool submit "$RUNNER_TEMP/notarize-${bin}.zip" \
--apple-id "$MACOS_NOTARIZATION_APPLE_ID" \
--password "$MACOS_NOTARIZATION_PASSWORD" \
--team-id "$MACOS_NOTARIZATION_TEAM_ID" \
--wait
echo "Notarized ${bin} successfully"
fi
done
- name: Clean up macOS keychain
if: startsWith(matrix.platform, 'macos') && always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true
- name: Strip binaries (Linux native)
if: matrix.platform == 'linux-x64-gnu'
run: |
strip target/${{ matrix.target }}/release/${{ matrix.binary }}
strip target/${{ matrix.target }}/release/x0xd
strip target/${{ matrix.target }}/release/x0x
- name: Package (tar.gz)
if: matrix.archive == 'tar.gz'
run: |
STAGING="x0x-${{ matrix.platform }}"
mkdir -p "$STAGING"
# Include x0xd daemon and x0x CLI binaries
for bin in x0xd x0x; do
BIN="target/${{ matrix.target }}/release/${bin}"
if [ -f "$BIN" ]; then cp "$BIN" "$STAGING/"; fi
done
cp LICENSE* "$STAGING/" 2>/dev/null || true
cp README.md "$STAGING/" 2>/dev/null || true
tar czf "x0x-${{ matrix.platform }}.tar.gz" "$STAGING"
echo "ASSET=x0x-${{ matrix.platform }}.tar.gz" >> $GITHUB_ENV
- name: Package (zip)
if: matrix.archive == 'zip'
shell: pwsh
run: |
$staging = "x0x-${{ matrix.platform }}"
New-Item -ItemType Directory -Path $staging -Force
# Include x0xd daemon and x0x CLI binaries
$x0xd = "target/${{ matrix.target }}/release/x0xd.exe"
if (Test-Path $x0xd) { Copy-Item $x0xd -Destination $staging }
$x0x = "target/${{ matrix.target }}/release/x0x.exe"
if (Test-Path $x0x) { Copy-Item $x0x -Destination $staging }
if (Test-Path "LICENSE*") { Copy-Item LICENSE* -Destination $staging }
if (Test-Path "README.md") { Copy-Item README.md -Destination $staging }
Compress-Archive -Path "$staging/*" -DestinationPath "x0x-${{ matrix.platform }}.zip"
"ASSET=x0x-${{ matrix.platform }}.zip" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Generate SHA-256 checksum
shell: bash
run: |
# Use sha256sum (Linux/Windows) or shasum (macOS)
hash_cmd="sha256sum"
if ! command -v sha256sum &>/dev/null; then
hash_cmd="shasum -a 256"
fi
if [ "${{ matrix.archive }}" = "zip" ]; then
$hash_cmd "x0x-${{ matrix.platform }}.zip" > "x0x-${{ matrix.platform }}.zip.sha256"
else
$hash_cmd "x0x-${{ matrix.platform }}.tar.gz" > "x0x-${{ matrix.platform }}.tar.gz.sha256"
fi
- name: Upload release artifacts
uses: actions/upload-artifact@v5
with:
name: release-${{ matrix.platform }}
path: |
x0x-${{ matrix.platform }}.*
if-no-files-found: error
sign-release:
name: ML-DSA-65 Sign Archives
needs: build-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-sign-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build x0x-keygen
run: cargo build --release --bin x0x-keygen
- name: Download all artifacts
uses: actions/download-artifact@v5
with:
path: artifacts
pattern: release-*
- name: Write ML-DSA-65 signing key
env:
ML_DSA_SECRET_KEY: ${{ secrets.ML_DSA_SECRET_KEY }}
run: |
echo "$ML_DSA_SECRET_KEY" | base64 --decode > /tmp/release-signing.secret
chmod 600 /tmp/release-signing.secret
- name: Sign archives with ML-DSA-65
run: |
for archive in artifacts/release-*/*.tar.gz artifacts/release-*/*.zip; do
[ -f "$archive" ] || continue
./target/release/x0x-keygen sign \
--key /tmp/release-signing.secret \
--input "$archive" \
--output "${archive}.sig" \
--context "x0x-release-v1"
echo "Signed: $archive"
done
- name: Generate and sign release manifest
run: |
# Collect all archives into a flat directory for manifest generation
mkdir -p manifest-assets
for archive in artifacts/release-*/*.tar.gz artifacts/release-*/*.zip; do
[ -f "$archive" ] || continue
cp "$archive" manifest-assets/
done
# Strip 'v' prefix from tag for version string
VERSION="${GITHUB_REF_NAME#v}"
# Validate that VERSION is a valid semver string (prevents broken manifests from workflow_dispatch)
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "ERROR: '$VERSION' is not a valid semver version (expected tag like v1.2.3)"
exit 1
fi
./target/release/x0x-keygen manifest \
--version "$VERSION" \
--assets-dir manifest-assets/ \
--skill-path SKILL.md \
--key /tmp/release-signing.secret \
--output-dir manifest-assets/
- name: Clean up signing key
if: always()
run: rm -f /tmp/release-signing.secret
- name: Upload signed artifacts
uses: actions/upload-artifact@v5
with:
name: ml-dsa-signatures
path: artifacts/release-*/*.sig
if-no-files-found: error
- name: Upload release manifest
uses: actions/upload-artifact@v5
with:
name: release-manifest
path: |
manifest-assets/release-manifest.json
manifest-assets/release-manifest.json.sig
if-no-files-found: error
create-release:
name: Create GitHub Release
needs: [build-release, sign-release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- name: Download build artifacts
uses: actions/download-artifact@v5
with:
path: artifacts
pattern: release-*
- name: Download ML-DSA-65 signatures
uses: actions/download-artifact@v5
with:
name: ml-dsa-signatures
path: artifacts
- name: Download release manifest
uses: actions/download-artifact@v5
with:
name: release-manifest
path: artifacts/manifest
- name: Import GPG key
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_PRIVATE_KEY" | gpg --import --batch --yes
- name: Sign SKILL.md
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
gpg --batch --pinentry-mode loopback --passphrase-fd 0 \
--detach-sign --armor --output SKILL.md.sig SKILL.md <<< "$GPG_PASSPHRASE"
gpg --verify SKILL.md.sig SKILL.md
- name: Export public key
run: |
gpg --armor --export david@saorsalabs.com > SAORSA_PUBLIC_KEY.asc
if [ ! -s SAORSA_PUBLIC_KEY.asc ]; then
echo "Error: Failed to export public key"
exit 1
fi
- name: GPG-sign release archives
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
for archive in artifacts/release-*/*.tar.gz artifacts/release-*/*.zip; do
[ -f "$archive" ] || continue
gpg --batch --pinentry-mode loopback --passphrase-fd 0 \
--detach-sign --armor --output "${archive}.asc" "$archive" <<< "$GPG_PASSPHRASE"
done
- name: Collect all release files
run: |
mkdir -p release-files
# Copy archives, checksums, GPG signatures, and ML-DSA-65 signatures
for dir in artifacts/release-*/; do
cp "$dir"/*.tar.gz release-files/ 2>/dev/null || true
cp "$dir"/*.zip release-files/ 2>/dev/null || true
cp "$dir"/*.sha256 release-files/ 2>/dev/null || true
cp "$dir"/*.asc release-files/ 2>/dev/null || true
cp "$dir"/*.sig release-files/ 2>/dev/null || true
done
# Copy release manifest
cp artifacts/manifest/release-manifest.json release-files/ 2>/dev/null || true
cp artifacts/manifest/release-manifest.json.sig release-files/ 2>/dev/null || true
# Copy SKILL.md artifacts
cp SKILL.md release-files/
cp SKILL.md.sig release-files/
cp SAORSA_PUBLIC_KEY.asc release-files/
cp .well-known/agent.json release-files/ 2>/dev/null || true
ls -la release-files/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-files/*
draft: false
generate_release_notes: true
body: |
## x0x ${{ github.ref_name }}
**Agent-to-Agent Secure Communication Network**
Post-quantum encrypted P2P gossip network for AI agents. Enable agents to discover each other, communicate securely, and collaborate on shared task lists without central servers.
### Binaries Included
- **x0xd** — Agent daemon with REST API (default: `127.0.0.1:12700`)
### Installation
**Rust:**
```bash
cargo add x0x
```
**Other languages:** x0x is daemon-only — run `x0xd` and consume the local REST/WebSocket API from any language. See [`docs/local-apps.md`](docs/local-apps.md) for examples.
### Platform Binaries
| Platform | File | Code Signed |
|----------|------|-------------|
| Linux x64 (glibc) | `x0x-linux-x64-gnu.tar.gz` | ML-DSA-65 + GPG |
| Linux x64 (musl/static) | `x0x-linux-x64-musl.tar.gz` | ML-DSA-65 + GPG |
| Linux ARM64 | `x0x-linux-arm64-gnu.tar.gz` | ML-DSA-65 + GPG |
| macOS x64 (Intel) | `x0x-macos-x64.tar.gz` | ML-DSA-65 + Apple + GPG |
| macOS ARM64 (Apple Silicon) | `x0x-macos-arm64.tar.gz` | ML-DSA-65 + Apple + GPG |
| Windows x64 | `x0x-windows-x64.zip` | ML-DSA-65 + GPG |
All binaries are signed with ML-DSA-65 (post-quantum, `.sig` files) and GPG (`.asc` files). macOS binaries are additionally signed with Apple Developer ID and notarized.
### Verify GPG Signatures
```bash
# Import Saorsa Labs public key
gpg --import SAORSA_PUBLIC_KEY.asc
# Verify any archive
gpg --verify x0x-linux-x64-gnu.tar.gz.asc x0x-linux-x64-gnu.tar.gz
# Verify SKILL.md
gpg --verify SKILL.md.sig SKILL.md
```
### Verify SHA-256 Checksums
```bash
sha256sum -c x0x-linux-x64-gnu.tar.gz.sha256
```
### SKILL.md (GPG-Signed)
This release includes the GPG-signed SKILL.md file for Anthropic Agent Skills.
### A2A Agent Card
The `agent.json` file provides Agent-to-Agent (A2A) discovery metadata compatible with Google's A2A spec.
### Bootstrap Nodes
- NYC, US: `quic://142.93.199.50:5483`
- SFO, US: `quic://147.182.234.192:5483`
- Helsinki, FI: `quic://65.21.157.229:5483`
- Nuremberg, DE: `quic://116.203.101.172:5483`
- Singapore, SG: `quic://149.28.156.231:5483`
- Tokyo, JP: `quic://45.77.176.184:5483`
---
**x0x** - A gift to the AI agent ecosystem from [Saorsa Labs](https://saorsalabs.com).
No winners, no losers - just cooperation.
publish-crates:
name: Publish to crates.io
needs: create-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- name: Resolve crate version
id: crate
run: |
version=$(grep -m1 '^version =' Cargo.toml | sed -E 's/^version *= *"([^"]+)".*/\1/')
if [ -z "$version" ]; then
echo "::error::could not parse [package].version from Cargo.toml"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "x0x crate version: $version"
- name: Publish x0x crate
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set +e
output=$(cargo publish -p x0x 2>&1)
status=$?
echo "$output"
if [ $status -eq 0 ]; then
echo "::notice::x0x ${{ steps.crate.outputs.version }} uploaded to crates.io"
exit 0
fi
if echo "$output" | grep -qiE "already (uploaded|exists)"; then
echo "::notice::x0x ${{ steps.crate.outputs.version }} already on crates.io — treating as success"
exit 0
fi
echo "::error::cargo publish failed (exit $status); see log above"
exit $status
- name: Verify version is indexed on crates.io
run: |
# crates.io API rejects requests without a User-Agent (HTTP 403),
# per https://crates.io/policies#api. v0.19.4 and v0.19.5 both
# had this verify-step false-fail while the actual cargo publish
# had already succeeded.
version="${{ steps.crate.outputs.version }}"
ua="x0x-release-ci/${version} (release@saorsalabs.com)"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
body=$(curl -fsSL --retry 2 -A "$ua" \
"https://crates.io/api/v1/crates/x0x" 2>/dev/null || echo "")
if [ -n "$body" ]; then
indexed=$(echo "$body" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("crate",{}).get("max_version",""))' \
2>/dev/null || echo "")
else
indexed=""
fi
if [ "$indexed" = "$version" ]; then
echo "::notice::x0x $version is the indexed max_version on crates.io"
exit 0
fi
echo "attempt $attempt: indexed max_version='$indexed', want='$version'; retrying in 30s"
sleep 30
done
echo "::error::crates.io did not index x0x $version within ~5 min (last seen max_version='$indexed')"
exit 1