Skip to content

Commit 88b6bde

Browse files
committed
Merge branch 'dev' of https://github.com/CDCgov/MicrobeTrace into dev
2 parents 230d8dd + f4b481e commit 88b6bde

5 files changed

Lines changed: 430 additions & 103 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
name: Delete Generated User Story Issues
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dry_run:
7+
description: List matching old generated user-story issues without deleting them.
8+
required: true
9+
type: boolean
10+
default: true
11+
confirm_delete:
12+
description: Type DELETE_OLD_USER_STORIES to allow deletion when dry_run=false.
13+
required: true
14+
type: string
15+
default: ""
16+
include_closed:
17+
description: Also delete matching closed issues.
18+
required: true
19+
type: boolean
20+
default: false
21+
max_delete:
22+
description: Maximum issues to delete in one real run. Use 0 for no explicit cap.
23+
required: true
24+
type: number
25+
default: 500
26+
27+
permissions:
28+
contents: read
29+
issues: write
30+
31+
jobs:
32+
delete-old-generated-user-stories:
33+
runs-on: ubuntu-latest
34+
env:
35+
DRY_RUN: ${{ inputs.dry_run }}
36+
CONFIRM_DELETE: ${{ inputs.confirm_delete }}
37+
INCLUDE_CLOSED: ${{ inputs.include_closed }}
38+
MAX_DELETE: ${{ inputs.max_delete }}
39+
PROJECT_TOKEN_CONFIGURED: ${{ secrets.USER_STORY_PROJECT_TOKEN != '' }}
40+
steps:
41+
- name: Delete old generated user story issues
42+
uses: actions/github-script@v7
43+
with:
44+
github-token: ${{ secrets.USER_STORY_PROJECT_TOKEN || github.token }}
45+
script: |
46+
const dryRun = process.env.DRY_RUN === 'true';
47+
const includeClosed = process.env.INCLUDE_CLOSED === 'true';
48+
const confirmDelete = process.env.CONFIRM_DELETE || '';
49+
const maxDelete = Number(process.env.MAX_DELETE || 0);
50+
51+
if (!dryRun && confirmDelete !== 'DELETE_OLD_USER_STORIES') {
52+
throw new Error('Refusing to delete issues. Set confirm_delete to DELETE_OLD_USER_STORIES.');
53+
}
54+
55+
if (!dryRun && process.env.PROJECT_TOKEN_CONFIGURED !== 'true') {
56+
core.warning('USER_STORY_PROJECT_TOKEN is not configured. Deletion will use GITHUB_TOKEN and may fail if it lacks admin/delete permissions.');
57+
}
58+
59+
function labelNames(issue) {
60+
return new Set((issue.labels || []).map((label) => label.name || label));
61+
}
62+
63+
function trackerKeyFromBody(body) {
64+
const markerMatch = (body || '').match(/<!--\s*user-story-key:\s*([^>]+?)\s*-->/);
65+
if (markerMatch) {
66+
return markerMatch[1].trim();
67+
}
68+
69+
const storyKeyMatch = (body || '').match(/Story Key:\s*`([^`]+)`/);
70+
if (storyKeyMatch) {
71+
return storyKeyMatch[1].trim();
72+
}
73+
74+
return null;
75+
}
76+
77+
function isOldPerQaRowKey(key) {
78+
if (!key || !key.includes(':')) {
79+
return false;
80+
}
81+
82+
const [source, id] = key.split(':');
83+
if (!source || !id) {
84+
return false;
85+
}
86+
87+
// Old row-level keys end in QA tracker row IDs such as L001,
88+
// M003, CT021, AG014, LNK009, NET003, or TL005.
89+
// New grouped keys are slugs such as 2d-network:load-supported-data.
90+
return /^[A-Z]+[0-9]{3}$/.test(id);
91+
}
92+
93+
function shouldDelete(issue) {
94+
if (issue.pull_request) {
95+
return false;
96+
}
97+
98+
if (!includeClosed && issue.state !== 'open') {
99+
return false;
100+
}
101+
102+
const labels = labelNames(issue);
103+
if (!labels.has('[issue-type] user story')) {
104+
return false;
105+
}
106+
107+
if (!labels.has('source-qa-tracker')) {
108+
return false;
109+
}
110+
111+
if (labels.has('source-user-stories')) {
112+
return false;
113+
}
114+
115+
const trackerKey = trackerKeyFromBody(issue.body || '');
116+
return isOldPerQaRowKey(trackerKey);
117+
}
118+
119+
const repositoryIssues = await github.paginate(
120+
github.rest.issues.listForRepo,
121+
{
122+
owner: context.repo.owner,
123+
repo: context.repo.repo,
124+
state: includeClosed ? 'all' : 'open',
125+
per_page: 100,
126+
}
127+
);
128+
129+
const candidates = repositoryIssues.filter(shouldDelete);
130+
candidates.sort((left, right) => left.number - right.number);
131+
132+
core.info(`Found ${candidates.length} old generated user-story issue(s) eligible for deletion.`);
133+
134+
for (const issue of candidates) {
135+
const trackerKey = trackerKeyFromBody(issue.body || '');
136+
core.info(`${dryRun ? '[dry-run] Would delete' : 'Deleting'} #${issue.number}: ${issue.title} (${trackerKey})`);
137+
}
138+
139+
if (dryRun) {
140+
core.info('Dry run only. No issues were deleted.');
141+
return;
142+
}
143+
144+
if (maxDelete > 0 && candidates.length > maxDelete) {
145+
throw new Error(`Refusing to delete ${candidates.length} issues because max_delete is ${maxDelete}. Increase max_delete or use 0 for no explicit cap.`);
146+
}
147+
148+
let deletedCount = 0;
149+
for (const issue of candidates) {
150+
await github.graphql(
151+
`mutation($issueId: ID!) {
152+
deleteIssue(input: { issueId: $issueId }) {
153+
repository {
154+
id
155+
}
156+
}
157+
}`,
158+
{ issueId: issue.node_id }
159+
);
160+
deletedCount += 1;
161+
core.info(`Deleted #${issue.number}: ${issue.title}`);
162+
}
163+
164+
core.info(`Deleted ${deletedCount} old generated user-story issue(s).`);

0 commit comments

Comments
 (0)