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 <noreply@anthropic.com>
This commit is contained in:
Claude Haiku 4.5 2026-10-30 17:07:27 -04:00
parent 32059f9b8a
commit a573b2d275
2 changed files with 105 additions and 1 deletions

View File

@ -44,10 +44,12 @@ function isNativeBuildPath(filePath) {
if (!p) return false; if (!p) return false;
// Native build requires: Android code, CI-critical files, or build config. // 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. // 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 ( return (
p.startsWith('android/') || p.startsWith('android/') ||
p === '.github/workflows/android-build.yml' || p === '.github/workflows/android-build.yml' ||
p.startsWith('.github/scripts/') || p.startsWith('.github/scripts/') ||
p.startsWith('.github/actions/flutter-ci-setup/') ||
p === 'pubspec.yaml' || p === 'pubspec.yaml' ||
p === 'pubspec.lock' || p === 'pubspec.lock' ||
p === 'analysis_options.yaml' || p === 'analysis_options.yaml' ||
@ -58,9 +60,11 @@ function isNativeBuildPath(filePath) {
function isCiPath(filePath) { function isCiPath(filePath) {
const p = normalizePath(filePath); const p = normalizePath(filePath);
if (!p) return false; if (!p) return false;
// CI paths: workflows, scripts, and Android-specific composite actions.
return ( return (
p === '.github/workflows/android-build.yml' || 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, isZeroSha,
isAppPath, isAppPath,
isCiPath, isCiPath,
isNativeBuildPath,
classifyFiles, classifyFiles,
getChangedFiles, getChangedFiles,
detectAndroidChanges detectAndroidChanges

View File

@ -12,6 +12,7 @@ const {
isZeroSha, isZeroSha,
isAppPath, isAppPath,
isCiPath, isCiPath,
isNativeBuildPath,
classifyFiles, classifyFiles,
getChangedFiles, getChangedFiles,
detectAndroidChanges detectAndroidChanges
@ -71,14 +72,37 @@ describe('File Path Classification', () => {
assert.strictEqual(isCiPath('.github/workflows/android-build.yml'), true); 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.cjs'), true);
assert.strictEqual(isCiPath('.github/scripts/detect-android-changes.test.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 // Negative matches for Android CI paths
assert.strictEqual(isCiPath('.github/workflows/website-ci.yml'), false); assert.strictEqual(isCiPath('.github/workflows/website-ci.yml'), false);
assert.strictEqual(isCiPath('.github/workflows/backend-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/waze-relay-ci.yml'), false);
assert.strictEqual(isCiPath('.github/workflows/firebase-emulator-tests.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); 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', () => { describe('classifyFiles Scenarios', () => {
@ -243,6 +267,26 @@ describe('classifyFiles Scenarios', () => {
assert.strictEqual(result.android_changed, false); assert.strictEqual(result.android_changed, false);
assert.strictEqual(result.app_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', () => { 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(nativeBuildChanged, false, 'Unrelated changes should output false');
assert.strictEqual(releaseJobShouldRun, false, 'Release job should NOT run for unrelated changes'); 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');
});
}); });