mirror of
https://github.com/hwchase17/langchain.git
synced 2026-03-18 02:53:16 +00:00
Bypass the issue-link requirement for external contributors who have earned the `trusted-contributor` tier label (>=5 merged PRs). Previously only PRs with the `internal` label skipped the gate, meaning repeat contributors still had to link an approved issue on every PR. Also includes minor template and linting tweaks for contributor experience. ## Changes - Add `trusted-contributor` bypass to the `check-issue-link` job condition in `require_issue_link.yml`, with a secondary live-label API fetch inside the script to cover the race where the `external` labeled event payload doesn't yet include the tier label - Add a `bypass-trusted-contributor` job in `require_issue_link.yml` that removes `missing-issue-link` and reopens the PR when the `trusted-contributor` label arrives after enforcement has already closed it - Reorder steps in `tag-external-contributions.yml` so the tier label is applied *before* the `external` label — eliminates the race window entirely since `trusted-contributor` is already on the PR when the downstream `labeled` event fires - Switch the tier-label step from `GITHUB_TOKEN` to the app token so the `trusted-contributor` labeled event propagates to downstream workflows - Add `hotfix` to allowed PR title types in `pr_lint.yml` - Promote the English language policy to a blockquote callout in issue and PR templates; add a "do not begin work without assignment" note to the feature request template
253 lines
11 KiB
YAML
253 lines
11 KiB
YAML
# Require external PRs to link to an approved issue or discussion using
|
|
# GitHub auto-close keywords (Fixes #NNN, Closes #NNN, Resolves #NNN),
|
|
# AND require that the PR author is assigned to the linked issue.
|
|
#
|
|
# - Reacts to the "external" label applied by tag-external-contributions.yml,
|
|
# avoiding a duplicate org membership check.
|
|
# - Also re-checks on PR edits/reopens for PRs that already have the label.
|
|
# - Bypasses the check for PRs with the "trusted-contributor" label, and
|
|
# automatically reopens/cleans up PRs that receive it after enforcement.
|
|
# - Validates the PR author is an assignee on at least one linked issue.
|
|
# - Adds a "missing-issue-link" label on failure; removes it on pass.
|
|
# - Automatically reopens PRs that were closed by this workflow once the
|
|
# check passes (e.g. author edits the body to add a valid issue link).
|
|
# - Posts a comment explaining the requirement on failure.
|
|
# - Deduplicates comments via an HTML marker so re-runs don't spam.
|
|
#
|
|
# Dependency: tag-external-contributions.yml must run first to apply the
|
|
# "external" label on new PRs. Both workflows trigger on pull_request_target
|
|
# opened events; this workflow additionally listens for the "labeled" event
|
|
# to chain off the external classification.
|
|
|
|
name: Require Issue Link
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, edited, reopened, labeled]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
check-issue-link:
|
|
# Run when the "external" label is added, or on edit/reopen if already labeled.
|
|
# Skip entirely when the PR already carries "trusted-contributor".
|
|
if: >-
|
|
!contains(github.event.pull_request.labels.*.name, 'trusted-contributor') &&
|
|
(
|
|
(github.event.action == 'labeled' && github.event.label.name == 'external') ||
|
|
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'external'))
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Check for issue link and assignee
|
|
id: check-link
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
|
|
// Fetch live labels to handle the race where "external" fires
|
|
// before "trusted-contributor" appears in the event payload.
|
|
const { data: liveLabels } = await github.rest.issues.listLabelsOnIssue({
|
|
owner, repo, issue_number: prNumber,
|
|
});
|
|
if (liveLabels.some(l => l.name === 'trusted-contributor')) {
|
|
console.log('PR has trusted-contributor label — bypassing issue link check');
|
|
core.setOutput('has-link', 'true');
|
|
core.setOutput('is-assigned', 'true');
|
|
return;
|
|
}
|
|
|
|
const body = context.payload.pull_request.body || '';
|
|
const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi;
|
|
const matches = [...body.matchAll(pattern)];
|
|
|
|
if (matches.length === 0) {
|
|
console.log('No issue link found in PR body');
|
|
core.setOutput('has-link', 'false');
|
|
core.setOutput('is-assigned', 'false');
|
|
return;
|
|
}
|
|
|
|
const issues = matches.map(m => `#${m[1]}`).join(', ');
|
|
console.log(`Found issue link(s): ${issues}`);
|
|
core.setOutput('has-link', 'true');
|
|
|
|
// Check whether the PR author is assigned to at least one linked issue
|
|
const prAuthor = context.payload.pull_request.user.login;
|
|
const issueNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
|
|
|
let assignedToAny = false;
|
|
for (const num of issueNumbers) {
|
|
try {
|
|
const { data: issue } = await github.rest.issues.get({
|
|
owner, repo, issue_number: num,
|
|
});
|
|
const assignees = issue.assignees.map(a => a.login.toLowerCase());
|
|
if (assignees.includes(prAuthor.toLowerCase())) {
|
|
console.log(`PR author "${prAuthor}" is assigned to #${num}`);
|
|
assignedToAny = true;
|
|
break;
|
|
} else {
|
|
console.log(`PR author "${prAuthor}" is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`);
|
|
}
|
|
} catch (error) {
|
|
console.log(`Could not fetch issue #${num}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
core.setOutput('is-assigned', assignedToAny ? 'true' : 'false');
|
|
|
|
- name: Add missing-issue-link label
|
|
if: steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true'
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
await github.rest.issues.addLabels({
|
|
owner, repo, issue_number: prNumber, labels: ['missing-issue-link'],
|
|
});
|
|
|
|
- name: Remove missing-issue-link label and reopen PR
|
|
if: steps.check-link.outputs.has-link == 'true' && steps.check-link.outputs.is-assigned == 'true'
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: prNumber, name: 'missing-issue-link',
|
|
});
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
|
|
// Reopen PR only if it was previously closed by this workflow
|
|
const labels = context.payload.pull_request.labels.map(l => l.name);
|
|
if (context.payload.pull_request.state === 'closed' && labels.includes('missing-issue-link')) {
|
|
await github.rest.pulls.update({
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
state: 'open',
|
|
});
|
|
console.log(`Reopened PR #${prNumber}`);
|
|
}
|
|
|
|
- name: Post comment, close PR, and fail
|
|
if: steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true'
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
const hasLink = '${{ steps.check-link.outputs.has-link }}' === 'true';
|
|
const isAssigned = '${{ steps.check-link.outputs.is-assigned }}' === 'true';
|
|
const marker = '<!-- require-issue-link -->';
|
|
|
|
let lines;
|
|
if (!hasLink) {
|
|
lines = [
|
|
marker,
|
|
'**This PR has been automatically closed** because it does not link to an approved issue.',
|
|
'',
|
|
'All external contributions must reference an approved issue or discussion. Please:',
|
|
'1. Find or [open an issue](https://github.com/' + owner + '/' + repo + '/issues/new/choose) describing the change',
|
|
'2. Wait for a maintainer to approve and assign you',
|
|
'3. Add `Fixes #<issue_number>`, `Closes #<issue_number>`, or `Resolves #<issue_number>` to your PR description and the PR will be reopened automatically',
|
|
];
|
|
} else {
|
|
lines = [
|
|
marker,
|
|
'**This PR has been automatically closed** because you are not assigned to the linked issue.',
|
|
'',
|
|
'External contributors must be assigned to an issue before opening a PR for it. Please:',
|
|
'1. Comment on the linked issue to request assignment from a maintainer',
|
|
'2. Once assigned, edit your PR description and the PR will be reopened automatically',
|
|
];
|
|
}
|
|
|
|
const body = lines.join('\n');
|
|
|
|
// Deduplicate: check for existing comment with the marker
|
|
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.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
body,
|
|
});
|
|
console.log('Posted requirement comment');
|
|
} else if (existing.body !== body) {
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existing.id,
|
|
body,
|
|
});
|
|
console.log('Updated existing comment with new message');
|
|
} else {
|
|
console.log('Comment already exists — skipping');
|
|
}
|
|
|
|
// Close the PR
|
|
if (context.payload.pull_request.state === 'open') {
|
|
await github.rest.pulls.update({
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
state: 'closed',
|
|
});
|
|
console.log(`Closed PR #${prNumber}`);
|
|
}
|
|
|
|
const reason = !hasLink
|
|
? 'PR must reference an issue using auto-close keywords (e.g., "Fixes #123").'
|
|
: 'PR author must be assigned to the linked issue.';
|
|
core.setFailed(reason);
|
|
|
|
# When a trusted-contributor label is added to a PR that was previously
|
|
# closed by check-issue-link, clean up and reopen it.
|
|
bypass-trusted-contributor:
|
|
if: github.event.action == 'labeled' && github.event.label.name == 'trusted-contributor'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Remove missing-issue-link label and reopen PR
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: prNumber, name: 'missing-issue-link',
|
|
});
|
|
console.log('Removed missing-issue-link label');
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
|
|
if (context.payload.pull_request.state === 'closed') {
|
|
await github.rest.pulls.update({
|
|
owner, repo, pull_number: prNumber, state: 'open',
|
|
});
|
|
console.log(`Reopened PR #${prNumber}`);
|
|
}
|