brain-edge 0.1.0

The HTTP/JSON edge for the Brain memory database — a self-hostable binary and an embeddable library that speaks the same API the Arc cloud hosts.
Documentation
name: Release

# Publishes brain-edge when a version tag is pushed, to two places:
#   • crates.io      — the library + `brain-edge` binary, for `cargo install`
#   • ghcr.io        — the self-host container image, how most people run it
#
# The tag normally comes from `release-please.yml`, not from a shell: merging
# the Release PR it maintains bumps the version in Cargo.toml, writes the
# CHANGELOG, and creates the tag. Tagging by hand still works and is the
# escape hatch:
#
#   git tag v0.1.0 && git push origin v0.1.0
#
# The version check below is what makes both paths safe — a tag that disagrees
# with the manifest cannot publish.
#
# Auth: crates.io is OIDC **Trusted Publishing** (no long-lived token in this
# repo); GHCR uses the workflow's own `GITHUB_TOKEN` with `packages: write`,
# so there is no registry secret to rotate for either.
#
# ONE-TIME registry setup (done on crates.io, not here) — register this repo +
# this workflow as a Trusted Publisher:
#   crates.io → crate settings → Trusted Publishing → GitHub:
#     owner arc-labs-ai, repo brain-edge, workflow release.yml, env release
# Until that is configured the crates.io job fails closed (it never falls back
# to a token). GHCR needs no setup beyond the package being owned by the org.
#
# ORDERING: brain-edge depends on `brain-db-sdk`, which must already be on
# crates.io at a matching version or `cargo publish` fails — crates.io refuses a
# crate whose dependencies it cannot resolve. brain-db-sdk 0.1.0 is published,
# and Cargo.toml now depends on it by version rather than by path, so both jobs
# resolve it from the registry like any other dependency.
#
# The two jobs are independent — crates.io failing does not block the image,
# and vice versa. Both registries reject re-publishing the same version, so
# re-running after a partial success is a no-op for whichever already landed.
#
# The `release` environment gates both publishes; require reviewers on it
# (Settings → Environments → release) to add a manual approval before anything
# goes public.

on:
  push:
    tags:
      - "v*"

# No repo-wide write; each job opts into exactly the scope it needs.
permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  crates-io:
    name: Publish → crates.io
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read
      id-token: write # OIDC → crates.io trusted publishing
    steps:
      - uses: actions/checkout@v7

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: brain-edge

      - name: Verify tag matches crate version
        run: |
          TAG="${GITHUB_REF_NAME#v}"
          CRATE="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
          echo "tag=$TAG crate=$CRATE"
          test "$TAG" = "$CRATE" || { echo "::error::tag $TAG != crate version $CRATE"; exit 1; }

      - name: Test
        run: cargo test --all-targets

      # Exchanges the GitHub OIDC token for a short-lived crates.io token.
      - name: Authenticate to crates.io (OIDC)
        id: auth
        uses: rust-lang/crates-io-auth-action@v1

      - name: Publish
        run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

  image:
    name: Publish image → ghcr.io
    runs-on: ubuntu-latest
    environment: release
    permissions:
      contents: read
      packages: write # push to ghcr.io
      id-token: write # build provenance attestation
    steps:
      - uses: actions/checkout@v7

      - name: Verify tag matches crate version
        run: |
          TAG="${GITHUB_REF_NAME#v}"
          CRATE="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
          echo "tag=$TAG crate=$CRATE"
          test "$TAG" = "$CRATE" || { echo "::error::tag $TAG != crate version $CRATE"; exit 1; }

      - uses: docker/setup-buildx-action@v4

      - name: Log in to ghcr.io
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # From tag v1.2.3 this produces :1.2.3, :1.2, :1 and :latest. The
      # floating tags let a self-hoster track patches without editing compose
      # files; :1.2.3 is the one to pin in production.
      - name: Derive image tags
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/arc-labs-ai/brain-edge
          # `latest` is not listed: flavor defaults to latest=auto, which adds it
          # for any non-prerelease semver tag. Listing it as well is what gave
          # brain-db a duplicate `latest` in its tag set.
          #
          # The bare major tag is guarded. metadata-action emits `0` for a 0.x
          # release, and under semver every 0.x MINOR may break — so a consumer
          # pinned to `:0` would be carried from 0.1.0 straight through 0.2.0's
          # breaking changes. The guard turns it on by itself at 1.0, where a
          # floating major is the point.
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
          labels: |
            org.opencontainers.image.title=brain-edge
            org.opencontainers.image.description=The HTTP/JSON edge for the Brain memory database
            org.opencontainers.image.licenses=Apache-2.0

      # Context is the PARENT of both crates, not brain-edge — see the header
      # comment in the Dockerfile. Same shape the CI build job uses.
      - name: Build and push
        id: push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Signed provenance linking the image digest back to this workflow run,
      # so a consumer can verify where the image came from.
      - name: Attest build provenance
        uses: actions/attest-build-provenance@v4
        with:
          subject-name: ghcr.io/arc-labs-ai/brain-edge
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true