ci: bypass issue-link gate for trusted contributors (#35720)

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
This commit is contained in:
Mason Daugherty
2026-03-10 12:01:07 -04:00
committed by GitHub
parent 292d0bda86
commit cb50fed2bb
6 changed files with 116 additions and 51 deletions

View File

@@ -25,6 +25,7 @@
# * chore — other changes that don't modify source or test files
# * revert — reverts a previous commit
# * release — prepare a new release
# * hotfix — urgent fix
#
# Allowed Scope(s) (optional):
# core, langchain, langchain-classic, model-profiles,
@@ -83,6 +84,7 @@ jobs:
chore
revert
release
hotfix
scopes: |
core
langchain

View File

@@ -5,6 +5,8 @@
# - 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
@@ -28,10 +30,14 @@ permissions:
jobs:
check-issue-link:
# Run when the "external" label is added, or on edit/reopen if already labeled
# Run when the "external" label is added, or on edit/reopen if already labeled.
# Skip entirely when the PR already carries "trusted-contributor".
if: >-
(github.event.action == 'labeled' && github.event.label.name == 'external') ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'external'))
!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
@@ -42,6 +48,21 @@ jobs:
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)];
@@ -58,7 +79,6 @@ jobs:
core.setOutput('has-link', 'true');
// Check whether the PR author is assigned to at least one linked issue
const { owner, repo } = context.repo;
const prAuthor = context.payload.pull_request.user.login;
const issueNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
@@ -198,3 +218,35 @@ jobs:
? '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}`);
}

View File

@@ -97,6 +97,55 @@ jobs:
}
}
# Apply tier label BEFORE the external/internal labels so that
# "trusted-contributor" is already present when the "external" labeled
# event fires and triggers require_issue_link.yml.
- name: Apply contributor tier label
if: steps.check-membership.outputs.is-external == 'true'
uses: actions/github-script@v8
with:
# Use App token so the "labeled" event propagates to downstream
# workflows (e.g. require_issue_link.yml bypass-trusted-contributor).
github-token: ${{ steps.app-token.outputs.token }}
script: |
const { owner, repo } = context.repo;
const isPR = context.eventName === 'pull_request_target';
const item = isPR
? context.payload.pull_request
: context.payload.issue;
const author = item.user.login;
const issueNumber = item.number;
const TRUSTED_THRESHOLD = 5;
const mergedQuery = `repo:${owner}/${repo} is:pr is:merged author:"${author}"`;
let mergedCount = 0;
try {
const result = await github.rest.search.issuesAndPullRequests({
q: mergedQuery,
per_page: 1,
});
mergedCount = result?.data?.total_count ?? 0;
} catch (error) {
if (error?.status !== 422) throw error;
core.warning(`Search failed for ${author}; skipping tier label.`);
return;
}
const label = mergedCount >= TRUSTED_THRESHOLD ? 'trusted-contributor' : null;
if (label) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: [label],
});
console.log(`Applied '${label}' to #${issueNumber} (${mergedCount} merged PRs)`);
} else {
console.log(`No tier label for ${author} (${mergedCount} merged PRs)`);
}
- name: Add external label to issue
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'issues'
uses: actions/github-script@v8
@@ -172,50 +221,6 @@ jobs:
console.log(`Added 'internal' label to pull request #${pull_number}`);
- name: Apply contributor tier label
if: steps.check-membership.outputs.is-external == 'true'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const isPR = context.eventName === 'pull_request_target';
const item = isPR
? context.payload.pull_request
: context.payload.issue;
const author = item.user.login;
const issueNumber = item.number;
const TRUSTED_THRESHOLD = 5;
const mergedQuery = `repo:${owner}/${repo} is:pr is:merged author:"${author}"`;
let mergedCount = 0;
try {
const result = await github.rest.search.issuesAndPullRequests({
q: mergedQuery,
per_page: 1,
});
mergedCount = result?.data?.total_count ?? 0;
} catch (error) {
if (error?.status !== 422) throw error;
core.warning(`Search failed for ${author}; skipping tier label.`);
return;
}
const label = mergedCount >= TRUSTED_THRESHOLD ? 'trusted-contributor' : null;
if (label) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: [label],
});
console.log(`Applied '${label}' to #${issueNumber} (${mergedCount} merged PRs)`);
} else {
console.log(`No tier label for ${author} (${mergedCount} merged PRs)`);
}
backfill:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest