mirror of
https://github.com/hwchase17/langchain.git
synced 2026-03-18 02:53:16 +00:00
Enforce that all external PRs reference an approved issue via GitHub auto-close keywords (`Fixes #NNN`, `Closes #NNN`, `Resolves #NNN`). This replaces the previous AI-disclaimer policy in the PR template with a stricter requirement: external contributors must link to a maintainer-approved issue before their PR can merge. ## Changes - Add `require_issue_link.yml` workflow that chains off the `external` label applied by `tag-external-contributions.yml` — listens for `labeled`, `edited`, and `reopened` events to avoid duplicating the org membership API call - Scan PR body with a case-insensitive regex matching all conjugations of `close/fix/resolve` + `#NNN`; fail the check and post a deduplicated comment (via `<!-- require-issue-link -->` HTML marker) when no link is found - Apply a `missing-issue-link` label on failure, remove it on pass — enables bulk cleanup via label filter - Add `workflow_dispatch` backfill job to `pr_size_labeler.yml` for retroactively applying size labels to open PRs - Quote `author` in GitHub search queries in `tag-external-contributions.yml` to prevent mismatches on usernames with special characters - Update `PULL_REQUEST_TEMPLATE.md` to replace the AI-disclaimer guideline with the new issue-link requirement > [!NOTE] > `require_issue_link.yml` depends on `tag-external-contributions.yml` running first to apply the `external` label. Deploy as a non-required check initially, then promote to required after validation.
175 lines
6.3 KiB
YAML
175 lines
6.3 KiB
YAML
# Label PRs by size (changed lines, excluding lockfiles and docs).
|
|
#
|
|
# Size thresholds:
|
|
# XS: < 50, S: < 200, M: < 500, L: < 1000, XL: >= 1000
|
|
|
|
name: "📏 PR Size Labeler"
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, synchronize, reopened]
|
|
workflow_dispatch:
|
|
inputs:
|
|
max_items:
|
|
description: "Maximum number of open PRs to process"
|
|
default: "100"
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
size-label:
|
|
if: github.event_name != 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
steps:
|
|
- name: Apply PR size label
|
|
uses: actions/github-script@v8
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const pullRequest = context.payload.pull_request;
|
|
if (!pullRequest) return;
|
|
|
|
const sizeLabels = ['size: XS', 'size: S', 'size: M', 'size: L', 'size: XL'];
|
|
const labelColor = 'b76e79';
|
|
|
|
// Ensure labels exist
|
|
for (const name of sizeLabels) {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name });
|
|
} catch (error) {
|
|
if (error?.status !== 404) throw error;
|
|
await github.rest.issues.createLabel({
|
|
owner, repo, name, color: labelColor,
|
|
});
|
|
}
|
|
}
|
|
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner, repo, pull_number: pullRequest.number, per_page: 100,
|
|
});
|
|
|
|
const excludedFiles = new Set(['poetry.lock', 'uv.lock']);
|
|
const totalChangedLines = files.reduce((total, file) => {
|
|
const path = file.filename ?? '';
|
|
if (path.startsWith('docs/') || excludedFiles.has(path)) return total;
|
|
return total + (file.additions ?? 0) + (file.deletions ?? 0);
|
|
}, 0);
|
|
|
|
let targetSizeLabel = 'size: XL';
|
|
if (totalChangedLines < 50) targetSizeLabel = 'size: XS';
|
|
else if (totalChangedLines < 200) targetSizeLabel = 'size: S';
|
|
else if (totalChangedLines < 500) targetSizeLabel = 'size: M';
|
|
else if (totalChangedLines < 1000) targetSizeLabel = 'size: L';
|
|
|
|
// Remove stale size labels
|
|
const currentLabels = await github.paginate(
|
|
github.rest.issues.listLabelsOnIssue,
|
|
{ owner, repo, issue_number: pullRequest.number, per_page: 100 },
|
|
);
|
|
for (const label of currentLabels) {
|
|
const name = label.name ?? '';
|
|
if (sizeLabels.includes(name) && name !== targetSizeLabel) {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: pullRequest.number, name,
|
|
});
|
|
}
|
|
}
|
|
|
|
await github.rest.issues.addLabels({
|
|
owner, repo, issue_number: pullRequest.number, labels: [targetSizeLabel],
|
|
});
|
|
|
|
console.log(`PR #${pullRequest.number}: ${totalChangedLines} changed lines → ${targetSizeLabel}`);
|
|
|
|
backfill:
|
|
if: github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
steps:
|
|
- name: Backfill size labels on open PRs
|
|
uses: actions/github-script@v8
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const maxItems = parseInt('${{ inputs.max_items }}') || 100;
|
|
|
|
const sizeLabels = ['size: XS', 'size: S', 'size: M', 'size: L', 'size: XL'];
|
|
const labelColor = 'b76e79';
|
|
|
|
// Ensure labels exist
|
|
for (const name of sizeLabels) {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name });
|
|
} catch (error) {
|
|
if (error?.status !== 404) throw error;
|
|
await github.rest.issues.createLabel({
|
|
owner, repo, name, color: labelColor,
|
|
});
|
|
console.log(`Created label: ${name}`);
|
|
}
|
|
}
|
|
|
|
function getSizeLabel(totalChangedLines) {
|
|
if (totalChangedLines < 50) return 'size: XS';
|
|
if (totalChangedLines < 200) return 'size: S';
|
|
if (totalChangedLines < 500) return 'size: M';
|
|
if (totalChangedLines < 1000) return 'size: L';
|
|
return 'size: XL';
|
|
}
|
|
|
|
const prs = await github.paginate(github.rest.pulls.list, {
|
|
owner, repo, state: 'open', per_page: 100,
|
|
});
|
|
|
|
let processed = 0;
|
|
for (const pr of prs) {
|
|
if (processed >= maxItems) break;
|
|
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner, repo, pull_number: pr.number, per_page: 100,
|
|
});
|
|
|
|
const excludedFiles = new Set(['poetry.lock', 'uv.lock']);
|
|
const totalChangedLines = files.reduce((total, file) => {
|
|
const path = file.filename ?? '';
|
|
if (path.startsWith('docs/') || excludedFiles.has(path)) return total;
|
|
return total + (file.additions ?? 0) + (file.deletions ?? 0);
|
|
}, 0);
|
|
|
|
const targetSizeLabel = getSizeLabel(totalChangedLines);
|
|
|
|
// Remove stale size labels
|
|
const currentLabels = await github.paginate(
|
|
github.rest.issues.listLabelsOnIssue,
|
|
{ owner, repo, issue_number: pr.number, per_page: 100 },
|
|
);
|
|
for (const label of currentLabels) {
|
|
const name = label.name ?? '';
|
|
if (sizeLabels.includes(name) && name !== targetSizeLabel) {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: pr.number, name,
|
|
});
|
|
}
|
|
}
|
|
|
|
await github.rest.issues.addLabels({
|
|
owner, repo, issue_number: pr.number, labels: [targetSizeLabel],
|
|
});
|
|
|
|
console.log(`PR #${pr.number}: ${totalChangedLines} changed lines → ${targetSizeLabel}`);
|
|
processed++;
|
|
}
|
|
|
|
console.log(`\nBackfill complete. Processed ${processed} PRs.`);
|