diff --git a/.agents/skills/nemoclaw-maintainer-fix-e2e-failures/SKILL.md b/.agents/skills/nemoclaw-maintainer-fix-e2e-failures/SKILL.md index a8efcbee667..ad1dac282ec 100644 --- a/.agents/skills/nemoclaw-maintainer-fix-e2e-failures/SKILL.md +++ b/.agents/skills/nemoclaw-maintainer-fix-e2e-failures/SKILL.md @@ -16,7 +16,6 @@ Continuously inspect automatic E2E results for `main`. Coordinate ownership and 2. Keep release operations out of scope. Never change, retag, publish, or otherwise touch a release, tag, or release artifact during this workflow. Route release work to the existing release workflow. 3. Confirm maintainer authorization. Merge only when the request grants it. Otherwise, leave the PR `approval-ready` and continue the loop. 4. Check Git and GitHub access. Follow [Git and GitHub Access Hard Stop](../_shared/git-github-hard-stop.md) on access failure. -5. Fetch trusted `origin/main`. Read its PR-limit policy with `git show origin/main:.github/workflows/pr-limit.yaml`. For a non-exempt author, do not create a claim that would exceed that limit. Do not declare success or end the loop because the queue is empty or the newest run passes. Wait for the next automatic `main` result. diff --git a/.github/pr-limits.json b/.github/pr-limits.json new file mode 100644 index 00000000000..5c8ee415d7a --- /dev/null +++ b/.github/pr-limits.json @@ -0,0 +1,20 @@ +{ + "apurvvkumaria": "10", + "chengjiew": "10", + "cjagwani": "10", + "cv": "10", + "dongni-yang": "10", + "ericksoa": "10", + "jyaunches": "10", + "laitingsheng": "10", + "prekshivyas": "10", + "rluo8": "10", + "rsliter": "10", + "sandl99": "10", + "senthilr-nv": "10", + "tedy-y": "10", + "tonyluo-nv": "10", + "wscurran": "10", + "yimoj": "10", + "*": "5" +} diff --git a/.github/workflows/pr-limit.yaml b/.github/workflows/pr-limit.yaml index 99f67fc38bf..feec1991b19 100644 --- a/.github/workflows/pr-limit.yaml +++ b/.github/workflows/pr-limit.yaml @@ -3,49 +3,88 @@ name: Automation / PR Limit -# pull_request_target runs in the base repo context, giving the token write -# access even for fork PRs. This is safe here because this workflow never -# checks out or executes code from the PR — it only counts open PRs and -# closes excess ones. Do NOT add a checkout step or run PR-sourced code -# in this workflow to prevent injection attacks. +# pull_request_target runs in the base repository context. This workflow never +# checks out or executes pull request code. on: pull_request_target: types: [opened, reopened] permissions: + contents: read pull-requests: write jobs: check-pr-limit: runs-on: ubuntu-latest timeout-minutes: 5 + concurrency: + group: pr-limit-${{ github.repository }}-${{ github.event.pull_request.user.login }} + queue: max + cancel-in-progress: false steps: - name: Check open PR count for author - env: - GH_TOKEN: ${{ github.token }} - AUTHOR: ${{ github.event.pull_request.user.login }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - run: | - LIMIT=5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const pull = context.payload.pull_request; + const author = pull.user.login; + const response = await github.rest.repos.getContent({ + ...context.repo, + path: ".github/pr-limits.json", + ref: pull.base.sha, + }); + if (Array.isArray(response.data) || response.data.type !== "file") { + throw new Error("Invalid PR limit policy file"); + } + const policy = JSON.parse(Buffer.from(response.data.content, "base64").toString()); + if ( + typeof policy !== "object" || + policy === null || + Array.isArray(policy) || + !("*" in policy) || + !Object.entries(policy).every( + ([login, value]) => + (login === "*" || + /^(?=.{1,39}$)[a-z\d](?:(?:[a-z\d]|-(?!-))*[a-z\d])?$/.test(login)) && + typeof value === "string" && + /^(0|[1-9]\d*)$/.test(value) && + Number.isSafeInteger(Number(value)), + ) + ) { + throw new Error("Invalid PR limit policy values"); + } + const normalizedAuthor = author.toLowerCase(); + const limit = Number( + Object.hasOwn(policy, normalizedAuthor) ? policy[normalizedAuthor] : policy["*"], + ); + const issues = await github.paginate(github.rest.issues.listForRepo, { + ...context.repo, + state: "open", + creator: author, + sort: "created", + direction: "asc", + per_page: 100, + }); + const openCount = issues.filter( + (issue) => + issue.pull_request && + (context.payload.action === "reopened" || issue.number <= pull.number), + ).length; - # Core maintainers are exempt from the PR limit. - EXEMPT="ericksoa kjw3 jacobtomlinson cv jyaunches" - for user in $EXEMPT; do - if [ "$AUTHOR" = "$user" ]; then - echo "Author $AUTHOR is a core maintainer — exempt from PR limit" - exit 0 - fi - done + core.info(`Author ${author} has ${openCount} open PR(s); limit is ${limit}`); + if (openCount <= limit) return; - OPEN_COUNT=$(gh pr list --repo "$REPO" --author "$AUTHOR" --state open --json number --jq 'length') - - echo "Author $AUTHOR has $OPEN_COUNT open PR(s)" - - if [ "$OPEN_COUNT" -gt "$LIMIT" ]; then - gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ - "This repository limits contributors to $LIMIT open pull requests. Please close or merge existing PRs before opening new ones." - gh pr close "$PR_NUMBER" --repo "$REPO" - echo "::error::PR closed — author $AUTHOR exceeds the $LIMIT open PR limit" - exit 1 - fi + const body = limit === 0 + ? "This account cannot have open pull requests under the repository policy." + : `This repository limits you to ${limit} open pull requests. Please close or merge an existing PR before opening another one.`; + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pull.number, + body, + }); + await github.rest.pulls.update({ + ...context.repo, + pull_number: pull.number, + state: "closed", + }); + core.setFailed(`PR closed because ${author} exceeds the ${limit}-open-PR limit`); diff --git a/AGENTS.md b/AGENTS.md index 256a25cb165..168ccbbfc7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -344,4 +344,4 @@ If the command trace contains no reviewer-request write, report the event as an - Direct PRs follow `.github/PULL_REQUEST_TEMPLATE.md`; the managed documentation workflow uses its generated body - PRs that change `scripts/prepare-dgx-station-host.sh` must include reviewable DGX Station test evidence identifying the tested commit, Station profile or scenario, result, and a supporting link. Any maintainer may review the evidence; without acceptable evidence, the PR is not ready to approve or merge. Treat the evidence as human-reviewed, not authenticated hardware provenance. Exceptional bypasses use existing repository governance and must document the reason on the PR. - No secrets, API keys, or credentials committed -- Apply the open PR limit from `.github/workflows/pr-limit.yaml` only to accounts that the workflow does not exempt +- Check `.github/pr-limits.json` for the contributor's open PR limit. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 615f797bfb7..a320aad4e9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -511,8 +511,7 @@ For Markdown docs routing, user-skill guidance, and release-prep documentation w ## Pull Requests -We welcome contributions. Every PR requires maintainer review before merge. Contributors may have up to 5 open PRs at one time. -Core maintainers listed in `.github/workflows/pr-limit.yaml` are exempt from this limit. +We welcome contributions. Every PR requires maintainer review before merge. The default limit is 5 open PRs at one time. Maintainers review pull requests according to project priority, security impact, release readiness, and reviewer availability. PRs that solve issues with Priority set to Urgent or High are more likely to receive earlier review when maintainers have capacity. For substantial features or behavior changes, start with a GitHub Discussion before opening a large implementation PR. @@ -561,8 +560,9 @@ If the PR description is missing the DCO declaration, update the PR description If any commit is missing GitHub verification, fix the branch before opening a PR. If force-push is not allowed after an unverified commit is published, open a fresh branch and fresh PR with a clean compliant history. -> [!WARNING] -> Non-exempt accounts that repeatedly exceed this limit or submit automated bulk PRs may have their PRs closed or their access restricted. +When an opened or reopened PR exceeds the account's limit, automation comments on and closes that PR. + +> Accounts that submit automated bulk PRs may have their PRs closed or their access restricted. ### No External Project Links