From 10be73c081dc63b39199c5262766ed962f42dbaf Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 12 Sep 2025 13:40:11 -0700 Subject: [PATCH] ci: fix github to jira issue sync (#26747) Add local actions for JIRA interactions to replace github actions that have been archived. --- .github/actions/jira/create/action.yml | 43 +++++++++++ .github/actions/jira/create/jira-create.bash | 75 +++++++++++++++++++ .github/actions/jira/search/action.yml | 22 ++++++ .github/actions/jira/search/jira-search.bash | 28 +++++++ .github/actions/jira/shared.bash | 67 +++++++++++++++++ .github/actions/jira/sync-comment/action.yml | 22 ++++++ .../jira/sync-comment/jira-sync-comment.bash | 38 ++++++++++ .github/actions/jira/transition/action.yml | 22 ++++++ .../jira/transition/jira-transition.bash | 44 +++++++++++ .github/workflows/jira-sync.yml | 58 ++++++-------- 10 files changed, 384 insertions(+), 35 deletions(-) create mode 100644 .github/actions/jira/create/action.yml create mode 100755 .github/actions/jira/create/jira-create.bash create mode 100644 .github/actions/jira/search/action.yml create mode 100755 .github/actions/jira/search/jira-search.bash create mode 100644 .github/actions/jira/shared.bash create mode 100644 .github/actions/jira/sync-comment/action.yml create mode 100755 .github/actions/jira/sync-comment/jira-sync-comment.bash create mode 100644 .github/actions/jira/transition/action.yml create mode 100755 .github/actions/jira/transition/jira-transition.bash diff --git a/.github/actions/jira/create/action.yml b/.github/actions/jira/create/action.yml new file mode 100644 index 000000000..9efcff978 --- /dev/null +++ b/.github/actions/jira/create/action.yml @@ -0,0 +1,43 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: jira-create +description: Create a JIRA issue +inputs: + project: + required: true + description: JIRA project + default: NMD + issuetype: + required: true + description: Type of JIRA issue + default: "GH Issue" + summary: + required: true + description: Title of the issue + description: + required: false + description: Description of the issue + extraFields: + required: false + description: Extra fields to add to issue +outputs: + issue: + description: JIRA issue ID of created issue + value: ${{ steps.create.outputs.issue }} + issue-key: + description: JIRA issue key of created issue + value: ${{ steps.create.outputs.issue-key }} +runs: + using: composite + steps: + - name: Create JIRA issue + id: create + shell: bash + run: ./.github/actions/jira/create/jira-create.bash + env: + PROJECT: ${{ inputs.project }} + ISSUE_TYPE: ${{ inputs.issueType }} + SUMMARY: ${{ inputs.summary }} + DESCRIPTION: ${{ inputs.description }} + EXTRA_FIELDS: ${{ inputs.extraFields }} diff --git a/.github/actions/jira/create/jira-create.bash b/.github/actions/jira/create/jira-create.bash new file mode 100755 index 000000000..803722232 --- /dev/null +++ b/.github/actions/jira/create/jira-create.bash @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +source "$(dirname "${BASH_SOURCE[0]}")/../shared.bash" + +# Check for required input values +if [ -z "${ISSUE_TYPE}" ]; then + error "Missing 'issueType' input value" + exit 1 +fi + +if [ -z "${PROJECT}" ]; then + error "Missing 'project' input value" + exit 1 +fi + +if [ -z "${SUMMARY}" ]; then + error "Missing 'summary' input value" + exit 1 +fi + +# Grab the issue type ID +result="$(jira-request "${JIRA_BASE_URL}/rest/api/3/issuetype")" || exit +query="$(printf '.[] | select(.name == "%s").id' "${ISSUE_TYPE}")" +type_id="$(jq -r "${query}" <<< "${result}")" + +if [ -z "${type_id}" ]; then + error "Could not find issue type with name '%s'" "${ISSUE_TYPE}" + exit 1 +fi + +info "Issue type ID for '%s': %s" "${ISSUE_TYPE}" "${type_id}" + +if [ -n "${DESCRIPTION}" ]; then + description="$(convert-gfm-to-jira "${DESCRIPTION}")" || exit +fi + +# Base template for issue creation +template=' +{ + description: $description, + issuetype: { + id: $issuetype + }, + project: { + key: $project + }, + summary: $summary +}' +new_issue="$(jq -n --arg description "${description}" --arg issuetype "${type_id}" --arg project "${PROJECT}" --arg summary "${SUMMARY}" "${template}")" || exit + +# If there are extra fields provided, merge them in +if [ -n "${EXTRA_FIELDS}" ]; then + new_issue="$(printf "%s %s" "${new_issue}" "${EXTRA_FIELDS}" | jq -s add)" +fi + +# Wrap the payload for submission +template='{fields: $fields}' +new_issue="$(jq -n --argjson fields "${new_issue}" "${template}")" || exit + +info "JIRA new issue payload:\n%s" "${new_issue}" + +# Create the issue +# NOTE: The v2 API is used here for creating the issue. This is because +# the v3 API only supports the Atlassian Document Format for which pandoc +# currently does not have support (https://github.com/jgm/pandoc/issues/9898) +result="$(jira-request --request "POST" --data "${new_issue}" "${JIRA_BASE_URL}/rest/api/2/issue")" || exit +key="$(jq -r '.key' <<< "${result}")" +id="$(jq -r '.id' <<< "${result}")" + +printf "issue=%s\n" "${id}" >> "${GITHUB_OUTPUT}" +printf "issue-key=%s\n" "${key}" >> "${GITHUB_OUTPUT}" + +info ">> New JIRA issue created: %s/browse/%s" "${JIRA_BASE_URL}" "${key}" diff --git a/.github/actions/jira/search/action.yml b/.github/actions/jira/search/action.yml new file mode 100644 index 000000000..21d60626e --- /dev/null +++ b/.github/actions/jira/search/action.yml @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: jira-search +description: Search for a JIRA issue +inputs: + jql: + required: true + description: JQL used to search for issue +outputs: + issue: + description: JIRA issue ID matching JQL + value: ${{ steps.search.outputs.issue }} +runs: + using: composite + steps: + - name: Search for JIRA issue + id: search + shell: bash + run: ./.github/actions/jira/search/jira-search.bash + env: + JQL: ${{ inputs.jql }} diff --git a/.github/actions/jira/search/jira-search.bash b/.github/actions/jira/search/jira-search.bash new file mode 100755 index 000000000..c5dce3ebc --- /dev/null +++ b/.github/actions/jira/search/jira-search.bash @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +source "$(dirname "${BASH_SOURCE[0]}")/../shared.bash" + +# Check for required inputs +if [ -z "${JQL}" ]; then + error "Missing 'jql' input value" + exit 1 +fi + +info "Searching for existing JIRA issue..." +info "JQL: %s" "${JQL}" +template='{jql: $jql}' +search="$(jq -n --arg jql "${JQL}" "${template}")" || exit +result="$(jira-request --request "POST" --data "${search}" "${JIRA_BASE_URL}/rest/api/3/search/jql")" || exit +issue="$(jq -r '.issues[].id' <<< "${result}")" + +if [ -z "${issue}" ]; then + info "No existing issue found in JIRA" + exit +fi + +info "Existing JIRA issue found: %s" "${issue}" + +# Make issue available in output +printf "issue=%s\n" "${issue}" >> "${GITHUB_OUTPUT}" diff --git a/.github/actions/jira/shared.bash b/.github/actions/jira/shared.bash new file mode 100644 index 000000000..0d474c39e --- /dev/null +++ b/.github/actions/jira/shared.bash @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +TEXT_RED='\e[31m' +TEXT_CLEAR='\e[0m' + +# Make a request to the Jira API +function jira-request() { + curl --show-error --location --fail \ + --user "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + "${@}" +} + +# Write an informational message +function info() { + local msg_template="${1}\n" + local i=$(( ${#} - 1 )) + local msg_args=("${@:2:$i}") + + #shellcheck disable=SC2059 + printf ">> ${msg_template}" "${msg_args[@]}" >&2 +} + +# Write an error message +function error() { + local msg_template="${1}\n" + local i=$(( ${#} - 1 )) + local msg_args=("${@:2:$i}") + + #shellcheck disable=SC2059 + printf "%b!! ERROR:%b ${msg_template}%b" "${TEXT_RED}" "${TEXT_CLEAR}" "${msg_args[@]}" >&2 +} + +# Convert content from GitHub format to Jira format +function convert-gfm-to-jira() { + local content="${1?Content value is required}" + local src + src="$(mktemp)" || + return 1 + printf "%s" "${content}" > "${src}" + # NOTE: Using docker here instead of installing the pandoc package directly. + # This is because when installing the pandoc package in CI the post install + # tasks take multiple minutes to complete. + docker run --rm -v "$(dirname "${src}"):/data" pandoc/core --from=gfm --to=jira "/data/$(basename "${src}")" || + return 1 + rm -f "${src}" + return 0 +} + +# Check for environment variables that must always be set +if [ -z "${JIRA_BASE_URL}" ]; then + error "Missing JIRA_BASE_URL environment variable" + exit 1 +fi + +if [ -z "${JIRA_USER_EMAIL}" ]; then + error "Missing JIRA_USER_EMAIL environment variable" + exit 1 +fi + +if [ -z "${JIRA_API_TOKEN}" ]; then + error "Missing JIRA_API_TOKEN environment variable" + exit 1 +fi diff --git a/.github/actions/jira/sync-comment/action.yml b/.github/actions/jira/sync-comment/action.yml new file mode 100644 index 000000000..694b444a8 --- /dev/null +++ b/.github/actions/jira/sync-comment/action.yml @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: jira-sync-comment +description: Sync GitHub comment to JIRA issue +inputs: + issue: + required: true + description: JIRA issue to sync comment + comment: + required: true + description: Comment to add to JIRA issue + +runs: + using: composite + steps: + - name: Sync comment to JIRA + shell: bash + run: ./.github/actions/jira/sync-comment/jira-sync-comment.bash + env: + ISSUE: ${{ inputs.issue }} + COMMENT: ${{ inputs.comment }} diff --git a/.github/actions/jira/sync-comment/jira-sync-comment.bash b/.github/actions/jira/sync-comment/jira-sync-comment.bash new file mode 100755 index 000000000..08b85ef23 --- /dev/null +++ b/.github/actions/jira/sync-comment/jira-sync-comment.bash @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +source "$(dirname "${BASH_SOURCE[0]}")/../shared.bash" + +# Check for required inputs +if [ -z "${ISSUE}" ]; then + error "Missing 'issue' input value" + exit 1 +fi + +if [ -z "${COMMENT}" ]; then + error "Missing 'comment' input value" + exit 1 +fi + +comment="$(convert-gfm-to-jira "${COMMENT}")" || exit +template=' +{ + body: $comment +} +' +issue_comment="$(jq -n --arg comment "${comment}" "${template}")" + +info "Adding comment to JIRA issue %s" "${ISSUE}" +info "Comment payload: %s" "${issue_comment}" + +# Create the comment +# NOTE: The v2 API is used here for creating the comment. This is because +# the v3 API only supports the Atlassian Document Format for which pandoc +# currently does not have support (https://github.com/jgm/pandoc/issues/9898) +result="$(jira-request --request "POST" --data "${issue_comment}" "${JIRA_BASE_URL}/rest/api/2/issue/${ISSUE}/comment")" || exit +comment_id="$(jq -r .id <<< "${result}")" + +info "JIRA issue ID %s updated with new comment ID %s" "${ISSUE}" "${comment_id}" + +printf "comment-id=%s\n" "${comment_id}" >> "${GITHUB_OUTPUT}" diff --git a/.github/actions/jira/transition/action.yml b/.github/actions/jira/transition/action.yml new file mode 100644 index 000000000..f5cf9cb99 --- /dev/null +++ b/.github/actions/jira/transition/action.yml @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: jira-transition +description: Transition state of JIRA issue +inputs: + issue: + required: true + description: JIRA issue to transition + transition: + required: true + description: Transition name to apply to issue + +runs: + using: composite + steps: + - name: Transition JIRA issue + shell: bash + run: ./.github/actions/jira/transition/jira-transition.bash + env: + ISSUE: ${{ inputs.issue }} + TRANSITION: ${{ inputs.transition }} diff --git a/.github/actions/jira/transition/jira-transition.bash b/.github/actions/jira/transition/jira-transition.bash new file mode 100755 index 000000000..40f4277d3 --- /dev/null +++ b/.github/actions/jira/transition/jira-transition.bash @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +source "$(dirname "${BASH_SOURCE[0]}")/../shared.bash" + +# Check for required inputs +if [ -z "${ISSUE}" ]; then + error "Missing 'issue' input value" + exit 1 +fi + +if [ -z "${TRANSITION}" ]; then + error "Missing 'transition' input value" + exit 1 +fi + +# Grab the transition ID +result="$(jira-request "${JIRA_BASE_URL}/rest/api/3/issue/${ISSUE}/transitions")" || exit +query="$(printf '.transitions[] | select(.name == "%s").id' "${TRANSITION}")" +transition_id="$(jq -r "${query}" <<< "${result}")" + +if [ -z "${transition_id}" ]; then + error "Could not find matching transition with name matching '%s'" "${TRANSITION}" + exit 1 +fi + +template=' +{ + transition: { + id: $transition + } +} +' +issue_transition="$(jq -n --arg transition "${transition_id}" "${template}")" || exit + +info "Transitioning JIRA issue '%s' to %s (ID: %s)" "${ISSUE}" \ + "${TRANSITION}" "${transition_id}" +info "Transition payload:\n%s" "${issue_transition}" + +jira-request --request "POST" --data "${issue_transition}" \ + "${JIRA_BASE_URL}/rest/api/3/issue/${ISSUE}/transitions" || exit + +info "JIRA issue '%s' transitioned to %s" "${ISSUE}" "${TRANSITION}" diff --git a/.github/workflows/jira-sync.yml b/.github/workflows/jira-sync.yml index 08dffe2a1..4b11d787d 100644 --- a/.github/workflows/jira-sync.yml +++ b/.github/workflows/jira-sync.yml @@ -10,30 +10,31 @@ on: name: Jira Issue Sync +env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + jobs: sync: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest name: Jira Issue sync steps: - - name: Login - uses: atlassian/gajira-login@45fd029b9f1d6d8926c6f04175aa80c0e42c9026 # v3.0.1 - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - - name: Set ticket type - id: set-ticket-type - run: | - echo "TYPE=GH Issue" >> "$GITHUB_OUTPUT" - + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Search + if: github.event.action != 'opened' + id: search + uses: ./.github/actions/jira/search + with: + # cf[10089] is Issue Link (use JIRA API to retrieve) + jql: 'cf[10089] = "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' - name: Create ticket if an issue is labeled with hcc/jira - if: github.event.action == 'labeled' && github.event.label.name == 'hcc/jira' - uses: tomhjp/gh-action-jira-create@3ed1789cad3521292e591a7cfa703215ec1348bf # v0.2.1 + if: github.event.action == 'labeled' && github.event.label.name == 'hcc/jira' && !steps.search.outputs.issue + uses: ./.github/actions/jira/create with: project: NMD - issuetype: "${{ steps.set-ticket-type.outputs.TYPE }}" - summary: "${{ github.event.repository.name }} [${{ steps.set-ticket-type.outputs.TYPE }} #${{ github.event.issue.number }}]: ${{ github.event.issue.title }}" + issuetype: "GH Issue" + summary: "${{ github.event.repository.name }} [GH Issue #${{ github.event.issue.number }}]: ${{ github.event.issue.title }}" description: "${{ github.event.issue.body || github.event.pull_request.body }}\n\n_Created in GitHub by ${{ github.actor }}._" # customfield_10089 is "Issue Link" # customfield_10371 is "Source" (use JIRA API to retrieve) @@ -41,38 +42,25 @@ jobs: # customfield_10001 is Team (jira default teams?) extraFields: '{ "customfield_10089": "${{ github.event.issue.html_url || github.event.pull_request.html_url }}", "customfield_10001": "72e166fb-d26c-4a61-b0de-7a290d91708f", + "customfield_10371": { "value": "GitHub" }, + "customfield_10091": ["NomadMinor"], "components": [{ "name": "nomad" }], "labels": ["community", "GitHub"] }' - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - - name: Search - if: github.event.action != 'opened' - id: search - uses: tomhjp/gh-action-jira-search@04700b457f317c3e341ce90da5a3ff4ce058f2fa # v0.2.2 - with: - # cf[10089] is Issue Link (use JIRA API to retrieve) - jql: 'cf[10089] = "${{ github.event.issue.html_url || github.event.pull_request.html_url }}"' - - name: Sync comment if: github.event.action == 'created' && steps.search.outputs.issue - uses: tomhjp/gh-action-jira-comment@6eb6b9ead70221916b6badd118c24535ed220bd9 # v0.2.0 + uses: ./.github/actions/jira/sync-comment with: issue: ${{ steps.search.outputs.issue }} comment: "${{ github.actor }} ${{ github.event.review.state || 'commented' }}:\n\n${{ github.event.comment.body || github.event.review.body }}\n\n${{ github.event.comment.html_url || github.event.review.html_url }}" - - name: Close ticket if: ( github.event.action == 'closed' || github.event.action == 'deleted' ) && steps.search.outputs.issue - uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 + uses: ./.github/actions/jira/transition with: issue: ${{ steps.search.outputs.issue }} transition: "Closed" - - name: Reopen ticket if: github.event.action == 'reopened' && steps.search.outputs.issue - uses: atlassian/gajira-transition@38fc9cd61b03d6a53dd35fcccda172fe04b36de3 # v3.0.1 + uses: ./.github/actions/jira/transition with: issue: ${{ steps.search.outputs.issue }} transition: "To Do"