Skip to content

Commit

Permalink
Add support for linking work items in PR title/body (#77)
Browse files Browse the repository at this point in the history
* feat!: add in PR validator

* fix: if statements

* fix: fix loop

* fix: update search

* fix: fi

* fix: logic

* fix: remove done

* feat: add pr comment

* fix: add token so gh works

* feat: link if PR

* refactor: consolidate PR link

* feat: extract work items from pr title/body

* feat: better if statement

* fix: adding back in comment

* fix: pr message

* fix: pr message

* fix: pr comment

* fix: remove duplicates

* fix: pr message

* fix: add error msg

* chore: updating notes/comments

* fix: pr msg

* chore: note

* fix: pr msg

* chore: adding todo on pr body/title

* fix: case insensitive grep

* feat: getting pr body/title via api

* fix: better pattern

* fix: logging

* fix: commit msg

* fix: npm install

* fix: npm logs

* feat: don't double post comments

* fix: api

* fix: update url

* fix: pr msg

* fix: comments

* fix: msg

* fix: pr

* fix: clean

* fix: updating pr msg

* feat: adding time

* fix: update url to edit comment

* fix: improving logs

* fix: test

* fix: logs

* fix: intentionally break

* fix: comment

* fix: update json5

* fix: npm install

* fix: updating errors

* fix: todo

* fix: update msg

* feat: dynamic github urls (support ghes)

* feat: prereq check

* feat: improve logging

* fix: comment url

* chore: clean up comments

* docs: enhance readme

* fix: only install npm modules if not exist
closes #69

* chore: workflow cleanup

* docs: add permissions and token to readme

* docs: clean and re-add azdo token

* docs: clean
  • Loading branch information
joshjohanning authored Dec 3, 2023
1 parent 82fdaff commit cf9ad19
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 106 deletions.
42 changes: 0 additions & 42 deletions .github/workflows/validator.yml

This file was deleted.

83 changes: 60 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
# Azure DevOps Commit Validator and Pull Request Linker Action
# Azure DevOps Commit/PR Validator and Pull Request Linker Action

This is an action to be ran in a pull request to make sure that all commits have a `AB#123` in the commit message.
This is an action to be ran in a pull request to make sure either that one or both of the following scenarios are met:

<img width="1033" alt="image" src="https://user-images.githubusercontent.com/19912012/182519049-3bd1281d-985c-41ea-b35c-c3cd35994d48.png">

It also automatically links pull request to all of the Azure DevOps work item(s).

<img width="290" alt="image" src="https://user-images.githubusercontent.com/19912012/182518941-4c7d5bad-b19f-456a-b3bd-504b3ab2f45d.png">

Screenshot of validating the logs and creating pull requests:

<img width="713" alt="image" src="https://user-images.githubusercontent.com/19912012/182616583-70ef5ac4-c669-40df-8fa4-60b15ab1f58f.png">
1. Pull request title or body contains an Azure DevOps work item link (e.g. `AB#123`)
2. Each commit in a pull request has an Azure DevOps work item link (e.g. `AB#123`) in the commit message
- Optionally, add a GitHub Pull Request link to the work item in Azure DevOps
- By default, Azure DevOps only adds the Pull Request link to work items mentioned directly in the PR title or body

## Usage

Expand All @@ -26,30 +21,72 @@ on:
jobs:
pr-commit-message-enforcer-and-linker:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Azure DevOps Commit Validator and Pull Request Linker
uses: joshjohanning/azdo_commit_message_validator@v1
uses: joshjohanning/azdo_commit_message_validator@v2
with:
azure-devops-organization: myorg # The name of the Azure DevOps organization
azure-devops-token: ${{ secrets.AZURE_DEVOPS_PAT }} # "Azure DevOps Personal Access Token (needs to be a full PAT)
fail-if-missing-workitem-commit-link: true # Fail the action if a commit in the pull request is missing AB# in the commit message
link-commits-to-pull-request: true # Link the work items found in commits to the pull request
check-pull-request: true
check-commits: true
fail-if-missing-workitem-commit-link: true
link-commits-to-pull-request: true
azure-devops-organization: my-azdo-org
azure-devops-token: ${{ secrets.AZURE_DEVOPS_PAT }}
```
### Inputs
| Name | Description | Required | Default |
| --- | --- | --- | --- |
| `check-pull-request` | Check the pull request body and title for `AB#xxx` | `true` | `true` |
| `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` |
| `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 }}` |

## Setup

1. Create a repository secret titled `AZURE_DEVOPS_PAT` - it needs to be a full PAT
2. Pass the Azure DevOps organization to the `azure-devops-organization` input parameter
### Runner Software Requirements

Required software installed on runner:

- [`gh` (GitHub CLI)](https://cli.github.com/)
- [`jq`](https://jqlang.github.io/jq/download/)

## Screenshots

### Failing pull request, including comment back to the pull request showing why it failed

Note: `jq` needs to be [installed](https://stedolan.github.io/jq/download/) on the runner running this action
<img width="917" alt="image" src="https://github.com/joshjohanning/azdo_commit_message_validator/assets/19912012/383358aa-748e-4666-be52-2ae6e371530e">

## How this works
<img width="868" alt="image" src="https://github.com/joshjohanning/azdo_commit_message_validator/assets/19912012/2dbc0775-7810-4ba3-98e0-b49391c63e7e">

### Failing commit

<img width="1033" alt="image" src="https://user-images.githubusercontent.com/19912012/182519049-3bd1281d-985c-41ea-b35c-c3cd35994d48.png">

### Adding Pull Request link in Azure DevOps to work item linked to a commit in a pull request

<img width="290" alt="image" src="https://user-images.githubusercontent.com/19912012/182518941-4c7d5bad-b19f-456a-b3bd-504b3ab2f45d.png">

### Validating the logs and creating pull requests

<img width="713" alt="image" src="https://user-images.githubusercontent.com/19912012/182616583-70ef5ac4-c669-40df-8fa4-60b15ab1f58f.png">

## How the commit / pull request linking in Azure DevOps works

If the `check-commits: true` the action will look at each commit in the pull request and check for `AB#123` in the commit message.

The action loops through each commit and:
1. makes sure it has `AB#123` in the commit message
2. if yes, add a GitHub Pull Request link to the work item in Azure DevOps

1. Makes sure it has `AB#123` in the commit message
2. If it does, and if `link-commits-to-pull-request: true`, add a GitHub Pull Request link to the work item in Azure DevOps

Adding the link to the GitHub Pull Request was the tricky part.

Expand Down
155 changes: 122 additions & 33 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,149 @@ branding:
color: "purple"

inputs:
azure-devops-organization:
description: "The name of the Azure DevOps organization"
required: true
github-token:
description: "The GitHub token that has commit and pull request access"
check-pull-request:
description: "Check the pull request body and title for AB#xxx"
required: true
default: ${{ github.token }}
azure-devops-token:
description: "Azure DevOps Personal Access Token (needs to be a full PAT)"
default: false
check-commits:
description: "Check each commit in the pull request for AB#xxx"
required: true
default: true
fail-if-missing-workitem-commit-link:
description: "Fail the action if a commit in the pull request is missing AB# in the commit message"
required: true
description: "Only if check-commits=true, fail the action if a commit in the pull request is missing AB# in every commit message"
required: false
default: true
link-commits-to-pull-request:
description: "Link the work items found in commits to the pull request"
description: "Only if check-commits=true, link the work items found in commits to the pull request"
required: false
default: true
azure-devops-token:
description: "Only required if link-commits-to-pull-request=true, Azure DevOps Personal Access Token to link work item to PR (needs to be a full PAT)"
required: false
azure-devops-organization:
description: "Only required if link-commits-to-pull-request=true, the name of the Azure DevOps organization"
required: false
github-token:
description: "The GitHub token that has contents-read and pull_request-write access"
required: true
default: ${{ github.token }}
comment-on-failure:
description: "Comment on the pull request if the action fails"
required: true
default: true

runs:
using: "composite"
steps:
- name: pr-workitems-validator
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
run: |
# run azdo_commit_message_validator
# prerequisite check
for cmd in gh jq cut grep; do
if ! command -v $cmd &> /dev/null; then
echo "::error title=${cmd} not installed::Could not find \`${cmd}\` on the runner"
exit 1
fi
done
# have to do some hocus pocus since this isn't a full javascript action
main=$(find ../../_actions/joshjohanning/azdo_commit_message_validator -name "main.js" | grep -v "node_modules")
echo "::debug::main.js script location: $main"
PULL_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
COMMITS=$(curl -s -H 'Authorization: Bearer ${{ inputs.github-token }}' -H "Accept: application/vnd.github.v3+json" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PULL_NUMBER}/commits")
echo $COMMITS | jq -c '.[]' | while read commit; do
COMMIT_SHA=$(echo $commit | jq '.sha')
COMMIT_MESSAGE=$(echo $commit | jq '.commit.message')
echo "Validating new commit: ${COMMIT_SHA} - ${COMMIT_MESSAGE}"
if [[ "$COMMIT_MESSAGE" != *"AB"\#""[0-9]""* ]] && [[ "$COMMIT_MESSAGE" != *"ab"\#""[0-9]""* ]]; then
# only fail the action if the input is true
if ${{ inputs.fail-if-missing-workitem-commit-link }}; then
echo ""
echo ""
echo "Pull request contains invalid commit: ${COMMIT_SHA}. This commit lacks an AB#xxx in the message, in the expected format: AB#xxx -- failing operation."
exit 1
# install npm modules if they are not found
if ${{ inputs.check-commits }}; then
if ${{ inputs.link-commits-to-pull-request }}; then
DIRECTORY=$(dirname "${main}")
if [ ! -d "${DIRECTORY}/node_modules" ]; then
CURRENT_DIRECTORY=$(pwd)
cd $DIRECTORY
npm install
echo "" && echo ""
cd $CURRENT_DIRECTORY
fi
fi
COMMITS=$(gh api --paginate ${{ github.event.pull_request.commits_url }})
echo $COMMITS | jq -c '.[]' | while read commit; do
COMMIT_SHA=$(echo $commit | jq '.sha')
SHORT_COMMIT_SHA=$(echo $COMMIT_SHA | cut -c 1-7)
COMMIT_MESSAGE=$(echo $commit | jq '.commit.message')
echo "Validating new commit: ${COMMIT_SHA} - ${COMMIT_MESSAGE}"
if ! echo "$COMMIT_MESSAGE" | grep -i -E -q "AB#[0-9]+"; then
# only fail the action if the input is true
if ${{ inputs.fail-if-missing-workitem-commit-link }}; then
echo ""
echo ""
echo "Pull request contains invalid commit: ${COMMIT_SHA}. This commit lacks an AB#xxx in the message, in the expected format: AB#xxx -- failing operation."
echo "::error title=Commit(s) not linked to work items::There is at least one commit (${SHORT_COMMIT_SHA}) in pull request #${PULL_NUMBER} that is not linked to a work item"
# TODO: add comment to pr
exit 1
fi
else
echo "valid commit"
# set WORKITEM equal to the number after the # in the commit message
WORKITEM=$(echo "$COMMIT_MESSAGE" | grep -i -o -E "AB#[0-9]+" | cut -c 4-)
echo "Workitem = $WORKITEM"
if ${{ inputs.link-commits-to-pull-request }}; then
# make the call to main.js to do the linking
# TODO: check to see if org/pat are set
echo "Attempting to link work item ${WORKITEM} to pull request ${PULL_NUMBER}..."
REPO_TOKEN=${{ inputs.github-token }} AZURE_DEVOPS_ORG=${{ inputs.azure-devops-organization }} AZURE_DEVOPS_PAT=${{ inputs.azure-devops-token }} WORKITEMID=$WORKITEM PULLREQUESTID=${{ github.event.number }} REPO=${{ github.repository }} node $main
echo "...PR linked to work item"
fi
fi
done
fi
if ${{ inputs.check-pull-request }}; then
PULL_REQUEST=$(gh api ${{ github.event.pull_request.url }})
PULL_BODY=$(echo $PULL_REQUEST | jq --raw-output .body)
PULL_TITLE=$(echo $PULL_REQUEST | jq --raw-output .title)
if ! echo "$PULL_TITLE $PULL_BODY" | grep -i -E -q "AB#[0-9]+"; then
echo "PR not linked to a work item"
echo "::error title=Pull Request not linked to work item(s)::The pull request #${PULL_NUMBER} is not linked to any work item(s)"
COMMENT=":x: This pull request is not linked to a work item. Please update the title or body to include a work item and re-run the failed job to continue. Any new commits to the pull request will also re-run the job."
CURRENT_DATE_TIME=$(date +"%Y-%m-%d %T")
COMMENT_EXTRA=$'\n\n[View workflow run for details](${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt}}) _(last ran\: '${CURRENT_DATE_TIME}')_'
COMMENT_COMBINED="${COMMENT}${COMMENT_EXTRA}"
COMMENTS_CLEAN=$(gh api --paginate ${{ github.event.pull_request.comments_url }} | tr -d '\000-\031')
COMMENT_ID=$(echo "$COMMENTS_CLEAN" | jq -r --arg comment "$COMMENT" '.[] | select(.body | contains($comment)) | .id')
echo "Comment ID(s): $COMMENT_ID"
# Check if the comment already exists
if [ -n "$COMMENT_ID" ]; then
echo "Comment already exists: $COMMENT_ID"
# Edit the comment
echo "... attempting to update the PR comment"
gh api ${{ github.event.repository.url }}/issues/comments/${COMMENT_ID} --method PATCH --field body="${COMMENT_COMBINED}" > /dev/null
echo "... PR comment updated"
else
echo "Comment does not exist. Posting a new comment."
gh pr comment $PULL_NUMBER --body "${COMMENT_COMBINED}"
fi
else
echo "valid commit"
# set WORKITEM equal to the number after the # in the commit message
WORKITEM=`echo $COMMIT_MESSAGE | cut -d'#' -f2 | cut -d' ' -f1 | tr -d '"'`
echo "Workitem = $WORKITEM"
exit 1
else
echo "PR linked to work item"
# TODO: do we want to update or delete the PR comment if it exists b/c previously failed?
# Extract work items from PR body and title
WORKITEMS=$(echo "$PULL_BODY $PULL_TITLE" | grep -i -o -E "AB#[0-9]+" | sort | uniq)
if ${{ inputs.link-commits-to-pull-request }}; then
# make the call to main.js to do the linking
REPO_TOKEN=${{ inputs.github-token }} AZURE_DEVOPS_ORG=${{ inputs.azure-devops-organization }} AZURE_DEVOPS_PAT=${{ inputs.azure-devops-token }} WORKITEMID=$WORKITEM PULLREQUESTID=${{ github.event.number }} REPO=${{ github.repository }} node $main
# Loop through each work item
for WORKITEM in $WORKITEMS; do
# Remove the "AB#" or "ab#" prefix and keep only the number
WORKITEM_NUMBER=${WORKITEM:3}
echo "Pull request linked to work item number: $WORKITEM_NUMBER"
# TODO: validate work item?
# TODO: add this as an ::info or to the job summary?
done
fi
fi
done
5 changes: 3 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function run() {
const devOpsOrg = process.env.AZURE_DEVOPS_ORG;
const azToken = process.env.AZURE_DEVOPS_PAT;
const workItemId = process.env.WORKITEMID;
const githubHostname = process.env.GITHUB_SERVER_URL;
const prRequestId = process.env.PULLREQUESTID;
const dataProviderUrl = dataProviderUrlBase.replace("%DEVOPS_ORG%", devOpsOrg);
const repo = process.env.REPO;
Expand Down Expand Up @@ -94,7 +95,7 @@ function run() {
properties: {
workItemId: workItemId,
urls: [
`https://github.com/${repo}/pull/${prRequestId}`,
`${githubHostname}/${repo}/pull/${prRequestId}`,
],
},
},
Expand Down Expand Up @@ -165,4 +166,4 @@ function run() {
});
}
exports.run = run;
run();
run();
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cf9ad19

Please sign in to comment.