Skip to content

Commit 2ffc3d2

Browse files
authored
ci: require accepted issues for community pull requests (#4340)
Evaluate issue acceptance using trusted default-branch code and the PR author's repository permissions. Report the native workflow job result, protect enforcement files, and align PR creation and review guidance. Closes #4339 Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 5e414ea commit 2ffc3d2

11 files changed

Lines changed: 306 additions & 5 deletions

File tree

‎.agents/skills/build-from-issue/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Use a single issue comment beginning with `> **🏗️ build-plan**` when a plan
2727
1. Check the current branch and working tree. Preserve unrelated work. Create an issue-specific branch or worktree as needed, following Branch Names in `CONTRIBUTING.md`.
2828
2. Implement the smallest coherent change that fulfills the acceptance criteria. Update relevant skills when behavior or commands change. Keep published documentation minimal: explain exactly what users need, avoid duplication across pages, and omit internal details with no user impact.
2929
3. Add meaningful tests for changed behavior and follow the verification guidance in `CONTRIBUTING.md`. Select format, lint, compile or type checks, and tests for affected components and their dependencies. Run the relevant E2E lane for infrastructure, sandbox, or policy changes. Guidance and template edits need applicable Markdown, YAML, link, and consistency checks. Do not require full Rust, SDK, or repository CI solely because a commit or PR is being created; broaden checks only for a concrete remaining risk or failed check.
30-
4. Review the diff, use a signed-off Conventional Commit, and prepare a PR following `create-github-pr`.
30+
4. Before pushing, apply the contributor access and issue acceptance check in `create-github-pr`. If blocked, preserve local work and report the reason. Review the diff, use a signed-off Conventional Commit, and prepare the PR.
3131

3232
Every PR from this issue-backed workflow must have its own existing issue and use `Closes #<id>` in its Related Issue section. For work needing multiple PRs, split the scope into a closable issue per PR. A high-level issue may track those issues but should not be closed by an incomplete PR. Report the implementation, verification, and any remaining limitation in the PR description, rather than copying earlier issue diagnostics.
3333

‎.agents/skills/create-github-pr/SKILL.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ Create pull requests on GitHub using the `gh` CLI.
1717

1818
## Before Creating a PR
1919

20+
### Verify Contributor Access and Issue Acceptance
21+
22+
Before pushing or creating a PR (including a draft), verify the author's access
23+
to the target repository, not their fork. For the authenticated `gh` user:
24+
25+
```bash
26+
gh repo view OWNER/REPO --json nameWithOwner,viewerPermission
27+
gh issue view ISSUE_NUMBER --repo OWNER/REPO --json number,url,labels
28+
```
29+
30+
Authors with verified `WRITE`, `MAINTAIN`, or `ADMIN` access may proceed without
31+
`state:accepted`; keep the issue link and note the missing label. Everyone else
32+
must link at least one target-repository issue and have `state:accepted` on each.
33+
Otherwise, preserve local work and report which issue needs acceptance. A direct
34+
request, roadmap placement, other labels, or vouch status does not bypass this gate.
35+
36+
When acting for another author, verify their permissions. Resolve failed lookups
37+
before proceeding; retry sandbox network failures outside the sandbox before
38+
reporting an authentication problem. Vouch requirements still apply separately.
39+
2040
### Check Config Documentation
2141

2242
If the branch changes gateway TOML parsing, `[openshell.gateway]` fields,
@@ -220,6 +240,9 @@ EOF
220240

221241
The command outputs the PR URL and number.
222242

243+
Rerun `PR Issue Acceptance` after linked issue labels change; it does not refresh
244+
automatically on issue-label events.
245+
223246
**Display the URL using markdown link syntax** so it's easily clickable:
224247

225248
```

‎.agents/skills/review-github-pr/SKILL.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,28 @@ gh pr list --head "<branch>" --state open
3939
Retrieve the PR metadata:
4040

4141
```bash
42-
gh pr view <number> --json title,body,state,headRefName,baseRefName,labels,author
42+
gh pr view <number> --json title,body,state,headRefName,baseRefName,labels,author,closingIssuesReferences
4343
```
4444

4545
Record the **title**, **body**, **headRefName**, and **baseRefName** for use in later steps.
4646

47+
### Check Issue Acceptance
48+
49+
Inspect every closing issue reference (paginate as needed) and fetch its current
50+
repository and labels. Verify the PR author's permission in the target repository:
51+
52+
```bash
53+
gh api repos/OWNER/REPO/collaborators/AUTHOR/permission --jq .permission
54+
```
55+
56+
Call out each linked issue missing `state:accepted`, with an issue link. Verified
57+
`write`, `maintain`, or `admin` access permits the exception; report it as context,
58+
not a blocking concern. Other authors need at least one target-repository issue,
59+
and every linked issue must be accepted; report violations under **Potential
60+
Concerns**. PR body text such as `No issue required` does not bypass this gate.
61+
Report failed lookups as unverified and continue the code review. For a branch
62+
without a PR, note that issue acceptance was not verified.
63+
4764
## Step 3: Generate the Diff
4865

4966
### Primary: gh pr diff

‎.github/CODEOWNERS‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Merge enforcement — these paths decide who can merge, so they notify the full
88
# list of maintainers to raise scrutiny on changes here.
9-
#
9+
#
1010
# PR checks run local to the branch, so unless the check specifically avoids
1111
# using branch-local files, a PR could change its own gates and appear to pass
1212
# them. Covering the required checks here closes that gap.
@@ -31,5 +31,9 @@
3131
/tasks/scripts/check_maintainer_approval_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
3232
/tasks/scripts/alert_maintainer_change_test.py @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
3333

34+
# PR issue acceptance checks
35+
/tasks/scripts/check_issue_acceptance.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
36+
/tasks/scripts/check_issue_acceptance.test.cjs @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr
37+
3438
# Auditable SBOM check
3539
/tasks/scripts/verify-image-sbom.sh @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr

‎.github/PULL_REQUEST_TEMPLATE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<!-- 1-3 sentences: what this PR does and why -->
33

44
## Related Issue
5-
<!-- Every PR must close an existing issue covering its scope, except automated dependency updates. Use Closes #NNN; see CONTRIBUTING.md for the exception. -->
5+
<!-- Every PR must close an existing issue covering its scope, except automated dependency updates. Use Closes #NNN; see CONTRIBUTING.md for the exception. Authors without repository write access must link at least one issue with state:accepted. PR body text cannot bypass this acceptance check. -->
66

77
## Changes
88
<!-- Bullet list of key changes -->
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
name: PR Issue Acceptance
5+
6+
on:
7+
pull_request_target:
8+
types: [opened, edited, reopened, synchronize]
9+
10+
permissions:
11+
contents: read
12+
issues: read
13+
pull-requests: read
14+
15+
concurrency:
16+
group: pr-issue-acceptance-${{ github.event.pull_request.number }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
issue-acceptance:
21+
name: OpenShell / PR Issue Acceptance
22+
timeout-minutes: 5
23+
if: github.repository_owner == 'NVIDIA'
24+
runs-on: ubuntu-latest
25+
steps:
26+
# pull_request_target supplies the trusted default-branch SHA. Load
27+
# only that helper; PR metadata is data, never executable code.
28+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
29+
with:
30+
ref: ${{ github.sha }}
31+
persist-credentials: false
32+
sparse-checkout: tasks/scripts/check_issue_acceptance.cjs
33+
sparse-checkout-cone-mode: false
34+
35+
- name: Check author access and linked issue acceptance
36+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
37+
with:
38+
script: |
39+
const check = require('./tasks/scripts/check_issue_acceptance.cjs');
40+
await check({ github, context, core });

‎.github/zizmor.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ rules:
99
- dco.yml:3
1010
- e2e-label-help.yml:13
1111
- maintainers-change-alert.yml:6
12+
- pr-issue-acceptance.yml:6
1213
- release-canary.yml:3
1314
- required-ci-gates.yml:3
1415
- vouch-check.yml:3

‎AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ ocsf_emit!(event);
174174

175175
Follow the [branch naming convention in CONTRIBUTING.md](CONTRIBUTING.md#branch-names).
176176

177+
## Pull Requests
178+
179+
Follow `CONTRIBUTING.md` and `create-github-pr`. Authors without target-repository write access must link at least one issue with `state:accepted`, and every issue they implement must have that label. Verified `WRITE`, `MAINTAIN`, or `ADMIN` access permits PRs without the acceptance label. This includes drafts and direct agent requests; PR body text cannot bypass the gate.
180+
177181
## Commits
178182

179183
- Always use [Conventional Commits](https://www.conventionalcommits.org/) format for commit messages

‎CONTRIBUTING.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,11 @@ See [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx) for the current docs authorin
394394

395395
Every PR except an automated dependency update must close an existing issue. In the PR's **Related Issue** section, use `Closes #NNN` for the issue covering that PR's scope. Split multi-PR work into a closable issue for each PR; a tracking issue can link them. Security fixes follow the private disclosure process in [SECURITY.md](SECURITY.md).
396396

397-
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.
397+
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.
398+
399+
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.
400+
401+
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.
398402

399403
### Branch Names
400404

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
module.exports = async function checkIssueAcceptance({ github, context, core }) {
5+
const { owner, repo } = context.repo;
6+
const { data: pr } = await github.rest.pulls.get({
7+
owner, repo, pull_number: context.payload.pull_request.number,
8+
});
9+
const author = pr.user.login;
10+
11+
async function evaluate() {
12+
try {
13+
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
14+
owner, repo, username: author,
15+
});
16+
// The REST API maps maintain to write and triage to read.
17+
if (['write', 'maintain', 'admin'].includes(data.permission)) {
18+
core.info(`${author} has repository write access; acceptance gate passed.`);
19+
return;
20+
}
21+
} catch (error) {
22+
if (error.status !== 404) throw error;
23+
}
24+
25+
const issues = [];
26+
let cursor = null;
27+
do {
28+
const result = await github.graphql(`
29+
query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
30+
repository(owner: $owner, name: $repo) {
31+
pullRequest(number: $number) {
32+
closingIssuesReferences(first: 100, after: $cursor) {
33+
nodes { number url repository { name owner { login } } }
34+
pageInfo { hasNextPage endCursor }
35+
}
36+
}
37+
}
38+
}
39+
`, { owner, repo, number: pr.number, cursor });
40+
const linked = result.repository.pullRequest.closingIssuesReferences;
41+
issues.push(...linked.nodes);
42+
cursor = linked.pageInfo.hasNextPage ? linked.pageInfo.endCursor : null;
43+
} while (cursor);
44+
45+
if (issues.length === 0) {
46+
return 'Community PRs must link an issue with state:accepted using '
47+
+ 'Fixes #NNN or Closes #NNN.';
48+
}
49+
50+
const unaccepted = [];
51+
for (const issue of issues) {
52+
if (`${issue.repository.owner.login}/${issue.repository.name}`.toLowerCase()
53+
!== `${owner}/${repo}`.toLowerCase()) {
54+
return `Acceptance must be recorded in ${owner}/${repo}, not ${issue.url}.`;
55+
}
56+
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
57+
owner: issue.repository.owner.login,
58+
repo: issue.repository.name,
59+
issue_number: issue.number,
60+
per_page: 100,
61+
});
62+
if (!labels.some(label => label.name === 'state:accepted')) {
63+
unaccepted.push(issue.url);
64+
}
65+
}
66+
67+
if (unaccepted.length) {
68+
return 'Community PRs require state:accepted on every linked issue. '
69+
+ `Ask a maintainer to accept: ${unaccepted.join(', ')}. `
70+
+ 'After acceptance, rerun this workflow.';
71+
}
72+
core.info(`All ${issues.length} linked issue(s) have state:accepted.`);
73+
}
74+
75+
const failure = await evaluate();
76+
if (failure) core.setFailed(failure);
77+
};

0 commit comments

Comments
 (0)