name: Release

on:
  push:
    tags: ['v*']

permissions: read-all

env:
  CARGO_TERM_COLOR: always

jobs:
  # Preflight runs before any side-effecting work (build, package, attest,
  # release upload, crates.io publish, Homebrew tap bump). It enforces the
  # RELEASING.md policy that Cargo.toml's version exactly matches the tag,
  # including any prerelease suffix. If this fails the rest of the workflow
  # is skipped and no GitHub Release is created -- the operator can delete
  # the bad tag, fix Cargo.toml, and re-tag.
  preflight:
    name: Verify tag matches Cargo.toml
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6
      - name: Compare tag with Cargo.toml version
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
          tag_version="${TAG#v}"
          echo "tag (no v): $tag_version"
          echo "Cargo.toml: $cargo_version"
          if [ "$cargo_version" != "$tag_version" ]; then
            echo "::error::tag/Cargo.toml mismatch -- bump Cargo.toml to match the tag (RELEASING.md step 1)"
            exit 1
          fi
          echo "version alignment: OK"

  manpages:
    name: Generate manpages
    needs: preflight
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6
      - uses: dtolnay/rust-toolchain@193d6aa1dbbc28bd2c0a6b0e327cfdce68baaf6e  # stable
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4  # v2
        with:
          key: manpages
      - name: Generate manpages
        run: cargo run -p xtask --no-default-features --release --locked -- man --out man/man1
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7
        with:
          name: bzr-manpages
          path: man/man1/*.1
          if-no-files-found: error

  build:
    name: Build ${{ matrix.target }}
    needs: manpages
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
      id-token: write       # OIDC token for keyless Sigstore signing
      attestations: write   # publish SLSA build provenance
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: false
            archive: tar.gz
            pkg_deb: true
            pkg_rpm: true
            # Statically link libdbus: this binary feeds the Homebrew bottle,
            # which cannot declare a system-library dependency.
            vendored_keyring: true
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: true
            archive: tar.gz
            pkg_deb: true
            pkg_rpm: true
            vendored_keyring: true
          - target: powerpc64le-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: false
            use_qemu: true
            archive: tar.gz
            pkg_deb: true
            pkg_rpm: true
          - target: s390x-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: true
            archive: tar.gz
            # .deb skipped: Debian s390x audience is effectively zero.
            pkg_deb: false
            pkg_rpm: true
          - target: aarch64-apple-darwin
            os: macos-14
            use_cross: false
            archive: tar.gz
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            use_cross: false
            archive: zip
          - target: aarch64-pc-windows-msvc
            os: windows-latest
            use_cross: false
            archive: zip
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6

      - name: Install libdbus-1-dev (native Linux)
        if: matrix.target == 'x86_64-unknown-linux-gnu'
        run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config

      - uses: dtolnay/rust-toolchain@193d6aa1dbbc28bd2c0a6b0e327cfdce68baaf6e  # stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4  # v2
        with:
          key: ${{ matrix.target }}

      - name: Set up QEMU
        if: matrix.use_qemu
        uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3  # v4

      - name: Install cross
        if: matrix.use_cross
        run: cargo install cross --locked

      - name: Static CRT link (Windows)
        if: contains(matrix.target, 'windows-msvc')
        shell: bash
        run: echo "RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV"

      - name: Build
        env:
          # pkg-config refuses to cross-compile by default; Cross.toml
          # installs target-arch libdbus via multiarch inside the cross
          # images, and this tells pkg-config to honour the resulting
          # target `.pc` files.
          PKG_CONFIG_ALLOW_CROSS: "1"
        run: |
          # Self-contained libdbus for the targets that feed Homebrew bottles
          # (x86_64/aarch64 Linux); other targets keep dynamic linking and the
          # .deb/.rpm declare libdbus as a package dependency. ppc64le (qemu)
          # carries no bottle, so it is not vendored.
          FEATURES=()
          if [ "${{ matrix.vendored_keyring }}" = "true" ]; then
            FEATURES=(--features vendored-keyring)
          fi
          if [ "${{ matrix.use_qemu }}" = "true" ]; then
            # Use the latest stable Rust image so the container's rustc tracks
            # `dtolnay/rust-toolchain@stable` used by the rest of the matrix.
            # A pinned older image (e.g. rust:1.85-bookworm) silently breaks
            # whenever Cargo.toml's `rust-version` advances.
            docker run --rm --platform linux/ppc64le \
              -v "${{ github.workspace }}:/workspace" -w /workspace \
              rust:bookworm \
              bash -c "apt-get update && apt-get install -y libdbus-1-dev pkg-config && cargo build --release --locked --target ${{ matrix.target }}"
            # The container ran as root and left target/ owned by root.
            # Subsequent host-side steps (cargo deb / cargo generate-rpm /
            # tarball staging / lintian) run as the runner user and would
            # otherwise hit "Permission denied" on target/.
            sudo chown -R "$(id -u):$(id -g)" target
          elif [ "${{ matrix.use_cross }}" = "true" ]; then
            cross build --release --locked --target ${{ matrix.target }} "${FEATURES[@]}"
          else
            cargo build --release --locked --target ${{ matrix.target }} "${FEATURES[@]}"
          fi
        shell: bash

      - name: Download manpages
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
        with:
          name: bzr-manpages
          path: man/man1

      - name: Package (unix)
        if: matrix.archive == 'tar.gz'
        run: |
          STAGING="bzr-${{ github.ref_name }}-${{ matrix.target }}"
          mkdir "$STAGING"
          cp "target/${{ matrix.target }}/release/bzr" "$STAGING/"
          cp LICENSE README.md "$STAGING/"
          mkdir -p "$STAGING/man/man1"
          cp man/man1/*.1 "$STAGING/man/man1/"
          tar czf "$STAGING.tar.gz" "$STAGING"
        shell: bash

      - name: Package (windows)
        if: matrix.archive == 'zip'
        run: |
          $STAGING = "bzr-${{ github.ref_name }}-${{ matrix.target }}"
          New-Item -ItemType Directory -Path $STAGING
          Copy-Item "target\${{ matrix.target }}\release\bzr.exe" "$STAGING\"
          Copy-Item LICENSE, README.md "$STAGING\"
          New-Item -ItemType Directory -Path "$STAGING\man\man1"
          Copy-Item "man\man1\*.1" "$STAGING\man\man1\"
          Compress-Archive -Path $STAGING -DestinationPath "$STAGING.zip"
        shell: pwsh

      - name: Install cargo-deb
        if: matrix.pkg_deb
        uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154  # v2.81.8
        with:
          tool: cargo-deb

      - name: Install cargo-generate-rpm
        if: matrix.pkg_rpm
        uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154  # v2.81.8
        with:
          tool: cargo-generate-rpm

      - name: Build .deb
        if: matrix.pkg_deb
        run: cargo deb --no-build --no-strip --target ${{ matrix.target }}
        shell: bash

      - name: Build .rpm
        if: matrix.pkg_rpm
        shell: bash
        run: |
          # RPM's Version field disallows '-' (it separates the package's
          # Version from its Release in NVR notation), so 0.2.0-rc6 is a
          # hard error from cargo-generate-rpm. Translate the prerelease
          # suffix to '~', which RPM accepts and which sorts correctly:
          # 0.2.0~rc6 < 0.2.0 < 0.2.0~rc7. cargo-deb (run earlier in this
          # job) accepts the hyphen form and is unaffected; this rewrite
          # only persists for the rest of the cross-compile job in CI.
          sed -i 's/^version = "\([^-"]*\)-\(.*\)"/version = "\1~\2"/' Cargo.toml
          cargo generate-rpm --target ${{ matrix.target }}

      - name: Stage packages alongside tarball
        if: matrix.pkg_deb || matrix.pkg_rpm
        run: |
          set -euo pipefail
          if [ "${{ matrix.pkg_deb }}" = "true" ]; then
            cp target/${{ matrix.target }}/debian/*.deb .
          fi
          if [ "${{ matrix.pkg_rpm }}" = "true" ]; then
            cp target/${{ matrix.target }}/generate-rpm/*.rpm .
          fi
        shell: bash

      - name: Lint .deb (warn-only)
        if: matrix.pkg_deb
        continue-on-error: true
        run: |
          sudo apt-get update && sudo apt-get install -y lintian
          lintian --no-tag-display-limit ./*.deb || true
        shell: bash

      - name: Lint .rpm (warn-only)
        if: matrix.pkg_rpm
        continue-on-error: true
        run: |
          sudo apt-get update && sudo apt-get install -y rpmlint
          rpmlint ./*.rpm || true
        shell: bash

      - name: Install-test .deb (x86_64 only)
        if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.pkg_deb
        run: |
          docker run --rm -v "$PWD:/pkg" -w /pkg debian:stable bash -c '
            apt-get update && apt-get install -y ./*.deb && bzr --version'
        shell: bash

      - name: Install-test .rpm (x86_64 only)
        if: matrix.target == 'x86_64-unknown-linux-gnu' && matrix.pkg_rpm
        run: |
          docker run --rm -v "$PWD:/pkg" -w /pkg fedora:latest bash -c '
            dnf install -y ./*.rpm && bzr --version'
        shell: bash

      - name: Attest build provenance
        uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32  # v4
        with:
          subject-path: |
            bzr-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }}
            *.deb
            *.rpm

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7
        with:
          name: bzr-${{ matrix.target }}
          path: |
            bzr-${{ github.ref_name }}-${{ matrix.target }}.*
            *.deb
            *.rpm

  release:
    name: Create Release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
        with:
          path: artifacts
          # Match per-target artifacts (e.g. bzr-x86_64-pc-windows-msvc) but
          # exclude bzr-manpages, which is an internal artifact consumed by
          # the build matrix and must not appear on the release page.
          pattern: bzr-*-*
          merge-multiple: true

      - name: Stage installer scripts (with version baked in)
        working-directory: artifacts
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          python3 - <<'PYEOF'
          import os, pathlib
          tag = os.environ["TAG"]

          sh_marker = 'BZR_VERSION="${BZR_VERSION:-}"'
          sh_replace = 'BZR_VERSION="${BZR_VERSION:-' + tag + '}"'
          sh_in = pathlib.Path("../install.sh").read_text()
          sh_out = sh_in.replace(sh_marker, sh_replace, 1)
          assert sh_out != sh_in, f"install.sh marker line not found: {sh_marker!r}"
          pathlib.Path("install.sh").write_text(sh_out)

          ps_marker = "$BzrVersion = $env:BZR_VERSION"
          ps_replace = (
              "$BzrVersion = if ($env:BZR_VERSION) "
              "{ $env:BZR_VERSION } else { '" + tag + "' }"
          )
          ps_in = pathlib.Path("../install.ps1").read_text()
          ps_out = ps_in.replace(ps_marker, ps_replace, 1)
          assert ps_out != ps_in, f"install.ps1 marker line not found: {ps_marker!r}"
          pathlib.Path("install.ps1").write_text(ps_out)
          PYEOF
          chmod +x install.sh

      - name: Generate SHA256SUMS
        working-directory: artifacts
        run: |
          set -euo pipefail
          # Hash every release artifact (tarballs, zips, .deb, .rpm).
          # Sorted, LF-only output so reproducible across re-runs of the
          # same set of inputs.
          find . -maxdepth 1 -type f ! -name 'SHA256SUMS' -printf '%f\n' \
            | sort \
            | xargs -d '\n' sha256sum \
            > SHA256SUMS
          echo "Generated SHA256SUMS:"
          cat SHA256SUMS

      - name: Extract release notes from CHANGELOG.md
        id: notes
        shell: bash
        run: |
          set -euo pipefail
          TAG="${{ github.ref_name }}"
          VERSION="${TAG#v}"
          NOTES_FILE="$(mktemp)"
          awk -v v="$VERSION" '
            $0 ~ "^## \\[" v "\\]" { capture = 1; next }
            capture && /^## \[/      { exit }
            capture                  { print }
          ' CHANGELOG.md > "$NOTES_FILE"
          if ! [ -s "$NOTES_FILE" ]; then
            echo "::error::No CHANGELOG.md entry found for [$VERSION]"
            exit 1
          fi
          echo "notes_file=$NOTES_FILE" >> "$GITHUB_OUTPUT"

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          PRERELEASE=""
          if [[ "$GITHUB_REF_NAME" == *-* ]]; then
            PRERELEASE="--prerelease"
          fi
          gh release create "$GITHUB_REF_NAME" \
            --title "bzr $GITHUB_REF_NAME" \
            --notes-file "${{ steps.notes.outputs.notes_file }}" \
            $PRERELEASE \
            artifacts/*

  installer-smoke:
    name: Installer smoke ${{ matrix.os }}
    needs: release
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
    steps:
      # Check out so we can read Cargo.toml's version field. Cargo.toml is the
      # source of truth for what `bzr --version` prints, so the smoke check
      # validates the installed binary's output against it. This is robust
      # whether or not the project carries an rc suffix in Cargo.toml across
      # the rc cycle.
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6

      # The tag-vs-Cargo.toml check is enforced by the `preflight` job before
      # any build runs, so we don't repeat it here. This step verifies the
      # downloaded binary's --version output matches Cargo.toml at the tagged
      # commit -- catches a stale build artifact (Cargo.toml bumped post-build).
      - name: Smoke install.sh
        if: matrix.os == 'ubuntu-latest'
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          export BZR_INSTALL_DIR="$HOME/.local/bin"
          curl -fsSL "https://github.com/randomparity/bzr/releases/download/${TAG}/install.sh" | sh
          installed_version="$("$BZR_INSTALL_DIR/bzr" --version 2>&1)"
          expected="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
          echo "installed: $installed_version"
          echo "expected:  $expected"
          case "$installed_version" in
            *"$expected"*) echo "binary version: OK" ;;
            *) echo "binary mismatch: expected=$expected output=$installed_version" >&2; exit 1 ;;
          esac
        shell: bash

      - name: Smoke install.ps1
        if: matrix.os == 'windows-latest'
        env:
          TAG: ${{ github.ref_name }}
        shell: pwsh
        run: |
          $env:BZR_INSTALL_DIR = Join-Path $env:LOCALAPPDATA 'Programs\bzr'
          irm "https://github.com/randomparity/bzr/releases/download/$env:TAG/install.ps1" | iex
          $installed = & (Join-Path $env:BZR_INSTALL_DIR 'bzr.exe') --version
          $expected = (Select-String -Path Cargo.toml -Pattern '^version = "(.*)"' |
                       Select-Object -First 1).Matches[0].Groups[1].Value
          Write-Host "installed: $installed"
          Write-Host "expected:  $expected"
          if ($installed -notlike "*$expected*") {
              Write-Error "binary mismatch: expected=$expected output=$installed"
              exit 1
          }
          Write-Host "binary version: OK"

  # Bump randomparity/homebrew-tap on stable releases. This used to be a
  # separate workflow triggered by `release: published`, but the release
  # event from a GITHUB_TOKEN-authenticated `gh release create` does not
  # fire downstream workflows (well-known GH Actions safeguard against
  # workflow recursion), so the bump never ran for v0.2.0. Folding the
  # logic into release.yml as a job that depends on `release` removes the
  # event-cascade dependency entirely.
  homebrew:
    name: Bump randomparity/homebrew-tap
    needs: release
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Checkout bzr (for template)
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6

      - name: Checkout tap
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6
        with:
          repository: randomparity/homebrew-tap
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          path: tap

      - name: Render formula
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          VERSION="${TAG#v}"
          BASE="https://github.com/randomparity/bzr/releases/download/${TAG}"

          fetch_sha() {
            curl --fail --silent --show-error --location "$1" | sha256sum | awk '{print $1}'
          }

          MAC_ARM_SHA=$(fetch_sha   "${BASE}/bzr-${TAG}-aarch64-apple-darwin.tar.gz")
          LINUX_ARM_SHA=$(fetch_sha "${BASE}/bzr-${TAG}-aarch64-unknown-linux-gnu.tar.gz")
          LINUX_INTEL_SHA=$(fetch_sha "${BASE}/bzr-${TAG}-x86_64-unknown-linux-gnu.tar.gz")
          SRC_SHA=$(fetch_sha       "https://github.com/randomparity/bzr/archive/refs/tags/${TAG}.tar.gz")

          mkdir -p tap/Formula
          sed \
            -e "s|{{VERSION}}|${VERSION}|g" \
            -e "s|{{MAC_ARM_SHA}}|${MAC_ARM_SHA}|g" \
            -e "s|{{LINUX_ARM_SHA}}|${LINUX_ARM_SHA}|g" \
            -e "s|{{LINUX_INTEL_SHA}}|${LINUX_INTEL_SHA}|g" \
            -e "s|{{SRC_SHA}}|${SRC_SHA}|g" \
            homebrew/bzr.rb.template > tap/Formula/bzr.rb

      - name: Commit and push
        working-directory: tap
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Formula/bzr.rb
          if git diff --staged --quiet; then
            echo "Formula already up-to-date for ${TAG}; nothing to commit."
            exit 0
          fi
          git commit -m "bzr ${TAG#v}"
          git push

  # Build real Homebrew bottles for the just-published formula. A bottle is
  # *poured* (extracted into the Cellar) rather than built, so it bypasses
  # FormulaInstaller#build and the build sandbox entirely -- including the
  # sandbox's deny_read_home pass, which raises on any $HOME entry containing
  # characters like '(' or ')'. Without bottles, even a binary-download
  # formula runs its `install` method under that sandbox and breaks for
  # users whose home directory holds such a path.
  #
  # Chicken-and-egg: bottling needs the formula installed, which needs the
  # formula already in the tap. So this job runs AFTER `homebrew` pushes the
  # (bottle-less) formula, builds the bottle on each target OS, uploads the
  # tarball to the release, and emits the bottle JSON as an artifact. The
  # `bottles-merge` job then folds the `bottle do` block back into the tap
  # formula. If any leg fails the merge is skipped and the formula stays
  # bottle-less -- i.e. it degrades to today's source/binary-download path.
  #
  # macOS is bottled on the OLDEST available arm runner (macos-14 / Sonoma)
  # on purpose: Homebrew's bottle resolution falls back to older macOS tags,
  # so an arm64_sonoma bottle is also poured on Sequoia/Tahoe, whereas a
  # bottle built on the newest OS would leave older-OS users on the source
  # path. Intel macOS is intentionally not bottled (no prebuilt Intel binary).
  bottles:
    name: Build bottle (${{ matrix.label }})
    needs: homebrew
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write   # upload bottle tarballs to the GitHub release
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-14
            label: arm64 macOS
          - os: ubuntu-latest
            label: x86_64 Linux
          - os: ubuntu-24.04-arm
            label: arm64 Linux
    steps:
      - name: Set up Homebrew on PATH (Linux)
        if: runner.os == 'Linux'
        run: |
          set -euo pipefail
          # GitHub's ubuntu images ship Homebrew at /home/linuxbrew; install
          # it if a future image (or the arm runner) does not.
          if [ ! -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
            NONINTERACTIVE=1 /bin/bash -c \
              "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
          fi
          echo "/home/linuxbrew/.linuxbrew/bin" >> "$GITHUB_PATH"
          # The Linux binary dynamically links libdbus (keyring default
          # feature); `brew test` runs `bzr --version`, so the runtime lib
          # must be present or the loader fails before main().
          sudo apt-get update && sudo apt-get install -y libdbus-1-3

      - name: Build, bottle, and upload
        env:
          TAG: ${{ github.ref_name }}
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          VERSION="${TAG#v}"
          BASE="https://github.com/randomparity/bzr/releases/download/${TAG}"

          brew tap randomparity/tap
          brew install --build-bottle randomparity/tap/bzr

          workdir="$(mktemp -d)"
          cd "$workdir"
          # --root-url bakes the GitHub-release download base into the JSON so
          # the later `--merge` writes a `bottle do` block that resolves to
          # these assets.
          brew bottle --json --no-rebuild --root-url "$BASE" randomparity/tap/bzr

          # `brew bottle` writes a DOUBLE-dash local file
          # (bzr--<ver>.<tag>.bottle.tar.gz); Homebrew downloads the
          # SINGLE-dash form from root_url. Rename before upload so the asset
          # name matches what `brew install` will request.
          dd_file="$(ls bzr--"${VERSION}".*.bottle.tar.gz)"
          sd_file="${dd_file/--/-}"
          mv "$dd_file" "$sd_file"

          gh release upload "$TAG" "$sd_file" --repo randomparity/bzr --clobber

          mkdir -p "$GITHUB_WORKSPACE/bottle-json"
          cp ./*.json "$GITHUB_WORKSPACE/bottle-json/"

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7
        with:
          name: bottle-json-${{ matrix.os }}
          path: bottle-json/*.json
          if-no-files-found: error

  # Fold the per-platform bottle metadata into the tap formula. `brew bottle
  # --merge --write` reads the JSONs (each carrying its tag, sha256, cellar,
  # and root_url) and inserts a single `bottle do` block into the tapped
  # formula. We then push that formula back to the tap -- a second tap commit
  # for the release, layered on top of the bottle-less one from `homebrew`.
  bottles-merge:
    name: Add bottle block to formula
    needs: bottles
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - name: Set up Homebrew on PATH
        run: echo "/home/linuxbrew/.linuxbrew/bin" >> "$GITHUB_PATH"

      - name: Download bottle JSONs
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
        with:
          pattern: bottle-json-*
          path: bottle-json
          merge-multiple: true

      - name: Merge bottle block and push to tap
        env:
          TAG: ${{ github.ref_name }}
          HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          set -euo pipefail
          # Tap (read-only https clone) so `--merge` can resolve and edit the
          # formula at $(brew --repository randomparity/tap)/Formula/bzr.rb.
          brew tap randomparity/tap
          brew bottle --merge --write --no-commit bottle-json/*.json

          tap_dir="$(brew --repository randomparity/tap)"
          cd "$tap_dir"
          # A bottle block exists => Homebrew pours instead of building, and a
          # broken formula here would hard-fail every install with no source
          # fallback. Gate the push on the formula at least parsing as Ruby.
          ruby -c Formula/bzr.rb
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git remote set-url origin \
            "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/randomparity/homebrew-tap.git"
          git add Formula/bzr.rb
          if git diff --staged --quiet; then
            echo "Formula already carries the bottle block for ${TAG}; nothing to commit."
            exit 0
          fi
          git commit -m "bzr ${TAG#v}: add bottles"
          git push origin HEAD:main

  # Open a post-release version-bump PR on main. Stable releases only
  # (`-` filters out pre-release tags). Folded into release.yml for the
  # same GITHUB_TOKEN-doesn't-fire-downstream-events reason as the
  # homebrew job above.
  #
  # The default bump is `patch` (e.g. v0.4.0 -> 0.4.1-dev), which is
  # the smallest forward step. If the next planned release is a minor
  # or major bump instead, the maintainer edits Cargo.toml/Cargo.lock
  # on the auto-opened PR before merging — that's the human-in-the-
  # loop angle. The PR is opened against `main`, so it can be amended
  # exactly like a hand-crafted release-prep PR.
  #
  # CI does not run on PRs opened by GITHUB_TOKEN; see the PR body
  # the job emits for the empty-commit kick-off recipe.
  post-release-bump:
    name: Open post-release version-bump PR
    needs: [release, installer-smoke]
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6
        with:
          ref: main
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@193d6aa1dbbc28bd2c0a6b0e327cfdce68baaf6e  # stable

      - name: Compute next dev version
        id: ver
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          VERSION="${TAG#v}"
          # Default bump policy: patch. Maintainer can override the
          # version on the auto-opened PR before merging.
          IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
          NEXT_PATCH=$((PATCH + 1))
          NEXT="${MAJOR}.${MINOR}.${NEXT_PATCH}-dev"
          BRANCH="chore/bump-to-v${MAJOR}.${MINOR}.${NEXT_PATCH}-dev"
          echo "next=$NEXT" >> "$GITHUB_OUTPUT"
          echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
          echo "Computed next dev version: $NEXT (branch: $BRANCH)"

      - name: Abort if branch already exists on remote
        env:
          BRANCH: ${{ steps.ver.outputs.branch }}
        run: |
          set -euo pipefail
          if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null; then
            echo "::error::Branch $BRANCH already exists on origin; resolve manually." >&2
            exit 1
          fi

      - name: Update Cargo.toml
        env:
          NEXT: ${{ steps.ver.outputs.next }}
        run: |
          set -euo pipefail
          # Replace the version line inside [package] only. Dependency
          # lines have `version = "..."` mid-line, so anchoring on `^`
          # is necessary; section tracking ensures we ignore version
          # entries in other tables (e.g. [workspace.package] if added).
          awk -v v="$NEXT" '
            /^\[/ { in_pkg = ($0 == "[package]") }
            in_pkg && /^version = "[^"]*"$/ {
              sub(/"[^"]*"/, "\"" v "\"")
            }
            { print }
          ' Cargo.toml > Cargo.toml.new
          mv Cargo.toml.new Cargo.toml
          grep -n "^version = " Cargo.toml | head -n 1

      - name: Refresh Cargo.lock
        run: cargo update -p bzr

      - name: Prepend [Unreleased] to CHANGELOG.md
        run: |
          set -euo pipefail
          if grep -q '^## \[Unreleased\]' CHANGELOG.md; then
            echo "[Unreleased] section already present; skipping prepend."
            exit 0
          fi
          awk '
            BEGIN { inserted = 0 }
            !inserted && /^## \[/ {
              print "## [Unreleased]"
              print ""
              inserted = 1
            }
            { print }
          ' CHANGELOG.md > CHANGELOG.md.new
          mv CHANGELOG.md.new CHANGELOG.md

      - name: Commit and push branch
        env:
          BRANCH: ${{ steps.ver.outputs.branch }}
          NEXT: ${{ steps.ver.outputs.next }}
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git checkout -b "$BRANCH"
          git add Cargo.toml Cargo.lock CHANGELOG.md
          if git diff --staged --quiet; then
            echo "::error::No changes staged after bump steps; investigate." >&2
            exit 1
          fi
          git commit -m "chore: bump version to $NEXT after $TAG release"
          git push -u origin "$BRANCH"

      - name: Render PR body
        env:
          BRANCH: ${{ steps.ver.outputs.branch }}
          NEXT: ${{ steps.ver.outputs.next }}
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          cat > /tmp/pr-body.md <<EOF
          Automated post-release version bump opened by \`release.yml\` after \`$TAG\` shipped successfully (\`release\` + \`installer-smoke\` green).

          The default bump policy is \`patch\` (\`$TAG\` -> \`$NEXT\`) -- the smallest forward step. If the next planned release is a minor or major bump, edit \`Cargo.toml\`, run \`cargo update -p bzr\` to refresh \`Cargo.lock\`, and amend the version line in this commit before merging.

          Per the dev-version convention in [\`RELEASING.md\`](../blob/main/RELEASING.md), \`Cargo.toml\` on \`main\` between releases carries a \`-dev\` SemVer pre-release suffix so dev builds are visually distinct from the most recent release. \`build.rs\` additionally embeds the git short SHA, so \`bzr --version\` prints e.g. \`bzr $NEXT (abcdef12)\`.

          ### CI on this PR

          Pull requests opened by \`GITHUB_TOKEN\` do not trigger downstream \`pull_request\` workflows. To run CI before merging, push an empty commit:

          \`\`\`bash
          gh pr checkout $BRANCH
          git commit --allow-empty -m "trigger ci"
          git push
          \`\`\`

          Or run the test suite locally (\`cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test\`).

          🤖 Generated with [Claude Code](https://claude.com/claude-code)
          EOF

      - name: Open PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ steps.ver.outputs.branch }}
          NEXT: ${{ steps.ver.outputs.next }}
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          gh pr create \
            --base main \
            --head "$BRANCH" \
            --title "chore: bump version to $NEXT (post-$TAG)" \
            --body-file /tmp/pr-body.md
