Skip to content

Commit f07e4c2

Browse files
committed
Enhance user story issue workflow and script functionality
- Added a new function to extract tracker keys from issue bodies in the GitHub workflow, improving issue indexing. - Refactored the issue matching logic to utilize an indexed approach, enhancing performance and reliability. - Removed outdated "Out of Scope" section from the user story issue candidate script to streamline content. These changes aim to improve the efficiency of issue handling and maintain clarity in documentation.
1 parent d02fac5 commit f07e4c2

2 files changed

Lines changed: 84 additions & 26 deletions

File tree

.github/workflows/user-story-issues.yml

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@ jobs:
145145
return `${nextBlock || nextBody}\n\n${currentBody}`;
146146
}
147147
148+
function trackerKeyFromBody(body) {
149+
const markerMatch = (body || '').match(/<!--\s*user-story-key:\s*([^>]+?)\s*-->/);
150+
if (markerMatch) {
151+
return markerMatch[1].trim();
152+
}
153+
154+
const storyKeyMatch = (body || '').match(/Story Key:\s*`([^`]+)`/);
155+
if (storyKeyMatch) {
156+
return storyKeyMatch[1].trim();
157+
}
158+
159+
return null;
160+
}
161+
148162
async function ensureLabel(labelName) {
149163
const definition = labelDefinitions[labelName] || {
150164
color: 'ededed',
@@ -172,21 +186,36 @@ jobs:
172186
}
173187
}
174188
175-
async function findMatchingIssue(issue) {
176-
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue "${issue.tracker_key}"`;
177-
const candidates = await github.paginate(
178-
github.rest.search.issuesAndPullRequests,
189+
async function loadExistingIssueIndex() {
190+
const repositoryIssues = await github.paginate(
191+
github.rest.issues.listForRepo,
179192
{
180-
q: query,
193+
owner: context.repo.owner,
194+
repo: context.repo.repo,
195+
state: 'all',
181196
per_page: 100,
182197
}
183198
);
184199
185-
return candidates.find((candidate) => {
186-
const body = candidate.body || '';
187-
return body.includes(`user-story-key: ${issue.tracker_key}`) ||
188-
body.includes(`Story Key: \`${issue.tracker_key}\``);
189-
});
200+
const issueIndex = new Map();
201+
let indexedCount = 0;
202+
203+
for (const repositoryIssue of repositoryIssues) {
204+
if (repositoryIssue.pull_request) {
205+
continue;
206+
}
207+
208+
const trackerKey = trackerKeyFromBody(repositoryIssue.body || '');
209+
if (!trackerKey || issueIndex.has(trackerKey)) {
210+
continue;
211+
}
212+
213+
issueIndex.set(trackerKey, repositoryIssue);
214+
indexedCount += 1;
215+
}
216+
217+
core.info(`Indexed ${indexedCount} existing user story issue(s) without using GitHub Search.`);
218+
return issueIndex;
190219
}
191220
192221
let projectCache = null;
@@ -257,23 +286,51 @@ jobs:
257286
return projectCache;
258287
}
259288
260-
async function syncProjectItem(issueNodeId) {
261-
const project = await loadProject();
262-
const addResult = await github.graphql(
263-
`mutation($projectId: ID!, $contentId: ID!) {
264-
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
265-
item {
266-
id
289+
async function findProjectItemId(projectId, issueNodeId) {
290+
const result = await github.graphql(
291+
`query($issueNodeId: ID!) {
292+
node(id: $issueNodeId) {
293+
... on Issue {
294+
projectItems(first: 50) {
295+
nodes {
296+
id
297+
project {
298+
id
299+
}
300+
}
301+
}
267302
}
268303
}
269304
}`,
270-
{
271-
projectId: project.id,
272-
contentId: issueNodeId,
273-
}
305+
{ issueNodeId }
274306
);
275307
276-
const itemId = addResult.addProjectV2ItemById.item.id;
308+
const projectItems = (result.node && result.node.projectItems && result.node.projectItems.nodes) || [];
309+
const existingItem = projectItems.find((item) => item.project && item.project.id === projectId);
310+
return existingItem ? existingItem.id : null;
311+
}
312+
313+
async function syncProjectItem(issueNodeId) {
314+
const project = await loadProject();
315+
let itemId = await findProjectItemId(project.id, issueNodeId);
316+
317+
if (!itemId) {
318+
const addResult = await github.graphql(
319+
`mutation($projectId: ID!, $contentId: ID!) {
320+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
321+
item {
322+
id
323+
}
324+
}
325+
}`,
326+
{
327+
projectId: project.id,
328+
contentId: issueNodeId,
329+
}
330+
);
331+
332+
itemId = addResult.addProjectV2ItemById.item.id;
333+
}
277334
278335
await github.graphql(
279336
`mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
@@ -310,8 +367,10 @@ jobs:
310367
await ensureLabel(label);
311368
}
312369
370+
const existingIssueIndex = await loadExistingIssueIndex();
371+
313372
for (const issue of issues) {
314-
let current = await findMatchingIssue(issue);
373+
let current = existingIssueIndex.get(issue.tracker_key);
315374
316375
if (!current) {
317376
try {
@@ -324,6 +383,7 @@ jobs:
324383
assignees: assignees.length ? assignees : undefined,
325384
});
326385
current = created.data;
386+
existingIssueIndex.set(issue.tracker_key, current);
327387
core.info(`Created ${issue.tracker_key}: ${current.html_url}`);
328388
} catch (error) {
329389
if (!assignees.length) {
@@ -339,6 +399,7 @@ jobs:
339399
labels: issue.labels,
340400
});
341401
current = created.data;
402+
existingIssueIndex.set(issue.tracker_key, current);
342403
core.info(`Created ${issue.tracker_key}: ${current.html_url}`);
343404
}
344405
} else {

scripts/user_story_issue_candidates.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,6 @@ def build_issue(row, repository: str, branch: str, commit_sha: str, actor: str):
259259
"**Tracker Notes**",
260260
notes or "None recorded",
261261
"",
262-
"## Out of Scope",
263-
"Backend services, mobile layouts, and design mockups are out of scope for this client-side MicrobeTrace story unless explicitly stated in the tracker row.",
264-
"",
265262
"## Definition of Done",
266263
"- Acceptance criteria are satisfied.",
267264
"- Relevant Cypress coverage remains passing or any coverage gap is documented.",

0 commit comments

Comments
 (0)