[OPIK-2095] [BE] Implement Webhook event handler logging system #1459
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
permissions: | |
contents: read | |
pull-requests: write | |
name: PR Linter | |
# This workflow runs on pull requests that are opened, edited, or have new commits pushed. | |
on: | |
pull_request: | |
types: [opened, edited, synchronize] | |
jobs: | |
lint-pr: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout Code | |
uses: actions/checkout@v4 | |
- name: Validate PR Title, Description, and Commits | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
// Get the PR context | |
const pr = context.payload.pull_request; | |
if (!pr) { | |
core.setFailed("Could not get PR from context."); | |
return; | |
} | |
const owner = context.repo.owner; | |
const repo = context.repo.repo; | |
let errors = []; | |
// 1. Validate PR Title | |
const title = pr.title; | |
const titleRegex = /^\[(OPIK-\d+|issue-\d+|NA)\](\s*\[(BE|FE|DOCS|SDK|GHA)\])*\s*.+$/; | |
if (!titleRegex.test(title)) { | |
errors.push( | |
"❌ **Invalid Title Format.** Your PR title must include a ticket/issue number and may optionally include component tags (`[FE]`, `[BE]`, etc.).\n\n" + | |
" - **Internal contributors: Open a JIRA ticket and link to it:** `[OPIK-xxxx] [COMPONENT] Your change`\n" + | |
" - **External contributors: Open a Github Issue and link to it via its number:** `[issue-xxxx] [COMPONENT] Your change`\n\n" + | |
" *Example: `[issue-3108] [BE] [FE] Fix authentication bug` or `[OPIK-1234] Fix bug`*" | |
); | |
} | |
// 2. Validate PR Description | |
const body = pr.body || ""; | |
const requiredHeadings = ["## Details", "## Change checklist", "## Issues", "## Testing", "## Documentation"]; | |
if (body.length === 0) { | |
errors.push("❌ **Missing Description.** The PR description cannot be empty."); | |
} else { | |
for (const heading of requiredHeadings) { | |
if (!body.includes(heading)) { | |
errors.push(`❌ **Missing Section.** The description is missing the \`${heading}\` section.`); | |
} | |
} | |
// Helper function to get content of a markdown section | |
const getSectionContent = (sectionTitle) => { | |
const startIndex = body.indexOf(sectionTitle); | |
if (startIndex === -1) return null; | |
const nextHeaderIndex = body.indexOf("## ", startIndex + sectionTitle.length); | |
const endIndex = nextHeaderIndex !== -1 ? nextHeaderIndex : body.length; | |
return body.substring(startIndex + sectionTitle.length, endIndex).trim(); | |
}; | |
const detailsContent = getSectionContent("## Details"); | |
if (detailsContent !== null && detailsContent.length === 0) { | |
errors.push("❌ **Incomplete Details Section.** The `## Details` section cannot be empty."); | |
} | |
const issuesContent = getSectionContent("## Issues"); | |
const issuesRegex = /(#\d+|OPIK-\d+)/; | |
const isNATicket = title.startsWith("[NA]"); | |
if (issuesContent !== null && !isNATicket && !issuesRegex.test(issuesContent)) { | |
errors.push( | |
"❌ **Incomplete Issues Section.** You must reference at least one GitHub issue (`#xxxx`) or Jira ticket (`OPIK-xxxx`) under the `## Issues` section." | |
); | |
} | |
} | |
// 3. Final step: Comment on PR and/or fail the job | |
if (errors.length > 0) { | |
const commentBody = "### 📋 PR Linter Failed\n\n" + errors.join("\n\n---\n\n"); | |
const logMessage = "PR Linter Failed:\n\n" + errors.join("\n\n") | |
.replace(/❌ \*\*/g, '❌ ') | |
.replace(/\*\*`/g, '`') | |
.replace(/`\*\*/g, '`') | |
.replace(/###\s/g, '') | |
.replace(/---\n\n/g, ''); | |
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: commentBody }); | |
core.setFailed(logMessage); | |
} else { | |
console.log("✅ PR linting passed!"); | |
} |