via-cli 0.2.0

Run commands and API requests with 1Password-backed credentials without exposing secrets to your shell
Documentation
name: Prepare Release

on:
  push:
    tags:
      - "v*"
  pull_request:
    types:
      - closed
    branches:
      - master
  workflow_dispatch:
    inputs:
      version:
        description: "Release version to prepare (for example 0.2.0 or v0.2.0)"
        required: true
        type: string

permissions:
  actions: write
  contents: write
  pull-requests: write

env:
  CARGO_TERM_COLOR: always

jobs:
  prepare:
    name: Open version-bump PR
    if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-24.04

    steps:
      - name: Checkout master
        uses: actions/checkout@v4
        with:
          ref: master

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal
          rustup default stable

      - name: Normalize release version
        id: release
        shell: bash
        env:
          INPUT_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
        run: |
          case "$INPUT_VERSION" in
            v*) CRATE_VERSION="${INPUT_VERSION#v}" ;;
            *) CRATE_VERSION="$INPUT_VERSION" ;;
          esac

          case "$CRATE_VERSION" in
            ''|*[!0-9A-Za-z.+-]*)
              echo "::error::version may only contain ASCII letters, digits, '.', '+', or '-'"
              exit 1
              ;;
            [0-9]*) ;;
            *)
              echo "::error::version must start with a digit, with or without a leading v"
              exit 1
              ;;
          esac

          RELEASE_TAG="v$CRATE_VERSION"
          if [ "$GITHUB_EVENT_NAME" != "push" ] && git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then
            echo "::error::tag $RELEASE_TAG already exists"
            exit 1
          fi

          echo "crate_version=$CRATE_VERSION" >> "$GITHUB_OUTPUT"
          echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"

      - name: Update Cargo version
        shell: bash
        env:
          CRATE_VERSION: ${{ steps.release.outputs.crate_version }}
        run: |
          perl -0pi -e 's/^version = "[^"]+"/version = "$ENV{CRATE_VERSION}"/m' Cargo.toml
          cargo update -p via-cli --precise "$CRATE_VERSION"

      - name: Validate release version
        shell: bash
        env:
          CRATE_VERSION: ${{ steps.release.outputs.crate_version }}
        run: |
          MANIFEST_VERSION="$(cargo pkgid --locked -p via-cli)"
          MANIFEST_VERSION="${MANIFEST_VERSION##*@}"
          if [ "$MANIFEST_VERSION" != "$CRATE_VERSION" ]; then
            echo "::error::Cargo.toml/Cargo.lock resolve to $MANIFEST_VERSION, expected $CRATE_VERSION"
            exit 1
          fi

          cargo test --locked

      - name: Delete request tag
        if: ${{ github.event_name == 'push' }}
        shell: bash
        env:
          RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
        run: |
          git push origin ":refs/tags/$RELEASE_TAG"

      - name: Open pull request
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          CRATE_VERSION: ${{ steps.release.outputs.crate_version }}
          RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
        run: |
          BRANCH="release/$RELEASE_TAG"

          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git fetch origin "refs/heads/$BRANCH:refs/remotes/origin/$BRANCH" || true
          git switch -c "$BRANCH"
          git add Cargo.toml Cargo.lock

          if git diff --cached --quiet; then
            echo "::error::No Cargo version changes to commit"
            exit 1
          fi

          git commit -m "chore: prepare $RELEASE_TAG"
          git push --force-with-lease origin "HEAD:refs/heads/$BRANCH"

          PR_URL="$(gh pr list --head "$BRANCH" --state open --json url --jq '.[0].url // ""')"
          if [ -n "$PR_URL" ]; then
            echo "Release PR already exists: $PR_URL"
            exit 0
          fi

          cat > pr-body.md <<EOF
          Updates Cargo.toml and Cargo.lock to via-cli $CRATE_VERSION.

          After this PR is merged, the release workflow will create $RELEASE_TAG on the merge commit, publish crates.io, and build release artifacts.
          EOF

          gh pr create \
            --base master \
            --head "$BRANCH" \
            --title "chore: prepare $RELEASE_TAG" \
            --body-file pr-body.md

  finalize:
    name: Tag and dispatch release
    if: >-
      ${{
        github.event_name == 'pull_request' &&
        github.event.pull_request.merged == true &&
        github.event.pull_request.head.repo.full_name == github.repository &&
        startsWith(github.event.pull_request.head.ref, 'release/v')
      }}
    runs-on: ubuntu-24.04

    steps:
      - name: Checkout merge commit
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal
          rustup default stable

      - name: Validate merged release
        id: release
        shell: bash
        env:
          HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
        run: |
          RELEASE_TAG="${HEAD_BRANCH#release/}"
          case "$RELEASE_TAG" in
            v*) ;;
            *)
              echo "::error::release branch must be named release/v*"
              exit 1
              ;;
          esac

          CRATE_VERSION="${RELEASE_TAG#v}"
          case "$CRATE_VERSION" in
            ''|*[!0-9A-Za-z.+-]*)
              echo "::error::release version may only contain ASCII letters, digits, '.', '+', or '-'"
              exit 1
              ;;
            [0-9]*) ;;
            *)
              echo "::error::release version must start with a digit"
              exit 1
              ;;
          esac

          MANIFEST_VERSION="$(cargo pkgid --locked -p via-cli)"
          MANIFEST_VERSION="${MANIFEST_VERSION##*@}"
          if [ "$MANIFEST_VERSION" != "$CRATE_VERSION" ]; then
            echo "::error::$HEAD_BRANCH requires via-cli $CRATE_VERSION, but Cargo.toml/Cargo.lock resolve to $MANIFEST_VERSION"
            exit 1
          fi

          if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then
            echo "::error::tag $RELEASE_TAG already exists"
            exit 1
          fi

          echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"

      - name: Create release tag
        shell: bash
        env:
          RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
          MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git tag "$RELEASE_TAG" "$MERGE_SHA"
          git push origin "refs/tags/$RELEASE_TAG"

      - name: Dispatch release workflows
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          RELEASE_TAG: ${{ steps.release.outputs.release_tag }}
        run: |
          gh workflow run publish-crate.yml --ref master -f tag="$RELEASE_TAG"
          gh workflow run build-binaries.yml --ref master -f release_tag="$RELEASE_TAG"