Freeze the engine half of fabro-workflow to bug fixes

The integration plan's F4.1: once Fabro runs on Petri, the engine half of
fabro-workflow (handler/, lifecycle/, pipeline/execute, graph/routing,
node_handler, retry, condition, context, model_fallback) takes bug fixes
only, and new engine behaviour goes to Petri.

scripts/check-engine-freeze.sh holds the frozen path list, diffs the
branch against a base ref and exits 1 when any frozen file gained lines;
scripts/check-engine-freeze-test.sh proves that on a synthetic
repository. The Engine freeze workflow runs both on every pull request
that touches the crate's src, re-runs on label changes, and fails unless
the pull request carries the `bugfix` label. AGENTS.md and the
fabro-petri README name the freeze, the label and the script.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-18 02:19:29 -04:00
parent 6e38ad27fb
commit 806ac90c98
No known key found for this signature in database
5 changed files with 214 additions and 0 deletions

53
.github/workflows/engine-freeze.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Engine freeze
# The engine half of fabro-workflow takes bug fixes only; new engine behaviour
# goes to Petri. A pull request that adds lines under the frozen paths fails
# here unless it carries the `bugfix` label. The frozen paths live in
# `scripts/check-engine-freeze.sh`, which runs locally the same way.
# Labeling re-runs the check so a label added after a failure clears it.
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- "lib/components/fabro-workflow/src/**"
- "scripts/check-engine-freeze.sh"
- "scripts/check-engine-freeze-test.sh"
- ".github/workflows/engine-freeze.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
freeze:
name: Engine half takes bug fixes only
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# The check diffs against the merge base with the base branch.
fetch-depth: 0
- name: Self-test the check
run: scripts/check-engine-freeze-test.sh
- name: Check the frozen paths
env:
BASE_REF: ${{ github.base_ref }}
HAS_BUGFIX_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'bugfix') }}
run: |
git fetch --no-tags origin "$BASE_REF"
if scripts/check-engine-freeze.sh "origin/$BASE_REF"; then
exit 0
fi
if [ "$HAS_BUGFIX_LABEL" = "true" ]; then
echo "The 'bugfix' label waives the engine freeze for this pull request."
exit 0
fi
echo "::error::This pull request adds lines to the frozen engine half of fabro-workflow (see the log for the paths). New engine behaviour goes to Petri (lib/components/fabro-petri). A bug fix needs the 'bugfix' label."
exit 1

View file

@ -136,6 +136,10 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as
- **fabro-telemetry** — CLI analytics (Segment) and crash reporting (Sentry), with anonymous IDs, command sanitization, and detached subprocess delivery
- **fabro-util** — Shared utilities (redaction, terminal formatting)
### Engine freeze
The engine half of `fabro-workflow` takes bug fixes only: `handler/`, `lifecycle/`, `pipeline/execute` (the file and the directory), `graph/routing.rs`, `node_handler.rs`, `retry.rs`, `condition.rs`, `context.rs` and `model_fallback.rs` under `lib/components/fabro-workflow/src/`. New engine behaviour goes to Petri through `fabro-petri`. The `Engine freeze` CI check (`.github/workflows/engine-freeze.yml`) fails a pull request that adds lines under those paths unless it carries the `bugfix` label. The path list lives in `scripts/check-engine-freeze.sh`, which runs locally as `scripts/check-engine-freeze.sh origin/main` and reports the added lines; `scripts/check-engine-freeze-test.sh` is its self-test.
### TypeScript (`apps/` and `lib/packages/`)
- **apps/fabro-web** — React 19 + React Router + Tailwind CSS frontend, bundled by a custom Bun script (`apps/fabro-web/scripts/build.ts`), not Vite
- **lib/packages/fabro-api-client** — Auto-generated TypeScript Axios client from OpenAPI spec

View file

