mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
chore: split external contribution tagger into issue and PR workflows
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# Maintainer override: an org member can reopen the PR or remove
|
||||
# "missing-issue-link" — both add "bypass-issue-check" and reopen.
|
||||
#
|
||||
# Dependency: tag-external-contributions.yml must apply the "external" label
|
||||
# Dependency: tag-external-prs.yml must apply the "external" label
|
||||
# first. This workflow does NOT trigger on "opened" (new PRs have no labels
|
||||
# yet, so the gate would always skip).
|
||||
|
||||
|
||||
+21
-61
@@ -1,10 +1,10 @@
|
||||
# Automatically tag issues and pull requests as "external" or "internal"
|
||||
# based on whether the author is a member of the langchain-ai GitHub
|
||||
# organization, and apply contributor tier labels to external contributors
|
||||
# based on their merged PR history.
|
||||
# Automatically tag issues as "external" or "internal" based on whether
|
||||
# the author is a member of the langchain-ai GitHub organization, and
|
||||
# apply contributor tier labels to external contributors based on their
|
||||
# merged PR history.
|
||||
#
|
||||
# NOTE: This repo does not have a separate pr_labeler.yml — this workflow
|
||||
# handles both issues and PRs.
|
||||
# PR labeling is handled by tag-external-prs.yml.
|
||||
# PR + issue backfill lives in the backfill job below (workflow_dispatch).
|
||||
#
|
||||
# Setup Requirements:
|
||||
# 1. Create a GitHub App with permissions:
|
||||
@@ -18,13 +18,11 @@
|
||||
# The GitHub App token is required to check private organization membership.
|
||||
# Without it, the workflow will fail.
|
||||
|
||||
name: Tag External Contributions
|
||||
name: Tag External Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
backfill_type:
|
||||
@@ -44,16 +42,15 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tag-external:
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
@@ -103,24 +100,16 @@ 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).
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
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 issue = context.payload.issue;
|
||||
const author = issue.user.login;
|
||||
const issueNumber = issue.number;
|
||||
|
||||
const TRUSTED_THRESHOLD = 5;
|
||||
const LABEL_COLOR = 'b76e79';
|
||||
@@ -164,8 +153,8 @@ jobs:
|
||||
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'
|
||||
- name: Add external label
|
||||
if: steps.check-membership.outputs.is-external == 'true'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -177,24 +166,8 @@ jobs:
|
||||
});
|
||||
console.log(`Added 'external' label to issue #${issue_number}`);
|
||||
|
||||
- name: Add external label to pull request
|
||||
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
# Use App token so the "labeled" event propagates to downstream
|
||||
# workflows (e.g. require_issue_link.yml). Events created by the
|
||||
# default GITHUB_TOKEN do not trigger additional workflow runs.
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: ['external'],
|
||||
});
|
||||
console.log(`Added 'external' label to PR #${issue_number}`);
|
||||
|
||||
- name: Add internal label to issue
|
||||
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'issues'
|
||||
- name: Add internal label
|
||||
if: steps.check-membership.outputs.is-external == 'false'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -206,19 +179,6 @@ jobs:
|
||||
});
|
||||
console.log(`Added 'internal' label to issue #${issue_number}`);
|
||||
|
||||
- name: Add internal label to pull request
|
||||
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: ['internal'],
|
||||
});
|
||||
console.log(`Added 'internal' label to PR #${issue_number}`);
|
||||
|
||||
backfill:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -254,7 +214,7 @@ jobs:
|
||||
|
||||
const tierLabels = ['trusted-contributor'];
|
||||
|
||||
// ── Helpers (inlined from pr-labeler.js) ─────────────────────────
|
||||
// ── Helpers ─────────────────────────────────────────────────
|
||||
|
||||
async function ensureLabel(name) {
|
||||
try {
|
||||
@@ -321,7 +281,7 @@ jobs:
|
||||
return info;
|
||||
}
|
||||
|
||||
// ── Setup ────────────────────────────────────────────────────────
|
||||
// ── Setup ────────────────────────────────────────────────────
|
||||
|
||||
for (const name of tierLabels) {
|
||||
await ensureLabel(name);
|
||||
@@ -332,7 +292,7 @@ jobs:
|
||||
let processed = 0;
|
||||
let failures = 0;
|
||||
|
||||
// ── Backfill PRs ─────────────────────────────────────────────────
|
||||
// ── Backfill PRs ─────────────────────────────────────────────
|
||||
|
||||
if (backfillType === 'prs' || backfillType === 'both') {
|
||||
const prs = await github.paginate(github.rest.pulls.list, {
|
||||
@@ -385,7 +345,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Backfill issues ──────────────────────────────────────────────
|
||||
// ── Backfill issues ──────────────────────────────────────────
|
||||
|
||||
if (backfillType === 'issues' || backfillType === 'both') {
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
@@ -0,0 +1,173 @@
|
||||
# Automatically tag pull requests as "external" or "internal" based on
|
||||
# whether the author is a member of the langchain-ai GitHub organization,
|
||||
# and apply contributor tier labels to external contributors based on
|
||||
# their merged PR history.
|
||||
#
|
||||
# Issue labeling is handled by tag-external-issues.yml.
|
||||
# Backfill (workflow_dispatch) also lives in tag-external-issues.yml.
|
||||
#
|
||||
# Setup Requirements:
|
||||
# 1. Create a GitHub App with permissions:
|
||||
# - Repository: Pull requests (write)
|
||||
# - Organization: Members (read)
|
||||
# 2. Install the app on your organization and this repository
|
||||
# 3. Add these repository secrets:
|
||||
# - ORG_MEMBERSHIP_APP_ID: Your app's ID
|
||||
# - ORG_MEMBERSHIP_APP_PRIVATE_KEY: Your app's private key
|
||||
#
|
||||
# The GitHub App token is required to check private organization membership.
|
||||
# Without it, the workflow will fail.
|
||||
|
||||
name: Tag External PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tag-external:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
|
||||
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Check if contributor is external
|
||||
if: steps.app-token.outcome == 'success'
|
||||
id: check-membership
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const author = context.payload.sender.login;
|
||||
const senderType = context.payload.sender.type;
|
||||
|
||||
if (senderType === 'Bot') {
|
||||
console.log(`${author} is a Bot — treating as internal`);
|
||||
core.setOutput('is-external', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const membership = await github.rest.orgs.getMembershipForUser({
|
||||
org: 'langchain-ai',
|
||||
username: author,
|
||||
});
|
||||
const isExternal = membership.data.state !== 'active';
|
||||
console.log(
|
||||
isExternal
|
||||
? `${author} has pending membership — treating as external`
|
||||
: `${author} is an active member of langchain-ai`,
|
||||
);
|
||||
core.setOutput('is-external', isExternal ? 'true' : 'false');
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log(`${author} is not a member of langchain-ai`);
|
||||
core.setOutput('is-external', 'true');
|
||||
} else {
|
||||
throw new Error(
|
||||
`Membership check failed for ${author} (${e.status}): ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# Apply tier label BEFORE the external label 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).
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
const author = pr.user.login;
|
||||
const prNumber = pr.number;
|
||||
|
||||
const TRUSTED_THRESHOLD = 5;
|
||||
const LABEL_COLOR = 'b76e79';
|
||||
|
||||
let mergedCount;
|
||||
try {
|
||||
const result = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${owner}/${repo} is:pr is:merged author:"${author}"`,
|
||||
per_page: 1,
|
||||
});
|
||||
mergedCount = result?.data?.total_count;
|
||||
} catch (error) {
|
||||
if (error?.status !== 422) throw error;
|
||||
core.warning(`Search failed for ${author}; skipping tier label.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergedCount == null) {
|
||||
core.warning(`Search response missing total_count for ${author}; skipping tier label.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const tierLabel = mergedCount >= TRUSTED_THRESHOLD ? 'trusted-contributor' : null;
|
||||
|
||||
if (tierLabel) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: tierLabel });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
try {
|
||||
await github.rest.issues.createLabel({ owner, repo, name: tierLabel, color: LABEL_COLOR });
|
||||
} catch (createErr) {
|
||||
if (createErr.status !== 422) throw createErr;
|
||||
}
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: prNumber, labels: [tierLabel],
|
||||
});
|
||||
console.log(`Applied '${tierLabel}' to PR #${prNumber} (${mergedCount} merged PRs)`);
|
||||
} else {
|
||||
console.log(`No tier label for ${author} (${mergedCount} merged PRs)`);
|
||||
}
|
||||
|
||||
- name: Add external 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). Events created by the
|
||||
# default GITHUB_TOKEN do not trigger additional workflow runs.
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: ['external'],
|
||||
});
|
||||
console.log(`Added 'external' label to PR #${issue_number}`);
|
||||
|
||||
- name: Add internal label
|
||||
if: steps.check-membership.outputs.is-external == 'false'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: ['internal'],
|
||||
});
|
||||
console.log(`Added 'internal' label to PR #${issue_number}`);
|
||||
Reference in New Issue
Block a user