@msalaam/xray-qe-toolkit 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,8 @@
22
22
  - [import-results](#xray-qe-import-results)
23
23
  - [gen-pipeline](#xray-qe-gen-pipeline)
24
24
  - [mcp-server](#xray-qe-mcp-server)
25
+ - [compare-openapi](#xray-qe-compare-openapi)
26
+ - [update-snapshot](#xray-qe-update-snapshot)
25
27
  - [Configuration](#configuration)
26
28
  - [Environment Variables (.env)](#environment-variables-env)
27
29
  - [Project Config (.xrayrc)](#project-config-xrayrc)
@@ -49,8 +51,9 @@
49
51
  4. **Postman collection generation** from test definitions
50
52
  5. **CI pipeline template** for Azure DevOps (Newman + Xray import)
51
53
  6. **Playwright integration** — import Playwright JSON results with automatic test mapping
52
- 7. **Modular architecture** — every function is importable for programmatic use
53
- 8. **AI-ready scaffolds** — optional AI assistance for test generation (manual workflow fully supported)
54
+ 7. **OpenAPI contract enforcement** — `compare-openapi` diffs a live spec against a QA snapshot and fails the pipeline on breaking changes; `update-snapshot` promotes a new baseline deliberately
55
+ 8. **Modular architecture** — every function is importable for programmatic use
56
+ 9. **AI-ready scaffolds** — optional AI assistance for test generation (manual workflow fully supported)
54
57
 
55
58
  ### QE Review Gate Philosophy
56
59
 
@@ -559,6 +562,100 @@ npx xqt mcp-server --port 3100
559
562
 
560
563
  ---
561
564
 
565
+ ### `xqt compare-openapi`
566
+
567
+ Compare an OpenAPI spec against an approved QA snapshot and fail the pipeline if breaking changes are detected.
568
+
569
+ ```bash
570
+ # Basic comparison — exits 1 on breaking changes
571
+ npx xqt compare-openapi \
572
+ --current ../api-repo/openapi.yaml \
573
+ --snapshot ./openapi.snapshot.yaml
574
+
575
+ # Save a JSON diff report as a pipeline artifact
576
+ npx xqt compare-openapi \
577
+ --current ../api-repo/openapi.yaml \
578
+ --snapshot ./openapi.snapshot.yaml \
579
+ --report openapi-diff-report.json
580
+ ```
581
+
582
+ | Option | Description | Required |
583
+ |----------------------|------------------------------------------------------------------|----------|
584
+ | `--current <path>` | Path to the current (live) OpenAPI spec | Yes |
585
+ | `--snapshot <path>` | Path to the approved QA baseline snapshot | Yes |
586
+ | `--report <path>` | Write full diff results to a JSON file (useful as CI artifact) | No |
587
+
588
+ **Behaviour:**
589
+ - Snapshot = approved QA contract baseline
590
+ - Current = proposed/live spec
591
+ - Exits `0` if no breaking changes; logs a count of non-breaking differences
592
+ - Exits `1` on any breaking change and prints the full diff
593
+ - If `--report` is specified and breaking changes are found, a JSON report is written
594
+
595
+ **Example Azure Pipelines step (from your test repo pipeline):**
596
+
597
+ ```yaml
598
+ steps:
599
+ - checkout: self
600
+
601
+ - checkout: git://Project/portfolio-api
602
+ path: api-repo
603
+
604
+ - script: |
605
+ npx xqt compare-openapi \
606
+ --current api-repo/openapi.yaml \
607
+ --snapshot openapi.snapshot.yaml \
608
+ --report openapi-diff-report.json
609
+ displayName: Compare OpenAPI Contracts
610
+
611
+ - publish: openapi-diff-report.json
612
+ artifact: openapi-diff
613
+ condition: failed()
614
+ ```
615
+
616
+ **Governance model:**
617
+ - Dev repo is never modified by QE
618
+ - QE test repo pipeline checks out the API repo and enforces the contract
619
+ - Breaking change → pipeline fails → dev must fix spec or raise a contract change review
620
+ - QE then runs `update-snapshot` to promote the new baseline
621
+
622
+ ---
623
+
624
+ ### `xqt update-snapshot`
625
+
626
+ Overwrite the QA snapshot baseline with the current spec. **Always a deliberate, manual action — never called automatically.**
627
+
628
+ ```bash
629
+ npx xqt update-snapshot \
630
+ --current ../api-repo/openapi.yaml \
631
+ --snapshot ./openapi.snapshot.yaml
632
+ ```
633
+
634
+ | Option | Description | Required |
635
+ |----------------------|-----------------------------------------------------|----------|
636
+ | `--current <path>` | Path to the current (live) OpenAPI spec to promote | Yes |
637
+ | `--snapshot <path>` | Path to the snapshot file to overwrite | Yes |
638
+
639
+ **Workflow:**
640
+ 1. Dev raises a contract change review
641
+ 2. QE approves the new contract
642
+ 3. QE runs `update-snapshot` locally
643
+ 4. QE raises a PR in the test repo with the updated snapshot
644
+ 5. PR is reviewed and merged — new baseline is established
645
+
646
+ ```bash
647
+ # After merging the approved contract change:
648
+ npx xqt update-snapshot \
649
+ --current ../api-repo/openapi.yaml \
650
+ --snapshot ./openapi.snapshot.yaml
651
+
652
+ git add openapi.snapshot.yaml
653
+ git commit -m "chore: promote OpenAPI snapshot to v2.5.0"
654
+ git push
655
+ ```
656
+
657
+ ---
658
+
562
659
  ## Configuration
563
660
 
564
661
  ### Environment Variables (.env)
@@ -754,6 +851,21 @@ Use this checklist to align a new team's board and Xray configuration with the t
754
851
  │ 10. newman run collection.postman.json --reporters junit │
755
852
  │ 11. npx xqt import-results --file results.xml --testExecKey QE-123│
756
853
  │ │
854
+ ├─────────────────────────────────────────────────────────────────────────┤
855
+ │ CONTRACT ENFORCEMENT (TEST REPO PIPELINE) │
856
+ │ │
857
+ │ 12. Checkout API repo │
858
+ │ 13. npx xqt compare-openapi │
859
+ │ --current api-repo/openapi.yaml │
860
+ │ --snapshot openapi.snapshot.yaml │
861
+ │ → Fails pipeline on breaking changes │
862
+ │ │
863
+ │ (When QE approves a contract change) │
864
+ │ 14. npx xqt update-snapshot │
865
+ │ --current api-repo/openapi.yaml │
866
+ │ --snapshot openapi.snapshot.yaml │
867
+ │ → Commit updated snapshot + raise PR │
868
+ │ │
757
869
  └─────────────────────────────────────────────────────────────────────────┘
758
870
  ```
759
871
 
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * @msalaam/xray-qe-toolkit — CLI Entry Point
@@ -16,6 +16,8 @@
16
16
  * import-results Import test results (JUnit XML or Playwright JSON) into Xray
17
17
  * gen-pipeline Generate an Azure Pipelines YAML template
18
18
  * mcp-server Start Model Context Protocol server for agent integration
19
+ * compare-openapi Compare OpenAPI specs and fail if breaking changes are detected
20
+ * update-snapshot Overwrite the QA snapshot baseline with the current spec
19
21
  */
20
22
 
21
23
  import { Command } from "commander";
@@ -131,6 +133,27 @@ program
131
133
  await mod.default({ ...opts, ...cmd.optsWithGlobals() });
132
134
  });
133
135
 
136
+ program
137
+ .command("compare-openapi")
138
+ .description("Compare OpenAPI specs against a QA snapshot and fail if breaking changes are detected")
139
+ .requiredOption("--current <path>", "Path to the current (live) OpenAPI spec")
140
+ .requiredOption("--snapshot <path>", "Path to the approved QA snapshot")
141
+ .option("--report <path>", "Write a JSON diff report to this path (useful as a pipeline artifact)")
142
+ .action(async (opts, cmd) => {
143
+ const mod = await import("../commands/compareOpenapi.js");
144
+ await mod.default({ ...opts, ...cmd.optsWithGlobals() });
145
+ });
146
+
147
+ program
148
+ .command("update-snapshot")
149
+ .description("Overwrite the QA snapshot baseline with the current spec (always a deliberate, manual action)")
150
+ .requiredOption("--current <path>", "Path to the current (live) OpenAPI spec to promote")
151
+ .requiredOption("--snapshot <path>", "Path to the snapshot file to overwrite")
152
+ .action(async (opts, cmd) => {
153
+ const mod = await import("../commands/updateSnapshot.js");
154
+ await mod.default({ ...opts, ...cmd.optsWithGlobals() });
155
+ });
156
+
134
157
  // ─── Parse & run ───────────────────────────────────────────────────────────────
135
158
 
136
159
  program.parseAsync(process.argv).catch((err) => {
@@ -0,0 +1,78 @@
1
+ import { diff } from 'openapi-diff';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Compare two OpenAPI specs and detect breaking changes.
7
+ * @param {string} currentPath - Path to the current (live) OpenAPI spec.
8
+ * @param {string} snapshotPath - Path to the approved QA snapshot.
9
+ * @param {string|null} reportPath - Optional path to write a JSON diff report.
10
+ */
11
+ export async function compareOpenApiSpecs(currentPath, snapshotPath, reportPath = null) {
12
+ const resolvedCurrent = path.resolve(currentPath);
13
+ const resolvedSnapshot = path.resolve(snapshotPath);
14
+
15
+ if (!fs.existsSync(resolvedCurrent)) {
16
+ throw new Error(`Current OpenAPI spec not found: ${resolvedCurrent}`);
17
+ }
18
+
19
+ if (!fs.existsSync(resolvedSnapshot)) {
20
+ throw new Error(`Snapshot OpenAPI spec not found: ${resolvedSnapshot}`);
21
+ }
22
+
23
+ const currentSpec = fs.readFileSync(resolvedCurrent, 'utf-8');
24
+ const snapshotSpec = fs.readFileSync(resolvedSnapshot, 'utf-8');
25
+
26
+ const result = await diff({
27
+ sourceSpec: { content: snapshotSpec }, // approved baseline
28
+ destinationSpec: { content: currentSpec } // proposed change
29
+ });
30
+
31
+ const summary = {
32
+ breaking: result.breakingDifferencesFound,
33
+ breakingDifferences: result.breakingDifferences,
34
+ nonBreakingDifferences: result.nonBreakingDifferences
35
+ };
36
+
37
+ if (reportPath && result.breakingDifferencesFound) {
38
+ const resolvedReport = path.resolve(reportPath);
39
+ fs.writeFileSync(resolvedReport, JSON.stringify(summary, null, 2), 'utf-8');
40
+ console.log(`📄 Diff report written to: ${resolvedReport}`);
41
+ }
42
+
43
+ return summary;
44
+ }
45
+
46
+ export default async function compareOpenapi(opts) {
47
+ const { current, snapshot, report } = opts;
48
+
49
+ try {
50
+ const result = await compareOpenApiSpecs(current, snapshot, report || null);
51
+
52
+ if (result.breaking) {
53
+ console.error('\n❌ Breaking changes detected:\n');
54
+ console.error(JSON.stringify(result.breakingDifferences, null, 2));
55
+
56
+ const count = result.breakingDifferences?.length ?? 0;
57
+ console.error(`\n${count} breaking difference(s) found.`);
58
+
59
+ if (!report) {
60
+ console.error('\nTip: use --report <path> to save a full diff report as a pipeline artifact.');
61
+ }
62
+
63
+ process.exit(1);
64
+ }
65
+
66
+ const nonBreaking = result.nonBreakingDifferences?.length ?? 0;
67
+ console.log('\n✅ No breaking changes detected.');
68
+
69
+ if (nonBreaking > 0) {
70
+ console.log(`ℹ️ ${nonBreaking} non-breaking difference(s) found (safe to proceed).`);
71
+ }
72
+
73
+ process.exit(0);
74
+ } catch (err) {
75
+ console.error(`\n❌ ${err.message}`);
76
+ process.exit(1);
77
+ }
78
+ }
@@ -0,0 +1,34 @@
1
+ import fse from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Copy the current OpenAPI spec over the snapshot baseline.
6
+ * This is intentionally a manual, explicit action — never called automatically.
7
+ *
8
+ * @param {string} currentPath - Path to the current (live) OpenAPI spec.
9
+ * @param {string} snapshotPath - Path to the snapshot file to overwrite.
10
+ */
11
+ export async function updateSnapshot(currentPath, snapshotPath) {
12
+ const resolvedCurrent = path.resolve(currentPath);
13
+ const resolvedSnapshot = path.resolve(snapshotPath);
14
+
15
+ if (!(await fse.pathExists(resolvedCurrent))) {
16
+ throw new Error(`Current OpenAPI spec not found: ${resolvedCurrent}`);
17
+ }
18
+
19
+ await fse.copy(resolvedCurrent, resolvedSnapshot, { overwrite: true });
20
+ console.log(`✅ Snapshot updated: ${resolvedSnapshot}`);
21
+ console.log(` Source: ${resolvedCurrent}`);
22
+ }
23
+
24
+ export default async function updateSnapshotCmd(opts) {
25
+ const { current, snapshot } = opts;
26
+
27
+ try {
28
+ await updateSnapshot(current, snapshot);
29
+ console.log('\nBaseline established. Commit the updated snapshot to your test repo to record the new contract.');
30
+ } catch (err) {
31
+ console.error(`\n❌ ${err.message}`);
32
+ process.exit(1);
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@msalaam/xray-qe-toolkit",
3
- "version": "1.3.4",
4
- "description": "Full QE workflow toolkit for Xray Cloud integration — test management, Postman generation, CI pipeline scaffolding, and browser-based review gates for API regression projects.",
3
+ "version": "1.4.0",
4
+ "description": "Full QE workflow toolkit for Xray Cloud integration — test management, Playwright integration, CI pipeline scaffolding, and browser-based review gates for API regression projects.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "xqt": "./bin/cli.js"
@@ -35,7 +35,10 @@
35
35
  "commander": "^13.1.0",
36
36
  "dotenv": "^17.2.3",
37
37
  "express": "^5.1.0",
38
- "open": "^10.1.2"
38
+ "fs-extra": "^11.3.3",
39
+ "open": "^10.1.2",
40
+ "openapi-diff": "^0.24.1",
41
+ "yaml": "^2.8.2"
39
42
  },
40
43
  "devDependencies": {
41
44
  "ajv": "^8.17.1"
@@ -81,6 +81,8 @@ npx xqt gen-pipeline
81
81
  | `npx xqt gen-pipeline` | Generate CI pipeline template |
82
82
  | `npx xqt create-execution` | Create Test Execution issue |
83
83
  | `npx xqt import-results` | Import Playwright JSON results (CI) |
84
+ | `npx xqt compare-openapi` | Diff live spec vs QA snapshot — fails on breaking changes |
85
+ | `npx xqt update-snapshot` | Promote current spec as new QA baseline (manual only) |
84
86
 
85
87
  Run any command with `--help` for details:
86
88
  ```bash
@@ -97,6 +99,15 @@ npx xqt gen-tests --help
97
99
  5. Generate CI pipeline: npx xqt gen-pipeline
98
100
  6. Run Playwright tests in CI
99
101
  7. Import results: npx xqt import-results --file playwright-results.json
102
+
103
+ Contract Enforcement (from your test repo pipeline):
104
+ 8. Checkout API repo in pipeline
105
+ 9. npx xqt compare-openapi --current api-repo/openapi.yaml --snapshot openapi.snapshot.yaml
106
+ → Fail pipeline on breaking changes
107
+
108
+ When a contract change is approved:
109
+ 10. npx xqt update-snapshot --current api-repo/openapi.yaml --snapshot openapi.snapshot.yaml
110
+ → Commit updated snapshot + raise PR to establish new baseline
100
111
  ```
101
112
 
102
113
  ## Files