Add extensive tests for phoenixd-rest #13
Workflow file for this run
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 Quality Gate | |
| on: | |
| pull_request: | |
| branches: [main, develop] | |
| types: [opened, edited, synchronize, ready_for_review] | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| review: | |
| if: ${{ github.actor != 'dependabot[bot]' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| - name: Verify Copilot instructions | |
| run: test -s .github/copilot-instructions.md | |
| - name: Run expertise standard checks | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Fetch fresh PR data + files | |
| const { data: prData } = await github.rest.pulls.get({ | |
| owner, repo, pull_number: pr.number | |
| }); | |
| // Sum changed lines (additions + deletions) | |
| const totalChanged = prData.additions + prData.deletions; | |
| // Pull files for basic heuristics (e.g., tests touched?) | |
| const files = await github.paginate( | |
| github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number } | |
| ); | |
| const commits = await github.paginate( | |
| github.rest.pulls.listCommits, { owner, repo, pull_number: pr.number } | |
| ); | |
| const extsCode = ['.js','.ts','.tsx','.jsx','.py','.rb','.go','.rs','.java','.kt','.cs','.php','.c','.cc','.cpp','.m','.mm','.swift','.scala','.sh','.yml','.yaml','.json','.toml']; | |
| const extsTests = ['.spec.','.test.','/tests/','/__tests__/']; | |
| const codeTouched = files.some(f => | |
| extsCode.some(ext => f.filename.includes(ext))); | |
| const testsTouched = files.some(f => | |
| extsTests.some(tok => f.filename.includes(tok))); | |
| // 1) Scope ≤ 300 lines (from GitHub blog checklist) | |
| const scopeOK = totalChanged <= 300; | |
| // 2) Title and commits follow type: description (verb + object) | |
| const title = prData.title.trim(); | |
| const types = ['feat','fix','docs','refactor','test','chore','ci','build','perf','style']; | |
| const naming = `^(${types.join('|')}):\\s+[A-Z][^\\s]*\\s+.+`; | |
| const titleOK = new RegExp(naming).test(title); | |
| const commitsOK = commits.every(c => new RegExp(naming).test(c.commit.message.split('\\n')[0])); | |
| // 3) Description “why now?” + links to issue | |
| const body = (prData.body || '').trim(); | |
| const hasIssueLink = /#[0-9]+|https?:\/\/github\.com\/.+\/issues\/[0-9]+/i.test(body); | |
| const mentionsWhy = /\bwhy\b|\bbecause\b|\brationale\b|\bcontext\b/i.test(body); | |
| const descOK = body.length >= 50 && (mentionsWhy || hasIssueLink); | |
| // 4) BREAKING change highlighted | |
| const breakingFlagPresent = /\*\*?BREAKING\*\*?|⚠️\s*BREAKING|BREAKING CHANGE/i.test(title) || /\*\*?BREAKING\*\*?|⚠️\s*BREAKING|BREAKING CHANGE/i.test(body); | |
| // Heuristic: if "breaking" appears anywhere, require emphasis flag; otherwise pass. | |
| const containsBreakingWord = /\bbreaking\b/i.test(title) || /\bbreaking\b/i.test(body); | |
| const breakingOK = containsBreakingWord ? breakingFlagPresent : true; | |
| // 5) Request specific feedback | |
| const feedbackOK = /\b(feedback|review focus|please focus|looking for|need input)\b/i.test(body); | |
| // Soft hint: if code changed but no tests changed, nudge (not blocking per article) | |
| const testsHint = codeTouched && !testsTouched; | |
| // Build result table | |
| function row(name, ok, hint='') { | |
| const status = ok ? '✅' : '❌'; | |
| const extra = hint ? ` — ${hint}` : ''; | |
| return `| ${status} | ${name}${extra} |`; | |
| } | |
| const report = [ | |
| `### PR Quality Gate — AI-Era Expertise Standard`, | |
| `This automated review checks your PR against the five items GitHub recommends for high-quality, human-in-the-loop reviews.`, | |
| ``, | |
| `| Pass | Check |`, | |
| `|:----:|:------|`, | |
| row(`Scope ≤ 300 changed lines (current: ${totalChanged})`, scopeOK, scopeOK ? '' : 'Consider splitting into smaller PRs (stacking).'), | |
| row(`Title and commits use type: description (verb + object)`, titleOK && commitsOK), | |
| row(`Description answers "why now?" and links an issue`, descOK, hasIssueLink ? '' : 'Add a linked issue (#123) or URL.'), | |
| row(`Highlight breaking changes with **BREAKING** or ⚠️ BREAKING`, breakingOK, containsBreakingWord && !breakingFlagPresent ? 'Add explicit BREAKING flag.' : ''), | |
| row(`Request specific feedback (e.g., "Concurrency strategy OK?")`, feedbackOK), | |
| ``, | |
| testsHint ? `> ℹ️ Heads-up: Code changed but tests weren’t touched. The blog suggests reviewers read tests first—consider adding or updating tests for clarity.` : ``, | |
| ``, | |
| `_This gate is derived from GitHub’s “Why developer expertise matters more than ever in the age of AI.”_` | |
| ].filter(Boolean).join('\n'); | |
| // Determine blocking result (fail if any required check fails) | |
| const failures = []; | |
| if (!scopeOK) failures.push('Scope > 300 lines'); | |
| if (!titleOK || !commitsOK) failures.push('Naming format invalid'); | |
| if (!descOK) failures.push('Description lacks why/issue link'); | |
| if (!breakingOK) failures.push('Missing explicit BREAKING flag'); | |
| if (!feedbackOK) failures.push('No specific feedback requested'); | |
| const sameRepo = pr.head.repo.full_name === `${owner}/${repo}`; | |
| if (sameRepo) { | |
| try { | |
| // Upsert a single sticky comment | |
| const bot = (await github.rest.users.getAuthenticated()).data.login; | |
| const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr.number }); | |
| const existing = comments.find(c => c.user?.login === bot && /PR Quality Gate — AI-Era/.test(c.body || '')); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: report }); | |
| } else { | |
| await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: report }); | |
| } | |
| // Add labels for visibility | |
| const addLabel = async (name) => { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [name] }); | |
| }; | |
| const removeLabel = async (name) => { | |
| await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name }); | |
| }; | |
| if (failures.length) { | |
| await addLabel('needs-quality-fixes'); | |
| } else { | |
| await removeLabel('needs-quality-fixes'); | |
| await addLabel('quality-checked'); | |
| } | |
| } catch (error) { | |
| if (error.message && error.message.includes('Resource not accessible by integration')) { | |
| core.warning('Skipping comment and label updates due to insufficient permissions.'); | |
| } else { | |
| throw error; | |
| } | |
| } | |
| } else { | |
| core.warning('PR originates from a fork; skipping comment and label updates.'); | |
| } | |
| // Fail the job if there are blocking issues | |
| if (failures.length) { | |
| core.setFailed('PR failed the expertise standard: ' + failures.join(', ')); | |
| } else { | |
| core.info('PR passes the expertise standard.'); | |
| } | |