Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @blairg23
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Security report
url: https://github.com/blairg23/cuequeue/security/advisories/new
about: Please privately disclose security issues through GitHub Security Advisories.
47 changes: 47 additions & 0 deletions .github/ISSUE_TEMPLATE/epic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
name: Epic
about: Track a multi-ticket initiative
title: ""
labels: ["epic", "needs-triage"]
assignees: []
---

## 🧾 Title
<!-- Short, actionable title for this epic. -->

## 🧠 Summary
<!-- Explain what needs to be done and why. Include business and technical context. -->

## 📦 Scope
<!-- What is included in this epic. Be specific about what this initiative will deliver. -->

## 🚫 Out of Scope
<!-- Explicitly list what is NOT included. -->

## ✅ Acceptance Criteria
<!-- Conditions for considering this epic complete. -->
- [ ]
- [ ]
- [ ]

## 🗂️ Milestones / Phases
<!-- Optional sequencing checkpoints for this epic. -->
- [ ] Phase 1
- [ ] Phase 2

## 🧪 Testing / Validation
<!-- How we validate this epic at a system level. -->
- [ ]

## 🔗 Child Tickets
<!-- Link the associated implementation tickets here. -->
- [ ]

## 🧰 Notes / Links
<!-- Any extra notes, docs, links, dependencies, or blockers. -->

## ✅ Addendum: Bugs Found/Fixed
<!-- Post-creation extras that still fit inside scope but were unforeseen. -->

## ⏳ Addendum: Pending
<!-- Remaining items that should become separate tickets if out-of-scope for this epic. -->
41 changes: 41 additions & 0 deletions .github/ISSUE_TEMPLATE/ticket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: Ticket
about: Describe one implementation task
title: ""
labels: ["needs-triage"]
assignees: []
---

## 🧾 Title
<!-- Short, actionable title: "Do X" -->

## 🧠 Summary
<!-- Explain what needs to be done and why. Include background/context. -->

## 📦 Scope
<!-- What is included in this work. Be specific about what this issue will deliver. -->

## 🚫 Out of Scope
<!-- Explicitly list what is NOT included, to avoid scope creep. -->

## ✅ Acceptance Criteria
<!-- Checklist of what must be true for this issue to be considered complete. -->
- [ ]
- [ ]
- [ ]

## 🧪 Testing / Validation
<!-- How this should be tested or validated (manual steps, scripts, etc.). -->
- [ ]

## 🧰 Implementation Notes
<!-- Optional: technical notes, suggested approach, important details for the implementer. -->

## 🧰 Notes / Links
<!-- Any extra notes, docs, links, or blockers. -->

## ✅ Addendum: Bugs Found/Fixed
<!-- Post-ticket creation extras that still fit inside the initial scope but were unforeseen like minor bug fixes during the ticket -->

## ⏳ Addendum: Pending
<!-- Anything still pending that might fit inside this scope, but we're either not doing or needs its own ticket (link it after creation of the ticket) -->
36 changes: 36 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## 🧾 Title
<!-- Example: Improve backlog bootstrap issue creation flow -->

## 🧠 Description
<!-- Briefly explain what this PR accomplishes and why it’s needed. -->
This pull request [summarize purpose here].

## 🧩 Changes Included
<!-- List the key modifications made in this PR. -->
- [ ] Added/updated documentation
- [ ] Implemented new feature or enhancement
- [ ] Refactored existing code
- [ ] Improved error handling or logging
- [ ] Updated environment configuration
- [ ] Other (specify)

## 🎯 Purpose
<!-- What problem does this PR solve or what value does it add? -->
This PR aims to [describe the goal or intended outcome].

## 🔗 Related Issues / Epics
<!-- Link GitHub issues/epics for traceability. -->
- **Epic:** #<epic-issue-number>
- **Issue:** #<issue-number>
- Closes #<issue-number>

## 🧪 Testing
<!-- Describe how reviewers can verify this PR works as intended. -->
1. [Step one of testing]
2. [Step two]
3. [Expected result]

## 🧰 Developer Notes
<!-- Optional: Add any additional notes, caveats, or follow-ups here. -->
- [ ] Verify environment variables are configured properly
- [ ] Ensure code runs under both local and CI/CD environments
74 changes: 74 additions & 0 deletions .github/workflows/validate-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Validate issue body format

on:
issues:
types: [opened, edited]

jobs:
validate:
runs-on: ubuntu-latest
permissions:
issues: write

steps:
- name: Check body and flag if malformed
uses: actions/github-script@v9
with:
script: |
const body = context.payload.issue.body || "";
const number = context.payload.issue.number;

const required = ["## 🧾 Title", "## 🧠 Summary"];
const missing = required.filter(h => !body.includes(h));

if (missing.length === 0) {
core.info(`Issue #${number} body looks good.`);
// Remove needs-format if it was added by a previous failed check
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
name: "needs-format",
});
core.info(`Removed needs-format label from issue #${number}.`);
} catch (_) {}
return;
}

// Create needs-format label if it does not exist yet (422 = already exists)
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: "needs-format",
color: "e11d48",
description: "Issue body does not match the required template format",
});
} catch (_) {}

await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
labels: ["needs-format"],
});

