Xray CLI - Test Management

SkillDev tools

Xray Cloud test management via `bun xray` CLI: create/list tests, manage test executions and plans, import JUnit/Cucumber/Xray JSON results, update run statuses, back up and restore projects, link defects. Triggers on: create a test in Xray, import test results to Xray, list Xray executions, update run status, backup Xray project, restore Xray tests, link defect to run, sync tests, Xray auth login. Do NOT use for: writing automated tests (test-automation); documenting test cases or ROI analysis (test-documentation); running CI regression suites (regression-testing); browser automation (playwright-cli).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Xray CLI - Test Management skill

What this skill tells your AI

The instructions your AI receives, as published by upex-galaxy/agentic-qa-boilerplate in .agents/skills/xray-cli/SKILL.md and read by ahel’s review.

Compact Rules

  • DO: confirm the project is in Modality jira-xray before invoking anything here; a jira-native project (no Xray plugin) routes to /acli instead. Modality is resolved once in /test-documentation Phase 0 and inherited downstream, never re-decided mid-flow.
  • DO NOT: call this CLI from a workflow skill. Workflow skills write [TMS_TOOL] pseudocode and load this skill; only this skill owns the literal syntax.
  • DO: pass an explicit --limit above the expected count on every list command — all of them default to 20 rows and truncate silently. Read the true count from the (N total) header, never by counting rows; a truncated read looks exactly like data loss.
  • DO: capture the key of anything you create from the bare KEY <PROJ-123> line or from --json, never by scraping the decorated success line — a create whose key was not captured leaves an orphan artifact nothing downstream can link.
  • DO NOT: pass Manual steps inline when creating a test — Xray Cloud silently drops them. Create the test first, add one step per call, then verify the steps landed.
  • DO: pin every ATR execution to a Test Environment (value from active_env), so results stay comparable across runs. An execution that slipped through without one is repaired in place, not left.
  • DO: keep the Set-first cascade: the per-Story ATS holds the membership, and the Plan (ATP) and Execution (ATR) derive their test lists from it rather than maintaining their own.
  • DO: fill Story coverage with the Jira-layer issue link from the ATS to the Story. Plan→Story and Execution→Story links are administrative traceability and cover nothing; a direct Test→Story link is a last resort for an instance with no Test Set work type. Plan/Execution/Set MEMBERSHIP is Xray-internal GraphQL and is never an issue link.
  • DO: verify traceability with the one-call three-edge check, never from the coverage edge alone — a missing ATP→Story or ATR→Story link is a FAIL, not a warning, and the same call compares the ATS membership against the Plan and Execution test lists.
  • WHEN a Jira-fallback path created the container without authenticated Xray: the Xray layer never registered the tests and runs come back empty. Reconcile with the per-entity sync (or the bulk repair scan) before importing results.
  • DO: import results onto an existing Execution key, never scoped to a project — the import API cannot set a parent, so a project-scoped import mints a fresh unparented Execution on every run, outside the artifact ladder.
  • DO NOT: hand-craft Xray JSON payloads outside this CLI, or reuse a bearer token past its 24h TTL. A stale token produces silent 401s mid-import that read like network blips.
  • DO: dry-run any import or backup restore before applying. Both write irreversibly across hundreds of tests and runs.
  • WHEN moving between sites: restore in sync-by-key mode (GraphQL ids are re-assigned per site, keys are not), re-authenticate between export and restore because auth holds ONE site at a time, and finish with the Jira instance-migration flow — field ids are reassigned and an old id silently resolves to a different field.
  • DO NOT: push run results for TCs the ROI verdict marked terminal-Manual. It creates audit noise and breaks the Candidate / Manual / Deferred reporting.
  • WHEN Xray credentials are missing or broken: STOP per Critical Rule #10 — name XRAY_CLIENT_ID / XRAY_CLIENT_SECRET, point at .env, ask for a session restart. Nothing reaches the Xray GraphQL layer without them.

