Skip to content

Commit e33f034

Browse files
committed
Update bug tracker workflow
1 parent 20a5a1f commit e33f034

6 files changed

Lines changed: 155 additions & 39 deletions

File tree

.github/workflows/bug-tracker-issues.yml

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111
issues: write
1212

1313
jobs:
14-
create-issues:
14+
sync-issues:
1515
runs-on: ubuntu-latest
1616
env:
1717
BUG_TRACKER_ASSIGNEE: ${{ vars.BUG_TRACKER_ASSIGNEE || github.actor }}
@@ -38,14 +38,66 @@ jobs:
3838
script: |
3939
const fs = require('fs');
4040
const issueFile = `${process.env.RUNNER_TEMP}/bug-tracker-issues.json`;
41-
const issues = JSON.parse(fs.readFileSync(issueFile, 'utf8'));
41+
const payload = JSON.parse(fs.readFileSync(issueFile, 'utf8'));
42+
const issues = payload.create || [];
4243
4344
if (!issues.length) {
4445
core.info('No new bug tracker rows found.');
45-
return;
46+
} else {
47+
const assignee = process.env.BUG_TRACKER_ASSIGNEE;
48+
49+
for (const issue of issues) {
50+
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue in:title "[Bug Tracker] ${issue.id}"`;
51+
const existing = await github.rest.search.issuesAndPullRequests({
52+
q: query,
53+
per_page: 1,
54+
});
55+
56+
if (existing.data.items.length) {
57+
core.info(`Issue already exists for ${issue.id}: ${existing.data.items[0].html_url}`);
58+
continue;
59+
}
60+
61+
let created;
62+
try {
63+
created = await github.rest.issues.create({
64+
owner: context.repo.owner,
65+
repo: context.repo.repo,
66+
title: issue.title,
67+
body: issue.body,
68+
assignees: assignee ? [assignee] : undefined,
69+
});
70+
} catch (error) {
71+
if (assignee) {
72+
core.warning(`Could not assign ${issue.id} to ${assignee}: ${error.message}`);
73+
created = await github.rest.issues.create({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
title: issue.title,
77+
body: issue.body,
78+
});
79+
} else {
80+
throw error;
81+
}
82+
}
83+
84+
core.info(`Created ${issue.id}: ${created.data.html_url}`);
85+
}
4686
}
4787
48-
const assignee = process.env.BUG_TRACKER_ASSIGNEE;
88+
- name: Close issues for resolved bug rows
89+
uses: actions/github-script@v7
90+
with:
91+
script: |
92+
const fs = require('fs');
93+
const issueFile = `${process.env.RUNNER_TEMP}/bug-tracker-issues.json`;
94+
const payload = JSON.parse(fs.readFileSync(issueFile, 'utf8'));
95+
const issues = payload.close || [];
96+
97+
if (!issues.length) {
98+
core.info('No bug tracker rows moved to a closed status.');
99+
return;
100+
}
49101
50102
for (const issue of issues) {
51103
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue in:title "[Bug Tracker] ${issue.id}"`;
@@ -54,33 +106,33 @@ jobs:
54106
per_page: 1,
55107
});
56108
57-
if (existing.data.items.length) {
58-
core.info(`Issue already exists for ${issue.id}: ${existing.data.items[0].html_url}`);
109+
if (!existing.data.items.length) {
110+
core.warning(`No existing GitHub issue found for ${issue.id}; skipping close.`);
59111
continue;
60112
}
61113
62-
let created;
63-
try {
64-
created = await github.rest.issues.create({
65-
owner: context.repo.owner,
66-
repo: context.repo.repo,
67-
title: issue.title,
68-
body: issue.body,
69-
assignees: assignee ? [assignee] : undefined,
70-
});
71-
} catch (error) {
72-
if (assignee) {
73-
core.warning(`Could not assign ${issue.id} to ${assignee}: ${error.message}`);
74-
created = await github.rest.issues.create({
75-
owner: context.repo.owner,
76-
repo: context.repo.repo,
77-
title: issue.title,
78-
body: issue.body,
79-
});
80-
} else {
81-
throw error;
82-
}
114+
const current = existing.data.items[0];
115+
const issueNumber = current.number;
116+
117+
await github.rest.issues.createComment({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
issue_number: issueNumber,
121+
body: issue.comment,
122+
});
123+
124+
if (current.state === 'closed') {
125+
core.info(`Issue already closed for ${issue.id}: ${current.html_url}`);
126+
continue;
83127
}
84128
85-
core.info(`Created ${issue.id}: ${created.data.html_url}`);
129+
const updated = await github.rest.issues.update({
130+
owner: context.repo.owner,
131+
repo: context.repo.repo,
132+
issue_number: issueNumber,
133+
state: 'closed',
134+
state_reason: 'completed',
135+
});
136+
137+
core.info(`Closed ${issue.id}: ${updated.data.html_url}`);
86138
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
id,area,priority,status,fixtures,observed_behavior,intended_behavior,current_spec,regression_specs,notes,last_reviewed
2-
BG003,2D Settings,P1,Closed,"nn-angulartesting-tn93-edgelist","The original contract assumed Width By = Distance started in Reciprocal mode; explicit reciprocal toggling now confirms the mapping is correct.","Smallest distance edge should be widest when reciprocal is on and thinnest when it is off.",cypress/e2e/journeys/flows/behavior-contracts.cy.ts,"cypress/e2e/journeys/flows/behavior-contracts.cy.ts; cypress/e2e/journeys/flows/link-label-width.cy.ts","Closed as a test-assumption issue after debugging the live edge width data. The reciprocal toggle path remains covered explicitly.",2026-03-13
3-
BG001,Filtering,P0,Closed,"AngularTesting_DistanceMatrix_TN93_BS.xlsx; AngularTesting_nodelist_withseqs_TN93_BS.csv","Merged matrix-plus-sequence links treated Genetic Distance as a non-distance fallback origin, so thresholding kept all 91 links visible.","Visible links should drop from 17 to 9 while polygons remain unchanged.",cypress/e2e/journeys/flows/change-link-threshold.cy.ts,"cypress/e2e/journeys/flows/change-link-threshold.cy.ts; cypress/e2e/journeys/flows/behavior-contracts.cy.ts","Fixed by tracking all distance-backed origins on merged links so thresholding no longer preserves Genetic Distance as a fallback origin.",2026-03-13
4-
BG002,Filtering,P1,Closed,"AngularTesting_seqs_TN93_BS.fasta; AngularTesting_nodelist_withseqs_TN93_BS.csv","Post-launch metric switch previously changed only the threshold widget, leaving sequence-derived link distances in the launch metric.","Post-launch switch should yield coherent visibility: TN93 sequence node-list should return to 11 links under SNPs and fasta should return to 17 links under TN93 in the configured steps.",cypress/e2e/journeys/flows/post-launch-distance-metric-switch.cy.ts,"cypress/e2e/journeys/flows/post-launch-distance-metric-switch.cy.ts","Fixed by rebuilding sequence-derived genetic links when the default distance metric changes; this coverage is now promoted into the regular smoke flow.",2026-03-13
1+
id,area,priority,status,fixtures,observed_behavior,intended_behavior,current_spec,regression_specs,cause_summary,fix_summary,notes,last_reviewed
2+
BG003,2D Settings,P1,Closed,"nn-angulartesting-tn93-edgelist","The original contract assumed Width By = Distance started in Reciprocal mode; explicit reciprocal toggling now confirms the mapping is correct.","Smallest distance edge should be widest when reciprocal is on and thinnest when it is off.",cypress/e2e/journeys/flows/behavior-contracts.cy.ts,"cypress/e2e/journeys/flows/behavior-contracts.cy.ts; cypress/e2e/journeys/flows/link-label-width.cy.ts","The contract encoded the wrong default assumption: reciprocal width mapping was not enabled until explicitly toggled.","Corrected the contract to exercise the explicit reciprocal toggle path instead of asserting a nonexistent default state.","Closed as a test-assumption issue after debugging the live edge width data. The reciprocal toggle path remains covered explicitly.",2026-03-13
3+
BG001,Filtering,P0,Closed,"AngularTesting_DistanceMatrix_TN93_BS.xlsx; AngularTesting_nodelist_withseqs_TN93_BS.csv","Merged matrix-plus-sequence links treated Genetic Distance as a non-distance fallback origin, so thresholding kept all 91 links visible.","Visible links should drop from 17 to 9 while polygons remain unchanged.",cypress/e2e/journeys/flows/change-link-threshold.cy.ts,"cypress/e2e/journeys/flows/change-link-threshold.cy.ts; cypress/e2e/journeys/flows/behavior-contracts.cy.ts","Merged links kept a generic Genetic Distance fallback origin instead of preserving the real distance-backed origins, so threshold filtering never excluded them.","Tracked all distance-backed origins on merged links and thresholded against the active origins instead of the fallback label.","Fixed by tracking all distance-backed origins on merged links so thresholding no longer preserves Genetic Distance as a fallback origin.",2026-03-13
4+
BG002,Filtering,P1,Closed,"AngularTesting_seqs_TN93_BS.fasta; AngularTesting_nodelist_withseqs_TN93_BS.csv","Post-launch metric switch previously changed only the threshold widget, leaving sequence-derived link distances in the launch metric.","Post-launch switch should yield coherent visibility: TN93 sequence node-list should return to 11 links under SNPs and fasta should return to 17 links under TN93 in the configured steps.",cypress/e2e/journeys/flows/post-launch-distance-metric-switch.cy.ts,"cypress/e2e/journeys/flows/post-launch-distance-metric-switch.cy.ts","Changing the default distance metric after launch updated the widget state but did not rebuild the sequence-derived genetic links in the new metric.","Rebuilt sequence-derived genetic links whenever the default distance metric changes so the rendered links and visibility counts stay coherent.","Fixed by rebuilding sequence-derived genetic links when the default distance metric changes; this coverage is now promoted into the regular smoke flow.",2026-03-13

docs/AGENTS.MD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ When a test fails, do not immediately weaken the test. First identify which laye
111111
* For suspected app bugs, document divergence explicitly
112112

113113
* Example: widget changed but SVG did not update, or Cytoscape moved but `session.data.nodes[*].x/y` stayed stale.
114-
* Record it in `docs/2d-network-cypress-bug-log.csv` with the fixture, observed behavior, intended behavior if known, and the regression spec path.
114+
* Record it in `docs/2d-network-cypress-bug-log.csv` with the fixture, observed behavior, intended behavior if known, the regression spec path, and fill in `cause_summary` / `fix_summary` before closing the bug row.
115115

116116
Tip: keep diagnostic logs inside the spec while debugging, but remove noisy logging before committing.
117117

docs/cypress-architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ Do not leave broken legacy specs under active `*.cy.ts` paths.
9393
- Prefer retryable assertions over one-shot reads when UI state is expected to change.
9494
- Use `commonService` as a cross-check, not the primary source of truth, for user-visible 2D behavior.
9595
- Prelaunch session mutation is allowed only as a narrow fallback inside shared helpers when the UI does not fully persist launch settings yet.
96-
- When a maintained journey exposes a product bug, record it in `docs/2d-network-cypress-bug-log.csv` with the observed behavior, intended behavior if known, the spec that caught it, and the regression specs that must stay green after a fix.
97-
- New bug-log rows pushed to GitHub automatically open GitHub issues through `.github/workflows/bug-tracker-issues.yml`. Set the repository variable `BUG_TRACKER_ASSIGNEE` to force assignment to a specific GitHub login; otherwise the workflow assigns the issue to the push actor.
96+
- When a maintained journey exposes a product bug, record it in `docs/2d-network-cypress-bug-log.csv` with the observed behavior, intended behavior if known, the spec that caught it, the regression specs that must stay green after a fix, and explicit `cause_summary` / `fix_summary` fields once the bug is understood.
97+
- New bug-log rows pushed to GitHub automatically open GitHub issues through `.github/workflows/bug-tracker-issues.yml`. When a row moves to `Closed`, `Fixed`, or `Resolved`, the workflow comments on the matching issue with the recorded root cause and fix summary, then closes it. Set the repository variable `BUG_TRACKER_ASSIGNEE` to force assignment to a specific GitHub login; otherwise the workflow assigns the issue to the push actor.
9898

9999
## Maintained Commands
100100

5.81 KB
Binary file not shown.

scripts/bug_tracker_issue_candidates.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def render_bullets(items):
6767
return "\n".join(f"- {item}" for item in items)
6868

6969

70+
def is_closed_status(value: str):
71+
return (value or "").strip().lower() in {"closed", "fixed", "resolved"}
72+
73+
7074
def build_issue(row, csv_path: str, repository: str, branch: str, commit_sha: str, actor: str):
7175
bug_id = row["id"].strip()
7276
area = (row.get("area") or "").strip()
@@ -78,6 +82,8 @@ def build_issue(row, csv_path: str, repository: str, branch: str, commit_sha: st
7882
regression_specs = as_list(row.get("regression_specs") or "")
7983
fixtures = as_list(row.get("fixtures") or "")
8084
notes = (row.get("notes") or "").strip()
85+
cause_summary = (row.get("cause_summary") or "").strip()
86+
fix_summary = (row.get("fix_summary") or "").strip()
8187
reviewed = (row.get("last_reviewed") or "").strip()
8288
line_number = row["_line_number"]
8389

@@ -122,6 +128,12 @@ def build_issue(row, csv_path: str, repository: str, branch: str, commit_sha: st
122128
**Intended behavior**
123129
{intended or "Not recorded"}
124130
131+
**Root cause**
132+
{cause_summary or "Not recorded"}
133+
134+
**Fix summary**
135+
{fix_summary or "Not recorded"}
136+
125137
**Notes**
126138
{notes or "None recorded"}
127139
"""
@@ -133,6 +145,41 @@ def build_issue(row, csv_path: str, repository: str, branch: str, commit_sha: st
133145
}
134146

135147

148+
def build_close_payload(row, csv_path: str, repository: str, branch: str, commit_sha: str, actor: str):
149+
bug_id = row["id"].strip()
150+
line_number = row["_line_number"]
151+
status = (row.get("status") or "").strip()
152+
cause_summary = (row.get("cause_summary") or "").strip()
153+
fix_summary = (row.get("fix_summary") or "").strip()
154+
reviewed = (row.get("last_reviewed") or "").strip()
155+
156+
tracker_url = (
157+
f"https://github.com/{repository}/blob/{commit_sha}/{csv_path}#L{line_number}"
158+
if repository and commit_sha
159+
else csv_path
160+
)
161+
162+
comment = f"""Bug tracker row updated to `{status or "Closed"}`.
163+
164+
**Root cause**
165+
{cause_summary or "Not recorded"}
166+
167+
**Fix summary**
168+
{fix_summary or "Not recorded"}
169+
170+
- Tracker row: [{csv_path}#L{line_number}]({tracker_url})
171+
- Branch: `{branch}`
172+
- Commit: `{commit_sha}`
173+
- Updated by: `{actor}`
174+
- Last reviewed: `{reviewed or "Unspecified"}`
175+
"""
176+
177+
return {
178+
"id": bug_id,
179+
"comment": comment,
180+
}
181+
182+
136183
def main():
137184
parser = argparse.ArgumentParser()
138185
parser.add_argument("--csv-path", required=True)
@@ -149,8 +196,7 @@ def main():
149196
previous_rows = read_rows_from_git(args.before, args.csv_path)
150197

151198
new_bug_ids = [bug_id for bug_id in current_rows if bug_id not in previous_rows]
152-
closed_statuses = {"closed", "fixed", "resolved"}
153-
issues = [
199+
create_issues = [
154200
build_issue(
155201
current_rows[bug_id],
156202
args.csv_path,
@@ -160,11 +206,29 @@ def main():
160206
args.actor,
161207
)
162208
for bug_id in new_bug_ids
163-
if (current_rows[bug_id].get("status") or "").strip().lower() not in closed_statuses
209+
if not is_closed_status(current_rows[bug_id].get("status") or "")
210+
]
211+
212+
close_issues = [
213+
build_close_payload(
214+
current_rows[bug_id],
215+
args.csv_path,
216+
args.repository,
217+
args.branch,
218+
args.commit_sha,
219+
args.actor,
220+
)
221+
for bug_id, row in current_rows.items()
222+
if is_closed_status(row.get("status") or "")
223+
and bug_id in previous_rows
224+
and not is_closed_status(previous_rows[bug_id].get("status") or "")
164225
]
165226

166227
output_path = Path(args.output)
167-
output_path.write_text(json.dumps(issues, indent=2), encoding="utf-8")
228+
output_path.write_text(
229+
json.dumps({"create": create_issues, "close": close_issues}, indent=2),
230+
encoding="utf-8",
231+
)
168232

169233

170234
if __name__ == "__main__":

0 commit comments

Comments
 (0)