Files
labeler/src/labeler.ts
T

275 lines
7.0 KiB
TypeScript
Raw Normal View History

import * as core from '@actions/core';
import * as github from '@actions/github';
import * as yaml from 'js-yaml';
2023-03-23 10:45:05 +01:00
import {Minimatch} from 'minimatch';
2021-06-03 16:15:47 -04:00
interface MatchConfig {
all?: string[];
any?: string[];
}
type StringOrMatchConfig = string | MatchConfig;
type ClientType = ReturnType<typeof github.getOctokit>;
2021-06-03 16:15:47 -04:00
export async function run() {
try {
const token = core.getInput('repo-token');
const configPath = core.getInput('configuration-path', {required: true});
const syncLabels = core.getBooleanInput('sync-labels', {required: false});
2023-03-27 09:44:05 +01:00
const dot = !!core.getBooleanInput('dot', {required: false});
2021-06-03 16:15:47 -04:00
const prNumber = getPrNumber();
if (!prNumber) {
core.info('Could not get pull request number from context, exiting');
2021-06-03 16:15:47 -04:00
return;
}
const client: ClientType = github.getOctokit(token);
2021-06-03 16:15:47 -04:00
const {data: pullRequest} = await client.rest.pulls.get({
2021-06-03 16:15:47 -04:00
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: prNumber
2021-06-03 16:15:47 -04:00
});
core.debug(`fetching changed files for pr #${prNumber}`);
const changedFiles: string[] = await getChangedFiles(client, prNumber);
const labelGlobs: Map<string, StringOrMatchConfig[]> = await getLabelGlobs(
client,
configPath
);
const labels: string[] = [];
const labelsToRemove: string[] = [];
for (const [label, globs] of labelGlobs.entries()) {
core.debug(`processing ${label}`);
2022-02-04 13:26:54 +00:00
if (checkGlobs(changedFiles, globs, dot)) {
2021-06-03 16:15:47 -04:00
labels.push(label);
} else if (pullRequest.labels.find(l => l.name === label)) {
2021-06-03 16:15:47 -04:00
labelsToRemove.push(label);
}
}
if (labels.length > 0) {
await addLabels(client, prNumber, labels);
}
if (syncLabels && labelsToRemove.length) {
await removeLabels(client, prNumber, labelsToRemove);
}
2021-09-02 14:30:05 +02:00
} catch (error: any) {
2021-06-03 16:15:47 -04:00
core.error(error);
core.setFailed(error.message);
}
}
function getPrNumber(): number | undefined {
const pullRequest = github.context.payload.pull_request;
if (!pullRequest) {
return undefined;
}
return pullRequest.number;
}
async function getChangedFiles(
client: ClientType,
2021-06-03 16:15:47 -04:00
prNumber: number
): Promise<string[]> {
const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({
2021-06-03 16:15:47 -04:00
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: prNumber
2021-06-03 16:15:47 -04:00
});
const listFilesResponse = await client.paginate(listFilesOptions);
const changedFiles = listFilesResponse.map((f: any) => f.filename);
2021-06-03 16:15:47 -04:00
core.debug('found changed files:');
2021-06-03 16:15:47 -04:00
for (const file of changedFiles) {
core.debug(' ' + file);
2021-06-03 16:15:47 -04:00
}
return changedFiles;
}
async function getLabelGlobs(
client: ClientType,
2021-06-03 16:15:47 -04:00
configurationPath: string
): Promise<Map<string, StringOrMatchConfig[]>> {
const configurationContent: string = await fetchContent(
client,
configurationPath
);
// loads (hopefully) a `{[label:string]: string | StringOrMatchConfig[]}`, but is `any`:
2021-06-04 23:30:19 -04:00
const configObject: any = yaml.load(configurationContent);
2021-06-03 16:15:47 -04:00
// transform `any` => `Map<string,StringOrMatchConfig[]>` or throw if yaml is malformed:
return getLabelGlobMapFromObject(configObject);
}
async function fetchContent(
client: ClientType,
2021-06-03 16:15:47 -04:00
repoPath: string
): Promise<string> {
const response: any = await client.rest.repos.getContent({
2021-06-03 16:15:47 -04:00
owner: github.context.repo.owner,
repo: github.context.repo.repo,
path: repoPath,
ref: github.context.sha
2021-06-03 16:15:47 -04:00
});
return Buffer.from(response.data.content, response.data.encoding).toString();
}
function getLabelGlobMapFromObject(
configObject: any
): Map<string, StringOrMatchConfig[]> {
const labelGlobs: Map<string, StringOrMatchConfig[]> = new Map();
for (const label in configObject) {
if (typeof configObject[label] === 'string') {
2021-06-03 16:15:47 -04:00
labelGlobs.set(label, [configObject[label]]);
} else if (configObject[label] instanceof Array) {
labelGlobs.set(label, configObject[label]);
} else {
throw Error(
`found unexpected type for label ${label} (should be string or array of globs)`
);
}
}
return labelGlobs;
}
function toMatchConfig(config: StringOrMatchConfig): MatchConfig {
if (typeof config === 'string') {
2021-06-03 16:15:47 -04:00
return {
any: [config]
2021-06-03 16:15:47 -04:00
};
}
return config;
}
2023-03-23 10:45:05 +01:00
function printPattern(matcher: Minimatch): string {
return (matcher.negate ? '!' : '') + matcher.pattern;
2021-06-03 16:15:47 -04:00
}
export function checkGlobs(
changedFiles: string[],
2022-02-04 13:26:54 +00:00
globs: StringOrMatchConfig[],
dot: boolean
2021-06-03 16:15:47 -04:00
): boolean {
for (const glob of globs) {
core.debug(` checking pattern ${JSON.stringify(glob)}`);
const matchConfig = toMatchConfig(glob);
2022-02-04 13:26:54 +00:00
if (checkMatch(changedFiles, matchConfig, dot)) {
2021-06-03 16:15:47 -04:00
return true;
}
}
return false;
}
2023-03-23 10:45:05 +01:00
function isMatch(changedFile: string, matchers: Minimatch[]): boolean {
2021-06-03 16:15:47 -04:00
core.debug(` matching patterns against file ${changedFile}`);
for (const matcher of matchers) {
core.debug(` - ${printPattern(matcher)}`);
if (!matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} did not match`);
return false;
}
}
core.debug(` all patterns matched`);
return true;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
2022-02-04 13:26:54 +00:00
function checkAny(
changedFiles: string[],
globs: string[],
dot: boolean
): boolean {
const matchers = globs.map(g => new Minimatch(g, {dot}));
2021-06-03 16:15:47 -04:00
core.debug(` checking "any" patterns`);
for (const changedFile of changedFiles) {
if (isMatch(changedFile, matchers)) {
core.debug(` "any" patterns matched against ${changedFile}`);
return true;
}
}
core.debug(` "any" patterns did not match any files`);
return false;
}
// equivalent to "Array.every()" but expanded for debugging and clarity
2022-02-04 13:26:54 +00:00
function checkAll(
changedFiles: string[],
globs: string[],
dot: boolean
): boolean {
2023-05-31 01:44:56 +01:00
const matchers = globs.map(g => new Minimatch(g, {dot}));
2021-06-03 16:15:47 -04:00
core.debug(` checking "all" patterns`);
for (const changedFile of changedFiles) {
if (!isMatch(changedFile, matchers)) {
core.debug(` "all" patterns did not match against ${changedFile}`);
return false;
}
}
core.debug(` "all" patterns matched all files`);
return true;
}
2022-02-04 13:26:54 +00:00
function checkMatch(
changedFiles: string[],
matchConfig: MatchConfig,
dot: boolean
): boolean {
2021-06-03 16:15:47 -04:00
if (matchConfig.all !== undefined) {
2022-02-04 13:26:54 +00:00
if (!checkAll(changedFiles, matchConfig.all, dot)) {
2021-06-03 16:15:47 -04:00
return false;
}
}
if (matchConfig.any !== undefined) {
2022-02-04 13:26:54 +00:00
if (!checkAny(changedFiles, matchConfig.any, dot)) {
2021-06-03 16:15:47 -04:00
return false;
}
}
return true;
}
async function addLabels(
client: ClientType,
2021-06-03 16:15:47 -04:00
prNumber: number,
labels: string[]
) {
await client.rest.issues.addLabels({
2021-06-03 16:15:47 -04:00
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: prNumber,
labels: labels
2021-06-03 16:15:47 -04:00
});
}
async function removeLabels(
client: ClientType,
2021-06-03 16:15:47 -04:00
prNumber: number,
labels: string[]
) {
await Promise.all(
labels.map(label =>
client.rest.issues.removeLabel({
2021-06-03 16:15:47 -04:00
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: prNumber,
name: label
2021-06-03 16:15:47 -04:00
})
)
);
}