Read full SKILL.md when: composing a specific command, wiring the canonical end-to-end Story flow, running backup/restore or a cross-site migration, or enriching the synced PBI cache.

Modality check (critical)

This skill owns [TMS_TOOL] only in Modality jira-xray (Jira Cloud + Xray plugin installed). Before invoking any command from this skill:

  1. Confirm the project is in Modality jira-xray. Resolution logic lives in test-documentation/SKILL.md §Phase 0.
  2. If the project is in Modality jira-native (no Xray plugin) -> do not use this skill. Instead, load /acli — TMS operations map to native Jira issues (see test-documentation/references/jira-setup.md).

Agents arriving here from a [TMS_TOOL] ... pseudocode block without having resolved modality first should pause and consult the modality resolver before proceeding.

Quick start

# Check authentication status
bun xray auth status
# Login with credentials
bun xray auth login --client-id ABC123 --client-secret xyz789
# List tests in a project
bun xray test list --project DEMO
# Create a test execution
bun xray exec create --project DEMO --summary "Sprint 1 Tests"
# Import JUnit results
bun xray import junit --file results.xml --project DEMO

Issue references: Jira keys vs numeric IDs

Every flag that takes an issue reference (--execution, --plan, --set, --tests, plus the positional argument of exec get / set get) accepts both forms interchangeably:

  • Jira key: {{PROJECT_KEY}}-194 — resolved via Jira REST in-process. Requires Jira credentials configured (auth login --jira-url --jira-email --jira-token or the JIRA_* env vars).
  • Numeric Xray issueId: 1042389 — used as-is, no resolution call.

If only Xray credentials are configured (no Jira creds) and you pass a Jira key, the CLI fails with a guiding error pointing at the missing flags. Test Run identifiers (run get, run status --id, etc.) are GraphQL run ids — these are NOT Jira keys and resolution does not apply to them.

# Both forms work identically
bun xray exec get {{PROJECT_KEY}}-194
bun xray exec get 1042389
bun xray exec add-tests --execution {{PROJECT_KEY}}-194 --tests {{PROJECT_KEY}}-100,{{PROJECT_KEY}}-101,{{PROJECT_KEY}}-102
bun xray exec add-tests --execution 1042389 --tests 1041000,1041001,1041002

Commands

Authentication

auth login reads every credential from .env by default (XRAY_CLIENT_ID/SECRET, ATLASSIAN_EMAIL/API_TOKEN); bun auto-loads .env. The Jira site host is not an env var — it comes from .agents/project.yaml. Pass a flag ONLY to override the environment — e.g. switching to another site mid-migration. With a populated .env, bun xray auth login (no args) is enough. Login prints which source (env/flag/unset) each credential resolved from.

bun xray auth login                                    # all creds from .env
bun xray auth login --client-id <id> --client-secret <secret>   # override XRAY creds (other site)
bun xray auth login --client-id <id> --client-secret <secret> --project DEMO
bun xray auth login --jira-url https://your-instance.atlassian.net --jira-email user@email.com --jira-token <token>
bun xray auth logout
bun xray auth status

Test Management

# Create tests
bun xray test create --project DEMO --summary "Verify login"
bun xray test create --project DEMO --summary "Verify login" --type Manual
bun xray test create --project DEMO --summary "API check" --type Generic --definition "curl http://api.test"
bun xray test create --project DEMO --summary "Login flow" --type Cucumber --gherkin "Feature: Login..."

# Manual test with steps — TWO-STEP pattern (create, THEN one add-step per step)
bun xray test create --project DEMO --summary "Verify login" --type Manual
bun xray test add-step --test <issueId> --action "Open app" --result "Login form is displayed"
bun xray test add-step --test <issueId> --action "Enter credentials" --data "user@test.com" --result "Success message"

# Get test details
bun xray test get DEMO-123
bun xray test get --id <issueId>

