mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-30 19:11:05 +00:00
* Add CONTRIBUTING.md Add a short contributor guide. It covers what every PR description should answer, what reviewers need for each kind of PR and which label it gets, rules that are easy to miss, using AI tools, what to do after opening a PR, and when a PR may be closed. The details for each kind of PR are on the wiki's How to Contribute page. AGENTS.md now tells agents to read CONTRIBUTING.md before they open a PR or post on GitHub. Agents load AGENTS.md automatically but not CONTRIBUTING.md. * Link the wiki's How to Contribute page for new contributors The label bot's comment greets first-time contributors and links the How to Contribute page. Other authors get the same comment as before, now with the missing period after the label commands. CONTRIBUTING.md links the wiki's Opening an Issue section, since GitHub shows CONTRIBUTING.md on the new issue page too.
386 lines
14 KiB
YAML
386 lines
14 KiB
YAML
name: PR Label Bot
|
|
|
|
on:
|
|
pull_request_target:
|
|
types:
|
|
- opened
|
|
- reopened
|
|
issue_comment:
|
|
types:
|
|
- created
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
jobs:
|
|
request-label:
|
|
if: github.event_name == 'pull_request_target' && github.event.pull_request.author_association != 'COLLABORATOR' && github.event.pull_request.author_association != 'OWNER' && github.event.pull_request.author_association != 'MEMBER'
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Ask PR author for label
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
function isPermissionDenied(error) {
|
|
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
|
}
|
|
|
|
const allowedLabels = [
|
|
// kind of change
|
|
'crash',
|
|
'bug-fix',
|
|
'SECURITY',
|
|
'enhancement',
|
|
'QoL',
|
|
'optimization',
|
|
// area
|
|
'UI/UX',
|
|
'profile',
|
|
'Localization',
|
|
// infrastructure
|
|
'build',
|
|
'test',
|
|
'dependencies',
|
|
'documentation'
|
|
];
|
|
const pr = context.payload.pull_request;
|
|
// First-time contributors also get a link to the contributing guide
|
|
const greeting = ['FIRST_TIME_CONTRIBUTOR', 'FIRST_TIMER'].includes(pr.author_association)
|
|
? `Hi @${pr.user.login}, thanks for contributing to OrcaSlicer! ` +
|
|
`[How to Contribute](https://www.orcaslicer.com/wiki/developer_reference/how_to_contribute) explains what reviewers need from a PR.\n\n` +
|
|
`You can`
|
|
: `Hi @${pr.user.login}, you can`;
|
|
const labelsList = `${allowedLabels
|
|
.slice(0, -1)
|
|
.map((label) => `\`${label}\``)
|
|
.join(', ')} and \`${allowedLabels[allowedLabels.length - 1]}\`.`;
|
|
const examplesText = [
|
|
'```',
|
|
'/bot add-label bug-fix',
|
|
'```',
|
|
'```',
|
|
'/bot add-label bug-fix, UI/UX',
|
|
'```',
|
|
'```',
|
|
'/bot remove-label bug-fix, UI/UX',
|
|
'```'
|
|
].join('\n');
|
|
|
|
try {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
body:
|
|
`${greeting} manage the labels for this PR by \`/bot add-label\` and \`/bot remove-label\`.\n\n` +
|
|
`Allowed labels are:\n${labelsList}\n\n` +
|
|
`Examples:\n${examplesText}`
|
|
});
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning(
|
|
'Skipping PR comment because token cannot write. Enable Actions write permissions, ' +
|
|
'or run with a token that has issues:write and pull_requests:write.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
localization-pr:
|
|
if: github.event_name == 'pull_request_target'
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Auto-label localization changes and remind contributors about the glossary
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
function isPermissionDenied(error) {
|
|
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
|
}
|
|
|
|
const pr = context.payload.pull_request;
|
|
|
|
// Mirrors the `localization/**` paths filter in check_locale.yml
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: pr.number,
|
|
per_page: 100
|
|
});
|
|
if (!files.some((file) => file.filename.startsWith('localization/'))) {
|
|
core.info('No localization changes detected; skipping Localization label.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
labels: ['Localization']
|
|
});
|
|
core.info('Applied Localization label.');
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning('Cannot add Localization label because token cannot write.');
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Collaborators know the glossary; only remind outside contributors
|
|
if (['COLLABORATOR', 'OWNER', 'MEMBER'].includes(pr.author_association)) {
|
|
core.info('Author is a collaborator; skipping glossary reminder.');
|
|
return;
|
|
}
|
|
|
|
// Avoid posting the reminder twice (e.g. on reopen)
|
|
const marker = '<!-- localization-glossary-reminder -->';
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
per_page: 100
|
|
});
|
|
if (comments.some((comment) => (comment.body || '').includes(marker))) {
|
|
core.info('Glossary reminder already present; skipping.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: pr.number,
|
|
body:
|
|
`${marker}\n` +
|
|
`Hi @${pr.user.login}, this PR changes translations (\`localization/**\`).\n\n` +
|
|
`Please make sure recurring terms follow the [Localization glossary](https://www.orcaslicer.com/wiki/localization_glossary), ` +
|
|
`so the same English term is always rendered the same way within a language and terms that must stay in English ` +
|
|
`(brand/product names, acronyms, file formats, G-code, macros/variables) are not translated.`
|
|
});
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning('Skipping glossary reminder because token cannot write comments.');
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
apply-label:
|
|
if: github.event_name == 'issue_comment'
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Apply label command from PR author
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
function isPermissionDenied(error) {
|
|
return error && error.status === 403 && /Resource not accessible by integration/i.test(error.message || '');
|
|
}
|
|
|
|
const allowedLabels = [
|
|
// kind of change
|
|
'crash',
|
|
'bug-fix',
|
|
'SECURITY',
|
|
'enhancement',
|
|
'QoL',
|
|
'optimization',
|
|
// area
|
|
'UI/UX',
|
|
'profile',
|
|
'Localization',
|
|
// infrastructure
|
|
'build',
|
|
'test',
|
|
'dependencies',
|
|
'documentation'
|
|
];
|
|
|
|
const issue = context.payload.issue;
|
|
if (!issue.pull_request) {
|
|
core.info('Ignoring comment that is not on a pull request.');
|
|
return;
|
|
}
|
|
|
|
const body = (context.payload.comment.body || '').trim();
|
|
const commandLine = body
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.find((line) => /^\/bot\s+(add-label|remove-label)(?:\s*:\s*|\s+)/i.test(line));
|
|
|
|
if (!commandLine) {
|
|
core.info('No /bot add-label or /bot remove-label command found.');
|
|
return;
|
|
}
|
|
|
|
const commandMatch = commandLine.match(/^\/bot\s+(add-label|remove-label)(?:\s*:\s*|\s+)(.+)\s*$/i);
|
|
if (!commandMatch) {
|
|
core.info('Label command format is invalid.');
|
|
return;
|
|
}
|
|
|
|
const action = commandMatch[1].toLowerCase() === 'add-label' ? 'add' : 'remove';
|
|
let labelsExpr = (commandMatch[2] || '').trim();
|
|
if (!labelsExpr) {
|
|
core.info('Label command is missing label name.');
|
|
return;
|
|
}
|
|
|
|
if (labelsExpr.startsWith('[') && labelsExpr.endsWith(']')) {
|
|
labelsExpr = labelsExpr.slice(1, -1).trim();
|
|
}
|
|
|
|
const requestedRawLabels = labelsExpr
|
|
.split(',')
|
|
.map((part) => part.trim())
|
|
.filter(Boolean);
|
|
|
|
if (!requestedRawLabels.length) {
|
|
core.info('No labels were provided in the command.');
|
|
return;
|
|
}
|
|
|
|
const { data: pr } = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: issue.number
|
|
});
|
|
|
|
const commenter = context.payload.comment.user.login;
|
|
if (commenter !== pr.user.login) {
|
|
core.info('Ignoring command because commenter is not the PR author.');
|
|
return;
|
|
}
|
|
|
|
const labelsByLower = new Map(
|
|
allowedLabels.map((label) => [label.toLowerCase(), label])
|
|
);
|
|
|
|
const resolvedLabels = [];
|
|
const invalidLabels = [];
|
|
|
|
for (const rawLabel of requestedRawLabels) {
|
|
const resolved = labelsByLower.get(rawLabel.toLowerCase());
|
|
if (resolved) {
|
|
resolvedLabels.push(resolved);
|
|
} else {
|
|
invalidLabels.push(rawLabel);
|
|
}
|
|
}
|
|
const uniqueRequestedLabels = [...new Set(resolvedLabels)];
|
|
|
|
if (action === 'add' && uniqueRequestedLabels.length) {
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
labels: uniqueRequestedLabels
|
|
});
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning(
|
|
'Cannot add labels because token cannot write. Enable Actions write permissions, ' +
|
|
'or run with a token that has issues:write and pull_requests:write.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
core.info(`Added labels: ${uniqueRequestedLabels.join(', ')}`);
|
|
}
|
|
|
|
if (action === 'remove' && uniqueRequestedLabels.length) {
|
|
let removedCount = 0;
|
|
try {
|
|
for (const label of uniqueRequestedLabels) {
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
name: label
|
|
});
|
|
removedCount += 1;
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning(
|
|
'Cannot remove labels because token cannot write. Enable Actions write permissions, ' +
|
|
'or run with a token that has issues:write and pull_requests:write.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (error.status === 404) {
|
|
core.info(`Label is not currently applied: ${label}`);
|
|
continue;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
core.info(`Removed labels count: ${removedCount}`);
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!uniqueRequestedLabels.length) {
|
|
core.info('No valid labels were provided in the command.');
|
|
}
|
|
|
|
if (invalidLabels.length) {
|
|
const allowedText = allowedLabels.map((label) => `\`${label}\``).join(', ');
|
|
const invalidText = invalidLabels.map((label) => `\`${label}\``).join(', ');
|
|
const validText = uniqueRequestedLabels.length
|
|
? `\n\nProcessed valid label(s): ${uniqueRequestedLabels.map((label) => `\`${label}\``).join(', ')}`
|
|
: '';
|
|
|
|
try {
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
body:
|
|
`@${commenter} invalid label(s): ${invalidText}.${validText}\n\n` +
|
|
`Allowed labels: ${allowedText}\n\n` +
|
|
`Use:\n` +
|
|
`- \`/bot add-label label\`\n` +
|
|
`- \`/bot remove-label label\`\n` +
|
|
`- \`/bot add-label label1, label2\``
|
|
});
|
|
} catch (error) {
|
|
if (isPermissionDenied(error)) {
|
|
core.warning('Cannot post invalid-label feedback because token cannot write comments.');
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const processedLabelsText = uniqueRequestedLabels.length ? uniqueRequestedLabels.join(', ') : '(none)';
|
|
core.info(`Processed label command: ${action} ${processedLabelsText}`);
|