feat: LAA Risk Score Derivations #4087
This file contains hidden or 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: "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}.`); | |
| } | |
| } |