feat(safeclaw): onboarding, slash commands, CI release workflow #2
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: SafeClaw Release | ||
|
Check failure on line 1 in .github/workflows/safeclaw-release.yml
|
||
| on: | ||
| push: | ||
| tags: | ||
| - "safeclaw-v*" | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: "Tag to release (e.g. safeclaw-v0.1.0)" | ||
| required: true | ||
| permissions: | ||
| contents: write | ||
| jobs: | ||
| build-dmg: | ||
| name: Build Universal macOS DMG | ||
| runs-on: macos-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
| - name: Resolve version | ||
| id: version | ||
| run: | | ||
| TAG="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}" | ||
| VERSION="${TAG#safeclaw-v}" | ||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: latest | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
| cache: pnpm | ||
| cache-dependency-path: apps/safeclaw/pnpm-lock.yaml | ||
| - name: Install Rust (stable) | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: aarch64-apple-darwin,x86_64-apple-darwin | ||
| - uses: Swatinem/rust-cache@v2 | ||
| with: | ||
| workspaces: apps/safeclaw/src-tauri -> target | ||
| cache-on-failure: true | ||
| - name: Install system dependencies | ||
| run: brew install cmake protobuf | ||
| - name: Install frontend dependencies | ||
| working-directory: apps/safeclaw | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Import Apple certificate | ||
| if: ${{ secrets.APPLE_CERTIFICATE != '' }} | ||
| env: | ||
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | ||
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||
| run: | | ||
| KEYCHAIN_PASSWORD="$(openssl rand -hex 16)" | ||
| KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" | ||
| echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12" | ||
| 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 "$RUNNER_TEMP/cert.p12" \ | ||
| -k "$KEYCHAIN_PATH" \ | ||
| -P "$APPLE_CERTIFICATE_PASSWORD" \ | ||
| -T /usr/bin/codesign \ | ||
| -T /usr/bin/productsign | ||
| security list-keychain -d user -s "$KEYCHAIN_PATH" | ||
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | ||
| - name: Build universal DMG | ||
| working-directory: apps/safeclaw | ||
| env: | ||
| CMAKE_POLICY_VERSION_MINIMUM: "3.5" | ||
| ORT_STRATEGY: download | ||
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||
| APPLE_ID: ${{ secrets.APPLE_ID }} | ||
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | ||
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | ||
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | ||
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | ||
| run: pnpm tauri build --target universal-apple-darwin | ||
| - name: Locate DMG | ||
| id: dmg | ||
| working-directory: apps/safeclaw | ||
| run: | | ||
| DMG_PATH="$(find src-tauri/target/universal-apple-darwin/release/bundle/dmg -name '*.dmg' | head -1)" | ||
| echo "path=$DMG_PATH" >> "$GITHUB_OUTPUT" | ||
| echo "name=$(basename "$DMG_PATH")" >> "$GITHUB_OUTPUT" | ||
| - name: Upload DMG artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: safeclaw-dmg | ||
| path: apps/safeclaw/${{ steps.dmg.outputs.path }} | ||
| if-no-files-found: error | ||
| github-release: | ||
| name: Create GitHub Release | ||
| runs-on: ubuntu-latest | ||
| needs: build-dmg | ||
| outputs: | ||
| version: ${{ steps.version.outputs.version }} | ||
| tag: ${{ steps.version.outputs.tag }} | ||
| sha256: ${{ steps.sha256.outputs.sha256 }} | ||
| dmg_name: ${{ steps.artifact.outputs.dmg_name }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Resolve version | ||
| id: version | ||
| run: | | ||
| TAG="${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}" | ||
| VERSION="${TAG#safeclaw-v}" | ||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| - name: Download DMG artifact | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: safeclaw-dmg | ||
| path: dist | ||
| - name: Get DMG name and SHA256 | ||
| id: sha256 | ||
| run: | | ||
| DMG_PATH="$(find dist -name '*.dmg' | head -1)" | ||
| SHA256="$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')" | ||
| echo "sha256=$SHA256" >> "$GITHUB_OUTPUT" | ||
| echo "dmg_path=$DMG_PATH" >> "$GITHUB_OUTPUT" | ||
| - name: Get DMG artifact name | ||
| id: artifact | ||
| run: | | ||
| DMG_PATH="$(find dist -name '*.dmg' | head -1)" | ||
| echo "dmg_name=$(basename "$DMG_PATH")" >> "$GITHUB_OUTPUT" | ||
| - name: Generate release notes | ||
| run: | | ||
| PREV_TAG=$(git tag --sort=-v:refname | grep '^safeclaw-v' | head -2 | tail -1) | ||
| if [ -z "$PREV_TAG" ]; then | ||
| NOTES="Initial release of SafeClaw." | ||
| else | ||
| NOTES=$(git log "${PREV_TAG}..HEAD" --oneline --no-merges --pretty=format:"- %s" | head -50) | ||
| fi | ||
| echo "$NOTES" > /tmp/release-notes.md | ||
| - name: Create GitHub Release | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| TAG: ${{ steps.version.outputs.tag }} | ||
| run: | | ||
| DMG_PATH="$(find dist -name '*.dmg' | head -1)" | ||
| if gh release view "$TAG" &>/dev/null; then | ||
| gh release upload "$TAG" "$DMG_PATH" --clobber | ||
| else | ||
| gh release create "$TAG" "$DMG_PATH" \ | ||
| --title "SafeClaw $TAG" \ | ||
| --notes-file /tmp/release-notes.md | ||
| fi | ||
| update-homebrew-cask: | ||
| name: Update Homebrew Cask | ||
| runs-on: ubuntu-latest | ||
| needs: github-release | ||
| steps: | ||
| - name: Checkout homebrew-tap | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| repository: A3S-Lab/homebrew-tap | ||
| token: ${{ secrets.HOMEBREW_TAP_TOKEN }} | ||
| path: homebrew-tap | ||
| - name: Write Cask | ||
| env: | ||
| VERSION: ${{ needs.github-release.outputs.version }} | ||
| TAG: ${{ needs.github-release.outputs.tag }} | ||
| SHA256: ${{ needs.github-release.outputs.sha256 }} | ||
| DMG_NAME: ${{ needs.github-release.outputs.dmg_name }} | ||
| run: | | ||
| mkdir -p homebrew-tap/Casks | ||
| cat > homebrew-tap/Casks/safeclaw.rb <<CASK | ||
| # typed: false | ||
| # frozen_string_literal: true | ||
| cask "safeclaw" do | ||
| version "${VERSION}" | ||
| sha256 "${SHA256}" | ||
| url "https://github.com/A3S-Lab/a3s/releases/download/${TAG}/${DMG_NAME}" | ||
| name "SafeClaw" | ||
| desc "Secure personal AI assistant with TEE support" | ||
| homepage "https://github.com/A3S-Lab/a3s" | ||
| depends_on macos: ">= :ventura" | ||
| app "SafeClaw.app" | ||
| zap trash: [ | ||
| "~/Library/Application Support/com.a3s.safeclaw", | ||
| "~/Library/Caches/com.a3s.safeclaw", | ||
| "~/Library/Preferences/com.a3s.safeclaw.plist", | ||
| "~/Library/Logs/com.a3s.safeclaw", | ||
| ] | ||
| end | ||
| CASK | ||
| # Strip leading whitespace from heredoc indentation | ||
| sed -i 's/^ //' homebrew-tap/Casks/safeclaw.rb | ||
| - name: Commit and push | ||
| working-directory: homebrew-tap | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git add Casks/safeclaw.rb | ||
| git diff --cached --quiet || git commit -m "chore: update SafeClaw cask to ${{ needs.github-release.outputs.tag }}" | ||
| git push | ||