Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/__shared-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ jobs:
security-events: write
uses: $/.github/workflows/__test-action-parse-ci-reports.yml

test-action-need-fix-to-issue:
needs: linter
permissions:
contents: read
id-token: write
packages: read
pull-requests: write
security-events: write
uses: $/.github/workflows/__test-action-need-fix-to-issue.yml

test-action-repository-owner-is-organization:
needs: linter
permissions:
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/__test-action-need-fix-to-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Internal - Tests for need-fix-to-issue action

on:
workflow_call:

permissions:
contents: read

jobs:
continuous-integration:
uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@81a4bca8445b7b273455ff26a9a5c3c31c86b2a1 # 0.25.0
permissions:
contents: read
id-token: write
packages: read
pull-requests: write
security-events: write
with:
working-directory: ./actions/need-fix-to-issue
build: null
lint: null
test: '{ "coverage": null }'
12 changes: 11 additions & 1 deletion .github/workflows/need-fix-to-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
## Overview

Reusable workflow to convert comments requiring fixes (todo, FIXME) to issues.
Mainly using [Todo to Issue Action](https://github.com/alstr/todo-to-issue-action), with some opinionated defaults.
Uses the [Need fix to Issue action](../../actions/need-fix-to-issue/README.md) to create, categorize, and deduplicate issues.
Generated issues receive the `need-fix` label and the `Bug` issue type when available.
Duplicate issues with the same title and source file are closed.

### Permissions

Expand Down Expand Up @@ -71,6 +73,14 @@ jobs:

<!-- usage:end -->

### Issue management

- Both `TODO` and `FIXME` issues receive the `need-fix` label, in addition to their `help wanted` or `bug` label. Only issues with `need-fix` are managed. Add it to an existing generated issue to include it in categorization and duplicate detection. Pull requests are excluded.
- Untyped generated issues receive the organization's `Bug` issue type. Existing types are preserved. Personal repositories skip typing; organizations without an enabled `Bug` type receive a warning while duplicate detection continues. See [managing issue types](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/managing-issue-types-in-an-organization).
- Duplicate detection keeps the oldest open issue with the exact same title and source file, ignoring commit and line changes in the generated source link. Newer duplicates are closed with a `Duplicate of #...` comment and linked to the original. Different files and issues without a recognized source link are not matched.

Repositories that need inactivity management can use the separate [Stale workflow](stale.md).

**Tips:**

It is recommended to add this trigger for this workflow:
Expand Down
20 changes: 8 additions & 12 deletions .github/workflows/need-fix-to-issue.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Reusable workflow to convert comments requiring fixes (todo, FIXME) to issues.
# Mainly using [Todo to Issue Action](https://github.com/alstr/todo-to-issue-action), with some opinionated defaults.
# Uses the [Need fix to Issue action](../../actions/need-fix-to-issue/README.md) to create, categorize, and deduplicate issues.
# Generated issues receive the need-fix label and the Bug issue type when available.
# Duplicate issues with the same title and source file are closed.

name: "Need fix to Issue"
on:
Expand Down Expand Up @@ -35,7 +37,7 @@ on:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: false

permissions: {}

Expand All @@ -51,14 +53,8 @@ jobs:
with:
persist-credentials: false

- name: "TODO to Issue"
uses: "alstr/todo-to-issue-action@37bb7b56e58569ef273b60678048030a7f0c261a" # v5.1.15
- name: Create and manage need-fix issues
uses: $/actions/need-fix-to-issue
with:
IDENTIFIERS: |
[
{"name": "TODO:", "labels": ["help wanted"]},
{"name": "FIXME:", "labels": ["bug"]}
]
env:
MANUAL_COMMIT_REF: ${{ inputs.manual-commit-ref }}
MANUAL_BASE_REF: ${{ inputs.manual-base-ref }}
manual-commit-ref: ${{ inputs.manual-commit-ref }}
manual-base-ref: ${{ inputs.manual-base-ref }}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Opinionated GitHub Actions and reusable workflows for foundational continuous-in
- [Create or update comment](actions/create-or-update-comment/README.md) - adds or updates comments on issues and pull requests idempotently.
- [Get GitHub Actions bot user](actions/get-github-actions-bot-user/README.md) - retrieves the profile information for the GitHub Actions bot.
- [Get issue number](actions/get-issue-number/README.md) - extracts the relevant issue number from the current workflow context.
- [Need fix to Issue](actions/need-fix-to-issue/README.md) - creates and categorizes TODO/FIXME issues and closes duplicates.

### Matrix & workflow data helpers

Expand All @@ -43,7 +44,7 @@ Opinionated GitHub Actions and reusable workflows for foundational continuous-in
### Community & issue hygiene

- [Greetings](.github/workflows/greetings.md) - welcomes first-time issue reporters and pull request authors.
- [Need fix to Issue](.github/workflows/need-fix-to-issue.md) - labels issues that require follow-up fixes.
- [Need fix to Issue](.github/workflows/need-fix-to-issue.md) - creates and categorizes TODO/FIXME issues and closes duplicates.
- [Stale](.github/workflows/stale.md) - automatically marks and closes stale issues and pull requests.

### Quality gates
Expand Down
1 change: 1 addition & 0 deletions actions/need-fix-to-issue/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage/
38 changes: 38 additions & 0 deletions actions/need-fix-to-issue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Need fix to Issue

Creates issues from TODO/FIXME comments with [Todo to Issue Action](https://github.com/alstr/todo-to-issue-action), then categorizes and deduplicates open issues carrying the `need-fix` label. Used by the [Need fix to Issue workflow](../../.github/workflows/need-fix-to-issue.md).

## Behavior

- Scans the commit diff for `TODO:` and `FIXME:` comments. Both receive the `need-fix` label, in addition to `help wanted` for `TODO:` and `bug` for `FIXME:`.
- Assigns the organization's `Bug` issue type to untyped issues. Existing types are preserved. Personal repositories skip typing; an unavailable `Bug` type produces a warning without preventing duplicate cleanup.
- Keeps the oldest open issue with an exact matching title and source file. Source links must be standalone URLs into the current repository, with a full commit SHA and a line anchor as generated by `alstr/todo-to-issue-action`. Commit and line changes do not affect matching.
- Comments `Duplicate of #<original>` on newer duplicates and closes them with a link to the original. Reruns reuse an existing matching comment.
- Excludes pull requests and leaves issues without recognized source links open. Only issues labeled `need-fix` are considered; applying that label opts an issue into management.
- Propagates API failures, including permission errors, so failed runs can be retried.

## Usage

Use the [reusable workflow](../../.github/workflows/need-fix-to-issue.md) to include checkout and runner configuration, or call this composite action after checking out the repository. The job requires `contents: read` and `issues: write` permissions. Both steps use the job's `GITHUB_TOKEN`. The action has no outputs.

The optional inputs select the commit diff when running manually:

| Input | Description | Required |
| ------------------- | ------------------------------------------------------------------------------------------------- | -------- |
| `manual-commit-ref` | The SHA of the commit to get the diff for. | No |
| `manual-base-ref` | An earlier SHA to compare against. Defaults to the commit immediately before `manual-commit-ref`. | No |

## Development

Run the tests with Node.js as specified in [package.json](package.json):

```sh
npm test --prefix actions/need-fix-to-issue
```

Tests mock the GitHub APIs and do not modify real issues.
The [CI workflow](../../.github/workflows/__test-action-need-fix-to-issue.yml) uses shared Node.js CI and runs `test:ci` to generate an LCOV report at `coverage/lcov.info` inside the action directory.

## License

[MIT](../../LICENSE)
41 changes: 41 additions & 0 deletions actions/need-fix-to-issue/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Need fix to Issue
description: Create issues from TODO/FIXME comments, assign the Bug issue type, and close duplicates.
author: hoverkraft
branding:
icon: check-circle
color: blue

inputs:
manual-commit-ref:
description: "The SHA of the commit to get the diff for."
required: false
manual-base-ref:
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here."
required: false

runs:
using: composite
steps:
- name: TODO to Issue
uses: alstr/todo-to-issue-action@37bb7b56e58569ef273b60678048030a7f0c261a # v5.1.15
with:
IDENTIFIERS: |
[
{"name": "TODO:", "labels": ["help wanted", "need-fix"]},
{"name": "FIXME:", "labels": ["bug", "need-fix"]}
]
env:
MANUAL_COMMIT_REF: ${{ inputs.manual-commit-ref }}
MANUAL_BASE_REF: ${{ inputs.manual-base-ref }}

- name: Categorize issues and close duplicates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ACTION_PATH: ${{ github.action_path }}
with:
script: |
const { pathToFileURL } = require('node:url');
const { join } = require('node:path');
const moduleUrl = pathToFileURL(join(process.env.ACTION_PATH, 'src', 'IssueManager.js'));
const { IssueManager } = await import(moduleUrl.href);
await new IssueManager({ github, context, core }).run();
16 changes: 16 additions & 0 deletions actions/need-fix-to-issue/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions actions/need-fix-to-issue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "need-fix-to-issue",
"version": "1.0.0",
"private": true,
"description": "Create and categorize TODO/FIXME issues and close duplicates",
"type": "module",
"scripts": {
"test": "node --test",
"pretest:ci": "node -e \"require('node:fs').mkdirSync('coverage', { recursive: true })\"",
"test:ci": "node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test"
},
"author": "hoverkraft",
"license": "MIT",
"engines": {
"node": ">=24.0.0"
}
}
95 changes: 95 additions & 0 deletions actions/need-fix-to-issue/src/IssueManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export class IssueManager {
constructor({ github, context, core }) {
this.github = github;
this.context = context;
this.core = core;
}

async run() {
const issues = await this.github.paginate(
this.github.rest.issues.listForRepo,
{
...this.context.repo,
state: "open",
labels: "need-fix",
sort: "created",
direction: "asc",
per_page: 100,
},
);
if (issues.length === 0) return;
const { data: owner } = await this.github.rest.users.getByUsername({
username: this.context.repo.owner,
});
this.canAssignType = owner.type === "Organization";
const originals = new Map();
for (const issue of issues) {
if (issue.pull_request) continue;
await this.assignBugType(issue);
const key = this.duplicateKey(issue);
if (!key) continue;
const original = originals.get(key);
if (original) {
await this.closeDuplicate(issue, original);
} else {
originals.set(key, issue);
}
}
}

duplicateKey(issue) {
const { owner, repo } = this.context.repo;
const prefix = `${this.context.serverUrl}/${owner}/${repo}/blob/`;
for (const line of (issue.body ?? "").split("\n")) {
if (!line.startsWith(prefix)) continue;
const source = line
.slice(prefix.length)
.trim()
.match(/^[a-f\d]{40}\/(.+)#L\d+(?:-L\d+)?$/i);
if (source) return JSON.stringify([issue.title, source[1]]);
}
return null;
}

async assignBugType(issue) {
if (!this.canAssignType || issue.type) return;
try {
await this.github.rest.issues.update({
...this.context.repo,
issue_number: issue.number,
type: "Bug",
});
} catch (error) {
const invalidType =
error.status === 422 &&
error.response?.data?.errors?.some(({ field }) => field === "type");
if (!invalidType) throw error;
this.canAssignType = false;
this.core.warning(
'The "Bug" issue type is unavailable. Enable it in the organization to categorize generated issues.',
);
}
}

async closeDuplicate(issue, original) {
const parameters = { ...this.context.repo, issue_number: issue.number };
const body = `Duplicate of #${original.number}`;
const comments = await this.github.paginate(
this.github.rest.issues.listComments,
{ ...parameters, per_page: 100 },
);
if (!comments.some((comment) => comment.body === body)) {
await this.github.rest.issues.createComment({ ...parameters, body });
}
await this.github.rest.issues.update({
...parameters,
headers: { "X-GitHub-Api-Version": "2026-03-10" },
state: "closed",
state_reason: "duplicate",
duplicate_issue_id: original.id,
});
this.core.info(
`Closed #${issue.number} as a duplicate of #${original.number}.`,
);
}
}
Loading
Loading