Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [Release / Tag](release/tag/README.md)
* [Roxie / Install CLI](roxie/install-cli/README.md)
* [Test](test/README.md)
* [Test / junit2jira](test/junit2jira/README.md)

## Workflows

Expand Down
155 changes: 155 additions & 0 deletions test/junit2jira/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Convert JUnit test failures into Jira tickets

Scans a directory of JUnit XML reports and creates (or deduplicates) Jira issues
for test failures using [`junit2jira`](https://github.com/stackrox/junit2jira).
Optionally uploads a CSV of test metrics to GCS for BigQuery ingestion.

If the GitHub job failed but produced no test-level `<failure>` records, the
action synthesises a JUnit failure so infrastructure/setup failures are still
reported.

The action is self-contained: it bundles its own helper scripts and does not
require the calling repository to provide any `scripts/ci` helpers.

## Recommended permissions

The action doesn't require any specific permission.

```yaml
permissions: {}
```

## All options

| Input | Description | Default |
| ------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------ |
| [dry-run](#dry-run) | When true, runs junit2jira with `--dry-run` and does not create Jira issues | `false` |
| [jira-user](#jira-user) | User used to authenticate with Jira | |
| [jira-token](#jira-token) | Token used to authenticate with Jira | |
| [jira-url](#jira-url) | Base URL of the Jira instance | `https://redhat.atlassian.net/`|
| [directory](#directory) | Directory containing the JUnit XML files to scan | |
| [threshold](#threshold) | Minimal number of failures that results in a single cumulative Jira issue | `5` |
| [gcp-account](#gcp-account) | Optional GCP service account JSON. When set, the action authenticates gcloud itself | unset |
| [gcp-project](#gcp-project) | GCP project to set as active when authenticating with gcp-account | `acs-san-stackroxci` |
| [gcp-metrics](#gcp-metrics) | Whether to upload test metrics to GCS for BigQuery | `true` |
| [gcs-bucket](#gcs-bucket) | GCS bucket root used to store test metrics | `gs://stackrox-ci-artifacts` |
| [gcs-subdir](#gcs-subdir) | Subdirectory (relative to the bucket root) used to store test metrics | `test-metrics/upload` |
| [version](#version) | `junit2jira` release version to download | `v0.0.27` |

## Outputs

| Output | Description |
| ----------- | -------------------------------------------------- |
| `new-jiras` | `"true"`/`"false"` — whether new issues were created |

### Detailed options

#### dry-run

When `true`, `junit2jira` runs with `--dry-run` and no issues are created.
Commonly wired to only create issues on pushes:
`${{ github.event_name != 'push' }}`.

Default value: `false`

#### jira-user

User used to authenticate with Jira. Pass via a secret, e.g.
`${{ secrets.JIRA_USER }}`.

#### jira-token

Token used to authenticate with Jira. Pass via a secret, e.g.
`${{ secrets.JIRA_TOKEN }}`. If empty, the reporting step is skipped so the
action no-ops gracefully on forks/PRs without secrets.

#### jira-url

Base URL of the Jira instance.

Default value: `https://redhat.atlassian.net/`

#### directory

Directory containing the JUnit XML files to scan. `junit2jira` scans it
recursively for `*.xml` files.

#### threshold

Minimal number of failed tests that results in a single cumulative Jira issue
instead of one issue per failure.

Default value: `5`

#### gcp-account

Optional GCP service account JSON. When provided, the action authenticates with
gcloud itself (via `google-github-actions/auth`). When omitted, the action
assumes the caller has already authenticated gcloud.

Default value: unset

#### gcp-project

GCP project to set as active when authenticating with `gcp-account`.

Default value: `acs-san-stackroxci`

#### gcp-metrics

Whether to upload the test metrics CSV to GCS for BigQuery ingestion. Requires
an authenticated gcloud session (see `gcp-account`).

Default value: `true`

#### gcs-bucket

GCS bucket root used to store test metrics.

Default value: `gs://stackrox-ci-artifacts`

#### gcs-subdir

Subdirectory (relative to the bucket root) used to store test metrics.

Default value: `test-metrics/upload`

#### version

`junit2jira` release version to download.

Default value: `v0.0.27`

## Usage

The action assumes gcloud is already authenticated (e.g. via
`google-github-actions/auth`) unless `gcp-account` is provided.

```yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
# ... run tests, producing JUnit XML under junit-reports/ ...

- name: Report test failures to Jira
if: (!cancelled())
id: junit2jira
uses: stackrox/actions/test/junit2jira@main
with:
dry-run: ${{ github.event_name != 'push' }}
jira-user: ${{ secrets.JIRA_USER }}
jira-token: ${{ secrets.JIRA_TOKEN }}
directory: junit-reports
```

To have the action authenticate to GCP itself, pass a service account:

```yaml
- uses: stackrox/actions/test/junit2jira@main
with:
jira-user: ${{ secrets.JIRA_USER }}
jira-token: ${{ secrets.JIRA_TOKEN }}
directory: junit-reports
gcp-account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
```
169 changes: 169 additions & 0 deletions test/junit2jira/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
name: junit2jira
description: Convert JUnit test failures into Jira tickets and upload test metrics

inputs:
dry-run:
description: When true, junit2jira runs with --dry-run and does not create Jira issues.
required: false
default: "false"
jira-user:
description: User used to authenticate with Jira.
required: true
jira-token:
description: Token used to authenticate with Jira.
required: true
jira-url:
description: Base URL of the Jira instance.
required: false
default: https://redhat.atlassian.net/
directory:
description: Directory containing the JUnit XML files to scan.
required: true
threshold:
description: Minimal number of failed tests that will result in a single cumulative Jira issue.
required: false
default: "5"
gcp-account:
description: |
Optional GCP service account JSON. When provided, the action authenticates
with gcloud itself. When omitted, the action assumes the caller has already
authenticated gcloud (e.g. via google-github-actions/auth).
required: false
default: ""
gcp-project:
description: GCP project to set as active when authenticating with gcp-account.
required: false
default: acs-san-stackroxci
gcp-metrics:
description: Whether to upload test metrics to GCS for BigQuery ingestion.
required: false
default: "true"
gcs-bucket:
description: GCS bucket root used to store test metrics.
required: false
default: gs://stackrox-ci-artifacts
gcs-subdir:
description: Subdirectory (relative to the bucket root) used to store test metrics.
required: false
default: test-metrics/upload
version:
description: junit2jira release version to download.
required: false
default: v0.0.27

outputs:
new-jiras:
description: |
Bool indicating if new Jira issues were created. Explicitly "false" when
jira-token is empty and Jira reporting was skipped.
value: ${{ steps.run.outputs.NEW_JIRAS || 'false' }}

runs:
using: composite
steps:
- name: Download junit2jira
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
BIN_DIR="${RUNNER_TEMP}/junit2jira/${VERSION}"
BIN_PATH="${BIN_DIR}/junit2jira"
# Skip downloading release if downloaded already, e.g. when the action is used multiple times.
if [[ ! -x "$BIN_PATH" ]]; then
mkdir -p "$BIN_DIR"
curl --retry 5 --retry-connrefused --silent --show-error --fail --location \
--output "$BIN_PATH" "https://github.com/stackrox/junit2jira/releases/download/${VERSION}/junit2jira"
chmod +x "$BIN_PATH"
fi
echo "JUNIT2JIRA_BIN=${BIN_PATH}" >> "$GITHUB_ENV"

- name: Capture job failure as JUnit if no test failures exist
shell: bash
if: always()
env:
STEPS_JSON: ${{ toJSON(steps) }}
ARTIFACT_DIR: ${{ inputs.directory }}
JOB_NAME: ${{ github.job }}
JOB_STATUS: ${{ job.status }}
WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
"${GITHUB_ACTION_PATH}/../../common/common.sh" \
"${GITHUB_ACTION_PATH}/junit2jira.sh" \
capture_job_failure_as_junit \
"$ARTIFACT_DIR" \
"$JOB_NAME" \
"$JOB_STATUS" \
"$STEPS_JSON" \
"$WORKFLOW_RUN_URL"

- name: Report failures to Jira
id: run
shell: bash
env:
JIRA_USER: ${{ inputs.jira-user }}
JIRA_TOKEN: ${{ inputs.jira-token }}
JIRA_URL: ${{ inputs.jira-url }}
DIRECTORY: ${{ inputs.directory }}
THRESHOLD: ${{ inputs.threshold }}
DRY_RUN: ${{ inputs.dry-run }}
BASE_LINK: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
BUILD_ID: ${{ github.run_id }}
BUILD_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
BUILD_TAG: ${{ github.ref_name }}@${{ github.sha }}
JOB_NAME: ${{ github.job }}
ORCHESTRATOR: ${{ runner.name }} ${{ runner.os }}-${{ runner.arch }}
if: ${{ env.JIRA_TOKEN != '' }}
run: |
set -euo pipefail
extra_args=()
if [[ "$DRY_RUN" == "true" ]]; then
extra_args=(--dry-run)
else
echo "Will create Jira issues for JUnit failures found in ${DIRECTORY}"
fi
csv_output="$(mktemp --suffix=.csv)"
summary_file="$(mktemp --suffix=.json)"
"$JUNIT2JIRA_BIN" \
-base-link "$BASE_LINK" \
-build-id "$BUILD_ID" \
-build-link "$BUILD_LINK" \
-build-tag "$BUILD_TAG" \
-csv-output "${csv_output}" \
-jira-url "$JIRA_URL" \
-job-name "$JOB_NAME" \
-junit-reports-dir "$DIRECTORY" \
-orchestrator "$ORCHESTRATOR" \
-threshold "$THRESHOLD" \
-summary-output "${summary_file}" \
"${extra_args[@]}"

echo "NEW_JIRAS=$(jq -r '.newJIRAs > 0' "${summary_file}")" >> "$GITHUB_OUTPUT"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "CSV_OUTPUT=${csv_output}" >> "$GITHUB_ENV"

- name: Authenticate with GCP
if: inputs.gcp-metrics == 'true' && inputs.gcp-account != ''
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ inputs.gcp-account }}
project_id: ${{ inputs.gcp-project }}

- name: Set up Cloud SDK
if: inputs.gcp-metrics == 'true' && inputs.gcp-account != ''
uses: google-github-actions/setup-gcloud@v2

- name: Upload test metrics to GCS
if: inputs.gcp-metrics == 'true' && steps.run.outcome == 'success'
shell: bash
env:
GCS_BUCKET: ${{ inputs.gcs-bucket }}
GCS_SUBDIR: ${{ inputs.gcs-subdir }}
run: |
set -euo pipefail
"${GITHUB_ACTION_PATH}/../../common/common.sh" \
"${GITHUB_ACTION_PATH}/junit2jira.sh" \
save_test_metrics \
"$CSV_OUTPUT" \
"$GCS_BUCKET" \
"$GCS_SUBDIR"
Loading
Loading