-
Notifications
You must be signed in to change notification settings - Fork 0
Add PR template and linked-ticket check #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| ## Ticket | ||
|
|
||
| Closes # | ||
|
|
||
| <!-- | ||
| Required. Every PR closes an issue. | ||
|
|
||
| Closes #12 | ||
|
|
||
| The "linked ticket" check looks for a closing keyword - closes, fixes or | ||
| resolves, any tense. A bare "Refs #12" will not satisfy it, because a plain | ||
| mention does not close the issue or create the link in its Development sidebar. | ||
|
|
||
| Doing partial work? Open a smaller issue this PR does close, and reference the | ||
| parent with "Refs #12" on a separate line. | ||
|
|
||
| Genuinely no ticket? Apply the `no-ticket` label to bypass the check. | ||
| --> | ||
|
|
||
| ## What and why | ||
|
|
||
| <!-- The diff shows how. Say what changed and why it needed changing. --> | ||
|
|
||
| ## How this was verified | ||
|
|
||
| <!-- | ||
| There is no CI on this repo yet, so this section is the only quality gate. | ||
| Be specific: "booted the stack, created a room, confirmed presence appears and | ||
| clears on disconnect" beats "tested locally". | ||
|
|
||
| Say what you did NOT verify too. | ||
| --> | ||
|
|
||
| ## Notes for the reviewer | ||
|
|
||
| <!-- Known gaps, follow-ups, decisions you want pushed back on. Delete if none. --> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| name: PR ticket link | ||
|
|
||
| # Every PR must close a GitHub issue. This only reports status - it blocks | ||
| # nothing until "linked ticket" is marked a required check in branch protection | ||
| # (Settings > Branches). Apply the "no-ticket" label to bypass it. | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: | ||
| - opened | ||
| - edited # re-run when the body is fixed | ||
| - reopened | ||
| - synchronize | ||
| - ready_for_review | ||
| - labeled # re-run when the bypass label is applied | ||
| - unlabeled | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| ticket: | ||
| name: linked ticket | ||
| runs-on: ubuntu-latest | ||
| if: github.event.pull_request.draft == false | ||
| steps: | ||
| - uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const BYPASS_LABEL = 'no-ticket'; | ||
| const MARKER = '<!-- pr-ticket-check -->'; | ||
|
|
||
| const pr = context.payload.pull_request; | ||
| const { owner, repo } = context.repo; | ||
| const labels = pr.labels.map(l => l.name); | ||
|
|
||
| // The PR template documents "Closes #12" inside an HTML comment. | ||
| // Strip comments first or every PR passes on the example text. | ||
| const body = (pr.body || '').replace(/<!--[\s\S]*?-->/g, ''); | ||
|
|
||
| const CLOSING = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi; | ||
| const REFS = /\b(?:refs?|references?|relate[sd]\s+to|part\s+of)\s+#(\d+)\b/gi; | ||
|
|
||
| const nums = re => [...new Set([...body.matchAll(re)].map(m => m[1]))]; | ||
| const closing = nums(CLOSING); | ||
| const refsOnly = nums(REFS).filter(n => !closing.includes(n)); | ||
|
|
||
| async function describe(n) { | ||
| try { | ||
| const { data } = await github.rest.issues.get({ | ||
| owner, repo, issue_number: Number(n), | ||
| }); | ||
| if (data.pull_request) return { n, ok: false, why: 'that is a pull request, not an issue' }; | ||
| return { n, ok: true, title: data.title, state: data.state, | ||
| assignee: data.assignee?.login, | ||
| labels: data.labels.map(l => l.name || l) }; | ||
| } catch (e) { | ||
| if (e.status === 404) return { n, ok: false, why: 'no such issue' }; | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| const found = await Promise.all(closing.map(describe)); | ||
| const valid = found.filter(f => f.ok); | ||
|
|
||
| let pass, lines; | ||
| if (labels.includes(BYPASS_LABEL)) { | ||
| pass = true; | ||
| lines = [`Bypassed with the \`${BYPASS_LABEL}\` label.`]; | ||
| } else if (valid.length) { | ||
| pass = true; | ||
|
Comment on lines
+72
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow files ---'
git ls-files .github/workflows/pr-ticket.yml README.md .github
printf '%s\n' '--- workflow outline ---'
ast-grep outline .github/workflows/pr-ticket.yml --lang yaml 2>/dev/null || true
printf '%s\n' '--- relevant workflow sections ---'
nl -ba .github/workflows/pr-ticket.yml | sed -n '1,130p'
printf '%s\n' '--- references to ticket/closing/base/default behavior ---'
rg -n -i 'closing|closes|closed|fixes|resolves|ticket|base|default|release' .github README.md 2>/dev/null || trueRepository: naman0r/tandemcode Length of output: 3423 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
sed -n '1,150p' .github/workflows/pr-ticket.yml
printf '%s\n' '--- pull request template ---'
sed -n '1,80p' .github/PULL_REQUEST_TEMPLATE.md
printf '%s\n' '--- exact locations ---'
grep -n -E 'base|head|default|valid|pass|CLOSING|pull_request|github.event' .github/workflows/pr-ticket.yml || true
printf '%s\n' '--- deterministic regex and branch-gate probe ---'
node - <<'JS'
const source = require('fs').readFileSync('.github/workflows/pr-ticket.yml', 'utf8');
const match = source.match(/const CLOSING = (\\/.*?\\/gi);/);
if (!match) throw new Error('CLOSING regex not found');
const closing = eval(match[1]);
for (const text of [
'Closes `#12`',
'Closes: `#12`',
'Fixes `#12`',
'Resolves: `#12`',
]) {
closing.lastIndex = 0;
console.log(JSON.stringify(text), [...text.matchAll(closing)].map(m => m[1]));
}
console.log('base-branch references:', (source.match(/base|default/gi) || []).length);
JSRepository: naman0r/tandemcode Length of output: 7977 🏁 Script executed: #!/bin/bash
set -eu
node - <<'JS'
const closing = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
for (const text of [
'Closes `#12`',
'Closes: `#12`',
'Fixes `#12`',
'Resolves: `#12`',
]) {
closing.lastIndex = 0;
console.log(JSON.stringify(text), [...text.matchAll(closing)].map(m => m[1]));
}
JS
python3 - <<'PY'
from pathlib import Path
text = Path(".github/workflows/pr-ticket.yml").read_text()
print("base/default branch references:",
[line.strip() for line in text.splitlines()
if "base" in line.lower() or "default" in line.lower()])
print("success branch:",
"else if (valid.length)" in text and "pass = true;" in text)
PYRepository: naman0r/tandemcode Length of output: 291 🌐 Web query:
💡 Result: On GitHub, the use of closing keywords (such as closes, fixes, or resolves) to automatically close an issue is only supported when the pull request targets the repository's default branch [1][2]. If a pull request targets any branch other than the default, GitHub ignores these closing keywords [1][3]. In such cases: - No automatic link is created between the pull request and the issue [1][2]. - Merging the pull request will not automatically close the linked issue, even if the non-default branch is later merged into the default branch [1][4]. This behavior is by design [4]. To ensure an issue is automatically closed by a pull request, the pull request must be opened against the repository's default branch [1][2]. Citations:
🌐 Web query:
💡 Result: To link a pull request (PR) to an issue so that the issue automatically closes upon merging, you can use specific closing keywords followed by the issue reference in your pull request description or commit messages [1][2]. Key details include: Supported Keywords: You can use any of the following, which are case-insensitive [1][3]: - close, closes, closed [1][4] - fix, fixes, fixed [1][4] - resolve, resolves, resolved [1][4] Syntax and Usage: - The keyword can be followed by an optional colon (e.g., Closes: Citations:
🏁 Script executed: #!/bin/bash
set -eu
url='https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue'
curl -LfsS "$url" \
| sed 's/<[^>]*>/ /g; s/"/"/g; s/&/\&/g; s/[[:space:]]\+/ /g' \
| grep -o -i -E '.{0,240}(default branch|non-default|closing keyword|linked).{0,300}' \
| head -20Repository: naman0r/tandemcode Length of output: 7092 Reject non-default-base PRs before setting GitHub ignores closing keywords and creates no issue link when a PR targets a non-default branch. This workflow still passes when 🤖 Prompt for AI Agents |
||
| lines = ['**Linked ticket**', '']; | ||
| for (const f of valid) { | ||
| const meta = [ | ||
| f.state, | ||
| f.assignee ? `@${f.assignee}` : 'unassigned', | ||
| ...(f.labels.length ? [f.labels.join(', ')] : []), | ||
| ].join(' · '); | ||
| lines.push(`- #${f.n} — ${f.title} \n <sub>${meta}</sub>`); | ||
| if (f.state === 'closed') lines.push(` > note: #${f.n} is already closed.`); | ||
| } | ||
| if (refsOnly.length) { | ||
| lines.push('', `Also references (will stay open): ${refsOnly.map(n => '#' + n).join(', ')}`); | ||
| } | ||
| } else { | ||
| pass = false; | ||
| lines = [ | ||
| '**No linked ticket.**', | ||
| '', | ||
| 'Add a closing reference to the PR description, on its own line:', | ||
| '', | ||
| '```', | ||
| 'Closes #12', | ||
| '```', | ||
| '', | ||
| 'Accepted keywords: `closes`, `fixes`, `resolves` (and their tenses).', | ||
| ]; | ||
| if (refsOnly.length) { | ||
| lines.push('', `Found ${refsOnly.map(n => '#' + n).join(', ')}, but a plain reference does not close the issue. Use \`Closes\`.`); | ||
| } | ||
| for (const f of found.filter(x => !x.ok)) { | ||
| lines.push('', `\`#${f.n}\` cannot be used: ${f.why}.`); | ||
| } | ||
| lines.push('', `No ticket to link? Apply the \`${BYPASS_LABEL}\` label.`); | ||
| } | ||
|
|
||
| const comment = `${MARKER}\n${lines.join('\n')}`; | ||
| const existing = (await github.rest.issues.listComments({ | ||
| owner, repo, issue_number: pr.number, per_page: 100, | ||
| })).data.find(c => c.body?.includes(MARKER)); | ||
|
|
||
| try { | ||
| if (existing) { | ||
| await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: comment }); | ||
| } else { | ||
| await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: comment }); | ||
| } | ||
| } catch (e) { | ||
| core.warning(`Could not post the summary comment: ${e.message}`); | ||
| } | ||
|
|
||
| if (!pass) core.setFailed('This PR does not close a ticket.'); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # PR workflow | ||
|
|
||
| Every pull request closes a GitHub issue. A CI check enforces the link. | ||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document the The workflow allows a pull request with the 🤖 Prompt for AI Agents |
||
|
|
||
| ## Opening a PR | ||
|
|
||
| The description template puts the reference at the top: | ||
|
|
||
| ``` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a language identifier to the fenced code block.
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 9-9: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| Closes #12 | ||
| ``` | ||
|
|
||
| Accepted keywords are `closes`, `fixes` and `resolves`, in any tense. On merge | ||
| the issue closes on its own and the PR appears in its Development sidebar. | ||
|
|
||
| `Refs #12` by itself does not satisfy the check — a plain mention neither closes | ||
| the issue nor creates that link. Doing partial work? Open a smaller issue this PR | ||
| does close, and reference the parent with `Refs` on a separate line. | ||
|
|
||
| ## The check | ||
|
|
||
| `linked ticket` runs on every PR and: | ||
|
|
||
| - parses the description for a closing keyword | ||
| - confirms the number is a real issue, not a pull request | ||
| - comments with the ticket's title, state, assignee and labels | ||
|
|
||
| It re-runs when the description is edited, so fixing a missing reference does not | ||
| need a new commit. HTML comments are stripped before parsing, so the example | ||
| inside the template does not count as a link. | ||
|
Comment on lines
+20
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document that draft pull requests skip the check.
🤖 Prompt for AI Agents |
||
|
|
||
| ## Bypassing it | ||
|
|
||
| Apply the `no-ticket` label, for hotfixes or work with genuinely no ticket. The | ||
| check re-runs when labels change. | ||
|
|
||
| ## Making it a merge blocker | ||
|
|
||
| It is report-only today: a failing check is visible but blocks nothing. To | ||
| enforce, with no code change: | ||
|
|
||
| > Settings → Branches → add a rule for `main` → Require status checks to pass → | ||
| > select `linked ticket` | ||
|
|
||
| ## Files | ||
|
|
||
| | Path | | | ||
| |---|---| | ||
| | `.github/PULL_REQUEST_TEMPLATE.md` | the description template | | ||
| | `.github/workflows/pr-ticket.yml` | the check | | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: naman0r/tandemcode
Length of output: 3697
🏁 Script executed:
Repository: naman0r/tandemcode
Length of output: 5901
🏁 Script executed:
Repository: naman0r/tandemcode
Length of output: 229
Accept the colon separator.
CLOSINGdoes not matchCloses:#12``, so a valid GitHub closing reference can cause the job to report no linked ticket. Update the pattern to accept:as a separator, such as `(?:\s+|:\s*)#`.🤖 Prompt for AI Agents