ebman 0.23.0

k9s-style TUI for AWS Elastic Beanstalk
Documentation
name: release

on:
  push:
    tags:
      - 'v*'
  # `release: published` fires when the maintainer flips the draft
  # release (created by the tag-push run below) to published. The
  # crates_io job below is gated on this event so cargo publish only
  # runs after the maintainer has sanity-checked the GitHub Release
  # artefacts — once published, crates.io can't be unpublished, so the
  # human gate matters.
  release:
    types: [published]
  # Manual re-run, e.g. when CARGO_REGISTRY_TOKEN was missing on the
  # first `release.published` fire and the crates_io job no-op'd.
  # Run from `main` (which has this trigger), passing the tag to
  # publish as an input:
  #   gh workflow run release.yml -f tag=v0.7.0
  # Build + publish-to-GH-release stay gated on `push` (artefacts
  # already exist for the tag); only the crates_io job re-fires.
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to publish to crates.io (e.g. v0.7.0)'
        required: true
        type: string

# When a new tag like `v0.2.0` is pushed:
#   1. Build a release binary on each target platform.
#   2. Tarball it together with README + LICENSE files.
#   3. Attach the tarballs to a GitHub Release for the tag.
# Homebrew can then point at the macOS aarch64 / x86_64 tarballs by URL +
# SHA-256. The workflow uses softprops/action-gh-release which is the
# de-facto standard for this pattern and re-uses GITHUB_TOKEN.
#
# Once the maintainer publishes the draft release, the `crates_io` job
# runs `cargo publish` so crates.io is kept in sync with the GitHub
# Release without a manual `cargo publish` step (which was forgotten
# for 0.4.0 / 0.4.1 — see BACKLOG).

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: short

jobs:
  build:
    name: build ${{ matrix.target }}
    # Only run on tag push — the release.published event reruns the
    # whole workflow but the build matrix already produced the artefacts
    # on the original tag-push run. Skipping here saves ~10 min of CI
    # time per published release.
    if: github.event_name == 'push'
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            archive_ext: tar.gz
          # Native ARM64 Linux runner (GA for public repos) — avoids the
          # cross-compile/linker pain of building aarch64 on an x86 host.
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
            archive_ext: tar.gz
          - target: aarch64-apple-darwin
            runner: macos-latest
            archive_ext: tar.gz
          - target: x86_64-apple-darwin
            runner: macos-latest
            archive_ext: tar.gz
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
      - name: Build release binary
        run: cargo build --locked --release --target ${{ matrix.target }}
      - name: Stage binary + docs
        run: |
          set -euo pipefail
          stage="ebman-${GITHUB_REF_NAME}-${{ matrix.target }}"
          mkdir "$stage"
          cp "target/${{ matrix.target }}/release/ebman" "$stage/"
          cp README.md LICENSE-MIT LICENSE-APACHE "$stage/"
          tar -czf "$stage.tar.gz" "$stage"
          shasum -a 256 "$stage.tar.gz" | tee "$stage.tar.gz.sha256"
      - name: Upload as workflow artifact
        uses: actions/upload-artifact@v4
        with:
          name: ebman-${{ matrix.target }}
          path: |
            ebman-*.tar.gz
            ebman-*.tar.gz.sha256

  publish:
    name: publish release
    needs: build
    # Tag-push only — the draft release attach is part of the build
    # phase. The release.published event doesn't need to re-run this.
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          path: dist
          pattern: ebman-*
          merge-multiple: true
      - name: Attach artefacts to release
        uses: softprops/action-gh-release@v2
        with:
          # GITHUB_REF_NAME is the tag, e.g. v0.2.0.
          tag_name: ${{ github.ref_name }}
          name: ebman ${{ github.ref_name }}
          files: |
            dist/ebman-*.tar.gz
            dist/ebman-*.tar.gz.sha256
          # Don't auto-publish a draft — the maintainer can flip the toggle
          # once they've sanity-checked the artefacts.
          draft: true
          generate_release_notes: true

  crates_io:
    # Publish to crates.io once the maintainer has reviewed the draft
    # GitHub Release and flipped it to published. Gated on the
    # `CARGO_REGISTRY_TOKEN` repository secret — when the secret isn't
    # configured the step is skipped (rather than failing), so forks /
    # scratch installations that don't own the crate don't fail loudly.
    #
    # Two earlier releases (0.4.0 / 0.4.1) shipped as GitHub Releases
    # only because the manual `cargo publish` step was forgotten; the
    # in-app update-check polls crates.io, so the gap surfaced as a
    # stale "latest" reported back to users. Automating closes that
    # gap. The release.published trigger keeps the maintainer in the
    # loop — crates.io can't be unpublished, so the human gate matters.
    name: publish to crates.io
    if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && !github.event.repository.fork
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # The release event payload's `tag_name` is what was published;
          # workflow_dispatch passes the tag via `inputs.tag`. Either
          # way we check out exactly that ref so cargo publishes the
          # released code, not whatever HEAD happens to be at workflow-
          # fire time.
          ref: ${{ github.event.release.tag_name || inputs.tag }}
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
            echo "CARGO_REGISTRY_TOKEN not set — skipping crates.io publish"
            exit 0
          fi
          # The workspace has two publishable crates: `tb-tui-common`
          # (shared TUI helpers, used by ebman + sibling pgman) and
          # `ebman`. ebman depends on tb-tui-common via a path-+-version
          # dep, so the shared crate must land on crates.io first or
          # ebman's manifest fails dep verification.
          #
          # `--locked` matches the build matrix's lockfile pinning so a
          # workflow that built clean from Cargo.lock publishes the same
          # resolved dependency graph.
          #
          # Tolerate the already-published case for tb-tui-common — most
          # ebman releases don't bump the shared crate, so most runs
          # re-attempt a publish that's already happened. We only fail
          # fast if the error isn't the already-published one.
          set +e
          tc_out=$(cargo publish -p tb-tui-common --locked 2>&1)
          tc_exit=$?
          set -e
          echo "$tc_out"
          if [ "$tc_exit" -ne 0 ]; then
            if echo "$tc_out" | grep -qiE "already (exists|uploaded|published)"; then
              echo "tb-tui-common version already on crates.io — continuing to ebman"
            else
              echo "tb-tui-common publish failed (not already-published case)" >&2
              exit "$tc_exit"
            fi
          fi
          cargo publish -p ebman --locked