# List tests
bun xray test list --project DEMO
bun xray test list --project DEMO --limit 50
bun xray test list --jql "project = DEMO AND labels = critical"

Capturing the key of what you just created. Every create in this CLI (test, precondition, exec, plan, set) ends with a bare, ANSI-free KEY <PROJ-123> line, and accepts --json for the same value as a field. Use one of those two; never scrape the decorated ✔ Test created: … success line — the colour codes, the icon and the label all sit between you and the value, which is how "created the TC but could not get its id" keeps happening.

TC_KEY=$(bun xray test create --project DEMO --summary "Verify login" | grep '^KEY ' | cut -d' ' -f2)
bun xray test create --project DEMO --summary "Verify login" --json   # {"key":"DEMO-123","issueId":"1042389",...}

The issueId (numeric) is in the --json object too. Prefer the KEY for anything a human will read or link; the numeric id is what add-step and the other Xray-internal mutations take, and every command here accepts either.

Every list command defaults to --limit 20 and truncates silently. test list, exec list, set list and plan list all print the true total in the header (Tests (114 total, showing 20)) while listing only 20 rows. If you are counting, iterating, or deciding anything from the result, pass an explicit --limit above the expected count and read the count from the (N total) header, not by counting rows. This bites hardest during post-migration verification, where a truncated read looks exactly like data loss.

# Add step to existing test (the reliable way to add Manual steps — one call per step)
bun xray test add-step --test <issueId> --action "Click button" --result "Form submits"
bun xray test add-step --test <issueId> --action "Submit form" --data "valid payload" --result "200 OK"

# Remove a step from a test
bun xray test remove-step --test <issueId> --step <stepId>

# Update an existing step in place — only the flags you pass change; the rest keep their value
bun xray test update-step <issueId> --step <stepId> --action "Click submit"
bun xray test update-step <issueId> --step <stepId> --data "valid payload" --result "201 Created"

# Enrich an EXISTING test (e.g. Stage-4 regression promotion: add rich Gherkin to a sprint test)
bun xray test update-gherkin --test <issueId> --gherkin "Feature: Login..."
bun xray test update-definition --test <issueId> --definition "curl http://api.test"
bun xray test update-type --test <issueId> --type Cucumber

Manual steps do NOT persist on create (gotcha). Xray Cloud silently drops any steps passed to the createTest mutation (observed stepCount:0 after a "Test created" success). The --step flag on test create is therefore deprecated: if you pass it the test is still created, but the CLI prints a loud WARNING listing the exact test add-step calls you must run. The reliable path is always: test create --type Manual (no inline steps) → one test add-step per step. Verify the steps landed with bun xray test get <key> (or the GraphQL getTest { steps { id } } / stepCount).

Enrich the synced PBI cache (test enrich)

scripts/sync-jira-issues.ts mirrors Jira over REST, so it can never see the two associations that are Xray-internal (not Jira issue links): Precondition content and Test Set membership. test enrich backfills them — it reads Xray GraphQL and writes local files only (no Jira/Xray writes): it splices a delimited enrichment section into each synced TEST-*.md under .context/PBI/epics/**/test-cases/ (Precondition definitions inlined; Test Set membership as metadata) and writes one Set-first index per Test Set to .context/PBI/test-sets/<KEY>.md. Run it after every sync that touches Tests.

# Enrich every synced Test file under .context/PBI (default --dir)
bun xray test enrich

# Scope to one project's keys / a different cache root
bun xray test enrich --project {{PROJECT_KEY}}
bun xray test enrich --dir .context/PBI

# Preview without writing; tune batch size; skip the test-sets/ indexes
bun xray test enrich --dry-run
bun xray test enrich --batch 25
bun xray test enrich --no-set-index

Additive and pipeline-safe by design: missing Xray credentials, or a Test the Jira cache knows but Xray does not, are reported and skipped, never thrown — a sync pipeline that chains enrichment cannot be failed by it. Unchanged files are not rewritten (mtimes stay stable), and a batch JQL that fails on one stale key degrades to per-key queries instead of blanking the batch.

