Skip to main content

Workflow Examples

Framework-specific end-to-end examples for using ado-sync push, pull, and publish-test-results.


Day-to-day

# Local changes first — edit your files, then push
ado-sync push

# Azure changes first — someone edited a Test Case in the ADO UI
ado-sync pull

# Check for drift before raising a PR
ado-sync status

C# MSTest

# 1. Create TCs and write IDs back into .cs files
ado-sync push --dry-run # preview
ado-sync push # writes [TestProperty("tc","ID")] above each [TestMethod]

# 2. Run tests — TRX contains [TestProperty] values, TC IDs extracted automatically
dotnet test --logger "trx;LogFileName=results.trx"

# 3. Publish results
ado-sync publish-test-results --testResult results/results.trx

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "csharp",
"include": ["**/RegressionTests/**/*.cs"],
"exclude": ["**/*BaseTest.cs", "**/*Helper.cs"]
},
"sync": { "markAutomated": true, "format": { "useExpectedResult": true } }
}

C# NUnit

ado-sync push

# Use native NUnit XML logger — TRX omits [Property] values
dotnet test --logger "nunit3;LogFileName=results.xml"

ado-sync publish-test-results --testResult results/results.xml

C# SpecFlow

ado-sync push # writes @tc:ID above each Scenario

dotnet test --logger "trx;LogFileName=results.trx"
ado-sync publish-test-results --testResult results/results.trx

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": { "type": "gherkin", "include": ["Features/**/*.feature"] },
"sync": { "tagPrefix": "tc", "markAutomated": true }
}

Java JUnit / TestNG

ado-sync push # writes // @tc:ID (JUnit 4/TestNG) or @Tag("tc:ID") (JUnit 5)

# Maven
mvn test # Surefire: target/surefire-reports/*.xml

# Gradle
./gradlew test # build/test-results/test/*.xml

ado-sync publish-test-results \
--testResult "target/surefire-reports/TEST-*.xml" \
--testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "java",
"include": ["**/src/test/**/*.java"],
"exclude": ["**/*BaseTest.java", "**/*Helper.java"]
},
"sync": { "markAutomated": true }
}

Python pytest

ado-sync push # writes @pytest.mark.tc(ID) above each def test_*()

pytest --junitxml=results/junit.xml

ado-sync publish-test-results --testResult results/junit.xml --testResultFormat junit

Optional — embed TC IDs directly into JUnit XML (more reliable than AutomatedTestName matching):

Add to conftest.py:

def pytest_runtest_makereport(item, call):
for marker in item.iter_markers("tc"):
if marker.args:
item.user_properties.append(("tc", str(marker.args[0])))

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "python",
"include": ["tests/**/*.py"],
"exclude": ["tests/conftest.py", "tests/**/helpers.py"]
},
"sync": { "markAutomated": true }
}

JavaScript / TypeScript (Jest, Jasmine, WebdriverIO)

ado-sync push # writes // @tc:ID above each it() / test()

npx jest --reporters=default --reporters=jest-junit
# JEST_JUNIT_OUTPUT_DIR=results JEST_JUNIT_OUTPUT_NAME=junit.xml

ado-sync publish-test-results --testResult results/junit.xml --testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "javascript",
"include": ["src/**/*.spec.ts", "tests/**/*.test.js"],
"exclude": ["**/*.helper.ts"]
},
"sync": { "markAutomated": true }
}

Playwright

ado-sync push # writes annotation: { type: 'tc', description: 'ID' } into test options

# playwright.config.ts: reporter: [['json', { outputFile: 'results/playwright.json' }]]
npx playwright test

# TC IDs from native test.annotations; screenshots/videos from attachments[]
ado-sync publish-test-results \
--testResult results/playwright.json \
--attachmentsFolder test-results/

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "playwright",
"include": ["tests/**/*.spec.ts"],
"exclude": ["**/*.helper.ts"]
},
"sync": { "markAutomated": true }
}

Detox (React Native)

ado-sync push # writes // @tc:ID above each it() / test()

npx detox test --configuration ios.sim.release

ado-sync publish-test-results --testResult results/junit.xml --testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": { "type": "detox", "include": ["e2e/**/*.test.ts"] },
"sync": { "markAutomated": true }
}

Espresso (Android)

ado-sync push # writes // @tc:ID above @Test

./gradlew connectedAndroidTest
# XML: app/build/outputs/androidTest-results/connected/TEST-*.xml

