Skip to content

Commit 1370072

Browse files
feat: add linked work items to job summary (#93)
* chore: empty commit to sync branch * feat: add work item linkage information to job summary and notices - Add GitHub Actions notice annotations when work items are linked - Add work item information to job summary for visibility - Update tests to mock core.notice and core.summary - Update README with Action Output section This implements the original requirement to make it easier to see what work items were linked to the PR from the job output. Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com> * chore: bump version to 3.0.7 * fix: ensure job summary is written only once after execution * refactor: simplify job summary annotations in commit and PR checks by removing the notices * test: verify job summary includes work item info in commit validation * docs: update README to clarify job summary visibility for linked work items * chore: bump version to 3.0.8 in package.json * fix: improve job summary handling to avoid duplicate work item entries * refactor: remove unused mockNotice from core actions mock * docs: update README to clarify job summary links and work item display * feat: enhance job summary visibility for linked work items in PRs * fix: update coverage badge to reflect current coverage percentage * fix: update coverage badge to reflect new coverage percentage (81.08%) * docs: enhance job summary visibility for linked work items * test: add validation for work item presence in commit and PR * test: simplify mock data structure in commit validation tests --------- Co-authored-by: Josh Johanning <joshjohanning@github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com>
1 parent 3d675be commit 1370072

6 files changed

Lines changed: 119 additions & 19 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ This action validates that pull requests and commits contain Azure DevOps work i
1414
2. **Validates Commits** - Ensures each commit in a pull request has an Azure DevOps work item link (e.g. `AB#123`) in the commit message
1515
3. **Automatically Links PRs to Work Items** - When a work item is referenced in a commit message, the action adds a GitHub Pull Request link to that work item in Azure DevOps
1616
- 🎯 **This is the key differentiator**: By default, Azure DevOps only adds the Pull Request link to work items mentioned directly in the PR title or body, but this action also links work items found in commit messages!
17+
4. **Visibility & Tracking** - Work item linkages are added to the job summary for easy visibility
18+
19+
## Action Output
20+
21+
The action provides visibility into work items through the **Job Summary**:
22+
23+
- A summary of all work items found in commits and PR is added to the workflow run's job summary page
24+
- Includes clickable links to commits and displays associated work items
25+
- Shows which work items were **linked** to the PR (when `link-commits-to-pull-request` is enabled) vs. **verified** (when `validate-work-item-exists` is enabled)
26+
- Provides a quick reference of work items associated with the PR
1727

1828
## Usage
1929

__tests__/index.test.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ const mockGetInput = jest.fn();
99
const mockSetFailed = jest.fn();
1010
const mockInfo = jest.fn();
1111
const mockError = jest.fn();
12+
const mockSummary = {
13+
addRaw: jest.fn().mockReturnThis(),
14+
write: jest.fn().mockResolvedValue(undefined)
15+
};
1216

1317
jest.unstable_mockModule('@actions/core', () => ({
1418
getInput: mockGetInput,
1519
setFailed: mockSetFailed,
1620
info: mockInfo,
17-
error: mockError
21+
error: mockError,
22+
summary: mockSummary
1823
}));
1924

2025
// Mock @actions/github
@@ -64,6 +69,10 @@ describe('Azure DevOps Commit Validator', () => {
6469
// Clear all mocks
6570
jest.clearAllMocks();
6671

72+
// Reset summary mock
73+
mockSummary.addRaw.mockClear().mockReturnThis();
74+
mockSummary.write.mockClear().mockResolvedValue(undefined);
75+
6776
// Setup default mock implementations
6877
mockGetInput.mockImplementation(name => {
6978
const defaults = {
@@ -444,6 +453,9 @@ describe('Azure DevOps Commit Validator', () => {
444453

445454
expect(mockLinkWorkItem).toHaveBeenCalled();
446455
expect(mockSetFailed).not.toHaveBeenCalled();
456+
// Verify job summary was written with work item info
457+
expect(mockSummary.addRaw).toHaveBeenCalledWith(expect.stringContaining('AB#12345'));
458+
expect(mockSummary.write).toHaveBeenCalled();
447459
});
448460

449461
it('should handle duplicate work items', async () => {
@@ -504,6 +516,9 @@ describe('Azure DevOps Commit Validator', () => {
504516
await run();
505517

506518
expect(mockSetFailed).not.toHaveBeenCalled();
519+
// Verify job summary was written with work item info
520+
expect(mockSummary.addRaw).toHaveBeenCalledWith(expect.stringContaining('AB#12345'));
521+
expect(mockSummary.write).toHaveBeenCalled();
507522
});
508523

509524
it('should pass when PR has work item in body', async () => {
@@ -525,6 +540,9 @@ describe('Azure DevOps Commit Validator', () => {
525540
await run();
526541

527542
expect(mockSetFailed).not.toHaveBeenCalled();
543+
// Verify job summary was written with work item info
544+
expect(mockSummary.addRaw).toHaveBeenCalledWith(expect.stringContaining('AB#12345'));
545+
expect(mockSummary.write).toHaveBeenCalled();
528546
});
529547

530548
it('should fail when PR has no work item link', async () => {
@@ -583,6 +601,37 @@ describe('Azure DevOps Commit Validator', () => {
583601
})
584602
);
585603
});
604+
605+
it('should pass when valid work item appears in both commit and PR', async () => {
606+
mockGetInput.mockImplementation(name => {
607+
if (name === 'check-commits') return 'true';
608+
if (name === 'check-pull-request') return 'true';
609+
if (name === 'github-token') return 'github-token';
610+
if (name === 'comment-on-failure') return 'true';
611+
return 'false';
612+
});
613+
614+
mockOctokit.rest.pulls.listCommits.mockResolvedValue({
615+
data: [{ sha: 'abc123', commit: { message: 'fix: resolve issue AB#12345' } }]
616+
});
617+
618+
mockOctokit.rest.pulls.get.mockResolvedValue({
619+
data: {
620+
title: 'fix: resolve issue AB#12345',
621+
body: 'This PR fixes AB#12345'
622+
}
623+
});
624+
625+
await run();
626+
627+
expect(mockSetFailed).not.toHaveBeenCalled();
628+
// Verify job summary was written and work item appears only once
629+
expect(mockSummary.addRaw).toHaveBeenCalled();
630+
expect(mockSummary.write).toHaveBeenCalled();
631+
// Work item AB#12345 should be in the summary from commit (where it was found first)
632+
const summaryCallArg = mockSummary.addRaw.mock.calls.find(call => call[0].includes('AB#12345'));
633+
expect(summaryCallArg).toBeDefined();
634+
});
586635
});
587636

588637
describe('Comment management', () => {

badges/coverage.svg

Lines changed: 1 addition & 1 deletion
Loading

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "azure-devops-work-item-link-enforcer-and-linker",
3-
"version": "3.0.7",
3+
"version": "3.0.8",
44
"private": true,
55
"type": "module",
66
"description": "GitHub Action to enforce that each commit in a pull request be linked to an Azure DevOps work item and automatically link the pull request to each work item ",

src/index.js

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ export async function run() {
181181
core.info('... invalid work item comment updated to success');
182182
}
183183
}
184+
185+
// Write job summary once at the end (summary content was added throughout execution)
186+
await core.summary.write();
184187
} catch (error) {
185188
core.setFailed(`Action failed with error: ${error}`);
186189
}
@@ -357,25 +360,43 @@ async function checkCommitsForWorkItems(
357360
// (Don't update success comment here - let caller handle it after checking PR too)
358361
}
359362

360-
// Link work items to PR if enabled (after deduplication)
361-
if (linkCommitsToPullRequest && allWorkItems.length > 0) {
363+
// Process work items found in commits (after deduplication)
364+
if (allWorkItems.length > 0) {
362365
// Remove duplicates
363366
const uniqueWorkItems = [...new Set(allWorkItems)];
364367

365368
for (const match of uniqueWorkItems) {
366369
const workItemId = match.substring(3); // Remove "AB#" prefix
367-
core.info(`Linking work item ${workItemId} to pull request ${pullNumber}...`);
368-
369-
// Set environment variables for main.js
370-
process.env.REPO_TOKEN = githubToken;
371-
process.env.AZURE_DEVOPS_ORG = azureDevopsOrganization;
372-
process.env.AZURE_DEVOPS_PAT = azureDevopsToken;
373-
process.env.WORKITEMID = workItemId;
374-
process.env.PULLREQUESTID = pullNumber.toString();
375-
process.env.REPO = `${context.repo.owner}/${context.repo.repo}`;
376-
process.env.GITHUB_SERVER_URL = process.env.GITHUB_SERVER_URL || 'https://github.com';
377-
378-
await linkWorkItem();
370+
const commitInfo = workItemToCommitMap.get(workItemId);
371+
372+
// Link work items to PR if enabled
373+
if (linkCommitsToPullRequest) {
374+
core.info(`Linking work item ${workItemId} to pull request ${pullNumber}...`);
375+
376+
// Set environment variables for main.js
377+
process.env.REPO_TOKEN = githubToken;
378+
process.env.AZURE_DEVOPS_ORG = azureDevopsOrganization;
379+
process.env.AZURE_DEVOPS_PAT = azureDevopsToken;
380+
process.env.WORKITEMID = workItemId;
381+
process.env.PULLREQUESTID = pullNumber.toString();
382+
process.env.REPO = `${context.repo.owner}/${context.repo.repo}`;
383+
process.env.GITHUB_SERVER_URL = process.env.GITHUB_SERVER_URL || 'https://github.com';
384+
385+
await linkWorkItem();
386+
}
387+
388+
// Add job summary for visibility (regardless of linking setting)
389+
if (commitInfo) {
390+
if (linkCommitsToPullRequest) {
391+
core.summary.addRaw(
392+
`- ✅ **Linked:** Work item AB#${workItemId} (from commit [\`${commitInfo.shortSha}\`](${context.payload.repository?.html_url}/commit/${commitInfo.sha})) linked to PR #${pullNumber}\n`
393+
);
394+
} else {
395+
core.summary.addRaw(
396+
`- ✔️ **Verified:** Work item AB#${workItemId} found in commit [\`${commitInfo.shortSha}\`](${context.payload.repository?.html_url}/commit/${commitInfo.sha})\n`
397+
);
398+
}
399+
}
379400
}
380401
}
381402

@@ -502,10 +523,30 @@ async function checkPullRequestForWorkItems(
502523
return invalidWorkItems;
503524
}
504525

526+
// All work items valid - add job summary for each (only if not already added from commits)
527+
for (const workItem of uniqueWorkItems) {
528+
const workItemNumber = workItem.substring(3); // Remove "AB#" prefix
529+
// Only add to summary if this work item wasn't already added from a commit
530+
if (!workItemToCommitMap.has(workItemNumber) || workItemToCommitMap.get(workItemNumber) === null) {
531+
core.summary.addRaw(`- ✔️ **Verified:** Work item AB#${workItemNumber} found in PR title/body\n`);
532+
}
533+
}
534+
505535
// All work items valid - return empty array
506536
return [];
507537
}
508538

539+
// Validation disabled - add job summary for each work item (only if not already added from commits)
540+
for (const workItem of uniqueWorkItems) {
541+
const workItemNumber = workItem.substring(3); // Remove "AB#" prefix
542+
543+
// Only add to map and summary if this work item wasn't already added from a commit
544+
if (!workItemToCommitMap.has(workItemNumber)) {
545+
workItemToCommitMap.set(workItemNumber, null); // null indicates it's from PR title/body
546+
core.summary.addRaw(`- ✔️ **Verified:** Work item AB#${workItemNumber} found in PR title/body\n`);
547+
}
548+
}
549+
509550
// Validation disabled - return empty array
510551
return [];
511552
}

0 commit comments

Comments
 (0)