Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/readiness-file-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
---

ci: add file list with line stats to PR readiness comment

The PR readiness bot now shows changed files with per-file addition/deletion
counts, scope classification (Product/Infrastructure/Mixed), and totals.
8 changes: 8 additions & 0 deletions .changeset/scope-boundary-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

ci: scope boundary enforcement for repo-health PRs

New CI check that fails repo-health PRs if they modify product source
code under packages/*/src/. Enforces separation between infrastructure
and product changes.
8 changes: 8 additions & 0 deletions .changeset/smart-pr-nudge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
---

ci: add smart PR nudge for stale PRs

New workflow that runs on weekdays and posts actionable diagnoses on PRs
stale for 7+ days. Checks CI status, unresolved threads, missing reviews,
outdated branches, and draft status. Won't nudge the same PR twice per week.
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ Any PR that modifies files under `packages/squad-cli/src/` or `packages/squad-sd
- The `changelog-gate` CI check will fail without this
- Escape hatch: add the `skip-changelog` label (use sparingly)

## Automated PR Nudge

The **PR Nudge** workflow (`.github/workflows/squad-pr-nudge.yml`) runs on weekdays at 2pm UTC and posts actionable comments on open PRs that have been stale for 7+ days. It diagnoses specific blockers — failing CI checks, unresolved review threads, missing approvals, outdated branches, and draft status — so PR authors know exactly what to do next. Draft PRs get a 14-day grace period. The workflow won't nudge the same PR more than once per week.

## Decisions

If you make a decision that affects other team members, write it to:
Expand Down
184 changes: 184 additions & 0 deletions .github/workflows/squad-pr-nudge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: PR Nudge
on:
schedule:
- cron: '0 14 * * 1-5' # 2pm UTC weekdays (morning US Pacific)
workflow_dispatch: {} # manual trigger for testing

permissions:
contents: read
pull-requests: write
checks: read
issues: read

jobs:
nudge-stale-prs:
name: "Nudge Stale PRs"
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const STALE_DAYS = 7;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - STALE_DAYS);

// Get all open PRs, oldest-updated first
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 50
});

for (const pr of prs.data) {
// Skip PRs updated recently
const lastPush = new Date(pr.updated_at);
if (lastPush > cutoff) continue;

// Give draft PRs 14 days grace period instead of 7
if (pr.draft) {
const created = new Date(pr.created_at);
const draftCutoff = new Date();
draftCutoff.setDate(draftCutoff.getDate() - 14);
if (created > draftCutoff) continue;
}

// Don't nudge the same PR more than once per week
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 5,
sort: 'created',
direction: 'desc'
});
const recentNudge = comments.data.find(c =>
c.user.login === 'github-actions[bot]' &&
c.body.includes('<!-- pr-nudge -->') &&
new Date(c.created_at) > cutoff
);
if (recentNudge) continue;

// Build the diagnosis — collect actionable items
const actions = [];
const daysSinceUpdate = Math.floor((Date.now() - lastPush) / (1000 * 60 * 60 * 24));

// 1. Check if still in draft
if (pr.draft) {
actions.push('📝 **Still in draft** — mark as "Ready for review" when you\'re done, or close if abandoned.');
}

// 2. Check CI status for failures
const checks = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha,
per_page: 50
});
const failedChecks = checks.data.check_runs.filter(c =>
c.conclusion === 'failure'
).map(c => c.name);
if (failedChecks.length > 0) {
actions.push(`🔴 **${failedChecks.length} CI check(s) failing:** ${failedChecks.join(', ')}. Fix these first.`);
}

// 3. Check for unresolved review threads (Copilot vs human)
const threads = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 50) {
nodes {
isResolved
isOutdated
comments(first: 1) {
nodes { author { login } }
}
}
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
number: pr.number
});
const unresolvedThreads = threads.repository.pullRequest.reviewThreads.nodes
.filter(t => !t.isResolved && !t.isOutdated);
if (unresolvedThreads.length > 0) {
const copilotThreads = unresolvedThreads.filter(t =>
t.comments.nodes[0]?.author?.login?.includes('copilot')
);
const humanThreads = unresolvedThreads.length - copilotThreads.length;
const parts = [];
if (copilotThreads.length > 0) parts.push(`${copilotThreads.length} from Copilot`);
if (humanThreads > 0) parts.push(`${humanThreads} from reviewers`);
actions.push(`💬 **${unresolvedThreads.length} unresolved review thread(s)** (${parts.join(', ')}). Address and resolve them.`);
}

// 4. Check review state (changes requested vs approved vs none)
const reviews = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
const latestByUser = {};
for (const r of reviews.data) {
if (r.state === 'COMMENTED') continue;
latestByUser[r.user.login] = r;
}
const changesRequested = Object.values(latestByUser).filter(r => r.state === 'CHANGES_REQUESTED');
const approvals = Object.values(latestByUser).filter(r => r.state === 'APPROVED');
if (changesRequested.length > 0) {
const reviewers = changesRequested.map(r => `@${r.user.login}`).join(', ');
actions.push(`🔄 **Changes requested** by ${reviewers}. Address their feedback and request re-review.`);
} else if (approvals.length === 0 && !pr.draft) {
actions.push('👀 **No approving reviews yet.** Request a review from a teammate.');
}

// 5. Check if branch is behind base
const comparison = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: pr.head.sha,
head: pr.base.ref
});
if (comparison.data.ahead_by > 10) {
actions.push(`⬇️ **${comparison.data.ahead_by} commits behind ${pr.base.ref}.** Rebase to pick up latest changes.`);
}

// 6. If everything looks good and approved — it's ready to merge
if (actions.length === 0 && approvals.length > 0) {
actions.push('✅ **Looks ready to merge!** All checks pass, approved — just needs someone to click merge.');
}

// Fallback if no specific blockers found
if (actions.length === 0) {
actions.push('🤔 **No obvious blockers found** — but this PR has been quiet. Is it still active?');
}

// Post the nudge comment
const body = [
'<!-- pr-nudge -->',
`👋 **Friendly nudge** — this PR has had no activity for **${daysSinceUpdate} days**.`,
'',
'**What needs attention:**',
...actions.map(a => `- ${a}`),
'',
'---',
'*If this PR is abandoned, please close it. If it\'s blocked on something external, leave a comment so the team knows.*',
'*This is an automated check that runs on weekdays. It won\'t nudge the same PR more than once per week.*'
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: body
});

core.info(`Nudged PR #${pr.number}: ${pr.title} (${daysSinceUpdate} days stale, ${actions.length} action items)`);
}
4 changes: 2 additions & 2 deletions .github/workflows/squad-repo-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:

# ─── Architectural Review (INFORMATIONAL) ───────────────────────────
architectural-review:
name: Architectural Review
name: Architectural Review — Structure & Design Rules
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
Expand Down Expand Up @@ -150,7 +150,7 @@ jobs:

# ─── Security Review (INFORMATIONAL) ────────────────────────────────
security-review:
name: Security Review
name: Security Review — Permissions & Secrets
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.actor != 'dependabot[bot]'
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/squad-scope-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Scope Check
on:
pull_request:
types: [opened, synchronize, reopened, labeled]

permissions:
pull-requests: read
contents: read

jobs:
scope-boundary:
name: "Scope Boundary"
runs-on: ubuntu-latest
if: >-
startsWith(github.head_ref, 'repo-health/') ||
contains(github.event.pull_request.labels.*.name, 'repo-health')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for product code in repo-health PR
run: |
PRODUCT_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'packages/squad-cli/src/' 'packages/squad-sdk/src/')
if [ -n "$PRODUCT_FILES" ]; then
echo "::error::Repo-health PRs must not modify product source code."
echo "::error::The following product files were changed:"
echo "$PRODUCT_FILES" | while read -r f; do echo "::error:: - $f"; done
echo "::error::Move product changes to a separate PR."
exit 1
fi
echo "✅ No product source files in this repo-health PR."
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ An automated readiness check runs on every PR and posts a checklist comment. Add
| **No merge conflicts** | Resolve any conflicts with the target branch |
| **CI passing** | All CI checks (build, test, lint) must be green |

The readiness comment also includes a **file list with line stats** — each changed file is shown with per-file addition/deletion counts, a scope classification (Product/Infrastructure/Mixed), and totals. This helps reviewers quickly gauge PR size and impact.

The readiness check is **informational** — it helps you self-serve before a human reviewer looks at your PR. It automatically re-runs after Squad CI completes, so the checklist stays up to date without manual intervention. See `.github/PR_REQUIREMENTS.md` for the full requirements spec.

## Code Style & Conventions
Expand Down
Loading
Loading