Skip to content

feat: LAA Risk Score Derivations #4087

feat: LAA Risk Score Derivations

feat: LAA Risk Score Derivations #4087

name: "PR Checks"
on:
pull_request:
types: [opened, edited, reopened, labeled, unlabeled, synchronize]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
permissions:
pull-requests: write
issues: write
jobs:
check-compliance:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Markdown Linting
id: markdown_lint
uses: DavidAnson/markdownlint-cli2-action@v18
continue-on-error: true
with:
globs: "**/*.md"
config: ".markdownlint-cli2.jsonc"
- name: Evaluate Compliance and Comment
uses: actions/github-script@v8
env:
MARKDOWN_LINT_SUCCESS: ${{ steps.markdown_lint.outcome == 'success' }}
with:
script: |
const pr = context.payload.pull_request;
if (!pr) {
core.warning("This workflow only runs on pull_request events.");
return;
}
const branchName = pr.head.ref;
const title = pr.title || "";
const body = pr.body || "";
let currentLabels = pr.labels.map(label => label.name);
// 1. Branch Naming Standards
const branchRegex = /^(feature|bugfix|hotfix|exp|tech-debt|docs|prototype|dependabot|chore)(\/[0-9]+)?\/.+$/;
const branchMatch = branchName.match(branchRegex);
const branchValid = branchMatch !== null;
// 2. Auto-assign Labels based on Branch Prefix
if (branchMatch && context.payload.action === 'opened') {
const category = branchMatch[1];
const labelMap = {
'feature': 'feature',
'bugfix': 'bug',
'hotfix': 'bug',
'tech-debt': 'tech-debt',
'docs': 'documentation',
'prototype': 'prototype',
'chore': 'chore',
'exp': 'experiment',
'dependabot': 'dependencies'
};
const labelToAdd = labelMap[category];
if (labelToAdd && !currentLabels.includes(labelToAdd)) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: [labelToAdd]
});
currentLabels.push(labelToAdd);
console.log(`Auto-added label: ${labelToAdd}`);
} catch (error) {
console.error(`Failed to add label ${labelToAdd}: ${error}`);
}
}
}
// 3. PR Labels validation
const labelsValid = currentLabels.length > 0;
// 4. Commit Title Standards (PR Title)
const titleRegex = /^(feat|fix|docs|style|refactor|perf|test|chore)(\([a-zA-Z0-9_-]+\))?!?: .+/;
const titleValid = titleRegex.test(title);
// 5. Ticket Linking
const isDependabot = branchName.startsWith('dependabot/');
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const commitMessages = commits.map(c => c.commit.message).join(" ");
const bodyTitleCommits = body + " " + title + " " + commitMessages;
const ticketValid = isDependabot || /AB#\d+/.test(bodyTitleCommits);
// 6. Markdown lint check
const markdownValid = process.env.MARKDOWN_LINT_SUCCESS === 'true';
const allValid = branchValid && labelsValid && titleValid && ticketValid && markdownValid;
// 7. PR Size Check
const totalLines = pr.additions + pr.deletions;
const isLarge = totalLines > 400;
const sizeWarning = isLarge
? `\n\n> ⚠️ **Size Warning:** This PR changes **${totalLines} lines**. Our guidelines recommend keeping PRs under 400 lines to support Trunk-based development and faster reviews. Consider breaking this down if possible.`
: "";
const checklist = [
`## Checklist`,
``,
`- [${branchValid ? 'x' : ' '}] Branch naming matches the standard (\`<category>/[<backlog-item>/][<task>-]description-in-kebab-case\`)`,
`- [${labelsValid ? 'x' : ' '}] Has at least one label attached`,
`- [${titleValid ? 'x' : ' '}] Title matches Conventional Commits (\`<type>(<optional-scope>): <description>\`)`,
`- [${ticketValid ? 'x' : ' '}] Linked to an ADO ticket (contains \`AB#<ticket-number>\`)`,
`- [${markdownValid ? 'x' : ' '}] Markdown files pass linting`
].join('\n');
const messageBody = (allValid ? "✅ **All checks have passed!**\n\n" + checklist : checklist) + sizeWarning;
// 8. Manage the Issue Comment (The Checklist)
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const botComment = comments.find(c => c.user.type === 'Bot' && c.body.includes('## Checklist'));
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: messageBody
});
console.log("Updated existing checklist comment.");
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: messageBody
});
console.log("Created new checklist comment.");
}
// 9. Manage the PR Review (Blocking State)
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const botReviews = reviews.filter(r => r.user.type === 'Bot' && r.state === 'CHANGES_REQUESTED');
if (!allValid) {
if (botReviews.length === 0) {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
event: 'REQUEST_CHANGES',
body: "PR does not meet all standards. Please review the checklist in the comments and resolve the issues."
});
console.log("Created a PR review requesting changes.");
}
} else {
for (const review of botReviews) {
await github.rest.pulls.dismissReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
review_id: review.id,
message: "All checks have now passed."
});
console.log(`Dismissed review ${review.id}.`);
}
}