Skip to content
Draft
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
312 changes: 312 additions & 0 deletions .github/workflows/pr-handler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
# PR Handler - Comprehensive Pull Request Management
# Handles incoming PRs with intelligent triage, labeling, and status tracking

name: PR Handler

on:
pull_request:
types: [opened, edited, synchronize, reopened, ready_for_review]
pull_request_review:
types: [submitted]
issue_comment:
types: [created]
Comment on lines +6 to +12
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is triggered on pull_request_review and issue_comment, but analyze-pr is gated to only pull_request/workflow_dispatch events, so review submissions won’t update labels/status (e.g., “approved”) despite the docs claiming approval/status tracking. Either handle pull_request_review events (fetch PR by pull_request.number) or remove unsupported triggers to avoid confusion and unnecessary runs.

Copilot uses AI. Check for mistakes.
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to handle'
required: false

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

jobs:
analyze-pr:
name: Analyze PR
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
outputs:
is_wip: ${{ steps.analyze.outputs.is_wip }}
pr_type: ${{ steps.analyze.outputs.pr_type }}
needs_review: ${{ steps.analyze.outputs.needs_review }}
can_auto_merge: ${{ steps.analyze.outputs.can_auto_merge }}
org_scope: ${{ steps.analyze.outputs.org_scope }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Analyze PR metadata
id: analyze
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request ||
await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.inputs?.pr_number || context.issue.number
}).then(r => r.data);
Comment on lines +46 to +51
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In analyze-pr, the workflow_dispatch path allows pr_number to be omitted, but then pull_number resolves to undefined (context.issue.number is also undefined) and the pulls.get call will error. Make pr_number required for workflow_dispatch runs, or short-circuit with a clear error when it’s missing.

Copilot uses AI. Check for mistakes.

// Check if WIP
const isWIP = pr.title.includes('[WIP]') || pr.title.includes('WIP:') || pr.draft;
core.setOutput('is_wip', isWIP);

// Determine PR type based on title and files
let prType = 'other';
const title = pr.title.toLowerCase();
Comment on lines +53 to +59
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP detection is case-sensitive (e.g., '[WIP]' and 'WIP:' only). If titles use '[wip]' or 'wip:' they’ll be treated as ready-for-review. Consider using a case-insensitive match (lowercasing the title first) to make this robust.

Suggested change
// Check if WIP
const isWIP = pr.title.includes('[WIP]') || pr.title.includes('WIP:') || pr.draft;
core.setOutput('is_wip', isWIP);
// Determine PR type based on title and files
let prType = 'other';
const title = pr.title.toLowerCase();
// Check if WIP (case-insensitive)
const lowerTitle = pr.title.toLowerCase();
const isWIP = lowerTitle.includes('[wip]') || lowerTitle.includes('wip:') || pr.draft;
core.setOutput('is_wip', isWIP);
// Determine PR type based on title and files
let prType = 'other';
const title = lowerTitle;

Copilot uses AI. Check for mistakes.
if (title.includes('workflow') || title.includes('ci') || title.includes('github actions')) {
prType = 'workflow';
} else if (title.includes('doc') || title.includes('wiki') || title.includes('readme')) {
prType = 'documentation';
} else if (title.includes('test') || title.includes('ci/cd')) {
prType = 'testing';
} else if (title.includes('infrastructure') || title.includes('setup')) {
Comment on lines +60 to +66
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR type detection checks title.includes('ci') before title.includes('ci/cd'), so a title containing “ci/cd” will always be categorized as workflow instead of testing. Reorder the checks or use more specific matching to avoid this misclassification.

Copilot uses AI. Check for mistakes.
prType = 'infrastructure';
} else if (title.includes('agent') || title.includes('ai') || title.includes('claude')) {
prType = 'ai-feature';
} else if (title.includes('collaboration') || title.includes('memory')) {
prType = 'core-feature';
}
core.setOutput('pr_type', prType);

// Determine org scope
const orgPatterns = [
'BlackRoad-OS', 'BlackRoad-AI', 'BlackRoad-Cloud', 'BlackRoad-Hardware',
'BlackRoad-Security', 'BlackRoad-Labs', 'BlackRoad-Foundation',
'BlackRoad-Media', 'BlackRoad-Studio', 'BlackRoad-Interactive',
'BlackRoad-Education', 'BlackRoad-Gov', 'BlackRoad-Archive',
'BlackRoad-Ventures', 'Blackbox-Enterprises'
];
const body = pr.body || '';
const orgsFound = orgPatterns.filter(org =>
title.includes(org) || body.includes(org)
);
Comment on lines +84 to +86
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Org scope detection won’t match because title is lowercased but orgPatterns contain mixed-case strings, and body.includes(org) is case-sensitive. Normalize both title/body and patterns to the same case before matching so org_scope labels are applied correctly.

