Update Jira releases from GitHub releases with GitHub Actions
~ 6 min read
Why do this?
If your team ships from GitHub but tracks release versions in Jira, it is easy for the two to drift apart.
You publish v1.8.0 on GitHub, but the matching Jira release still sits in an unreleased state.
This setup makes GitHub Releases the source of truth. You create a tagged release in GitHub, and a second workflow marks the matching Jira version as released through the Jira Cloud REST API.
The example below assumes this naming convention:
- GitHub tag:
v1.8.0 - Jira version:
1.8.0
If you use the same value in both systems, remove the v prefix normalisation step.
What you need first
Before adding the workflows, make sure you have:
- A GitHub repository with Actions enabled.
- A Jira Cloud project using Releases / Versions.
- An Atlassian API token for the user that is allowed to manage project versions.
- Repository secrets for the Jira connection.
Add these GitHub Actions secrets to your repository:
GH_RELEASE_TOKEN: a token that can create releases in this repository.JIRA_BASE_URL: for examplehttps://your-domain.atlassian.netJIRA_EMAIL: the Atlassian account email used for the API callJIRA_API_TOKEN: the Atlassian API tokenJIRA_PROJECT_KEY: for exampleAPPJIRA_PROJECT_ID: the numeric Jira project ID used when creating a version if it does not exist yet
Use a dedicated GH_RELEASE_TOKEN rather than the built-in GITHUB_TOKEN for release creation.
GitHub documents that events created by GITHUB_TOKEN do not start new workflow runs, which matters here because we want a second workflow to run on release.published.
Useful docs:
- Events that trigger workflows
- Automatic token authentication
- gh release create
- Jira Cloud project versions REST API

Workflow 1: create the GitHub tag and release
Create .github/workflows/create-release.yml:
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version, for example 1.8.0"
required: true
type: string
prerelease:
description: "Mark as a prerelease"
required: false
default: false
type: boolean
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Normalise the tag
id: vars
shell: bash
run: |
set -euo pipefail
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
- name: Create the GitHub release
env:
GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
TAG: ${{ steps.vars.outputs.tag }}
PRERELEASE: ${{ inputs.prerelease }}
shell: bash
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists" >&2
exit 1
fi
args=(
"$TAG"
"--target" "$GITHUB_SHA"
"--title" "$TAG"
"--generate-notes"
)
if [ "$PRERELEASE" = "true" ]; then
args+=("--prerelease")
fi
gh release create "${args[@]}"
This workflow is triggered manually because it uses workflow_dispatch.
That means GitHub shows a Run workflow button in the Actions tab for this workflow, and the version input appears as a form field when you launch it.
How to trigger the release workflow
After committing .github/workflows/create-release.yml to your default branch:
- Open your repository on GitHub.
- Go to Actions.
- Select Create Release in the left-hand workflow list.
- Click Run workflow.
- Enter a value for
version, for example1.8.0. - Optionally set
prereleasetotrue. - Click Run workflow to start the job.
GitHub passes that form value into ${{ inputs.version }} inside the workflow.
That is what this line is reading:
VERSION="${{ inputs.version }}"
If you prefer triggering it from the command line, you can do the same thing with the GitHub CLI:
gh workflow run create-release.yml -f version=1.8.0 -f prerelease=false
If the workflow does not appear in the Actions tab yet, make sure the workflow file is already on the repository default branch.
What this does:
- Accepts a version like
1.8.0. - Normalises it to
v1.8.0. - Creates the GitHub release and the missing Git tag in one step.
- Publishes generated release notes.
The tag is created from the commit the workflow is running against.
If you want stricter control, change --target "$GITHUB_SHA" to a specific commit SHA or branch tip that matches your release process.

