mirror of
https://github.com/peter-evans/create-pull-request.git
synced 2026-05-14 02:22:41 +00:00
fix: retry post-creation API calls on 422 eventual consistency errors (#4356)
Add retry logic to handle GitHub API eventual consistency errors that can occur after creating a new pull request. Follow-up API calls for milestones, labels, assignees, and reviewers may fail with a 422 "Could not resolve to a node" error before the PR is fully propagated. - Add generic `retryWithBackoff` helper in `src/utils.ts` with exponential backoff (default 2 retries, starting at 1s delay) - Wrap post-creation API calls in `src/github-helper.ts` with `withRetryForNewPr()`, which only retries for newly created PRs - Use `@octokit/request-error` `RequestError` type for precise error matching (status 422 + "Could not resolve to a node" message) - Add unit tests for `retryWithBackoff` covering success, retry, exhaustion, and non-retryable error scenarios - Update `dist/index.js` bundle and `package.json` dependencies
This commit is contained in:
@@ -118,3 +118,71 @@ describe('utils tests', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('retryWithBackoff', () => {
|
||||||
|
const makeConsistencyError = () => {
|
||||||
|
const error = new Error(
|
||||||
|
'Validation Failed: "Could not resolve to a node with the global id of \'PR_abc123\'."'
|
||||||
|
)
|
||||||
|
;(error as any).status = 422
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRetry = (e: unknown): boolean =>
|
||||||
|
e instanceof Error &&
|
||||||
|
(e as any).status === 422 &&
|
||||||
|
e.message.includes('Could not resolve to a node')
|
||||||
|
|
||||||
|
test('succeeds on first attempt without retrying', async () => {
|
||||||
|
const fn = jest.fn().mockResolvedValue('success')
|
||||||
|
const result = await utils.retryWithBackoff(fn, shouldRetry, 2, 1)
|
||||||
|
expect(result).toBe('success')
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('retries on eventual consistency 422 and succeeds', async () => {
|
||||||
|
const fn = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(makeConsistencyError())
|
||||||
|
.mockResolvedValue('success')
|
||||||
|
const result = await utils.retryWithBackoff(fn, shouldRetry, 2, 1)
|
||||||
|
expect(result).toBe('success')
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('exhausts retries on persistent 422 and throws', async () => {
|
||||||
|
const fn = jest.fn().mockRejectedValue(makeConsistencyError())
|
||||||
|
await expect(utils.retryWithBackoff(fn, shouldRetry, 2, 1)).rejects.toThrow(
|
||||||
|
'Could not resolve to a node'
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3) // 1 initial + 2 retries
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not retry on non-422 errors', async () => {
|
||||||
|
const error = new Error('Forbidden')
|
||||||
|
;(error as any).status = 403
|
||||||
|
const fn = jest.fn().mockRejectedValue(error)
|
||||||
|
await expect(utils.retryWithBackoff(fn, shouldRetry, 2, 1)).rejects.toThrow(
|
||||||
|
'Forbidden'
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not retry on 422 without the consistency error message', async () => {
|
||||||
|
const error = new Error('Validation Failed: invalid label')
|
||||||
|
;(error as any).status = 422
|
||||||
|
const fn = jest.fn().mockRejectedValue(error)
|
||||||
|
await expect(utils.retryWithBackoff(fn, shouldRetry, 2, 1)).rejects.toThrow(
|
||||||
|
'Validation Failed: invalid label'
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not retry on plain Error objects', async () => {
|
||||||
|
const fn = jest.fn().mockRejectedValue(new Error('Something broke'))
|
||||||
|
await expect(utils.retryWithBackoff(fn, shouldRetry, 2, 1)).rejects.toThrow(
|
||||||
|
'Something broke'
|
||||||
|
)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Vendored
+50
-10
@@ -1375,6 +1375,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.GitHubHelper = void 0;
|
exports.GitHubHelper = void 0;
|
||||||
const core = __importStar(__nccwpck_require__(7484));
|
const core = __importStar(__nccwpck_require__(7484));
|
||||||
|
const request_error_1 = __nccwpck_require__(1015);
|
||||||
const octokit_client_1 = __nccwpck_require__(3489);
|
const octokit_client_1 = __nccwpck_require__(3489);
|
||||||
const p_limit_1 = __importDefault(__nccwpck_require__(7989));
|
const p_limit_1 = __importDefault(__nccwpck_require__(7989));
|
||||||
const utils = __importStar(__nccwpck_require__(9277));
|
const utils = __importStar(__nccwpck_require__(9277));
|
||||||
@@ -1501,20 +1502,28 @@ class GitHubHelper {
|
|||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
// Create or update the pull request
|
// Create or update the pull request
|
||||||
const pull = yield this.createOrUpdate(inputs, baseRepository, headRepository);
|
const pull = yield this.createOrUpdate(inputs, baseRepository, headRepository);
|
||||||
|
// After creating a new PR, follow-up API calls can fail with a 422
|
||||||
|
// "Could not resolve to a node" error due to GitHub API eventual
|
||||||
|
// consistency. Wrap post-creation calls with targeted retry logic.
|
||||||
|
// See: https://github.com/peter-evans/create-pull-request/issues/4321
|
||||||
|
const isEventualConsistencyError = (e) => e instanceof request_error_1.RequestError &&
|
||||||
|
e.status === 422 &&
|
||||||
|
e.message.includes('Could not resolve to a node');
|
||||||
|
const withRetryForNewPr = (fn) => pull.created ? utils.retryWithBackoff(fn, isEventualConsistencyError) : fn();
|
||||||
// Apply milestone
|
// Apply milestone
|
||||||
if (inputs.milestone) {
|
if (inputs.milestone) {
|
||||||
core.info(`Applying milestone '${inputs.milestone}'`);
|
core.info(`Applying milestone '${inputs.milestone}'`);
|
||||||
yield this.octokit.rest.issues.update(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, milestone: inputs.milestone }));
|
yield withRetryForNewPr(() => this.octokit.rest.issues.update(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, milestone: inputs.milestone })));
|
||||||
}
|
}
|
||||||
// Apply labels
|
// Apply labels
|
||||||
if (inputs.labels.length > 0) {
|
if (inputs.labels.length > 0) {
|
||||||
core.info(`Applying labels '${inputs.labels}'`);
|
core.info(`Applying labels '${inputs.labels}'`);
|
||||||
yield this.octokit.rest.issues.addLabels(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, labels: inputs.labels }));
|
yield withRetryForNewPr(() => this.octokit.rest.issues.addLabels(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, labels: inputs.labels })));
|
||||||
}
|
}
|
||||||
// Apply assignees
|
// Apply assignees
|
||||||
if (inputs.assignees.length > 0) {
|
if (inputs.assignees.length > 0) {
|
||||||
core.info(`Applying assignees '${inputs.assignees}'`);
|
core.info(`Applying assignees '${inputs.assignees}'`);
|
||||||
yield this.octokit.rest.issues.addAssignees(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, assignees: inputs.assignees }));
|
yield withRetryForNewPr(() => this.octokit.rest.issues.addAssignees(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { issue_number: pull.number, assignees: inputs.assignees })));
|
||||||
}
|
}
|
||||||
// Request reviewers and team reviewers
|
// Request reviewers and team reviewers
|
||||||
const requestReviewersParams = {};
|
const requestReviewersParams = {};
|
||||||
@@ -1529,7 +1538,7 @@ class GitHubHelper {
|
|||||||
}
|
}
|
||||||
if (Object.keys(requestReviewersParams).length > 0) {
|
if (Object.keys(requestReviewersParams).length > 0) {
|
||||||
try {
|
try {
|
||||||
yield this.octokit.rest.pulls.requestReviewers(Object.assign(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pull.number }), requestReviewersParams));
|
yield withRetryForNewPr(() => this.octokit.rest.pulls.requestReviewers(Object.assign(Object.assign(Object.assign({}, this.parseRepository(baseRepository)), { pull_number: pull.number }), requestReviewersParams)));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
||||||
@@ -1900,6 +1909,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.isSelfHosted = void 0;
|
exports.isSelfHosted = void 0;
|
||||||
exports.getInputAsArray = getInputAsArray;
|
exports.getInputAsArray = getInputAsArray;
|
||||||
@@ -1913,6 +1931,7 @@ exports.parseDisplayNameEmail = parseDisplayNameEmail;
|
|||||||
exports.fileExistsSync = fileExistsSync;
|
exports.fileExistsSync = fileExistsSync;
|
||||||
exports.readFile = readFile;
|
exports.readFile = readFile;
|
||||||
exports.getErrorMessage = getErrorMessage;
|
exports.getErrorMessage = getErrorMessage;
|
||||||
|
exports.retryWithBackoff = retryWithBackoff;
|
||||||
const core = __importStar(__nccwpck_require__(7484));
|
const core = __importStar(__nccwpck_require__(7484));
|
||||||
const fs = __importStar(__nccwpck_require__(9896));
|
const fs = __importStar(__nccwpck_require__(9896));
|
||||||
const path = __importStar(__nccwpck_require__(6928));
|
const path = __importStar(__nccwpck_require__(6928));
|
||||||
@@ -2014,6 +2033,26 @@ const isSelfHosted = () => process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted'
|
|||||||
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
|
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
|
||||||
process.env['AGENT_ISSELFHOSTED'] === undefined);
|
process.env['AGENT_ISSELFHOSTED'] === undefined);
|
||||||
exports.isSelfHosted = isSelfHosted;
|
exports.isSelfHosted = isSelfHosted;
|
||||||
|
function retryWithBackoff(fn_1, shouldRetry_1) {
|
||||||
|
return __awaiter(this, arguments, void 0, function* (fn, shouldRetry, maxRetries = 2, delayMs = 1000) {
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return yield fn();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (attempt < maxRetries && shouldRetry(e)) {
|
||||||
|
const delay = delayMs * Math.pow(2, attempt);
|
||||||
|
core.info(`Request failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...`);
|
||||||
|
yield new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unexpected: retry loop exited without return or throw');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
@@ -32825,7 +32864,7 @@ async function fetchWrapper(requestOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const requestError = new dist_src/* RequestError */.G(message, 500, {
|
const requestError = new dist_src.RequestError(message, 500, {
|
||||||
request: requestOptions
|
request: requestOptions
|
||||||
});
|
});
|
||||||
requestError.cause = error;
|
requestError.cause = error;
|
||||||
@@ -32857,21 +32896,21 @@ async function fetchWrapper(requestOptions) {
|
|||||||
if (status < 400) {
|
if (status < 400) {
|
||||||
return octokitResponse;
|
return octokitResponse;
|
||||||
}
|
}
|
||||||
throw new dist_src/* RequestError */.G(fetchResponse.statusText, status, {
|
throw new dist_src.RequestError(fetchResponse.statusText, status, {
|
||||||
response: octokitResponse,
|
response: octokitResponse,
|
||||||
request: requestOptions
|
request: requestOptions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (status === 304) {
|
if (status === 304) {
|
||||||
octokitResponse.data = await getResponseData(fetchResponse);
|
octokitResponse.data = await getResponseData(fetchResponse);
|
||||||
throw new dist_src/* RequestError */.G("Not modified", status, {
|
throw new dist_src.RequestError("Not modified", status, {
|
||||||
response: octokitResponse,
|
response: octokitResponse,
|
||||||
request: requestOptions
|
request: requestOptions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (status >= 400) {
|
if (status >= 400) {
|
||||||
octokitResponse.data = await getResponseData(fetchResponse);
|
octokitResponse.data = await getResponseData(fetchResponse);
|
||||||
throw new dist_src/* RequestError */.G(toErrorMessage(octokitResponse.data), status, {
|
throw new dist_src.RequestError(toErrorMessage(octokitResponse.data), status, {
|
||||||
response: octokitResponse,
|
response: octokitResponse,
|
||||||
request: requestOptions
|
request: requestOptions
|
||||||
});
|
});
|
||||||
@@ -36220,7 +36259,7 @@ async function requestWithGraphqlErrorHandling(state, octokit, request, options)
|
|||||||
if (response.data && response.data.errors && response.data.errors.length > 0 && /Something went wrong while executing your query/.test(
|
if (response.data && response.data.errors && response.data.errors.length > 0 && /Something went wrong while executing your query/.test(
|
||||||
response.data.errors[0].message
|
response.data.errors[0].message
|
||||||
)) {
|
)) {
|
||||||
const error = new _octokit_request_error__WEBPACK_IMPORTED_MODULE_1__/* .RequestError */ .G(response.data.errors[0].message, 500, {
|
const error = new _octokit_request_error__WEBPACK_IMPORTED_MODULE_1__.RequestError(response.data.errors[0].message, 500, {
|
||||||
request: options,
|
request: options,
|
||||||
response
|
response
|
||||||
});
|
});
|
||||||
@@ -36505,8 +36544,9 @@ throttling.triggersNotification = triggersNotification;
|
|||||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
__nccwpck_require__.r(__webpack_exports__);
|
||||||
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
|
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
|
||||||
/* harmony export */ G: () => (/* binding */ RequestError)
|
/* harmony export */ RequestError: () => (/* binding */ RequestError)
|
||||||
/* harmony export */ });
|
/* harmony export */ });
|
||||||
class RequestError extends Error {
|
class RequestError extends Error {
|
||||||
name;
|
name;
|
||||||
|
|||||||
Generated
+14
@@ -16,6 +16,7 @@
|
|||||||
"@octokit/plugin-rest-endpoint-methods": "^13.5.0",
|
"@octokit/plugin-rest-endpoint-methods": "^13.5.0",
|
||||||
"@octokit/plugin-retry": "^7.2.1",
|
"@octokit/plugin-retry": "^7.2.1",
|
||||||
"@octokit/plugin-throttling": "^9.6.1",
|
"@octokit/plugin-throttling": "^9.6.1",
|
||||||
|
"@octokit/request-error": "^6.1.8",
|
||||||
"node-fetch-native": "^1.6.7",
|
"node-fetch-native": "^1.6.7",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.24.7",
|
||||||
@@ -718,6 +720,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -741,6 +744,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -1635,6 +1639,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz",
|
||||||
"integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==",
|
"integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^5.0.0",
|
"@octokit/auth-token": "^5.0.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
@@ -2883,6 +2888,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3358,6 +3364,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001640",
|
"caniuse-lite": "^1.0.30001640",
|
||||||
"electron-to-chromium": "^1.4.820",
|
"electron-to-chromium": "^1.4.820",
|
||||||
@@ -4166,6 +4173,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
|
||||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -4412,6 +4420,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5990,6 +5999,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
||||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.7.0",
|
"@jest/core": "^29.7.0",
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
@@ -6864,6 +6874,7 @@
|
|||||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.2.1",
|
"cssstyle": "^4.2.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -7702,6 +7713,7 @@
|
|||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -8568,6 +8580,7 @@
|
|||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8904,6 +8917,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@octokit/plugin-rest-endpoint-methods": "^13.5.0",
|
"@octokit/plugin-rest-endpoint-methods": "^13.5.0",
|
||||||
"@octokit/plugin-retry": "^7.2.1",
|
"@octokit/plugin-retry": "^7.2.1",
|
||||||
"@octokit/plugin-throttling": "^9.6.1",
|
"@octokit/plugin-throttling": "^9.6.1",
|
||||||
|
"@octokit/request-error": "^6.1.8",
|
||||||
"node-fetch-native": "^1.6.7",
|
"node-fetch-native": "^1.6.7",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
|
|||||||
+42
-20
@@ -1,4 +1,5 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
import {RequestError} from '@octokit/request-error'
|
||||||
import {Inputs} from './create-pull-request'
|
import {Inputs} from './create-pull-request'
|
||||||
import {Commit, GitCommandManager} from './git-command-manager'
|
import {Commit, GitCommandManager} from './git-command-manager'
|
||||||
import {
|
import {
|
||||||
@@ -209,32 +210,51 @@ export class GitHubHelper {
|
|||||||
headRepository
|
headRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// After creating a new PR, follow-up API calls can fail with a 422
|
||||||
|
// "Could not resolve to a node" error due to GitHub API eventual
|
||||||
|
// consistency. Wrap post-creation calls with targeted retry logic.
|
||||||
|
// See: https://github.com/peter-evans/create-pull-request/issues/4321
|
||||||
|
const isEventualConsistencyError = (e: unknown): boolean =>
|
||||||
|
e instanceof RequestError &&
|
||||||
|
e.status === 422 &&
|
||||||
|
e.message.includes('Could not resolve to a node')
|
||||||
|
const withRetryForNewPr = <T>(fn: () => Promise<T>): Promise<T> =>
|
||||||
|
pull.created
|
||||||
|
? utils.retryWithBackoff(fn, isEventualConsistencyError)
|
||||||
|
: fn()
|
||||||
|
|
||||||
// Apply milestone
|
// Apply milestone
|
||||||
if (inputs.milestone) {
|
if (inputs.milestone) {
|
||||||
core.info(`Applying milestone '${inputs.milestone}'`)
|
core.info(`Applying milestone '${inputs.milestone}'`)
|
||||||
await this.octokit.rest.issues.update({
|
await withRetryForNewPr(() =>
|
||||||
...this.parseRepository(baseRepository),
|
this.octokit.rest.issues.update({
|
||||||
issue_number: pull.number,
|
...this.parseRepository(baseRepository),
|
||||||
milestone: inputs.milestone
|
issue_number: pull.number,
|
||||||
})
|
milestone: inputs.milestone
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Apply labels
|
// Apply labels
|
||||||
if (inputs.labels.length > 0) {
|
if (inputs.labels.length > 0) {
|
||||||
core.info(`Applying labels '${inputs.labels}'`)
|
core.info(`Applying labels '${inputs.labels}'`)
|
||||||
await this.octokit.rest.issues.addLabels({
|
await withRetryForNewPr(() =>
|
||||||
...this.parseRepository(baseRepository),
|
this.octokit.rest.issues.addLabels({
|
||||||
issue_number: pull.number,
|
...this.parseRepository(baseRepository),
|
||||||
labels: inputs.labels
|
issue_number: pull.number,
|
||||||
})
|
labels: inputs.labels
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Apply assignees
|
// Apply assignees
|
||||||
if (inputs.assignees.length > 0) {
|
if (inputs.assignees.length > 0) {
|
||||||
core.info(`Applying assignees '${inputs.assignees}'`)
|
core.info(`Applying assignees '${inputs.assignees}'`)
|
||||||
await this.octokit.rest.issues.addAssignees({
|
await withRetryForNewPr(() =>
|
||||||
...this.parseRepository(baseRepository),
|
this.octokit.rest.issues.addAssignees({
|
||||||
issue_number: pull.number,
|
...this.parseRepository(baseRepository),
|
||||||
assignees: inputs.assignees
|
issue_number: pull.number,
|
||||||
})
|
assignees: inputs.assignees
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request reviewers and team reviewers
|
// Request reviewers and team reviewers
|
||||||
@@ -250,11 +270,13 @@ export class GitHubHelper {
|
|||||||
}
|
}
|
||||||
if (Object.keys(requestReviewersParams).length > 0) {
|
if (Object.keys(requestReviewersParams).length > 0) {
|
||||||
try {
|
try {
|
||||||
await this.octokit.rest.pulls.requestReviewers({
|
await withRetryForNewPr(() =>
|
||||||
...this.parseRepository(baseRepository),
|
this.octokit.rest.pulls.requestReviewers({
|
||||||
pull_number: pull.number,
|
...this.parseRepository(baseRepository),
|
||||||
...requestReviewersParams
|
pull_number: pull.number,
|
||||||
})
|
...requestReviewersParams
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
|
||||||
core.error(
|
core.error(
|
||||||
|
|||||||
@@ -140,3 +140,27 @@ export const isSelfHosted = (): boolean =>
|
|||||||
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
|
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
|
||||||
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
|
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
|
||||||
process.env['AGENT_ISSELFHOSTED'] === undefined)
|
process.env['AGENT_ISSELFHOSTED'] === undefined)
|
||||||
|
|
||||||
|
export async function retryWithBackoff<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
shouldRetry: (error: unknown) => boolean,
|
||||||
|
maxRetries = 2,
|
||||||
|
delayMs = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (e) {
|
||||||
|
if (attempt < maxRetries && shouldRetry(e)) {
|
||||||
|
const delay = delayMs * Math.pow(2, attempt)
|
||||||
|
core.info(
|
||||||
|
`Request failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms...`
|
||||||
|
)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unexpected: retry loop exited without return or throw')
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user