Add structured close reason for issues (GitHub-compatible state_reason)#37041
Add structured close reason for issues (GitHub-compatible state_reason)#37041a1012112796 wants to merge 15 commits intogo-gitea:mainfrom
Conversation
Closes can now carry one of six structured reasons: `completed`,
`not_planned`, `duplicate`, `answered`, `completed_by_commit` (system),
and `completed_by_pull` (system). The last two are written automatically
by the commit-reference and PR-merge paths respectively; the first four
are user-selectable via the issue detail page and REST API.
## Changes
### Data model
- `Issue` gains `close_reason` (int enum, indexed) and
`close_reason_param` (TEXT, JSON-serialised per-reason params).
- `CommentMetaData` gains matching `close_reason` / `close_reason_param`
fields so each close event in the timeline retains its own reason
snapshot, even after reopen.
- New `CommentTypeCloseWithReason` (39) keeps old `CommentTypeClose`
intact for pre-existing history.
- Migration v330 adds the two columns to the `issue` table.
### Service / validation layer (`services/issue/close_reason.go`)
- `IssueCloseOptions` struct with `Reason` + `ReasonParam`.
- `Normalize()` fills the default (`completed`) when reason is absent.
- `Validate()` enforces: duplicate → same-repo, non-PR, non-self issue;
answered → comment must belong to the current issue; system reasons →
validated internally.
- `IsSystemOnly()` blocks `completed_by_commit` / `completed_by_pull`
from being written by external API or Web requests.
- Constructor helpers: `CloseOptionsCompleted()`, `CloseOptionsDuplicate()`,
`CloseOptionsAnswered()`, `CloseOptionsCompletedByCommit()`,
`CloseOptionsCompletedByPull()`.
### CloseIssue / ReopenIssue call chain
- `services/issue/status.go`: `CloseIssue` gains an `IssueCloseOptions`
param; PRs are unconditionally stripped of any reason (design choice).
- `models/issues/issue_update.go`: `SetIssueAsClosed` persists reason
into the issue row and selects the right comment type; `setIssueAsReopen`
clears both fields; `IsMergePull` legacy flag preserved for
`CommentTypeMergePull`.
### REST API
- `modules/structs/issue.go`: `Issue` gets `state_reason` /
`state_reason_param`; `EditIssueOption` gets `state_reason` /
`state_reason_param`.
- `services/convert/issue.go`: closed issues with empty `close_reason`
default to `"completed"` in the API response.
- `routers/api/v1/repo/issue.go`: `EditIssue` parses the new fields,
rejects system-only reasons, and forwards to the service layer.
### Web UI
- Split-button close group in `view_content.tmpl` for open, non-PR
issues: main button + dropdown (Completed / Not planned / Duplicate /
Answered).
- `Duplicate` opens a `<dialog>` modal with a debounced issue-search
input; results exclude PRs and the current issue; confirmation writes
the serialised `{"issue_index": N}` param.
- `Answered` is shown only when the comment editor is non-empty; the
backend auto-binds the newly created comment's ID.
- `routers/web/repo/issue_comment.go`: parses `state_reason` /
`state_reason_param` from the form; rejects system-only reasons.
- `services/forms/repo_form.go`: `CreateCommentForm` gains `StateReason`
and `StateReasonParam`.
### Display
- `view_title.tmpl`: closed-issue badge uses reason text and toggles
`octicon-skip` + neutral grey background for `not_planned` /
`duplicate`, `octicon-issue-closed` + purple for the rest.
- `view_content/comments.tmpl`: `CommentTypeCloseWithReason` renders a
single-sentence close phrase with links (issue link for duplicate,
comment anchor for answered, SHA link for commit, PR link for pull).
- `templates/shared/issueicon.tmpl`: issue list icons follow the same
icon/colour rules.
- `web_src/css/repo.css`: `issue-state-label-closed-completed` (purple),
`issue-state-label-closed-neutral` (grey), duplicate-modal and
duplicate-list item styles.
### Locale
- 34 new keys under `repo.issues.close_reason.*` covering button labels,
timeline phrases, page-header phrases, and validation messages.
### Auto-close paths
- `services/issue/commit.go`: commit-reference close passes
`CloseOptionsCompletedByCommit(sha)`.
- `services/pull/merge.go`: PR-merge cross-reference close passes
`CloseOptionsCompletedByPull(pr.Index)`.
- `routers/web/repo/issue_list.go`: bulk close passes
`CloseOptionsCompleted()`.
### Tests
- `services/issue/close_reason_test.go`: 25 sub-tests covering
Normalize, IsSystemOnly, constructors, no-DB validation paths, and
DB-backed duplicate / answered validation.
- `services/issue/status_test.go`: full close/reopen cycle for all six
reasons; PR close leaves no reason stored.
- `services/convert/issue_test.go`: legacy-closed (empty reason) →
`state_reason=completed`; param pass-through.
- `tests/integration/api_issue_test.go`: API set/read of `duplicate`
reason; system-only reason rejection.
- `tests/integration/issue_test.go`: Web form close with `duplicate` and
`answered` (auto-bound comment).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
wxiaoguang
left a comment
There was a problem hiding this comment.
Horrible AI slop .......
Please review it by yourself line-by-line first
|
It seems many people wrongly expect AI to be able to one-shot complex issues, but it's far from that as of today. Still needs careful review. With Claude Code, I can strongly recommend running |
|
Another problem (even without AI) is, people don't fix their own bugs ....
So when using AI, there will be far more bugs become "nobody cares" |
Badge and label colours introduced in 8f27e66 are reverted to the original red (`tw-bg-red tw-text-white` / `ui red label`) so that the UI stays visually consistent with existing closed-issue style. The icon-type switch (octicon-skip for duplicate/not_planned vs octicon-issue-closed for other reasons) is intentionally preserved.left color related design in future. Changes: - comments.tmpl: drop issue-close-reason-badge-neutral/completed, badge colour stays tw-bg-red tw-text-white - view_title.tmpl: drop issue-state-label-closed-completed/neutral class assignments, label colour stays ui red label issue-state-label - issueicon.tmpl: tw-text-text-light / tw-text-purple → tw-text-red - issue.ts: revert getIssueColorClass to original (returns tw-text-red for all closed issues) - repo.css: remove issue-state-label-closed-completed/neutral rules Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add clearer action labels and descriptions, show the selected close reason in the dropdown, and keep the completed state selected when answered becomes unavailable. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Use dedicated locale keys for fallback close-reason text so rendered labels stay natural across the issue title view. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Wrap the issue action footer and make the close-reason button group responsive so the controls stay usable on narrow screens. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds GitHub-compatible, structured close reasons for issues (state_reason + optional params), persisting them on the issue and on close-event timeline entries, and exposing them via REST API and the web UI.
Changes:
- Adds
close_reason/close_reason_parampersistence on issues and close timeline comments, plus migration v330. - Introduces service-layer close reason options + validation, and wires reasons through close/reopen flows (API, web, commit auto-close, PR merge xrefs, bulk close).
- Updates UI/UX to select close reason (including duplicate modal/search), updates API structs + swagger, and adds unit/integration tests.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| web_src/js/types.ts | Adds state_reason to the frontend Issue type. |
| web_src/js/features/repo-issue.ts | Implements close-reason split button, duplicate modal/search, and submit handling. |
| web_src/js/features/issue.ts | Adjusts issue icon selection based on state_reason. |
| web_src/css/repo.css | Adds styling for close-reason split button and duplicate modal. |
| tests/integration/issue_test.go | Adds web integration coverage for duplicate/answered/system-only close reasons. |
| tests/integration/api_issue_test.go | Adds API integration coverage for setting/reading duplicate close reason. |
| tests/integration/actions_trigger_test.go | Updates CloseIssue calls for new signature. |
| templates/swagger/v1_json.tmpl | Documents state_reason / state_reason_param in swagger output. |
| templates/shared/issueicon.tmpl | Updates issue icon rendering to use skip icon for duplicate/not planned. |
| templates/repo/issue/view_title.tmpl | Displays close state label text/links based on close reason. |
| templates/repo/issue/view_content/comments.tmpl | Renders close timeline entry with reason-specific phrases/links for new comment type. |
| templates/repo/issue/view_content.tmpl | Adds close-reason split button UI and duplicate <dialog> modal. |
| services/pull/pull.go | Updates PR-closing paths for new CloseIssue signature. |
| services/pull/merge.go | Passes completed_by_pull reason for cross-ref closes; adapts merge close path to new model API. |
| services/issue/status.go | Extends CloseIssue to accept close-reason options (strips for PRs). |
| services/issue/status_test.go | Adds service-level close/reopen cycle tests for all reasons + PR exclusion. |
| services/issue/commit.go | Uses completed_by_commit reason for commit-reference auto-close. |
| services/issue/close_reason.go | Introduces close-reason options, normalization, validation, and constructors. |
| services/issue/close_reason_test.go | Adds unit tests for normalize/system-only/constructors/validation. |
| services/forms/repo_form.go | Extends comment form to accept state_reason / state_reason_param. |
| services/convert/issue.go | Exposes state_reason (defaults legacy to completed) and parses params for API output. |
| services/convert/issue_test.go | Tests API conversion defaulting + param pass-through. |
| routers/web/repo/issue_list.go | Updates bulk close to pass completed close reason. |
| routers/web/repo/issue_comment.go | Parses/validates close reason from web form; auto-binds answered comment id. |
| routers/api/v1/repo/pull.go | Updates helper call for new close/reopen signature. |
| routers/api/v1/repo/issue.go | Accepts/validates state_reason + param on edit/close operations. |
| options/locale/locale_en-US.json | Adds locale strings for close reason UI and timeline phrases. |
| modules/structs/issue.go | Adds API fields state_reason / state_reason_param and edit options. |
| models/migrations/v1_26/v330.go | Adds migration to create close reason columns on issue. |
| models/migrations/migrations.go | Registers migration 330. |
| models/issues/issue.go | Adds CloseReason + CloseReasonParam fields to Issue model. |
| models/issues/issue_xref_test.go | Updates model CloseIssue calls for new signature. |
| models/issues/issue_update.go | Persists close reason on close, clears on reopen, adds new close comment type. |
| models/issues/dependency_test.go | Updates model CloseIssue calls for new signature. |
| models/issues/comment.go | Adds CommentTypeCloseWithReason and stores close reason snapshot in comment metadata. |
| models/issues/close_reason.go | Adds enum + parsing for structured close reasons. |
| models/issues/close_reason_display.go | Adds helpers for displaying reasons and extracting params from JSON. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch o.Reason { | ||
| case CloseReasonCompleted, CloseReasonNotPlanned: | ||
| // no param required | ||
|
|
There was a problem hiding this comment.
For completed / not_planned, Validate() currently accepts any non-empty ReasonParam and will allow storing arbitrary JSON in issue.close_reason_param for reasons that don’t use params. Consider rejecting non-empty params for these reasons (or normalizing by clearing ReasonParam) to keep persisted data consistent.
routers/web/repo/issue_comment.go
Outdated
| } | ||
| opts.Normalize() | ||
| if opts.IsSystemOnly() { | ||
| ctx.JSONError("this close reason is system-only") |
There was a problem hiding this comment.
The user-facing JSON error "this close reason is system-only" is hard-coded English and isn’t consistent with the other localized close-reason errors in this handler. Consider using a locale key via ctx.Tr(...) (and/or returning a structured error code) so the UI can display a translated message.
| ctx.JSONError("this close reason is system-only") | |
| ctx.JSONError(ctx.Tr("repo.issues.close_reason.system_only")) |
Being used to the GitHub UI, I personally dislike buttons that have so much text in them, the pull request merge button has the same issue. Therefor I recommend making the button look and behave like GitHub's.
You can just use the existing color variables from the states, no need to define any new colors. And if you want to do browser testing with an AI agent, I recommend using https://github.com/microsoft/playwright-mcp. |
Make close-reason dropdown items submit immediately, remove the persistent selected-state UI, and align the duplicate confirm flow and styling with the main close action. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>



Closes can now carry one of six structured reasons:
completed,not_planned,duplicate,answered,completed_by_commit(system), andcompleted_by_pull(system). The last two are written automatically by the commit-reference and PR-merge paths respectively; the first four are user-selectable via the issue detail page and REST API.shotcuts
ux updates: