From 88ea3d19eb76f26cc4ecaf5b36ce75a4b6cb85c1 Mon Sep 17 00:00:00 2001 From: diffwireauto <330573775+diffwireauto@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:35:46 -0400 Subject: [PATCH] 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 --- .github/workflows/build.yml | 129 +++++++++++++++++++++++++++++ .github/workflows/flutter_dart.yml | 71 ++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b362774..1788da7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml index d8123689..33449ac0 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_dart.yml @@ -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