Preconditions

Preconditions are first-class Xray issues (issuetype Precondition) that hold setup state shared across Tests. The GraphQL mutations were always available; these commands expose them so you never have to drop to raw GraphQL.

# Create a precondition (default --type Manual; also Generic / Cucumber)
bun xray precondition create --project DEMO --summary "User is logged in"
bun xray precondition create --project DEMO --summary "DB seeded" --type Generic \
  --definition "bun run db:seed" --labels setup,smoke --folder /Auth

# Attach precondition(s) to a test (keys or numeric ids, both forms accepted)
bun xray precondition add-to-test --test {{PROJECT_KEY}}-123 --preconditions {{PROJECT_KEY}}-90,{{PROJECT_KEY}}-91

# Update a precondition's definition and/or type
bun xray precondition update --precondition {{PROJECT_KEY}}-90 --definition "Seed with v2 fixtures"
bun xray precondition update --precondition {{PROJECT_KEY}}-90 --type Generic

# List preconditions in a project (same --limit truncation rules as every list command)
bun xray precondition list --project {{PROJECT_KEY}}
bun xray precondition list --project {{PROJECT_KEY}} --limit 50

# Get one precondition's full detail (type, definition, linked tests)
bun xray precondition get {{PROJECT_KEY}}-90

# Detach precondition(s) from a test (the inverse of add-to-test)
bun xray precondition remove-from-test {{PROJECT_KEY}}-90 --test {{PROJECT_KEY}}-123

Parametrized Tests (convention, not a CLI feature)

Verified against the Xray Cloud GraphQL schema: datasets/parametrization are READ-only (getDataset / getDatasets queries exist; there are ZERO dataset mutations). No CLI command can therefore create or edit parameter values, and shared "Parameter Lists" remain UI-managed in Xray. Encode parameters by convention instead:

# Preferred: Cucumber Scenario Outline + Examples — parameters live in the Gherkin
# (works today via --gherkin on create, or update-gherkin on an existing test)
bun xray test create --project {{PROJECT_KEY}} --summary "Login matrix" --type Cucumber --gherkin "
Feature: Login
  Scenario Outline: Login as <role>
    Given I log in as \"<role>\"
    Then I see the \"<landing>\" page
  Examples:
    | role  | landing   |
    | admin | dashboard |
    | guest | catalog   |
"

# Manual tests: encode the parameter matrix in each step's --data field
bun xray test add-step --test <issueId> --action "Log in" --data "role=admin" --result "Dashboard shown"
bun xray test add-step --test <issueId> --action "Log in" --data "role=guest" --result "Catalog shown"

Pick ONE convention per test: Gherkin Scenario Outline + Examples when the variations share a flow (preferred), or explicit Manual steps with the variation in --data when steps genuinely differ. Do not hand-edit datasets in the UI and expect the CLI or backup/restore to round-trip them.

Test Executions

# Create execution
bun xray exec create --project DEMO --summary "Sprint 1 Regression"
bun xray exec create --project DEMO --summary "Sprint 1" --tests <id1>,<id2>,<id3>

# Pin the execution to a Test Environment (repeatable OR comma-separated).
# MANDATORY for every ATR — the CLI warns when --environment is omitted.
bun xray exec create --project DEMO --summary "Sprint 1" --environment staging
bun xray exec create --project DEMO --summary "Sprint 1" --environment staging --environment chrome
bun xray exec create --project DEMO --summary "Sprint 1" --environment staging,chrome

# Get execution details
bun xray exec get <issueId>

# List executions
bun xray exec list --project DEMO

# Manage tests in execution
bun xray exec add-tests --execution <id> --tests <id1>,<id2>
bun xray exec remove-tests --execution <id> --tests <id1>,<id2>

# Derive the execution's test list from a Test Set's membership (Set-first cascade)
bun xray exec add-set {{PROJECT_KEY}}-194 --set {{PROJECT_KEY}}-180

