Added ability to pass in an optional PR number as a parameter (#349)

* Adding pr-number as an optional parameter

* Updated README

* Tests on the pr-number parameter

* Added missing | to table

* re-built script

* Adding support for multiple pr-numbers

* excluded .idea

* Updated readme to reflect that there might be more than one PR

* Additional warning

* Removed unused

* Reformatted and re-built

* Corrected message

* Removed required check

* Added (s) to pull request numbers in the description

Co-authored-by: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com>

* Reworded PR-number parameter description

Co-authored-by: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com>

* adding getMultilineInput into the tests

* Fixing tests for single pr

* Fixing tests for multiple prs

* Updated README.md to make it more obvious that it can take a list of PRs

* Added example that labels PR's 1-3

* Handled no pull requests better (from code review)

* Handled no pull requests better (from code review)

* Handled missing pull request better (from code review)

* Back out suggested change as it broke the tests

* Rebuilt dist

* Update src/labeler.ts

Co-authored-by: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com>

* Added Emphasis to the note

Co-authored-by: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com>

* Changed mockInput for pr-number to be string[]

---------

Co-authored-by: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com>
This commit is contained in:
Mark Ridgwell
2023-07-06 16:10:50 +01:00
committed by GitHub
parent 65f306b6dd
commit 327d35fdca
7 changed files with 287 additions and 115 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
.DS_Store .DS_Store
node_modules/ node_modules/
lib/ lib/
.idea .idea

View File

@@ -126,8 +126,9 @@ Various inputs are defined in [`action.yml`](action.yml) to let you configure th
| `configuration-path` | The path to the label configuration file | `.github/labeler.yml` | | `configuration-path` | The path to the label configuration file | `.github/labeler.yml` |
| `sync-labels` | Whether or not to remove labels when matching files are reverted or no longer changed by the PR | `false` | | `sync-labels` | Whether or not to remove labels when matching files are reverted or no longer changed by the PR | `false` |
| `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `false` | | `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `false` |
| `pr-number` | The number(s) of pull request to update, rather than detecting from the workflow context | N/A |
When `dot` is disabled and you want to include _all_ files in a folder: When `dot` is disabled, and you want to include _all_ files in a folder:
```yml ```yml
label1: label1:
@@ -142,6 +143,35 @@ label1:
- path/to/folder/** - path/to/folder/**
``` ```
##### Example workflow specifying Pull request numbers
```yml
name: "Label Previous Pull Requests"
on:
schedule:
- cron: "0 1 * * 1"
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
# Label PRs 1, 2, and 3
- uses: actions/labeler@v4
with:
pr-number: |
1
2
3
```
**Note:** in normal usage the `pr-number` input is not required as the action will detect the PR number from the workflow context.
#### Outputs #### Outputs
Labeler provides the following outputs: Labeler provides the following outputs:

View File

@@ -16,7 +16,11 @@ const mockApi = {
setLabels: jest.fn() setLabels: jest.fn()
}, },
pulls: { pulls: {
get: jest.fn().mockResolvedValue({}), get: jest.fn().mockResolvedValue({
data: {
labels: []
}
}),
listFiles: { listFiles: {
endpoint: { endpoint: {
merge: jest.fn().mockReturnValue({}) merge: jest.fn().mockReturnValue({})

View File

@@ -25,11 +25,15 @@ const configureInput = (
'configuration-path': string; 'configuration-path': string;
'sync-labels': boolean; 'sync-labels': boolean;
dot: boolean; dot: boolean;
'pr-number': string[];
}> }>
) => { ) => {
jest jest
.spyOn(core, 'getInput') .spyOn(core, 'getInput')
.mockImplementation((name: string, ...opts) => mockInput[name]); .mockImplementation((name: string, ...opts) => mockInput[name]);
jest
.spyOn(core, 'getMultilineInput')
.mockImplementation((name: string, ...opts) => mockInput[name]);
jest jest
.spyOn(core, 'getBooleanInput') .spyOn(core, 'getBooleanInput')
.mockImplementation((name: string, ...opts) => mockInput[name]); .mockImplementation((name: string, ...opts) => mockInput[name]);
@@ -209,6 +213,88 @@ describe('run', () => {
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', ''); expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', allLabels); expect(setOutputSpy).toHaveBeenCalledWith('all-labels', allLabels);
}); });
it('(with pr-number: array of one item, uses the PR number specified in the parameters', async () => {
configureInput({
'repo-token': 'foo',
'configuration-path': 'bar',
'pr-number': ['104']
});
usingLabelerConfigYaml('only_pdfs.yml');
mockGitHubResponseChangedFiles('foo.pdf');
getPullMock.mockResolvedValue(<any>{
data: {
labels: [{name: 'manually-added'}]
}
});
await run();
expect(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
labels: ['manually-added', 'touched-a-pdf-file']
});
expect(setOutputSpy).toHaveBeenCalledWith(
'new-labels',
'touched-a-pdf-file'
);
expect(setOutputSpy).toHaveBeenCalledWith(
'all-labels',
'manually-added,touched-a-pdf-file'
);
});
it('(with pr-number: array of two items, uses the PR number specified in the parameters', async () => {
configureInput({
'repo-token': 'foo',
'configuration-path': 'bar',
'pr-number': ['104', '150']
});
usingLabelerConfigYaml('only_pdfs.yml');
mockGitHubResponseChangedFiles('foo.pdf');
getPullMock.mockResolvedValueOnce(<any>{
data: {
labels: [{name: 'manually-added'}]
}
});
getPullMock.mockResolvedValueOnce(<any>{
data: {
labels: []
}
});
await run();
expect(setLabelsMock).toHaveBeenCalledTimes(2);
expect(setLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
labels: ['manually-added', 'touched-a-pdf-file']
});
expect(setLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 150,
labels: ['touched-a-pdf-file']
});
expect(setOutputSpy).toHaveBeenCalledWith(
'new-labels',
'touched-a-pdf-file'
);
expect(setOutputSpy).toHaveBeenCalledWith(
'all-labels',
'manually-added,touched-a-pdf-file'
);
});
}); });
function usingLabelerConfigYaml(fixtureName: keyof typeof yamlFixtures): void { function usingLabelerConfigYaml(fixtureName: keyof typeof yamlFixtures): void {

View File

@@ -18,6 +18,9 @@ inputs:
description: 'Whether or not to auto-include paths starting with dot (e.g. `.github`)' description: 'Whether or not to auto-include paths starting with dot (e.g. `.github`)'
default: false default: false
required: false required: false
pr-number:
description: 'The pull request number(s)'
required: false
outputs: outputs:
new-labels: new-labels:

119
dist/index.js vendored
View File

@@ -54,55 +54,66 @@ function run() {
const configPath = core.getInput('configuration-path', { required: true }); const configPath = core.getInput('configuration-path', { required: true });
const syncLabels = !!core.getInput('sync-labels'); const syncLabels = !!core.getInput('sync-labels');
const dot = core.getBooleanInput('dot'); const dot = core.getBooleanInput('dot');
const prNumber = getPrNumber(); const prNumbers = getPrNumbers();
if (!prNumber) { if (!prNumbers.length) {
core.info('Could not get pull request number from context, exiting'); core.warning('Could not get pull request number(s), exiting');
return; return;
} }
const client = github.getOctokit(token, {}, pluginRetry.retry); const client = github.getOctokit(token, {}, pluginRetry.retry);
const { data: pullRequest } = yield client.rest.pulls.get({ for (const prNumber of prNumbers) {
owner: github.context.repo.owner, core.debug(`looking for pr #${prNumber}`);
repo: github.context.repo.repo, let pullRequest;
pull_number: prNumber try {
}); const result = yield client.rest.pulls.get({
core.debug(`fetching changed files for pr #${prNumber}`); owner: github.context.repo.owner,
const changedFiles = yield getChangedFiles(client, prNumber); repo: github.context.repo.repo,
const labelGlobs = yield getLabelGlobs(client, configPath); pull_number: prNumber
const preexistingLabels = pullRequest.labels.map(l => l.name);
const allLabels = new Set(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 = [];
if (!isListEqual(labelsToAdd, preexistingLabels)) {
yield 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) {
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); pullRequest = result.data;
} }
else { catch (error) {
throw error; core.warning(`Could not find pull request #${prNumber}, skipping`);
continue;
}
core.debug(`fetching changed files for pr #${prNumber}`);
const changedFiles = yield getChangedFiles(client, prNumber);
const labelGlobs = yield getLabelGlobs(client, configPath);
const preexistingLabels = pullRequest.labels.map(l => l.name);
const allLabels = new Set(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 = [];
if (!isListEqual(labelsToAdd, preexistingLabels)) {
yield 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) {
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;
}
} }
} }
} }
@@ -113,12 +124,26 @@ function run() {
}); });
} }
exports.run = run; exports.run = run;
function getPrNumber() { function getPrNumbers() {
const pullRequestNumbers = core.getMultilineInput('pr-number');
if (pullRequestNumbers && pullRequestNumbers.length) {
const prNumbers = [];
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; const pullRequest = github.context.payload.pull_request;
if (!pullRequest) { if (!pullRequest) {
return undefined; return [];
} }
return pullRequest.number; return [pullRequest.number];
} }
function getChangedFiles(client, prNumber) { function getChangedFiles(client, prNumber) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {

View File

@@ -22,75 +22,83 @@ export async function run() {
const syncLabels = !!core.getInput('sync-labels'); const syncLabels = !!core.getInput('sync-labels');
const dot = core.getBooleanInput('dot'); const dot = core.getBooleanInput('dot');
const prNumber = getPrNumber(); const prNumbers = getPrNumbers();
if (!prNumber) { if (!prNumbers.length) {
core.info('Could not get pull request number from context, exiting'); core.warning('Could not get pull request number(s), exiting');
return; return;
} }
const client: ClientType = github.getOctokit(token, {}, pluginRetry.retry); const client: ClientType = github.getOctokit(token, {}, pluginRetry.retry);
const {data: pullRequest} = await client.rest.pulls.get({ for (const prNumber of prNumbers) {
owner: github.context.repo.owner, core.debug(`looking for pr #${prNumber}`);
repo: github.context.repo.repo, let pullRequest: any;
pull_number: prNumber try {
}); const result = await client.rest.pulls.get({
owner: github.context.repo.owner,
core.debug(`fetching changed files for pr #${prNumber}`); repo: github.context.repo.repo,
const changedFiles: string[] = await getChangedFiles(client, prNumber); pull_number: prNumber
const labelGlobs: Map<string, StringOrMatchConfig[]> = await getLabelGlobs( });
client, pullRequest = result.data;
configPath } catch (error: any) {
); core.warning(`Could not find pull request #${prNumber}, skipping`);
continue;
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.debug(`fetching changed files for pr #${prNumber}`);
core.setOutput('all-labels', labelsToAdd.join(',')); const changedFiles: string[] = await getChangedFiles(client, prNumber);
const labelGlobs: Map<string, StringOrMatchConfig[]> =
await getLabelGlobs(client, configPath);
if (excessLabels.length) { const preexistingLabels = pullRequest.labels.map(l => l.name);
core.warning( const allLabels: Set<string> = new Set<string>(preexistingLabels);
`Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join(
', ' for (const [label, globs] of labelGlobs.entries()) {
)}`, core.debug(`processing ${label}`);
{title: 'Label limit for a PR exceeded'} if (checkGlobs(changedFiles, globs, dot)) {
); allLabels.add(label);
} else if (syncLabels) {
allLabels.delete(label);
}
} }
} catch (error: any) {
if ( const labelsToAdd = [...allLabels].slice(0, GITHUB_MAX_LABELS);
error.name === 'HttpError' && const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
error.message === 'Resource not accessible by integration'
) { try {
core.warning( let newLabels: string[] = [];
`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`,
{ if (!isListEqual(labelsToAdd, preexistingLabels)) {
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured` await setLabels(client, prNumber, labelsToAdd);
} newLabels = labelsToAdd.filter(l => !preexistingLabels.includes(l));
); }
core.setFailed(error.message);
} else { core.setOutput('new-labels', newLabels.join(','));
throw error; 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) { } catch (error: any) {
@@ -99,13 +107,29 @@ export async function run() {
} }
} }
function getPrNumber(): number | undefined { function getPrNumbers(): number[] {
const pullRequest = github.context.payload.pull_request; const pullRequestNumbers = core.getMultilineInput('pr-number');
if (!pullRequest) { if (pullRequestNumbers && pullRequestNumbers.length) {
return undefined; 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;
} }
return pullRequest.number; const pullRequest = github.context.payload.pull_request;
if (!pullRequest) {
return [];
}
return [pullRequest.number];
} }
async function getChangedFiles( async function getChangedFiles(