From 4381b099c5968060bd3bfcfcc1b7a6a1f5b42461 Mon Sep 17 00:00:00 2001 From: Eric T Date: Tue, 19 Aug 2025 18:51:54 +0100 Subject: [PATCH 1/4] Assign Copilot reviewer for develop PRs --- .github/copilot-instructions.md | 9 ++ .github/pull_request_template.md | 20 ++++ .github/workflows/assign-copilot-review.yml | 24 ++++ .github/workflows/pr-quality-gate.yml | 121 ++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/assign-copilot-review.yml create mode 100644 .github/workflows/pr-quality-gate.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..872fd5cfc --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,9 @@ +# Repository Instructions for Copilot + +- Follow the Nostr protocol (NIP-xx) specifications. + - Spec index: https://github.com/nostr-protocol/nips + - Each NIP is at `https://github.com/nostr-protocol/nips/blob/master/XX.md` (e.g. NIP-01 → https://github.com/nostr-protocol/nips/blob/master/01.md) +- All changes must include unit tests and update relevant docs. +- Use clear names and remove unused imports. +- Prefer readable, maintainable code over clever shortcuts. +- Run `mvn -q verify` locally before pushing. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..62b0ede2f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Why now? + +Related issue: #____ + +## What changed? + + +## BREAKING + + + +## Review focus + + +## Checklist +- [ ] Scope ≤ 300 lines (or split/stack) +- [ ] Title is **verb + object** (e.g., “Refactor auth middleware to async”) +- [ ] Description links the issue and answers “why now?” +- [ ] **BREAKING** flagged if needed +- [ ] Tests/docs updated (if relevant) diff --git a/.github/workflows/assign-copilot-review.yml b/.github/workflows/assign-copilot-review.yml new file mode 100644 index 000000000..2013a274d --- /dev/null +++ b/.github/workflows/assign-copilot-review.yml @@ -0,0 +1,24 @@ +name: Assign Copilot Reviewer + +on: + pull_request: + branches: [develop] + types: [opened, reopened, ready_for_review] + +permissions: + pull-requests: write + +jobs: + assign: + runs-on: ubuntu-latest + steps: + - name: Request review from GitHub Copilot + uses: actions/github-script@v7 + with: + script: | + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + reviewers: ['github-copilot'] + }); diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml new file mode 100644 index 000000000..fd6f18d7f --- /dev/null +++ b/.github/workflows/pr-quality-gate.yml @@ -0,0 +1,121 @@ +name: PR Quality Gate — AI-Era Expertise Standard + +on: + pull_request: + branches: [develop] + types: [opened, edited, synchronize, ready_for_review] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + 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 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 = verb + object (simple verb list heuristic) + const title = prData.title.trim(); + const verbs = ['Add','Fix','Update','Refactor','Remove','Implement','Document','Docs','Test','Build','Improve','Feat','Enable','Disable','Migrate']; + const titleOK = new RegExp(`^(${verbs.join('|')})\\b.+`).test(title); + // 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 starts with a verb + object (e.g., "Refactor auth middleware")`, titleOK), + 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) failures.push('Title not verb + object'); + 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'); + // 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) => { + try { await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [name] }); } + catch (_) {} // ignore + }; + const removeLabel = async (name) => { + try { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name }); } + catch (_) {} + }; + if (failures.length) { + await addLabel('needs-quality-fixes'); + } else { + await removeLabel('needs-quality-fixes'); + await addLabel('quality-checked'); + } + // 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.'); + } + From edce3722331f3551ed91abc8d9cffc6cbf6a31ed Mon Sep 17 00:00:00 2001 From: Eric T Date: Tue, 19 Aug 2025 19:12:46 +0100 Subject: [PATCH 2/4] fix: allow PR quality gate to label issues --- .github/workflows/pr-quality-gate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml index fd6f18d7f..dc746204f 100644 --- a/.github/workflows/pr-quality-gate.yml +++ b/.github/workflows/pr-quality-gate.yml @@ -8,6 +8,7 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: review: From 510c233eaf857a6863373d81a331aab8145380d6 Mon Sep 17 00:00:00 2001 From: Eric T Date: Tue, 19 Aug 2025 19:26:03 +0100 Subject: [PATCH 3/4] Handle permission errors in quality gate workflow --- .github/workflows/pr-quality-gate.yml | 55 ++++++++++++++++----------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml index dc746204f..e139712e6 100644 --- a/.github/workflows/pr-quality-gate.yml +++ b/.github/workflows/pr-quality-gate.yml @@ -89,29 +89,40 @@ jobs: 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'); - // 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 }); + 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 { - await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: report }); - } - // Add labels for visibility - const addLabel = async (name) => { - try { await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [name] }); } - catch (_) {} // ignore - }; - const removeLabel = async (name) => { - try { await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name }); } - catch (_) {} - }; - if (failures.length) { - await addLabel('needs-quality-fixes'); - } else { - await removeLabel('needs-quality-fixes'); - await addLabel('quality-checked'); + core.warning('PR originates from a fork; skipping comment and label updates.'); } // Fail the job if there are blocking issues if (failures.length) { From ab49f9fa990573795753b4f9cbf6eeecad6db435 Mon Sep 17 00:00:00 2001 From: Eric T Date: Tue, 19 Aug 2025 19:35:54 +0100 Subject: [PATCH 4/4] fix: set PR gate label permissions --- .github/workflows/pr-quality-gate.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml index e139712e6..01171664e 100644 --- a/.github/workflows/pr-quality-gate.yml +++ b/.github/workflows/pr-quality-gate.yml @@ -6,9 +6,8 @@ on: types: [opened, edited, synchronize, ready_for_review] permissions: - contents: read - pull-requests: write issues: write + pull-requests: write jobs: review: