mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
ci: fix github to jira issue sync (#26747)
Add local actions for JIRA interactions to replace github actions that have been archived.
This commit is contained in:
43
.github/actions/jira/create/action.yml
vendored
Normal file
43
.github/actions/jira/create/action.yml
vendored
Normal file
@@ -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 }}
|
||||
75
.github/actions/jira/create/jira-create.bash
vendored
Executable file
75
.github/actions/jira/create/jira-create.bash
vendored
Executable file
@@ -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}"
|
||||
22
.github/actions/jira/search/action.yml
vendored
Normal file
22
.github/actions/jira/search/action.yml
vendored
Normal file
@@ -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 }}
|
||||
28
.github/actions/jira/search/jira-search.bash
vendored
Executable file
28
.github/actions/jira/search/jira-search.bash
vendored
Executable file
@@ -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}"
|
||||
67
.github/actions/jira/shared.bash
vendored
Normal file
67
.github/actions/jira/shared.bash
vendored
Normal file
@@ -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
|
||||
22
.github/actions/jira/sync-comment/action.yml
vendored
Normal file
22
.github/actions/jira/sync-comment/action.yml
vendored
Normal file
@@ -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 }}
|
||||
38
.github/actions/jira/sync-comment/jira-sync-comment.bash
vendored
Executable file
38
.github/actions/jira/sync-comment/jira-sync-comment.bash
vendored
Executable file
@@ -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}"
|
||||
22
.github/actions/jira/transition/action.yml
vendored
Normal file
22
.github/actions/jira/transition/action.yml
vendored
Normal file
@@ -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 }}
|
||||
44
.github/actions/jira/transition/jira-transition.bash
vendored
Executable file
44
.github/actions/jira/transition/jira-transition.bash
vendored
Executable file
@@ -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}"
|
||||
58
.github/workflows/jira-sync.yml
vendored
58
.github/workflows/jira-sync.yml
vendored
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user