# Associate Test Environment(s) with an EXISTING execution
bun xray exec set-environment --execution <id> --environment staging
bun xray exec set-environment --execution {{PROJECT_KEY}}-194 --environment staging,chrome

Test Environments are MANDATORY on every ATR execution. An execution pinned to an environment (e.g. staging vs production, or chrome vs firefox) makes results congruent and comparable — you never blindly compare a staging run against a prod run. You MUST pass --environment on exec create for every ATR, with the value resolved from active_env in .agents/project.yaml; the CLI prints a warning when --environment is omitted. If an execution slipped through without one, repair it with exec set-environment.

Test Runs

# Get run details
bun xray run get <runId>

# List runs from execution
bun xray run list --execution <issueId>

# Update run status
bun xray run status --id <runId> --status PASSED
bun xray run status --id <runId> --status FAILED
bun xray run status --id <runId> --status TODO
bun xray run status --id <runId> --status EXECUTING
bun xray run status --id <runId> --status ABORTED
bun xray run status --id <runId> --status BLOCKED

# Update step status
bun xray run step-status --run <runId> --step <stepId> --status PASSED

# Add comment to a specific step (overwrites the previous step comment)
bun xray run step-comment --run <runId> --step <stepId> --comment "Reproduced on 2026-04-29 build 4172"

# Add comment
bun xray run comment --id <runId> --comment "Test completed successfully"

# Link defects
bun xray run defect --id <runId> --issues DEMO-456,DEMO-789

# Attach evidence files (PNG/JPG/PDF/log/JSON/...) to a run
bun xray run evidence --id <runId> --file ./screenshots/error.png
bun xray run evidence --id <runId> --file a.png --file b.png --file c.png
bun xray run evidence --id <runId> --dir ./.context/PBI/epics/EPIC-{{PROJECT_KEY}}-100-<epic-slug>/stories/STORY-{{PROJECT_KEY}}-8-<slug>/evidence/

# Attach evidence to a specific step within a run
bun xray run step-evidence --run <runId> --step <stepId> --file step3.png

# Inspect what is already attached
bun xray run evidence-list --id <runId>

# Remove an attachment (by id or by filename)
bun xray run evidence-rm --id <runId> --evidence <evidenceId>
bun xray run evidence-rm --id <runId> --filename error.png

Body size limit: Xray Cloud rejects requests larger than 20 MB. The CLI auto-chunks large --dir uploads into batches under that limit (using ~15 MB per batch to leave headroom for the GraphQL envelope), so a folder of 14 PNGs at 600 KB each ships in a single round trip while a folder with one 30 MB recording would be rejected — split or compress those before uploading.

Test Plans

# Create plan
bun xray plan create --project DEMO --summary "Q1 2025 Test Plan"
bun xray plan create --project DEMO --summary "Release 2.0" --tests <id1>,<id2>

# Get plan details (summary, status, attached tests) — key or numeric id
bun xray plan get {{PROJECT_KEY}}-110

# List plans
bun xray plan list --project DEMO

# Manage tests in plan
bun xray plan add-tests --plan {{PROJECT_KEY}}-110 --tests {{PROJECT_KEY}}-100,{{PROJECT_KEY}}-101
bun xray plan remove-tests --plan {{PROJECT_KEY}}-110 --tests {{PROJECT_KEY}}-100

# Derive the plan's test list from a Test Set's membership (Set-first cascade)
bun xray plan add-set {{PROJECT_KEY}}-110 --set {{PROJECT_KEY}}-180

# Associate Test Execution(s) with the plan (Plan <-> Execution, Xray-internal)
bun xray plan add-executions {{PROJECT_KEY}}-110 --executions {{PROJECT_KEY}}-194,{{PROJECT_KEY}}-195

Sync & Repair (Jira-layer ↔ Xray-layer reconciliation)

