Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ on:
jobs:
pr-commit-message-enforcer-and-linker:
runs-on: ubuntu-latest
# Skip runs triggered by azure-boards bot editing the PR body to avoid duplicate workflow runs
if: github.actor != 'azure-boards[bot]'
permissions:
contents: read
pull-requests: write
Expand All @@ -63,19 +65,20 @@ jobs:

### Inputs

| Name | Description | Required | Default |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- |
| `check-pull-request` | Check the pull request for `AB#xxx` (scope configurable via `pull-request-check-scope`) | `true` | `false` |
| `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` |
| `check-commits` | Check each commit in the pull request for `AB#xxx` | `true` | `true` |
| `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` |
| `link-commits-to-pull-request` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `true` |
| `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` |
| `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` |
| `azure-devops-organization` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `''` |
| `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` | `''` |
| `github-token` | The GitHub token that has contents-read and pull_request-write access | `true` | `${{ github.token }}` |
| `comment-on-failure` | Comment on the pull request if the action fails | `true` | `true` |
| Name | Description | Required | Default |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- |
| `check-pull-request` | Check the pull request for `AB#xxx` (scope configurable via `pull-request-check-scope`) | `true` | `false` |
| `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` |
| `check-commits` | Check each commit in the pull request for `AB#xxx` | `true` | `true` |
| `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` |
| `link-commits-to-pull-request` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `true` |
| `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` |
| `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` |
| `append-work-item-title` | **Deprecated** - use `add-work-item-table` instead. Will be removed in a future major version. | `false` | `false` |
| `azure-devops-organization` | Only if `check-commits=true`, link the work items found in commits to the pull request | `false` | `''` |
| `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` | `''` |
| `github-token` | The GitHub token that has contents-read and pull_request-write access | `true` | `${{ github.token }}` |
| `comment-on-failure` | Comment on the pull request if the action fails | `true` | `true` |

## Screenshots

Expand Down
96 changes: 68 additions & 28 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ describe('Azure DevOps Commit Validator', () => {
'github-token': 'github-token',
'comment-on-failure': 'true',
'validate-work-item-exists': 'false',
'append-work-item-title': 'false'
'append-work-item-title': 'false',
'add-work-item-table': 'false'
};
return defaults[name] || '';
});
Expand Down Expand Up @@ -809,15 +810,15 @@ describe('Azure DevOps Commit Validator', () => {
});
});