@ -10,6 +10,18 @@ member that lists them as dependencies. Every other Fabro crate reaches the
engine through what this crate exports. A Petri pin move is therefore a change
to this crate and the lockfile, nothing else.
## Engine freeze
The engine half of `fabro-workflow` (`handler/`, `lifecycle/`,
`pipeline/execute`, `graph/routing.rs`, `node_handler.rs`, `retry.rs`,
`condition.rs`, `context.rs` and `model_fallback.rs` under its `src/`) takes
bug fixes only. New engine behaviour goes to Petri and reaches Fabro through
this crate. The `Engine freeze` CI check
(`.github/workflows/engine-freeze.yml`) fails a pull request that adds lines
under those paths unless it carries the `bugfix` label. The path list is in
`scripts/check-engine-freeze.sh`; run it locally as
`scripts/check-engine-freeze.sh origin/main` to see what a branch adds there.
## What it holds
Every adapter the integration plan describes lands here.

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Self-test for scripts/check-engine-freeze.sh against a synthetic repository:
# an added line under a frozen path exits 1, and deletions or additions
# elsewhere exit 0.
set -euo pipefail
CHECK="$(cd "$(dirname "$0")" && pwd)/check-engine-freeze.sh"
SRC="lib/components/fabro-workflow/src"
export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.com
export GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.com
repo="$(mktemp -d)"
trap 'rm -rf "$repo"' EXIT
cd "$repo"
git init -q -b main
mkdir -p "$SRC/handler" "$SRC/pipeline/execute" "$SRC/transforms"
printf 'a\nb\nc\n' > "$SRC/handler/agent.rs"
printf 'a\nb\n' > "$SRC/pipeline/execute/tests.rs"
printf 'a\n' > "$SRC/retry.rs"
printf 'a\n' > "$SRC/transforms/preamble.rs"
printf 'a\n' > "$SRC/pipeline/finalize.rs"
git add -A
git commit -q -m base
failures=0
expect() {
local name="$1" want="$2"
git checkout -q -b "$name" main
"case_$name"
git add -A
git commit -q --allow-empty -m "$name"
local got=0
"$CHECK" main > /dev/null || got=$?
if [ "$got" -eq "$want" ]; then
echo "ok $name (exit $got)"
else
echo "FAIL $name: expected exit $want, got $got"
failures=$((failures + 1))
fi
git checkout -q main
}
case_adds_to_frozen_file() { echo d >> "$SRC/handler/agent.rs"; }
case_adds_to_frozen_dir() { echo c >> "$SRC/pipeline/execute/tests.rs"; }
case_rewrites_frozen_line() { printf 'a\nB\nc\n' > "$SRC/handler/agent.rs"; }
case_deletes_from_frozen_file() { printf 'a\n' > "$SRC/handler/agent.rs"; }
case_adds_outside_freeze() {
echo b >> "$SRC/transforms/preamble.rs"
echo b >> "$SRC/pipeline/finalize.rs"
}
case_no_change() { :; }
expect adds_to_frozen_file 1
expect adds_to_frozen_dir 1
expect rewrites_frozen_line 1
expect deletes_from_frozen_file 0
expect adds_outside_freeze 0
expect no_change 0
if [ "$failures" -ne 0 ]; then
echo "$failures case(s) failed"
exit 1
fi
echo "all cases passed"

80
scripts/check-engine-freeze.sh Executable file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Report added lines under the frozen engine half of fabro-workflow.
#
# The engine half of `lib/components/fabro-workflow` takes bug fixes only;
# new engine behaviour goes to Petri (`lib/components/fabro-petri`). This
# script lists every frozen file the current branch adds lines to, compared
# with the base ref, and exits 1 when there is at least one. It knows nothing
# about pull request labels: the CI job (`.github/workflows/engine-freeze.yml`)
# waives a failure when the pull request carries the `bugfix` label.
#
# Usage: scripts/check-engine-freeze.sh [<base-ref>] (default: origin/main)
set -euo pipefail
BASE_REF="${1:-origin/main}"
LABEL="bugfix"
# The frozen paths, relative to the fabro-workflow crate's `src/`. A directory
# freezes everything under it. `preamble` in the integration plan is
# `handler/llm/preamble.rs`, which `handler/` covers; `transforms/preamble.rs`
# is a graph transform and is not frozen.
FROZEN=(
handler
lifecycle
pipeline/execute.rs
pipeline/execute
graph/routing.rs
node_handler.rs
retry.rs
condition.rs
context.rs
model_fallback.rs
)
if [ "${FREEZE_LIST_ONLY:-}" = "1" ]; then
printf '%s\n' "${FROZEN[@]}"
exit 0
fi
ROOT="$(git rev-parse --show-toplevel)"
CRATE_SRC="lib/components/fabro-workflow/src"
paths=()
for entry in "${FROZEN[@]}"; do
paths+=("$CRATE_SRC/$entry")
done
# Three dots: the changes since the merge base, which is what a pull request
# adds to its base branch.
numstat="$(git -C "$ROOT" diff --numstat "$BASE_REF...HEAD" -- "${paths[@]}")"
offenders=()
while IFS=$'\t' read -r added _deleted path; do
[ -n "${path:-}" ] || continue
case "$added" in
''|*[!0-9]*) continue ;; # binary files report '-'
esac
if [ "$added" -gt 0 ]; then
offenders+=("$added $path")
fi
done <<< "$numstat"
if [ "${#offenders[@]}" -eq 0 ]; then
echo "engine freeze: no lines added under the frozen paths of fabro-workflow since $BASE_REF"
exit 0
fi
echo "engine freeze: this branch adds lines to the frozen engine half of fabro-workflow (since $BASE_REF):"
for line in "${offenders[@]}"; do
echo " +${line%% *} ${line#* }"
done
echo
echo "The engine half of lib/components/fabro-workflow takes bug fixes only."
echo "New engine behaviour goes to Petri (lib/components/fabro-petri)."
echo "Frozen paths under $CRATE_SRC/:"
for entry in "${FROZEN[@]}"; do
echo " $entry"
done
echo
echo "A bug fix passes CI when the pull request carries the '$LABEL' label."
exit 1