Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/automated-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ jobs:
parent-epic: ${{ steps.resolve-ktlo-epic.outputs.epic-key }}
edition: ${{ inputs.sqs-ticket-edition }}
team: ${{ inputs.sqs-sqc-ticket-team }}
secret-name: ${{ inputs.release-automation-secret-name || format('sonar-{0}-release-automation', inputs.plugin-name) }}

- name: Summary
if: ${{ inputs.verbose }}
Expand Down
38 changes: 33 additions & 5 deletions .github/workflows/test-create-integration-ticket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ jobs:
--use-sandbox "true" \
--ticket-key "$TICKET_KEY" \
--team "f1da89c9-3712-4d15-b194-a4b24406e3e4" \
--edition NONE
--edition NONE \
--fix-versions NONE

# Sandbox only proves fixVersions is stored; which version wins is unit-tested.
- name: Create ticket with edition and team
id: with-edition
uses: ./create-integration-ticket
Expand All @@ -104,7 +106,7 @@ jobs:
team: 'f1da89c9-3712-4d15-b194-a4b24406e3e4'
use-jira-sandbox: 'true'

- name: Verify edition and team were set in step before
- name: Verify edition, team and fix versions were set in step before
env:
JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
Expand All @@ -115,7 +117,32 @@ jobs:
--use-sandbox "true" \
--ticket-key "$TICKET_KEY" \
--team "f1da89c9-3712-4d15-b194-a4b24406e3e4" \
--edition "Community Build & Server"
--edition "Community Build & Server" \
--fix-versions ANY

- name: Create ticket with N/A edition
id: with-na-edition
uses: ./create-integration-ticket
with:
target-jira-project: SONAR
release-ticket-key: 'SONAR-22193'
ticket-summary: 'Integration ticket test ${{ github.run_id }} — N/A edition'
edition: 'N/A'
use-jira-sandbox: 'true'

- name: Verify N/A edition sets no fix versions
env:
JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
TICKET_KEY: ${{ steps.with-na-edition.outputs.ticket-key }}
run: |
set -euo pipefail
python test-fixtures/jira/assert_ticket_fields.py \
--use-sandbox "true" \
--ticket-key "$TICKET_KEY" \
--team NONE \
--edition "N/A" \
--fix-versions NONE

- name: Create ticket without edition or team
id: without-fields
Expand All @@ -139,7 +166,8 @@ jobs:
--use-sandbox "true" \
--ticket-key "$TICKET_KEY" \
--team NONE \
--edition NONE
--edition NONE \
--fix-versions NONE

# Deleting the created tickets also removes the issue links they added to SONAR-22193.
- name: Clean up created tickets
Expand All @@ -148,7 +176,7 @@ jobs:
JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
CREATED_KEYS: >-
${{ steps.with-team.outputs.ticket-key }},${{ steps.with-edition.outputs.ticket-key }},${{ steps.without-fields.outputs.ticket-key }}
${{ steps.with-team.outputs.ticket-key }},${{ steps.with-edition.outputs.ticket-key }},${{ steps.with-na-edition.outputs.ticket-key }},${{ steps.without-fields.outputs.ticket-key }}
run: |
python test-fixtures/jira/cleanup.py \
--use-sandbox "true" \
Expand Down
11 changes: 10 additions & 1 deletion .okf/actions/create-integration-ticket.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ dropped. Availability differs per project (`SONAR`: both; `SC`: team only), whic
Covered by a real Jira sandbox job that re-reads the created tickets and asserts the stored
values — the only check that catches a wrong custom field ID or value shape. It runs against
fixed sandbox state (`SONAR-22193`, a fixed team UUID) instead of a setup script, like
[get-jira-release-notes](/actions/get-jira-release-notes.md).
[get-jira-release-notes](/actions/get-jira-release-notes.md). `fixVersions` is only asserted as
set, since which version wins is unit-tested.

# Automatic Fix versions

