codetether-agent 4.0.0

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
name: Release

on:
  workflow_dispatch:
    inputs:
      bump:
        description: Semver bump type
        required: true
        default: patch
        type: choice
        options:
          - patch
          - minor
          - major
      version_override:
        description: Optional explicit version (for example 0.2.1). If set, bump is ignored.
        required: false
        default: ""
        type: string
      publish_crates:
        description: Publish crate to crates.io
        required: true
        default: true
        type: boolean
      prerelease:
        description: Mark GitHub release as prerelease
        required: true
        default: false
        type: boolean
      dry_run:
        description: Compute and build only (do not push tag/commit, do not publish)
        required: true
        default: false
        type: boolean

permissions:
  contents: write

concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: false

jobs:
  prepare:
    name: Prepare Version + Tag
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.next }}
      tag: ${{ steps.version.outputs.tag }}
      sha: ${{ steps.gitmeta.outputs.sha }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.RELEASE_PAT || github.token }}

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Compute next version
        id: version
        env:
          INPUT_BUMP: ${{ inputs.bump }}
          INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
        run: |
          set -euo pipefail
          current="$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name=="codetether-agent").version')"
          if [ -n "${INPUT_VERSION_OVERRIDE}" ]; then
            next="${INPUT_VERSION_OVERRIDE#v}"
          else
            IFS='.' read -r major minor patch <<< "${current}"
            case "${INPUT_BUMP}" in
              major)
                major=$((major + 1))
                minor=0
                patch=0
                ;;
              minor)
                minor=$((minor + 1))
                patch=0
                ;;
              patch)
                patch=$((patch + 1))
                ;;
              *)
                echo "Unsupported bump type: ${INPUT_BUMP}" >&2
                exit 1
                ;;
            esac
            next="${major}.${minor}.${patch}"
          fi
          tag="v${next}"
          echo "current=${current}" >> "$GITHUB_OUTPUT"
          echo "next=${next}" >> "$GITHUB_OUTPUT"
          echo "tag=${tag}" >> "$GITHUB_OUTPUT"
          echo "Current version: ${current}"
          echo "Next version: ${next}"

      - name: Ensure release tag does not already exist on origin
        run: |
          set -euo pipefail
          tag="${{ steps.version.outputs.tag }}"
          if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
            echo "Tag ${tag} already exists on origin." >&2
            exit 1
          fi

      - name: Install cargo-edit
        if: ${{ steps.version.outputs.current != steps.version.outputs.next }}
        run: cargo install cargo-edit --locked

      - name: Bump Cargo package version
        if: ${{ steps.version.outputs.current != steps.version.outputs.next }}
        run: |
          set -euo pipefail
          cargo set-version "${{ steps.version.outputs.next }}"
          cargo check

      - name: Commit and tag
        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 Cargo.toml
          # Only stage Cargo.lock when the repository tracks it.
          # Some repos intentionally ignore lockfiles.
          if git ls-files --error-unmatch Cargo.lock >/dev/null 2>&1; then
            git add Cargo.lock
          fi
          if git diff --cached --quiet; then
            echo "No Cargo version changes staged; continuing with tag from current HEAD."
          else
            git commit -m "chore(release): v${{ steps.version.outputs.next }}"
          fi
          git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.tag }}"

      - name: Push commit and tag
        if: ${{ !inputs.dry_run }}
        run: |
          set -euo pipefail
          branch="${GITHUB_REF_NAME}"
          git push origin "HEAD:${branch}"
          git push origin "${{ steps.version.outputs.tag }}"

      - name: Capture commit SHA
        id: gitmeta
        run: |
          set -euo pipefail
          echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

  build:
    name: Build Binaries (${{ matrix.target }})
    needs: prepare
    if: ${{ !inputs.dry_run }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            binary: codetether
            archive: tar.gz
          - os: macos-14
            target: aarch64-apple-darwin
            binary: codetether
            archive: tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            binary: codetether.exe
            archive: zip
    steps:
      - name: Checkout release commit
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare.outputs.sha }}
          fetch-depth: 0

      - name: Setup Rust target
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Build binary
        run: cargo build --release --target ${{ matrix.target }}

      - name: Package Unix artifacts
        if: runner.os != 'Windows'
        run: |
          set -euo pipefail
          mkdir -p dist
          asset="codetether-v${{ needs.prepare.outputs.version }}-${{ matrix.target }}"
          cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/${asset}"
          chmod 755 "dist/${asset}"
          tar -C dist -czf "dist/${asset}.tar.gz" "${asset}"
          ls -lh dist

      - name: Package Windows artifacts
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Path dist -Force | Out-Null
          $asset = "codetether-v${{ needs.prepare.outputs.version }}-${{ matrix.target }}"
          Copy-Item "target\\${{ matrix.target }}\\release\\${{ matrix.binary }}" "dist\\$asset.exe"
          Compress-Archive -Path "dist\\$asset.exe" -DestinationPath "dist\\$asset.zip" -Force
          Get-ChildItem dist

      - name: Upload platform artifacts
        uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.target }}
          path: dist/*
          if-no-files-found: error

  publish:
    name: Publish Crate + GitHub Release
    needs: [prepare, build]
    if: ${{ !inputs.dry_run && !cancelled() }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout release commit
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare.outputs.sha }}
          fetch-depth: 0

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Download release artifacts
        uses: actions/download-artifact@v4
        with:
          pattern: release-*
          merge-multiple: true
          path: dist

      - name: Create checksum manifest
        run: |
          set -euo pipefail
          cd dist
          sha256sum * > "SHA256SUMS-v${{ needs.prepare.outputs.version }}.txt"
          ls -lh

      - name: Publish to crates.io
        if: ${{ inputs.publish_crates }}
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail
          if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
            echo "CARGO_REGISTRY_TOKEN is not set." >&2
            exit 1
          fi
          if git ls-files --error-unmatch Cargo.lock >/dev/null 2>&1; then
            cargo publish --locked
          else
            cargo publish
          fi

      - name: Create GitHub release and upload assets
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.prepare.outputs.tag }}
          name: ${{ needs.prepare.outputs.tag }}
          generate_release_notes: true
          prerelease: ${{ inputs.prerelease }}
          files: |
            dist/*