-
Notifications
You must be signed in to change notification settings - Fork 0
feat(test): add standalone junit2jira action #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # 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 | | ||
| | ------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------ | | ||
| | [create-jiras](#create-jiras) | Whether to actually create Jira issues (otherwise runs `--dry-run`) | `true` | | ||
| | [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-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 | ||
|
|
||
| #### create-jiras | ||
|
|
||
| Whether to actually create Jira issues. When `false`, `junit2jira` runs with | ||
| `--dry-run` and no issues are created. Commonly wired to only create issues on | ||
| pushes: `${{ github.event_name == 'push' }}`. | ||
|
|
||
| Default value: `true` | ||
|
|
||
| #### 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-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: | ||
| create-jiras: ${{ 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 }} | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| name: junit2jira | ||
| description: Convert JUnit test failures into Jira tickets and upload test metrics | ||
|
|
||
| inputs: | ||
| create-jiras: | ||
| description: Whether to actually create Jira issues. When false, junit2jira runs with --dry-run. | ||
| required: false | ||
| default: "true" | ||
| 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-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. | ||
| value: ${{ steps.run.outputs.NEW_JIRAS }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Download junit2jira | ||
| shell: bash | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| set -u | ||
| LOCATION="https://github.com/stackrox/junit2jira/releases/download/$VERSION/junit2jira" | ||
| # Skip downloading release if downloaded already, e.g. when the action is used multiple times. | ||
| if [[ ! -x junit2jira ]]; then | ||
| curl --retry 5 --retry-connrefused --silent --show-error --fail --location --output junit2jira "$LOCATION" | ||
| chmod +x junit2jira | ||
| fi | ||
|
|
||
| - name: Capture job failure as JUnit if no test failures exist | ||
| shell: bash | ||
| if: always() | ||
| env: | ||
| STEPS_JSON: ${{ toJSON(steps) }} | ||
| ARTIFACT_DIR: ${{ inputs.directory }} | ||
| run: | | ||
| set -uo pipefail | ||
| "${GITHUB_ACTION_PATH}/../../common/common.sh" \ | ||
| "${GITHUB_ACTION_PATH}/junit2jira.sh" \ | ||
| capture_job_failure_as_junit \ | ||
| "${{ inputs.directory }}" \ | ||
| "${{ github.job }}" \ | ||
| "${{ job.status }}" \ | ||
| "$STEPS_JSON" \ | ||
| "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
|
Comment on lines
+82
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- action file ---'
cat -n test/junit2jira/action.yml
printf '%s\n' '--- repository references ---'
rg -n --hidden --glob '!node_modules' 'junit2jira|build-tag|STEPS_JSON|directory' test .github 2>/dev/null | head -200Repository: stackrox/actions Length of output: 10348 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Git ref-name character check ---'
git check-ref-format 'refs/heads/feature/$(touch-PWNED)' && echo 'ref accepted' || echo 'ref rejected'
git check-ref-format 'refs/heads/feature/$(echo PWNED)' && echo 'ref accepted' || echo 'ref rejected'
printf '%s\n' '--- Bash interpolation probe ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
cat >"$tmpdir/probe.sh" <<'SH'
set -eu
-build_tag="${REF_NAME}@${SHA}"
SH
REF_NAME='feature/$(printf INJECTED >&2)' SHA='abc123' bash "$tmpdir/probe.sh" 2>"$tmpdir/stderr" || true
printf 'stderr from env-based probe: '
cat "$tmpdir/stderr"
cat >"$tmpdir/interpolated.sh" <<'SH'
set -eu
-build_tag="feature/$(printf INJECTED >&2)`@abc123`"
SH
bash "$tmpdir/interpolated.sh" 2>"$tmpdir/stderr2" || true
printf 'stderr from expression-interpolated probe: '
cat "$tmpdir/stderr2"
printf '%s\n' '--- Relevant helper implementation ---'
cat -n test/junit2jira/junit2jira.sh | sed -n '1,155p'
printf '%s\n' '--- Action usages ---'
rg -n --glob '*.yml' --glob '*.yaml' 'uses:.*junit2jira|directory:|gcs-bucket:|gcs-subdir:' . | head -200Repository: stackrox/actions Length of output: 6604 Pass dynamic values through environment variables. GitHub expands expressions before Bash parses 🤖 Prompt for AI Agents |
||
|
|
||
| - name: Authenticate with GCP | ||
| if: inputs.gcp-account != '' | ||
| uses: google-github-actions/auth@v2 | ||
| with: | ||
| credentials_json: ${{ inputs.gcp-account }} | ||
|
|
||
| - name: Set up Cloud SDK | ||
| if: inputs.gcp-account != '' | ||
| uses: google-github-actions/setup-gcloud@v2 | ||
|
|
||
| - name: Report failures to Jira and upload metrics | ||
| id: run | ||
| shell: bash | ||
| env: | ||
| JIRA_USER: ${{ inputs.jira-user }} | ||
| JIRA_TOKEN: ${{ inputs.jira-token }} | ||
| if: ${{ env.JIRA_TOKEN != '' }} | ||
| run: | | ||
| set -uo pipefail | ||
| extra_args=() | ||
| if [[ "${{ inputs.create-jiras }}" == "false" ]]; then | ||
| extra_args=(--dry-run) | ||
| else | ||
| echo "Will create Jira issues for JUnit failures found in ${{ inputs.directory }}" | ||
| fi | ||
| csv_output="$(mktemp --suffix=.csv)" | ||
| summary_file="$(mktemp --suffix=.json)" | ||
| ./junit2jira \ | ||
| -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 }}" \ | ||
| -csv-output "${csv_output}" \ | ||
| -jira-url "${{ inputs.jira-url }}" \ | ||
| -job-name "${{ github.job }}" \ | ||
| -junit-reports-dir "${{ inputs.directory }}" \ | ||
| -orchestrator "${{ runner.name }} ${{ runner.os }}-${{ runner.arch }}" \ | ||
| -threshold "${{ inputs.threshold }}" \ | ||
| -summary-output "${summary_file}" \ | ||
| "${extra_args[@]}" | ||
|
|
||
| echo "NEW_JIRAS=$(jq -r '.newJIRAs > 0' "${summary_file}")" >> "$GITHUB_OUTPUT" | ||
|
|
||
| if [[ "${{ inputs.gcp-metrics }}" == "true" ]]; then | ||
| "${GITHUB_ACTION_PATH}/../../common/common.sh" \ | ||
| "${GITHUB_ACTION_PATH}/junit2jira.sh" \ | ||
| save_test_metrics \ | ||
| "${csv_output}" \ | ||
| "${{ inputs.gcs-bucket }}" \ | ||
| "${{ inputs.gcs-subdir }}" | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: stackrox/actions
Length of output: 10186
🏁 Script executed:
Repository: stackrox/actions
Length of output: 10122
🏁 Script executed:
Repository: stackrox/actions
Length of output: 1517
🏁 Script executed:
Repository: stackrox/actions
Length of output: 1492
🏁 Script executed:
Repository: stackrox/actions
Length of output: 179
Execute only the downloaded
junit2jirabinary.The existence check accepts any workspace executable and runs it with
JIRA_TOKEN. It also ignoresVERSION, so repeated action use can run a previously downloaded release. Store each download under a unique$RUNNER_TEMPdirectory, export its full path through$GITHUB_ENV, and invoke that path at lines 115-127.🤖 Prompt for AI Agents