Skip to content

Commit 830a97d

Browse files
fix: duplicate PR fetch and spurious success-comment update (#187)
* fix: deduplicate PR fetch (#170) and gate success-comment on actual validation (#171) Agent-Logs-Url: https://github.com/joshjohanning/azdo_commit_message_validator/sessions/b5e0234d-7619-4ff4-8ff5-6c9ba670b3d5 Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com> * refactor: return consistent {body} from all non-error paths in addWorkItemsToPRBody Agent-Logs-Url: https://github.com/joshjohanning/azdo_commit_message_validator/sessions/b5e0234d-7619-4ff4-8ff5-6c9ba670b3d5 Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com> * chore: bump version to 4.1.3 and run npm run all Agent-Logs-Url: https://github.com/joshjohanning/azdo_commit_message_validator/sessions/6a897449-d5cd-4f9d-8e44-c1e301e15c5b Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.github.com> * refactor: only extract prTitle when checkPullRequest is enabled Agent-Logs-Url: https://github.com/joshjohanning/azdo_commit_message_validator/sessions/ad33ea36-d735-4c7c-a78c-b914a2e6da95 Co-authored-by: joshjohanning <19912012+joshjohanning@users.noreply.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 b210d62 commit 830a97d

5 files changed

Lines changed: 89 additions & 29 deletions

File tree

__tests__/index.test.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2502,7 +2502,7 @@ describe('Azure DevOps Commit Validator', () => {
25022502

25032503
await run();
25042504

2505-
// Should NOT call pulls.get or pulls.update since no IDs found
2505+
// Should NOT call pulls.update since no IDs found in branch
25062506
expect(mockOctokit.rest.pulls.update).not.toHaveBeenCalled();
25072507
});
25082508

@@ -2862,5 +2862,53 @@ describe('Azure DevOps Commit Validator', () => {
28622862
expect(mockSetFailed).toHaveBeenCalledWith(expect.stringContaining('Personal Access Token (PAT) may be expired'));
28632863
expect(mockOctokit.rest.pulls.update).not.toHaveBeenCalled();
28642864
});
2865+
2866+
it('should NOT update invalid work item comment to success when only add-work-item-from-branch is enabled', async () => {
2867+
// Regression test for issue #171: the success-comment path must be gated on
2868+
// checkCommits || checkPullRequest so that branch-only runs don't incorrectly
2869+
// flip an existing "invalid work items" comment to success.
2870+
mockContext.payload.pull_request = { number: 42, head: { ref: 'task/12345/fix' } };
2871+
2872+
mockGetInput.mockImplementation(name => {
2873+
const inputs = {
2874+
'check-commits': 'false',
2875+
'check-pull-request': 'false',
2876+
'fail-if-missing-workitem-commit-link': 'false',
2877+
'link-commits-to-pull-request': 'false',
2878+
'comment-on-failure': 'true',
2879+
'validate-work-item-exists': 'true',
2880+
'add-work-item-from-branch': 'true',
2881+
'branch-work-item-prefixes': 'task, bug, bugfix',
2882+
'branch-work-item-min-digits': '5',
2883+
'github-token': 'github-token',
2884+
'azure-devops-token': 'fake-token',
2885+
'azure-devops-organization': 'my-org'
2886+
};
2887+
return inputs[name] || '';
2888+
});
2889+
2890+
mockOctokit.rest.pulls.get.mockResolvedValue({
2891+
data: { title: 'My PR', body: 'Description' }
2892+
});
2893+
2894+
// Branch work item 12345 exists
2895+
mockValidateWorkItemExists.mockResolvedValueOnce({ exists: true });
2896+
2897+
// Simulate an existing invalid work item comment from a previous run
2898+
mockOctokit.rest.issues.listComments.mockResolvedValue({
2899+
data: [
2900+
{
2901+
id: 555,
2902+
body: `${COMMENT_MARKERS.INVALID_WORK_ITEMS}\n:x: There is 1 work item in pull request #42 that does not exist in Azure DevOps.`
2903+
}
2904+
]
2905+
});
2906+
2907+
await run();
2908+
2909+
expect(mockSetFailed).not.toHaveBeenCalled();
2910+
// The invalid work item comment must NOT be updated to success — no commit/PR validation ran
2911+
expect(mockOctokit.rest.issues.updateComment).not.toHaveBeenCalled();
2912+
});
28652913
});
28662914
});

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": "4.1.2",
3+
"version": "4.1.3",
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: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,29 @@ export async function run() {
138138

139139
const octokit = github.getOctokit(githubToken);
140140

141+
// Fetch pull request data once, shared between addWorkItemsToPRBody and checkPullRequestForWorkItems
142+
let prBody = '';
143+
let prTitle = '';
144+
if (addWorkItemFromBranch || checkPullRequest) {
145+
const { owner, repo } = context.repo;
146+
const pullRequestData = await octokit.rest.pulls.get({
147+
owner,
148+
repo,
149+
pull_number: pullNumber
150+
});
151+
prBody = pullRequestData.data.body || '';
152+
if (checkPullRequest) {
153+
prTitle = pullRequestData.data.title || '';
154+
}
155+
}
156+
141157
// Automatically add AB# tags from branch name if enabled
142158
if (addWorkItemFromBranch) {
143159
const branchResult = await addWorkItemsToPRBody(
144160
octokit,
145161
context,
146162
pullNumber,
163+
prBody,
147164
azureDevopsOrganization,
148165
azureDevopsToken,
149166
branchWorkItemPrefixes,
@@ -154,6 +171,11 @@ export async function run() {
154171
if (branchResult && branchResult.authError) {
155172
return;
156173
}
174+
175+
// Use updated body for subsequent checks (e.g. checkPullRequest sees the newly added AB# tags)
176+
if (branchResult?.body !== undefined) {
177+
prBody = branchResult.body;
178+
}
157179
}
158180

159181
// Store work item to commit mapping and validation results
@@ -190,6 +212,8 @@ export async function run() {
190212
octokit,
191213
context,
192214
pullNumber,
215+
prBody,
216+
prTitle,
193217
commentOnFailure,
194218
validateWorkItemExistsFlag,
195219
azureDevopsOrganization,
@@ -247,7 +271,7 @@ export async function run() {
247271
core.setFailed(
248272
`There ${allInvalidWorkItems.length === 1 ? 'is' : 'are'} ${allInvalidWorkItems.length} work item${allInvalidWorkItems.length === 1 ? '' : 's'} that ${allInvalidWorkItems.length === 1 ? 'does' : 'do'} not exist in Azure DevOps`
249273
);
250-
} else if (commentOnFailure && validateWorkItemExistsFlag) {
274+
} else if (commentOnFailure && validateWorkItemExistsFlag && (checkCommits || checkPullRequest)) {
251275
// All work items are valid - check if there's an existing invalid work item comment to update to success
252276
const { owner, repo } = context.repo;
253277
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
@@ -512,6 +536,8 @@ async function checkCommitsForWorkItems(
512536
* @param {Object} octokit - GitHub API client
513537
* @param {Object} context - GitHub Actions context
514538
* @param {number} pullNumber - Pull request number
539+
* @param {string} pullBody - Current PR body text (pre-fetched by caller)
540+
* @param {string} pullTitle - Current PR title (pre-fetched by caller)
515541
* @param {boolean} commentOnFailure - Whether to comment on PR if validation fails
516542
* @param {boolean} validateWorkItemExistsFlag - Whether to validate work items exist in Azure DevOps
517543
* @param {string} azureDevopsOrganization - Azure DevOps organization name
@@ -525,6 +551,8 @@ async function checkPullRequestForWorkItems(
525551
octokit,
526552
context,
527553
pullNumber,
554+
pullBody,
555+
pullTitle,
528556
commentOnFailure,
529557
validateWorkItemExistsFlag,
530558
azureDevopsOrganization,
@@ -535,16 +563,6 @@ async function checkPullRequestForWorkItems(
535563
) {
536564
const { owner, repo } = context.repo;
537565

538-
// Get pull request details
539-
const pullRequest = await octokit.rest.pulls.get({
540-
owner,
541-
repo,
542-
pull_number: pullNumber
543-
});
544-
545-
const pullBody = pullRequest.data.body || '';
546-
const pullTitle = pullRequest.data.title || '';
547-
548566
// Determine which text to check based on pull-request-check-scope
549567
let textToCheck;
550568
let scopeDescription;
@@ -840,16 +858,18 @@ export function extractWorkItemIdsFromBranch(branchName, prefixes, minDigits = 1
840858
* @param {Object} octokit - GitHub API client
841859
* @param {Object} context - GitHub Actions context
842860
* @param {number} pullNumber - Pull request number
861+
* @param {string} currentBody - Current PR body text (pre-fetched by caller)
843862
* @param {string} azureDevopsOrganization - Azure DevOps organization name
844863
* @param {string} azureDevopsToken - Azure DevOps PAT token
845864
* @param {string[]} branchPrefixes - Keyword prefixes for identifying work item IDs in branch names
846865
* @param {number} [minDigits=1] - Minimum number of digits for a work item ID
847-
* @returns {Promise<{authError: true}|undefined>} - Auth error status if auth failed, undefined otherwise
866+
* @returns {Promise<{body: string}|{authError: true}>} - Final PR body (updated or unchanged), or auth error status
848867
*/
849868
async function addWorkItemsToPRBody(
850869
octokit,
851870
context,
852871
pullNumber,
872+
currentBody,
853873
azureDevopsOrganization,
854874
azureDevopsToken,
855875
branchPrefixes,
@@ -865,7 +885,7 @@ async function addWorkItemsToPRBody(
865885

866886
if (workItemIds.length === 0) {
867887
core.info('No work item IDs found in branch name');
868-
return;
888+
return { body: currentBody };
869889
}
870890

871891
// Cap the number of IDs to validate to avoid excessive API calls
@@ -879,15 +899,6 @@ async function addWorkItemsToPRBody(
879899

880900
core.info(`Found work item ID(s) in branch: ${workItemIds.join(', ')}`);
881901

882-
// Get current PR body
883-
const pullRequest = await octokit.rest.pulls.get({
884-
owner,
885-
repo,
886-
pull_number: pullNumber
887-
});
888-
889-
const currentBody = pullRequest.data.body || '';
890-
891902
// Filter to only IDs not already in the PR body
892903
const missingIds = workItemIds.filter(id => {
893904
const pattern = new RegExp(`AB#${id}(?!\\d)`, 'i');
@@ -896,7 +907,7 @@ async function addWorkItemsToPRBody(
896907

897908
if (missingIds.length === 0) {
898909
core.info('All work item IDs from branch are already in the PR body');
899-
return;
910+
return { body: currentBody };
900911
}
901912

902913
// Validate IDs against Azure DevOps before adding
@@ -922,7 +933,7 @@ async function addWorkItemsToPRBody(
922933
idsToAdd = validatedIds;
923934
if (idsToAdd.length === 0) {
924935
core.info('No valid work item IDs from branch to add (all failed validation)');
925-
return;
936+
return { body: currentBody };
926937
}
927938

928939
// Build the AB# tags to add
@@ -939,6 +950,7 @@ async function addWorkItemsToPRBody(
939950
core.info('PR body updated with work item tag(s) from branch name');
940951
const sanitizedBranchName = branchName.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
941952
core.summary.addRaw(`- :link: **Added from branch:** ${abTags} extracted from branch \`${sanitizedBranchName}\`\n`);
953+
return { body: updatedBody };
942954
}
943955

944956
/**

0 commit comments

Comments
 (0)