baseplate 0.2.1

Shared substrate for agentic-harness tooling — model registry, trace types, path resolution, a cxpak MCP client, and Java-test detection
Documentation
name: Release

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'        # release: full pipeline (binaries + image + crates.io + brew)
      - 'v[0-9]+.[0-9]+.[0-9]+-*'      # prerelease (e.g. -rc.1): binaries + image only

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    strategy:
      fail-fast: false   # one target failing shouldn't cancel/mask the others
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            archive: tar.gz
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            archive: tar.gz
          - target: x86_64-apple-darwin
            os: macos-latest
            archive: tar.gz
          - target: aarch64-apple-darwin
            os: macos-latest
            archive: tar.gz
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            archive: zip
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: resolve toolchain
        id: rust
        shell: bash   # windows-latest defaults to pwsh, where sed/$GITHUB_OUTPUT don't work
        run: echo "channel=$(sed -n 's/^channel = \"\(.*\)\"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
      # Pin to the rust-toolchain.toml channel AND add the cross target to THAT
      # toolchain — @stable would add the target to stable while cargo (driven by
      # rust-toolchain.toml) uses the pinned channel, leaving it without std for
      # the target (error[E0463]: can't find crate for `std`).
      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master @ 2026-06-30
        with:
          toolchain: ${{ steps.rust.outputs.channel }}
          targets: ${{ matrix.target }}
      - 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
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}
      # `--features cli` builds the `baseplate` query binary (off by default so library
      # consumers pull in no extra build); it is what the image and brew ship.
      - name: Build
        run: cargo build --release --locked --features cli --target ${{ matrix.target }}
      - name: Package (tar.gz)
        if: matrix.archive == 'tar.gz'
        run: |
          cd target/${{ matrix.target }}/release
          tar czf ../../../baseplate-${{ matrix.target }}.tar.gz baseplate
      - name: Package (zip)
        if: matrix.archive == 'zip'
        shell: pwsh
        run: Compress-Archive -Path target/${{ matrix.target }}/release/baseplate.exe -DestinationPath baseplate-${{ matrix.target }}.zip
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: baseplate-${{ matrix.target }}
          path: baseplate-${{ matrix.target }}.*

  release:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write   # create the GitHub Release
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          prerelease: ${{ contains(github.ref_name, '-') }}
          files: artifacts/*

  docker:
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read
      packages: write
      id-token: write   # cosign keyless signing via GitHub OIDC
    steps:
      - uses: actions/checkout@v4
      - name: Compute lowercase image name
        run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
      - name: Download Linux artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true
      - name: Stage per-arch binaries for Dockerfile.dist
        run: |
          mkdir -p dist/linux/amd64 dist/linux/arm64
          tar -xzf artifacts/baseplate-x86_64-unknown-linux-gnu.tar.gz -C dist/linux/amd64/
          tar -xzf artifacts/baseplate-aarch64-unknown-linux-gnu.tar.gz -C dist/linux/arm64/
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Image metadata (tags + OCI labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.IMAGE }}
          tags: |
            type=semver,pattern={{version}}
            type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
      # COPY-only Dockerfile.dist needs no QEMU: buildx assembles the manifest
      # from the prebuilt per-arch binaries.
      - name: Build and push multi-arch image
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.dist
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          provenance: true
          sbom: true
      - name: Install cosign
        uses: sigstore/cosign-installer@v3
      - name: Sign image (keyless)
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: cosign sign --yes "${IMAGE}@${DIGEST}"

  publish:
    needs: build
    if: ${{ !contains(github.ref_name, '-') }}   # skip crates.io on prerelease tags
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: resolve toolchain
        id: rust
        shell: bash   # windows-latest defaults to pwsh, where sed/$GITHUB_OUTPUT don't work
        run: echo "channel=$(sed -n 's/^channel = \"\(.*\)\"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master @ 2026-06-30
        with:
          toolchain: ${{ steps.rust.outputs.channel }}
      - name: Publish to crates.io
        # Idempotent: a re-run or re-tag of an already-published version is a
        # no-op success, not a hard failure — so brew/artifact fixes can re-release.
        run: |
          # Capture stderr OUTSIDE the crate dir — a log file inside it makes the tree
          # dirty and `cargo publish`'s cleanliness check refuses before uploading.
          log="${RUNNER_TEMP:-/tmp}/cw-publish.log"
          if cargo publish --locked 2>"$log"; then
            echo "published"
          elif grep -qiE "already (exists|uploaded)|is already uploaded" "$log"; then
            echo "already on crates.io — skipping"; cat "$log"
          else
            cat "$log"; exit 1
          fi
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

  update-homebrew:
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}   # skip Homebrew tap on prerelease tags
    runs-on: ubuntu-latest
    steps:
      - name: Download release binaries
        # Only the baseplate-* binary artifacts are needed for the formula shas;
        # pulling *all* artifacts also fetches the buildx provenance blobs, which
        # have flaked the download in sibling repos ("failed after 5 retries").
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          pattern: baseplate-*
          merge-multiple: true
      - name: Update Homebrew formula
        env:
          TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
        run: |
          # Optional: the Homebrew tap needs a push-scoped TAP_TOKEN secret. Skip (green) when it
          # is absent so the crate + image release is never blocked on the tap.
          if [ -z "$TAP_TOKEN" ]; then
            echo "::notice::TAP_TOKEN unset — skipping Homebrew formula update"
            exit 0
          fi
          VERSION="${GITHUB_REF_NAME#v}"
          SHA_LINUX_X86=$(sha256sum artifacts/baseplate-x86_64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)
          SHA_LINUX_ARM=$(sha256sum artifacts/baseplate-aarch64-unknown-linux-gnu.tar.gz | cut -d' ' -f1)
          SHA_MAC_X86=$(sha256sum artifacts/baseplate-x86_64-apple-darwin.tar.gz | cut -d' ' -f1)
          SHA_MAC_ARM=$(sha256sum artifacts/baseplate-aarch64-apple-darwin.tar.gz | cut -d' ' -f1)

          git clone "https://x-access-token:${TAP_TOKEN}@github.com/Barnett-Studios/homebrew-tap.git" tap
          cd tap
          mkdir -p Formula
          cat > Formula/baseplate.rb << 'RUBY'
          class Baseplate < Formula
            desc "Agentic-harness substrate query CLI (java-test / patterns / registry)"
            homepage "https://github.com/Barnett-Studios/baseplate"
          RUBY
          cat >> Formula/baseplate.rb << EOF
            version "${VERSION}"

            on_macos do
              if Hardware::CPU.arm?
                url "https://github.com/Barnett-Studios/baseplate/releases/download/v${VERSION}/baseplate-aarch64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_ARM}"
              else
                url "https://github.com/Barnett-Studios/baseplate/releases/download/v${VERSION}/baseplate-x86_64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_X86}"
              end
            end

            on_linux do
              if Hardware::CPU.arm?
                url "https://github.com/Barnett-Studios/baseplate/releases/download/v${VERSION}/baseplate-aarch64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LINUX_ARM}"
              else
                url "https://github.com/Barnett-Studios/baseplate/releases/download/v${VERSION}/baseplate-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LINUX_X86}"
              end
            end

            def install
              bin.install "baseplate"
            end

            test do
              system "#{bin}/baseplate", "--help"
            end
          end
          EOF

          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add Formula/baseplate.rb
          git diff --cached --quiet || git commit -m "Update baseplate to ${VERSION}"
          git push