Skip to content

Commit 3959fd1

Browse files
committed
bugfix-314-error-merging-release-branch-after-successful-deployment: Enhance MergeRepository and DeployedActionUseCase documentation to clarify PR-specific check run handling and deployment flow. Updated comments and type definitions to reflect new behavior, ensuring better understanding of the merging process and check wait logic.
1 parent e752467 commit 3959fd1

6 files changed

Lines changed: 128 additions & 18 deletions

File tree

build/cli/index.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51275,8 +51275,14 @@ const github = __importStar(__nccwpck_require__(5438));
5127551275
const logger_1 = __nccwpck_require__(8836);
5127651276
const result_1 = __nccwpck_require__(7305);
5127751277
/**
51278-
* Repository for merging branches (via PR or direct merge).
51279-
* Isolated to allow unit tests with mocked Octokit.
51278+
* Repository for merging branches: creates a PR, waits for that PR's check runs (or status checks),
51279+
* then merges the PR; on failure, falls back to a direct Git merge.
51280+
*
51281+
* Check runs are filtered by PR (pull_requests) so we only wait for the current PR's checks,
51282+
* not those of another PR sharing the same head (e.g. release→main vs release→develop).
51283+
* If the PR has no check runs after a short wait, we proceed to merge (branch may have no required checks).
51284+
*
51285+
* @see docs/single-actions/deploy-label-and-merge.mdx for the deploy flow and check-wait behaviour.
5128051286
*/
5128151287
class MergeRepository {
5128251288
constructor() {
@@ -51318,33 +51324,41 @@ This PR merges **${head}** into **${base}**.
5131851324
'\n\nThis PR was automatically created by [`copilot`](https://github.com/vypdev/copilot).',
5131951325
});
5132051326
const iteration = 10;
51327+
/** Give workflows a short window to register check runs for this PR; after this, we allow merge with no check runs (e.g. branch has no required checks). */
51328+
const maxWaitForPrChecksAttempts = 3;
5132151329
if (timeout > iteration) {
5132251330
// Wait for checks to complete - can use regular token for reading checks
5132351331
let checksCompleted = false;
5132451332
let attempts = 0;
51333+
let waitForPrChecksAttempts = 0;
5132551334
const maxAttempts = timeout > iteration ? Math.floor(timeout / iteration) : iteration;
5132651335
while (!checksCompleted && attempts < maxAttempts) {
5132751336
const { data: checkRuns } = await octokit.rest.checks.listForRef({
5132851337
owner: owner,
5132951338
repo: repository,
5133051339
ref: head,
5133151340
});
51341+
// Only consider check runs that are for this PR. When the same branch is used in
51342+
// multiple PRs (e.g. release→master and release→develop), listForRef returns runs
51343+
// for all PRs; we must wait for runs tied to the current PR or we may see completed
51344+
// runs from the other PR and merge before this PR's checks have run.
51345+
const runsForThisPr = checkRuns.check_runs.filter(run => run.pull_requests?.some(pr => pr.number === pullRequest.number));
5133251346
// Get commit status checks for the PR head commit
5133351347
const { data: commitStatus } = await octokit.rest.repos.getCombinedStatusForRef({
5133451348
owner: owner,
5133551349
repo: repository,
5133651350
ref: head,
5133751351
});
5133851352
(0, logger_1.logDebugInfo)(`Combined status state: ${commitStatus.state}`);
51339-
(0, logger_1.logDebugInfo)(`Number of check runs: ${checkRuns.check_runs.length}`);
51340-
// If there are check runs, prioritize those over status checks
51341-
if (checkRuns.check_runs.length > 0) {
51342-
const pendingCheckRuns = checkRuns.check_runs.filter(check => check.status !== 'completed');
51353+
(0, logger_1.logDebugInfo)(`Number of check runs for this PR: ${runsForThisPr.length} (total on ref: ${checkRuns.check_runs.length})`);
51354+
// If there are check runs for this PR, wait for them to complete
51355+
if (runsForThisPr.length > 0) {
51356+
const pendingCheckRuns = runsForThisPr.filter(check => check.status !== 'completed');
5134351357
if (pendingCheckRuns.length === 0) {
5134451358
checksCompleted = true;
5134551359
(0, logger_1.logDebugInfo)('All check runs have completed.');
5134651360
// Verify if all checks passed
51347-
const failedChecks = checkRuns.check_runs.filter(check => check.conclusion === 'failure');
51361+
const failedChecks = runsForThisPr.filter(check => check.conclusion === 'failure');
5134851362
if (failedChecks.length > 0) {
5134951363
throw new Error(`Checks failed: ${failedChecks.map(check => check.name).join(', ')}`);
5135051364
}
@@ -51359,6 +51373,21 @@ This PR merges **${head}** into **${base}**.
5135951373
continue;
5136051374
}
5136151375
}
51376+
else if (checkRuns.check_runs.length > 0 && runsForThisPr.length === 0) {
51377+
// There are runs on the ref but none for this PR. Either workflows for this PR
51378+
// haven't registered yet, or this PR/base has no required checks.
51379+
waitForPrChecksAttempts++;
51380+
if (waitForPrChecksAttempts >= maxWaitForPrChecksAttempts) {
51381+
checksCompleted = true;
51382+
(0, logger_1.logDebugInfo)(`No check runs for this PR after ${maxWaitForPrChecksAttempts} polls; proceeding to merge (branch may have no required checks).`);
51383+
}
51384+
else {
51385+
(0, logger_1.logDebugInfo)('Check runs exist on ref but none for this PR yet; waiting for workflows to register.');
51386+
await new Promise(resolve => setTimeout(resolve, iteration * 1000));
51387+
attempts++;
51388+
}
51389+
continue;
51390+
}
5136251391
else {
5136351392
// Fall back to status checks if no check runs exist
5136451393
const pendingChecks = commitStatus.statuses.filter(status => {
@@ -53911,6 +53940,16 @@ const branch_repository_1 = __nccwpck_require__(7701);
5391153940
const issue_repository_1 = __nccwpck_require__(57);
5391253941
const logger_1 = __nccwpck_require__(8836);
5391353942
const task_emoji_1 = __nccwpck_require__(9785);
53943+
/**
53944+
* Single action run after a successful deployment (triggered with the "deployed" action and an issue number).
53945+
*
53946+
* Requires the issue to have the "deploy" label and not already have the "deployed" label. Then:
53947+
* 1. Replaces the "deploy" label with "deployed".
53948+
* 2. If a release or hotfix branch is configured: merges it into default and develop (each via PR, waiting for that PR's checks).
53949+
* 3. Closes the issue only when all merges succeed.
53950+
*
53951+
* @see docs/single-actions/deploy-label-and-merge.mdx for the full flow and how merge/check waiting works.
53952+
*/
5391453953
class DeployedActionUseCase {
5391553954
constructor() {
5391653955
this.taskId = 'DeployedActionUseCase';

build/cli/src/data/repository/merge_repository.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Result } from '../model/result';
22
/**
3-
* Repository for merging branches (via PR or direct merge).
4-
* Isolated to allow unit tests with mocked Octokit.
3+
* Repository for merging branches: creates a PR, waits for that PR's check runs (or status checks),
4+
* then merges the PR; on failure, falls back to a direct Git merge.
5+
*
6+
* Check runs are filtered by PR (pull_requests) so we only wait for the current PR's checks,
7+
* not those of another PR sharing the same head (e.g. release→main vs release→develop).
8+
* If the PR has no check runs after a short wait, we proceed to merge (branch may have no required checks).
9+
*
10+
* @see docs/single-actions/deploy-label-and-merge.mdx for the deploy flow and check-wait behaviour.
511
*/
612
export declare class MergeRepository {
713
mergeBranch: (owner: string, repository: string, head: string, base: string, timeout: number, token: string) => Promise<Result[]>;

build/cli/src/usecase/actions/deployed_action_use_case.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { Execution } from "../../data/model/execution";
22
import { Result } from "../../data/model/result";
33
import { ParamUseCase } from "../base/param_usecase";
4+
/**
5+
* Single action run after a successful deployment (triggered with the "deployed" action and an issue number).
6+
*
7+
* Requires the issue to have the "deploy" label and not already have the "deployed" label. Then:
8+
* 1. Replaces the "deploy" label with "deployed".
9+
* 2. If a release or hotfix branch is configured: merges it into default and develop (each via PR, waiting for that PR's checks).
10+
* 3. Closes the issue only when all merges succeed.
11+
*
12+
* @see docs/single-actions/deploy-label-and-merge.mdx for the full flow and how merge/check waiting works.
13+
*/
414
export declare class DeployedActionUseCase implements ParamUseCase<Execution, Result[]> {
515
taskId: string;
616
private issueRepository;

build/github_action/index.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46343,8 +46343,14 @@ const github = __importStar(__nccwpck_require__(5438));
4634346343
const logger_1 = __nccwpck_require__(8836);
4634446344
const result_1 = __nccwpck_require__(7305);
4634546345
/**
46346-
* Repository for merging branches (via PR or direct merge).
46347-
* Isolated to allow unit tests with mocked Octokit.
46346+
* Repository for merging branches: creates a PR, waits for that PR's check runs (or status checks),
46347+
* then merges the PR; on failure, falls back to a direct Git merge.
46348+
*
46349+
* Check runs are filtered by PR (pull_requests) so we only wait for the current PR's checks,
46350+
* not those of another PR sharing the same head (e.g. release→main vs release→develop).
46351+
* If the PR has no check runs after a short wait, we proceed to merge (branch may have no required checks).
46352+
*
46353+
* @see docs/single-actions/deploy-label-and-merge.mdx for the deploy flow and check-wait behaviour.
4634846354
*/
4634946355
class MergeRepository {
4635046356
constructor() {
@@ -46386,33 +46392,41 @@ This PR merges **${head}** into **${base}**.
4638646392
'\n\nThis PR was automatically created by [`copilot`](https://github.com/vypdev/copilot).',
4638746393
});
4638846394
const iteration = 10;
46395+
/** Give workflows a short window to register check runs for this PR; after this, we allow merge with no check runs (e.g. branch has no required checks). */
46396+
const maxWaitForPrChecksAttempts = 3;
4638946397
if (timeout > iteration) {
4639046398
// Wait for checks to complete - can use regular token for reading checks
4639146399
let checksCompleted = false;
4639246400
let attempts = 0;
46401+
let waitForPrChecksAttempts = 0;
4639346402
const maxAttempts = timeout > iteration ? Math.floor(timeout / iteration) : iteration;
4639446403
while (!checksCompleted && attempts < maxAttempts) {
4639546404
const { data: checkRuns } = await octokit.rest.checks.listForRef({
4639646405
owner: owner,
4639746406
repo: repository,
4639846407
ref: head,
4639946408
});
46409+
// Only consider check runs that are for this PR. When the same branch is used in
46410+
// multiple PRs (e.g. release→master and release→develop), listForRef returns runs
46411+
// for all PRs; we must wait for runs tied to the current PR or we may see completed
46412+
// runs from the other PR and merge before this PR's checks have run.
46413+
const runsForThisPr = checkRuns.check_runs.filter(run => run.pull_requests?.some(pr => pr.number === pullRequest.number));
4640046414
// Get commit status checks for the PR head commit
4640146415
const { data: commitStatus } = await octokit.rest.repos.getCombinedStatusForRef({
4640246416
owner: owner,
4640346417
repo: repository,
4640446418
ref: head,
4640546419
});
4640646420
(0, logger_1.logDebugInfo)(`Combined status state: ${commitStatus.state}`);
46407-
(0, logger_1.logDebugInfo)(`Number of check runs: ${checkRuns.check_runs.length}`);
46408-
// If there are check runs, prioritize those over status checks
46409-
if (checkRuns.check_runs.length > 0) {
46410-
const pendingCheckRuns = checkRuns.check_runs.filter(check => check.status !== 'completed');
46421+
(0, logger_1.logDebugInfo)(`Number of check runs for this PR: ${runsForThisPr.length} (total on ref: ${checkRuns.check_runs.length})`);
46422+
// If there are check runs for this PR, wait for them to complete
46423+
if (runsForThisPr.length > 0) {
46424+
const pendingCheckRuns = runsForThisPr.filter(check => check.status !== 'completed');
4641146425
if (pendingCheckRuns.length === 0) {
4641246426
checksCompleted = true;
4641346427
(0, logger_1.logDebugInfo)('All check runs have completed.');
4641446428
// Verify if all checks passed
46415-
const failedChecks = checkRuns.check_runs.filter(check => check.conclusion === 'failure');
46429+
const failedChecks = runsForThisPr.filter(check => check.conclusion === 'failure');
4641646430
if (failedChecks.length > 0) {
4641746431
throw new Error(`Checks failed: ${failedChecks.map(check => check.name).join(', ')}`);
4641846432
}
@@ -46427,6 +46441,21 @@ This PR merges **${head}** into **${base}**.
4642746441
continue;
4642846442
}
4642946443
}
46444+
else if (checkRuns.check_runs.length > 0 && runsForThisPr.length === 0) {
46445+
// There are runs on the ref but none for this PR. Either workflows for this PR
46446+
// haven't registered yet, or this PR/base has no required checks.
46447+
waitForPrChecksAttempts++;
46448+
if (waitForPrChecksAttempts >= maxWaitForPrChecksAttempts) {
46449+
checksCompleted = true;
46450+
(0, logger_1.logDebugInfo)(`No check runs for this PR after ${maxWaitForPrChecksAttempts} polls; proceeding to merge (branch may have no required checks).`);
46451+
}
46452+
else {
46453+
(0, logger_1.logDebugInfo)('Check runs exist on ref but none for this PR yet; waiting for workflows to register.');
46454+
await new Promise(resolve => setTimeout(resolve, iteration * 1000));
46455+
attempts++;
46456+
}
46457+
continue;
46458+
}
4643046459
else {
4643146460
// Fall back to status checks if no check runs exist
4643246461
const pendingChecks = commitStatus.statuses.filter(status => {
@@ -48979,6 +49008,16 @@ const branch_repository_1 = __nccwpck_require__(7701);
4897949008
const issue_repository_1 = __nccwpck_require__(57);
4898049009
const logger_1 = __nccwpck_require__(8836);
4898149010
const task_emoji_1 = __nccwpck_require__(9785);
49011+
/**
49012+
* Single action run after a successful deployment (triggered with the "deployed" action and an issue number).
49013+
*
49014+
* Requires the issue to have the "deploy" label and not already have the "deployed" label. Then:
49015+
* 1. Replaces the "deploy" label with "deployed".
49016+
* 2. If a release or hotfix branch is configured: merges it into default and develop (each via PR, waiting for that PR's checks).
49017+
* 3. Closes the issue only when all merges succeed.
49018+
*
49019+
* @see docs/single-actions/deploy-label-and-merge.mdx for the full flow and how merge/check waiting works.
49020+
*/
4898249021
class DeployedActionUseCase {
4898349022
constructor() {
4898449023
this.taskId = 'DeployedActionUseCase';

build/github_action/src/data/repository/merge_repository.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Result } from '../model/result';
22
/**
3-
* Repository for merging branches (via PR or direct merge).
4-
* Isolated to allow unit tests with mocked Octokit.
3+
* Repository for merging branches: creates a PR, waits for that PR's check runs (or status checks),
4+
* then merges the PR; on failure, falls back to a direct Git merge.
5+
*
6+
* Check runs are filtered by PR (pull_requests) so we only wait for the current PR's checks,
7+
* not those of another PR sharing the same head (e.g. release→main vs release→develop).
8+
* If the PR has no check runs after a short wait, we proceed to merge (branch may have no required checks).
9+
*
10+
* @see docs/single-actions/deploy-label-and-merge.mdx for the deploy flow and check-wait behaviour.
511
*/
612
export declare class MergeRepository {
713
mergeBranch: (owner: string, repository: string, head: string, base: string, timeout: number, token: string) => Promise<Result[]>;

build/github_action/src/usecase/actions/deployed_action_use_case.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { Execution } from "../../data/model/execution";
22
import { Result } from "../../data/model/result";
33
import { ParamUseCase } from "../base/param_usecase";
4+
/**
5+
* Single action run after a successful deployment (triggered with the "deployed" action and an issue number).
6+
*
7+
* Requires the issue to have the "deploy" label and not already have the "deployed" label. Then:
8+
* 1. Replaces the "deploy" label with "deployed".
9+
* 2. If a release or hotfix branch is configured: merges it into default and develop (each via PR, waiting for that PR's checks).
10+
* 3. Closes the issue only when all merges succeed.
11+
*
12+
* @see docs/single-actions/deploy-label-and-merge.mdx for the full flow and how merge/check waiting works.
13+
*/
414
export declare class DeployedActionUseCase implements ParamUseCase<Execution, Result[]> {
515
taskId: string;
616
private issueRepository;

0 commit comments

Comments
 (0)