mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 05:31:27 +00:00
fae6a92c9d
* fix: Allow label operations on pull requests in external plugin approval workflow The sync-merged-pr-labels job needs pull-requests: write permission to add/remove labels on merged PRs. Previously it only had issues: write which is for issues, not pull requests. This fixes the permission error when workflows try to modify PR labels from a non-contributor account. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: Handle 403 permission errors when creating external plugin intake labels When running on PRs from fork contributors, the GitHub token may not have permission to create labels in the repository. This is expected and should not cause the workflow to fail. Allow the ensureLabel function to gracefully handle 403 Forbidden errors in addition to 422 (label already exists) errors. This fixes the sync-pr-state job failure in external-plugin-pr-quality-gates.yml when run on PRs from external contributors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: Centralize label management into a single workflow_dispatch workflow Create a new 'setup-labels' workflow that is manually dispatched and handles all label creation and updates. This workflow: - Creates all labels used by the repository - Updates descriptions if labels already exist - Reports success/failure counts - Fails if any labels cannot be created All individual workflows now assume labels exist and will fail (loudly) if they don't. This makes it clear to maintainers when the setup-labels workflow needs to be dispatched: - label-pr-intent.yml - skill-check-comment.yml - external-plugin-approval-command.yml - external-plugin-command-router.yml - external-plugin-rereview.yml - external-plugin-rereview-command.yml - eng/external-plugin-intake-state.mjs This approach is better because: - Single source of truth for label definitions - Avoids permission issues with fork contributors - Clear failure modes when labels are missing - Easier to maintain consistent label configuration - No more scattered label creation logic across workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unused ensureLabel methods and managedLabels constants Labels are now centrally managed by the setup-labels workflow and assumed to exist in all other workflows. Removed: - ensureLabel() methods from all 6 workflows and 1 JS module - managedLabels constants that were only used by ensureLabel - Promise.all() calls that invoked ensureLabel for each label - Updated syncManagedLabels in skill-check-comment.yml to remove ensureLabel call All workflows now assume labels exist and will fail if they don't, which is the desired behavior—it signals maintainers to dispatch the setup-labels workflow when new labels need to be created. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
226 lines
8.1 KiB
YAML
226 lines
8.1 KiB
YAML
name: Skill Validator — PR Comment
|
||
|
||
# Posts results from the "Skill Validator — PR Gate" workflow.
|
||
# Runs with write permissions but never checks out PR code,
|
||
# so it is safe for fork PRs.
|
||
|
||
on:
|
||
workflow_run:
|
||
workflows: ["Skill Validator — PR Gate"]
|
||
types: [completed]
|
||
|
||
permissions:
|
||
issues: write
|
||
pull-requests: write
|
||
actions: read # needed to download artifacts
|
||
|
||
jobs:
|
||
comment:
|
||
runs-on: ubuntu-latest
|
||
if: github.event.workflow_run.event == 'pull_request'
|
||
steps:
|
||
- name: Download results artifact
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: skill-validator-results
|
||
run-id: ${{ github.event.workflow_run.id }}
|
||
github-token: ${{ github.token }}
|
||
|
||
- name: Post PR comment with results
|
||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const managedLabels = {
|
||
'skill-check-warning': {
|
||
color: 'FBCA04',
|
||
description: 'Skill validator reported warnings'
|
||
},
|
||
'skill-check-error': {
|
||
color: 'B60205',
|
||
description: 'Skill validator reported errors'
|
||
}
|
||
};
|
||
|
||
async function syncManagedLabels(issueNumber, desiredLabels) {
|
||
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: issueNumber,
|
||
per_page: 100
|
||
});
|
||
|
||
const currentManagedLabels = currentLabels
|
||
.map((label) => label.name)
|
||
.filter((name) => Object.prototype.hasOwnProperty.call(managedLabels, name));
|
||
|
||
const labelsToAdd = [...desiredLabels].filter((name) => !currentManagedLabels.includes(name));
|
||
const labelsToRemove = currentManagedLabels.filter((name) => !desiredLabels.has(name));
|
||
|
||
if (labelsToAdd.length > 0) {
|
||
await github.rest.issues.addLabels({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: issueNumber,
|
||
labels: labelsToAdd
|
||
});
|
||
}
|
||
|
||
for (const name of labelsToRemove) {
|
||
await github.rest.issues.removeLabel({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: issueNumber,
|
||
name
|
||
});
|
||
}
|
||
|
||
console.log(`Managed skill check labels: ${[...desiredLabels].sort().join(', ') || 'none'}`);
|
||
}
|
||
|
||
const prNumber = parseInt(fs.readFileSync('pr-number.txt', 'utf8').trim(), 10);
|
||
const total = parseInt(fs.readFileSync('total.txt', 'utf8').trim(), 10);
|
||
const exitCode = fs.readFileSync('exit-code.txt', 'utf8').trim();
|
||
const skillCount = parseInt(fs.readFileSync('skill-count.txt', 'utf8').trim(), 10);
|
||
const agentCount = parseInt(fs.readFileSync('agent-count.txt', 'utf8').trim(), 10);
|
||
const totalChecked = skillCount + agentCount;
|
||
|
||
const marker = '<!-- skill-validator-results -->';
|
||
const rawOutput = fs.existsSync('sv-output.txt')
|
||
? fs.readFileSync('sv-output.txt', 'utf8')
|
||
: '';
|
||
const output = rawOutput.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
||
|
||
const errorCount = (output.match(/❌/g) || []).length;
|
||
const warningCount = (output.match(/⚠/g) || []).length;
|
||
const advisoryCount = (output.match(/ℹ/g) || []).length;
|
||
const desiredLabels = new Set();
|
||
|
||
if (warningCount > 0) {
|
||
desiredLabels.add('skill-check-warning');
|
||
}
|
||
|
||
if (exitCode !== '0' || errorCount > 0) {
|
||
desiredLabels.add('skill-check-error');
|
||
}
|
||
|
||
await syncManagedLabels(prNumber, desiredLabels);
|
||
|
||
if (total === 0) {
|
||
console.log('No skills/agents were checked — skipping comment.');
|
||
return;
|
||
}
|
||
|
||
let verdict = '✅ All checks passed';
|
||
if (exitCode !== '0' || errorCount > 0) {
|
||
verdict = '⛔ Findings need attention';
|
||
} else if (warningCount > 0 || advisoryCount > 0) {
|
||
verdict = '⚠️ Warnings or advisories found';
|
||
}
|
||
|
||
const highlightedLines = output
|
||
.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(Boolean)
|
||
.filter(line => !line.startsWith('###'))
|
||
.filter(line => /^[❌⚠ℹ]/.test(line));
|
||
|
||
const summaryLines = highlightedLines.length > 0
|
||
? highlightedLines.slice(0, 10)
|
||
: output
|
||
.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(Boolean)
|
||
.filter(line => !line.startsWith('###'))
|
||
.slice(0, 10);
|
||
|
||
const scopeTable = [
|
||
'| Scope | Checked |',
|
||
'|---|---:|',
|
||
`| Skills | ${skillCount} |`,
|
||
`| Agents | ${agentCount} |`,
|
||
`| Total | ${totalChecked} |`,
|
||
];
|
||
|
||
const severityTable = [
|
||
'| Severity | Count |',
|
||
'|---|---:|',
|
||
`| ❌ Errors | ${errorCount} |`,
|
||
`| ⚠️ Warnings | ${warningCount} |`,
|
||
`| ℹ️ Advisories | ${advisoryCount} |`,
|
||
];
|
||
|
||
const findingsTable = summaryLines.length === 0
|
||
? ['_No findings were emitted by the validator._']
|
||
: [
|
||
'| Level | Finding |',
|
||
'|---|---|',
|
||
...summaryLines.map(line => {
|
||
const level = line.startsWith('❌')
|
||
? '❌'
|
||
: line.startsWith('⚠')
|
||
? '⚠️'
|
||
: line.startsWith('ℹ')
|
||
? 'ℹ️'
|
||
: (exitCode !== '0' ? '⛔' : 'ℹ️');
|
||
const text = line.replace(/^[❌⚠ℹ️\s]+/, '').replace(/\|/g, '\\|');
|
||
return `| ${level} | ${text} |`;
|
||
}),
|
||
];
|
||
|
||
const body = [
|
||
marker,
|
||
'## 🔍 Skill Validator Results',
|
||
'',
|
||
`**${verdict}**`,
|
||
'',
|
||
...scopeTable,
|
||
'',
|
||
...severityTable,
|
||
'',
|
||
'### Summary',
|
||
'',
|
||
...findingsTable,
|
||
'',
|
||
'<details>',
|
||
'<summary>Full validator output</summary>',
|
||
'',
|
||
'```text',
|
||
output || 'No validator output captured.',
|
||
'```',
|
||
'',
|
||
'</details>',
|
||
'',
|
||
exitCode !== '0'
|
||
? '> **Note:** The validator returned a non-zero exit code. Please review the findings above before merge.'
|
||
: '',
|
||
].join('\n');
|
||
|
||
// Find existing comment with our marker
|
||
const { data: comments } = await github.rest.issues.listComments({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: prNumber,
|
||
per_page: 100,
|
||
});
|
||
|
||
const existing = comments.find(c => c.body.includes(marker));
|
||
|
||
if (existing) {
|
||
await github.rest.issues.updateComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
comment_id: existing.id,
|
||
body,
|
||
});
|
||
console.log(`Updated existing comment ${existing.id}`);
|
||
} else {
|
||
await github.rest.issues.createComment({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
issue_number: prNumber,
|
||
body,
|
||
});
|
||
console.log('Created new PR comment');
|
||
}
|