ferrflow 7.3.1

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. "publish" runs the configured publishers for the currently-released version (no bump/tag) — for a separate job that has the build toolchain + registry auth the publishers need.'
    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: ''
  package:
    description: 'For mode "publish": space-separated package names to publish (e.g. "api web"). Omit to auto-detect from the triggering tag (GITHUB_REF), falling back to every package.'
    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.ferrflow.com/ferrflow/token.'
    required: false
    default: 'https://api.ferrflow.com/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
      env:
        INPUT_VERSION: ${{ inputs.version }}
        GITHUB_TOKEN: ${{ github.token }}
      run: |
        set -euo pipefail

        if [ "$INPUT_VERSION" != "latest" ] && ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9._-]+)*$'; then
          echo "Invalid version: '$INPUT_VERSION' (expected 'latest' or X.Y.Z)" >&2
          exit 1
        fi

        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" ;;
          armv7l|armv7)  ARCH_NAME="armv7" ;;
          *)             echo "Unsupported architecture: $ARCH" && exit 1 ;;
        esac

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

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

        INSTALL_DIR="${RUNNER_TEMP:-$HOME/.local/bin}/ferrflow-bin"
        DOWNLOAD_DIR="$(mktemp -d)"
        mkdir -p "$INSTALL_DIR"

        curl --fail --location --silent --show-error \
          --output "${DOWNLOAD_DIR}/${ARCHIVE}" "${BASE}/${ARCHIVE}"

        # Verify the digest before anything is extracted or executed. A
        # release without SHA256SUMS predates #789; warn rather than break
        # every consumer pinned to an older version.
        if curl --fail --location --silent --show-error \
             --output "${DOWNLOAD_DIR}/SHA256SUMS" "${BASE}/SHA256SUMS"; then
          grep " \*\?${ARCHIVE}\$" "${DOWNLOAD_DIR}/SHA256SUMS" > "${DOWNLOAD_DIR}/expected.sums" || true
          if [ ! -s "${DOWNLOAD_DIR}/expected.sums" ]; then
            echo "SHA256SUMS has no entry for ${ARCHIVE} — refusing to install" >&2
            exit 1
          fi
          ( cd "$DOWNLOAD_DIR" && sha256sum -c expected.sums )
          echo "Verified ${ARCHIVE} against SHA256SUMS"
        else
          echo "::warning::No SHA256SUMS published for this release; skipping checksum verification. Pin a version >= 5.52.0 to get verified installs."
        fi

        # Provenance is bound to a specific release, so it can only be
        # checked on a pinned version. gh is preinstalled on GitHub-hosted
        # runners; skip rather than fail where it isn't available.
        if [ "$INPUT_VERSION" != "latest" ] && command -v gh >/dev/null 2>&1 && [ -n "${GITHUB_TOKEN:-}" ]; then
          if gh attestation verify "${DOWNLOAD_DIR}/${ARCHIVE}" --repo FerrLabs/FerrFlow >/dev/null 2>&1; then
            echo "Verified build provenance for ${ARCHIVE}"
          else
            echo "::warning::Could not verify build provenance for ${ARCHIVE} (older release, or gh attestation unavailable)"
          fi
        fi

        if [ "$PLATFORM" = "windows" ]; then
          unzip -q -o "${DOWNLOAD_DIR}/${ARCHIVE}" -d "$INSTALL_DIR"
          # No chmod needed on Windows; the .exe is already executable.
        else
          tar -xzf "${DOWNLOAD_DIR}/${ARCHIVE}" -C "$INSTALL_DIR"
          chmod +x "$INSTALL_DIR/ferrflow"
        fi

        rm -rf "$DOWNLOAD_DIR"
        echo "$INSTALL_DIR" >> "$GITHUB_PATH"

    # 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: Optimize commit-graph
      if: inputs.mode != 'publish'
      shell: bash
      run: git commit-graph write --reachable 2>/dev/null || true

    - 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 }}
        INPUT_DRY_RUN: ${{ inputs.dry_run }}
        INPUT_FORCE_VERSION: ${{ inputs.force_version }}
      run: |
        set -euo pipefail

        args=()
        [ "$INPUT_DRY_RUN" = "true" ] && args+=(--dry-run)
        args+=(release)

        if [ -n "$INPUT_FORCE_VERSION" ]; then
          if ! printf '%s' "$INPUT_FORCE_VERSION" | grep -Eq '^([A-Za-z0-9._@/-]+@)?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9._-]+)*$'; then
            echo "Invalid force_version: '$INPUT_FORCE_VERSION' (expected VERSION or NAME@VERSION)" >&2
            exit 1
          fi
          args+=(--force-version "$INPUT_FORCE_VERSION")
        fi

        ferrflow "${args[@]}"

    - name: Run FerrFlow publishers
      if: inputs.mode == 'publish'
      shell: bash
      env:
        FERRFLOW_BOT: ${{ inputs.bot }}
        FERRFLOW_BOT_ENDPOINT: ${{ inputs.bot_endpoint }}
        FERRFLOW_BOT_AUDIENCE: ${{ inputs.bot_audience }}
        INPUT_DRY_RUN: ${{ inputs.dry_run }}
        INPUT_PACKAGE: ${{ inputs.package }}
      run: |
        set -euo pipefail

        args=()
        [ "$INPUT_DRY_RUN" = "true" ] && args+=(--dry-run)
        args+=(publish)

        read -ra packages <<< "$INPUT_PACKAGE"
        for pkg in ${packages[@]+"${packages[@]}"}; do
          if ! printf '%s' "$pkg" | grep -Eq '^[A-Za-z0-9._@/-]+$'; then
            echo "Invalid package name: '$pkg'" >&2
            exit 1
          fi
          args+=("$pkg")
        done

        ferrflow "${args[@]}"