commitbot 0.6.4

A CLI assistant that generates commit and PR messages from your diffs using LLMs.
Documentation
name: Release

on:
  workflow_dispatch:
    inputs:
      target_branch:
        description: Release branch
        required: true
        type: choice
        options:
          - develop
          - main
      version:
        description: Release version (X.Y.Z)
        required: true
        type: string
  pull_request:
    types: [closed]
    branches:
      - develop
      - main

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

concurrency:
  group: release-${{ github.event_name == 'workflow_dispatch' && inputs.target_branch || github.event.pull_request.base.ref }}
  cancel-in-progress: false

env:
  CARGO_TERM_COLOR: always
  MAIN_BASE_VERSION: ${{ vars.MAIN_BASE_VERSION }}
  DEVELOP_BASE_VERSION: ${{ vars.DEVELOP_BASE_VERSION }}

jobs:
  plan:
    if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    outputs:
      should_release: ${{ steps.plan.outputs.should_release }}
      reason: ${{ steps.plan.outputs.reason }}
      bump: ${{ steps.plan.outputs.bump }}
      base_version: ${{ steps.plan.outputs.base_version }}
      previous_version: ${{ steps.plan.outputs.previous_version }}
      version: ${{ steps.plan.outputs.version }}
      target_branch: ${{ steps.plan.outputs.target_branch }}
      source_branch: ${{ steps.plan.outputs.source_branch }}
      merge_sha: ${{ steps.context.outputs.merge_sha }}
    steps:
      - name: Checkout selected branch
        if: github.event_name == 'workflow_dispatch'
        uses: actions/checkout@v4
        with:
          ref: ${{ inputs.target_branch }}
          fetch-depth: 0

      - name: Checkout merge commit
        if: github.event_name == 'pull_request'
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.merge_commit_sha }}
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --force --tags

      - name: Resolve base versions
        id: bases
        shell: bash
        run: |
          set -euo pipefail

          current_version="$(
            sed -nE 's/^version = "([0-9]+\.[0-9]+\.[0-9]+)"/\1/p' Cargo.toml | head -n1
          )"

          if [[ -z "${current_version}" ]]; then
            echo "Could not determine the current Cargo version." >&2
            exit 1
          fi

          echo "main_base_version=${MAIN_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT"
          echo "develop_base_version=${DEVELOP_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT"

      - name: Resolve release context
        id: context
        shell: bash
        run: |
          set -euo pipefail

          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            echo "target_branch=${{ inputs.target_branch }}" >> "$GITHUB_OUTPUT"
            echo "source_branch=workflow_dispatch" >> "$GITHUB_OUTPUT"
            echo "release_version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
            echo "merge_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
          else
            echo "target_branch=${{ github.event.pull_request.base.ref }}" >> "$GITHUB_OUTPUT"
            echo "source_branch=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT"
            echo "release_version=" >> "$GITHUB_OUTPUT"
            echo "merge_sha=${{ github.event.pull_request.merge_commit_sha }}" >> "$GITHUB_OUTPUT"
          fi

      - name: Plan release
        id: plan
        env:
          TARGET_BRANCH: ${{ steps.context.outputs.target_branch }}
          SOURCE_BRANCH: ${{ steps.context.outputs.source_branch }}
          RELEASE_VERSION: ${{ steps.context.outputs.release_version }}
          MAIN_BASE_VERSION: ${{ steps.bases.outputs.main_base_version }}
          DEVELOP_BASE_VERSION: ${{ steps.bases.outputs.develop_base_version }}
        run: devops/ga-release-plan.sh

      - name: Stop on non-release branches
        if: steps.plan.outputs.should_release != 'true'
        shell: bash
        run: |
          echo "Skipping release: ${{ steps.plan.outputs.reason }}"

  prepare_release:
    needs: plan
    if: needs.plan.outputs.should_release == 'true'
    runs-on: ubuntu-latest
    outputs:
      release_sha: ${{ steps.release_commit.outputs.release_sha }}
    steps:
      - name: Checkout target branch
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.plan.outputs.target_branch }}
          fetch-depth: 0

      - name: Fetch tags
        run: git fetch --force --tags

      - name: Verify tag does not already exist
        shell: bash
        run: |
          if git rev-parse "refs/tags/${{ needs.plan.outputs.version }}" >/dev/null 2>&1; then
            echo "Tag ${{ needs.plan.outputs.version }} already exists."
            exit 1
          fi

      - name: Ensure release commit includes merged change
        if: github.event_name == 'pull_request'
        shell: bash
        run: |
          set -euo pipefail

          if ! git merge-base --is-ancestor "${{ needs.plan.outputs.merge_sha }}" HEAD; then
            echo "The target branch no longer contains merge commit ${{ needs.plan.outputs.merge_sha }}." >&2
            exit 1
          fi

      - name: Configure git author
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Create release commit
        id: release_commit
        shell: bash
        run: |
          set -euo pipefail

          version="${{ needs.plan.outputs.version }}"

          perl -pi -e 's/^version = "[0-9]+\.[0-9]+\.[0-9]+"/version = "'"$version"'"/' Cargo.toml
          perl -0pi -e 's/name = "commitbot"\nversion = "[0-9]+\.[0-9]+\.[0-9]+"/name = "commitbot"\nversion = "'"$version"'"/m' Cargo.lock

          if git diff --quiet -- Cargo.toml Cargo.lock; then
            echo "Release version ${version} is already present in Cargo metadata." >&2
            exit 1
          fi

          git add Cargo.toml Cargo.lock
          git commit -m "Release ${version}"
          git tag "${version}"

          echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

      - name: Push release commit and tag
        shell: bash
        run: |
          set -euo pipefail
          git push origin "HEAD:${{ needs.plan.outputs.target_branch }}"
          git push origin "refs/tags/${{ needs.plan.outputs.version }}"

  create_release:
    needs: [plan, prepare_release]
    if: needs.plan.outputs.should_release == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Check out release commit
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare_release.outputs.release_sha }}

      - name: Generate release notes
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail

          NOTES_ARGS=()
          if [[ "${{ needs.plan.outputs.previous_version }}" != "${{ needs.plan.outputs.base_version }}" ]]; then
            NOTES_ARGS+=(-f previous_tag_name="${{ needs.plan.outputs.previous_version }}")
          fi

          gh api \
            -H "Accept: application/vnd.github+json" \
            "/repos/${{ github.repository }}/releases/generate-notes" \
            -f tag_name="${{ needs.plan.outputs.version }}" \
            -f target_commitish="${{ needs.prepare_release.outputs.release_sha }}" \
            "${NOTES_ARGS[@]}" \
            --jq .body > generated-release-notes.md

          bash devops/render-release-notes.sh \
            "${{ needs.plan.outputs.version }}" \
            "${{ github.repository }}" \
            generated-release-notes.md > release-notes.md

      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail

          gh release create "${{ needs.plan.outputs.version }}" \
            --draft \
            --verify-tag \
            --target "${{ needs.prepare_release.outputs.release_sha }}" \
            --title "${{ needs.plan.outputs.version }}" \
            --notes-file release-notes.md

      - name: Dispatch release-assets workflow
        id: dispatch_assets
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail

          dispatch_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          source_run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"

          gh workflow run release-assets.yml \
            --ref "${{ needs.plan.outputs.target_branch }}" \
            -f tag="${{ needs.plan.outputs.version }}" \
            -f source_run_id="${{ github.run_id }}" \
            -f source_run_url="${source_run_url}"

          assets_run_url=""
          for _ in {1..12}; do
            sleep 5

            assets_run_url="$(
              gh api "/repos/${{ github.repository }}/actions/workflows/release-assets.yml/runs?event=workflow_dispatch&branch=${{ needs.plan.outputs.target_branch }}&per_page=20" \
                --jq '.workflow_runs
                  | map(select(.head_sha == "${{ needs.prepare_release.outputs.release_sha }}" and .status != null))
                  | sort_by(.created_at)
                  | reverse
                  | .[0].html_url // ""'
            )"

            if [[ -n "${assets_run_url}" ]]; then
              break
            fi
          done

          echo "dispatch_started_at=${dispatch_started_at}" >> "$GITHUB_OUTPUT"
          echo "assets_run_url=${assets_run_url}" >> "$GITHUB_OUTPUT"

      - name: Add release summary
        shell: bash
        run: |
          release_url="https://github.com/${{ github.repository }}/releases/tag/${{ needs.plan.outputs.version }}"
          assets_url="${{ steps.dispatch_assets.outputs.assets_run_url }}"

          {
            echo "## Release"
            echo
            echo "- Version: \`${{ needs.plan.outputs.version }}\`"
            echo "- Release: [${{ needs.plan.outputs.version }}](${release_url})"
            if [[ -n "${assets_url}" ]]; then
              echo "- Release assets workflow: [Open run](${assets_url})"
            else
              echo "- Release assets workflow: dispatched, but the run URL was not discovered yet."
              echo "- Release assets workflow page: [Open workflow](https://github.com/${{ github.repository }}/actions/workflows/release-assets.yml)"
            fi
          } >> "$GITHUB_STEP_SUMMARY"