// Relative markdown links in a comment resolve against the comment's
// own URL (.../issues/<n>), not the repo root, so a plain relative
// path 404s instead of opening the template. Build an absolute blob
// URL against the actual default branch instead (see #299).
const ref = context.payload.repository.default_branch;
const repoBlobUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${ref}`;

await github.rest.issues.createComment({
Comment thread
blairg23 marked this conversation as resolved.
Resolved
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
body: [
"This issue is missing required template sections:",
...missing.map(h => `- \`${h}\``),
"",
`Please update the body to match the [epic template](${repoBlobUrl}/.github/ISSUE_TEMPLATE/epic.md) or [ticket template](${repoBlobUrl}/.github/ISSUE_TEMPLATE/ticket.md).`,
].join("\n"),
});
178 changes: 178 additions & 0 deletions .github/workflows/validate-pr-sop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Validate PR review thread SOP

on:
pull_request:
types: [opened, synchronize, reopened]
pull_request_review:
types: [submitted]
# Replying to a review thread is a new review comment, so this re-triggers
# the check after the "reply" SOP step. Resolving a thread and reacting
# +1 have no corresponding Actions trigger event (GitHub does not expose
# pull_request_review_thread's resolved/unresolved action, or comment
# reactions, to workflow `on:` triggers) -- `repo-scaffold pr rerun
# --failed-only` after finishing those steps remains the way to refresh
# the check without a new commit or review submission.
pull_request_review_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to check
required: true
type: number

jobs:
check-sop:
runs-on: ubuntu-latest
permissions:
pull-requests: read

steps:
- name: Check review threads for SOP compliance
uses: actions/github-script@v9
with:
script: |
const prNumber =
context.payload.pull_request?.number ||
parseInt(context.payload.inputs?.pr_number || "0");

if (!prNumber) {
core.setFailed("Could not determine PR number from event payload.");
return;
}

// The SOP's prescribed reply form is "Fixed in <hash>. <sentence>."
// (see AGENTS.md Review thread SOP step 2) -- anchor to that phrase
// instead of matching a bare hex-looking token anywhere in the
// comment, which false-positives on ordinary words built entirely
// from hex letters (e.g. "defaced"). See #263 (original bug) and
// the follow-up review on #307 (this anchoring fix).
const REPLY_RE = /\bfixed in\s+[0-9a-f]{7,40}\b/i;

// A thread's own comments connection can in principle exceed one
// page too, same as the outer reviewThreads connection -- fetch
// whatever's left with a follow-up query scoped to that thread.
async function fetchRemainingComments(threadId, after) {
const comments = [];
while (true) {
const data = await github.graphql(`
query($threadId: ID!, $after: String) {
node(id: $threadId) {
... on PullRequestReviewThread {
comments(first: 50, after: $after) {
pageInfo { hasNextPage endCursor }
nodes {
databaseId
body
reactionGroups {
content
reactors { totalCount }
}
}
}
}
}
}
`, { threadId, after });
const page = data.node.comments;
comments.push(...page.nodes);
if (!page.pageInfo.hasNextPage) break;
after = page.pageInfo.endCursor;
}
return comments;
}

const threads = [];
let after = null;
while (true) {
const data = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 50, after: $after) {
pageInfo { hasNextPage endCursor }
nodes {
id
isResolved
comments(first: 50) {
pageInfo { hasNextPage endCursor }
nodes {
databaseId
body
reactionGroups {
content
reactors { totalCount }
}
}
}
}
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
number: prNumber,
after,
});

const page = data.repository.pullRequest.reviewThreads;
threads.push(...page.nodes);
if (!page.pageInfo.hasNextPage) break;
after = page.pageInfo.endCursor;
}

if (threads.length === 0) {
core.info("No review threads found. SOP check passed.");
return;
}

const failing = [];
for (const thread of threads) {
let comments = thread.comments.nodes;
if (thread.comments.pageInfo.hasNextPage) {
comments = comments.concat(
await fetchRemainingComments(thread.id, thread.comments.pageInfo.endCursor)
);
}

const hasReply = comments.slice(1).some(c => REPLY_RE.test(c.body || ""));
const isResolved = thread.isResolved;
const firstComment = comments[0] || {};
// reactionGroups gives an aggregate count per reaction type in
// one shot -- no pagination needed, unlike the raw `reactions`
// connection this replaced (see #303).
const reactionGroups = firstComment.reactionGroups || [];
const hasPlusOne = reactionGroups.some(
g => g.content === "THUMBS_UP" && g.reactors.totalCount > 0
);

if (!hasReply || !isResolved || !hasPlusOne) {
const missing = [];
if (!hasReply) missing.push("reply");
if (!isResolved) missing.push("resolved");
if (!hasPlusOne) missing.push("reaction(+1)");
failing.push({
threadId: thread.id,
commentId: firstComment.databaseId,
missing,
});
}
}

const total = threads.length;
const passed = total - failing.length;
core.info(`${passed}/${total} threads SOP-compliant.`);

if (failing.length > 0) {
for (const f of failing) {
core.error(
`Thread ${f.threadId} (comment #${f.commentId}) missing: ${f.missing.join(", ")}`
);
}
core.setFailed(
`${failing.length} review thread(s) not SOP-compliant. ` +
"SOP: push fix -> reply with commit hash -> resolve thread -> react +1."
);
}
Loading
Loading