mirror of
https://github.com/actions/labeler.git
synced 2025-12-13 13:07:24 +00:00
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
This commit is contained in:
27
README.md
27
README.md
@@ -265,15 +265,36 @@ jobs:
|
|||||||
|
|
||||||
## Recommended Permissions
|
## Recommended Permissions
|
||||||
|
|
||||||
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
|
To successfully add labels to pull requests using the GitHub Labeler Action, specific permissions must be granted based on your use case:
|
||||||
[`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.
|
|
||||||
|
1. **Adding Existing Labels**:
|
||||||
|
- Requires: `pull-requests: write`
|
||||||
|
- Use this if all labels already exist in the repository (i.e., pre-defined in `.github/labeler.yml`).
|
||||||
|
|
||||||
|
2. **Creating New Labels**:
|
||||||
|
- Requires: `issues: write`
|
||||||
|
- This is necessary if the action needs to create labels that do not already exist in the repository.
|
||||||
|
|
||||||
|
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
|
||||||
|
[`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 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.
|
||||||
|
|
||||||
|
### Example Workflow Permissions
|
||||||
|
|
||||||
|
To ensure the action works correctly, include the following permissions in your workflow file:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Manual Label Creation as an Alternative to Granting issues write Permission
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Notes regarding `pull_request_target` event
|
## Notes regarding `pull_request_target` event
|
||||||
|
|
||||||
Using the `pull_request_target` event trigger involves several peculiarities related to initial set up of the labeler or updating version of the labeler.
|
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
|
|||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
|
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
|
import * as api from '../src/api';
|
||||||
|
import {labeler} from '../src/labeler';
|
||||||
|
import * as github from '@actions/github';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {checkMatchConfigs} from '../src/labeler';
|
import {checkMatchConfigs} from '../src/labeler';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +13,7 @@ import {
|
|||||||
} from '../src/api/get-label-configs';
|
} from '../src/api/get-label-configs';
|
||||||
|
|
||||||
jest.mock('@actions/core');
|
jest.mock('@actions/core');
|
||||||
|
jest.mock('../src/api');
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
|
jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
|
||||||
@@ -159,3 +163,73 @@ describe('checkMatchConfigs', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('labeler error handling', () => {
|
||||||
|
const mockClient = {} as any;
|
||||||
|
const mockPullRequest = {
|
||||||
|
number: 123,
|
||||||
|
data: {labels: []},
|
||||||
|
changedFiles: []
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
(github.getOctokit as jest.Mock).mockReturnValue(mockClient);
|
||||||
|
(api.getPullRequests as jest.Mock).mockReturnValue([
|
||||||
|
{
|
||||||
|
...mockPullRequest,
|
||||||
|
data: {labels: [{name: 'old-label'}]}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
(api.getLabelConfigs as jest.Mock).mockResolvedValue(
|
||||||
|
new Map([['new-label', ['dummy-config']]])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force match so "new-label" is always added
|
||||||
|
jest.spyOn({checkMatchConfigs}, 'checkMatchConfigs').mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws a custom error for HttpError 403 with "unauthorized" message', async () => {
|
||||||
|
(api.setLabels as jest.Mock).mockRejectedValue({
|
||||||
|
name: 'HttpError',
|
||||||
|
status: 403,
|
||||||
|
message: 'Request failed with status code 403: Unauthorized'
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(labeler()).rejects.toThrow(
|
||||||
|
/does not have permission to create labels/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rethrows unexpected HttpError', async () => {
|
||||||
|
const unexpectedError = {
|
||||||
|
name: 'HttpError',
|
||||||
|
status: 404,
|
||||||
|
message: 'Not Found'
|
||||||
|
};
|
||||||
|
(api.setLabels as jest.Mock).mockRejectedValue(unexpectedError);
|
||||||
|
|
||||||
|
// NOTE: In the current implementation, labeler rethrows the raw error object (not an Error instance).
|
||||||
|
// `rejects.toThrow` only works with real Error objects, so here we must use `rejects.toEqual`.
|
||||||
|
// If labeler is updated to always wrap errors in `Error`, this test can be changed to use `rejects.toThrow`.
|
||||||
|
await expect(labeler()).rejects.toEqual(unexpectedError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles "Resource not accessible by integration" gracefully', async () => {
|
||||||
|
const error = {
|
||||||
|
name: 'HttpError',
|
||||||
|
message: 'Resource not accessible by integration'
|
||||||
|
};
|
||||||
|
(api.setLabels as jest.Mock).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await labeler();
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("requires 'issues: write'"),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
expect(core.setFailed).toHaveBeenCalledWith(error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
12
dist/index.js
vendored
12
dist/index.js
vendored
@@ -1028,6 +1028,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.run = void 0;
|
exports.run = void 0;
|
||||||
|
exports.labeler = labeler;
|
||||||
exports.checkMatchConfigs = checkMatchConfigs;
|
exports.checkMatchConfigs = checkMatchConfigs;
|
||||||
exports.checkAny = checkAny;
|
exports.checkAny = checkAny;
|
||||||
exports.checkAll = checkAll;
|
exports.checkAll = checkAll;
|
||||||
@@ -1083,11 +1084,18 @@ function labeler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (error.name !== 'HttpError' ||
|
if (error.name === 'HttpError' &&
|
||||||
|
error.status === 403 &&
|
||||||
|
error.message.toLowerCase().includes('unauthorized')) {
|
||||||
|
throw new Error(`Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
|
||||||
|
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`);
|
||||||
|
}
|
||||||
|
else if (error.name !== 'HttpError' ||
|
||||||
error.message !== 'Resource not accessible by integration') {
|
error.message !== 'Resource not accessible by integration') {
|
||||||
throw error;
|
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#recommended-permissions`, {
|
core.warning(`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
|
||||||
|
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`, {
|
||||||
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
|
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
|
||||||
});
|
});
|
||||||
core.setFailed(error.message);
|
core.setFailed(error.message);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const run = () =>
|
|||||||
core.setFailed(error.message);
|
core.setFailed(error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function labeler() {
|
export async function labeler() {
|
||||||
const {token, configPath, syncLabels, dot, prNumbers} = getInputs();
|
const {token, configPath, syncLabels, dot, prNumbers} = getInputs();
|
||||||
|
|
||||||
if (!prNumbers.length) {
|
if (!prNumbers.length) {
|
||||||
@@ -65,6 +65,15 @@ async function labeler() {
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (
|
if (
|
||||||
|
error.name === 'HttpError' &&
|
||||||
|
error.status === 403 &&
|
||||||
|
error.message.toLowerCase().includes('unauthorized')
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
|
||||||
|
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
error.name !== 'HttpError' ||
|
error.name !== 'HttpError' ||
|
||||||
error.message !== 'Resource not accessible by integration'
|
error.message !== 'Resource not accessible by integration'
|
||||||
) {
|
) {
|
||||||
@@ -72,7 +81,8 @@ async function labeler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
core.warning(
|
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`,
|
`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
|
||||||
|
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`,
|
||||||
{
|
{
|
||||||
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
|
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user