Add codeowner workflow check #5
Workflow file for this run
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
name: Code Owners Approval Check | |
on: | |
pull_request: | |
branches: | |
- master | |
types: [opened, synchronize, reopened, ready_for_review] | |
pull_request_review: | |
types: [submitted, dismissed] | |
permissions: {} | |
jobs: | |
check-code-owners-approval: | |
runs-on: ubuntu-latest | |
if: github.base_ref == 'master' | |
permissions: | |
pull-requests: write | |
contents: read | |
steps: | |
- name: Check Code Owners Approval | |
id: check_approvals | |
uses: actions/github-script@v7 | |
with: | |
github-token: ${{secrets.GITHUB_TOKEN}} | |
script: | | |
const { owner, repo, number } = context.issue; | |
// Get pull request details and files | |
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: number }); | |
const { data: files } = await github.rest.pulls.listFiles({ owner, repo, pull_number: number }); | |
// Get CODEOWNERS file content | |
let codeowners; | |
try { | |
const { data } = await github.rest.repos.getContent({ | |
owner, | |
repo, | |
path: '.github/CODEOWNERS', | |
}); | |
codeowners = Buffer.from(data.content, 'base64').toString('utf8'); | |
} catch (error) { | |
console.log('CODEOWNERS file not found in .github directory. Skipping check.'); | |
return; | |
} | |
// Parse CODEOWNERS file | |
const codeownersRules = codeowners.split('\n') | |
.filter(line => line.trim() && !line.startsWith('#')) | |
.map(line => { | |
const [pattern, ...owners] = line.split(/\s+/); | |
return { pattern, owners: owners.map(o => o.replace('@', '')) }; | |
}); | |
// Function to check if a file matches a pattern | |
const matchesPattern = (file, pattern) => { | |
const regexPattern = pattern | |
.replace(/\*/g, '.*') | |
.replace(/\?/g, '.') | |
.replace(/\//g, '\\/'); | |
return new RegExp(`^${regexPattern}$`).test(file); | |
}; | |
// Get relevant code owners for the changed files | |
const relevantOwners = new Set(); | |
files.forEach(file => { | |
codeownersRules.forEach(rule => { | |
if (matchesPattern(file.filename, rule.pattern)) { | |
rule.owners.forEach(owner => relevantOwners.add(owner)); | |
} | |
}); | |
}); | |
if (relevantOwners.size === 0) { | |
console.log('No relevant code owners found for the changed files. Skipping check.'); | |
return; | |
} | |
// Get reviews | |
const { data: reviews } = await github.rest.pulls.listReviews({ owner, repo, pull_number: number }); | |
const approvals = new Set( | |
reviews | |
.filter(review => review.state === 'APPROVED') | |
.map(review => review.user.login) | |
); | |
const codeOwnerStatus = Array.from(relevantOwners).map(owner => ({ | |
owner, | |
approved: approvals.has(owner) | |
})); | |
const missingApprovals = codeOwnerStatus.filter(status => !status.approved); | |
if (missingApprovals.length > 0) { | |
core.setFailed(`Missing approvals from code owners: ${missingApprovals.map(status => status.owner).join(', ')}`); | |
} else { | |
console.log('All relevant code owners have approved the pull request.'); | |
} | |
core.setOutput('codeOwnerStatus', JSON.stringify(codeOwnerStatus)); | |
- name: Update PR status | |
if: always() | |
uses: actions/github-script@v7 | |
with: | |
github-token: ${{secrets.GITHUB_TOKEN}} | |
script: | | |
const { owner, repo, number } = context.issue; | |
const codeOwnerStatus = JSON.parse(process.env.CODE_OWNER_STATUS); | |
const statusList = codeOwnerStatus.map(status => { | |
const emoji = status.approved ? '✅' : '❌'; | |
return `${emoji} ${status.owner}`; | |
}).join('\n'); | |
const commentBody = `## Code Owners Approval Status | |
${statusList} | |
${codeOwnerStatus.every(status => status.approved) | |
? '✅ All required code owners have approved this pull request.' | |
: '❌ This pull request is still missing approvals from one or more code owners.'}`; | |
// Find existing bot comment | |
const { data: comments } = await github.rest.issues.listComments({ | |
owner, | |
repo, | |
issue_number: number, | |
}); | |
const botComment = comments.find(comment => | |
comment.user.type === 'Bot' && comment.body.includes('Code Owners Approval Status') | |
); | |
if (botComment) { | |
// Update existing comment | |
await github.rest.issues.updateComment({ | |
owner, | |
repo, | |
comment_id: botComment.id, | |
body: commentBody, | |
}); | |
} else { | |
// Create new comment | |
await github.rest.issues.createComment({ | |
owner, | |
repo, | |
issue_number: number, | |
body: commentBody, | |
}); | |
} | |
env: | |
CODE_OWNER_STATUS: ${{ steps.check_approvals.outputs.codeOwnerStatus }} |