Skip to content

Instantly share code, notes, and snippets.

@dmitry-stepanenko
Created February 28, 2025 14:09
Show Gist options
  • Save dmitry-stepanenko/af297def0340f091a05cd6be21a0532f to your computer and use it in GitHub Desktop.
Save dmitry-stepanenko/af297def0340f091a05cd6be21a0532f to your computer and use it in GitHub Desktop.
nx-run-touched
import { execSync } from 'child_process';
import { output } from '@nx/devkit';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import {
ensureHeadAndBaseAreSet,
findClosestProjectByFilePath,
getFilesUsingBaseAndHead,
getUncommittedFiles,
getUntrackedFiles,
} from './utils.js';
// Can be used as `node nx-run-touched.js -t lint` to run only those projects that were modified between revisions
const argv = yargs(hideBin(process.argv))
.option('parallel', {
alias: 'p',
type: 'number',
description: 'Max number of parallel processes [default is 3].',
})
.option('nxBail', {
type: 'boolean',
description: 'Stop execution after the first failed task',
})
.option('exclude', {
type: 'string',
description: 'Exclude certain projects from being processed.',
})
.option('skipNxCache', {
type: 'boolean',
description:
'Rerun the tasks even when the results are available in the cache.',
})
.option('configuration', {
alias: 'c',
type: 'string',
description:
'This is the configuration to use when performing tasks on projects.',
})
.option('base', {
describe: 'Base of the current branch (usually master).',
type: 'string',
})
.option('head', {
describe: 'Latest commit of the current branch (usually HEAD).',
type: 'string',
})
.option('target', {
alias: 't',
type: 'string',
description: 'Specify the target task',
demandOption: true,
})
.help().argv;
ensureHeadAndBaseAreSet(argv);
const optionalArgs = [
argv.nxBail ? '--nxBail' : '',
argv.skipNxCache ? '--skipNxCache' : '',
argv.parallel && '--parallel=' + argv.parallel,
argv.exclude && '--exclude=' + argv.exclude,
argv.configuration && '--configuration=' + argv.configuration,
];
const touchedFiles = Array.from(
new Set([
...getFilesUsingBaseAndHead(argv.base, argv.head),
...getUncommittedFiles(),
...getUntrackedFiles(),
])
);
const projects = [
...new Set(
touchedFiles.map((f) => findClosestProjectByFilePath(f)).filter(Boolean)
),
];
if (!projects.length) {
output.warn({ title: 'No projects to run' });
process.exit(0);
}
execSync(
`npx nx run-many --target=${argv.target} --base=${argv.base} --head=${argv.head} --projects=${projects.join(',')} ${optionalArgs.join(' ')}`.trim(),
{ stdio: ['inherit', 'inherit', 'inherit'] }
);
import { execSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { dirname, join, resolve } from 'path';
import { output, workspaceRoot } from '@nx/devkit';
// mostly taken from https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/command-line-utils.ts
const TEN_MEGABYTES = 1024 * 10000;
const knownProjectsByPath = new Map();
export function findClosestProjectByFilePath(filePath) {
filePath = resolve(workspaceRoot, filePath);
let currentDir = dirname(filePath);
while (currentDir && currentDir.startsWith(workspaceRoot)) {
const projectJsonPath = join(currentDir, 'project.json');
if (knownProjectsByPath.has(projectJsonPath)) {
return knownProjectsByPath.get(projectJsonPath);
}
if (existsSync(projectJsonPath)) {
const projectName = JSON.parse(
readFileSync(projectJsonPath, 'utf-8')
).name;
knownProjectsByPath.set(projectJsonPath, projectName);
return projectName;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) break; // Stop if reached root
currentDir = parentDir;
}
return null;
}
export function ensureHeadAndBaseAreSet(args) {
args.head ??= 'HEAD';
if (!args.base) {
args.base = getBaseRef();
output.note({
title: `Affected criteria defaulted to --base=${output.bold(
`${args.base}`
)} --head=${output.bold(args.head)}`,
});
}
if (!args.base) {
throw new Error('Failed to resolve "base" ref');
}
args.base = getMergeBase(args.base, args.head);
}
export function getBaseRef() {
// we don't really need to be flexible here, just return our base
return 'master';
}
export function getMergeBase(base, head = 'HEAD') {
try {
return execSync(`git merge-base "${base}" "${head}"`, {
maxBuffer: TEN_MEGABYTES,
cwd: workspaceRoot,
stdio: 'pipe',
windowsHide: false,
})
.toString()
.trim();
} catch {
try {
return execSync(`git merge-base --fork-point "${base}" "${head}"`, {
maxBuffer: TEN_MEGABYTES,
cwd: workspaceRoot,
stdio: 'pipe',
windowsHide: false,
})
.toString()
.trim();
} catch {
return base;
}
}
}
export function getFilesUsingBaseAndHead(base, head) {
return parseGitOutput(
`git diff --name-only --no-renames --relative "${base}" "${head}"`
);
}
export function getUncommittedFiles() {
return parseGitOutput(`git diff --name-only --no-renames --relative HEAD .`);
}
export function getUntrackedFiles() {
return parseGitOutput(`git ls-files --others --exclude-standard`);
}
export function parseGitOutput(command) {
return execSync(command, {
maxBuffer: TEN_MEGABYTES,
cwd: workspaceRoot,
windowsHide: false,
})
.toString('utf-8')
.split('\n')
.map((a) => a.trim())
.filter((a) => a.length > 0);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment