mirror of
https://github.com/actions/labeler.git
synced 2025-12-11 12:07:32 +00:00
Merge pull request #691 from akv-platform/v4-general-refactoring
Labeler general refactoring (ver. 4)
This commit is contained in:
58
.licenses/npm/lodash.isequal.dep.yml
generated
Normal file
58
.licenses/npm/lodash.isequal.dep.yml
generated
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: lodash.isequal
|
||||
version: 4.5.0
|
||||
type: npm
|
||||
summary: The Lodash method `_.isEqual` exported as a module.
|
||||
homepage: https://lodash.com/
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
|
||||
Based on Underscore.js, copyright Jeremy Ashkenas,
|
||||
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/lodash/lodash
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright and related rights for sample code are waived via CC0. Sample
|
||||
code is defined as all source code displayed within the prose of the
|
||||
documentation.
|
||||
|
||||
CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
====
|
||||
|
||||
Files located in the node_modules and vendor directories are externally
|
||||
maintained libraries used by this software which have their own
|
||||
licenses; we recommend you read them, as their terms may differ from the
|
||||
terms above.
|
||||
notices: []
|
||||
2644
dist/index.js
vendored
2644
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
42
package-lock.json
generated
42
package-lock.json
generated
@@ -13,11 +13,13 @@
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-yaml": "^4.0.6",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^16.11.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
@@ -1429,6 +1431,21 @@
|
||||
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.199",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz",
|
||||
"integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash.isequal": {
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz",
|
||||
"integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -4829,6 +4846,11 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -7403,6 +7425,21 @@
|
||||
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.199",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz",
|
||||
"integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash.isequal": {
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz",
|
||||
"integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
@@ -9882,6 +9919,11 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
||||
@@ -28,11 +28,13 @@
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimatch": "^9.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-yaml": "^4.0.6",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^16.11.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
|
||||
24
src/api/get-changed-files.ts
Normal file
24
src/api/get-changed-files.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
|
||||
export const getChangedFiles = async (
|
||||
client: ClientType,
|
||||
prNumber: number
|
||||
): Promise<string[]> => {
|
||||
const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const listFilesResponse = await client.paginate(listFilesOptions);
|
||||
const changedFiles = listFilesResponse.map((f: any) => f.filename);
|
||||
|
||||
core.debug('found changed files:');
|
||||
for (const file of changedFiles) {
|
||||
core.debug(' ' + file);
|
||||
}
|
||||
|
||||
return changedFiles;
|
||||
};
|
||||
38
src/api/get-changed-pull-requests.ts
Normal file
38
src/api/get-changed-pull-requests.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import {getChangedFiles} from './get-changed-files';
|
||||
import {ClientType} from './types';
|
||||
|
||||
export async function* getPullRequests(
|
||||
client: ClientType,
|
||||
prNumbers: number[]
|
||||
) {
|
||||
for (const prNumber of prNumbers) {
|
||||
core.debug(`looking for pr #${prNumber}`);
|
||||
let prData: any;
|
||||
try {
|
||||
const result = await client.rest.pulls.get({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
prData = result.data;
|
||||
} catch (error: any) {
|
||||
core.warning(`Could not find pull request #${prNumber}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.debug(`fetching changed files for pr #${prNumber}`);
|
||||
const changedFiles: string[] = await getChangedFiles(client, prNumber);
|
||||
if (!changedFiles.length) {
|
||||
core.warning(`Pull request #${prNumber} has no changed files, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {
|
||||
data: prData,
|
||||
number: prNumber,
|
||||
changedFiles
|
||||
};
|
||||
}
|
||||
}
|
||||
16
src/api/get-content.ts
Normal file
16
src/api/get-content.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
|
||||
export const getContent = async (
|
||||
client: ClientType,
|
||||
repoPath: string
|
||||
): Promise<string> => {
|
||||
const response: any = await client.rest.repos.getContent({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
path: repoPath,
|
||||
ref: github.context.sha
|
||||
});
|
||||
|
||||
return Buffer.from(response.data.content, response.data.encoding).toString();
|
||||
};
|
||||
70
src/api/get-label-globs.ts
Normal file
70
src/api/get-label-globs.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as yaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import {ClientType} from './types';
|
||||
import {getContent} from './get-content';
|
||||
|
||||
export interface MatchConfig {
|
||||
all?: string[];
|
||||
any?: string[];
|
||||
}
|
||||
|
||||
export type StringOrMatchConfig = string | MatchConfig;
|
||||
|
||||
export const getLabelGlobs = (
|
||||
client: ClientType,
|
||||
configurationPath: string
|
||||
): Promise<Map<string, StringOrMatchConfig[]>> =>
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (!fs.existsSync(configurationPath)) {
|
||||
core.info(
|
||||
`The configuration file (path: ${configurationPath}) was not found locally, fetching via the api`
|
||||
);
|
||||
|
||||
return getContent(client, configurationPath);
|
||||
}
|
||||
|
||||
core.info(
|
||||
`The configuration file (path: ${configurationPath}) was found locally, reading from the file`
|
||||
);
|
||||
|
||||
return fs.readFileSync(configurationPath, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name == 'HttpError' || error.name == 'NotFound') {
|
||||
core.warning(
|
||||
`The config file was not found at ${configurationPath}. Make sure it exists and that this action has the correct access rights.`
|
||||
);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.then(configuration => {
|
||||
// loads (hopefully) a `{[label:string]: string | StringOrMatchConfig[]}`, but is `any`:
|
||||
const configObject: any = yaml.load(configuration);
|
||||
|
||||
// transform `any` => `Map<string,StringOrMatchConfig[]>` or throw if yaml is malformed:
|
||||
return getLabelGlobMapFromObject(configObject);
|
||||
});
|
||||
|
||||
function getLabelGlobMapFromObject(
|
||||
configObject: any
|
||||
): Map<string, StringOrMatchConfig[]> {
|
||||
const labelGlobs: Map<string, StringOrMatchConfig[]> = new Map();
|
||||
|
||||
for (const label in configObject) {
|
||||
if (typeof configObject[label] === 'string') {
|
||||
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;
|
||||
}
|
||||
6
src/api/index.ts
Normal file
6
src/api/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './get-changed-files';
|
||||
export * from './get-changed-pull-requests';
|
||||
export * from './get-content';
|
||||
export * from './get-label-globs';
|
||||
export * from './set-labels';
|
||||
export * from './types';
|
||||
15
src/api/set-labels.ts
Normal file
15
src/api/set-labels.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
|
||||
export const setLabels = async (
|
||||
client: ClientType,
|
||||
prNumber: number,
|
||||
labels: string[]
|
||||
) => {
|
||||
await client.rest.issues.setLabels({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: labels
|
||||
});
|
||||
};
|
||||
2
src/api/types.ts
Normal file
2
src/api/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import * as github from '@actions/github';
|
||||
export type ClientType = ReturnType<typeof github.getOctokit>;
|
||||
10
src/get-inputs/get-inputs.ts
Normal file
10
src/get-inputs/get-inputs.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as core from '@actions/core';
|
||||
import {getPrNumbers} from './get-pr-numbers';
|
||||
|
||||
export const getInputs = () => ({
|
||||
token: core.getInput('repo-token'),
|
||||
configPath: core.getInput('configuration-path', {required: true}),
|
||||
syncLabels: !!core.getInput('sync-labels'),
|
||||
dot: core.getBooleanInput('dot'),
|
||||
prNumbers: getPrNumbers()
|
||||
});
|
||||
28
src/get-inputs/get-pr-numbers.ts
Normal file
28
src/get-inputs/get-pr-numbers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
|
||||
const getPrNumberFromContext = () =>
|
||||
github.context.payload.pull_request?.number;
|
||||
|
||||
export const getPrNumbers = (): number[] => {
|
||||
const prInput = core.getMultilineInput('pr-number');
|
||||
|
||||
if (!prInput?.length) {
|
||||
return [getPrNumberFromContext()].filter(Boolean) as number[];
|
||||
}
|
||||
|
||||
const result: number[] = [];
|
||||
|
||||
for (const line of prInput) {
|
||||
const prNumber = parseInt(line, 10);
|
||||
|
||||
if (isNaN(prNumber) && prNumber <= 0) {
|
||||
core.warning(`'${prNumber}' is not a valid pull request number`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(prNumber);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
1
src/get-inputs/index.ts
Normal file
1
src/get-inputs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './get-inputs';
|
||||
297
src/labeler.ts
297
src/labeler.ts
@@ -1,9 +1,11 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import * as pluginRetry from '@octokit/plugin-retry';
|
||||
import * as yaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import {Minimatch} from 'minimatch';
|
||||
import * as api from './api';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import {printPattern} from './utils';
|
||||
import {getInputs} from './get-inputs';
|
||||
|
||||
interface MatchConfig {
|
||||
all?: string[];
|
||||
@@ -16,217 +18,83 @@ type ClientType = ReturnType<typeof github.getOctokit>;
|
||||
// GitHub Issues cannot have more than 100 labels
|
||||
const GITHUB_MAX_LABELS = 100;
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
const token = core.getInput('repo-token');
|
||||
const configPath = core.getInput('configuration-path', {required: true});
|
||||
const syncLabels = !!core.getInput('sync-labels');
|
||||
const dot = core.getBooleanInput('dot');
|
||||
export const run = () =>
|
||||
labeler().catch(error => {
|
||||
core.error(error);
|
||||
core.setFailed(error.message);
|
||||
});
|
||||
|
||||
async function labeler() {
|
||||
const {token, configPath, syncLabels, dot, prNumbers} = getInputs();
|
||||
|
||||
if (!prNumbers.length) {
|
||||
core.warning('Could not get pull request number(s), exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const client: ClientType = github.getOctokit(token, {}, pluginRetry.retry);
|
||||
|
||||
const pullRequests = api.getPullRequests(client, prNumbers);
|
||||
|
||||
for await (const pullRequest of pullRequests) {
|
||||
const labelGlobs: Map<string, StringOrMatchConfig[]> =
|
||||
await api.getLabelGlobs(client, configPath);
|
||||
const preexistingLabels = pullRequest.data.labels.map(l => l.name);
|
||||
const allLabels: Set<string> = new Set<string>(preexistingLabels);
|
||||
|
||||
for (const [label, globs] of labelGlobs.entries()) {
|
||||
core.debug(`processing ${label}`);
|
||||
if (checkGlobs(pullRequest.changedFiles, globs, dot)) {
|
||||
allLabels.add(label);
|
||||
} else if (syncLabels) {
|
||||
allLabels.delete(label);
|
||||
}
|
||||
}
|
||||
|
||||
const labelsToAdd = [...allLabels].slice(0, GITHUB_MAX_LABELS);
|
||||
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
|
||||
|
||||
let newLabels: string[] = [];
|
||||
|
||||
try {
|
||||
if (!isEqual(labelsToAdd, preexistingLabels)) {
|
||||
await api.setLabels(client, pullRequest.number, labelsToAdd);
|
||||
newLabels = labelsToAdd.filter(
|
||||
label => !preexistingLabels.includes(label)
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.name !== 'HttpError' ||
|
||||
error.message !== 'Resource not accessible by integration'
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const prNumbers = getPrNumbers();
|
||||
if (!prNumbers.length) {
|
||||
core.warning('Could not get pull request number(s), exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const client: ClientType = github.getOctokit(token, {}, pluginRetry.retry);
|
||||
core.setOutput('new-labels', newLabels.join(','));
|
||||
core.setOutput('all-labels', labelsToAdd.join(','));
|
||||
|
||||
for (const prNumber of prNumbers) {
|
||||
core.debug(`looking for pr #${prNumber}`);
|
||||
let pullRequest: any;
|
||||
try {
|
||||
const result = await client.rest.pulls.get({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
pullRequest = result.data;
|
||||
} catch (error: any) {
|
||||
core.warning(`Could not find pull request #${prNumber}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.debug(`fetching changed files for pr #${prNumber}`);
|
||||
const changedFiles: string[] = await getChangedFiles(client, prNumber);
|
||||
if (!changedFiles.length) {
|
||||
core.warning(
|
||||
`Pull request #${prNumber} has no changed files, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelGlobs: Map<string, StringOrMatchConfig[]> =
|
||||
await getLabelGlobs(client, configPath);
|
||||
|
||||
const preexistingLabels = pullRequest.labels.map(l => l.name);
|
||||
const allLabels: Set<string> = new Set<string>(preexistingLabels);
|
||||
|
||||
for (const [label, globs] of labelGlobs.entries()) {
|
||||
core.debug(`processing ${label}`);
|
||||
if (checkGlobs(changedFiles, globs, dot)) {
|
||||
allLabels.add(label);
|
||||
} else if (syncLabels) {
|
||||
allLabels.delete(label);
|
||||
}
|
||||
}
|
||||
|
||||
const labelsToAdd = [...allLabels].slice(0, GITHUB_MAX_LABELS);
|
||||
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
|
||||
|
||||
try {
|
||||
let newLabels: string[] = [];
|
||||
|
||||
if (!isListEqual(labelsToAdd, preexistingLabels)) {
|
||||
await setLabels(client, prNumber, labelsToAdd);
|
||||
newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l));
|
||||
}
|
||||
|
||||
core.setOutput('new-labels', newLabels.join(','));
|
||||
core.setOutput('all-labels', labelsToAdd.join(','));
|
||||
|
||||
if (excessLabels.length) {
|
||||
core.warning(
|
||||
`Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join(
|
||||
', '
|
||||
)}`,
|
||||
{title: 'Label limit for a PR exceeded'}
|
||||
);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
core.error(error);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function getPrNumbers(): number[] {
|
||||
const pullRequestNumbers = core.getMultilineInput('pr-number');
|
||||
if (pullRequestNumbers && pullRequestNumbers.length) {
|
||||
const prNumbers: number[] = [];
|
||||
|
||||
for (const prNumber of pullRequestNumbers) {
|
||||
const prNumberInt = parseInt(prNumber, 10);
|
||||
if (isNaN(prNumberInt) || prNumberInt <= 0) {
|
||||
core.warning(`'${prNumber}' is not a valid pull request number`);
|
||||
} else {
|
||||
prNumbers.push(prNumberInt);
|
||||
}
|
||||
}
|
||||
|
||||
return prNumbers;
|
||||
}
|
||||
|
||||
const pullRequest = github.context.payload.pull_request;
|
||||
if (!pullRequest) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [pullRequest.number];
|
||||
}
|
||||
|
||||
async function getChangedFiles(
|
||||
client: ClientType,
|
||||
prNumber: number
|
||||
): Promise<string[]> {
|
||||
const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const listFilesResponse = await client.paginate(listFilesOptions);
|
||||
const changedFiles = listFilesResponse.map((f: any) => f.filename);
|
||||
|
||||
core.debug('found changed files:');
|
||||
for (const file of changedFiles) {
|
||||
core.debug(' ' + file);
|
||||
}
|
||||
|
||||
return changedFiles;
|
||||
}
|
||||
|
||||
async function getLabelGlobs(
|
||||
client: ClientType,
|
||||
configurationPath: string
|
||||
): Promise<Map<string, StringOrMatchConfig[]>> {
|
||||
let configurationContent: string;
|
||||
try {
|
||||
if (!fs.existsSync(configurationPath)) {
|
||||
core.info(
|
||||
`The configuration file (path: ${configurationPath}) was not found locally, fetching via the api`
|
||||
);
|
||||
configurationContent = await fetchContent(client, configurationPath);
|
||||
} else {
|
||||
core.info(
|
||||
`The configuration file (path: ${configurationPath}) was found locally, reading from the file`
|
||||
);
|
||||
configurationContent = fs.readFileSync(configurationPath, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.name == 'HttpError' || e.name == 'NotFound') {
|
||||
if (excessLabels.length) {
|
||||
core.warning(
|
||||
`The config file was not found at ${configurationPath}. Make sure it exists and that this action has the correct access rights.`
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// loads (hopefully) a `{[label:string]: string | StringOrMatchConfig[]}`, but is `any`:
|
||||
const configObject: any = yaml.load(configurationContent);
|
||||
|
||||
// transform `any` => `Map<string,StringOrMatchConfig[]>` or throw if yaml is malformed:
|
||||
return getLabelGlobMapFromObject(configObject);
|
||||
}
|
||||
|
||||
async function fetchContent(
|
||||
client: ClientType,
|
||||
repoPath: string
|
||||
): Promise<string> {
|
||||
const response: any = await client.rest.repos.getContent({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
path: repoPath,
|
||||
ref: github.context.sha
|
||||
});
|
||||
|
||||
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') {
|
||||
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)`
|
||||
`Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join(
|
||||
', '
|
||||
)}`,
|
||||
{title: 'Label limit for a PR exceeded'}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return labelGlobs;
|
||||
}
|
||||
|
||||
function toMatchConfig(config: StringOrMatchConfig): MatchConfig {
|
||||
@@ -239,10 +107,6 @@ function toMatchConfig(config: StringOrMatchConfig): MatchConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
function printPattern(matcher: Minimatch): string {
|
||||
return (matcher.negate ? '!' : '') + matcher.pattern;
|
||||
}
|
||||
|
||||
export function checkGlobs(
|
||||
changedFiles: string[],
|
||||
globs: StringOrMatchConfig[],
|
||||
@@ -329,20 +193,3 @@ function checkMatch(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isListEqual(listA: string[], listB: string[]): boolean {
|
||||
return listA.length === listB.length && listA.every(el => listB.includes(el));
|
||||
}
|
||||
|
||||
async function setLabels(
|
||||
client: ClientType,
|
||||
prNumber: number,
|
||||
labels: string[]
|
||||
) {
|
||||
await client.rest.issues.setLabels({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
|
||||
1
src/utils/index.ts
Normal file
1
src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './print-pattern';
|
||||
5
src/utils/print-pattern.ts
Normal file
5
src/utils/print-pattern.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {Minimatch} from 'minimatch';
|
||||
|
||||
export const printPattern = (matcher: Minimatch): string => {
|
||||
return (matcher.negate ? '!' : '') + matcher.pattern;
|
||||
};
|
||||
Reference in New Issue
Block a user