-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
workflow: Add check-main-status workflow
The workflow checks the result of check runs on the latest commit on the main branch. When checks on main are failing the check will fail. If main is pending the check will retry every 5 mintues for an hour. This prevents introducing more potenisal issues when main is already broken. To skip the check when making a fix for main, add the fix-ci label to the PR. Signed-off-by: Gregers Gram Rygg <[email protected]>
- Loading branch information
1 parent
8805bfb
commit a04b2ad
Showing
6 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: Check Main Status Tests | ||
|
||
on: | ||
pull_request: | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Set up Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: 22 | ||
|
||
- name: Install jest | ||
run: npm install [email protected] | ||
|
||
- name: Run tests | ||
run: npx jest ./.github/workflows/lib/checkMain.test.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
name: Check Main Branch Status | ||
on: | ||
pull_request: | ||
merge_group: | ||
workflow_dispatch: | ||
inputs: | ||
pr_number: | ||
description: "Pull Request number" | ||
required: true | ||
|
||
jobs: | ||
check-main-status: | ||
runs-on: ubuntu-24.04 | ||
env: | ||
PR_NUMBER: ${{ github.event.number || github.event.inputs.pr_number }} | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
sparse-checkout: | | ||
.github/workflows/lib/ | ||
- run: pwd | ||
- run: ls -lap | ||
- run: ls -lap .github/workflows/lib/ | ||
- uses: actions/github-script@v7 | ||
name: Check if PR has 'fix-ci' label | ||
id: has_fix_ci_label | ||
with: | ||
retries: 3 | ||
script: | | ||
const checkMain = require('./.github/workflows/lib/checkMain.js'); | ||
const result = await checkMain.hasFixCiLabel(github, context.repo.owner, context.repo.repo, process.env.PR_NUMBER); | ||
console.log(`Has 'fix-ci' label: ${result}`); | ||
return result; | ||
- uses: actions/github-script@v7 | ||
name: Ensure Main Branch Check Runs Are Successful | ||
if: ${{ steps.has_fix_ci_label.outputs.result == 'false' }} | ||
with: | ||
retries: 3 | ||
script: | | ||
const checkMain = require('./.github/workflows/lib/checkMain.js'); | ||
function delay(ms) { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} | ||
try { | ||
await checkMain.ensureChecksRunsSuccessful(delay, github, context.repo.owner, context.repo.repo); | ||
} catch (error) { | ||
core.setFailed(error.message); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ name: DFU image compatibility check | |
|
||
on: | ||
workflow_call: | ||
merge_group: | ||
push: | ||
branches: | ||
- main | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// jest.config.js | ||
module.exports = { | ||
transform: { | ||
"^.+\\.[t|j]sx?$": "babel-jest", | ||
}, | ||
transformIgnorePatterns: [ | ||
"/node_modules/(?!(@octokit)/)" | ||
], | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
module.exports = { | ||
async hasFixCiLabel(octokit, owner, repo, prNumber) { | ||
const labels = await octokit.rest.issues.listLabelsOnIssue({ | ||
owner, | ||
repo, | ||
issue_number: prNumber, | ||
}); | ||
|
||
return labels.data.map(label => label.name).includes('fix-ci'); | ||
}, | ||
|
||
async ensureChecksRunsSuccessful(delay, octokit, owner, repo) { | ||
let retryPending = 12; // Retry for up to 1 hour (12 retries with 5 minutes interval) | ||
|
||
while (retryPending > 0) { | ||
const mainRef = await octokit.rest.git.getRef({ | ||
owner, | ||
repo, | ||
ref: 'heads/main', | ||
}); | ||
|
||
const mainSha = mainRef.data.object.sha; | ||
console.log(`Main branch SHA: ${mainSha}`); | ||
|
||
let checkRuns = await octokit.rest.checks.listForRef({ | ||
owner, | ||
repo, | ||
ref: mainSha, | ||
}); | ||
|
||
console.log(`Check runs (${checkRuns.data.total_count}):`); | ||
for (let run of checkRuns.data.check_runs) { | ||
console.log(`- ${run.name} ${run.html_url}`); | ||
console.log(`\tstatus: ${run.status}`); | ||
console.log(`\tconclusion: ${run.conclusion}`); | ||
} | ||
|
||
const failedChecks = checkRuns.data.check_runs.filter(run => run.conclusion !== null && run.conclusion !== 'success'); | ||
const pendingChecks = checkRuns.data.check_runs.filter(run => run.status !== 'completed'); | ||
|
||
if (failedChecks.length > 0) { | ||
throw new Error('The main branch has failing checks.'); | ||
} else if (pendingChecks.length === 0) { | ||
console.log('Main branch is healthy.'); | ||
break; | ||
} else { | ||
console.log('Checks are still pending.'); | ||
|
||
retryPending--; | ||
if (retryPending === 0) { | ||
throw new Error('Timeout waiting for pending checks on main branch.'); | ||
} | ||
|
||
console.log('Waiting for 5 minutes before retrying...'); | ||
await delay(5 * 60 * 1000); // Wait for 5 minutes | ||
} | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
const { hasFixCiLabel, ensureChecksRunsSuccessful } = require('./checkMain'); | ||
|
||
// define class Octokit without depending on the original Octokit package. only the methods used in the test are defined. | ||
class Octokit { | ||
constructor() { | ||
this.rest = { | ||
issues: { | ||
listLabelsOnIssue: jest.fn() | ||
}, | ||
checks: { | ||
listForRef: jest.fn() | ||
}, | ||
git: { | ||
getRef: jest.fn() | ||
} | ||
}; | ||
} | ||
} | ||
|
||
function delay(ms) { | ||
return new Promise(resolve => resolve()); | ||
} | ||
|
||
describe('check-main-status', () => { | ||
let octokit; | ||
const owner = 'test-owner'; | ||
const repo = 'test-repo'; | ||
const prNumber = 1; | ||
const mainSha = 'test-sha'; | ||
|
||
beforeEach(() => { | ||
octokit = new Octokit(); | ||
octokit.rest.git.getRef.mockResolvedValue({ | ||
data: { object: { sha: mainSha } } | ||
}); | ||
}); | ||
|
||
describe('hasFixCiLabel', () => { | ||
it('should return true if the PR has the fix-ci label', async () => { | ||
octokit.rest.issues.listLabelsOnIssue.mockResolvedValue({ | ||
data: [{ name: 'fix-ci' }] | ||
}); | ||
|
||
const result = await hasFixCiLabel(octokit, owner, repo, prNumber); | ||
expect(result).toBe(true); | ||
}); | ||
|
||
it('should return false if the PR does not have the fix-ci label', async () => { | ||
octokit.rest.issues.listLabelsOnIssue.mockResolvedValue({ | ||
data: [{ name: 'other-label' }] | ||
}); | ||
|
||
const result = await hasFixCiLabel(octokit, owner, repo, prNumber); | ||
expect(result).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('ensureChecksRunsSuccessful', () => { | ||
beforeEach(() => { | ||
//octokit = new Octokit(); | ||
octokit.rest.git.getRef.mockResolvedValue({ | ||
data: { object: { sha: mainSha } } | ||
}); | ||
}); | ||
|
||
it('should not throw an error if all checks are successful', async () => { | ||
octokit.rest.checks.listForRef.mockResolvedValue({ | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'completed', conclusion: 'success' }] | ||
} | ||
}); | ||
|
||
await expect(ensureChecksRunsSuccessful(delay, octokit, owner, repo, mainSha)).resolves.not.toThrow(); | ||
}); | ||
|
||
it('should throw an error if there are failing checks', async () => { | ||
octokit.rest.checks.listForRef.mockResolvedValue({ | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'completed', conclusion: 'failure' }] | ||
} | ||
}); | ||
|
||
await expect(ensureChecksRunsSuccessful(delay, octokit, owner, repo, mainSha)).rejects.toThrow('The main branch has failing checks.'); | ||
}); | ||
|
||
it('should throw an error if checks are still pending after 12 retries', async () => { | ||
octokit.rest.checks.listForRef.mockResolvedValue({ | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'in_progress', conclusion: null }] | ||
} | ||
}); | ||
|
||
await expect(ensureChecksRunsSuccessful(delay, octokit, owner, repo, mainSha)).rejects.toThrow('Timeout waiting for pending checks on main branch.'); | ||
expect(octokit.rest.checks.listForRef).toHaveBeenCalledTimes(12); | ||
}); | ||
|
||
it('should not throw an error if checks are successfull after pending 1 time', async () => { | ||
const pendingResponse = { | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'in_progress', conclusion: null }] | ||
} | ||
}; | ||
const successResponse = { | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'completed', conclusion: 'success' }] | ||
} | ||
}; | ||
|
||
octokit.rest.checks.listForRef | ||
.mockResolvedValueOnce(pendingResponse) | ||
.mockResolvedValueOnce(successResponse); | ||
|
||
await expect(ensureChecksRunsSuccessful(delay, octokit, owner, repo, mainSha)).resolves.not.toThrow(); | ||
expect(octokit.rest.checks.listForRef).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('should throw an error if there are failing checks after pending 1 time', async () => { | ||
const pendingResponse = { | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'in_progress', conclusion: null }] | ||
} | ||
}; | ||
const failureResponse = { | ||
data: { | ||
total_count: 1, | ||
check_runs: [{ name: 'test-check', status: 'completed', conclusion: 'failure' }] | ||
} | ||
}; | ||
|
||
octokit.rest.checks.listForRef | ||
.mockResolvedValueOnce(pendingResponse) | ||
.mockResolvedValueOnce(failureResponse); | ||
|
||
await expect(ensureChecksRunsSuccessful(delay, octokit, owner, repo, mainSha)).rejects.toThrow('The main branch has failing checks.'); | ||
expect(octokit.rest.checks.listForRef).toHaveBeenCalledTimes(2); | ||
}); | ||
}); | ||
}); |