2022-12-19 17:09:22 +01:00
import * as core from '@actions/core' ;
import * as github from '@actions/github' ;
2023-06-21 05:49:43 -04:00
import * as pluginRetry from '@octokit/plugin-retry' ;
2022-12-19 17:09:22 +01:00
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 ;
2021-06-21 11:39:52 -04:00
type ClientType = ReturnType < typeof github.getOctokit >;
2021-06-03 16:15:47 -04:00
2023-06-21 05:49:43 -04:00
// GitHub Issues cannot have more than 100 labels
const GITHUB_MAX_LABELS = 100 ;
2021-06-03 16:15:47 -04:00
export async function run() {
try {
2023-03-07 12:25:56 +01:00
const token = core . getInput ( 'repo-token' );
2022-12-19 17:09:22 +01:00
const configPath = core . getInput ( 'configuration-path' , { required : true });
2023-05-31 17:23:26 +01:00
const syncLabels = !! core . getInput ( 'sync-labels' );
2023-05-31 18:46:23 +01:00
const dot = core . getBooleanInput ( 'dot' );
2021-06-03 16:15:47 -04:00
const prNumber = getPrNumber ();
if ( ! prNumber ) {
2023-03-09 15:02:17 +02:00
core . info ( 'Could not get pull request number from context, exiting' );
2021-06-03 16:15:47 -04:00
return ;
}
2023-06-21 05:49:43 -04:00
const client : ClientType = github . getOctokit ( token , {}, pluginRetry . retry );
2021-06-03 16:15:47 -04:00
2022-12-19 17:09:22 +01: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 ,
2022-12-19 17:09:22 +01:00
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
);
2023-06-21 05:49:43 -04:00
const prLabels : string [] = pullRequest . labels . map ( label => label . name );
const allLabels : Set < string > = new Set ( prLabels );
2021-06-03 16:15:47 -04:00
for ( const [ label , globs ] of labelGlobs . entries ()) {
core . debug ( `processing ${ label } ` );
2022-02-04 13:26:54 +00:00
if ( checkGlobs ( changedFiles , globs , dot )) {
2023-06-21 05:49:43 -04:00
allLabels . add ( label );
} else if ( syncLabels ) {
allLabels . delete ( label );
2021-06-03 16:15:47 -04:00
}
}
2023-06-21 05:49:43 -04:00
const labels = [... allLabels ]. slice ( 0 , GITHUB_MAX_LABELS );
const excessLabels = [... allLabels ]. slice ( GITHUB_MAX_LABELS );
2023-06-19 07:07:00 -04:00
try {
2023-06-21 05:49:43 -04:00
if ( ! isListEqual ( prLabels , labels )) {
await setLabels ( client , prNumber , labels );
2023-06-19 07:07:00 -04:00
}
2021-06-03 16:15:47 -04:00
2023-06-21 05:49:43 -04:00
if ( excessLabels . length ) {
core . warning (
`Maximum of ${ GITHUB_MAX_LABELS } labels allowed. Excess labels: ${ excessLabels . join (
', '
) } ` ,
{ title : 'Label limit for a PR exceeded' }
);
2023-06-19 07:07:00 -04:00
}
} catch ( error : any ) {
if (
error . name === 'HttpError' &&
error . message === 'Resource not accessible by integration'
) {
core . warning (
`The action requires write permission to add labels to pull requests. For more information please refer to the action documentation: https://github.com/actions/labeler#permissions` ,
{
title : ` ${ process . env [ 'GITHUB_ACTION_REPOSITORY' ] } running under ' ${ github . context . eventName } ' is misconfigured`
}
);
core . setFailed ( error . message );
} else {
throw error ;
}
2021-06-03 16:15:47 -04:00
}
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 (
2021-06-21 11:39:52 -04:00
client : ClientType ,
2021-06-03 16:15:47 -04:00
prNumber : number
) : Promise < string [] > {
2021-06-21 11:39:52 -04:00
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 ,
2022-12-19 17:09:22 +01:00
pull_number : prNumber
2021-06-03 16:15:47 -04:00
});
const listFilesResponse = await client . paginate ( listFilesOptions );
2021-06-21 11:39:52 -04:00
const changedFiles = listFilesResponse . map (( f : any ) => f . filename );
2021-06-03 16:15:47 -04:00
2022-12-19 17:09:22 +01:00
core . debug ( 'found changed files:' );
2021-06-03 16:15:47 -04:00
for ( const file of changedFiles ) {
2022-12-19 17:09:22 +01:00
core . debug ( ' ' + file );
2021-06-03 16:15:47 -04:00
}
return changedFiles ;
}
async function getLabelGlobs (
2021-06-21 11:39:52 -04:00
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 (
2021-06-21 11:39:52 -04:00
client : ClientType ,
2021-06-03 16:15:47 -04:00
repoPath : string
) : Promise < string > {
2021-06-21 11:39:52 -04:00
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 ,
2022-12-19 17:09:22 +01:00
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 ) {
2022-12-19 17:09:22 +01:00
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 {
2022-12-19 17:09:22 +01:00
if ( typeof config === 'string' ) {
2021-06-03 16:15:47 -04:00
return {
2022-12-19 17:09:22 +01:00
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 {
2022-12-19 17:09:22 +01:00
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 {
2023-01-16 17:36:24 +00:00
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 ;
}
2023-06-21 05:49:43 -04:00
function isListEqual ( listA : string [], listB : string []) : boolean {
return listA . length === listB . length && listA . every ( el => listB . includes ( el ));
}
async function setLabels (
2021-06-21 11:39:52 -04:00
client : ClientType ,
2021-06-03 16:15:47 -04:00
prNumber : number ,
labels : string []
) {
2023-06-21 05:49:43 -04:00
await client . rest . issues . setLabels ({
2021-06-03 16:15:47 -04:00
owner : github.context.repo.owner ,
repo : github.context.repo.repo ,
issue_number : prNumber ,
2022-12-19 17:09:22 +01:00
labels : labels
2021-06-03 16:15:47 -04:00
});
}