-
Notifications
You must be signed in to change notification settings - Fork 3
feat(roadmap-planner): W4 — sprint card 4 lanes (todo/in_progress/done/cancelled) #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /* | ||
| Copyright 2026 The AlaudaDevops Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| */ | ||
|
|
||
| package contributions | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "github.com/AlaudaDevops/toolbox/roadmap-planner/backend/internal/config" | ||
| ) | ||
|
|
||
| // Lane is the four-bucket sprint classification used by the sprint card | ||
| // and (eventually) any other panel that wants a coarser "what state is | ||
| // this issue in?" answer than Jira's per-issue-type workflow. | ||
| type Lane int | ||
|
|
||
| const ( | ||
| LaneTodo Lane = iota | ||
| LaneInProgress | ||
| LaneDone | ||
| LaneCancelled | ||
| ) | ||
|
|
||
| // String returns the lower-case lane name as it appears on the wire and | ||
| // in config. Useful for warning logs when a status name doesn't match | ||
| // any lane. | ||
| func (l Lane) String() string { | ||
| switch l { | ||
| case LaneTodo: | ||
| return "todo" | ||
| case LaneInProgress: | ||
| return "in_progress" | ||
| case LaneDone: | ||
| return "done" | ||
| case LaneCancelled: | ||
| return "cancelled" | ||
| } | ||
| return "in_progress" | ||
| } | ||
|
|
||
| // StatusClassifier maps a Jira `status.name` to a Lane. Lookups are | ||
| // case-folded. A name that doesn't match any of the four explicit | ||
| // lists falls back to in_progress and is recorded in Unknown so the | ||
| // caller can log a warning once. | ||
| // | ||
| // Construction is via NewStatusClassifier; the default cover-set comes | ||
| // from DefaultStatusLanes and is merged with any operator-supplied | ||
| // overrides (config wins per lane — empty lane in config means "use | ||
| // default for that lane"). | ||
| type StatusClassifier struct { | ||
| bucket map[string]Lane | ||
| } | ||
|
|
||
| // NewStatusClassifier builds a classifier from the configured lanes. | ||
| // Any lane left empty in cfg inherits from DefaultStatusLanes so the | ||
| // operator can override one lane without re-declaring the others. | ||
| func NewStatusClassifier(cfg config.StatusLanes) StatusClassifier { | ||
| d := DefaultStatusLanes() | ||
| merge := func(configured, fallback []string) []string { | ||
| if len(configured) > 0 { | ||
| return configured | ||
| } | ||
| return fallback | ||
| } | ||
| cfg.Todo = merge(cfg.Todo, d.Todo) | ||
| cfg.InProgress = merge(cfg.InProgress, d.InProgress) | ||
| cfg.Done = merge(cfg.Done, d.Done) | ||
| cfg.Cancelled = merge(cfg.Cancelled, d.Cancelled) | ||
|
|
||
| b := make(map[string]Lane, len(cfg.Todo)+len(cfg.InProgress)+len(cfg.Done)+len(cfg.Cancelled)) | ||
| for _, s := range cfg.Todo { | ||
| b[normalizeStatus(s)] = LaneTodo | ||
| } | ||
| for _, s := range cfg.InProgress { | ||
| b[normalizeStatus(s)] = LaneInProgress | ||
| } | ||
| for _, s := range cfg.Done { | ||
| b[normalizeStatus(s)] = LaneDone | ||
| } | ||
| for _, s := range cfg.Cancelled { | ||
| b[normalizeStatus(s)] = LaneCancelled | ||
| } | ||
| return StatusClassifier{bucket: b} | ||
| } | ||
|
|
||
| // Classify returns the lane for the given status name. Unknown | ||
| // statuses fall back to in_progress with `known=false` so the caller | ||
| // can record the miss and surface it to the operator. | ||
| func (c StatusClassifier) Classify(status string) (lane Lane, known bool) { | ||
| if c.bucket == nil { | ||
| return LaneInProgress, false | ||
| } | ||
| l, ok := c.bucket[normalizeStatus(status)] | ||
| if !ok { | ||
| return LaneInProgress, false | ||
| } | ||
| return l, true | ||
| } | ||
|
|
||
| func normalizeStatus(s string) string { | ||
| return strings.ToLower(strings.TrimSpace(s)) | ||
| } | ||
|
|
||
| // DefaultStatusLanes returns the W4 (2026-05-19) baseline mapping | ||
| // derived from a live Jira query against the DEVOPS project. The lists | ||
| // cover all 37 statuses across 17 issue types (Bug, Story, Tech-debt, | ||
| // Improvement, Vulnerability, Task, Sub-task, Pillar, Milestone, | ||
| // Document, Epic, Job, Customer Reported Incident, Platform | ||
| // application, Components, Components-sub-task). Operators can | ||
| // override one lane in the ConfigMap without re-declaring the others. | ||
| func DefaultStatusLanes() config.StatusLanes { | ||
| return config.StatusLanes{ | ||
| Todo: []string{ | ||
| "Backlog", | ||
| "Blocked", | ||
| "Open", | ||
| "待处理", | ||
| "阻塞中", | ||
| }, | ||
| InProgress: []string{ | ||
| "Acceptance Testing", | ||
| "CONFIRM RELEASE", | ||
| "Components-test", | ||
| "DEPLOY", | ||
| "Designing", | ||
| "Developing", | ||
| "Doc Reviewing", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical Issue ( |
||
| "In Progress", | ||
| "In Testing", | ||
| "Mitigated", | ||
| "Ready for Delivery", | ||
| "Ready for Doc Review", | ||
| "Ready for QA", | ||
| "Review/Test Failed", | ||
| "Signed Off", | ||
| "Test Failed", | ||
| "Testing", | ||
| "Under Review", | ||
| "Verify", | ||
| "Wait for Verify", | ||
| "调研中", | ||
| "调研完成", | ||
| "设计完成", | ||
| "开发完成", | ||
| "测试完成", | ||
| "验收完成", | ||
| }, | ||
| Done: []string{ | ||
| "Done", | ||
| "Resolved", | ||
| "using", | ||
| "已完成", | ||
| }, | ||
| Cancelled: []string{ | ||
| "Cancelled", | ||
| "已取消", | ||
| }, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| Copyright 2026 The AlaudaDevops Authors. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| */ | ||
|
|
||
| package contributions | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/AlaudaDevops/toolbox/roadmap-planner/backend/internal/config" | ||
| ) | ||
|
|
||
| func TestStatusClassifier(t *testing.T) { | ||
| c := NewStatusClassifier(config.StatusLanes{}) | ||
|
|
||
| cases := []struct { | ||
| status string | ||
| want Lane | ||
| known bool | ||
| }{ | ||
| // English defaults. | ||
| {"Backlog", LaneTodo, true}, | ||
| {"Blocked", LaneTodo, true}, | ||
| {"In Progress", LaneInProgress, true}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion ( |
||
| {"Acceptance Testing", LaneInProgress, true}, | ||
| {"Ready for Delivery", LaneInProgress, true}, | ||
| {"Done", LaneDone, true}, | ||
| {"Resolved", LaneDone, true}, | ||
| {"Cancelled", LaneCancelled, true}, | ||
|
|
||
| // Chinese (DEVOPS Epic workflow). | ||
| {"待处理", LaneTodo, true}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion ( |
||
| {"调研中", LaneInProgress, true}, | ||
| {"已完成", LaneDone, true}, | ||
| {"已取消", LaneCancelled, true}, | ||
|
|
||
| // Case + whitespace tolerance. | ||
| {" done ", LaneDone, true}, | ||
| {"IN PROGRESS", LaneInProgress, true}, | ||
|
|
||
| // Unknown status falls back to in_progress and reports known=false. | ||
| {"made up status", LaneInProgress, false}, | ||
| {"", LaneInProgress, false}, | ||
| } | ||
|
|
||
| for _, tc := range cases { | ||
| got, known := c.Classify(tc.status) | ||
| if got != tc.want || known != tc.known { | ||
| t.Errorf("Classify(%q) = (%s, %v), want (%s, %v)", | ||
| tc.status, got, known, tc.want, tc.known) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestStatusClassifierConfigOverrideOneLane(t *testing.T) { | ||
| // Operator overrides only `todo` — the other three lanes must fall | ||
| // back to the defaults so the operator doesn't have to re-declare | ||
| // them. | ||
| c := NewStatusClassifier(config.StatusLanes{ | ||
| Todo: []string{"Inbox"}, | ||
| }) | ||
| if got, _ := c.Classify("Inbox"); got != LaneTodo { | ||
| t.Errorf("Classify(Inbox) = %s, want todo", got) | ||
| } | ||
| if got, _ := c.Classify("Done"); got != LaneDone { | ||
| t.Errorf("override should not drop default Done lane; got %s", got) | ||
| } | ||
| if got, _ := c.Classify("Cancelled"); got != LaneCancelled { | ||
| t.Errorf("override should not drop default Cancelled lane; got %s", got) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion (
refactor/clarity): Consider logging a warning whenClassifyreturnsknown=falsefor unknown statuses, as mentioned in the code comments. This would help operators discover missing status mappings in their Jira workflow.