1# Block PRs whose head ref is `main` (or `master`) from a fork. This topology2# (`<fork>:master -> langchain-ai/langchain:master`) lets contributors click3# "Update branch" on the PR, producing a `Merge branch 'master' into master`4# commit on the source side that — under admin merge override — can land5# directly on `master` as a 2-parent merge commit, bypassing the repo's6# squash-only policy and polluting the changelog.7#8# `pull_request_target` is required so the job receives a token scoped to9# write PR labels/comments on fork PRs (the standard `pull_request` token is10# read-only for forks). This also means the job MUST NOT check out PR code —11# see the inline warning in the trigger block below.12#13# Maintainer bypass: add the `bypass-fork-main-check` label to the PR.1415name: Block fork main PRs1617on:18 pull_request_target:19 # NEVER CHECK OUT UNTRUSTED CODE FROM A PR's HEAD IN A pull_request_target JOB.20 # Doing so would allow attackers to execute arbitrary code in the context of your repository.21 types: [opened, reopened, synchronize, labeled, unlabeled]2223permissions:24 contents: read2526jobs:27 guard:28 if: >-29 github.repository_owner == 'langchain-ai' &&30 github.event.pull_request.head.repo.fork == true &&31 (github.event.pull_request.head.ref == 'main' || github.event.pull_request.head.ref == 'master') &&32 !contains(github.event.pull_request.labels.*.name, 'bypass-fork-main-check')33 runs-on: ubuntu-latest34 permissions:35 pull-requests: write36 steps:37 - name: Close PR and post guidance38 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.039 with:40 script: |41 const { owner, repo } = context.repo;42 const prNumber = context.payload.pull_request.number;43 const headRef = context.payload.pull_request.head.ref;44 const marker = '<!-- block-fork-main -->';4546 // Ensure the warning label exists and apply it47 const labelName = 'fork-main-head';48 try {49 await github.rest.issues.getLabel({ owner, repo, name: labelName });50 } catch (e) {51 if (e.status !== 404) {52 throw new Error(`getLabel(${labelName}) failed: ${e.message}`);53 }54 try {55 await github.rest.issues.createLabel({56 owner, repo, name: labelName, color: 'b76e79',57 });58 } catch (createErr) {59 // A 422 with code `already_exists` means a race created the60 // label between getLabel and createLabel — safe to ignore.61 // Any other 422 (bad color, name too long) indicates a real62 // bug introduced by editing this step, so rethrow.63 const alreadyExists =64 createErr.status === 422 &&65 Array.isArray(createErr.errors) &&66 createErr.errors.some(e => e.code === 'already_exists');67 if (!alreadyExists) throw createErr;68 }69 }70 await github.rest.issues.addLabels({71 owner, repo, issue_number: prNumber, labels: [labelName],72 });7374 const defaultBranch = context.payload.repository.default_branch;75 const lines = [76 marker,77 `**This PR has been automatically closed** because its head branch is \`${headRef}\` on a fork.`,78 '',79 'PRs opened from a fork\'s `main` (or `master`) branch can produce a `Merge branch \'main\' into main` commit on the source side. Under an admin merge override that commit can land directly on this repo\'s default branch, bypassing the squash-only policy and polluting the changelog.',80 '',81 'To fix:',82 `1. Sync your fork's \`${defaultBranch}\` first (\`git fetch upstream && git switch ${defaultBranch} && git merge --ff-only upstream/${defaultBranch}\`)`,83 '2. Create a feature branch: `git switch -c feat/my-change`',84 '3. Push it: `git push -u origin feat/my-change`',85 `4. Open a new PR from \`feat/my-change\` → \`langchain-ai/langchain:${defaultBranch}\``,86 '',87 '*Maintainers: add the `bypass-fork-main-check` label to override.*',88 ];89 const body = lines.join('\n');9091 // Dedup: update existing marker comment instead of stacking.92 const comments = await github.paginate(93 github.rest.issues.listComments,94 { owner, repo, issue_number: prNumber, per_page: 100 },95 );96 const existing = comments.find(c => c.body && c.body.includes(marker));9798 if (!existing) {99 await github.rest.issues.createComment({100 owner, repo, issue_number: prNumber, body,101 });102 } else if (existing.body !== body) {103 await github.rest.issues.updateComment({104 owner, repo, comment_id: existing.id, body,105 });106 }107108 if (context.payload.pull_request.state === 'open') {109 await github.rest.pulls.update({110 owner, repo, pull_number: prNumber, state: 'closed',111 });112 }113114 // Cancel still-queued/in-progress checks on this PR head.115 // Best-effort: new runs may still queue after this loop (e.g., other116 // pull_request triggers fanning out). The PR is already closed above,117 // so leftover runs are wasted compute, not a correctness issue.118 // We track the cancel ratio so a wholesale failure (token-scope119 // regression making EVERY cancel return 403) is surfaced rather120 // than silently producing N warnings + green job.121 const headSha = context.payload.pull_request.head.sha;122 let attempted = 0;123 let cancelled = 0;124 for (const status of ['in_progress', 'queued']) {125 const runs = await github.paginate(126 github.rest.actions.listWorkflowRunsForRepo,127 { owner, repo, head_sha: headSha, status, per_page: 100 },128 );129 for (const run of runs) {130 if (run.id === context.runId) continue;131 attempted++;132 try {133 await github.rest.actions.cancelWorkflowRun({134 owner, repo, run_id: run.id,135 });136 cancelled++;137 } catch (err) {138 core.warning(`Could not cancel run ${run.id}: ${err.message}`);139 }140 }141 }142 if (attempted > 0 && cancelled === 0) {143 core.warning(`Attempted to cancel ${attempted} run(s) on head ${headSha} but none succeeded — check token scope.`);144 }145146 core.setFailed(`PR head ref is \`${headRef}\` on a fork — open from a feature branch instead.`);
Findings
✓ No findings reported for this file.