Use when CI test results must flow into TestRail, Xray for Jira or Zephyr Scale without manual copying. Maps tests to case IDs, opens a run per build, pushes results in batches and closes runs on main only; vendor details are in its references.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Teams that manage test cases in a test management tool need automation result sync - without it, automated runs don't update the tool and the test-management view drifts from reality. The sync job has the same five-step shape across every vendor; only the auth model, the endpoints, and the case-ID scheme differ.
This skill wires that sync: the vendor-independent workflow below,
TestRail as the worked example, and per-vendor API detail in
references/.
| Vendor | Shape | Auth | Results API | Reference |
|---|---|---|---|---|
| TestRail (Gurock / Idera) | Standalone TCM (not a Jira app) | Basic (email + API key) | add_run + batched add_results_for_cases | references/testrail.md |
| Xray | Jira app (Test / Test Execution issue types) | Cloud: client_id+client_secret → 24h JWT; Server/DC: PAT | /api/v2/import/execution/{junit,cucumber,nunit,testng,robot} | references/xray.md |
| Zephyr Scale (SmartBear, formerly TM4J) | Jira app (Test Cycles) | Long-lived Bearer token | POST /testexecutions (or bulk /automations/executions/junit) | references/zephyr.md |
Routing rules:
currents-integration (different job entirely).Every vendor sync follows the same five steps:
test('adds to cart [C1234]')) or use an
annotation (@XrayTest(key=...), @TestRailCase(id=...),
@TestCaseKey(...)). Name-embedding is lowest-friction; either
way the sync script must recover the ID from the result.<branch> · <sha-short>.main only - closed containers are
read-only; PR runs stay open so flake-fix reruns can update them.Run the sync as an if: always() CI step after the test step so
failed runs still update the tool. The input is the runner's JUnit
XML - parse it with junit-xml-analysis.
A Jest suite syncing one build to TestRail project 42, suite 7.
test('can add to cart [C1234]', ...).export TESTRAIL_HOST=https://acme.testrail.io
export TESTRAIL_USER=ci@acme.com
export TESTRAIL_API_KEY=... # My Settings > API Keys
npm test -- --reporters=jest-junitinclude_all: False + explicit case_ids - without this a
5,000-case project produces a 5,000-row run of empty cells), then
batches the results in one POST:run_id = open_run(42, 7, "main · a1b2c3d", [1234]) # add_run → Run ID
add_results(run_id, [ # add_results_for_cases (batch)
{'case_id': 1234, 'status_id': 1, 'comment': 'green on CI', 'elapsed': '12s'},
])status_id: 1 is Passed per the stock convention (1 Passed /
2 Blocked / 3 Untested / 4 Retest / 5 Failed) - but read
get_statuses at script init and verify every ID in your map exists,
because TestRail accepts unknown IDs and writes to the wrong status
silently. Full script, per-result fields, and CI wiring:
references/testrail.md.
C1234 marked Passed with the
12s elapsed time. Close the run only when this is the main
build (Step 5).For the same build against Xray the container is a Test Execution
fed by POST /api/v2/import/execution/junit?projectKey=...
(references/xray.md); against Zephyr Scale it
is a Test Cycle fed by POST /testexecutions
(references/zephyr.md).
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Per-test result POSTs | N API calls trip every vendor's rate limit (TestRail 180 req/min, Zephyr 60 req/min). | Batch (TestRail add_results_for_cases, Xray format import) or bound concurrency (Zephyr). |
| Hard-coded status IDs / names | Custom statuses break the mapping silently. | Fetch the status list at script init; build the map dynamically. |
| Auto-creating cases / issues from CI | Every renamed test creates a new case; the project fills with orphans. | Pre-create cases; sync references existing IDs only. |
| Posting credentials as URL params or logging tokens | Secrets leak in proxy logs / CI logs. | Auth headers only; mask tokens (::add-mask::). |
| Closing every run, including PR runs | Closed runs can't accept the rerun after a flake fix. | Close / finalize only main runs. |
| One container reused across many builds | The run accumulates noise; release sign-off is unreadable. | One run / execution / cycle per build. |
Sync step not if: always() | Failed test runs - the ones release management most needs - never sync. | if: always() after the test step. |
tcm-case-management).add_run / add_results_for_cases / close_run, per-result fields, CI wiring, untested-case handling.@XrayTest / @Requirement mapping, testExecKey lifecycle, Playwright reporter./testexecutions, bounded-concurrency batching, bulk JUnit import.junit-xml-analysis - upstream parser for the input every sync script consumes.currents-integration - different role: test analytics over time, not test management.