ado-sync publish-test-results \
--testResult "app/build/outputs/androidTest-results/connected/TEST-*.xml" \
--testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "espresso",
"include": ["app/src/androidTest/**/*.java", "app/src/androidTest/**/*.kt"],
"exclude": ["**/*BaseTest.java"]
},
"sync": { "markAutomated": true }
}

XCUITest (iOS)

ado-sync push # writes // @tc:ID above func test*()

xcodebuild test \
-project MyApp.xcodeproj -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-resultBundlePath TestResults.xcresult
xcrun xcresulttool get --path TestResults.xcresult --format junit > results/junit.xml

ado-sync publish-test-results --testResult results/junit.xml --testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "xcuitest",
"include": ["UITests/**/*.swift"],
"exclude": ["UITests/**/*Helper.swift", "UITests/**/*Base.swift"]
},
"sync": { "markAutomated": true }
}

Flutter

ado-sync push # writes // @tc:ID above testWidgets() / test()

flutter test --reporter junit > results/junit.xml

ado-sync publish-test-results --testResult results/junit.xml --testResultFormat junit

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "flutter",
"include": ["test/**/*_test.dart", "integration_test/**/*_test.dart"]
},
"sync": { "markAutomated": true }
}

Robot Framework

ado-sync push # inserts/updates tc:ID in [Tags] of each test case

robot --outputdir results tests/

ado-sync publish-test-results --testResult results/output.xml

TC IDs are read directly from the tc:N tag in <tags> in output.xml — no extra config needed.

Recommended config:

{
"orgUrl": "https://dev.azure.com/my-org",
"project": "MyProject",
"auth": { "type": "pat", "token": "$AZURE_DEVOPS_TOKEN" },
"testPlan": { "id": 1234 },
"local": {
"type": "robot",
"include": ["tests/**/*.robot"],
"exclude": ["tests/resources/**"]
},
"sync": { "markAutomated": true }
}

CI pipeline (GitHub Actions)

Basic push + publish

- name: Sync test cases to Azure DevOps
run: ado-sync push --config-override sync.disableLocalChanges=true
env:
AZURE_DEVOPS_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}

- name: Publish test results
run: ado-sync publish-test-results --testResult results/test.trx
env:
AZURE_DEVOPS_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}

Use sync.disableLocalChanges=true in CI so the pipeline never commits ID writeback files.


PR comment bot

Copy the ado-sync-pr-check.yml workflow into your repo. On every pull request it runs ado-sync push --dry-run and posts a comment showing:

  • Unlinked specs that would be created in Azure DevOps
  • Specs with drift vs. existing Test Cases
  • Conflicts where both local and remote changed

Requires one secret: ADO_PAT (Azure DevOps PAT with Test Management read/write).


Coverage gate + AC gate

Copy the ado-sync-coverage-gate.yml workflow into your repo. Runs three checks on push/PR to main:

Step 1 — Spec coverage gate
ado-sync coverage --fail-below 80
Fails if fewer than 80% of local specs are linked to Azure Test Cases.

Step 2 — Acceptance criteria gate
ado-sync ac-gate --area-path "..."
Fails if any Active/Resolved story is missing AC or linked Test Cases.

Step 3 — Trend report (informational)
ado-sync trend --days 30
Optionally posts a Slack/Teams webhook summary. Never fails the build.

Configure via GitHub repository variables (no code changes needed):

VariableDescriptionDefault
ADO_SYNC_COVERAGE_MINMinimum spec link rate %80
ADO_SYNC_AC_AREA_PATHArea path scope for AC gate(all stories)
ADO_SYNC_TREND_DAYSDays of history for trend report30
ADO_SYNC_TREND_WEBHOOKSlack/Teams webhook URL(none)
ADO_SYNC_TREND_WEBHOOK_TYPEslack, teams, or genericslack

Stale TC cleanup in CI

To periodically retire stale Azure Test Cases (TCs that no longer have a local spec):

- name: Retire stale test cases
run: ado-sync stale --retire --dry-run # remove --dry-run to apply
env:
AZURE_DEVOPS_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}

This transitions orphaned TCs to Closed state and tags them ado-sync:retired. Use --retire-state "Inactive" if your process uses a different state name.


Flaky test detection in CI

- name: Detect flaky tests
run: |
ado-sync trend \
--days 14 \
--fail-on-flaky \
--webhook-url ${{ secrets.SLACK_WEBHOOK }}
env:
AZURE_DEVOPS_TOKEN: ${{ secrets.AZURE_DEVOPS_TOKEN }}

Exits 1 if any test both passed and failed in the last 14 days, and posts a summary to Slack. Drop --fail-on-flaky to run as informational-only.