ferrflow 5.1.0

Universal semantic versioning for monorepos and classic repos
Documentation
name: 'FerrFlow Release'
description: 'Universal semantic versioning for monorepos and classic repos'
author: 'FerrLabs'

branding:
  icon: 'tag'
  color: 'orange'

inputs:
  version:
    description: 'FerrFlow version to use (e.g. 1.2.3). Defaults to latest.'
    required: false
    default: 'latest'
  mode:
    description: 'Action mode: "release" runs the full release pipeline. "preview" posts a PR comment with version bump preview.'
    required: false
    default: 'release'
  dry_run:
    description: 'Run without creating releases or pushing changes'
    required: false
    default: 'false'
  force_version:
    description: 'Force a specific version, skipping commit analysis. Format: VERSION (single repo) or NAME@VERSION (monorepo)'
    required: false
    default: ''
  bot:
    description: 'Opt into the hosted FerrFlow bot identity (ferrflow[bot]). Requires permissions.id-token: write on the caller workflow. If false/unset, the caller''s token or GITHUB_TOKEN is used as before.'
    required: false
    default: 'false'
  bot_endpoint:
    description: 'Override the hosted bot token endpoint. Defaults to https://api.ferrlabs.com/api/v1/ferrflow/token.'
    required: false
    default: 'https://api.ferrlabs.com/api/v1/ferrflow/token'
  bot_audience:
    description: 'OIDC audience requested from the GitHub Actions runner. Must match the server-side expected audience.'
    required: false
    default: 'ferrflow.ferrlabs.com'

runs:
  using: composite
  steps:
    - name: Install FerrFlow
      shell: bash
      run: |
        set -euo pipefail

        OS=$(uname -s | tr '[:upper:]' '[:lower:]')
        ARCH=$(uname -m)

        # Git Bash / MSYS / Cygwin on Windows runners all report a compound
        # kernel name like "mingw64_nt-10.0". Normalise to "windows".
        case "$OS" in
          linux)                  PLATFORM="linux"   ; EXT="tar.gz" ;;
          darwin)                 PLATFORM="darwin"  ; EXT="tar.gz" ;;
          mingw*|msys*|cygwin*)   PLATFORM="windows" ; EXT="zip"    ;;
          *)                      echo "Unsupported OS: $OS" && exit 1 ;;
        esac

        case "$ARCH" in
          x86_64)        ARCH_NAME="x64" ;;
          aarch64|arm64) ARCH_NAME="arm64" ;;
          *)             echo "Unsupported architecture: $ARCH" && exit 1 ;;
        esac

        ARCHIVE="ferrflow-${PLATFORM}-${ARCH_NAME}.${EXT}"

        if [ "${{ inputs.version }}" = "latest" ]; then
          URL="https://github.com/FerrLabs/FerrFlow/releases/latest/download/${ARCHIVE}"
        else
          URL="https://github.com/FerrLabs/FerrFlow/releases/download/v${{ inputs.version }}/${ARCHIVE}"
        fi

        INSTALL_DIR="${RUNNER_TEMP:-$HOME/.local/bin}/ferrflow-bin"
        mkdir -p "$INSTALL_DIR"

        if [ "$PLATFORM" = "windows" ]; then
          # Can't pipe into unzip (needs a seekable file), so download first.
          TMP_ZIP="${RUNNER_TEMP:-/tmp}/ferrflow.zip"
          curl --fail --location --silent --show-error --output "$TMP_ZIP" "$URL"
          unzip -q -o "$TMP_ZIP" -d "$INSTALL_DIR"
          rm -f "$TMP_ZIP"
          # No chmod needed on Windows; the .exe is already executable.
        else
          curl --fail --location --silent --show-error "$URL" | tar -xz -C "$INSTALL_DIR"
          chmod +x "$INSTALL_DIR/ferrflow"
        fi

        echo "$INSTALL_DIR" >> "$GITHUB_PATH"

    - name: Strip cached GITHUB_TOKEN credentials
      # actions/checkout writes `AUTHORIZATION: basic <GITHUB_TOKEN>` into
      # the repo's git extraheader. When ferrflow later swaps in the App
      # installation token via OIDC, that extraheader still wins on every
      # `git push` / `git fetch` — so the push authenticates as
      # github-actions[bot] (not ferrflow[bot]) and is rejected by any
      # branch ruleset that lists ferrflow[bot] in its bypass actors.
      #
      # Dropping the cache here means ferrflow's URL-rewrite path
      # (`build_authenticated_url` in src/git.rs) takes over and the bot
      # identity is what reaches the server. Idempotent: a no-op when the
      # caller already set `persist-credentials: false` on the checkout.
      if: inputs.bot == 'true'
      shell: bash
      run: |
        git config --unset-all http.https://github.com/.extraheader || true

    # Git identity (`user.name` / `user.email`) is now set by the
    # binary itself in `ensure_bot_token`, so the action no longer
    # configures it. Self-hosters running their own App can override
    # via `FERRFLOW_BOT_LOGIN` and `FERRFLOW_BOT_USER_ID` env vars on
    # the calling job.

    - name: Preview release
      if: inputs.mode == 'preview'
      shell: bash
      env:
        FERRFLOW_BOT: ${{ inputs.bot }}
        FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
        FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
      run: ferrflow check --comment

    - name: Run FerrFlow release
      if: inputs.mode == 'release'
      shell: bash
      env:
        FERRFLOW_BOT: ${{ inputs.bot }}
        FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
        FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
      run: ferrflow ${{ inputs.dry_run == 'true' && '--dry-run' || '' }} release ${{ inputs.force_version != '' && format('--force-version {0}', inputs.force_version) || '' }}