From cf9ad19309159e18db62f2ccd88151f9b501f162 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Sun, 3 Dec 2023 15:01:29 -0500 Subject: [PATCH] Add support for linking work items in PR title/body (#77) * 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 --- .github/workflows/validator.yml | 42 --------- README.md | 83 ++++++++++++----- action.yml | 155 +++++++++++++++++++++++++------- main.js | 5 +- package-lock.json | 12 +-- 5 files changed, 191 insertions(+), 106 deletions(-) delete mode 100644 .github/workflows/validator.yml diff --git a/.github/workflows/validator.yml b/.github/workflows/validator.yml deleted file mode 100644 index 3b3de0f..0000000 --- a/.github/workflows/validator.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: pr_check -on: - pull_request: - branches: main - -env: - # update this - azure_devops_org: jjohanning0798 - -jobs: - pr-issues-validation: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: npm install - shell: bash - run: npm install - - name: pr-issues-validation - id: pr-issues-validation - shell: bash - run: | - PULL_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH") - COMMITS=$(curl -s -H 'Authorization: Bearer ${{ secrets.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 - 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 - 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" - - # make the call to main.js to do the linking - REPO_TOKEN=${{ github.token }} AZURE_DEVOPS_ORG=${{ env.azure_devops_org }} AZURE_DEVOPS_PAT=${{ secrets.AZURE_DEVOPS_PAT }} WORKITEMID=$WORKITEM PULLREQUESTID=${{ github.event.number }} REPO=${{ github.repository }} node main.js - fi - done diff --git a/README.md b/README.md index abcc0ae..02ea912 100644 --- a/README.md +++ b/README.md @@ -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: -image - -It also automatically links pull request to all of the Azure DevOps work item(s). - -image - -Screenshot of validating the logs and creating pull requests: - -image +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 @@ -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 +image -## How this works +image + +### Failing commit + +image + +### Adding Pull Request link in Azure DevOps to work item linked to a commit in a pull request + +image + +### Validating the logs and creating pull requests + +image + +## 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. diff --git a/action.yml b/action.yml index e4a560d..bafac60 100644 --- a/action.yml +++ b/action.yml @@ -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 diff --git a/main.js b/main.js index a4736ff..ceaf2ae 100644 --- a/main.js +++ b/main.js @@ -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; @@ -94,7 +95,7 @@ function run() { properties: { workItemId: workItemId, urls: [ - `https://github.com/${repo}/pull/${prRequestId}`, + `${githubHostname}/${repo}/pull/${prRequestId}`, ], }, }, @@ -165,4 +166,4 @@ function run() { }); } exports.run = run; -run(); \ No newline at end of file +run(); diff --git a/package-lock.json b/package-lock.json index ad14618..a926e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2236,9 +2236,9 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -4888,9 +4888,9 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0"