Skip to content

Commit 500464f

Browse files
fix: replace inline AB# modification with linked work items table (#155)
* fix: add work item table as separate section to preserve AB# links The append-work-item-title feature replaced AB# references inline, which removed the Development section link in Azure DevOps. Instead, add a "Linked Work Items" table at the bottom of the PR body, keeping original AB# references intact. - Replace inline AB# modification with a separate H3 table section - Add new `add-work-item-table` input, deprecate `append-work-item-title` - Section is idempotent (replaced on each run, not accumulated) - Bump version to 3.2.1 Closes #154 * fix: deprecate `append-work-item-title` input and update warning message * test: update test for deprecated append-work-item-title input to use alias * fix: update work item table to use IDs instead of AB# references to prevent duplicate entries * docs: skip workflow runs triggered by azure-boards bot to prevent duplicates * fix(deps): update version to 3.2.1 in package-lock.json * fix: update work item summary message and sanitize table cell values * fix: update coverage percentage in SVG badge to reflect current status * fix: improve sanitization of table cell values to handle backslashes correctly
1 parent 0851f88 commit 500464f

7 files changed

Lines changed: 169 additions & 81 deletions

File tree

README.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ on:
4444
jobs:
4545
pr-commit-message-enforcer-and-linker:
4646
runs-on: ubuntu-latest
47+
# Skip runs triggered by azure-boards bot editing the PR body to avoid duplicate workflow runs
48+
if: github.actor != 'azure-boards[bot]'
4749
permissions:
4850
contents: read
4951
pull-requests: write
@@ -63,19 +65,20 @@ jobs:
6365
6466
### Inputs
6567
66-
| Name | Description | Required | Default |
67-
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- |
68-
| `check-pull-request` | Check the pull request for `AB#xxx` (scope configurable via `pull-request-check-scope`) | `true` | `false` |
69-
| `pull-request-check-scope` | Only if `check-pull-request=true`, where to look for `AB#` in the PR: `title-or-body`, `body-only`, or `title-only` | `false` | `title-or-body` |
70-
| `check-commits` | Check each commit in the pull request for `AB#xxx` | `true` | `true` |
71-
| `fail-if-missing-workitem-commit-link` | Only if `check-commits=true`, fail the action if a commit in the pull request is missing AB# in every commit message | `false` | `true` |
72-
| `link-commits-to-pull-request` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `true` |
73-
| `validate-work-item-exists` | Validate that the work item(s) referenced in commits and PR exist in Azure DevOps (requires `azure-devops-token` and `azure-devops-organization`) | `false` | `true` |
74-
| `append-work-item-title` | Append the work item title to `AB#xxx` references in the PR body (e.g. `AB#123` becomes `AB#123 - Fix bug`). Requires `azure-devops-token` and `azure-devops-organization` | `false` | `false` |
75-
| `azure-devops-organization` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `''` |
76-
| `azure-devops-token` | Only required if `link-commits-to-pull-request=true`, Azure DevOps PAT used to link work item to PR (needs to be a `full` PAT) | `false` | `''` |
77-
| `github-token` | The GitHub token that has contents-read and pull_request-write access | `true` | `${{ github.token }}` |
78-
| `comment-on-failure` | Comment on the pull request if the action fails | `true` | `true` |
68+
| Name | Description | Required | Default |
69+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- |
70+
| `check-pull-request` | Check the pull request for `AB#xxx` (scope configurable via `pull-request-check-scope`) | `true` | `false` |
71+
| `pull-request-check-scope` | Only if `check-pull-request=true`, where to look for `AB#` in the PR: `title-or-body`, `body-only`, or `title-only` | `false` | `title-or-body` |
72+
| `check-commits` | Check each commit in the pull request for `AB#xxx` | `true` | `true` |
73+
| `fail-if-missing-workitem-commit-link` | Only if `check-commits=true`, fail the action if a commit in the pull request is missing AB# in every commit message | `false` | `true` |
74+
| `link-commits-to-pull-request` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `true` |
75+
| `validate-work-item-exists` | Validate that the work item(s) referenced in commits and PR exist in Azure DevOps (requires `azure-devops-token` and `azure-devops-organization`) | `false` | `true` |
76+
| `add-work-item-table` | Add a "Linked Work Items" table to the PR body showing titles for `AB#xxx` references (original references are preserved). Requires `azure-devops-token` and `azure-devops-organization` | `false` | `false` |
77+
| `append-work-item-title` | **Deprecated** - use `add-work-item-table` instead. Will be removed in a future major version. | `false` | `false` |
78+
| `azure-devops-organization` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `''` |
79+
| `azure-devops-token` | Only required if `link-commits-to-pull-request=true`, Azure DevOps PAT used to link work item to PR (needs to be a `full` PAT) | `false` | `''` |
80+
| `github-token` | The GitHub token that has contents-read and pull_request-write access | `true` | `${{ github.token }}` |
81+
| `comment-on-failure` | Comment on the pull request if the action fails | `true` | `true` |
7982