When `edition` is set, `fixVersions` gets the lowest open `major.minor` version per prefix:
`sqcb-` (Community Build), `sqs-` (Server) or both; `N/A` sets nothing. Released, archived and
`sonar-enterprise`-tagged versions are skipped. The tag lookup uses a token resolved like
[create-pull-request](/actions/create-pull-request.md) (vault `SonarSource-<secret-name>` if the `secret-name` input is set, else `-release-automation`, else the `token`
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Resolved
input; no token only warns). Any failure falls back to Jira-only or no fix version, never blocking.

# Citations

Expand Down
12 changes: 12 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Update Log

## 2026-10-06
* **Fix versions skip sonar-enterprise tags**: `create-integration-ticket` skips versions already
tagged in `sonar-enterprise`, via a vault or new `token` input (see
[actions/create-integration-ticket.md](/actions/create-integration-ticket.md)).
* **Configurable vault secret for integration tickets**: `create-integration-ticket` takes a
`secret-name` input, passed from `automated-release.yml` like the update-analyzer steps.

## 2026-10-05
* **Automatic Fix versions for integration tickets**: With `edition` set, `create-integration-ticket`
fills `fixVersions` with the lowest open `sqcb-*`/`sqs-*` version (see
[actions/create-integration-ticket.md](/actions/create-integration-ticket.md)).

## 2026-09-17
* **Repo-specific repox status preferred**: [get-release-version](/actions/get-release-version.md)
now reads the exact `repox-<repo-name>-<branch>` context first, falling back to the generic
Expand Down
20 changes: 20 additions & 0 deletions create-integration-ticket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This action requires:
| `parent-epic` | Jira issue key to set as parent of the created ticket (e.g. CPP-7858) | No | - |
| `edition` | Jira "Edition" value. One of: `N/A`, `Community Build`, `Server`, `Community Build & Server` | No | - |
| `team` | Atlassian team **UUID** for the Jira "Team" field | No | - |
| `secret-name` | Release automation vault secret name (read as `SonarSource-<secret-name>`) with access to `sonar-enterprise`; defaults to `{REPO_OWNER_NAME_DASH}-release-automation` | No | - |
| `token` | GitHub token for reading `sonar-enterprise` tags; fallback if the vault token is unavailable | No | `${{ github.token }}` |

**Note:** Either `ticket-summary` must be provided, or both `plugin-name` and `release-version` must be provided. If `ticket-summary` is not provided, it will be automatically generated as "Update {plugin-name} to {release-version}".

