mirror of
https://github.com/actions/labeler
synced 2026-09-25 15:11:47 +02:00
Preserve externally managed labels during synchronization (#957)
* Preserve externally managed pull request labels Replace whole-set label writes with batched selective additions and removals so sync-labels only manages configured labels. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182 * Reconcile ambiguous label addition failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182 * Handle paginated label reconciliation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182 --------- Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182
This commit is contained in:
co-authored by
Copilot App
parent
bf12e9b00b
commit
b7a8804475
@@ -0,0 +1,131 @@
|
||||
import {jest, describe, it, expect} from '@jest/globals';
|
||||
import type {ClientType} from '../src/api/types.js';
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: {
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
}
|
||||
}));
|
||||
|
||||
const {addLabels} = await import('../src/api/add-labels.js');
|
||||
|
||||
const createClient = () => {
|
||||
const addLabelsMock = jest.fn<any>();
|
||||
const listLabelsOnIssueMock = jest.fn<any>();
|
||||
const client = {
|
||||
rest: {
|
||||
issues: {
|
||||
addLabels: addLabelsMock,
|
||||
listLabelsOnIssue: listLabelsOnIssueMock
|
||||
}
|
||||
}
|
||||
} as ClientType;
|
||||
|
||||
return {client, addLabelsMock, listLabelsOnIssueMock};
|
||||
};
|
||||
|
||||
describe('addLabels', () => {
|
||||
it('does not verify a successful addition', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
addLabelsMock.mockResolvedValue({data: []});
|
||||
|
||||
await addLabels(client, 123, ['bug']);
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['bug'],
|
||||
request: {retries: 0}
|
||||
});
|
||||
expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts a server error when every requested label was committed', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockResolvedValue({
|
||||
data: [{name: 'BUG'}, {name: 'documentation'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
addLabels(client, 123, ['bug', 'documentation'])
|
||||
).resolves.toBeUndefined();
|
||||
expect(listLabelsOnIssueMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
per_page: 100,
|
||||
page: 1,
|
||||
request: {retries: 0}
|
||||
});
|
||||
});
|
||||
|
||||
it('checks subsequent pages after a committed server error', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock
|
||||
.mockResolvedValueOnce({
|
||||
data: Array.from({length: 100}, (_, index) => ({
|
||||
name: `label-${index}`
|
||||
})),
|
||||
headers: {
|
||||
link: '<https://api.github.com/issues/123/labels?page=2>; rel="next"'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: [{name: 'documentation'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
addLabels(client, 123, ['label-0', 'documentation'])
|
||||
).resolves.toBeUndefined();
|
||||
expect(listLabelsOnIssueMock).toHaveBeenNthCalledWith(2, {
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
per_page: 100,
|
||||
page: 2,
|
||||
request: {retries: 0}
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a server error when any requested label is missing', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockResolvedValue({
|
||||
data: [{name: 'bug'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(addLabels(client, 123, ['bug', 'documentation'])).rejects.toBe(
|
||||
serverError
|
||||
);
|
||||
expect(listLabelsOnIssueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not verify non-server errors', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const validationError = Object.assign(new Error('Validation Failed'), {
|
||||
status: 422
|
||||
});
|
||||
addLabelsMock.mockRejectedValue(validationError);
|
||||
|
||||
await expect(addLabels(client, 123, ['bug'])).rejects.toBe(validationError);
|
||||
expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves the server error when verification fails', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockRejectedValue(new Error('Service unavailable'));
|
||||
|
||||
await expect(addLabels(client, 123, ['bug'])).rejects.toBe(serverError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user