Suggested change
const orgsFound = orgPatterns.filter(org =>
title.includes(org) || body.includes(org)
);
const bodyLower = body.toLowerCase();
const orgPatternsLower = orgPatterns.map(org => org.toLowerCase());
const orgsFound = orgPatterns.filter((org, index) => {
const orgLower = orgPatternsLower[index];
return title.includes(orgLower) || bodyLower.includes(orgLower);
});

Copilot uses AI. Check for mistakes.
core.setOutput('org_scope', orgsFound.join(',') || 'all');

// Check if needs review
const needsReview = !isWIP && pr.requested_reviewers.length === 0;
core.setOutput('needs_review', needsReview);

// Check if can auto-merge (copilot branches with checks passed)
const canAutoMerge = pr.head.ref.startsWith('copilot/') &&
!isWIP &&
pr.mergeable_state === 'clean';
core.setOutput('can_auto_merge', canAutoMerge);

return {
number: pr.number,
isWIP,
prType,
orgScope: orgsFound.join(',') || 'all',
needsReview,
canAutoMerge
};

label-pr:
name: Label PR
runs-on: ubuntu-latest
needs: analyze-pr
steps:
- name: Apply labels
uses: actions/github-script@v7
with:
script: |
const prType = '${{ needs.analyze-pr.outputs.pr_type }}';
const isWIP = '${{ needs.analyze-pr.outputs.is_wip }}' === 'true';
const orgScope = '${{ needs.analyze-pr.outputs.org_scope }}';

const labels = [];

// Type labels
const typeLabels = {
'workflow': 'workflows',
'documentation': 'documentation',
'testing': 'testing',
'infrastructure': 'infrastructure',
'ai-feature': 'ai-enhancement',
'core-feature': 'enhancement'
};
if (typeLabels[prType]) {
labels.push(typeLabels[prType]);
}

// Status labels
if (isWIP) {
labels.push('work-in-progress');
} else {
labels.push('ready-for-review');
}

// Org scope labels
if (orgScope && orgScope !== 'all') {
const orgs = orgScope.split(',');
if (orgs.length > 3) {
labels.push('multi-org');
} else {
orgs.forEach(org => {
const code = org.split('-').pop();
labels.push(`org:${code.toLowerCase()}`);
});
}
}
Comment on lines +143 to +154
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no orgs are detected, the workflow sets org_scope to all and applies no scope label. The docs/label taxonomy mention an all-orgs label; if that label is part of the intended system, consider emitting/applying it explicitly so “no specific org detected” is still represented consistently.

Copilot uses AI. Check for mistakes.

// Apply labels
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
Comment on lines +156 to +161
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow_dispatch path will fail because this job uses context.issue.number, which is undefined for workflow_dispatch events. Consider setting an explicit PR number output in analyze-pr (e.g., pr_number) and using that for issue_number/pull_number across label/comment/reviewer steps when github.event_name == 'workflow_dispatch'.

Suggested change
// Apply labels
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
// Determine PR number (supports both pull_request and workflow_dispatch)
const prNumber = context.eventName === 'workflow_dispatch'
? Number(context.payload && context.payload.inputs && context.payload.inputs.pr_number)
: (context.issue && context.issue.number);
// Apply labels
if (labels.length > 0 && prNumber) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,

Copilot uses AI. Check for mistakes.
labels: labels
});
Comment on lines +158 to +163
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying labels will fail the step if any label doesn’t already exist in the repo (GitHub API returns 422). Since org labels are dynamically generated (e.g., org:foundation, org:enterprises), consider either creating missing labels first or wrapping addLabels in try/catch and only applying labels that exist.

Suggested change
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labels
});
const owner = context.repo.owner;
const repo = context.repo.repo;
// Fetch all existing labels in the repo to avoid 422 on non-existent labels
const existingLabels = await github.paginate(
github.rest.issues.listLabelsForRepo,
{ owner, repo }
);
const existingLabelNames = new Set(existingLabels.map(l => l.name));
const validLabels = labels.filter(label => existingLabelNames.has(label));
if (validLabels.length > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: context.issue.number,
labels: validLabels
});
}

