From c1401d8679b5ea7d6ace524ea8da52af26e2adb3 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 14 Oct 2026 15:46:43 +0200 Subject: [PATCH 1/2] ci: disable PR issue acceptance check Signed-off-by: Evan Lezar --- .github/workflows/pr-issue-acceptance.yml | 3 ++- CONTRIBUTING.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-issue-acceptance.yml b/.github/workflows/pr-issue-acceptance.yml index 742647d763..2a508b8ea5 100644 --- a/.github/workflows/pr-issue-acceptance.yml +++ b/.github/workflows/pr-issue-acceptance.yml @@ -20,7 +20,8 @@ jobs: issue-acceptance: name: OpenShell / PR Issue Acceptance timeout-minutes: 5 - if: github.repository_owner == 'NVIDIA' + # Disabled until automated issue acceptance enforcement is needed. + if: ${{ false }} runs-on: ubuntu-latest steps: # pull_request_target supplies the trusted default-branch SHA. Load diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c1e215b4b..fd4489b988 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -397,7 +397,7 @@ Every PR except an automated dependency update must close an existing issue. In Authors without write access to the target repository must link at least one issue with `state:accepted`, and every linked issue must be accepted. This applies to drafts and direct requests to agents. Authors with verified `WRITE`, `MAINTAIN`, or `ADMIN` access may create a PR without the acceptance label; they must still follow the issue-linking requirement above. PR body text, roadmap placement, other labels, and vouch status do not bypass acceptance. -The `PR Issue Acceptance` workflow reports the native `OpenShell / PR Issue Acceptance` job result for the triggering PR when it is opened, edited, reopened, or updated with new commits. It does not publish a shared commit status; lookup and execution errors fail the job. Link accepted issues in this repository using `Fixes #NNN` or `Closes #NNN`; GitHub recognizes closing references when a PR targets the default branch. Rerun the workflow after issue labels change. Making the job required for merging and configuring any required Actions event policy are separate repository settings; merge-queue support is not included. +The `PR Issue Acceptance` workflow is disabled and does not check linked issue acceptance. Maintainers review issue acceptance manually. Automated dependency update PRs, including Dependabot PRs, are exempt from the separate issue and closing-reference requirement. Human-authored dependency updates follow the normal issue requirement. The exception does not bypass `PR Issue Acceptance` for authors without repository write access or change applicable verification, review, or private vulnerability disclosure requirements. From 0843b27e9c63c17a2034665cff6607a08bc0acb7 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 14 Oct 2026 16:23:24 +0200 Subject: [PATCH 2/2] ci: remove unused PR issue acceptance machinery Signed-off-by: Evan Lezar --- .github/CODEOWNERS | 4 - .github/workflows/pr-issue-acceptance.yml | 41 ------ .github/zizmor.yml | 1 - CONTRIBUTING.md | 4 +- tasks/scripts/check_issue_acceptance.cjs | 77 ---------- tasks/scripts/check_issue_acceptance.test.cjs | 131 ------------------ 6 files changed, 2 insertions(+), 256 deletions(-) delete mode 100644 .github/workflows/pr-issue-acceptance.yml delete mode 100644 tasks/scripts/check_issue_acceptance.cjs delete mode 100644 tasks/scripts/check_issue_acceptance.test.cjs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2b1e795f29..c9575a44aa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,9 +31,5 @@ /tasks/scripts/check_maintainer_approval_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr /tasks/scripts/alert_maintainer_change_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr -# PR issue acceptance checks -/tasks/scripts/check_issue_acceptance.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr -/tasks/scripts/check_issue_acceptance.test.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr - # Auditable SBOM check /tasks/scripts/verify-image-sbom.sh @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr diff --git a/.github/workflows/pr-issue-acceptance.yml b/.github/workflows/pr-issue-acceptance.yml deleted file mode 100644 index 2a508b8ea5..0000000000 --- a/.github/workflows/pr-issue-acceptance.yml +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: PR Issue Acceptance - -on: - pull_request_target: - types: [opened, edited, reopened, synchronize] - -permissions: - contents: read - issues: read - pull-requests: read - -concurrency: - group: pr-issue-acceptance-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - issue-acceptance: - name: OpenShell / PR Issue Acceptance - timeout-minutes: 5 - # Disabled until automated issue acceptance enforcement is needed. - if: ${{ false }} - runs-on: ubuntu-latest - steps: - # pull_request_target supplies the trusted default-branch SHA. Load - # only that helper; PR metadata is data, never executable code. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: false - sparse-checkout: tasks/scripts/check_issue_acceptance.cjs - sparse-checkout-cone-mode: false - - - name: Check author access and linked issue acceptance - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - with: - script: | - const check = require('./tasks/scripts/check_issue_acceptance.cjs'); - await check({ github, context, core }); diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 5e19eaefa9..27a89f878e 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -9,7 +9,6 @@ rules: - dco.yml:3 - e2e-label-help.yml:13 - maintainers-change-alert.yml:6 - - pr-issue-acceptance.yml:6 - release-canary.yml:3 - required-ci-gates.yml:3 - vouch-check.yml:3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd4489b988..844069f420 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -397,9 +397,9 @@ Every PR except an automated dependency update must close an existing issue. In Authors without write access to the target repository must link at least one issue with `state:accepted`, and every linked issue must be accepted. This applies to drafts and direct requests to agents. Authors with verified `WRITE`, `MAINTAIN`, or `ADMIN` access may create a PR without the acceptance label; they must still follow the issue-linking requirement above. PR body text, roadmap placement, other labels, and vouch status do not bypass acceptance. -The `PR Issue Acceptance` workflow is disabled and does not check linked issue acceptance. Maintainers review issue acceptance manually. +Issue acceptance is reviewed manually by maintainers; there is no automated PR issue acceptance check. -Automated dependency update PRs, including Dependabot PRs, are exempt from the separate issue and closing-reference requirement. Human-authored dependency updates follow the normal issue requirement. The exception does not bypass `PR Issue Acceptance` for authors without repository write access or change applicable verification, review, or private vulnerability disclosure requirements. +Automated dependency update PRs, including Dependabot PRs, are exempt from the separate issue and closing-reference requirement. Human-authored dependency updates follow the normal issue requirement. The exception does not change applicable verification, review, or private vulnerability disclosure requirements. ### Branch Names diff --git a/tasks/scripts/check_issue_acceptance.cjs b/tasks/scripts/check_issue_acceptance.cjs deleted file mode 100644 index 8b81ef82fc..0000000000 --- a/tasks/scripts/check_issue_acceptance.cjs +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -module.exports = async function checkIssueAcceptance({ github, context, core }) { - const { owner, repo } = context.repo; - const { data: pr } = await github.rest.pulls.get({ - owner, repo, pull_number: context.payload.pull_request.number, - }); - const author = pr.user.login; - - async function evaluate() { - try { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner, repo, username: author, - }); - // The REST API maps maintain to write and triage to read. - if (['write', 'maintain', 'admin'].includes(data.permission)) { - core.info(`${author} has repository write access; acceptance gate passed.`); - return; - } - } catch (error) { - if (error.status !== 404) throw error; - } - - const issues = []; - let cursor = null; - do { - const result = await github.graphql(` - query($owner: String!, $repo: String!, $number: Int!, $cursor: String) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - closingIssuesReferences(first: 100, after: $cursor) { - nodes { number url repository { name owner { login } } } - pageInfo { hasNextPage endCursor } - } - } - } - } - `, { owner, repo, number: pr.number, cursor }); - const linked = result.repository.pullRequest.closingIssuesReferences; - issues.push(...linked.nodes); - cursor = linked.pageInfo.hasNextPage ? linked.pageInfo.endCursor : null; - } while (cursor); - - if (issues.length === 0) { - return 'Community PRs must link an issue with state:accepted using ' - + 'Fixes #NNN or Closes #NNN.'; - } - - const unaccepted = []; - for (const issue of issues) { - if (`${issue.repository.owner.login}/${issue.repository.name}`.toLowerCase() - !== `${owner}/${repo}`.toLowerCase()) { - return `Acceptance must be recorded in ${owner}/${repo}, not ${issue.url}.`; - } - const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: issue.repository.owner.login, - repo: issue.repository.name, - issue_number: issue.number, - per_page: 100, - }); - if (!labels.some(label => label.name === 'state:accepted')) { - unaccepted.push(issue.url); - } - } - - if (unaccepted.length) { - return 'Community PRs require state:accepted on every linked issue. ' - + `Ask a maintainer to accept: ${unaccepted.join(', ')}. ` - + 'After acceptance, rerun this workflow.'; - } - core.info(`All ${issues.length} linked issue(s) have state:accepted.`); - } - - const failure = await evaluate(); - if (failure) core.setFailed(failure); -}; diff --git a/tasks/scripts/check_issue_acceptance.test.cjs b/tasks/scripts/check_issue_acceptance.test.cjs deleted file mode 100644 index 443c8edaf9..0000000000 --- a/tasks/scripts/check_issue_acceptance.test.cjs +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -const assert = require('node:assert/strict'); -const test = require('node:test'); -const checkIssueAcceptance = require('./check_issue_acceptance.cjs'); - -async function check({ - permission = 'read', issues = [], body = '', permissionError, - number = 7, headSha = 'pr-head', pullError, issueError, labelError, -} = {}) { - const failures = []; - const github = { - rest: { - pulls: { - get: async ({ pull_number }) => { - assert.equal(pull_number, number); - if (pullError) throw new Error('PR lookup failed'); - return { data: { - number, user: { login: 'contributor' }, head: { sha: headSha }, body, - } }; - }, - }, - repos: { - getCollaboratorPermissionLevel: async ({ username }) => { - assert.equal(username, 'contributor'); - if (permissionError) throw Object.assign(new Error('lookup failed'), { status: permissionError }); - return { data: { permission } }; - }, - createCommitStatus() { assert.fail('PR decisions must not publish commit statuses'); }, - }, - issues: { listLabelsOnIssue() {} }, - }, - graphql: async (_query, variables) => { - assert.equal(variables.number, number); - if (issueError) throw new Error('Issue lookup failed'); - return { repository: { pullRequest: { closingIssuesReferences: { - nodes: issues.map(issue => ({ - number: issue.number, - url: `https://github.com/${issue.owner || 'NVIDIA'}/OpenShell/issues/${issue.number}`, - repository: { name: 'OpenShell', owner: { login: issue.owner || 'NVIDIA' } }, - })), - pageInfo: { hasNextPage: false, endCursor: null }, - } } } }; - }, - paginate: async (_method, { issue_number }) => { - if (labelError) throw new Error('Label lookup failed'); - return issues.find(issue => issue.number === issue_number).labels || []; - }, - }; - await checkIssueAcceptance({ - github, - context: { - repo: { owner: 'NVIDIA', repo: 'OpenShell' }, - payload: { pull_request: { number } }, - actor: 'maintainer', sha: 'base-commit', serverUrl: 'https://github.com', runId: 42, - }, - core: { info() {}, setFailed: message => failures.push(message) }, - }); - return failures; -} - -for (const permission of ['write', 'maintain', 'admin']) { - test(`${permission} access allows an issue-less PR`, async () => { - assert.deepEqual(await check({ permission }), []); - }); -} - -for (const body of ['', 'No issue required: typo fix', '']) { - test(`community PR without linked issues fails for body ${JSON.stringify(body)}`, async () => { - const failures = await check({ body }); - assert.equal(failures.length, 1); - assert.match(failures[0], /must link an issue with state:accepted/); - }); -} - -test('all linked issues must be accepted, regardless of PR body or triggering actor', async () => { - const failures = await check({ - body: 'No issue required: small fix', - issues: [{ number: 1, labels: [{ name: 'state:accepted' }] }, { number: 2 }], - }); - assert.equal(failures.length, 1); - assert.match(failures[0], /issues\/2/); -}); - -test('accepted issues in the target repository pass', async () => { - assert.deepEqual(await check({ issues: [{ number: 1, labels: [{ name: 'state:accepted' }] }] }), []); -}); - -test('accepted issues in another repository cannot bypass the gate', async () => { - const failures = await check({ issues: [{ number: 1, owner: 'contributor', labels: [{ name: 'state:accepted' }] }] }); - assert.equal(failures.length, 1); - assert.match(failures[0], /Acceptance must be recorded in NVIDIA\/OpenShell/); -}); - -test('a non-collaborator still needs an accepted issue', async () => { - assert.equal((await check({ permissionError: 404, body: 'No issue required: typo fix' })).length, 1); -}); - -test('permission lookup failures do not grant an exemption', async () => { - await assert.rejects(check({ permissionError: 403 }), /lookup failed/); -}); - -for (const [option, message] of [ - ['pullError', /PR lookup failed/], - ['issueError', /Issue lookup failed/], - ['labelError', /Label lookup failed/], -]) { - test(`${option} fails the workflow invocation`, async () => { - await assert.rejects(check({ [option]: true, issues: [{ number: 1 }] }), message); - }); -} - -test('PRs sharing a head SHA evaluate their own issue references in either order', async () => { - const rejected = { number: 7, headSha: 'shared-head', issues: [{ number: 1 }] }; - const accepted = { - number: 8, headSha: 'shared-head', - issues: [{ number: 2, labels: [{ name: 'state:accepted' }] }], - }; - for (const pair of [[rejected, accepted], [accepted, rejected]]) { - const results = await Promise.all(pair.map(input => check(input))); - pair.forEach((input, index) => { - assert.equal(results[index].length, input.number === 7 ? 1 : 0); - }); - } -}); - -test('a failed refresh rejects after an earlier successful evaluation', async () => { - assert.deepEqual(await check({ permission: 'write' }), []); - await assert.rejects(check({ permission: 'write', pullError: true }), /PR lookup failed/); -});