diff --git a/.github/workflows/tag-external-contributions.yml b/.github/workflows/tag-external-contributions.yml new file mode 100644 index 0000000000..be0401a8cc --- /dev/null +++ b/.github/workflows/tag-external-contributions.yml @@ -0,0 +1,109 @@ +# Automatically tag issues and pull requests as "external" +# when created by users who are not members of the langchain-ai +# GitHub organization. +# +# Setup Requirements: +# 1. Create a GitHub App with the following permissions: +# - Repository permissions: Issues (write), Pull requests (write) +# - Organization permissions: Members (read) +# 2. Install the GitHub App on the organization and repositories +# 3. Add the GitHub App's private key as ORG_MEMBERSHIP_TOKEN repository secret +# +# Alternatively, use a Personal Access Token (classic) with: +# - repo scope (for issues/PRs) +# - read:org scope (for organization membership) +# +# The workflow will fall back to GITHUB_TOKEN if ORG_MEMBERSHIP_TOKEN is not set, +# but this will only show public organization memberships. + +name: Tag External Contributions + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + +jobs: + tag-external: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + contents: read + + steps: + - name: Check if contributor is external + id: check-membership + uses: actions/github-script@v7 + with: + # Use a GitHub App token or PAT with org:read permissions to check private membership + github-token: ${{ secrets.ORG_MEMBERSHIP_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const author = context.payload.sender.login; + + try { + // Check if the author is a member of the langchain-ai organization + // This requires org:read permissions to see private memberships + const membership = await github.rest.orgs.getMembershipForUser({ + org: 'langchain-ai', + username: author + }); + + // Check if membership is active (not just pending invitation) + if (membership.data.state === 'active') { + console.log(`User ${author} is an active member of langchain-ai organization`); + core.setOutput('is-external', 'false'); + } else { + console.log(`User ${author} has pending membership in langchain-ai organization`); + core.setOutput('is-external', 'true'); + } + } catch (error) { + if (error.status === 404) { + console.log(`User ${author} is not a member of langchain-ai organization`); + core.setOutput('is-external', 'true'); + } else { + console.error('Error checking membership:', error); + console.log('Status:', error.status); + console.log('Message:', error.message); + // If we can't determine membership due to API error, assume external for safety + core.setOutput('is-external', 'true'); + } + } + + - name: Add external label to issue + if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'issues' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const issue_number = context.payload.issue.number; + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: ['external'] + }); + + 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@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pull_number, + labels: ['external'] + }); + + console.log(`Added 'external' label to pull request #${pull_number}`);