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/v1/ferrflow/token.'
required: false
default: 'https://api.ferrflow.com/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
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"
- 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[@]}"