describe('Append work item title', () => {
it('should append work item title to AB# in PR body when enabled', async () => {
describe('Work item title table', () => {
it('should add work item title table to PR body when enabled', async () => {
mockGetInput.mockImplementation(name => {
if (name === 'check-commits') return 'false';
if (name === 'check-pull-request') return 'true';
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'false';
if (name === 'append-work-item-title') return 'true';
if (name === 'add-work-item-table') return 'true';
if (name === 'azure-devops-token') return 'azdo-token';
if (name === 'azure-devops-organization') return 'my-org';
return '';
Expand All @@ -836,14 +837,16 @@ describe('Azure DevOps Commit Validator', () => {

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

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

mockGetWorkItemTitle.mockResolvedValue({ title: 'Fix login bug', type: 'Bug' });

await run();

expect(mockSetFailed).not.toHaveBeenCalled();
expect(mockGetWorkItemTitle).not.toHaveBeenCalled();
expect(mockOctokit.rest.pulls.update).not.toHaveBeenCalled();
expect(mockOctokit.rest.pulls.update).toHaveBeenCalled();
});

it('should update section when work item titles section already exists', async () => {
mockGetInput.mockImplementation(name => {
if (name === 'check-commits') return 'false';
if (name === 'check-pull-request') return 'true';
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'false';
if (name === 'add-work-item-table') return 'true';
if (name === 'azure-devops-token') return 'azdo-token';
if (name === 'azure-devops-organization') return 'my-org';
return '';
});

mockOctokit.rest.pulls.get.mockResolvedValue({
data: {
title: 'feat: new feature',
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 -->'
}
});

mockGetWorkItemTitle.mockResolvedValue({ title: 'Fix login bug', type: 'Bug' });

await run();

expect(mockSetFailed).not.toHaveBeenCalled();
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
expect(updateCall.body).toContain('This PR implements AB#12345');
expect(updateCall.body).toContain(
'| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Fix login bug |'
);
expect(updateCall.body).not.toContain('Old title');
});

it('should not append when append-work-item-title is false', async () => {
it('should not add table when add-work-item-table is false', async () => {
mockGetInput.mockImplementation(name => {
if (name === 'check-commits') return 'false';
if (name === 'check-pull-request') return 'true';
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'false';
if (name === 'append-work-item-title') return 'false';
if (name === 'add-work-item-table') return 'false';
return '';
});

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

expect(mockSetFailed).not.toHaveBeenCalled();
expect(mockGetWorkItemTitle).toHaveBeenCalledTimes(2);
expect(mockOctokit.rest.pulls.update).toHaveBeenCalledWith(
expect.objectContaining({
body: 'This PR implements AB#111 - First item and AB#222 - Second item'
})
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
expect(updateCall.body).toContain('This PR implements AB#111 and AB#222');
expect(updateCall.body).toContain(
'| [111](https://dev.azure.com/my-org/_workitems/edit/111) | User Story | First item |'
);
expect(updateCall.body).toContain(
'| [222](https://dev.azure.com/my-org/_workitems/edit/222) | Bug | Second item |'
);
});

Expand All @@ -937,7 +977,7 @@ describe('Azure DevOps Commit Validator', () => {
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'false';
if (name === 'append-work-item-title') return 'true';
if (name === 'add-work-item-table') return 'true';
if (name === 'azure-devops-token') return 'azdo-token';
if (name === 'azure-devops-organization') return 'my-org';
return '';
Expand Down Expand Up @@ -965,25 +1005,25 @@ describe('Azure DevOps Commit Validator', () => {
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'false';
if (name === 'append-work-item-title') return 'true';
if (name === 'add-work-item-table') return 'true';
if (name === 'azure-devops-token') return '';
if (name === 'azure-devops-organization') return '';
return '';
});

await run();

expect(mockSetFailed).toHaveBeenCalledWith(expect.stringContaining('append-work-item-title'));
expect(mockSetFailed).toHaveBeenCalledWith(expect.stringContaining('add-work-item-table'));
});

it('should append title with validate-work-item-exists also enabled', async () => {
it('should add table with validate-work-item-exists also enabled', async () => {
mockGetInput.mockImplementation(name => {
if (name === 'check-commits') return 'false';
if (name === 'check-pull-request') return 'true';
if (name === 'github-token') return 'github-token';
if (name === 'comment-on-failure') return 'false';
if (name === 'validate-work-item-exists') return 'true';
if (name === 'append-work-item-title') return 'true';
if (name === 'add-work-item-table') return 'true';
if (name === 'azure-devops-token') return 'azdo-token';
if (name === 'azure-devops-organization') return 'my-org';
return '';
Expand All @@ -1004,10 +1044,10 @@ describe('Azure DevOps Commit Validator', () => {
expect(mockSetFailed).not.toHaveBeenCalled();
expect(mockValidateWorkItemExists).toHaveBeenCalled();
expect(mockGetWorkItemTitle).toHaveBeenCalledWith('my-org', 'azdo-token', '12345');
expect(mockOctokit.rest.pulls.update).toHaveBeenCalledWith(
expect.objectContaining({
body: 'This PR implements AB#12345 - Fix login bug'
})
const updateCall = mockOctokit.rest.pulls.update.mock.calls[0][0];
expect(updateCall.body).toContain('This PR implements AB#12345');
expect(updateCall.body).toContain(
'| [12345](https://dev.azure.com/my-org/_workitems/edit/12345) | Bug | Fix login bug |'
);
});
});
Expand Down
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ inputs:
required: false
default: 'true'
append-work-item-title:
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.'
description: 'Deprecated: Use add-work-item-table instead. This input will be removed in a future major version.'
deprecationMessage: 'The append-work-item-title input is deprecated. Use add-work-item-table instead.'
required: false
default: 'false'
add-work-item-table:
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.'
required: false
default: 'false'

Expand Down
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading