From ea7ea366af453b0d53edc3ed83819b78a10a095d Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:09:45 -0500 Subject: [PATCH 1/9] Add PR linked-issue check action and workflow Introduce a composite GitHub Action that verifies a pull request has at least one linked issue via the GraphQL closingIssuesReferences field, plus a reusable workflow to run it. Adds action implementation (.github/actions/check-linked-issue/action.yml) and documentation (README.md) and a reusable workflow (.github/workflows/check-issue.yml). The action uses actions/github-script to query linked issues and fails the step if none are found; the workflow demonstrates usage and required permissions (contents: read, pull-requests: read). Also removes a placeholder .gitkeep file. Signed-off-by: John McCall --- .github/actions/.gitkeep | 0 .github/actions/check-linked-issue/README.md | 122 ++++++++++++++++++ .github/actions/check-linked-issue/action.yml | 56 ++++++++ .github/workflows/check-issue.yml | 47 +++++++ 4 files changed, 225 insertions(+) delete mode 100644 .github/actions/.gitkeep create mode 100644 .github/actions/check-linked-issue/README.md create mode 100644 .github/actions/check-linked-issue/action.yml create mode 100644 .github/workflows/check-issue.yml diff --git a/.github/actions/.gitkeep b/.github/actions/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/actions/check-linked-issue/README.md b/.github/actions/check-linked-issue/README.md new file mode 100644 index 0000000..c667538 --- /dev/null +++ b/.github/actions/check-linked-issue/README.md @@ -0,0 +1,122 @@ +# Check Linked Issue + +A composite GitHub Action that verifies a pull request has at least one linked GitHub issue. + +## Explanation + +### What it does + +This action queries the GitHub GraphQL API for `closingIssuesReferences` on a +pull request. It detects issues linked by: + +- Body keywords: `Fixes #123`, `Closes #456`, `Resolves owner/repo#789` +- Manual linking via the GitHub UI sidebar + +If no linked issues are found, the step fails with a message guiding the author +to link one. + +### Why GraphQL over regex + +| Approach | Body keywords | UI-linked issues | Cross-repo refs | Format-proof | +|----------|:---:|:---:|:---:|:---:| +| Regex on PR body | ✅ | ❌ | Fragile | ❌ | +| `closingIssuesReferences` | ✅ | ✅ | ✅ | ✅ | + +The GraphQL approach reflects GitHub's actual internal linkage rather than +parsing text, making it reliable across formatting styles and linking methods. + +## How-to guides + +### Use the reusable workflow (recommended) + +The simplest way to adopt this check from any repo in the OvertureMaps +organization. Create a workflow file in your repo: + +```yaml +# .github/workflows/check-issue.yml +name: Check Linked Issue + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + check-issue: + uses: OvertureMaps/workflows/.github/workflows/check-issue.yml@main +``` + +No checkout step is needed — GitHub resolves the reusable workflow and its +actions automatically. + +### Use the composite action directly from this repo + +If you need to combine this check with other steps in an existing job, check out +the action and reference it locally: + +```yaml +# .github/workflows/pr-checks.yml +name: PR Checks + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + contents: read + pull-requests: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout workflows repo + uses: actions/checkout@v4 + with: + repository: OvertureMaps/workflows + sparse-checkout: .github/actions/check-linked-issue + path: .workflows + + - name: Check for linked issue + uses: ./.workflows/.github/actions/check-linked-issue + + # ... additional steps in the same job +``` + +### Use the composite action within the workflows repo + +When referencing the action from a workflow in this same repository, use a +relative path without a checkout step: + +```yaml +steps: + - uses: ./.github/actions/check-linked-issue +``` + +## Reference + +### Permissions + +Requires the default `GITHUB_TOKEN` with: + +```yaml +permissions: + contents: read + pull-requests: read +``` + +For private repos, these permissions allow the token to read PR metadata and +query linked issues. Cross-repo issue detection is limited to public repos and +repos within the same organization that the token has access to. + +### Inputs + +This action has no inputs. + +### Outputs + +This action has no outputs. It either passes or fails the step. + +### Supported trigger events + +The action reads `context.payload.pull_request.number`, so it must run on a +`pull_request` or `pull_request_target` event. diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml new file mode 100644 index 0000000..7e7d9a5 --- /dev/null +++ b/.github/actions/check-linked-issue/action.yml @@ -0,0 +1,56 @@ +--- +name: Check Linked Issue +description: > + Checks that a pull request has at least one linked GitHub issue via + closingIssuesReferences. Covers 'Fixes #123', 'Closes owner/repo#456', + and issues linked manually through the GitHub UI. + +runs: + using: composite + steps: + - name: Check for linked issue via GraphQL + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const { owner, repo } = context.repo; + + const query = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 10) { + nodes { + number + title + state + url + } + } + } + } + } + `; + + const result = await github.graphql(query, { + owner, + repo, + number: prNumber + }); + + const issues = + result.repository.pullRequest.closingIssuesReferences.nodes; + + if (!issues || issues.length === 0) { + core.setFailed( + "This PR does not reference any linked issues. " + + "Please link an issue using 'Fixes #123', " + + "'Closes OvertureMaps/other-repo#123', or the GitHub UI " + + "(https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)" + ); + } else { + core.info("Linked issues found:"); + issues.forEach(issue => { + core.info(` #${issue.number} - ${issue.title} (${issue.state}) ${issue.url}`); + }); + } diff --git a/.github/workflows/check-issue.yml b/.github/workflows/check-issue.yml new file mode 100644 index 0000000..6d42dbb --- /dev/null +++ b/.github/workflows/check-issue.yml @@ -0,0 +1,47 @@ +--- +# Reusable workflow to enforce that PRs have a linked GitHub issue. +# +# Uses the check-linked-issue composite action which queries GraphQL +# closingIssuesReferences to detect linked issues, covering: +# - Body keywords: Fixes #123, Closes #456, Resolves owner/repo#789 +# - Issues manually linked via the GitHub UI +# +# Usage: Reference this workflow from any repo in the OvertureMaps organization. +# +# Example (.github/workflows/check-issue.yml): +# name: Check Linked Issue +# on: +# pull_request: +# types: [opened, edited, synchronize] +# jobs: +# check-issue: +# uses: OvertureMaps/workflows/.github/workflows/check-issue.yml@main +# +name: Check Linked Issue + +on: + workflow_call: + pull_request: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + pull-requests: read + +jobs: + check-linked-issue: + name: Check Linked Issue + runs-on: ubuntu-latest + steps: + - name: Checkout workflows repo + uses: actions/checkout@v4 + with: + repository: OvertureMaps/workflows + sparse-checkout: .github/actions/check-linked-issue + path: .workflows + + - name: Check for linked issue + uses: ./.workflows/.github/actions/check-linked-issue From 067c5be65ad9675508c6b49e461fb511cbc7568f Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:11:11 -0500 Subject: [PATCH 2/9] Update README.md Signed-off-by: John McCall --- .github/actions/check-linked-issue/README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/actions/check-linked-issue/README.md b/.github/actions/check-linked-issue/README.md index c667538..54277d1 100644 --- a/.github/actions/check-linked-issue/README.md +++ b/.github/actions/check-linked-issue/README.md @@ -82,16 +82,6 @@ jobs: # ... additional steps in the same job ``` -### Use the composite action within the workflows repo - -When referencing the action from a workflow in this same repository, use a -relative path without a checkout step: - -```yaml -steps: - - uses: ./.github/actions/check-linked-issue -``` - ## Reference ### Permissions From 200a6d5f0acb840e6250fd8a94dfd7bcf4174fb0 Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:29:00 -0500 Subject: [PATCH 3/9] Log linked issues and use ubuntu-slim runner Add a debug/info log in the check-linked-issue Action to report how many linked issues the GraphQL query returned (in .github/actions/check-linked-issue/action.yml). Also change the workflow runner from ubuntu-latest to ubuntu-slim in .github/workflows/check-issue.yml to use a slimmer runner image. These changes improve observability and reduce runner footprint. Signed-off-by: John McCall --- .github/actions/check-linked-issue/action.yml | 1 + .github/workflows/check-issue.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml index 7e7d9a5..1b3e564 100644 --- a/.github/actions/check-linked-issue/action.yml +++ b/.github/actions/check-linked-issue/action.yml @@ -40,6 +40,7 @@ runs: const issues = result.repository.pullRequest.closingIssuesReferences.nodes; + core.info(`GraphQL query returned ${issues.length} linked issues.`); if (!issues || issues.length === 0) { core.setFailed( diff --git a/.github/workflows/check-issue.yml b/.github/workflows/check-issue.yml index 6d42dbb..551623b 100644 --- a/.github/workflows/check-issue.yml +++ b/.github/workflows/check-issue.yml @@ -34,7 +34,7 @@ permissions: jobs: check-linked-issue: name: Check Linked Issue - runs-on: ubuntu-latest + runs-on: ubuntu-slim steps: - name: Checkout workflows repo uses: actions/checkout@v4 From 0e70dca71fd43e5f40ed7b3392791ac9bef36ddb Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:36:50 -0500 Subject: [PATCH 4/9] Update action.yml Signed-off-by: John McCall --- .github/actions/check-linked-issue/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml index 1b3e564..578fdfa 100644 --- a/.github/actions/check-linked-issue/action.yml +++ b/.github/actions/check-linked-issue/action.yml @@ -40,7 +40,7 @@ runs: const issues = result.repository.pullRequest.closingIssuesReferences.nodes; - core.info(`GraphQL query returned ${issues.length} linked issues.`); + core.info(`${JSON.stringify(result, null, 2)}`); if (!issues || issues.length === 0) { core.setFailed( From 3be8896dfe24c62ecfd7dc2f502fd2fcf27af94c Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:38:51 -0500 Subject: [PATCH 5/9] Update check-issue.yml Signed-off-by: John McCall --- .github/workflows/check-issue.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-issue.yml b/.github/workflows/check-issue.yml index 551623b..a353720 100644 --- a/.github/workflows/check-issue.yml +++ b/.github/workflows/check-issue.yml @@ -29,6 +29,7 @@ on: permissions: contents: read + issues: read pull-requests: read jobs: From 95045c138f4bb6bc5e6109cbee9f5dd9dfc33b98 Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:44:04 -0500 Subject: [PATCH 6/9] Update action.yml Signed-off-by: John McCall --- .github/actions/check-linked-issue/action.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml index 578fdfa..039dd89 100644 --- a/.github/actions/check-linked-issue/action.yml +++ b/.github/actions/check-linked-issue/action.yml @@ -14,6 +14,7 @@ runs: script: | const prNumber = context.payload.pull_request.number; const { owner, repo } = context.repo; + const minimumLinkedIssues = 1; const query = ` query($owner: String!, $repo: String!, $number: Int!) { @@ -40,17 +41,18 @@ runs: const issues = result.repository.pullRequest.closingIssuesReferences.nodes; - core.info(`${JSON.stringify(result, null, 2)}`); - if (!issues || issues.length === 0) { + core.debug(`${JSON.stringify(result, null, 2)}`); + + if (!issues || issues.length < minimumLinkedIssues) { core.setFailed( - "This PR does not reference any linked issues. " + + `❌ This PR does not reference any linked issues, but should have at least ${minimumLinkedIssues}!\n` + "Please link an issue using 'Fixes #123', " + "'Closes OvertureMaps/other-repo#123', or the GitHub UI " + "(https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)" ); } else { - core.info("Linked issues found:"); + core.info(`✅ ${issues.length} linked issues found:`); issues.forEach(issue => { core.info(` #${issue.number} - ${issue.title} (${issue.state}) ${issue.url}`); }); From 6a459b7b1b1f7185d75af3cbe0bc192206b0bb7c Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:44:42 -0500 Subject: [PATCH 7/9] Update lint-python.yml Signed-off-by: John McCall --- .github/workflows/lint-python.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index 2968e9c..cfc66d8 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -32,11 +32,6 @@ on: required: false type: string default: "error" - pull_request: - types: - - opened - - reopened - - synchronize permissions: contents: read From 484f0dcc9d181177ee22ee018dd47d3043a3bfd1 Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:49:27 -0500 Subject: [PATCH 8/9] Update action.yml Signed-off-by: John McCall --- .github/actions/check-linked-issue/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml index 039dd89..8347c46 100644 --- a/.github/actions/check-linked-issue/action.yml +++ b/.github/actions/check-linked-issue/action.yml @@ -49,7 +49,8 @@ runs: `❌ This PR does not reference any linked issues, but should have at least ${minimumLinkedIssues}!\n` + "Please link an issue using 'Fixes #123', " + "'Closes OvertureMaps/other-repo#123', or the GitHub UI " + - "(https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)" + "(https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)\n" + + "After adding a linked issue, you may need to manually re-run this check from the Checks tab to update the status." ); } else { core.info(`✅ ${issues.length} linked issues found:`); From 9ba6a6eb8a3ef1a4f18cbdbc6e9ea310df1bc4af Mon Sep 17 00:00:00 2001 From: John McCall Date: Thu, 12 Feb 2026 15:55:02 -0500 Subject: [PATCH 9/9] Add minimumLinkedIssues input to action Introduce a new optional input `minimumLinkedIssues` (default 1) to the check-linked-issue action and parse it in the composite script so callers can require more than one linked issue. Update action.yml to declare the input and adjust messaging to show found vs required counts. Update README to document the new input and example usage, and add `issues: read` permission to the recommended permissions block. Signed-off-by: John McCall --- .github/actions/check-linked-issue/README.md | 17 +++++++++++------ .github/actions/check-linked-issue/action.yml | 12 +++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/actions/check-linked-issue/README.md b/.github/actions/check-linked-issue/README.md index 54277d1..02c4676 100644 --- a/.github/actions/check-linked-issue/README.md +++ b/.github/actions/check-linked-issue/README.md @@ -10,7 +10,7 @@ This action queries the GitHub GraphQL API for `closingIssuesReferences` on a pull request. It detects issues linked by: - Body keywords: `Fixes #123`, `Closes #456`, `Resolves owner/repo#789` -- Manual linking via the GitHub UI sidebar +- Manual linking via the GitHub UI If no linked issues are found, the step fails with a message guiding the author to link one. @@ -78,6 +78,8 @@ jobs: - name: Check for linked issue uses: ./.workflows/.github/actions/check-linked-issue + with: + minimumLinkedIssues: 2 # Require at least 2 linked issues (optional, default is 1) # ... additional steps in the same job ``` @@ -91,16 +93,19 @@ Requires the default `GITHUB_TOKEN` with: ```yaml permissions: contents: read + issues: read pull-requests: read ``` -For private repos, these permissions allow the token to read PR metadata and -query linked issues. Cross-repo issue detection is limited to public repos and -repos within the same organization that the token has access to. - ### Inputs -This action has no inputs. +- `minimumLinkedIssues` (optional): Minimum number of linked issues required for the PR. Default is `1`. Set this input to require more than one linked issue: + +```yaml +with: + minimumLinkedIssues: 2 +``` + ### Outputs diff --git a/.github/actions/check-linked-issue/action.yml b/.github/actions/check-linked-issue/action.yml index 8347c46..c2b7351 100644 --- a/.github/actions/check-linked-issue/action.yml +++ b/.github/actions/check-linked-issue/action.yml @@ -5,6 +5,12 @@ description: > closingIssuesReferences. Covers 'Fixes #123', 'Closes owner/repo#456', and issues linked manually through the GitHub UI. +inputs: + minimumLinkedIssues: + description: Minimum number of linked issues required + required: false + default: 1 + runs: using: composite steps: @@ -14,7 +20,7 @@ runs: script: | const prNumber = context.payload.pull_request.number; const { owner, repo } = context.repo; - const minimumLinkedIssues = 1; + const minimumLinkedIssues = parseInt(core.getInput('minimumLinkedIssues')) || 1; const query = ` query($owner: String!, $repo: String!, $number: Int!) { @@ -46,14 +52,14 @@ runs: if (!issues || issues.length < minimumLinkedIssues) { core.setFailed( - `❌ This PR does not reference any linked issues, but should have at least ${minimumLinkedIssues}!\n` + + `❌ This PR does not reference enough linked issues (found ${issues.length}, required ${minimumLinkedIssues})!\n` + "Please link an issue using 'Fixes #123', " + "'Closes OvertureMaps/other-repo#123', or the GitHub UI " + "(https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)\n" + "After adding a linked issue, you may need to manually re-run this check from the Checks tab to update the status." ); } else { - core.info(`✅ ${issues.length} linked issues found:`); + core.info(`✅ ${issues.length} linked issues found out of ${minimumLinkedIssues} required:`); issues.forEach(issue => { core.info(` #${issue.number} - ${issue.title} (${issue.state}) ${issue.url}`); });