Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/presubmit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
with:
fetch-depth: 300
persist-credentials: false
- name: Fetch base branch for linter diff
run: git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}

Check failure on line 40 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code

Check failure on line 40 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code

Check failure on line 40 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

template-injection

presubmit.yaml:40: code injection via template expansion: may expand into attacker-controllable code

Check failure on line 40 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

template-injection

presubmit.yaml:40: code injection via template expansion: may expand into attacker-controllable code
- name: Use Node.js 24
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24
- run: npm install
- run: npm run lint
- run: node ./bin/linter.mjs --strict --git-diff-arg "origin/${{ github.base_ref }}...HEAD"

Check failure on line 46 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/template-injection

code injection via template expansion: may expand into attacker-controllable code

Check failure on line 46 in .github/workflows/presubmit.yaml

View workflow job for this annotation

GitHub Actions / zizmor-output

template-injection

presubmit.yaml:46: code injection via template expansion: may expand into attacker-controllable code
name: Run monorepo linter
164 changes: 126 additions & 38 deletions bin/linter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,56 +63,144 @@ function runGit(args, options = {}) {
});
}

/**
* Helper to get modified/added TypeScript files against a given git ref.
* Uses merge-base when possible to compare against the common ancestor (e.g. when base and branch have both moved).
*/
function getDiffFiles(ref) {
let diffTarget = ref;
try {
const mergeBase = runGit(['merge-base', ref, 'HEAD']).trim();
if (mergeBase) {
diffTarget = mergeBase;
}
} catch {
// If merge-base fails (e.g. shallow clone or invalid ref), fall back to using ref directly
}

const output = runGit([
'diff',
'--name-only',
'--diff-filter=ACMRT',
diffTarget,
'--',
'*.ts',
]);
return output
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0 && existsSync(f));
}

/**
* Helper to get modified/added TypeScript files in strict mode given GIT_DIFF_ARG.
* Validates that `git diff --quiet ${gitDiffArg}` succeeds (exit code 0 or 1).
*/
function getStrictDiffFiles(gitDiffArg) {
const args = gitDiffArg.trim().split(/\s+/);

try {
runGit(['diff', '--quiet', ...args]);
} catch (err) {
if (err.status !== 1) {
throw new Error(
`Strict mode error: git diff --quiet ${gitDiffArg} failed with exit code ${err.status}.\n` +
`Ensure that the git reference '${gitDiffArg}' exists locally and that you have fetched the required commits/branches.\n` +
`Details: ${String(err.stderr || err.message || '').trim()}`
);
}
}

const output = runGit([
'diff',
'--name-only',
'--diff-filter=ACMRT',
...args,
'--',
'*.ts',
]);
return output
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0 && existsSync(f));
}

/**
* Retrieves the GIT_DIFF_ARG from CLI flags or environment variable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just choose one or the other (flag or env), I think @bshaffer went with / reused the env variable so maybe let's just use that

*/
function getGitDiffArg() {
const cliIndex = process.argv.findIndex(arg => arg.startsWith('--git-diff-arg'));
if (cliIndex !== -1) {
const arg = process.argv[cliIndex];
if (arg.includes('=')) {
return arg.substring(arg.indexOf('=') + 1);
}
if (process.argv[cliIndex + 1] && !process.argv[cliIndex + 1].startsWith('-')) {
return process.argv[cliIndex + 1];
}
}
return process.env.GIT_DIFF_ARG;
}

/**
* Returns a list of changed TypeScript files comparing against target branches/references.
*/
function getChangedFiles() {
const base = process.env.GITHUB_BASE_REF || 'main';
const refsToTry = [
base,
`upstream/${base}`,
`origin/${base}`,
'FETCH_HEAD',
'HEAD~1',
'HEAD^',
];
const isStrict = process.argv.includes('--strict');

const gitDiffArg = getGitDiffArg();

if (isStrict) {
if (!gitDiffArg) {
throw new Error(
'Strict mode is enabled (--strict), but GIT_DIFF_ARG was not provided. ' +
'Please supply --git-diff-arg <arg> or set the GIT_DIFF_ARG environment variable.'
);
}
console.log(`Strict mode enabled. Comparing using GIT_DIFF_ARG: ${gitDiffArg}`);
return getStrictDiffFiles(gitDiffArg);
}

const isCI = Boolean(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITHUB_BASE_REF);

let refsToTry = [];
let isStrictCI = false;

if (isCI) {
const baseRef = process.env.GITHUB_BASE_REF;
if (!baseRef) {
throw new Error('Running in CI but GITHUB_BASE_REF environment variable is not set.');
}
refsToTry = baseRef.startsWith('origin/') ? [baseRef] : [`origin/${baseRef}`, baseRef];
isStrictCI = true;
} else {
let currentBranch = '';
try {
currentBranch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
} catch {
// Continue with fallback refs if branch detection fails
}

refsToTry = (currentBranch === 'main')
? ['origin/main', 'upstream/main', 'HEAD~1']
: ['upstream/main', 'origin/main', 'main'];
}

for (const ref of refsToTry) {
try {
const output = runGit([
'diff',
'--name-only',
'--diff-filter=ACMRT',
ref,
'--',
'*.ts',
]);
return output
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0 && existsSync(f));
const files = getDiffFiles(ref);
console.log(`Comparing against base reference: ${ref}`);
return files;
} catch {
// Continue to the next fallback ref
// Continue to next ref if this one fails/does not exist
}
}

// Fallback to checking uncommitted working tree changes against HEAD if all specific refs fail
try {
const output = runGit([
'diff',
'--name-only',
'--diff-filter=ACMRT',
'HEAD',
'--',
'*.ts',
]);
return output
.split('\n')
.map(f => f.trim())
.filter(f => f.length > 0 && existsSync(f));
} catch {
return [];
if (isStrictCI) {
throw new Error(`Failed to determine changed files against GITHUB_BASE_REF '${refsToTry[0]}' in CI.`);
}

throw new Error(`Failed to determine changed files. Tried refs: ${refsToTry.join(', ')}`);
}

// --- ESLint Checker ---
Expand Down
Loading