Expand All @@ -46,6 +48,24 @@ request and the action fails. `edition` is available on `SONAR`, not on `SC`; `t
`team` takes the team UUID, not the name (find it via `customfield_10001.id` on an existing
ticket's `/rest/api/2/issue/<KEY>`). UUIDs differ between production and sandbox.

### Automatic 'Fix versions'

When `edition` is set, `Fix versions` is filled with the lowest open version per prefix:

| `edition` | Fix versions set |
|-------------------------------|-------------------------------------|
| `N/A` | none |
| `Community Build` | next unreleased `sqcb-*` |
| `Server` | next unreleased `sqs-*` |
| `Community Build & Server` | next unreleased `sqcb-*` and `sqs-*`|

Released, archived and `sonar-enterprise`-tagged versions (tag `sqs-2026.5.2.1` ⇒ `sqs-2026.5`) are
skipped. Failures never block ticket creation.

The tag lookup uses the vault `SonarSource-<secret-name>` token if `secret-name` is set, else
`{REPO_OWNER_NAME_DASH}-release-automation`, falling back to `token`, as in
[`create-pull-request`](../create-pull-request). Without access to `sonar-enterprise`, only Jira is consulted.

## Outputs

| Output | Description |
Expand Down
37 changes: 37 additions & 0 deletions create-integration-ticket/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ inputs:
team:
description: 'Optional team UUID for the Jira "Team" field.'
required: false
secret-name:
description: 'Release automation vault secret name with read access to sonar-enterprise (e.g. sonar-foo-release-automation). Defaults to {REPO_OWNER_NAME_DASH}-release-automation.'
required: false
token:
description: 'GitHub token for reading sonar-enterprise tags; fallback if the vault token is unavailable.'
required: false
default: ${{ github.token }}

outputs:
ticket-key:
Expand All @@ -54,6 +61,35 @@ runs:
development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN;

- name: Get GitHub token from Vault
id: github_secrets
if: ${{ inputs.edition != '' }}
continue-on-error: true
uses: SonarSource/vault-action-wrapper@320bd31b03e5dacaac6be51bbbb15adf7caccc32 # v3.1
with:
secrets: |
development/github/token/${{ inputs.secret-name && format('SonarSource-{0}', inputs.secret-name) || '{REPO_OWNER_NAME_DASH}-release-automation' }} token | VAULT_GITHUB_TOKEN;

- name: Resolve GitHub token
id: resolve_github_token
if: ${{ inputs.edition != '' }}
shell: bash
env:
VAULT_OUTCOME: ${{ steps.github_secrets.outcome }}
VAULT_TOKEN: ${{ steps.github_secrets.outcome == 'success' && fromJSON(steps.github_secrets.outputs.vault).VAULT_GITHUB_TOKEN || '' }}
INPUT_TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail
if [[ "$VAULT_OUTCOME" == "success" && -n "$VAULT_TOKEN" ]]; then
echo "Using vault token"
echo "token=${VAULT_TOKEN}" >> "$GITHUB_OUTPUT"
elif [[ -n "$INPUT_TOKEN" ]]; then
echo "Vault token not available, using input token"
echo "token=${INPUT_TOKEN}" >> "$GITHUB_OUTPUT"
else
echo "::warning::No GitHub token available, Fix versions will not be cross-checked against sonar-enterprise tags."
fi

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
Expand Down Expand Up @@ -94,6 +130,7 @@ runs:
env:
JIRA_USER: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
JIRA_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}
GITHUB_TOKEN: ${{ steps.resolve_github_token.outputs.token }}
USE_SANDBOX: ${{ inputs.use-jira-sandbox || env.USE_JIRA_SANDBOX }}
TICKET_DESCRIPTION: |
${{ inputs.ticket-description }}${{ inputs.jira-release-url && inputs.ticket-description && '
Expand Down
114 changes: 114 additions & 0 deletions create-integration-ticket/create_integration_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,122 @@

import argparse
import os
import re
import sys
import time
import requests

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'shared'))
from jira_common import eprint, get_jira_instance, CUSTOM_FIELDS
from jira.exceptions import JIRAError

# Fix-version name prefixes per Edition; editions not listed (e.g. 'N/A') get none.
EDITION_VERSION_PREFIXES = {
'Community Build': ('sqcb-',),
'Server': ('sqs-',),
'Community Build & Server': ('sqcb-', 'sqs-'),
}

SONAR_ENTERPRISE_TAGS_URL = 'https://api.github.com/repos/SonarSource/sonar-enterprise/git/matching-refs/tags/'

# Jira versions are 'major.minor' only (no bugfix or '-M1' suffix).
VERSION_NAME_PATTERN = re.compile(r'^(\d+)\.(\d+)$')


def version_sort_key(name):
"""Numeric sort key for 'major.minor', so '26.9' < '26.10'."""
major, minor = VERSION_NAME_PATTERN.match(name).groups()
return int(major), int(minor)


def find_lowest_version(versions, prefix):
"""Lowest 'prefix' + 'major.minor' version name, or None."""
candidates = [
v.name for v in versions
if v.name.startswith(prefix) and VERSION_NAME_PATTERN.match(v.name[len(prefix):])
]
if not candidates:
eprint(f"No open '{prefix}*' version found.")
return None
candidates.sort(key=lambda name: version_sort_key(name[len(prefix):]))
eprint(f"Found '{prefix}*' versions {candidates}, using '{candidates[0]}'.")
return candidates[0]


def list_tag_refs(github_token, prefix):
"""sonar-enterprise tag refs starting with prefix; raises on HTTP/JSON errors."""
refs = []
url, params = SONAR_ENTERPRISE_TAGS_URL + prefix, {'per_page': 100}
while url:
response = requests.get(
url,
headers={'Authorization': f'Bearer {github_token}', 'Accept': 'application/vnd.github+json'},
params=params,
timeout=30,
)
response.raise_for_status()
refs.extend(item['ref'] for item in response.json())
url, params = response.links.get('next', {}).get('url'), None
return refs


def parse_shipped_versions(refs, prefix):
"""Maps tag refs to Jira names, e.g. 'refs/tags/sqs-2026.5.2.1' -> 'sqs-2026.5'."""
tag_pattern = re.compile(rf'^refs/tags/{re.escape(prefix)}(\d+)\.(\d+)\.')
matches = (tag_pattern.match(ref) for ref in refs)
return {f'{prefix}{m.group(1)}.{m.group(2)}' for m in matches if m}


def fetch_shipped_versions(github_token, prefix):
"""Jira version names already tagged in sonar-enterprise, or None on failure."""
try:
return parse_shipped_versions(list_tag_refs(github_token, prefix), prefix)
except (requests.RequestException, ValueError) as e:
eprint(f"Warning: Failed to list sonar-enterprise '{prefix}*' tags: {e}")
return None


def fetch_open_versions(jira_client, project_key):
"""Unreleased, non-archived project versions, or None on failure."""
try:
versions = jira_client.project_versions(project_key)
except JIRAError as e:
eprint(f"Warning: Failed to fetch versions for project '{project_key}'. Status: {e.status_code}")
eprint("Warning: Skipping automatic 'Fix versions' assignment.")
return None
return [
v for v in versions
if not getattr(v, 'released', False) and not getattr(v, 'archived', False)
]


def exclude_shipped_versions(versions, github_token, prefix):
"""Drops versions already tagged in sonar-enterprise; unchanged if the lookup fails."""
shipped = fetch_shipped_versions(github_token, prefix)
if not shipped:
return versions
eprint(f"Skipping '{prefix}*' versions already tagged in sonar-enterprise: {sorted(shipped)}")
return [v for v in versions if v.name not in shipped]


def resolve_fix_versions(jira_client, project_key, edition, github_token):
"""Fix version names for the edition, skipping tagged ones; [] on failure, never blocks."""
prefixes = EDITION_VERSION_PREFIXES.get(edition)
if not prefixes:
return []

open_versions = fetch_open_versions(jira_client, project_key)
if open_versions is None:
return []

if not github_token:
eprint("Warning: No GITHUB_TOKEN, not cross-referencing versions with sonar-enterprise tags.")

fix_versions = []
for prefix in prefixes:
candidates = exclude_shipped_versions(open_versions, github_token, prefix) if github_token else open_versions
Comment thread
jonas-wielage-sonarsource marked this conversation as resolved.
Resolved
fix_versions.append(find_lowest_version(candidates, prefix))
return [name for name in fix_versions if name]


def validate_release_ticket(jira_client, release_ticket_key):
Expand All @@ -38,7 +147,7 @@
sys.exit(1)


def create_integration_ticket(jira_client, args):

Check failure on line 150 in create-integration-ticket/create_integration_ticket.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=SonarSource_release-github-actions&issues=AaEQozZNZJ-eSz3L6dgH&open=AaEQozZNZJ-eSz3L6dgH&pullRequest=214
"""
Creates the integration ticket in Jira.
"""
Expand Down Expand Up @@ -82,6 +191,11 @@

if getattr(args, 'edition', None):
ticket_details[CUSTOM_FIELDS['EDITION']] = {'value': args.edition}
fix_versions = resolve_fix_versions(
jira_client, args.target_jira_project, args.edition, os.environ.get('GITHUB_TOKEN')
)
if fix_versions:
ticket_details['fixVersions'] = [{'name': name} for name in fix_versions]

if getattr(args, 'team', None):
ticket_details[CUSTOM_FIELDS['TEAM']] = args.team
Expand Down
2 changes: 1 addition & 1 deletion create-integration-ticket/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
jira==3.10.5

requests==2.34.2
Loading
Loading