Copilot uses AI. Check for mistakes.
}

comment-on-pr:
name: Comment on PR
runs-on: ubuntu-latest
needs: analyze-pr
if: needs.analyze-pr.outputs.needs_review == 'true'
steps:
- name: Add helpful comment
uses: actions/github-script@v7
with:
script: |
const prType = '${{ needs.analyze-pr.outputs.pr_type }}';
const isWIP = '${{ needs.analyze-pr.outputs.is_wip }}' === 'true';

let comment = '## 🤖 PR Handler Analysis\n\n';
comment += `**Type:** ${prType}\n`;
comment += `**Status:** ${isWIP ? 'Work in Progress' : 'Ready for Review'}\n\n`;

if (!isWIP) {
comment += '### Next Steps\n';
comment += '- [ ] Code review by maintainers\n';
comment += '- [ ] CI checks pass\n';
comment += '- [ ] Resolve any review comments\n';
comment += '- [ ] Ready to merge\n\n';
}

// Type-specific guidance
const guidance = {
'workflow': '⚠️ **Workflow changes** require careful review for security and permissions.',
'documentation': '📚 **Documentation** - Ensure accuracy and completeness.',
'testing': '🧪 **Testing changes** - Verify test coverage and quality.',
'infrastructure': '🏗️ **Infrastructure** - Review for production readiness.',
'ai-feature': '🤖 **AI Feature** - Test thoroughly with different scenarios.',
'core-feature': '⭐ **Core Feature** - Requires comprehensive review and testing.'
};

if (guidance[prType]) {
comment += `### ℹ️ ${guidance[prType]}\n\n`;
}

comment += '---\n';
comment += '*Automated by BlackRoad PR Handler*';

// Check if comment already exists
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existing = comments.data.find(c =>
c.body.includes('PR Handler Analysis') &&
c.user.type === 'Bot'
);

if (!existing) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}

request-reviewers:
name: Request Reviewers
runs-on: ubuntu-latest
needs: analyze-pr
if: needs.analyze-pr.outputs.needs_review == 'true' && needs.analyze-pr.outputs.is_wip == 'false'
steps:
- name: Assign reviewers
uses: actions/github-script@v7
with:
script: |
// Request review from repository owner
try {
await github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: ['blackboxprogramming']
});
} catch (error) {
console.log('Could not request reviewers:', error.message);
}

check-merge-readiness:
name: Check Merge Readiness
runs-on: ubuntu-latest
needs: analyze-pr
if: needs.analyze-pr.outputs.can_auto_merge == 'true'
steps:
- name: Check if ready to merge
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;

// Check CI status
const checks = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha
});

const allPassed = checks.data.check_runs.every(check =>
check.conclusion === 'success' || check.conclusion === 'skipped'
);

if (allPassed && pr.mergeable) {
await github.rest.issues.createComment({
Comment on lines +261 to +275
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This job assumes context.payload.pull_request exists, but it will be undefined for workflow_dispatch runs even if can_auto_merge is true, causing a runtime error. Also, check_runs.every(...) returns true when there are zero checks, which could incorrectly report “ready to merge”; ensure at least one relevant check run exists (or query required checks) before declaring success.

Copilot uses AI. Check for mistakes.
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: '✅ **This PR is ready to merge!**\n\nAll checks have passed and the PR is mergeable. A maintainer can merge this PR.'
});
}

update-pr-status:
name: Update PR Status
runs-on: ubuntu-latest
needs: [analyze-pr, label-pr, comment-on-pr]
if: always()
steps:
- name: Update status
uses: actions/github-script@v7
with:
script: |
const prType = '${{ needs.analyze-pr.outputs.pr_type }}';
const isWIP = '${{ needs.analyze-pr.outputs.is_wip }}' === 'true';

console.log('PR Analysis Complete:');
console.log(' Type:', prType);
console.log(' WIP:', isWIP);
console.log(' Labels:', '${{ needs.label-pr.result }}');
console.log(' Comment:', '${{ needs.comment-on-pr.result }}');

// Update summary
core.summary
.addHeading('PR Handler Summary')
.addTable([
[{data: 'Property', header: true}, {data: 'Value', header: true}],
['PR Type', prType],
['Status', isWIP ? '🚧 Work in Progress' : '✅ Ready for Review'],
['Labels Applied', '${{ needs.label-pr.result }}'],
['Comment Added', '${{ needs.comment-on-pr.result }}']
])
.write();
Loading