When a Test Execution or Test Plan is created through a Jira fallback path without authenticated Xray, the Jira layer (issuelinks, custom fields) accepts the issue but the Xray layer never registers the test attachment — runs come back empty and statuses cannot be set. Use these commands to detect and repair the drift.

# Diff a single Test Execution (dry-run by default)
bun xray exec sync --execution {{PROJECT_KEY}}-194
bun xray exec sync --execution {{PROJECT_KEY}}-194 --apply       # re-attach missing tests at the Xray layer

# Same for a Test Plan
bun xray plan sync --plan {{PROJECT_KEY}}-110
bun xray plan sync --plan {{PROJECT_KEY}}-110 --apply

# Same for a Test Set (dry-run by default, --apply to re-attach)
bun xray set sync --set {{PROJECT_KEY}}-180
bun xray set sync --set {{PROJECT_KEY}}-180 --apply

# Bulk scan every Test Execution + Test Plan + Test Set in a project
bun xray repair --project {{PROJECT_KEY}}                        # report only
bun xray repair --project {{PROJECT_KEY}} --apply                # re-attach every drift detected
bun xray repair --project {{PROJECT_KEY}} --apply --limit 200    # scan up to 200 of each type

What sync reports

  • Missing at Xray layer: tests linked at the Jira layer but not registered with Xray. --apply re-attaches them.
  • Missing at Jira layer: tests registered with Xray but without a Jira issuelink. Reported only — sync never auto-deletes.

Requirements: both Xray AND Jira credentials must be configured (auth login --jira-url --jira-email --jira-token); the Jira-layer view comes from Jira REST, separate from the Xray GraphQL API.

Test Sets

# Create set
bun xray set create --project DEMO --summary "Smoke Tests"
bun xray set create --project DEMO --summary "Regression" --tests <id1>,<id2>

# Get set details
bun xray set get <issueId>

# List sets
bun xray set list --project DEMO

# Manage tests in set
bun xray set add-tests --set <id> --tests <id1>,<id2>
bun xray set remove-tests --set <id> --tests <id1>,<id2>

ATS naming: the per-Story Acceptance Test Set is titled ATS: {US_ID}: {story title} and is the coverage backbone of the Set-first flow (see the Canonical End-to-End Flow below). Feature-level sets keep the TS: prefix. Set membership drift is repaired with set sync (see Sync & Repair).

Jira Issue Links (link create — the coverage write-path)

Xray's GraphQL API has no coverage mutation: requirement coverage is nothing but the Jira issue link whose inward description is is tested by. link create writes that link via Jira REST (POST /rest/api/3/issueLink) — it is the ONLY command in this CLI that fills the Story's coverage panel. The link-type --type takes a slug resolved from .agents/jira-required.yamllink_types (default test); never pass a literal Jira link-type name. Direction: <FROM> is the outward side, <TO> the inward side — for coverage, the ATS tests the Story, so the Story ends up is tested by the ATS.

# Coverage: link the Story's ATS to the Story (fills the coverage panel)
bun xray link create {{PROJECT_KEY}}-180 {{PROJECT_KEY}}-42 --type test

# Default --type is test — equivalent to the above
bun xray link create {{PROJECT_KEY}}-180 {{PROJECT_KEY}}-42

# Any other slug from jira-required.yaml link_types works the same way
bun xray link create {{PROJECT_KEY}}-110 {{PROJECT_KEY}}-42 --type test_design

Two layers, never confused: link create writes Jira-layer issue links (coverage, traceability). Plan/Execution/Set membership (plan add-tests, exec add-set, set add-tests, ...) is Xray-internal GraphQL and is never expressed as an issue link in Modality jira-xray.

Traceability verification (trace — the three-edge check in one call)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
22
Forks
13
Last commit
Sep 2026

ahel review

  • K2info
    exfiltration (in references/migration-runbook.md)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
xray-cli
Source
github.com/upex-galaxy/agentic-qa-boilerplate