8083
## Screenshots
8184

__tests__/index.test.js

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ describe('Azure DevOps Commit Validator', () => {
9090
'github-token': 'github-token',
9191
'comment-on-failure': 'true',
9292
'validate-work-item-exists': 'false',
93-
'append-work-item-title': 'false'
93+
'append-work-item-title': 'false',
94+
'add-work-item-table': 'false'
9495
};
9596
return defaults[name] || '';
9697
});
@@ -809,15 +810,15 @@ describe('Azure DevOps Commit Validator', () => {
809810
});
810811
});
811812

812-
describe('Append work item title', () => {
813-
it('should append work item title to AB# in PR body when enabled', async () => {
813+
describe('Work item title table', () => {
814+
it('should add work item title table to PR body when enabled', async () => {
814815
mockGetInput.mockImplementation(name => {
815816
if (name === 'check-commits') return 'false';
816817
if (name === 'check-pull-request') return 'true';
817818
if (name === 'github-token') return 'github-token';
818819
if (name === 'comment-on-failure') return 'false';
819820
if (name === 'validate-work-item-exists') return 'false';
820-
if (name === 'append-work-item-title') return 'true';
821+
if (name === 'add-work-item-table') return 'true';
821822
if (name === 'azure-devops-token') return 'azdo-token';
822823
if (name === 'azure-devops-organization') return 'my-org';
823824
return '';
@@ -836,14 +837,16 @@ describe('Azure DevOps Commit Validator', () => {
836837

837838
expect(mockSetFailed).not.toHaveBeenCalled();
838839
expect(mockGetWorkItemTitle).toHaveBeenCalledWith('my-org', 'azdo-token', '12345');
839-
expect(mockOctokit.rest.pulls.update).toHaveBeenCalledWith(
840-
expect.objectContaining({
841-
body: 'This PR implements AB#12345 - Fix login bug'
842-
})
840+
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
841+
expect(updateCall.body).toContain('This PR implements AB#12345');
842+
expect(updateCall.body).toContain('<!-- AZDO-VALIDATOR: WORK-ITEM-TITLES-START -->');
843+
expect(updateCall.body).toContain('### Linked Work Items');
844+
expect(updateCall.body).toContain(
845+
'| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Fix login bug |'
843846
);
844847
});
845848

846-
it('should not update PR body when work item already has title appended', async () => {
849+
it('should support deprecated append-work-item-title input as alias', async () => {
847850
mockGetInput.mockImplementation(name => {
848851
if (name === 'check-commits') return 'false';
849852
if (name === 'check-pull-request') return 'true';
@@ -859,25 +862,59 @@ describe('Azure DevOps Commit Validator', () => {
859862
mockOctokit.rest.pulls.get.mockResolvedValue({
860863
data: {
861864
title: 'feat: new feature',
862-
body: 'This PR implements AB#12345 - Fix login bug'
865+
body: 'This PR implements AB#12345'
863866
}
864867
});
865868

869+
mockGetWorkItemTitle.mockResolvedValue({ title: 'Fix login bug', type: 'Bug' });
870+
866871
await run();
867872

868873
expect(mockSetFailed).not.toHaveBeenCalled();
869-
expect(mockGetWorkItemTitle).not.toHaveBeenCalled();
870-
expect(mockOctokit.rest.pulls.update).not.toHaveBeenCalled();
874+
expect(mockOctokit.rest.pulls.update).toHaveBeenCalled();
875+
});
876+
877+
it('should update section when work item titles section already exists', async () => {
878+
mockGetInput.mockImplementation(name => {
879+
if (name === 'check-commits') return 'false';
880+
if (name === 'check-pull-request') return 'true';
881+
if (name === 'github-token') return 'github-token';
882+
if (name === 'comment-on-failure') return 'false';
883+
if (name === 'validate-work-item-exists') return 'false';
884+
if (name === 'add-work-item-table') return 'true';
885+
if (name === 'azure-devops-token') return 'azdo-token';
886+
if (name === 'azure-devops-organization') return 'my-org';
887+
return '';
888+
});
889+
890+
mockOctokit.rest.pulls.get.mockResolvedValue({
891+
data: {
892+
title: 'feat: new feature',
893+
body: 'This PR implements AB#12345\n\n---\n<!-- AZDO-VALIDATOR: WORK-ITEM-TITLES-START -->\n### Linked Work Items\n| Work Item | Type | Title |\n|---|---|---|\n| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Old title |\n<!-- AZDO-VALIDATOR: WORK-ITEM-TITLES-END -->'
894+
}
895+
});
896+
897+
mockGetWorkItemTitle.mockResolvedValue({ title: 'Fix login bug', type: 'Bug' });
898+
899+
await run();
900+
901+
expect(mockSetFailed).not.toHaveBeenCalled();
902+
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
903+
expect(updateCall.body).toContain('This PR implements AB#12345');
904+
expect(updateCall.body).toContain(
905+
'| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Fix login bug |'
906+
);
907+
expect(updateCall.body).not.toContain('Old title');
871908
});
872909

873-
it('should not append when append-work-item-title is false', async () => {
910+
it('should not add table when add-work-item-table is false', async () => {
874911
mockGetInput.mockImplementation(name => {
875912
if (name === 'check-commits') return 'false';
876913
if (name === 'check-pull-request') return 'true';
877914
if (name === 'github-token') return 'github-token';
878915
if (name === 'comment-on-failure') return 'false';
879916
if (name === 'validate-work-item-exists') return 'false';
880-
if (name === 'append-work-item-title') return 'false';
917+
if (name === 'add-work-item-table') return 'false';
881918
return '';
882919
});
883920

@@ -902,7 +939,7 @@ describe('Azure DevOps Commit Validator', () => {
902939
if (name === 'github-token') return 'github-token';
903940
if (name === 'comment-on-failure') return 'false';
904941
if (name === 'validate-work-item-exists') return 'false';
905-
if (name === 'append-work-item-title') return 'true';
942+
if (name === 'add-work-item-table') return 'true';
906943
if (name === 'azure-devops-token') return 'azdo-token';
907944
if (name === 'azure-devops-organization') return 'my-org';
908945
return '';
@@ -923,10 +960,13 @@ describe('Azure DevOps Commit Validator', () => {
923960

924961
expect(mockSetFailed).not.toHaveBeenCalled();
925962
expect(mockGetWorkItemTitle).toHaveBeenCalledTimes(2);
926-
expect(mockOctokit.rest.pulls.update).toHaveBeenCalledWith(
927-
expect.objectContaining({
928-
body: 'This PR implements AB#111 - First item and AB#222 - Second item'
929-
})
963+
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
964+
expect(updateCall.body).toContain('This PR implements AB#111 and AB#222');
965+
expect(updateCall.body).toContain(
966+
'| [111](https://dev.azure.com/my-org/_workitems/edit/111) | User Story | First item |'
967+
);
968+
expect(updateCall.body).toContain(
969+
'| [222](https://dev.azure.com/my-org/_workitems/edit/222) | Bug | Second item |'
930970
);
931971
});
932972

@@ -937,7 +977,7 @@ describe('Azure DevOps Commit Validator', () => {
937977
if (name === 'github-token') return 'github-token';
938978
if (name === 'comment-on-failure') return 'false';
939979
if (name === 'validate-work-item-exists') return 'false';
940-
if (name === 'append-work-item-title') return 'true';
980+
if (name === 'add-work-item-table') return 'true';
941981
if (name === 'azure-devops-token') return 'azdo-token';
942982
if (name === 'azure-devops-organization') return 'my-org';
943983
return '';
@@ -965,25 +1005,25 @@ describe('Azure DevOps Commit Validator', () => {
9651005
if (name === 'github-token') return 'github-token';
9661006
if (name === 'comment-on-failure') return 'false';
9671007
if (name === 'validate-work-item-exists') return 'false';
968-
if (name === 'append-work-item-title') return 'true';
1008+
if (name === 'add-work-item-table') return 'true';
9691009
if (name === 'azure-devops-token') return '';
9701010
if (name === 'azure-devops-organization') return '';
9711011
return '';
9721012
});
9731013

9741014
await run();
9751015

976-
expect(mockSetFailed).toHaveBeenCalledWith(expect.stringContaining('append-work-item-title'));
1016+
expect(mockSetFailed).toHaveBeenCalledWith(expect.stringContaining('add-work-item-table'));
9771017
});
9781018

979-
it('should append title with validate-work-item-exists also enabled', async () => {
1019+
it('should add table with validate-work-item-exists also enabled', async () => {
9801020
mockGetInput.mockImplementation(name => {
9811021
if (name === 'check-commits') return 'false';
9821022
if (name === 'check-pull-request') return 'true';
9831023
if (name === 'github-token') return 'github-token';
9841024
if (name === 'comment-on-failure') return 'false';
9851025
if (name === 'validate-work-item-exists') return 'true';
986-
if (name === 'append-work-item-title') return 'true';
1026+
if (name === 'add-work-item-table') return 'true';
9871027
if (name === 'azure-devops-token') return 'azdo-token';
9881028
if (name === 'azure-devops-organization') return 'my-org';
9891029
return '';
@@ -1004,10 +1044,10 @@ describe('Azure DevOps Commit Validator', () => {
10041044
expect(mockSetFailed).not.toHaveBeenCalled();
10051045
expect(mockValidateWorkItemExists).toHaveBeenCalled();
10061046
expect(mockGetWorkItemTitle).toHaveBeenCalledWith('my-org', 'azdo-token', '12345');
1007-
expect(mockOctokit.rest.pulls.update).toHaveBeenCalledWith(
1008-
expect.objectContaining({
1009-
body: 'This PR implements AB#12345 - Fix login bug'
1010-
})
1047+
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
1048+
expect(updateCall.body).toContain('This PR implements AB#12345');
1049+
expect(updateCall.body).toContain(
1050+
'| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Fix login bug |'
10111051
);
10121052
});
10131053
});

action.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ inputs:
4545
required: false
4646
default: 'true'
4747
append-work-item-title:
48-
description: 'Append the work item title to AB#xxx references in the PR body (e.g. AB#123 becomes AB#123 - Fix bug). Requires azure-devops-token and azure-devops-organization to be set.'
48+
description: 'Deprecated: Use add-work-item-table instead. This input will be removed in a future major version.'
49+
deprecationMessage: 'The append-work-item-title input is deprecated. Use add-work-item-table instead.'
50+
required: false
51+
default: 'false'
52+
add-work-item-table:
53+
description: 'Add a Linked Work Items table to the PR body showing titles for AB#xxx references (original AB# references are preserved). Requires azure-devops-token and azure-devops-organization to be set.'
4954
required: false
5055
default: 'false'
5156

badges/coverage.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)