name: Release on: push: tags: ["v*"] # Manual re-publish of the Docker image for an existing release, without # rebuilding binaries or cutting a new version. Runs only the docker (+ # homebrew) jobs against the given tag's already-published release assets. workflow_dispatch: inputs: tag: description: "Existing release tag to (re)build + push the Docker image for, e.g. v0.6.9" required: true type: string permissions: contents: read env: CARGO_TERM_COLOR: always jobs: build: # Binaries are only built when a tag is pushed. A manual dispatch reuses # the existing release's binaries, so it skips this job entirely. if: github.event_name == 'push' permissions: contents: read name: Build ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - target: x86_64-apple-darwin os: macos-latest - target: aarch64-apple-darwin os: macos-latest - target: x86_64-unknown-linux-gnu os: ubuntu-latest - target: aarch64-unknown-linux-gnu os: ubuntu-latest - target: x86_64-pc-windows-msvc os: windows-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} # Cross-compilation support for aarch64-linux # boring-sys2 builds BoringSSL from C source via cmake — needs cross-compiler + cmake - name: Install cross-compilation tools if: matrix.target == 'aarch64-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV echo "CMAKE_SYSTEM_NAME=Linux" >> $GITHUB_ENV echo "CMAKE_SYSTEM_PROCESSOR=aarch64" >> $GITHUB_ENV # BoringSSL build tools for native targets - name: Install cmake if: matrix.target != 'aarch64-unknown-linux-gnu' && runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y cmake - name: Install NASM (Windows) if: runner.os == 'Windows' run: | choco install nasm -y echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Build run: cargo build --release --target ${{ matrix.target }} - name: Package shell: bash run: | tag="${GITHUB_REF#refs/tags/}" staging="webclaw-${tag}-${{ matrix.target }}" mkdir "$staging" # Fail loud if any binary is missing. A silent `|| true` on the # copy was how v0.4.0 shipped tarballs that lacked webclaw-server — # don't repeat that mistake. If a future binary gets renamed or # removed, this step should scream, not quietly publish an # incomplete release. if [[ "${{ matrix.os }}" == "windows-latest" ]]; then cp target/${{ matrix.target }}/release/webclaw.exe "$staging/" cp target/${{ matrix.target }}/release/webclaw-mcp.exe "$staging/" cp target/${{ matrix.target }}/release/webclaw-server.exe "$staging/" cp README.md LICENSE "$staging/" 7z a -tzip "$staging.zip" "$staging" echo "ASSET=$staging.zip" >> $GITHUB_ENV else cp target/${{ matrix.target }}/release/webclaw "$staging/" cp target/${{ matrix.target }}/release/webclaw-mcp "$staging/" cp target/${{ matrix.target }}/release/webclaw-server "$staging/" cp README.md LICENSE "$staging/" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi - name: Upload artifact uses: actions/upload-artifact@v5 with: name: ${{ matrix.target }} path: ${{ env.ASSET }} release: name: Release if: github.event_name == 'push' needs: build runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@v5 with: path: artifacts - name: Compute checksums run: | cd artifacts find . -name '*.tar.gz' -exec mv {} . \; find . -name '*.zip' -exec mv {} . \; sha256sum *.tar.gz *.zip > SHA256SUMS 2>/dev/null || sha256sum * > SHA256SUMS cat SHA256SUMS - name: Create GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${GITHUB_REF#refs/tags/}" gh release create "$tag" \ artifacts/*.tar.gz \ artifacts/*.zip \ artifacts/SHA256SUMS \ --repo "$GITHUB_REPOSITORY" \ --generate-notes docker: name: Docker needs: release # Runs after a successful release on tag push, or standalone via # workflow_dispatch to (re)publish an existing tag's image. `always()` lets # it run even though `release` is skipped on a manual dispatch. if: ${{ always() && (github.event_name == 'workflow_dispatch' || needs.release.result == 'success') }} runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v5 - uses: docker/setup-qemu-action@v3 with: platforms: arm64 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # The pushed tag, or the workflow_dispatch input for a manual re-publish. - name: Resolve tag id: tag run: echo "tag=${{ github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_OUTPUT" # Download pre-built binaries into TARGETARCH-named dirs (amd64/arm64) so # a single multi-platform build picks the matching binary per platform. - name: Download release binaries run: | tag="${{ steps.tag.outputs.tag }}" declare -A arch=( [x86_64-unknown-linux-gnu]=amd64 [aarch64-unknown-linux-gnu]=arm64 ) for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do dir="webclaw-${tag}-${target}" curl -sSL "https://github.com/0xMassi/webclaw/releases/download/${tag}/${dir}.tar.gz" -o "${target}.tar.gz" tar xzf "${target}.tar.gz" a="${arch[$target]}" mkdir -p "binaries-${a}" cp "${dir}/webclaw" "${dir}/webclaw-mcp" "${dir}/webclaw-server" "binaries-${a}/" chmod +x "binaries-${a}"/* done ls -laR binaries-*/ # One atomic multi-platform build + push. buildx assembles a single # manifest list and pushes it in one shot, so there is no separate # `imagetools create` step to race GHCR's read-after-write (that is what # failed before: "v0.6.9-arm64: not found"). Provenance/SBOM attestations # are disabled so each platform entry stays a plain image manifest. - name: Build and push run: | tag="${{ steps.tag.outputs.tag }}" docker buildx build -f Dockerfile.ci \ --platform linux/amd64,linux/arm64 \ --provenance=false --sbom=false \ -t "ghcr.io/0xmassi/webclaw:${tag}" \ -t ghcr.io/0xmassi/webclaw:latest \ --push . homebrew: name: Update Homebrew needs: [release, docker] # Runs once Docker succeeds, on both tag push and manual re-publish. if: ${{ always() && needs.docker.result == 'success' }} runs-on: ubuntu-latest permissions: contents: read steps: - name: Compute all checksums and update formula env: COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: | tag="${{ github.event.inputs.tag || github.ref_name }}" base="https://github.com/0xMassi/webclaw/releases/download/${tag}" # Download all tarballs (Linux + macOS) and compute SHAs for target in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu; do curl -sSL "${base}/webclaw-${tag}-${target}.tar.gz" -o "${target}.tar.gz" done SHA_MAC_ARM=$(sha256sum aarch64-apple-darwin.tar.gz | cut -d' ' -f1) SHA_MAC_X86=$(sha256sum x86_64-apple-darwin.tar.gz | cut -d' ' -f1) SHA_LINUX_ARM=$(sha256sum aarch64-unknown-linux-gnu.tar.gz | cut -d' ' -f1) SHA_LINUX_X86=$(sha256sum x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1) echo "macOS arm64: $SHA_MAC_ARM" echo "macOS x86_64: $SHA_MAC_X86" echo "Linux arm64: $SHA_LINUX_ARM" echo "Linux x86_64: $SHA_LINUX_X86" # Generate formula cat > webclaw.rb << FORMULA class Webclaw < Formula desc "The fastest web scraper for AI agents. 67% fewer tokens. Sub-ms extraction." homepage "https://webclaw.io" license "AGPL-3.0" version "${tag#v}" on_macos do if Hardware::CPU.arm? url "${base}/webclaw-${tag}-aarch64-apple-darwin.tar.gz" sha256 "${SHA_MAC_ARM}" else url "${base}/webclaw-${tag}-x86_64-apple-darwin.tar.gz" sha256 "${SHA_MAC_X86}" end end on_linux do if Hardware::CPU.arm? url "${base}/webclaw-${tag}-aarch64-unknown-linux-gnu.tar.gz" sha256 "${SHA_LINUX_ARM}" else url "${base}/webclaw-${tag}-x86_64-unknown-linux-gnu.tar.gz" sha256 "${SHA_LINUX_X86}" end end def install bin.install "webclaw" bin.install "webclaw-mcp" bin.install "webclaw-server" end test do assert_match "webclaw", shell_output("#{bin}/webclaw --version") end end FORMULA # Remove leading whitespace from heredoc sed -i 's/^ //' webclaw.rb # Push to homebrew tap git clone "https://x-access-token:${COMMITTER_TOKEN}@github.com/0xMassi/homebrew-webclaw.git" tap cp webclaw.rb tap/Formula/webclaw.rb cd tap git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/webclaw.rb git diff --cached --quiet || git commit -m "Update webclaw to ${tag}" git push