Skip to content

Commit 25abb3c

Browse files
Improve Labeler Action Documentation and Error Handling for Permissions (#897)
* Update README.md and labeler.ts to clarify permissions for GitHub Labeler Action * Update dist/index.js with latest build changes * Update README.md to clarify manual label creation as an alternative to granting issues write permission * Fix labeler error handling to ensure case-insensitive check for unauthorized access * Refactor error handling in labeler to throw an error for unauthorized access instead of logging * Add tests for labeler error handling and improve error reporting
1 parent 395c8cf commit 25abb3c

4 files changed

Lines changed: 120 additions & 7 deletions

File tree

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,15 +265,36 @@ jobs:
265265

266266
## Recommended Permissions
267267

268-
In order to add labels to pull requests, the GitHub labeler action requires write permissions on the pull-request. However, when the action runs on a pull request from a forked repository, GitHub only grants read access tokens for `pull_request` events, at most. If you encounter an `Error: HttpError: Resource not accessible by integration`, it's likely due to these permission constraints. To resolve this issue, you can modify the `on:` section of your workflow to use
269-
[`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) instead of `pull_request` (see example [above](#create-workflow)). This change allows the action to have write access, because `pull_request_target` alters the [context of the action](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) and safely grants additional permissions. There exists a potentially dangerous misuse of the pull_request_target workflow trigger that may lead to malicious PR authors (i.e. attackers) being able to obtain repository write permissions or stealing repository secrets, Hence it is advisible that pull_request_target should only be used in workflows that are carefully designed to avoid executing untrusted code and to also ensure that workflows using pull_request_target limit access to sensitive resources. Refer to the [GitHub token permissions documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) for more details about access levels and event contexts.
268+
To successfully add labels to pull requests using the GitHub Labeler Action, specific permissions must be granted based on your use case:
269+
270+
1. **Adding Existing Labels**:
271+
- Requires: `pull-requests: write`
272+
- Use this if all labels already exist in the repository (i.e., pre-defined in `.github/labeler.yml`).
273+
274+
2. **Creating New Labels**:
275+
- Requires: `issues: write`
276+
- This is necessary if the action needs to create labels that do not already exist in the repository.
277+
278+
However, when the action runs on a pull request from a forked repository, GitHub only grants read access tokens for `pull_request` events, at most. If you encounter an `Error: HttpError: Resource not accessible by integration`, it's likely due to these permission constraints. To resolve this issue, you can modify the `on:` section of your workflow to use
279+
[`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) instead of `pull_request` (see example [above](#create-workflow)). This change allows the action to have write access, because `pull_request_target` alters the [context of the action](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) and safely grants additional permissions.
280+
281+
There exists a potentially dangerous misuse of the `pull_request_target` workflow trigger that may lead to malicious PR authors (i.e. attackers) being able to obtain repository write permissions or stealing repository secrets. Hence, it is advisable that `pull_request_target` should only be used in workflows that are carefully designed to avoid executing untrusted code and to also ensure that workflows using `pull_request_target` limit access to sensitive resources. Refer to the [GitHub token permissions documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) for more details about access levels and event contexts.
282+
283+
### Example Workflow Permissions
284+
285+
To ensure the action works correctly, include the following permissions in your workflow file:
270286

271287
```yml
272288
permissions:
273289
contents: read
274290
pull-requests: write
291+
issues: write
275292
```
276293

294+
### Manual Label Creation as an Alternative to Granting issues write Permission
295+
296+
If you prefer not to grant the `issues: write` permission in your workflow, you can manually create all required labels in the repository before the action runs.
297+
277298
## Notes regarding `pull_request_target` event
278299

279300
Using the `pull_request_target` event trigger involves several peculiarities related to initial set up of the labeler or updating version of the labeler.
@@ -298,4 +319,4 @@ Once you confirm that the updated configuration files function as intended, you
298319

299320
## Contributions
300321

301-
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
322+
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).

__tests__/labeler.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as yaml from 'js-yaml';
22
import * as core from '@actions/core';
3+
import * as api from '../src/api';
4+
import {labeler} from '../src/labeler';
5+
import * as github from '@actions/github';
36
import * as fs from 'fs';
47
import {checkMatchConfigs} from '../src/labeler';
58
import {
@@ -10,6 +13,7 @@ import {
1013
} from '../src/api/get-label-configs';
1114

1215
jest.mock('@actions/core');
16+
jest.mock('../src/api');
1317

1418
beforeAll(() => {
1519
jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
@@ -159,3 +163,73 @@ describe('checkMatchConfigs', () => {
159163
});
160164
});
161165
});
166+
167+
describe('labeler error handling', () => {
168+
const mockClient = {} as any;
169+
const mockPullRequest = {
170+
number: 123,
171+
data: {labels: []},
172+
changedFiles: []
173+
};
174+
175+
beforeEach(() => {
176+
jest.resetAllMocks();
177+
178+
(github.getOctokit as jest.Mock).mockReturnValue(mockClient);
179+
(api.getPullRequests as jest.Mock).mockReturnValue([
180+
{
181+
...mockPullRequest,
182+
data: {labels: [{name: 'old-label'}]}
183+
}
184+
]);
185+
186+
(api.getLabelConfigs as jest.Mock).mockResolvedValue(
187+
new Map([['new-label', ['dummy-config']]])
188+
);
189+
190+
// Force match so "new-label" is always added
191+
jest.spyOn({checkMatchConfigs}, 'checkMatchConfigs').mockReturnValue(true);
192+
});
193+
194+
it('throws a custom error for HttpError 403 with "unauthorized" message', async () => {
195+
(api.setLabels as jest.Mock).mockRejectedValue({
196+
name: 'HttpError',
197+
status: 403,
198+
message: 'Request failed with status code 403: Unauthorized'
199+
});
200+
201+
await expect(labeler()).rejects.toThrow(
202+
/does not have permission to create labels/
203+
);
204+
});
205+
206+
it('rethrows unexpected HttpError', async () => {
207+
const unexpectedError = {
208+
name: 'HttpError',
209+
status: 404,
210+
message: 'Not Found'
211+
};
212+
(api.setLabels as jest.Mock).mockRejectedValue(unexpectedError);
213+
214+
// NOTE: In the current implementation, labeler rethrows the raw error object (not an Error instance).
215+
// `rejects.toThrow` only works with real Error objects, so here we must use `rejects.toEqual`.
216+
// If labeler is updated to always wrap errors in `Error`, this test can be changed to use `rejects.toThrow`.
217+
await expect(labeler()).rejects.toEqual(unexpectedError);
218+
});
219+
220+
it('handles "Resource not accessible by integration" gracefully', async () => {
221+
const error = {
222+
name: 'HttpError',
223+
message: 'Resource not accessible by integration'
224+
};
225+
(api.setLabels as jest.Mock).mockRejectedValue(error);
226+
227+
await labeler();
228+
229+
expect(core.warning).toHaveBeenCalledWith(
230+
expect.stringContaining("requires 'issues: write'"),
231+
expect.any(Object)
232+
);
233+
expect(core.setFailed).toHaveBeenCalledWith(error.message);
234+
});
235+
});

dist/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10281028
};
10291029
Object.defineProperty(exports, "__esModule", ({ value: true }));
10301030
exports.run = void 0;
1031+
exports.labeler = labeler;
10311032
exports.checkMatchConfigs = checkMatchConfigs;
10321033
exports.checkAny = checkAny;
10331034
exports.checkAll = checkAll;
@@ -1083,11 +1084,18 @@ function labeler() {
10831084
}
10841085
}
10851086
catch (error) {
1086-
if (error.name !== 'HttpError' ||
1087+
if (error.name === 'HttpError' &&
1088+
error.status === 403 &&
1089+
error.message.toLowerCase().includes('unauthorized')) {
1090+
throw new Error(`Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
1091+
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`);
1092+
}
1093+
else if (error.name !== 'HttpError' ||
10871094
error.message !== 'Resource not accessible by integration') {
10881095
throw error;
10891096
}
1090-
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#recommended-permissions`, {
1097+
core.warning(`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
1098+
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`, {
10911099
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
10921100
});
10931101
core.setFailed(error.message);

src/labeler.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const run = () =>
2222
core.setFailed(error.message);
2323
});
2424

25-
async function labeler() {
25+
export async function labeler() {
2626
const {token, configPath, syncLabels, dot, prNumbers} = getInputs();
2727

2828
if (!prNumbers.length) {
@@ -65,14 +65,24 @@ async function labeler() {
6565
}
6666
} catch (error: any) {
6767
if (
68+
error.name === 'HttpError' &&
69+
error.status === 403 &&
70+
error.message.toLowerCase().includes('unauthorized')
71+
) {
72+
throw new Error(
73+
`Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
74+
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`
75+
);
76+
} else if (
6877
error.name !== 'HttpError' ||
6978
error.message !== 'Resource not accessible by integration'
7079
) {
7180
throw error;
7281
}
7382

7483
core.warning(
75-
`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#recommended-permissions`,
84+
`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
85+
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`,
7686
{
7787
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
7888
}

0 commit comments

Comments
 (0)