Files
langchain/.github/workflows/close_unchecked_issues.yml
Mason Daugherty 3246caf831 ci(infra): add org guards to workflows and remove dead v0.3 docs workflow (#38665)
Adds `github.repository_owner == 'langchain-ai'` guards to 14 GitHub
Actions workflows that were missing them. Without these guards,
write-capable automation (issue/PR labeling, closing, commenting, PR
reopening, release publishing) and CI jobs fire on forks — causing
unwanted mutations, wasted runner minutes, and failed GitHub App token
generation on repos that don't have the required secrets.

Also removes `v03_api_doc_build.yml`, which pushed built docs to
`langchain-ai/langchain-api-docs-html` via `TOKEN_GITHUB_API_DOCS_HTML`
— a secret that isn't set on this repo. The workflow was dead code with
no consumers.

---

## What changed

**Write-capable workflows now guarded** (8 files):
- `auto-label-by-package` — was adding/removing issue labels on forks
- `close_unchecked_issues` — was closing issues and posting comments on
forks via GitHub App token
- `tag-external-issues` — both `tag-external` and `backfill` jobs were
generating App tokens and labeling issues on forks
- `reopen_on_assignment` — was reopening PRs and re-running workflows on
forks
- `require_issue_link` — was closing PRs, creating labels, posting
comments, and canceling workflow runs on forks
- `remove_waiting_on_author` — was removing labels on fork issues/PRs
- `pr_labeler` — was generating App tokens and adding/removing PR labels
on forks
- `pr_labeler_backfill` — same, on manual trigger from a fork

**Read-only CI workflows now guarded** (5 files):
- `check_diffs` — `build` and `check-release-options` jobs (downstream
jobs inherit the skip)
- `codspeed` — `build` job
- `check_agents_sync`, `check_versions`, `check_release_deps`

**Release workflow guarded** (1 file):
- `_release` — `build` job (all downstream jobs chain via `needs`, so
they inherit the skip). Previously relied only on `environment: Release`
approval and a `github.ref` check.

**Removed**:
- `v03_api_doc_build.yml` — dead workflow depending on unset
`TOKEN_GITHUB_API_DOCS_HTML` secret

## What was already guarded

`block_fork_main_prs`, `bump_uv_pin`, `check_extras_sync`,
`integration_tests`, `pr_lint_trailer`, `refresh_model_profiles` already
had the guard. `pr_lint` (read-only, `pull-requests: read`) and the
`_*.yml` reusable workflows (only run when called by a guarded parent)
were intentionally left unguarded.

## Release note

- GitHub Actions workflows now skip execution on forks via
`repository_owner == 'langchain-ai'` guards, preventing unwanted
issue/PR automation and CI runs outside the canonical repo.
- Removed the dead `v03_api_doc_build` workflow that depended on an
unset secret.

---

🤖 Generated with AI-agent involvement.
2026-07-04 21:30:23 -04:00

198 lines
8.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Auto-close issues that bypass or ignore the issue template checkboxes.
#
# GitHub issue forms enforce `required: true` checkboxes in the web UI,
# but the API bypasses form validation entirely — bots/scripts can open
# issues with every box unchecked or skip the template altogether.
#
# Rules:
# 0. No issue type -> close unless author is an org member
# 1. No checkboxes at all -> close unless author is an org member or bot
# 2. Checkboxes present but none checked -> close
# 3. "Submission checklist" section incomplete -> close
# 4. "Package (Required)" section has no selection -> close
#
# Org membership check reuses the shared helper from pr-labeler.js and
# the same GitHub App used by tag-external-issues.yml.
name: Close Unchecked Issues
on:
issues:
types: [opened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
check-boxes:
if: github.repository_owner == 'langchain-ai'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
client-id: ${{ secrets.ORG_MEMBERSHIP_APP_CLIENT_ID }}
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
- name: Validate issue checkboxes
if: steps.app-token.outcome == 'success'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
const body = context.payload.issue.body ?? '';
const allChecked = (body.match(/- \[x\]/gi) || []).length;
const allUnchecked = (body.match(/- \[ \]/g) || []).length;
const total = allChecked + allUnchecked;
// ── Helpers ─────────────────────────────────────────────────
// Extract checkboxes under a markdown H2/H3 heading.
// Returns { checked, unchecked } counts, or null if the
// section heading is not found in the body.
function parseSection(heading) {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Find the heading line
const headingRe = new RegExp(`^#{2,3}\\s+${escaped}\\s*$`, 'm');
const headingMatch = headingRe.exec(body);
if (!headingMatch) return null;
// Slice from after the heading to the next heading or end
const rest = body.slice(headingMatch.index + headingMatch[0].length);
const nextHeading = rest.search(/\n#{2,3}\s/);
const block = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
return {
checked: (block.match(/- \[x\]/gi) || []).length,
unchecked: (block.match(/- \[ \]/g) || []).length,
};
}
let _cachedMember;
async function isOrgMember() {
if (_cachedMember) return _cachedMember;
const { h } = require('./.github/scripts/pr-labeler.js')
.loadAndInit(github, owner, repo, core);
const author = context.payload.sender.login;
const { isExternal } = await h.checkMembership(
author, context.payload.sender.type,
);
_cachedMember = { internal: !isExternal, author };
return _cachedMember;
}
async function closeWithComment(lines) {
const templateUrl = `https://github.com/${owner}/${repo}/issues/new/choose`;
lines.push(
'',
`Please use one of the [issue templates](${templateUrl}).`,
);
// Post comment first so the author sees the reason even if
// the subsequent close call fails.
await github.rest.issues.createComment({
owner, repo, issue_number,
body: lines.join('\n'),
});
await github.rest.issues.update({
owner, repo, issue_number,
state: 'closed',
state_reason: 'not_planned',
});
}
// ── Rule 0: no issue type (API/CLI bypass) ──────────────────
// Issue types are set automatically when using web UI templates.
// External users cannot set issue types via the API (requires
// write/triage permissions), so a missing type reliably indicates
// programmatic submission.
if (!context.payload.issue.type) {
let membership;
try {
membership = await isOrgMember();
} catch (e) {
// Org membership check failed — skip Rule 0 and let
// Rules 1-4 handle validation via checkboxes.
core.warning(`Rule 0: org membership check failed, skipping: ${e.message}`);
}
if (membership?.internal) {
console.log(`No issue type, but ${membership.author} is internal — OK`);
} else if (membership) {
console.log(`No issue type and ${membership.author} is external — closing`);
await closeWithComment([
'This issue was automatically closed because it appears to have been submitted programmatically — issue types are automatically set when using the GitHub web interface, and this issue has none.',
'',
'We do not allow automated issue submission at this time.',
]);
return;
}
}
// ── Rule 1: no checkboxes at all ────────────────────────────
if (total === 0) {
const { internal, author } = await isOrgMember();
if (internal) {
console.log(`No checkboxes, but ${author} is internal — OK`);
return;
}
console.log(`No checkboxes and ${author} is external — closing`);
await closeWithComment([
'This issue was automatically closed because no issue template was used.',
]);
return;
}
// ── Rule 2: checkboxes present but none checked ─────────────
if (allChecked === 0) {
console.log(`${allUnchecked} checkbox(es) present, none checked — closing`);
await closeWithComment([
'This issue was automatically closed because none of the required checkboxes were checked. Please re-file using an issue template and complete the checklist.',
]);
return;
}
// ── Rules 34: parse sections for targeted feedback ─────────
const checklist = parseSection('Submission checklist');
const pkg = parseSection('Package (Required)');
console.log(`Section parse — checklist: ${JSON.stringify(checklist)}, pkg: ${JSON.stringify(pkg)}`);
const problems = [];
if (checklist && checklist.unchecked > 0) {
problems.push(
'the submission checklist is incomplete — please confirm you searched for duplicates, included a reproduction, etc.'
);
}
if (pkg !== null && pkg.checked === 0) {
problems.push(
'no package was selected (e.g. langchain-core, langchain, langgraph) — this helps us route the issue to the right team'
);
} else if (pkg === null) {
problems.push(
'the package selection is missing (e.g. langchain-core, langchain, langgraph) — this helps us route the issue to the right team'
);
}
if (problems.length === 0) {
console.log(`All section checks passed (${allChecked} checked) — OK`);
return;
}
console.log(`Closing — problems: ${problems.join('; ')}`);
await closeWithComment([
'Thanks for opening an issue! It was automatically closed because:',
'',
...problems.map(p => `- ${p}`),
]);