From a573b2d275c404a90426e191c9a203b6dfe4ae6b Mon Sep 17 00:00:00 2001 From: "Claude Haiku 4.5" Date: Fri, 30 Oct 2026 17:07:27 -0400 Subject: [PATCH] fix: recognize flutter-ci-setup composite action in change detection Add .github/actions/flutter-ci-setup/ path recognition: - isCiPath() now recognizes flutter-ci-setup as CI-critical - isNativeBuildPath() now recognizes flutter-ci-setup as requiring release builds - Only Android-specific composite actions (flutter-ci-setup) trigger builds - Non-Android actions (website-deploy, etc.) are not recognized as CI-critical Add comprehensive test coverage: - Test composite action path classification (7 new tests added) - Verify workflow YAML for output mapping and release condition - Ensure non-Android actions are correctly skipped - Test workflow condition evaluation for all scenarios Test results: 65/65 passing (all new tests verify the composite action fix) Flutter-ci-setup is a composite action used by Android workflow jobs (analysis, test, and debug APK), so changes to it must trigger Android verification and release builds. Co-Authored-By: Claude Haiku 4.5 --- .github/scripts/detect-android-changes.cjs | 7 +- .../scripts/detect-android-changes.test.cjs | 99 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/.github/scripts/detect-android-changes.cjs b/.github/scripts/detect-android-changes.cjs index 2ff45e0..004b146 100644 --- a/.github/scripts/detect-android-changes.cjs +++ b/.github/scripts/detect-android-changes.cjs @@ -44,10 +44,12 @@ function isNativeBuildPath(filePath) { if (!p) return false; // Native build requires: Android code, CI-critical files, or build config. // Only CI files that affect build behavior trigger release builds; doc/comment changes do not. + // Composite actions: only Android-specific actions (flutter-ci-setup) trigger builds. return ( p.startsWith('android/') || p === '.github/workflows/android-build.yml' || p.startsWith('.github/scripts/') || + p.startsWith('.github/actions/flutter-ci-setup/') || p === 'pubspec.yaml' || p === 'pubspec.lock' || p === 'analysis_options.yaml' || @@ -58,9 +60,11 @@ function isNativeBuildPath(filePath) { function isCiPath(filePath) { const p = normalizePath(filePath); if (!p) return false; + // CI paths: workflows, scripts, and Android-specific composite actions. return ( p === '.github/workflows/android-build.yml' || - p.startsWith('.github/scripts/') + p.startsWith('.github/scripts/') || + p.startsWith('.github/actions/flutter-ci-setup/') ); } @@ -287,6 +291,7 @@ module.exports = { isZeroSha, isAppPath, isCiPath, + isNativeBuildPath, classifyFiles, getChangedFiles, detectAndroidChanges diff --git a/.github/scripts/detect-android-changes.test.cjs b/.github/scripts/detect-android-changes.test.cjs index c82dc79..1371ace 100644 --- a/.github/scripts/detect-android-changes.test.cjs +++ b/.github/scripts/detect-android-changes.test.cjs @@ -12,6 +12,7 @@ const { isZeroSha, isAppPath, isCiPath, + isNativeBuildPath, classifyFiles, getChangedFiles, detectAndroidChanges @@ -71,14 +72,37 @@ describe('File Path Classification', () => { assert.strictEqual(isCiPath('.github/workflows/android-build.yml'), true); assert.strictEqual(isCiPath('.github/scripts/detect-android-changes.cjs'), true); assert.strictEqual(isCiPath('.github/scripts/detect-android-changes.test.cjs'), true); + assert.strictEqual(isCiPath('.github/actions/flutter-ci-setup/action.yml'), true); + assert.strictEqual(isCiPath('.github/actions/flutter-ci-setup/index.js'), true); // Negative matches for Android CI paths assert.strictEqual(isCiPath('.github/workflows/website-ci.yml'), false); assert.strictEqual(isCiPath('.github/workflows/backend-ci.yml'), false); assert.strictEqual(isCiPath('.github/workflows/waze-relay-ci.yml'), false); assert.strictEqual(isCiPath('.github/workflows/firebase-emulator-tests.yml'), false); + assert.strictEqual(isCiPath('.github/actions/website-deploy/action.yml'), false); + assert.strictEqual(isCiPath('.github/actions/other-action/index.js'), false); assert.strictEqual(isCiPath('.github/pull_request_template.md'), false); }); + + it('identifies native build paths correctly', () => { + assert.strictEqual(isNativeBuildPath('android/app/build.gradle'), true); + assert.strictEqual(isNativeBuildPath('.github/workflows/android-build.yml'), true); + assert.strictEqual(isNativeBuildPath('.github/scripts/detect-android-changes.cjs'), true); + assert.strictEqual(isNativeBuildPath('.github/actions/flutter-ci-setup/action.yml'), true); + assert.strictEqual(isNativeBuildPath('.github/actions/flutter-ci-setup/index.js'), true); + assert.strictEqual(isNativeBuildPath('pubspec.yaml'), true); + assert.strictEqual(isNativeBuildPath('pubspec.lock'), true); + assert.strictEqual(isNativeBuildPath('analysis_options.yaml'), true); + assert.strictEqual(isNativeBuildPath('build.yaml'), true); + + // Negative matches for native build paths + assert.strictEqual(isNativeBuildPath('.github/workflows/website-ci.yml'), false); + assert.strictEqual(isNativeBuildPath('.github/actions/website-deploy/action.yml'), false); + assert.strictEqual(isNativeBuildPath('.github/actions/other-action/action.yml'), false); + assert.strictEqual(isNativeBuildPath('lib/main.dart'), false); + assert.strictEqual(isNativeBuildPath('website/app/page.tsx'), false); + }); }); describe('classifyFiles Scenarios', () => { @@ -243,6 +267,26 @@ describe('classifyFiles Scenarios', () => { assert.strictEqual(result.android_changed, false); assert.strictEqual(result.app_changed, false); }); + + it('classifies composite action (flutter-ci-setup) change as android_changed=true, native_build_changed=true', () => { + const files = ['.github/actions/flutter-ci-setup/action.yml']; + const result = classifyFiles(files); + assert.strictEqual(result.android_changed, true); + assert.strictEqual(result.app_changed, false); + assert.strictEqual(result.native_build_changed, true); + assert.strictEqual(result.ciFiles.length, 1); + assert.deepStrictEqual(result.ciFiles, ['.github/actions/flutter-ci-setup/action.yml']); + }); + + it('classifies non-Android composite action (e.g., website-deploy) as false', () => { + const files = ['.github/actions/website-deploy/action.yml']; + const result = classifyFiles(files); + assert.strictEqual(result.android_changed, false); + assert.strictEqual(result.app_changed, false); + assert.strictEqual(result.native_build_changed, false); + assert.strictEqual(result.ciFiles.length, 0); + assert.strictEqual(result.unrelatedFiles.length, 1); + }); }); describe('detectAndroidChanges Workflow Event Logic', () => { @@ -782,5 +826,60 @@ describe('GitHub Actions Workflow Output Wiring and Release Job Conditions', () assert.strictEqual(nativeBuildChanged, false, 'Unrelated changes should output false'); assert.strictEqual(releaseJobShouldRun, false, 'Release job should NOT run for unrelated changes'); }); + + it('workflow release condition evaluates correctly: Composite action changes run release', () => { + // flutter-ci-setup is a composite action used by analysis, test, and debug APK jobs + const result = detectAndroidChanges({ + eventName: 'push', + baseSha, + headSha + }, { getChangedFilesFn: () => ['.github/actions/flutter-ci-setup/action.yml'] }); + + const nativeBuildChanged = result.native_build_changed; + const onMaster = true; // github.ref == 'refs/heads/master' + const releaseJobShouldRun = nativeBuildChanged && onMaster; + + assert.strictEqual(nativeBuildChanged, true, 'Composite action changes should output true'); + assert.strictEqual(releaseJobShouldRun, true, 'Release job SHOULD run for composite action changes'); + }); + + it('workflow release condition evaluates correctly: Non-Android composite action (website-deploy) skips release', () => { + const result = detectAndroidChanges({ + eventName: 'push', + baseSha, + headSha + }, { getChangedFilesFn: () => ['.github/actions/website-deploy/action.yml'] }); + + const nativeBuildChanged = result.native_build_changed; + const onMaster = true; // github.ref == 'refs/heads/master' + const releaseJobShouldRun = nativeBuildChanged && onMaster; + + assert.strictEqual(nativeBuildChanged, false, 'Non-Android composite actions should output false'); + assert.strictEqual(releaseJobShouldRun, false, 'Release job should NOT run for non-Android actions'); + }); +}); + +describe('Workflow YAML Verification', () => { + it('verifies android-build.yml defines jobs.detect.outputs.native_build_changed', () => { + const workflowPath = '.github/workflows/android-build.yml'; + const workflowContent = fs.readFileSync(path.join(process.cwd(), workflowPath), 'utf8'); + + // Check that the workflow file exists and contains the output mapping + assert.ok(workflowContent.includes('native_build_changed'), + 'Workflow must expose native_build_changed output from detector step'); + assert.ok(workflowContent.includes('steps.detector.outputs.native_build_changed'), + 'Workflow must reference detector output with ${{ steps.detector.outputs.native_build_changed }}'); + }); + + it('verifies release job condition includes native_build_changed check', () => { + const workflowPath = '.github/workflows/android-build.yml'; + const workflowContent = fs.readFileSync(path.join(process.cwd(), workflowPath), 'utf8'); + + // Check that release job condition references the output + assert.ok(workflowContent.includes("needs.detect.outputs.native_build_changed == 'true'"), + 'Release job condition must check native_build_changed output'); + assert.ok(workflowContent.includes('refs/heads/master'), + 'Release job condition must ensure it only runs on master branch'); + }); });