Compare commits

..

10 Commits

Author SHA1 Message Date
Peter Evans 5f6978faf0 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
2026-04-10 17:23:55 +01:00
dependabot[bot] d32e88dac7 build(deps-dev): bump the npm group with 3 updates (#4349)
Bumps the npm group with 3 updates: [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom), [ts-jest](https://github.com/kulshekhar/ts-jest) and [undici](https://github.com/nodejs/undici).


Updates `jest-environment-jsdom` from 30.2.0 to 30.3.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.3.0/packages/jest-environment-jsdom)

Updates `ts-jest` from 29.4.6 to 29.4.9
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.6...v29.4.9)

Updates `undici` from 6.24.0 to 6.24.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.24.0...v6.24.1)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-version: 30.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: ts-jest
  dependency-version: 29.4.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 15:23:34 +00:00
dependabot[bot] 8170bccad1 build(deps-dev): bump handlebars from 4.7.8 to 4.7.9 (#4344)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 20:17:57 +00:00
dependabot[bot] 00418193b4 build(deps): bump picomatch (#4339)
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `picomatch` from 4.0.2 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 21:50:05 +00:00
dependabot[bot] b993918c85 build(deps-dev): bump flatted from 3.3.1 to 3.4.2 (#4334)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.1 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.1...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 00:22:17 +00:00
dependabot[bot] 36d7c8468b build(deps-dev): bump undici from 6.23.0 to 6.24.0 (#4328)
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 06:18:43 +00:00
dependabot[bot] a45d1fb447 build(deps): bump @tootallnate/once and jest-environment-jsdom (#4323)
Removes [@tootallnate/once](https://github.com/TooTallNate/once). It's no longer used after updating ancestor dependency [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom). These dependencies need to be updated together.


Removes `@tootallnate/once`

Updates `jest-environment-jsdom` from 29.7.0 to 30.2.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.2.0/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: "@tootallnate/once"
  dependency-version: 
  dependency-type: indirect
- dependency-name: jest-environment-jsdom
  dependency-version: 30.2.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 06:14:22 +00:00
dependabot[bot] 3499eb6183 build(deps): bump the github-actions group with 2 updates (#4316)
Bumps the github-actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/upload-artifact` from 6 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

Updates `actions/download-artifact` from 7 to 8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 12:34:50 +00:00
dependabot[bot] 3f3b473b8c build(deps): bump minimatch (#4311)
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 06:50:02 +00:00
dependabot[bot] 6699836a21 build(deps-dev): bump the npm group with 2 updates (#4305)
Bumps the npm group with 2 updates: [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) and [prettier](https://github.com/prettier/prettier).


Updates `eslint-plugin-prettier` from 5.5.4 to 5.5.5
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.4...v5.5.5)

Updates `prettier` from 3.7.4 to 3.8.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: prettier
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 12:35:41 +00:00
7 changed files with 1027 additions and 430 deletions
+5 -5
View File
@@ -29,11 +29,11 @@ jobs:
- run: npm run format-check
- run: npm run lint
- run: npm run test
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: action.yml
path: action.yml
@@ -50,12 +50,12 @@ jobs:
with:
ref: main
- if: matrix.target == 'built' || github.event_name == 'pull_request'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: dist
path: dist
- if: matrix.target == 'built' || github.event_name == 'pull_request'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: action.yml
path: .
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
name: dist
path: dist
+68
View File
@@ -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)
})
})
+50 -10
View File
@@ -1375,6 +1375,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.GitHubHelper = void 0;
const core = __importStar(__nccwpck_require__(7484));
const request_error_1 = __nccwpck_require__(1015);
const octokit_client_1 = __nccwpck_require__(3489);
const p_limit_1 = __importDefault(__nccwpck_require__(7989));
const utils = __importStar(__nccwpck_require__(9277));
@@ -1501,20 +1502,28 @@ class GitHubHelper {
return __awaiter(this, void 0, void 0, function* () {
// Create or update the pull request
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
if (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
if (inputs.labels.length > 0) {
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
if (inputs.assignees.length > 0) {
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
const requestReviewersParams = {};
@@ -1529,7 +1538,7 @@ class GitHubHelper {
}
if (Object.keys(requestReviewersParams).length > 0) {
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) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
@@ -1900,6 +1909,15 @@ var __importStar = (this && this.__importStar) || (function () {
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 }));
exports.isSelfHosted = void 0;
exports.getInputAsArray = getInputAsArray;
@@ -1913,6 +1931,7 @@ exports.parseDisplayNameEmail = parseDisplayNameEmail;
exports.fileExistsSync = fileExistsSync;
exports.readFile = readFile;
exports.getErrorMessage = getErrorMessage;
exports.retryWithBackoff = retryWithBackoff;
const core = __importStar(__nccwpck_require__(7484));
const fs = __importStar(__nccwpck_require__(9896));
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'] === undefined);
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
});
requestError.cause = error;
@@ -32857,21 +32896,21 @@ async function fetchWrapper(requestOptions) {
if (status < 400) {
return octokitResponse;
}
throw new dist_src/* RequestError */.G(fetchResponse.statusText, status, {
throw new dist_src.RequestError(fetchResponse.statusText, status, {
response: octokitResponse,
request: requestOptions
});
}
if (status === 304) {
octokitResponse.data = await getResponseData(fetchResponse);
throw new dist_src/* RequestError */.G("Not modified", status, {
throw new dist_src.RequestError("Not modified", status, {
response: octokitResponse,
request: requestOptions
});
}
if (status >= 400) {
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,
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(
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,
response
});
@@ -36505,8 +36544,9 @@ throttling.triggersNotification = triggersNotification;
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
"use strict";
__nccwpck_require__.r(__webpack_exports__);
/* harmony export */ __nccwpck_require__.d(__webpack_exports__, {
/* harmony export */ G: () => (/* binding */ RequestError)
/* harmony export */ RequestError: () => (/* binding */ RequestError)
/* harmony export */ });
class RequestError extends Error {
name;
+832 -390
View File
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -39,6 +39,7 @@
"@octokit/plugin-rest-endpoint-methods": "^13.5.0",
"@octokit/plugin-retry": "^7.2.1",
"@octokit/plugin-throttling": "^9.6.1",
"@octokit/request-error": "^6.1.8",
"node-fetch-native": "^1.6.7",
"p-limit": "^6.2.0",
"uuid": "^9.0.1"
@@ -54,14 +55,14 @@
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.3.0",
"js-yaml": "^4.1.1",
"prettier": "^3.7.4",
"ts-jest": "^29.4.6",
"prettier": "^3.8.1",
"ts-jest": "^29.4.9",
"typescript": "^5.9.3",
"undici": "^6.23.0"
"undici": "^6.24.1"
}
}
+42 -20
View File
@@ -1,4 +1,5 @@
import * as core from '@actions/core'
import {RequestError} from '@octokit/request-error'
import {Inputs} from './create-pull-request'
import {Commit, GitCommandManager} from './git-command-manager'
import {
@@ -209,32 +210,51 @@ export class GitHubHelper {
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
if (inputs.milestone) {
core.info(`Applying milestone '${inputs.milestone}'`)
await this.octokit.rest.issues.update({
...this.parseRepository(baseRepository),
issue_number: pull.number,
milestone: inputs.milestone
})
await withRetryForNewPr(() =>
this.octokit.rest.issues.update({
...this.parseRepository(baseRepository),
issue_number: pull.number,
milestone: inputs.milestone
})
)
}
// Apply labels
if (inputs.labels.length > 0) {
core.info(`Applying labels '${inputs.labels}'`)
await this.octokit.rest.issues.addLabels({
...this.parseRepository(baseRepository),
issue_number: pull.number,
labels: inputs.labels
})
await withRetryForNewPr(() =>
this.octokit.rest.issues.addLabels({
...this.parseRepository(baseRepository),
issue_number: pull.number,
labels: inputs.labels
})
)
}
// Apply assignees
if (inputs.assignees.length > 0) {
core.info(`Applying assignees '${inputs.assignees}'`)
await this.octokit.rest.issues.addAssignees({
...this.parseRepository(baseRepository),
issue_number: pull.number,
assignees: inputs.assignees
})
await withRetryForNewPr(() =>
this.octokit.rest.issues.addAssignees({
...this.parseRepository(baseRepository),
issue_number: pull.number,
assignees: inputs.assignees
})
)
}
// Request reviewers and team reviewers
@@ -250,11 +270,13 @@ export class GitHubHelper {
}
if (Object.keys(requestReviewersParams).length > 0) {
try {
await this.octokit.rest.pulls.requestReviewers({
...this.parseRepository(baseRepository),
pull_number: pull.number,
...requestReviewersParams
})
await withRetryForNewPr(() =>
this.octokit.rest.pulls.requestReviewers({
...this.parseRepository(baseRepository),
pull_number: pull.number,
...requestReviewersParams
})
)
} catch (e) {
if (utils.getErrorMessage(e).includes(ERROR_PR_REVIEW_TOKEN_SCOPE)) {
core.error(
+24
View File
@@ -140,3 +140,27 @@ export const isSelfHosted = (): boolean =>
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
(process.env['AGENT_ISSELFHOSTED'] === '1' ||
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')
}