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
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ func (s *Service) MemberExtras(ctx context.Context, memberID string, q storage.M
// sprintCounts gathers (wip, done, prs_open, prs_merged) for a member
// scoped to one sprint. WIP / Done come from the *latest* snapshot
// per issue under that sprint id; PR counts come from pull_requests
// linked via epic_key (best-effort).
// linked via jira_key (best-effort).
func (s *Service) sprintCounts(ctx context.Context, db *sql.DB, dialect storage.Dialect, memberID, sprintID string) (*SprintStats, error) {
out := &SprintStats{Name: sprintID} // default name = id; we overwrite below if we see something better

Expand Down Expand Up @@ -529,16 +529,16 @@ func (s *Service) sprintCounts(ctx context.Context, db *sql.DB, dialect storage.
}

// PR counts: look at pull_requests opened by this member that
// reference any issue assigned to the sprint via epic_key. This
// only works for projects where the linker resolves an epic key
// reference any issue assigned to the sprint via jira_key. This
// only works for projects where the linker resolves a jira key
// onto every PR — when it doesn't, the counts stay at zero.
prQ := rebindSimple(dialect, `
SELECT
SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN merged_at IS NULL AND closed_at IS NULL THEN 1 ELSE 0 END)
FROM pull_requests
WHERE author_id = ?
AND epic_key IN (
AND jira_key IN (
SELECT DISTINCT issue_key FROM issue_snapshots
WHERE sprint_id = ? AND assignee_id = ?
)`)
Expand Down
2 changes: 1 addition & 1 deletion roadmap-planner/backend/internal/github/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (s *Syncer) Sync(ctx context.Context) error {
Additions: pr.Additions,
Deletions: pr.Deletions,
ChangedFiles: pr.ChangedFiles,
EpicKey: epicKey,
JiraKey: epicKey,
CreatedAt: pr.CreatedAt,
MergedAt: pr.MergedAt,
ClosedAt: pr.ClosedAt,
Expand Down
2 changes: 1 addition & 1 deletion roadmap-planner/backend/internal/gitlab/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (s *Syncer) Sync(ctx context.Context) error {
AuthorLogin: authorLogin,
HeadBranch: mr.SourceBranch,
BaseBranch: mr.TargetBranch,
EpicKey: epicKey,
JiraKey: epicKey,
CreatedAt: mr.CreatedAt,
MergedAt: mr.MergedAt,
ClosedAt: mr.ClosedAt,
Expand Down
6 changes: 3 additions & 3 deletions roadmap-planner/backend/internal/storage/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest
INSERT INTO pull_requests (
id, source, repo_id, number, title, state, author_id, author_login,
head_branch, base_branch, additions, deletions, changed_files,
epic_key, created_at, first_review_at, merged_at, closed_at, fetched_at
jira_key, created_at, first_review_at, merged_at, closed_at, fetched_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
source = excluded.source,
Expand All @@ -135,7 +135,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest
additions = excluded.additions,
deletions = excluded.deletions,
changed_files = excluded.changed_files,
epic_key = excluded.epic_key,
jira_key = excluded.jira_key,
first_review_at = excluded.first_review_at,
merged_at = excluded.merged_at,
closed_at = excluded.closed_at,
Expand All @@ -153,7 +153,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest
_, err := stmt.ExecContext(ctx,
p.ID, source, p.RepoID, p.Number, p.Title, p.State, nullable(p.AuthorID), nullable(p.AuthorLogin),
nullable(p.HeadBranch), nullable(p.BaseBranch), p.Additions, p.Deletions, p.ChangedFiles,
nullable(p.EpicKey), p.CreatedAt, p.FirstReviewAt, p.MergedAt, p.ClosedAt, p.FetchedAt,
nullable(p.JiraKey), p.CreatedAt, p.FirstReviewAt, p.MergedAt, p.ClosedAt, p.FetchedAt,
)
if err != nil {
return fmt.Errorf("upsert pr %s: %w", p.ID, err)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- ----------------------------------------------------------------------
-- 0006_rename_epic_key_to_jira_key — W5 (2026-05-19).
--
-- The column was misnamed at birth: the linker pulls *any* matching Jira
-- key out of the source branch / title regex, not specifically an Epic
-- key. In a 4411-PR-linked sample from prod, the distribution was:
-- Story 2817, Bug 996, Job 199, Technical Debt 195, Document 118,
-- Sub-task 57, Task 25, Improvement 4, **Epic 0**.
--
-- Zero rows ever pointed at an actual Epic. The misnomer wasted future
-- reader time and was load-bearing in one place — `sprintCounts` joins
-- `pull_requests.epic_key = issue_snapshots.issue_key` and happened to
-- be correct only because most sprint members are Stories.
--
-- This rename is internal — no API client reads the JSON tag (verified
-- by grep on `frontend/` + the docs/). Safe to flip in one migration.
-- ----------------------------------------------------------------------

ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key;

DROP INDEX IF EXISTS idx_pr_epic;
CREATE INDEX IF NOT EXISTS idx_pr_jira ON pull_requests(jira_key);
11 changes: 7 additions & 4 deletions roadmap-planner/backend/internal/storage/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@ type IssueSnapshot struct {
ResolvedAt *time.Time
}

// PullRequest is a GitHub PR or GitLab MR record. Linked to an Epic via
// EpicKey when the configured Linker can resolve one (branch regex,
// title, etc.).
// PullRequest is a GitHub PR or GitLab MR record. Linked to a Jira
// issue via JiraKey when the configured Linker can resolve one
// (branch regex, title, etc.). JiraKey is *not* restricted to Epics —
// the regex matches any DEVOPS-NNN id, which in prod is mostly
// Stories / Bugs (see migration 0006 for the audit data behind the
// rename from `epic_key`).
//
// AuthorLogin is the raw login string returned by the source API (lower-
// cased on write). It's stored alongside the resolved AuthorID so the
Expand All @@ -120,7 +123,7 @@ type PullRequest struct {
Additions int
Deletions int
ChangedFiles int
EpicKey string
JiraKey string // any Jira key matched by the linker; was misnamed `EpicKey` pre-W5
CreatedAt time.Time
FirstReviewAt *time.Time
MergedAt *time.Time
Expand Down
2 changes: 1 addition & 1 deletion roadmap-planner/backend/internal/storage/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestSQLiteRoundTrip(t *testing.T) {
Number: 173, Title: "ci: smoke test", State: "merged",
AuthorID: "alice", HeadBranch: "DEVOPS-1-smoke", BaseBranch: "main",
Additions: 42, Deletions: 8, ChangedFiles: 3,
EpicKey: "DEVOPS-1",
JiraKey: "DEVOPS-1",
CreatedAt: now.Add(-12 * time.Hour),
FirstReviewAt: &first,
MergedAt: &merged,
Expand Down
62 changes: 31 additions & 31 deletions roadmap-planner/docs/team-analytics/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,37 @@ should consult this when upgrading.

Newest entries first.

## W5 — `pull_requests.epic_key` → `jira_key` rename

**What changed**

- Migration `0006_rename_epic_key_to_jira_key.sql` renames the column
via `ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key`
(portable in both SQLite ≥3.25 and Postgres), drops the
`idx_pr_epic` index, and re-creates it as `idx_pr_jira`.
- Go struct field `PullRequest.EpicKey` → `PullRequest.JiraKey`, with
no alias (hard rename).
- Every SQL reference and assignment in the github / gitlab syncers,
the storage upsert, and the `sprintCounts` join updates in lock-step.

**Why**

The column was misnamed at birth: the linker pulls *any* Jira key out
of the source branch / title via `[A-Z]+-\d+`, so the value is mostly
a Story or Bug, never an Epic. Audit on prod (4411 linked PRs) showed
**zero** rows pointing at an actual Epic. The misnomer wasted reader
time and was load-bearing in one place — `sprintCounts` happened to be
correct only because most sprint members are Stories.

**Backward compatibility**

The JSON tag `epic_key` flips to `jira_key`. We grep'd `frontend/` and
no API consumer reads it (the PR list endpoint never exposed it; only
counts are surfaced). Safe one-shot rename.

**Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md`
finding B8.

## W6 — GitLab parity: `hydrate_diff` default flips to `true`

**What changed**
Expand Down Expand Up @@ -44,37 +75,6 @@ merged MRs.
**Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md`
finding B12.

## W5 — `pull_requests.epic_key` → `jira_key` rename

**What changed**

- Migration `0006_rename_epic_key_to_jira_key.sql` renames the column
via `ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key`
(portable in both SQLite ≥3.25 and Postgres), drops the
`idx_pr_epic` index, and re-creates it as `idx_pr_jira`.
- Go struct field `PullRequest.EpicKey` → `PullRequest.JiraKey`, with
no alias (hard rename).
- Every SQL reference and assignment in the github / gitlab syncers,
the storage upsert, and the `sprintCounts` join updates in lock-step.

**Why**

The column was misnamed at birth: the linker pulls *any* Jira key out
of the source branch / title via `[A-Z]+-\d+`, so the value is mostly
a Story or Bug, never an Epic. Audit on prod (4411 linked PRs) showed
**zero** rows pointing at an actual Epic. The misnomer wasted reader
time and was load-bearing in one place — `sprintCounts` happened to be
correct only because most sprint members are Stories.

**Backward compatibility**

The JSON tag `epic_key` flips to `jira_key`. We grep'd `frontend/` and
no API consumer reads it (the PR list endpoint never exposed it; only
counts are surfaced). Safe one-shot rename.

**Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md`
finding B8.

## W2 — Bot consolidation under one synthetic `bot` member

**What changed**
Expand Down
Loading