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
5 changes: 5 additions & 0 deletions .github/workflows/contributor-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ jobs:
github.actor != 'github-actions[bot]' &&
github.actor != 'copilot-swe-agent[bot]'
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
Expand Down
241 changes: 241 additions & 0 deletions .github/workflows/label-pr-intent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
name: Label PR Intent

on:
pull_request_target:
types: [opened, synchronize, reopened, edited, ready_for_review]

permissions:
issues: write
pull-requests: read

jobs:
label-pr:
runs-on: ubuntu-latest
if: >-
github.actor != 'dependabot[bot]' &&
github.actor != 'github-actions[bot]'
steps:
- name: Apply intent labels
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const managedLabels = {
'targets-main': {
color: 'B60205',
description: 'PR targets main instead of staged'
},
'branched-main': {
color: 'D93F0B',
description: 'PR appears to include plugin files materialized from main'
},
'skills': {
color: '1D76DB',
description: 'PR touches skills'
},
'plugin': {
color: '5319E7',
description: 'PR touches plugins'
},
'agent': {
color: '0E8A16',
description: 'PR touches agents'
},
'instructions': {
color: 'FBCA04',
description: 'PR touches instructions'
},
'new-submission': {
color: '006B75',
description: 'PR adds at least one new contribution'
},
'website-update': {
color: '0052CC',
description: 'PR touches website content or code'
},
'external-plugin': {
color: 'FEF2C0',
description: 'PR updates plugins/external.json'
},
'hooks': {
color: 'C2E0C6',
description: 'PR touches hooks'
},
'workflow': {
color: 'BFD4F2',
description: 'PR touches workflow automation'
}
};

const matchesAny = (filename, patterns) => patterns.some((pattern) => pattern.test(filename));

async function listAllFiles() {
const files = [];
let page = 1;

while (true) {
const response = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100,
page
});

files.push(...response.data);

if (response.data.length < 100) {
return files;
}

page += 1;
}
}

async function ensureLabel(name, { color, description }) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color,
description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
Comment thread
aaronpowell marked this conversation as resolved.
}

const files = await listAllFiles();
const filenames = files.map((file) => file.filename);

const patterns = {
branchedMain: [
/^plugins\/[^/]+\/(?:agents|commands|skills)\//
],
skills: [
/^skills\//
],
plugin: [
/^plugins\//
],
agent: [
/^agents\/.+\.agent\.md$/
],
instructions: [
/^instructions\/.+\.instructions\.md$/
],
websiteUpdate: [
/^website\//
],
externalPlugin: [
/^plugins\/external\.json$/
],
hooks: [
/^hooks\//
],
workflow: [
/^workflows\/.+\.md$/,
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/
],
newSubmission: [
/^agents\/.+\.agent\.md$/,
/^instructions\/.+\.instructions\.md$/,
/^skills\/[^/]+\/SKILL\.md$/,
/^hooks\/[^/]+\/(?:README\.md|hooks\.json)$/,
/^plugins\/[^/]+\/\.github\/plugin\/plugin\.json$/,
/^workflows\/.+\.md$/,
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/,
/^website\//
]
};

const isBranchedMain = filenames.some((filename) => matchesAny(filename, patterns.branchedMain));
const hasNewSubmission = files.some(
(file) => file.status === 'added' && matchesAny(file.filename, patterns.newSubmission)
);

const desiredLabels = new Set();

if (context.payload.pull_request.base.ref === 'main') {
desiredLabels.add('targets-main');
}

if (filenames.some((filename) => matchesAny(filename, patterns.externalPlugin))) {
desiredLabels.add('external-plugin');
}

if (isBranchedMain) {
desiredLabels.add('branched-main');
} else {
if (filenames.some((filename) => matchesAny(filename, patterns.skills))) {
desiredLabels.add('skills');
}

if (filenames.some((filename) => matchesAny(filename, patterns.plugin))) {
desiredLabels.add('plugin');
}

if (filenames.some((filename) => matchesAny(filename, patterns.agent))) {
desiredLabels.add('agent');
}

if (filenames.some((filename) => matchesAny(filename, patterns.instructions))) {
desiredLabels.add('instructions');
}

if (filenames.some((filename) => matchesAny(filename, patterns.websiteUpdate))) {
desiredLabels.add('website-update');
}

if (filenames.some((filename) => matchesAny(filename, patterns.hooks))) {
desiredLabels.add('hooks');
}

if (filenames.some((filename) => matchesAny(filename, patterns.workflow))) {
desiredLabels.add('workflow');
}

if (hasNewSubmission) {
desiredLabels.add('new-submission');
}
}

await Promise.all(
Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))
);

const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
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: context.issue.number,
labels: labelsToAdd
});
}

for (const name of labelsToRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name
});
}

core.info(`Managed labels: ${[...desiredLabels].sort().join(', ') || 'none'}`);
Loading