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
204 changes: 204 additions & 0 deletions .github/workflows/reusable-pr-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
name: Reusable — PR Labeler

on:
workflow_call:
inputs:
pr_number:
description: Pull request number
type: number
required: true
repo_owner:
description: Repository owner
type: string
required: true
repo_name:
description: Repository name
type: string
required: true

permissions: {}

jobs:
label:
name: Label PR size and detect dependency changes
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
steps:
- name: Apply size label and detect dependency changes
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const owner = '${{ inputs.repo_owner }}';
const repo = '${{ inputs.repo_name }}';
const prNumber = Number('${{ inputs.pr_number }}');

const LOCKFILES = new Set([
'go.sum', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock',
]);
const DEP_FILES = new Set([
'go.mod', 'go.sum', 'package.json', 'pnpm-lock.yaml', 'yarn.lock',
]);

// Fetch all changed files in the PR
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});

// Calculate net lines excluding lockfiles
let netLines = 0;
const depChanged = [];
for (const f of files) {
const basename = f.filename.split('/').pop();
if (!LOCKFILES.has(basename)) {
netLines += (f.additions || 0) + (f.deletions || 0);
}
if (DEP_FILES.has(basename)) {
depChanged.push(f.filename);
}
}

// Determine size tier
let sizeLabel;
if (netLines < 10) sizeLabel = 'size/XS';
else if (netLines < 50) sizeLabel = 'size/S';
else if (netLines < 200) sizeLabel = 'size/M';
else if (netLines < 500) sizeLabel = 'size/L';
else sizeLabel = 'size/XL';

// Ensure all size labels exist in the repo
const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'];
const labelColors = {
'size/XS': '3cbf00',
'size/S': '5d9801',
'size/M': 'e4c008',
'size/L': 'ee6d01',
'size/XL': 'e11d48',
};
for (const sl of sizeLabels) {
try {
await github.rest.issues.getLabel({ owner, repo, name: sl });
} catch {
try {
await github.rest.issues.createLabel({
owner,
repo,
name: sl,
color: labelColors[sl],
description: `PR size: ${sl.split('/')[1]}`,
});
} catch (e) {
core.debug(`Could not create label ${sl}: ${e.message}`);
}
}
}

// Remove stale size/* labels, then apply the current one
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: prNumber,
});
for (const l of currentLabels) {
if (l.name.startsWith('size/') && l.name !== sizeLabel) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: l.name,
}).catch(() => {});
}
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [sizeLabel],
});
core.info(`Applied label: ${sizeLabel} (${netLines} net lines, lockfiles excluded)`);

// Handle dependency file changes
// Remove stale label if dep files are no longer present (e.g. after a sync push)
if (depChanged.length === 0) {
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner, repo, issue_number: prNumber,
});
if (labels.some(l => l.name === 'dependencies-changed')) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: 'dependencies-changed',
}).catch(() => {});
}
}

if (depChanged.length > 0) {
// Ensure dependencies-changed label exists
try {
await github.rest.issues.getLabel({ owner, repo, name: 'dependencies-changed' });
} catch {
try {
await github.rest.issues.createLabel({
owner,
repo,
name: 'dependencies-changed',
color: 'f9d0c4',
description: 'This PR modifies dependency files',
});
} catch (e) {
core.debug(`Could not create dependencies-changed label: ${e.message}`);
}
}

await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['dependencies-changed'],
}).catch(() => {});

const marker = '<!-- octo-dep-change-bot -->';
const fileList = depChanged.map(f => `- \`${f}\``).join('\n');
const body = [
marker,
'### Dependency Changes Detected',
'',
'This PR modifies dependency files. Please review whether these changes are intentional.',
'',
'**Changed files:**',
fileList,
'',
'**Maintainer checklist:**',
'- [ ] Confirm dependency changes are intentional',
'- [ ] Review package delta if lockfile changed',
].join('\n');

// Update existing comment or create a new one
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
core.info('Updated existing dependency-change comment');
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
core.info('Created dependency-change comment');
}
core.info(`Dependency files changed: ${depChanged.join(', ')}`);
}
114 changes: 114 additions & 0 deletions .github/workflows/reusable-stale.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Reusable — Stale

on:
workflow_call:
inputs:
days_before_issue_stale:
description: Days before an issue is marked stale
type: number
default: 14
days_before_issue_close:
description: Days before a stale issue is closed
type: number
default: 7
days_before_pr_stale:
description: Days before a PR is marked stale
type: number
default: 14
days_before_pr_close:
description: Days before a stale PR is closed
type: number
default: 7

permissions: {}

jobs:
stale-unassigned:
name: Stale — unassigned issues and PRs
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
days-before-issue-stale: ${{ inputs.days_before_issue_stale }}
days-before-issue-close: ${{ inputs.days_before_issue_close }}
days-before-pr-stale: ${{ inputs.days_before_pr_stale }}
days-before-pr-close: ${{ inputs.days_before_pr_close }}
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale
exempt-all-assignees: true
operations-per-run: 500
ascending: true
remove-stale-when-updated: true
stale-issue-message: >
This issue has been automatically marked as stale due to inactivity.
Please add an update or it will be closed.
stale-pr-message: >
This pull request has been automatically marked as stale due to inactivity.
Please add an update or it will be closed.
close-issue-message: >
Closing due to inactivity.
If this is still an issue, please retry on the latest release and share updated details.
If you are sure it still happens, open a new issue with fresh reproduction steps.
close-issue-reason: not_planned
close-pr-message: >
Closing due to inactivity.
If you believe this PR should be revived, please open a new issue or contact a maintainer.

stale-assigned-issues:
name: Stale — assigned issues
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: maintainer,pinned,security,no-stale
include-only-assigned: true
operations-per-run: 500
ascending: true
remove-stale-when-updated: true
stale-issue-message: >
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add an update or it will be closed.
close-issue-message: >
Closing due to inactivity.
If this is still an issue, please retry on the latest release and share updated details.
close-issue-reason: not_planned

stale-assigned-prs:
name: Stale — assigned PRs
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 7
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale
include-only-assigned: true
ignore-pr-updates: true
operations-per-run: 500
ascending: true
remove-stale-when-updated: true
stale-pr-message: >
This assigned pull request has been automatically marked as stale after 27 days.
Please add an update or it will be closed.
close-pr-message: >
Closing due to inactivity.
If you believe this PR should be revived, please open a new issue or contact a maintainer.
Loading