chore(#905): docs-only CI short-circuit, SPIKE commit with forced classify

Build phase of #904 (plan owner-AGREED 2026-10-19).

Adds a `classify` job to build.yml and flutter_dart.yml (plain in-repo git
diff, no third-party actions) and conditions every step of the 7 heavy
required checks on its docs_only output. Jobs always run and always report
their required check names; on a docs-only diff every step is skipped and a
marker step states why, so the check is an honest fast SUCCESS, never a
pending ghost.

Allowlist (owner-approved on #904): **/*.md, docs/**, LICENSE,
.github/ISSUE_TEMPLATE/**. Everything else is code, all .json included;
empty or unreadable diffs classify as code, so a wrong classification can
only waste a build, never skip one.

THIS COMMIT CARRIES SPIKE_FORCE_DOCS_ONLY=true in both classify jobs: the
machinery PR's own .yml diff would classify as code, so the force is the only
way to prove the short-circuit semantics against the real required checks on
a real dev-based PR. The follow-up commit removes the force, and that same
PR run doubles as the negative test (full matrix on a code diff). The force
must never reach dev.

The em-dash guard stays inside the short-circuit: verified it scans only
lib/ and test/ Dart files, so a docs-only diff cannot contain what it hunts.
That .md files are outside its scope at all is a pre-existing gap, noted on
the PR, not widened here.

gate (release-gate.yml, ~7s) is deliberately untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/918/head
diffwireauto 4 weeks ago committed by Strycher
parent 285249ae7c
commit 88ea3d19eb

@ -8,19 +8,88 @@ on:
pull_request:
jobs:
# #904: decides whether this change touches only documentation. Plain git,
# no third-party actions - the merge gate must not grow a supply-chain
# dependency. Every build job below still RUNS (so all required check names
# report), but short-circuits its steps to an honest success when the diff
# is docs-only. A wrong classification must only ever waste a build, never
# skip one, so every ambiguous case classifies as code.
classify:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.diff.outputs.docs_only }}
env:
# #904 SPIKE ONLY: forces the docs-only path so the short-circuit
# semantics can be proven on the machinery PR itself (whose .yml diff
# would otherwise classify as code). REMOVED in the follow-up commit;
# it must never reach dev.
SPIKE_FORCE_DOCS_ONLY: "true"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Classify changed files
id: diff
run: |
set -eu
if [ "${SPIKE_FORCE_DOCS_ONLY:-}" = "true" ]; then
echo "SPIKE: forcing docs_only=true (#904 verification)"
echo "docs_only=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "${{ github.event_name }}" = "pull_request" ]; then
# checkout hands us the PR merge commit; parent 1 is the current
# base tip, so this is exactly the PR's effective diff.
git diff --name-only HEAD^1 HEAD > changed.txt
else
before="${{ github.event.before }}"
if [ "$before" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$before" 2>/dev/null; then
echo "no usable before-SHA; classifying as code"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$before" "${{ github.sha }}" > changed.txt
fi
echo "changed files:"
cat changed.txt
if [ ! -s changed.txt ]; then
echo "empty diff; classifying as code"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
docs_only=true
while IFS= read -r f; do
case "$f" in
*.md) ;;
docs/*) ;;
LICENSE) ;;
.github/ISSUE_TEMPLATE/*) ;;
*) docs_only=false; echo "not docs: $f"; break ;;
esac
done < changed.txt
echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"
android:
runs-on: ubuntu-latest
needs: classify
steps:
- name: "Docs-only change: android build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: actions/setup-java@v4
if: needs.classify.outputs.docs_only != 'true'
with:
distribution: "temurin"
java-version: "17"
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- name: Cache Gradle
if: needs.classify.outputs.docs_only != 'true'
uses: actions/cache@v4
with:
path: |
@ -30,13 +99,18 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
- name: Write dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
env:
GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json
- run: flutter build apk --release --no-pub --dart-define-from-file=dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
- name: Upload APK
if: needs.classify.outputs.docs_only != 'true'
uses: actions/upload-artifact@v4
with:
name: offband-android-r${{ github.run_number }}-${{ github.event.pull_request.head.sha || github.sha }}
@ -46,36 +120,58 @@ jobs:
ios:
runs-on: macos-latest
needs: classify
steps:
- name: "Docs-only change: ios build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
- run: flutter build ios --release --no-codesign --no-pub
if: needs.classify.outputs.docs_only != 'true'
linux:
runs-on: ubuntu-latest
needs: classify
steps:
- name: "Docs-only change: linux build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- name: Install Linux build deps
if: needs.classify.outputs.docs_only != 'true'
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
- name: Write dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
env:
GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json
- name: Enable native assets (bundles sqlite3 + llamadart libs, #445)
if: needs.classify.outputs.docs_only != 'true'
run: flutter config --enable-native-assets
- run: flutter build linux --release --no-pub --dart-define-from-file=dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
- name: Upload Linux bundle
if: needs.classify.outputs.docs_only != 'true'
uses: actions/upload-artifact@v4
with:
name: offband-linux-r${{ github.run_number }}-${{ github.event.pull_request.head.sha || github.sha }}
@ -85,34 +181,55 @@ jobs:
macos:
runs-on: macos-latest
needs: classify
steps:
- name: "Docs-only change: macos build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
- name: Enable native assets (bundles sqlite3 + llamadart libs, #445)
if: needs.classify.outputs.docs_only != 'true'
run: flutter config --enable-native-assets
- run: flutter build macos --release --no-pub
if: needs.classify.outputs.docs_only != 'true'
web:
runs-on: ubuntu-latest
needs: classify
steps:
- name: "Docs-only change: web build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
- name: Write dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
env:
GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json
- run: flutter build web --release --no-pub --dart-define-from-file=dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
- name: Upload web build
if: needs.classify.outputs.docs_only != 'true'
uses: actions/upload-artifact@v4
with:
name: offband-web-r${{ github.run_number }}-${{ github.event.pull_request.head.sha || github.sha }}
@ -122,26 +239,38 @@ jobs:
windows:
runs-on: windows-latest
needs: classify
steps:
- name: "Docs-only change: windows build short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to build"
- uses: actions/checkout@v4
if: needs.classify.outputs.docs_only != 'true'
- uses: subosito/flutter-action@v2
if: needs.classify.outputs.docs_only != 'true'
with:
flutter-version: "3.44.1"
cache: true
- run: flutter pub get
if: needs.classify.outputs.docs_only != 'true'
- run: dart run build_runner build --delete-conflicting-outputs
if: needs.classify.outputs.docs_only != 'true'
# Windows runner defaults to PowerShell; jq is not guaranteed there,
# so build the JSON with ConvertTo-Json, which escapes correctly.
- name: Write dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
env:
GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
run: |
@{ GIPHY_API_KEY = $env:GIPHY_API_KEY } | ConvertTo-Json -Compress |
Set-Content -Path dart_defines.json -Encoding utf8 -NoNewline
- name: Enable native assets (bundles sqlite3 + llamadart libs, #445)
if: needs.classify.outputs.docs_only != 'true'
run: flutter config --enable-native-assets
- run: flutter build windows --release --no-pub --dart-define-from-file=dart_defines.json
if: needs.classify.outputs.docs_only != 'true'
- name: Upload Windows build
if: needs.classify.outputs.docs_only != 'true'
uses: actions/upload-artifact@v4
with:
name: offband-windows-r${{ github.run_number }}-${{ github.event.pull_request.head.sha || github.sha }}

@ -8,31 +8,102 @@ on:
- dev
jobs:
# #904: same classifier as build.yml (workflows cannot share jobs without a
# reusable-workflow indirection; the duplication is the cheaper evil and the
# two copies are kept textually identical). Plain git, no third-party
# actions. Ambiguity always classifies as code.
classify:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.diff.outputs.docs_only }}
env:
# #904 SPIKE ONLY: forces the docs-only path so the short-circuit
# semantics can be proven on the machinery PR itself (whose .yml diff
# would otherwise classify as code). REMOVED in the follow-up commit;
# it must never reach dev.
SPIKE_FORCE_DOCS_ONLY: "true"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Classify changed files
id: diff
run: |
set -eu
if [ "${SPIKE_FORCE_DOCS_ONLY:-}" = "true" ]; then
echo "SPIKE: forcing docs_only=true (#904 verification)"
echo "docs_only=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "${{ github.event_name }}" = "pull_request" ]; then
# checkout hands us the PR merge commit; parent 1 is the current
# base tip, so this is exactly the PR's effective diff.
git diff --name-only HEAD^1 HEAD > changed.txt
else
before="${{ github.event.before }}"
if [ "$before" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$before" 2>/dev/null; then
echo "no usable before-SHA; classifying as code"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$before" "${{ github.sha }}" > changed.txt
fi
echo "changed files:"
cat changed.txt
if [ ! -s changed.txt ]; then
echo "empty diff; classifying as code"
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
docs_only=true
while IFS= read -r f; do
case "$f" in
*.md) ;;
docs/*) ;;
LICENSE) ;;
.github/ISSUE_TEMPLATE/*) ;;
*) docs_only=false; echo "not docs: $f"; break ;;
esac
done < changed.txt
echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"
analyze:
runs-on: ubuntu-latest
needs: classify
steps:
- name: "Docs-only change: analyze short-circuited (#904)"
if: needs.classify.outputs.docs_only == 'true'
run: echo "docs-only diff; nothing to analyze"
- name: Checkout
if: needs.classify.outputs.docs_only != 'true'
uses: actions/checkout@v4
- name: Guard against em-dash characters (#464)
if: needs.classify.outputs.docs_only != 'true'
run: python3 .github/scripts/check_no_emdash.py
- name: Set up Flutter
if: needs.classify.outputs.docs_only != 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: "3.44.1"
- name: Install dependencies
if: needs.classify.outputs.docs_only != 'true'
run: flutter pub get
- name: Generate code (drift / build_runner)
if: needs.classify.outputs.docs_only != 'true'
run: dart run build_runner build --delete-conflicting-outputs
- name: Analyze code
if: needs.classify.outputs.docs_only != 'true'
run: flutter analyze --fatal-infos --fatal-warnings
- name: Verify formatting
if: needs.classify.outputs.docs_only != 'true'
run: dart format --output=none --set-exit-if-changed .
- name: Run tests
if: needs.classify.outputs.docs_only != 'true'
run: flutter test -r github

Loading…
Cancel
Save

Powered by TurnKey Linux.