chore(release): v3.31.0 (#1755) #231
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # .github/workflows/release.yml | |
| # Cross-platform binary releases for Assay | |
| # | |
| # Triggers on: | |
| # - Push to tags matching v* | |
| # - Manual dispatch | |
| # | |
| # Builds for: | |
| # - Linux x86_64 (GNU) | |
| # - Linux aarch64 (GNU) | |
| # - macOS x86_64 (Intel, cross from ARM) | |
| # - macOS aarch64 (Apple Silicon) | |
| # - Windows x86_64 | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version tag (e.g., v1.1.0)' | |
| required: true | |
| type: string | |
| verify_lsm: | |
| description: 'Run self-hosted LSM enforcement gate' | |
| required: false | |
| default: false | |
| type: boolean | |
| # Default deny; each job declares minimal permissions. | |
| permissions: {} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_NET_RETRY: 5 | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| # ============================================================ | |
| # Build matrix for all platforms | |
| # ============================================================ | |
| build: | |
| name: Build ${{ matrix.target }} | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux x86_64 | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| artifact: assay | |
| archive: tar.gz | |
| # Linux aarch64 (cross-compile) | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| artifact: assay | |
| archive: tar.gz | |
| cross: true | |
| # macOS x86_64 (cross-compile on ARM runner) | |
| - os: macos-15 | |
| target: x86_64-apple-darwin | |
| artifact: assay | |
| archive: tar.gz | |
| # macOS aarch64 (Apple Silicon) | |
| - os: macos-15 | |
| target: aarch64-apple-darwin | |
| artifact: assay | |
| archive: tar.gz | |
| # Windows x86_64 | |
| - os: windows-2025 | |
| target: x86_64-pc-windows-msvc | |
| artifact: assay.exe | |
| archive: zip | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Ensure Rust target is installed | |
| shell: bash | |
| run: rustup target add "${{ matrix.target }}" | |
| - name: Install cross-compilation tools | |
| if: matrix.cross | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Ensure python3 is available (for Deb822 rewrite) and update strictly for current arch first | |
| command -v python3 >/dev/null 2>&1 || { | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get update -y | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3 | |
| } | |
| sudo dpkg --add-architecture arm64 | |
| . /etc/os-release | |
| CODENAME="${VERSION_CODENAME:-noble}" | |
| # 1) Force amd64-only for default Ubuntu sources so apt doesn't try arm64 on archive/security | |
| if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then | |
| sudo python3 scripts/ci/fix_apt_sources.py | |
| fi | |
| if [ -f /etc/apt/sources.list ]; then | |
| # If a line already has an arch bracket, leave it alone. | |
| # Otherwise, force [arch=amd64] for any ubuntu.com/ubuntu repo (incl azure.archive.ubuntu.com). | |
| sudo sed -i -E ' | |
| /^deb(-src)?[[:space:]]+\[.*\][[:space:]]+/b | |
| s|^(deb(-src)?[[:space:]]+)(http(s)?://[^[:space:]]*ubuntu\.com/ubuntu)|\1[arch=amd64] \3| | |
| ' /etc/apt/sources.list | |
| fi | |
| # 2) Add arm64-only ubuntu-ports repo (arm64 packages live here) | |
| sudo tee /etc/apt/sources.list.d/ubuntu-ports-arm64.list >/dev/null <<EOF | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME} main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-updates main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-security main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-backports main universe restricted multiverse | |
| EOF | |
| # 3) Rewrite any */ubuntu-ports to the chosen mirror + robust update | |
| sudo bash scripts/ci/apt_ports_failover.sh | |
| # Toolchain + sysroot (no arm64 libs needed - using bundled sqlite) | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ | |
| gcc-aarch64-linux-gnu \ | |
| libc6-dev-arm64-cross \ | |
| linux-libc-dev-arm64-cross \ | |
| pkg-config | |
| { | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" | |
| echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" | |
| echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" | |
| } >> "$GITHUB_ENV" | |
| - name: Build release binary | |
| run: cargo build --release --target ${{ matrix.target }} --package assay-cli | |
| - name: Get version | |
| id: version | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_VERSION_INPUT: ${{ github.event.inputs.version }} | |
| run: | | |
| if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| V="${RELEASE_VERSION_INPUT}" | |
| else | |
| V="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ "$V" == *$'\n'* || "$V" == *$'\r'* ]]; then | |
| echo "::error::Release version must be a single-line value." | |
| exit 1 | |
| fi | |
| printf 'version=%s\n' "$V" >> "$GITHUB_OUTPUT" | |
| - name: Package (Unix) | |
| if: matrix.archive == 'tar.gz' | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| ARCHIVE_NAME="assay-${VERSION}-${{ matrix.target }}" | |
| mkdir -p "dist/${ARCHIVE_NAME}" | |
| cp "target/${{ matrix.target }}/release/${{ matrix.artifact }}" "dist/${ARCHIVE_NAME}/" | |
| cp README.md LICENSE "dist/${ARCHIVE_NAME}/" 2>/dev/null || true | |
| cd dist | |
| tar -czvf "${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}" | |
| shasum -a 256 "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256" | |
| - name: Package (Windows) | |
| if: matrix.archive == 'zip' | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| $VERSION = $env:VERSION | |
| $ARCHIVE_NAME = "assay-${VERSION}-${{ matrix.target }}" | |
| New-Item -ItemType Directory -Force -Path "dist\${ARCHIVE_NAME}" | |
| Copy-Item "target\${{ matrix.target }}\release\${{ matrix.artifact }}" "dist\${ARCHIVE_NAME}\" | |
| Copy-Item README.md, LICENSE "dist\${ARCHIVE_NAME}\" -ErrorAction SilentlyContinue | |
| Compress-Archive -Path "dist\${ARCHIVE_NAME}" -DestinationPath "dist\${ARCHIVE_NAME}.zip" | |
| $hash = (Get-FileHash "dist\${ARCHIVE_NAME}.zip" -Algorithm SHA256).Hash.ToLower() | |
| "${hash} ${ARCHIVE_NAME}.zip" | Out-File -Encoding ASCII "dist\${ARCHIVE_NAME}.zip.sha256" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: assay-${{ matrix.target }} | |
| path: dist/assay-* | |
| retention-days: 7 | |
| build-mcp-server-linux: | |
| name: Build assay-mcp-server ${{ matrix.target }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: x86_64-unknown-linux-gnu | |
| - target: aarch64-unknown-linux-gnu | |
| cross: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Ensure Rust target is installed | |
| shell: bash | |
| run: rustup target add "${{ matrix.target }}" | |
| - name: Install cross-compilation tools | |
| if: matrix.cross | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| command -v python3 >/dev/null 2>&1 || { | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get update -y | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3 | |
| } | |
| sudo dpkg --add-architecture arm64 | |
| . /etc/os-release | |
| CODENAME="${VERSION_CODENAME:-noble}" | |
| if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then | |
| sudo python3 scripts/ci/fix_apt_sources.py | |
| fi | |
| if [ -f /etc/apt/sources.list ]; then | |
| sudo sed -i -E ' | |
| /^deb(-src)?[[:space:]]+\[.*\][[:space:]]+/b | |
| s|^(deb(-src)?[[:space:]]+)(http(s)?://[^[:space:]]*ubuntu\.com/ubuntu)|\1[arch=amd64] \3| | |
| ' /etc/apt/sources.list | |
| fi | |
| sudo tee /etc/apt/sources.list.d/ubuntu-ports-arm64.list >/dev/null <<EOF | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME} main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-updates main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-security main universe restricted multiverse | |
| deb [arch=arm64] https://mirror.gofoss.xyz/ubuntu-ports ${CODENAME}-backports main universe restricted multiverse | |
| EOF | |
| sudo bash scripts/ci/apt_ports_failover.sh | |
| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ | |
| gcc-aarch64-linux-gnu \ | |
| libc6-dev-arm64-cross \ | |
| linux-libc-dev-arm64-cross \ | |
| pkg-config | |
| { | |
| echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" | |
| echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" | |
| echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" | |
| } >> "$GITHUB_ENV" | |
| - name: Build assay-mcp-server binary | |
| run: cargo build --release --target ${{ matrix.target }} --package assay-mcp-server | |
| - name: Get version | |
| id: version | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_VERSION_INPUT: ${{ github.event.inputs.version }} | |
| run: | | |
| if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| V="${RELEASE_VERSION_INPUT}" | |
| else | |
| V="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ "$V" == *$'\n'* || "$V" == *$'\r'* ]]; then | |
| echo "::error::Release version must be a single-line value." | |
| exit 1 | |
| fi | |
| printf 'version=%s\n' "$V" >> "$GITHUB_OUTPUT" | |
| - name: Package assay-mcp-server | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| ARCHIVE_NAME="assay-mcp-server-${VERSION}-${{ matrix.target }}" | |
| mkdir -p "dist/${ARCHIVE_NAME}" | |
| cp "target/${{ matrix.target }}/release/assay-mcp-server" "dist/${ARCHIVE_NAME}/" | |
| cp README.md LICENSE "dist/${ARCHIVE_NAME}/" 2>/dev/null || true | |
| cd dist | |
| tar -czvf "${ARCHIVE_NAME}.tar.gz" "${ARCHIVE_NAME}" | |
| shasum -a 256 "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256" | |
| - name: Upload assay-mcp-server artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: assay-mcp-server-${{ matrix.target }} | |
| path: dist/assay-mcp-server-* | |
| retention-days: 7 | |
| # ============================================================ | |
| # Create GitHub Release | |
| # ============================================================ | |
| release: | |
| name: Create Release | |
| needs: [build, build-mcp-server-linux] | |
| runs-on: ubuntu-latest | |
| environment: release | |
| permissions: | |
| contents: write | |
| actions: read | |
| attestations: write | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Get version | |
| id: version | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| RELEASE_VERSION_INPUT: ${{ github.event.inputs.version }} | |
| run: | | |
| if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then | |
| V="${RELEASE_VERSION_INPUT}" | |
| else | |
| V="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ "$V" == *$'\n'* || "$V" == *$'\r'* ]]; then | |
| echo "::error::Release version must be a single-line value." | |
| exit 1 | |
| fi | |
| printf 'version=%s\n' "$V" >> "$GITHUB_OUTPUT" | |
| - name: Install CycloneDX generator | |
| shell: bash | |
| run: cargo install cargo-cyclonedx --version 0.5.9 --locked | |
| - name: Generate CycloneDX SBOM bundle | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| cargo cyclonedx \ | |
| --format json \ | |
| --spec-version 1.5 \ | |
| --target all \ | |
| --override-filename assay-sbom \ | |
| --manifest-path Cargo.toml | |
| mkdir -p release/sbom | |
| while IFS= read -r file; do | |
| cp --parents "$file" release/sbom/ | |
| done < <(find . -name 'assay-sbom.json' -print) | |
| tar -czf "release/assay-${VERSION}-sbom-cyclonedx.tar.gz" -C release/sbom . | |
| shasum -a 256 "release/assay-${VERSION}-sbom-cyclonedx.tar.gz" > "release/assay-${VERSION}-sbom-cyclonedx.tar.gz.sha256" | |
| rm -rf release/sbom | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts | |
| - name: Prepare release assets | |
| run: | | |
| mkdir -p release | |
| find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.sha256" \) -exec cp {} release/ \; | |
| ls -la release/ | |
| - name: Build assay-mcp-server MCPB | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/build_mcpb_bundle.sh \ | |
| --version-tag "${VERSION}" \ | |
| --linux-x86-archive "release/assay-mcp-server-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \ | |
| --linux-arm64-archive "release/assay-mcp-server-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \ | |
| --output "release/assay-mcp-server-${VERSION}-linux.mcpb" | |
| - name: Render generated registry metadata | |
| shell: bash | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| MCPB_FILE="release/assay-mcp-server-${VERSION}-linux.mcpb" | |
| MCPB_SHA="$(cut -d' ' -f1 "${MCPB_FILE}.sha256")" | |
| MCPB_URL="https://github.com/${REPO}/releases/download/${VERSION}/$(basename "${MCPB_FILE}")" | |
| bash scripts/ci/render_registry_server_json.sh \ | |
| --version-tag "${VERSION}" \ | |
| --mcpb-url "${MCPB_URL}" \ | |
| --file-sha256 "${MCPB_SHA}" \ | |
| --output "release/server.json" | |
| - name: ADR-025 enforce readiness (fail-closed) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p input/readiness | |
| rid="$(gh run list --workflow "adr025-nightly-readiness.yml" --branch main --status success --limit 1 --json databaseId --jq '.[0].databaseId')" | |
| if [ -z "${rid}" ] || [ "${rid}" = "null" ]; then | |
| echo "No successful adr025-nightly-readiness run found" | |
| exit 2 | |
| fi | |
| echo "Using readiness run id: ${rid}" | |
| gh run download "${rid}" -n "adr025-nightly-readiness" -D input/readiness | |
| test -f input/readiness/nightly_readiness.json | |
| bash scripts/ci/adr025-soak-enforce.sh \ | |
| --policy schemas/soak_readiness_policy_v1.json \ | |
| --readiness input/readiness/nightly_readiness.json | |
| - name: ADR-025 closure release integration | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| MODE: ${{ vars.ASSAY_CLOSURE_GATE || 'attach' }} | |
| POLICY: schemas/closure_release_policy_v1.json | |
| OUT_DIR: artifacts/adr025-closure | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/adr025-closure-release.sh | |
| - name: Upload ADR-025 closure release evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: adr025-closure-release-evidence | |
| path: artifacts/adr025-closure/* | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: ADR-025 OTel release integration | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| MODE: ${{ vars.ASSAY_OTEL_GATE || 'attach' }} | |
| POLICY: schemas/otel_release_policy_v1.json | |
| OUT_DIR: artifacts/adr025-otel | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/adr025-otel-release.sh | |
| - name: Upload ADR-025 OTel release evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: adr025-otel-bridge-release-evidence | |
| path: artifacts/adr025-otel/* | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Generate build provenance attestations | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: release/* | |
| - name: Enforce release attestation policy | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ASSETS_DIR: release | |
| OUT_SUMMARY: release/assay-${{ steps.version.outputs.version }}-release-provenance.json | |
| OUT_RAW_DIR: artifacts/release-provenance/raw | |
| REPO: ${{ github.repository }} | |
| SIGNER_WORKFLOW: ${{ github.repository }}/.github/workflows/release.yml | |
| SOURCE_REF: ${{ github.ref }} | |
| SOURCE_DIGEST: ${{ github.sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/release_attestation_enforce.sh | |
| shasum -a 256 "${OUT_SUMMARY}" > "${OUT_SUMMARY}.sha256" | |
| - name: Build release proof kit | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ASSETS_DIR: release | |
| PROVENANCE_SUMMARY: release/assay-${{ steps.version.outputs.version }}-release-provenance.json | |
| PROVENANCE_SUMMARY_SHA256: release/assay-${{ steps.version.outputs.version }}-release-provenance.json.sha256 | |
| OUT_ARCHIVE: release/assay-${{ steps.version.outputs.version }}-release-proof-kit.tar.gz | |
| VERSION: ${{ steps.version.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/release_proof_kit_build.sh | |
| shasum -a 256 "${OUT_ARCHIVE}" > "${OUT_ARCHIVE}.sha256" | |
| - name: Check release asset preflight | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| ASSETS_DIR: release | |
| REPO: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/check-release-assets.sh | |
| - name: Upload release provenance evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-provenance-evidence | |
| path: artifacts/release-provenance/ | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Generate release notes | |
| id: notes | |
| env: | |
| REPO: ${{ github.repository }} | |
| VERSION: ${{ steps.version.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cat > release_notes.md << 'EOF' | |
| ## Assay __VERSION__ | |
| Deterministic policy enforcement, canonical evidence, and reviewable trust artifacts for AI agent systems. | |
| ### Installation | |
| **One-liner (Linux/macOS):** | |
| ```bash | |
| curl -fsSL https://getassay.dev/install.sh | sh | |
| ``` | |
| **GitHub Action:** | |
| ```yaml | |
| - uses: Rul1an/assay-action@v3 | |
| with: | |
| bundles: ".assay/evidence/*.tar.gz" | |
| fail_on: error | |
| ``` | |
| **Manual download:** | |
| Download the appropriate archive for your platform below. | |
| ### Checksums | |
| Verify your download with the `.sha256` files. | |
| ### Release Notes | |
| See [CHANGELOG.md](__CHANGELOG_URL__) for line-specific shipped features and trust-compiler scope. | |
| ### Changelog | |
| See [CHANGELOG.md](__CHANGELOG_URL__) for details. | |
| EOF | |
| python3 - <<'PY' | |
| import os | |
| from pathlib import Path | |
| notes = Path("release_notes.md") | |
| text = notes.read_text() | |
| text = text.replace("__VERSION__", os.environ["VERSION"]) | |
| text = text.replace( | |
| "__CHANGELOG_URL__", | |
| f"https://github.com/{os.environ['REPO']}/blob/main/CHANGELOG.md", | |
| ) | |
| notes.write_text(text) | |
| PY | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.version.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| assets=() | |
| while IFS= read -r -d '' asset; do | |
| assets+=("$asset") | |
| done < <(find release -maxdepth 1 -type f -print0 | sort -z) | |
| if [[ ${#assets[@]} -eq 0 ]]; then | |
| echo "::error::No release assets found in release/." | |
| exit 1 | |
| fi | |
| release_args=( | |
| "$VERSION" | |
| "${assets[@]}" | |
| --title "Assay $VERSION" | |
| --target "$GITHUB_SHA" | |
| --notes-file release_notes.md | |
| ) | |
| if [[ "$VERSION" == *-rc* || "$VERSION" == *-beta* ]]; then | |
| release_args+=(--prerelease) | |
| fi | |
| gh release create "${release_args[@]}" | |
| # ============================================================ | |
| # Verify LSM Enforcement (Self-Hosted Gate) | |
| # ============================================================ | |
| verify-lsm-blocking: | |
| name: Verify LSM Enforcement | |
| needs: [build, build-mcp-server-linux] | |
| # Manual check only, to prevent release blocking on runner availability | |
| if: github.event_name == 'workflow_dispatch' && inputs.verify_lsm | |
| runs-on: [self-hosted] | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Verify LSM blocking (CI gate) | |
| run: | | |
| chmod +x scripts/verify_lsm_docker.sh | |
| # Standard Mode + Enforcement: Will FAIL if BPF is missing. | |
| # Pass PATH so root can find cargo | |
| sudo -E env "PATH=$PATH" ./scripts/verify_lsm_docker.sh --enforce-lsm | |
| - name: Fix Runner Permissions (Cleanup) | |
| if: always() | |
| run: | | |
| echo "🧹 Cleaning up root-owned files..." | |
| sudo chown -R "$(whoami)":"$(id -gn)" . || true | |
| - name: Upload verification logs | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: lsm-verification-logs | |
| path: /tmp/assay-lsm-verify/ | |
| # ============================================================ | |
| # Publish to crates.io (optional) | |
| # ============================================================ | |
| publish-crates: | |
| name: Publish to crates.io | |
| needs: release | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc') && !contains(github.ref, '-beta')) || | |
| (github.event_name == 'workflow_dispatch' && startsWith(github.event.inputs.version, 'v') && !contains(github.event.inputs.version, '-rc') && !contains(github.event.inputs.version, '-beta')) | |
| environment: crates | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Debug Cargo Version | |
| run: cargo --version | |
| - name: Check public crate policy | |
| run: bash scripts/ci/check-public-crate-policy.sh | |
| - name: Authenticate with crates.io | |
| id: auth | |
| uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 | |
| - name: Publish Crates (Idempotent) | |
| run: | | |
| chmod +x scripts/ci/publish_idempotent.sh | |
| ./scripts/ci/publish_idempotent.sh | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | |
| # ============================================================ | |
| # Build and Publish Python Wheels (PyPI) | |
| # ============================================================ | |
| wheels: | |
| name: Build Wheels | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| - os: macos-15 | |
| target: x86_64-apple-darwin | |
| - os: macos-15 | |
| target: aarch64-apple-darwin | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.12' | |
| - name: Build wheels | |
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1 | |
| with: | |
| working-directory: assay-python-sdk | |
| target: ${{ matrix.target }} | |
| # Keep the interpreter explicit for all wheel targets. This is | |
| # required inside the manylinux container and harmless on macOS, | |
| # where setup-python provides the same Python version. | |
| args: --release --out dist --locked -i python3.12 --compatibility pypi | |
| sccache: 'false' | |
| manylinux: auto | |
| - name: Upload wheels | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: wheels-${{ matrix.os }}-${{ matrix.target }} | |
| path: assay-python-sdk/dist/*.whl | |
| publish-pypi: | |
| name: Publish to PyPI | |
| needs: [release, wheels] | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc') && !contains(github.ref, '-beta')) || | |
| (github.event_name == 'workflow_dispatch' && startsWith(github.event.inputs.version, 'v') && !contains(github.event.inputs.version, '-rc') && !contains(github.event.inputs.version, '-beta')) | |
| environment: pypi | |
| permissions: | |
| id-token: write # Required for PyPI trusted publishing | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: wheels-* | |
| merge-multiple: true | |
| path: dist | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 | |
| with: | |
| packages-dir: dist | |
| skip-existing: true # Idempotent: skip already-published versions |