Workflow 2: mark the Jira version as released
Create .github/workflows/sync-jira-release.yml:
name: Sync Jira Release
on:
release:
types:
- published
jobs:
sync-jira-release:
runs-on: ubuntu-latest
steps:
- name: Update the Jira version
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT_KEY }}
JIRA_PROJECT_ID: ${{ secrets.JIRA_PROJECT_ID }}
TAG_NAME: ${{ github.event.release.tag_name }}
RELEASED_AT: ${{ github.event.release.published_at }}
shell: bash
run: |
set -euo pipefail
VERSION_NAME="${TAG_NAME#v}"
RELEASE_DATE="${RELEASED_AT%%T*}"
versions_json="$(
curl --fail-with-body --silent --show-error \
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
--header "Accept: application/json" \
"$JIRA_BASE_URL/rest/api/3/project/$JIRA_PROJECT_KEY/versions"
)"
version_json="$(
printf '%s' "$versions_json" | jq -cer --arg name "$VERSION_NAME" '
map(select(.name == $name)) | first
' || true
)"
if [ -z "$version_json" ] || [ "$version_json" = "null" ]; then
create_payload="$(
jq -n \
--arg name "$VERSION_NAME" \
--arg releaseDate "$RELEASE_DATE" \
--argjson projectId "$JIRA_PROJECT_ID" \
'{
name: $name,
projectId: $projectId,
released: true,
releaseDate: $releaseDate
}'
)"
curl --fail-with-body --silent --show-error \
--request POST \
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "$create_payload" \
"$JIRA_BASE_URL/rest/api/3/version"
echo "Created and released Jira version $VERSION_NAME"
exit 0
fi
version_id="$(printf '%s' "$version_json" | jq -r '.id')"
update_payload="$(
printf '%s' "$version_json" | jq -c --arg releaseDate "$RELEASE_DATE" '
{
name,
projectId,
released: true,
releaseDate: $releaseDate
}
+ (if has("description") and .description != null then {description} else {} end)
+ (if has("archived") and .archived != null then {archived} else {} end)
'
)"
curl --fail-with-body --silent --show-error \
--request PUT \
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "$update_payload" \
"$JIRA_BASE_URL/rest/api/3/version/$version_id"
echo "Marked Jira version $VERSION_NAME as released"
This workflow listens for the published GitHub release event, looks up the Jira version by name, and then:
- updates it if it already exists
- creates it first if it does not
The release date is taken from the GitHub release timestamp, not from the runner clock. That keeps the dates consistent if the job queues for a few minutes before starting.
How the mapping works
The key line is this one:
VERSION_NAME="${TAG_NAME#v}"
That converts v1.8.0 into 1.8.0.
If your Jira versions also include the v, remove that line and use TAG_NAME directly.
Keep the mapping boring and predictable. If you start translating branch names, app names, or environment names inside the workflow, the release process gets harder to debug.
Verify the flow end to end
Run the manual workflow once with a test version such as 1.8.0.
Then check these points:
- A GitHub release called
v1.8.0exists. - The repository now has a
v1.8.0tag. - The Jira project has a version
1.8.0. - That Jira version is marked as released with the expected release date.
If the GitHub release appears but Jira does not update, check the sync-jira-release workflow run first.
Most failures come down to one of three things:
GH_RELEASE_TOKENwas replaced withGITHUB_TOKEN, so the second workflow never triggeredJIRA_PROJECT_KEYorJIRA_PROJECT_IDpoints at the wrong project- the Jira user behind the API token does not have permission to manage versions
A couple of useful variations
You can adjust this pattern without changing the basic flow:
- If you already create tags somewhere else, delete the first workflow and keep only the
release.publishedsync workflow. - If you want release notes from a file instead of generated notes, replace
--generate-noteswith--notes-file CHANGELOG.md. - If you never want Jira versions auto-created, remove the POST branch and fail when the version cannot be found.
Summary
The clean way to keep Jira releases aligned with GitHub releases is to treat the GitHub release as the event that closes the loop. Create the tag and release in one workflow, then let a second workflow update the Jira version through the official Jira API.
Once this is in place, release status stops being a manual admin task and becomes part of the release itself.