fix(agent): harden macos es/ne scaffolding and supervision #264
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
| name: Release | ||
| on: | ||
| push: | ||
| tags: | ||
| - "v*" | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: "Release version (X.Y.Z)" | ||
| required: true | ||
| type: string | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| concurrency: | ||
| group: release-${{ github.ref_name || inputs.version }} | ||
| cancel-in-progress: false | ||
| jobs: | ||
| resolve-version: | ||
| name: Resolve Version | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| version: ${{ steps.resolve.outputs.version }} | ||
| tag: ${{ steps.resolve.outputs.tag }} | ||
| steps: | ||
| - id: resolve | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then | ||
| TAG="${GITHUB_REF_NAME}" | ||
| VERSION="${TAG#v}" | ||
| else | ||
| VERSION="${{ inputs.version }}" | ||
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "Version must be strict semver: X.Y.Z" >&2 | ||
| exit 1 | ||
| fi | ||
| TAG="v${VERSION}" | ||
| fi | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||
| echo "Resolved release version: $VERSION" | ||
| echo "Resolved release tag: $TAG" | ||
| preflight: | ||
| name: Preflight Checks | ||
| runs-on: ubuntu-latest | ||
| needs: resolve-version | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Install protoc | ||
| run: sudo apt-get update && sudo apt-get install -y protobuf-compiler | ||
| - name: Validate version consistency | ||
| run: scripts/release-preflight.sh "${{ needs.resolve-version.outputs.version }}" | ||
| - name: Validate macOS packaging sources are release-capable | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| required=( | ||
| apps/agent/src-tauri/macos/system-extension/entitlements/agent-app.entitlements | ||
| apps/agent/src-tauri/macos/system-extension/entitlements/combined-system-extension.entitlements | ||
| apps/agent/src-tauri/macos/system-extension/plists/agent-packaging-template.plist | ||
| apps/agent/src-tauri/macos/system-extension/plists/combined-system-extension-template.plist | ||
| apps/agent/src-tauri/macos/system-extension/profiles/developer-id-profile-template.plist | ||
| ) | ||
| for path in "${required[@]}"; do | ||
| [[ -f "$path" ]] || { | ||
| echo "missing macOS packaging asset: $path" >&2 | ||
| exit 1 | ||
| } | ||
| done | ||
| if grep -R -nE "__[A-Z0-9_]+__" apps/agent/src-tauri/macos/system-extension; then | ||
| echo "macOS combined-system-extension packaging still contains placeholders; replace them before release." >&2 | ||
| exit 1 | ||
| fi | ||
| if grep -R -n "scaffold_only" apps/agent/src-tauri/macos/system-extension; then | ||
| echo "macOS packaging sources still declare scaffold_only state; release requires concrete source metadata plus a real embedded system extension bundle." >&2 | ||
| exit 1 | ||
| fi | ||
| - name: Run tests | ||
| run: cargo test --workspace | ||
| - name: Run clippy | ||
| run: cargo clippy --all-targets --all-features -- -D warnings | ||
| publish-crates: | ||
| name: Publish crates.io | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, preflight] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Detect missing crate versions | ||
| id: detect | ||
| env: | ||
| VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| crates=(hush-core hush-proxy hush-spine clawdstrike clawdstrike-ocsf hunt-scan hunt-query clawdstrike-policy-event hunt-correlate hush-cli) | ||
| missing=() | ||
| for crate in "${crates[@]}"; do | ||
| code="$(curl -s -o /dev/null -w '%{http_code}' "https://crates.io/api/v1/crates/${crate}/${VERSION}")" | ||
| if [[ "$code" == "200" ]]; then | ||
| echo "${crate}@${VERSION} already exists" | ||
| else | ||
| echo "${crate}@${VERSION} missing" | ||
| missing+=("$crate") | ||
| fi | ||
| done | ||
| if [[ "${#missing[@]}" -eq 0 ]]; then | ||
| echo "none_missing=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "none_missing=false" >> "$GITHUB_OUTPUT" | ||
| echo "missing_crates=${missing[*]}" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Publish missing crates in dependency order | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| env: | ||
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | ||
| VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| publish_with_retry() { | ||
| local crate="$1" | ||
| local max_attempts=20 | ||
| local attempt=1 | ||
| local output | ||
| local status | ||
| while (( attempt <= max_attempts )); do | ||
| echo "Publishing ${crate}@${VERSION} (attempt ${attempt}/${max_attempts})" | ||
| set +e | ||
| output="$(cargo publish -p "$crate" --token "$CARGO_REGISTRY_TOKEN" 2>&1)" | ||
| status=$? | ||
| set -e | ||
| printf '%s\n' "$output" | ||
| if [[ $status -eq 0 ]]; then | ||
| echo "Published ${crate}@${VERSION}" | ||
| return 0 | ||
| fi | ||
| if grep -q "already exists on crates.io index" <<<"$output"; then | ||
| echo "${crate}@${VERSION} already published; skipping" | ||
| return 0 | ||
| fi | ||
| echo "Publish attempt failed for ${crate}; waiting for index propagation" | ||
| sleep 30 | ||
| attempt=$((attempt + 1)) | ||
| done | ||
| echo "Failed to publish ${crate}@${VERSION}" | ||
| exit 1 | ||
| } | ||
| publish_with_retry hush-core | ||
| publish_with_retry hush-proxy | ||
| publish_with_retry hush-spine | ||
| publish_with_retry clawdstrike | ||
| publish_with_retry clawdstrike-ocsf | ||
| publish_with_retry hunt-scan | ||
| publish_with_retry hunt-query | ||
| publish_with_retry clawdstrike-policy-event | ||
| publish_with_retry hunt-correlate | ||
| publish_with_retry hush-cli | ||
| - name: Crates already published | ||
| if: steps.detect.outputs.none_missing == 'true' | ||
| run: echo "All crate versions already exist; skipping crates publish" | ||
| publish-npm: | ||
| name: Publish npm packages | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, preflight] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "24" | ||
| registry-url: "https://registry.npmjs.org" | ||
| - name: Detect missing npm package versions | ||
| id: detect | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| packages=( | ||
| packages/adapters/clawdstrike-adapter-core | ||
| packages/sdk/hush-ts | ||
| packages/sdk/clawdstrike | ||
| packages/policy/clawdstrike-policy | ||
| packages/adapters/clawdstrike-claude | ||
| packages/adapters/clawdstrike-openai | ||
| packages/adapters/clawdstrike-vercel-ai | ||
| packages/adapters/clawdstrike-langchain | ||
| packages/adapters/clawdstrike-openclaw | ||
| packages/adapters/clawdstrike-opencode | ||
| packages/adapters/clawdstrike-hush-cli-engine | ||
| packages/adapters/clawdstrike-hushd-engine | ||
| ) | ||
| missing=() | ||
| for pkg in "${packages[@]}"; do | ||
| name="$(node -e "console.log(require('./${pkg}/package.json').name)")" | ||
| version="$(node -e "console.log(require('./${pkg}/package.json').version)")" | ||
| if npm view "${name}@${version}" version >/dev/null 2>&1; then | ||
| echo "${name}@${version} already exists" | ||
| else | ||
| echo "${name}@${version} missing" | ||
| missing+=("$pkg") | ||
| fi | ||
| done | ||
| if [[ "${#missing[@]}" -eq 0 ]]; then | ||
| echo "none_missing=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "none_missing=false" >> "$GITHUB_OUTPUT" | ||
| echo "missing_packages=${missing[*]}" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Install workspace dependencies | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| run: npm ci --ignore-scripts | ||
| - name: Publish missing npm packages in dependency order | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| RELEASE_VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| publish_workspace() { | ||
| local pkg="$1" | ||
| local name version | ||
| name="$(node -e "console.log(require('./${pkg}/package.json').name)")" | ||
| version="$(node -e "console.log(require('./${pkg}/package.json').version)")" | ||
| if [[ "$version" != "$RELEASE_VERSION" ]]; then | ||
| echo "Version mismatch for $name: package.json=$version release=$RELEASE_VERSION" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "Building ${name}@${version}" | ||
| npm run build -w "$pkg" | ||
| echo "Testing ${name}@${version}" | ||
| npm test -w "$pkg" | ||
| if npm view "${name}@${version}" version >/dev/null 2>&1; then | ||
| echo "${name}@${version} already published; skipping publish" | ||
| return 0 | ||
| fi | ||
| echo "Publishing ${name}@${version}" | ||
| local output status | ||
| set +e | ||
| output="$(npm publish -w "$pkg" --access public --provenance 2>&1)" | ||
| status=$? | ||
| set -e | ||
| printf '%s\n' "$output" | ||
| if [[ $status -eq 0 ]]; then | ||
| return 0 | ||
| fi | ||
| if grep -q "Cannot publish over previously published version" <<<"$output" \ | ||
| || grep -q "You cannot publish over the previously published versions" <<<"$output"; then | ||
| echo "${name}@${version} appears already published; continuing" | ||
| return 0 | ||
| fi | ||
| return $status | ||
| } | ||
| publish_workspace packages/adapters/clawdstrike-adapter-core | ||
| publish_workspace packages/sdk/hush-ts | ||
| publish_workspace packages/sdk/clawdstrike | ||
| publish_workspace packages/policy/clawdstrike-policy | ||
| publish_workspace packages/adapters/clawdstrike-claude | ||
| publish_workspace packages/adapters/clawdstrike-openai | ||
| publish_workspace packages/adapters/clawdstrike-vercel-ai | ||
| publish_workspace packages/adapters/clawdstrike-langchain | ||
| publish_workspace packages/adapters/clawdstrike-openclaw | ||
| publish_workspace packages/adapters/clawdstrike-opencode | ||
| publish_workspace packages/adapters/clawdstrike-hush-cli-engine | ||
| publish_workspace packages/adapters/clawdstrike-hushd-engine | ||
| - name: npm packages already published | ||
| if: steps.detect.outputs.none_missing == 'true' | ||
| run: echo "All npm package versions already exist; skipping npm publish" | ||
| publish-wasm-npm: | ||
| name: Publish @clawdstrike/wasm | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, preflight, publish-npm] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "24" | ||
| registry-url: "https://registry.npmjs.org" | ||
| - name: Detect missing wasm npm version | ||
| id: detect | ||
| env: | ||
| RELEASE_VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| name="$(node -e "console.log(require('./crates/libs/hush-wasm/package.json').name)")" | ||
| version="$(node -e "console.log(require('./crates/libs/hush-wasm/package.json').version)")" | ||
| echo "name=${name}" >> "$GITHUB_OUTPUT" | ||
| echo "version=${version}" >> "$GITHUB_OUTPUT" | ||
| if [[ "$version" != "$RELEASE_VERSION" ]]; then | ||
| echo "Version mismatch for $name: package.json=$version release=$RELEASE_VERSION" >&2 | ||
| exit 1 | ||
| fi | ||
| if npm view "${name}@${version}" version >/dev/null 2>&1; then | ||
| echo "none_missing=true" >> "$GITHUB_OUTPUT" | ||
| echo "${name}@${version} already exists" | ||
| else | ||
| echo "none_missing=false" >> "$GITHUB_OUTPUT" | ||
| echo "${name}@${version} missing" | ||
| fi | ||
| - name: Setup Rust target | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: wasm32-unknown-unknown | ||
| - name: Install wasm-pack | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| run: cargo install wasm-pack --locked --version 0.14.0 | ||
| - name: Build and publish wasm package | ||
| if: steps.detect.outputs.none_missing == 'false' | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| cd crates/libs/hush-wasm | ||
| wasm-pack build --target web --release --out-dir pkg | ||
| cp package.json pkg/ | ||
| cp README.npm.md pkg/README.md | ||
| cp types/hush_wasm.d.ts pkg/ | ||
| cd pkg | ||
| npm publish --access public --provenance | ||
| - name: wasm package already published | ||
| if: steps.detect.outputs.none_missing == 'true' | ||
| run: echo "wasm package version already exists; skipping wasm publish" | ||
| pypi-detect: | ||
| name: Detect PyPI package status | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, preflight] | ||
| outputs: | ||
| name: ${{ steps.detect.outputs.name }} | ||
| version: ${{ steps.detect.outputs.version }} | ||
| none_missing: ${{ steps.detect.outputs.none_missing }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Detect missing PyPI version | ||
| id: detect | ||
| env: | ||
| RELEASE_VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| python3 - <<'PY' | ||
| import tomllib | ||
| from pathlib import Path | ||
| release_version = "${{ needs.resolve-version.outputs.version }}" | ||
| pure = tomllib.loads(Path("packages/sdk/hush-py/pyproject.toml").read_text(encoding="utf-8")) | ||
| native = tomllib.loads(Path("packages/sdk/hush-py/hush-native/pyproject.toml").read_text(encoding="utf-8")) | ||
| pure_name = pure["project"]["name"] | ||
| pure_version = pure["project"]["version"] | ||
| native_name = native["project"]["name"] | ||
| native_version = native["project"]["version"] | ||
| if pure_name != "clawdstrike": | ||
| raise SystemExit(f"Unexpected Python package name in pure pyproject: {pure_name}") | ||
| if native_name != "clawdstrike": | ||
| raise SystemExit(f"Unexpected Python package name in native pyproject: {native_name}") | ||
| if pure_version != release_version: | ||
| raise SystemExit( | ||
| f"Version mismatch for pure pyproject: pyproject={pure_version} release={release_version}" | ||
| ) | ||
| if native_version != release_version: | ||
| raise SystemExit( | ||
| f"Version mismatch for native pyproject: pyproject={native_version} release={release_version}" | ||
| ) | ||
| PY | ||
| pkg_name="$(python3 - <<'PY' | ||
| import tomllib | ||
| with open("packages/sdk/hush-py/pyproject.toml", "rb") as f: | ||
| data = tomllib.load(f) | ||
| print(data["project"]["name"]) | ||
| PY | ||
| )" | ||
| pkg_version="$(python3 - <<'PY' | ||
| import tomllib | ||
| with open("packages/sdk/hush-py/pyproject.toml", "rb") as f: | ||
| data = tomllib.load(f) | ||
| print(data["project"]["version"]) | ||
| PY | ||
| )" | ||
| echo "name=${pkg_name}" >> "$GITHUB_OUTPUT" | ||
| echo "version=${pkg_version}" >> "$GITHUB_OUTPUT" | ||
| code="$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${pkg_name}/${pkg_version}/json")" | ||
| if [[ "$code" == "200" ]]; then | ||
| echo "none_missing=true" >> "$GITHUB_OUTPUT" | ||
| echo "${pkg_name}==${pkg_version} already exists on PyPI" | ||
| else | ||
| echo "none_missing=false" >> "$GITHUB_OUTPUT" | ||
| echo "${pkg_name}==${pkg_version} missing on PyPI" | ||
| fi | ||
| build-pypi-native-wheels: | ||
| name: Build native PyPI wheels (${{ matrix.os }}) | ||
| runs-on: ${{ matrix.os }} | ||
| needs: [resolve-version, preflight, pypi-detect] | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - os: ubuntu-latest | ||
| cibw_archs_linux: "x86_64 aarch64" | ||
| cibw_archs_macos: "" | ||
| cibw_archs_windows: "" | ||
| artifact: pypi-native-wheels-linux | ||
| - os: macos-latest | ||
| cibw_archs_linux: "" | ||
| cibw_archs_macos: "x86_64 arm64" | ||
| cibw_archs_windows: "" | ||
| artifact: pypi-native-wheels-macos | ||
| - os: windows-latest | ||
| cibw_archs_linux: "" | ||
| cibw_archs_macos: "" | ||
| cibw_archs_windows: "AMD64" | ||
| artifact: pypi-native-wheels-windows | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Add Rust cross-compilation targets (macOS) | ||
| if: runner.os == 'macOS' | ||
| run: rustup target add x86_64-apple-darwin aarch64-apple-darwin | ||
| - name: Setup QEMU (linux aarch64 wheels) | ||
| if: runner.os == 'Linux' | ||
| uses: docker/setup-qemu-action@v3 | ||
| - name: Sync Python sources for native wheel build | ||
| shell: bash | ||
| run: scripts/sync-hush-py-native-sources.sh | ||
| - name: Build native wheels | ||
| env: | ||
| CIBW_BUILD: "cp310-*" | ||
| CIBW_SKIP: "*-musllinux_* *-manylinux_i686 *-win32" | ||
| CIBW_ARCHS_LINUX: ${{ matrix.cibw_archs_linux }} | ||
| CIBW_ARCHS_MACOS: ${{ matrix.cibw_archs_macos }} | ||
| CIBW_ARCHS_WINDOWS: ${{ matrix.cibw_archs_windows }} | ||
| CIBW_BEFORE_ALL_LINUX: "curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal && . $HOME/.cargo/env" | ||
| MACOSX_DEPLOYMENT_TARGET: "10.12" | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| python -m pip install --upgrade pip | ||
| python -m pip install "cibuildwheel==2.23.3" | ||
| python -m cibuildwheel packages/sdk/hush-py/hush-native --output-dir wheelhouse | ||
| ls -la wheelhouse | ||
| - name: Upload native wheel artifacts | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: ${{ matrix.artifact }} | ||
| path: wheelhouse/*.whl | ||
| if-no-files-found: error | ||
| publish-pypi: | ||
| name: Publish PyPI package | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, preflight, pypi-detect, build-pypi-native-wheels] | ||
| if: > | ||
| always() && | ||
| needs.resolve-version.result == 'success' && | ||
| needs.preflight.result == 'success' && | ||
| needs.pypi-detect.result == 'success' | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Ensure native wheel matrix succeeded | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ "${{ needs.build-pypi-native-wheels.result }}" != "success" ]]; then | ||
| echo "Native wheel build matrix did not complete successfully" >&2 | ||
| exit 1 | ||
| fi | ||
| - name: Build pure Python sdist and universal wheel | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| python -m pip install --upgrade pip | ||
| python -m pip install build | ||
| python -m build --sdist --wheel packages/sdk/hush-py | ||
| - name: Download native wheel artifacts | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| uses: actions/download-artifact@v6 | ||
| with: | ||
| pattern: pypi-native-wheels-* | ||
| path: packages/sdk/hush-py/dist | ||
| merge-multiple: true | ||
| - name: Verify wheel set completeness | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| python3 - <<'PY' | ||
| from pathlib import Path | ||
| dist = Path("packages/sdk/hush-py/dist") | ||
| files = [p.name for p in dist.glob("clawdstrike-*.whl")] | ||
| if not files: | ||
| raise SystemExit("No clawdstrike wheels found in dist/") | ||
| checks = { | ||
| "pure py3-none-any wheel": any("py3-none-any.whl" in f for f in files), | ||
| "linux native wheel": any("manylinux" in f for f in files), | ||
| "macOS native wheel": any("macosx" in f for f in files), | ||
| "windows x86_64 native wheel": any("win_amd64" in f for f in files), | ||
| } | ||
| missing = [name for name, ok in checks.items() if not ok] | ||
| if missing: | ||
| raise SystemExit(f"Missing expected wheel artifacts: {', '.join(missing)}") | ||
| if not any(p.suffix == ".gz" and p.name.endswith(".tar.gz") for p in dist.glob("clawdstrike-*.tar.gz")): | ||
| raise SystemExit("Missing clawdstrike source distribution (.tar.gz)") | ||
| PY | ||
| - name: Upload PyPI package | ||
| if: needs.pypi-detect.outputs.none_missing == 'false' | ||
| env: | ||
| TWINE_USERNAME: __token__ | ||
| TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| python -m pip install --upgrade pip | ||
| python -m pip install twine | ||
| twine upload packages/sdk/hush-py/dist/* | ||
| - name: PyPI package already published | ||
| if: needs.pypi-detect.outputs.none_missing == 'true' | ||
| run: echo "PyPI version already exists; skipping PyPI publish" | ||
| build-binaries: | ||
| name: Build Binaries | ||
| runs-on: ${{ matrix.os }} | ||
| needs: preflight | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| - target: x86_64-unknown-linux-gnu | ||
| os: ubuntu-latest | ||
| artifact: hush-linux-x86_64 | ||
| archive: clawdstrike-linux-x86_64 | ||
| - target: x86_64-apple-darwin | ||
| os: macos-latest | ||
| artifact: hush-darwin-x86_64 | ||
| archive: clawdstrike-darwin-x86_64 | ||
| - target: aarch64-apple-darwin | ||
| os: macos-latest | ||
| artifact: hush-darwin-aarch64 | ||
| archive: clawdstrike-darwin-aarch64 | ||
| - target: x86_64-pc-windows-msvc | ||
| os: windows-latest | ||
| artifact: hush-windows-x86_64.exe | ||
| archive: "" | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: ${{ matrix.target }} | ||
| - name: Build release binaries | ||
| run: | | ||
| cargo build --release --target ${{ matrix.target }} -p hush-cli | ||
| cargo build --release --target ${{ matrix.target }} -p hushd | ||
| - name: Rename binary (Unix) | ||
| if: runner.os != 'Windows' | ||
| run: cp target/${{ matrix.target }}/release/hush ${{ matrix.artifact }} | ||
| - name: Rename binary (Windows) | ||
| if: runner.os == 'Windows' | ||
| run: cp target/${{ matrix.target }}/release/hush.exe ${{ matrix.artifact }} | ||
| - name: Upload artifact | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: ${{ matrix.artifact }} | ||
| path: ${{ matrix.artifact }} | ||
| - name: Create release archive (Unix) | ||
| if: runner.os != 'Windows' | ||
| shell: bash | ||
| run: | | ||
| mkdir -p _archive | ||
| cp target/${{ matrix.target }}/release/hush _archive/ | ||
| cp target/${{ matrix.target }}/release/clawdstrike _archive/ | ||
| cp target/${{ matrix.target }}/release/hushd _archive/ | ||
| tar -czf ${{ matrix.archive }}.tar.gz -C _archive . | ||
| - name: Upload release archive | ||
| if: runner.os != 'Windows' | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: ${{ matrix.archive }}.tar.gz | ||
| path: ${{ matrix.archive }}.tar.gz | ||
| build-hushd-binaries: | ||
| name: Build hushd Binaries | ||
| runs-on: ${{ matrix.os }} | ||
| needs: preflight | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| - target: x86_64-unknown-linux-gnu | ||
| os: ubuntu-latest | ||
| artifact: hushd-linux-x86_64 | ||
| - target: x86_64-apple-darwin | ||
| os: macos-latest | ||
| artifact: hushd-darwin-x86_64 | ||
| - target: aarch64-apple-darwin | ||
| os: macos-latest | ||
| artifact: hushd-darwin-aarch64 | ||
| - target: x86_64-pc-windows-msvc | ||
| os: windows-latest | ||
| artifact: hushd-windows-x86_64.exe | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: ${{ matrix.target }} | ||
| - name: Build release hushd binary | ||
| run: cargo build --release --target ${{ matrix.target }} -p hushd | ||
| - name: Rename hushd binary (Unix) | ||
| if: runner.os != 'Windows' | ||
| run: cp target/${{ matrix.target }}/release/hushd ${{ matrix.artifact }} | ||
| - name: Rename hushd binary (Windows) | ||
| if: runner.os == 'Windows' | ||
| run: cp target/${{ matrix.target }}/release/hushd.exe ${{ matrix.artifact }} | ||
| - name: Upload hushd artifact | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: ${{ matrix.artifact }} | ||
| path: ${{ matrix.artifact }} | ||
| build-agent-dmg: | ||
| name: Build Agent DMG | ||
| runs-on: macos-latest | ||
| needs: preflight | ||
| env: | ||
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | ||
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||
| NOTARYTOOL_PROFILE: ${{ secrets.NOTARYTOOL_PROFILE }} | ||
| APPLE_ID: ${{ secrets.APPLE_ID }} | ||
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||
| CLAWDSTRIKE_REQUIRE_CONCRETE_MACOS_PACKAGING: "1" | ||
| NOTARIZE_OUT_DIR: ${{ runner.temp }}/clawdstrike-notarization | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "24" | ||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Install tauri-cli | ||
| run: cargo install tauri-cli --locked --version '^2' | ||
| - name: Build and notarize agent app bundle | ||
| run: bash scripts/notarize-agent-macos.sh | ||
| - name: Upload agent DMG artifact | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: clawdstrike-agent-dmg | ||
| path: apps/agent/src-tauri/target/release/bundle/dmg/*.dmg | ||
| if-no-files-found: error | ||
| - name: Upload notarization evidence | ||
| if: always() | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: clawdstrike-agent-notarization | ||
| path: ${{ env.NOTARIZE_OUT_DIR }}/ | ||
| if-no-files-found: ignore | ||
| create-release: | ||
| name: Create GitHub Release | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, build-binaries, build-hushd-binaries, build-agent-dmg, publish-crates, publish-npm, publish-wasm-npm, publish-pypi] | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Ensure release tag exists | ||
| env: | ||
| TAG: ${{ needs.resolve-version.outputs.tag }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| git fetch --tags --force | ||
| if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then | ||
| echo "Tag ${TAG} already exists" | ||
| exit 0 | ||
| fi | ||
| if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then | ||
| echo "Expected existing tag ${TAG} for non-dispatch release" >&2 | ||
| exit 1 | ||
| fi | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git tag -a "${TAG}" "${GITHUB_SHA}" -m "Release ${TAG}" | ||
| git push origin "refs/tags/${TAG}" | ||
| echo "Created and pushed missing tag ${TAG}" | ||
| - name: Download all artifacts | ||
| uses: actions/download-artifact@v6 | ||
| with: | ||
| path: artifacts | ||
| - name: Create checksums | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| cd artifacts | ||
| for dir in */; do | ||
| cd "$dir" | ||
| for file in *; do | ||
| sha256sum "$file" > "$file.sha256" | ||
| done | ||
| cd .. | ||
| done | ||
| - name: Flatten artifacts | ||
| run: | | ||
| mkdir -p release-files | ||
| find artifacts -type f -exec mv {} release-files/ \; | ||
| - name: Generate and sign hushd OTA manifests | ||
| shell: bash | ||
| env: | ||
| RELEASE_VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| RELEASE_TAG: ${{ needs.resolve-version.outputs.tag }} | ||
| HUSHD_OTA_SIGNING_PRIVATE_KEY_PEM: ${{ secrets.HUSHD_OTA_SIGNING_PRIVATE_KEY_PEM }} | ||
| HUSHD_OTA_SIGNING_PUBLIC_KEY_HEX: ${{ secrets.HUSHD_OTA_SIGNING_PUBLIC_KEY_HEX }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ -z "${HUSHD_OTA_SIGNING_PRIVATE_KEY_PEM:-}" ]]; then | ||
| echo "Missing required secret: HUSHD_OTA_SIGNING_PRIVATE_KEY_PEM" >&2 | ||
| exit 1 | ||
| fi | ||
| pub_args=() | ||
| if [[ -n "${HUSHD_OTA_SIGNING_PUBLIC_KEY_HEX:-}" ]]; then | ||
| pub_args=(--public-key "${HUSHD_OTA_SIGNING_PUBLIC_KEY_HEX}") | ||
| fi | ||
| scripts/generate-hushd-ota-manifest.sh \ | ||
| --version "${RELEASE_VERSION}" \ | ||
| --tag "${RELEASE_TAG}" \ | ||
| --channel stable \ | ||
| --assets-dir release-files \ | ||
| --output release-files/hushd-ota-manifest-stable.unsigned.json \ | ||
| "${pub_args[@]}" | ||
| scripts/sign-hushd-ota-manifest.sh \ | ||
| --input release-files/hushd-ota-manifest-stable.unsigned.json \ | ||
| --output release-files/hushd-ota-manifest-stable.json \ | ||
| "${pub_args[@]}" | ||
| rm -f release-files/hushd-ota-manifest-stable.unsigned.json | ||
| scripts/generate-hushd-ota-manifest.sh \ | ||
| --version "${RELEASE_VERSION}" \ | ||
| --tag "${RELEASE_TAG}" \ | ||
| --channel beta \ | ||
| --assets-dir release-files \ | ||
| --output release-files/hushd-ota-manifest-beta.unsigned.json \ | ||
| "${pub_args[@]}" | ||
| scripts/sign-hushd-ota-manifest.sh \ | ||
| --input release-files/hushd-ota-manifest-beta.unsigned.json \ | ||
| --output release-files/hushd-ota-manifest-beta.json \ | ||
| "${pub_args[@]}" | ||
| rm -f release-files/hushd-ota-manifest-beta.unsigned.json | ||
| - name: Create or update GitHub Release | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| tag_name: ${{ needs.resolve-version.outputs.tag }} | ||
| generate_release_notes: true | ||
| files: release-files/* | ||
| overwrite_files: true | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| update-homebrew: | ||
| name: Update Homebrew Tap | ||
| runs-on: ubuntu-latest | ||
| needs: [resolve-version, create-release] | ||
| steps: | ||
| - name: Download release archives | ||
| env: | ||
| GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | ||
| TAG: ${{ needs.resolve-version.outputs.tag }} | ||
| run: | | ||
| mkdir -p dl | ||
| gh release download "$TAG" \ | ||
| --repo backbay-labs/clawdstrike \ | ||
| --pattern "clawdstrike-*.tar.gz" \ | ||
| --dir dl | ||
| - name: Compute SHA256 checksums | ||
| id: sha | ||
| run: | | ||
| echo "darwin_arm64=$(sha256sum dl/clawdstrike-darwin-aarch64.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" | ||
| echo "darwin_x86_64=$(sha256sum dl/clawdstrike-darwin-x86_64.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" | ||
| echo "linux_x86_64=$(sha256sum dl/clawdstrike-linux-x86_64.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" | ||
| - name: Update homebrew-tap formula | ||
| env: | ||
| GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | ||
| VERSION: ${{ needs.resolve-version.outputs.version }} | ||
| SHA_DARWIN_ARM64: ${{ steps.sha.outputs.darwin_arm64 }} | ||
| SHA_DARWIN_X86_64: ${{ steps.sha.outputs.darwin_x86_64 }} | ||
| SHA_LINUX_X86_64: ${{ steps.sha.outputs.linux_x86_64 }} | ||
| run: | | ||
| set -euo pipefail | ||
| git clone "https://x-access-token:${GH_TOKEN}@github.com/backbay-labs/homebrew-tap.git" tap | ||
| mkdir -p tap/Formula | ||
| cat > tap/Formula/clawdstrike.rb << 'RUBY' | ||
| class Clawdstrike < Formula | ||
| desc "Runtime security enforcement for AI agents" | ||
| homepage "https://github.com/backbay-labs/clawdstrike" | ||
| version "__VERSION__" | ||
| license "Apache-2.0" | ||
| on_macos do | ||
| if Hardware::CPU.arm? | ||
| url "https://github.com/backbay-labs/clawdstrike/releases/download/v#{version}/clawdstrike-darwin-aarch64.tar.gz" | ||
| sha256 "__SHA_DARWIN_ARM64__" | ||
| else | ||
| url "https://github.com/backbay-labs/clawdstrike/releases/download/v#{version}/clawdstrike-darwin-x86_64.tar.gz" | ||
| sha256 "__SHA_DARWIN_X86_64__" | ||
| end | ||
| end | ||
| on_linux do | ||
| url "https://github.com/backbay-labs/clawdstrike/releases/download/v#{version}/clawdstrike-linux-x86_64.tar.gz" | ||
| sha256 "__SHA_LINUX_X86_64__" | ||
| end | ||
| def install | ||
| bin.install "hush" | ||
| bin.install "clawdstrike" | ||
| bin.install "hushd" | ||
| end | ||
| test do | ||
| assert_match version.to_s, shell_output("#{bin}/hush --version") | ||
| assert_match "hushd", shell_output("#{bin}/hushd --version") | ||
| end | ||
| end | ||
| RUBY | ||
| sed -i "s/__VERSION__/${VERSION}/" tap/Formula/clawdstrike.rb | ||
| sed -i "s/__SHA_DARWIN_ARM64__/${SHA_DARWIN_ARM64}/" tap/Formula/clawdstrike.rb | ||
| sed -i "s/__SHA_DARWIN_X86_64__/${SHA_DARWIN_X86_64}/" tap/Formula/clawdstrike.rb | ||
| sed -i "s/__SHA_LINUX_X86_64__/${SHA_LINUX_X86_64}/" tap/Formula/clawdstrike.rb | ||
| cd tap | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add Formula/clawdstrike.rb | ||
| git diff --cached --quiet && echo "Formula unchanged; skipping" && exit 0 | ||
| git commit -m "clawdstrike ${VERSION}" | ||
| git push | ||