263 lines
7.0 KiB
JavaScript
263 lines
7.0 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const SHA_REGEX = /^[0-9a-fA-F]{40}$/;
|
|
const ZERO_SHA_REGEX = /^0{40}$/;
|
|
|
|
function normalizePath(filePath) {
|
|
if (typeof filePath !== 'string') return '';
|
|
return filePath.replace(/\\/g, '/').replace(/^\.\//, '').trim();
|
|
}
|
|
|
|
function isValidSha(sha) {
|
|
if (typeof sha !== 'string') return false;
|
|
return SHA_REGEX.test(sha.trim());
|
|
}
|
|
|
|
function isZeroSha(sha) {
|
|
if (typeof sha !== 'string') return false;
|
|
return ZERO_SHA_REGEX.test(sha.trim());
|
|
}
|
|
|
|
function isAppPath(filePath) {
|
|
const p = normalizePath(filePath);
|
|
if (!p) return false;
|
|
return (
|
|
p.startsWith('lib/') ||
|
|
p.startsWith('test/') ||
|
|
p.startsWith('integration_test/') ||
|
|
p.startsWith('android/') ||
|
|
p.startsWith('assets/') ||
|
|
p === 'pubspec.yaml' ||
|
|
p === 'pubspec.lock' ||
|
|
p === 'analysis_options.yaml' ||
|
|
p === 'build.yaml'
|
|
);
|
|
}
|
|
|
|
function isCiPath(filePath) {
|
|
const p = normalizePath(filePath);
|
|
if (!p) return false;
|
|
return (
|
|
p === '.github/workflows/android-build.yml' ||
|
|
p.startsWith('.github/scripts/')
|
|
);
|
|
}
|
|
|
|
function classifyFiles(fileList) {
|
|
if (!Array.isArray(fileList)) {
|
|
return {
|
|
android_changed: false,
|
|
app_changed: false,
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
const appFiles = [];
|
|
const ciFiles = [];
|
|
const unrelatedFiles = [];
|
|
|
|
for (const rawFile of fileList) {
|
|
const file = normalizePath(rawFile);
|
|
if (!file) continue;
|
|
|
|
if (isAppPath(file)) {
|
|
appFiles.push(file);
|
|
} else if (isCiPath(file)) {
|
|
ciFiles.push(file);
|
|
} else {
|
|
unrelatedFiles.push(file);
|
|
}
|
|
}
|
|
|
|
const app_changed = appFiles.length > 0;
|
|
const android_changed = appFiles.length > 0 || ciFiles.length > 0;
|
|
|
|
return {
|
|
android_changed,
|
|
app_changed,
|
|
appFiles,
|
|
ciFiles,
|
|
unrelatedFiles
|
|
};
|
|
}
|
|
|
|
function getChangedFiles(baseSha, headSha, repoDir = process.cwd()) {
|
|
if (!isValidSha(baseSha) || !isValidSha(headSha)) {
|
|
throw new Error(`Invalid git SHA: base=${baseSha}, head=${headSha}`);
|
|
}
|
|
|
|
// Attempt 3-dot diff (merge-base between base and head)
|
|
try {
|
|
const stdout = execFileSync('git', ['diff', '--name-only', `${baseSha}...${headSha}`], {
|
|
cwd: repoDir,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
});
|
|
return stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
} catch (threeDotError) {
|
|
// Fall back to direct 2-commit diff
|
|
try {
|
|
const stdout = execFileSync('git', ['diff', '--name-only', baseSha, headSha], {
|
|
cwd: repoDir,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
});
|
|
return stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
} catch (fallbackError) {
|
|
throw new Error(`git diff failed: ${threeDotError.message}; fallback failed: ${fallbackError.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function detectAndroidChanges({ eventName, baseSha, headSha, repoDir }, deps = {}) {
|
|
const getFiles = deps.getChangedFilesFn || getChangedFiles;
|
|
|
|
// Manual dispatch: always run full job
|
|
if (eventName === 'workflow_dispatch') {
|
|
return {
|
|
android_changed: true,
|
|
app_changed: true,
|
|
reason: 'Manual workflow_dispatch; full pipeline authorized',
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
// Push with zero or missing base SHA (e.g. new branch or missing before SHA): fail safe
|
|
if (!baseSha || isZeroSha(baseSha) || !isValidSha(baseSha)) {
|
|
return {
|
|
android_changed: true,
|
|
app_changed: true,
|
|
reason: `Zero, missing, or invalid base SHA (${baseSha}); failing safe to full build`,
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
// Missing or invalid head SHA: fail safe
|
|
if (!headSha || isZeroSha(headSha) || !isValidSha(headSha)) {
|
|
return {
|
|
android_changed: true,
|
|
app_changed: true,
|
|
reason: `Zero, missing, or invalid head SHA (${headSha}); failing safe to full build`,
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
// Identical base and head: no changes
|
|
if (baseSha.trim() === headSha.trim()) {
|
|
return {
|
|
android_changed: false,
|
|
app_changed: false,
|
|
reason: 'Base SHA and Head SHA are identical; no changes detected',
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
// Compare diff
|
|
let files;
|
|
try {
|
|
files = getFiles(baseSha.trim(), headSha.trim(), repoDir);
|
|
} catch (err) {
|
|
return {
|
|
android_changed: true,
|
|
app_changed: true,
|
|
reason: `Failed to retrieve git diff: ${err.message}; failing safe to full build`,
|
|
appFiles: [],
|
|
ciFiles: [],
|
|
unrelatedFiles: []
|
|
};
|
|
}
|
|
|
|
const classification = classifyFiles(files);
|
|
return {
|
|
...classification,
|
|
totalFiles: files.length,
|
|
reason: `Analyzed ${files.length} changed file(s): ` +
|
|
`${classification.appFiles.length} app, ` +
|
|
`${classification.ciFiles.length} CI, ` +
|
|
`${classification.unrelatedFiles.length} unrelated`
|
|
};
|
|
}
|
|
|
|
function runCli() {
|
|
const eventName = process.argv[2] || process.env.GITHUB_EVENT_NAME || 'pull_request';
|
|
const baseSha = process.argv[3] || process.env.BASE_SHA || '';
|
|
const headSha = process.argv[4] || process.env.HEAD_SHA || '';
|
|
|
|
console.log(`[detect-android-changes] Starting detection...`);
|
|
console.log(`[detect-android-changes] Event: ${eventName}`);
|
|
console.log(`[detect-android-changes] Base SHA: ${baseSha}`);
|
|
console.log(`[detect-android-changes] Head SHA: ${headSha}`);
|
|
|
|
const result = detectAndroidChanges({
|
|
eventName,
|
|
baseSha,
|
|
headSha
|
|
});
|
|
|
|
console.log(`[detect-android-changes] android_changed=${result.android_changed}`);
|
|
console.log(`[detect-android-changes] app_changed=${result.app_changed}`);
|
|
console.log(`[detect-android-changes] Reason: ${result.reason}`);
|
|
|
|
if (result.appFiles && result.appFiles.length > 0) {
|
|
console.log(`[detect-android-changes] App files (${result.appFiles.length}):`);
|
|
for (const f of result.appFiles) {
|
|
console.log(` + ${f}`);
|
|
}
|
|
}
|
|
|
|
if (result.ciFiles && result.ciFiles.length > 0) {
|
|
console.log(`[detect-android-changes] CI files (${result.ciFiles.length}):`);
|
|
for (const f of result.ciFiles) {
|
|
console.log(` * ${f}`);
|
|
}
|
|
}
|
|
|
|
if (result.unrelatedFiles && result.unrelatedFiles.length > 0) {
|
|
console.log(`[detect-android-changes] Unrelated files (${result.unrelatedFiles.length}):`);
|
|
for (const f of result.unrelatedFiles) {
|
|
console.log(` - ${f}`);
|
|
}
|
|
}
|
|
|
|
const githubOutput = process.env.GITHUB_OUTPUT;
|
|
if (githubOutput) {
|
|
fs.appendFileSync(
|
|
githubOutput,
|
|
`android_changed=${result.android_changed}\napp_changed=${result.app_changed}\n`
|
|
);
|
|
console.log(`[detect-android-changes] Wrote outputs to GITHUB_OUTPUT.`);
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
runCli();
|
|
}
|
|
|
|
module.exports = {
|
|
normalizePath,
|
|
isValidSha,
|
|
isZeroSha,
|
|
isAppPath,
|
|
isCiPath,
|
|
classifyFiles,
|
|
getChangedFiles,
|
|
detectAndroidChanges
|
|
};
|