diff --git a/.github/workflows/trigger-labs-tests.yml b/.github/workflows/trigger-labs-tests.yml new file mode 100644 index 0000000..91abb57 --- /dev/null +++ b/.github/workflows/trigger-labs-tests.yml @@ -0,0 +1,250 @@ +name: Trigger labs-tests + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + labs_branch: + description: "Optional branch of specmatic/labs to test. Defaults to the branch this workflow runs on." + required: false + type: string + labs_tests_branch: + description: "Branch of specmatic/labs-tests whose workflows should run. Defaults to main." + required: false + default: "main" + type: string + labs: + description: "Optional space-separated list of labs to run in the main labs-tests workflow. Leave empty to run the full suite." + required: false + type: string + +permissions: + contents: read + +jobs: + dispatch-labs-tests: + runs-on: ubuntu-latest + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - workflow_id: labs-tests.yml + workflow_label: Main labs-tests workflow + artifact_name: specmatic-labs-reports + accepts_labs_input: true + - workflow_id: labs-tests-readme-pilot.yml + workflow_label: README-driven pilot workflow + artifact_name: specmatic-labs-readme-pilot-reports + accepts_labs_input: false + env: + TARGET_OWNER: specmatic + TARGET_REPO: labs-tests + EFFECTIVE_LABS_BRANCH: ${{ github.event.inputs.labs_branch || github.ref_name }} + EFFECTIVE_LABS_TESTS_BRANCH: ${{ github.event.inputs.labs_tests_branch || 'main' }} + SELECTED_LABS: ${{ github.event.inputs.labs || '' }} + steps: + - name: Validate labs-tests dispatch token + env: + TOKEN_PRESENT: ${{ secrets.SPECMATIC_GITHUB_TOKEN != '' }} + run: | + if [[ "${TOKEN_PRESENT}" != "true" ]]; then + echo "::error::Missing SPECMATIC_GITHUB_TOKEN secret in specmatic/labs." + exit 1 + fi + + - name: Dispatch downstream workflow + id: dispatch + uses: actions/github-script@v7 + env: + TARGET_WORKFLOW_ID: ${{ matrix.workflow_id }} + WORKFLOW_LABEL: ${{ matrix.workflow_label }} + ACCEPTS_LABS_INPUT: ${{ matrix.accepts_labs_input }} + with: + github-token: ${{ secrets.SPECMATIC_GITHUB_TOKEN }} + script: | + const owner = process.env.TARGET_OWNER; + const repo = process.env.TARGET_REPO; + const workflow_id = process.env.TARGET_WORKFLOW_ID; + const workflowLabel = process.env.WORKFLOW_LABEL; + const ref = process.env.EFFECTIVE_LABS_TESTS_BRANCH; + const labsBranch = process.env.EFFECTIVE_LABS_BRANCH; + const selectedLabs = process.env.SELECTED_LABS; + const acceptsLabsInput = process.env.ACCEPTS_LABS_INPUT === 'true'; + const dispatchStartedAt = new Date(); + const inputs = { labs_branch: labsBranch }; + if (acceptsLabsInput) { + inputs.labs = selectedLabs; + } + + core.info(`Dispatching ${workflowLabel}: ${owner}/${repo}:${workflow_id}`); + core.info(`labs branch: ${labsBranch}`); + core.info(`labs-tests branch: ${ref}`); + core.info(`selected labs: ${selectedLabs || '(all labs)'}`); + + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id, + ref, + inputs, + }); + + let run = null; + for (let attempt = 1; attempt <= 30; attempt += 1) { + const response = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id, + branch: ref, + event: 'workflow_dispatch', + per_page: 20, + }); + run = response.data.workflow_runs.find(candidate => { + const createdAt = new Date(candidate.created_at); + return createdAt >= new Date(dispatchStartedAt.getTime() - 60_000); + }); + if (run) { + break; + } + core.info(`Waiting for ${workflowLabel} run to appear (attempt ${attempt}/30)...`); + await new Promise(resolve => setTimeout(resolve, 10000)); + } + + if (!run) { + core.setFailed(`Unable to locate the dispatched ${workflowLabel} run.`); + return; + } + + core.info(`Matched ${workflowLabel} run: ${run.html_url}`); + core.setOutput('run_id', String(run.id)); + core.setOutput('run_url', run.html_url); + + - name: Wait for downstream workflow completion + id: wait + uses: actions/github-script@v7 + env: + RUN_ID: ${{ steps.dispatch.outputs.run_id }} + WORKFLOW_LABEL: ${{ matrix.workflow_label }} + with: + github-token: ${{ secrets.SPECMATIC_GITHUB_TOKEN }} + script: | + const owner = process.env.TARGET_OWNER; + const repo = process.env.TARGET_REPO; + const run_id = Number(process.env.RUN_ID); + const workflowLabel = process.env.WORKFLOW_LABEL; + + if (!run_id) { + core.setFailed(`Missing downstream workflow run id for ${workflowLabel}.`); + return; + } + + let run = null; + for (let attempt = 1; attempt <= 180; attempt += 1) { + const response = await github.rest.actions.getWorkflowRun({ owner, repo, run_id }); + run = response.data; + core.info(`${workflowLabel} status: ${run.status}, conclusion: ${run.conclusion || 'n/a'} (attempt ${attempt}/180)`); + if (run.status === 'completed') { + break; + } + await new Promise(resolve => setTimeout(resolve, 30000)); + } + + if (!run || run.status !== 'completed') { + core.setFailed(`Timed out waiting for ${workflowLabel} to finish.`); + return; + } + + core.setOutput('conclusion', run.conclusion || 'unknown'); + core.setOutput('run_url', run.html_url); + core.setOutput('run_number', String(run.run_number)); + + - name: Download downstream reports artifact + env: + GH_TOKEN: ${{ secrets.SPECMATIC_GITHUB_TOKEN }} + RUN_ID: ${{ steps.dispatch.outputs.run_id }} + ARTIFACT_NAME: ${{ matrix.artifact_name }} + WORKFLOW_ID: ${{ matrix.workflow_id }} + run: | + set -euo pipefail + rm -rf downstream-artifact + mkdir -p downstream-artifact + gh run download "$RUN_ID" --repo "${TARGET_OWNER}/${TARGET_REPO}" --name "$ARTIFACT_NAME" --dir downstream-artifact + + - name: Build summary from downstream result + env: + RUN_URL: ${{ steps.wait.outputs.run_url }} + RUN_CONCLUSION: ${{ steps.wait.outputs.conclusion }} + RUN_NUMBER: ${{ steps.wait.outputs.run_number }} + SELECTED_LABS: ${{ env.SELECTED_LABS }} + EFFECTIVE_LABS_BRANCH: ${{ env.EFFECTIVE_LABS_BRANCH }} + EFFECTIVE_LABS_TESTS_BRANCH: ${{ env.EFFECTIVE_LABS_TESTS_BRANCH }} + WORKFLOW_LABEL: ${{ matrix.workflow_label }} + run: | + python3 - <<'PY' >> "$GITHUB_STEP_SUMMARY" + import json + import os + from pathlib import Path + + root = Path('downstream-artifact') + candidates = [ + root / 'output' / 'consolidated-report' / 'consolidated-report.json', + root / 'consolidated-report' / 'consolidated-report.json', + root / 'output' / 'consolidated-report.json', + ] + report_path = next((path for path in candidates if path.exists()), None) + + workflow_label = os.environ['WORKFLOW_LABEL'] + run_url = os.environ['RUN_URL'] + conclusion = os.environ.get('RUN_CONCLUSION', 'unknown') + run_number = os.environ.get('RUN_NUMBER', 'n/a') + selected_labs = os.environ.get('SELECTED_LABS', '') or '(all labs)' + labs_branch = os.environ.get('EFFECTIVE_LABS_BRANCH', 'main') + labs_tests_branch = os.environ.get('EFFECTIVE_LABS_TESTS_BRANCH', 'main') + + print(f'## {workflow_label}') + print('') + print(f'- labs branch: `{labs_branch}`') + print(f'- labs-tests branch: `{labs_tests_branch}`') + print(f'- selected labs: `{selected_labs}`') + print(f'- labs-tests run: [#{run_number}]({run_url})') + print(f'- conclusion: **{conclusion.upper()}**') + + if report_path is None: + print('') + print('No consolidated report was found in the downloaded downstream artifact.') + raise SystemExit(0) + + payload = json.loads(report_path.read_text(encoding='utf-8')) + summary = {item['label']: item['value'] for item in payload.get('summary', [])} + environment = payload.get('environment', {}) + print('') + print(f'- Specmatic: `{environment.get("specmaticVersion", "n/a")}`') + print(f'- labs discovered: `{summary.get("Labs discovered", 0)}`') + print(f'- labs passed: `{summary.get("Labs passed", 0)}`') + print(f'- labs failed: `{summary.get("Labs failed", 0)}`') + print('') + print('| Lab | Status | Time (s) | Exit Code | Failures | Validations |') + print('| --- | --- | ---: | ---: | ---: | ---: |') + for lab in payload.get('labs', []): + failures = 'n/a' + validations = 'n/a' + for item in lab.get('summary', []): + if item.get('label') == 'Failures': + failures = item.get('value', 'n/a') + if item.get('label') == 'Validations': + validations = item.get('value', 'n/a') + print( + f"| {lab.get('name', 'unknown')} | {lab.get('status', 'unknown')} | " + f"{lab.get('durationSeconds', 'n/a')} | {lab.get('exitCode', 'n/a')} | " + f"{failures} | {validations} |" + ) + PY + + - name: Fail when downstream workflow fails + if: steps.wait.outputs.conclusion != 'success' + run: | + echo "Downstream workflow failed: ${{ steps.wait.outputs.run_url }}" + exit 1 diff --git a/LAB_AUTOMATION_COMMANDS_SUMMARY.md b/LAB_AUTOMATION_COMMANDS_SUMMARY.md new file mode 100644 index 0000000..411d6cc --- /dev/null +++ b/LAB_AUTOMATION_COMMANDS_SUMMARY.md @@ -0,0 +1,31 @@ +# Lab Learner Tasks Summary + +This summary lists the learner task attached to each lab README, excluding `order-bff`, `coding-agents`, and `arazzo-workflow-testing`. + +Command cells use HTML `
...` blocks because raw triple-backtick fences break Markdown tables.
+
+| Lab | Task | Command | Status | Files Changed | Notes |
+|---|---|---|---|---|---|
+| `api-coverage` | Fix `specs/service.yaml`: change `GET /pets/search` to `GET /pets/find`. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#/pets/search:$#/pets/find:#' specs/service.yaml" | PASS | `api-coverage/specs/service.yaml` | Changed `/pets/search` to `/pets/find`. |
+| `api-resiliency-testing` | Fix downstream timeout mock examples, then enable full schema resiliency: add transient delays in `examples/order-service/stub_timeout_get_products.json` and `stub_timeout_post_product.json`, change `schemaResiliencyTests` to `all`, then update `stub_timeout_post_product.json` with `value:each` matchers. | Not simple enough for a safe one-liner; it needs multiple structured JSON edits. | Skipped | | No concrete command to run. |
+| `api-security-schemes` | Fix `specmatic.yaml` auth values: replace invalid OAuth token, Basic Auth value, and API key with valid values. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#INVALID_OAUTH_TOKEN#OAUTH_TOKEN#g; s#dXNlcjppbnZhbGlkcGFzcw==#dXNlcjpwYXNzd29yZA==#g; s#INVALID_APIKEY1234#APIKEY1234#g' specmatic.yaml" | PASS | `api-security-schemes/specmatic.yaml` | Replaced the invalid OAuth token env key, Basic Auth fallback token, and API key fallback. |
+| `async-event-flow` | Fix async examples: add missing `before` fixture in `acceptOrder.json` and correct the `after` fixture count in `outForDeliveryOrder.json`. | Not simple enough for a safe one-liner; it needs inserting a multi-line fixture plus another JSON edit. | Skipped | | No concrete command to run. |
+| `backward-compatibility-testing` | In `products.yaml`, keep the new `category` field but change `name` back from `number` to `string` to restore backward compatibility. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^ name:$/,/^ sku:$/ s#type: number#type: string#' products.yaml" | No-op | | Current checkout is not in the post-Part-A broken state; `name` is already `string`, so there was no diff to verify. |
+| `continuous-integration` | In `contracts/order_api.yaml`, make CI pass by removing `priority` from the required list while keeping the `priority` property. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "awk 'BEGIN{removed=0} {if (removed==0 && \$0==\" - priority\") {removed=1; next} print}' contracts/order_api.yaml > /tmp/order_api.yaml && mv /tmp/order_api.yaml contracts/order_api.yaml" | PASS | `continuous-integration/contracts/order_api.yaml` | Removed only the first `- priority` from the `POST /orders` required list. Replaced the original sed command because it exited zero without changing the file, and removed `!` so zsh does not trigger history expansion. |
+| `data-adapters` | Wire existing request/response adapter hooks in `specmatic.yaml` and ensure hook scripts are executable. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ data:\n adapters:\n pre_specmatic_request_processor: ./hooks/pre_specmatic_request_processor.sh\n post_specmatic_response_processor: ./hooks/post_specmatic_response_processor.sh\n' specmatic.yaml && chmod +x hooks/pre_specmatic_request_processor.sh hooks/post_specmatic_response_processor.sh" | PASS | `data-adapters/specmatic.yaml` | Added `dependencies.data.adapters` with both hook paths. Hook chmod produced no tracked diff because executable bits were already set. |
+| `dictionary` | Generate `specs/dictionary.yaml` from examples and configure `specmatic.yaml` to use it for dictionary-driven mock data. | docker run --rm -v "$PWD:/usr/src/app" specmatic/enterprise examples dictionary --examples-dir examples --spec-file specs/simple-openapi-spec.yaml --out specs/dictionary.yamldocker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ data:\n dictionary:\n path: specs/dictionary.yaml\n' specmatic.yaml" | PASS | `dictionary/specs/dictionary.yaml`, `dictionary/specmatic.yaml` | Generated `specs/dictionary.yaml` and added `dependencies.services[0].service.data.dictionary.path`. |
+| `external-examples` | Fix invalid external examples under `examples/`, then generate missing `201` examples for create APIs in Studio. | No simple command; the lab includes Studio validation/fix flow and example generation. | Skipped | | No concrete command to run. |
+| `filters` | Use Studio to exclude failing scenarios and uncovered `429` scenarios, then export/persist filters into `specmatic.yaml`. | No simple command; the task depends on Studio selections and exported filter config. | Skipped | | No concrete command to run. |
+| `kafka-avro` | Update Avro schemas with missing constraints, then update AsyncAPI example messages so they satisfy those constraints. | Not simple enough for a safe one-liner; it replaces larger structured JSON schema and example documents. | Skipped | | No concrete command to run. |
+| `kafka-sqs-retry-dlq` | Fix `service/app.py` by enabling the retry consumer thread so retry-topic messages are consumed and reprocessed. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's@ # threading.Thread(target=self._run_retry_consumer, name=\"RetryConsumer\", daemon=True),@ threading.Thread(target=self._run_retry_consumer, name=\"RetryConsumer\", daemon=True),@' service/app.py" | PASS | `kafka-sqs-retry-dlq/service/app.py` | Uncommented the `RetryConsumer` thread entry. |
+| `mcp-auto-test` | Fix two bugs in `service/order_service.py`: shipment status lookup and return quote reason-to-fee lookup for `damaged`. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"damage\": 0.0#\"damaged\": 0.0#; s#order\\[\"shipment\"\\]#order[\"shipmentStatus\"]#' service/order_service.py" | PASS | `mcp-auto-test/service/order_service.py` | Replaced `damage` with `damaged` and fixed shipment status lookup. |
+| `overlays` | Update `overlays/path-prefix.overlay.yaml` to replace `/api/users/{id}` with `/api/v1/users/{id}`, then enable the overlay in `specmatic.yaml`. | Not simple enough for a safe one-liner; it needs a structured overlay action block and config change. | Skipped | | No concrete command to run. |
+| `partial-examples` | Fix three examples using partial examples: two create-flow examples and one product search example. | No simple command; the lab intentionally uses partial-example conversion and Studio validation. | Skipped | | No concrete command to run. |
+| `quick-start-api-testing` | Fix two response examples: loosen `decision` with a pattern, then use `dataType: date` and a `VRF-[0-9]{6}` pattern for dynamic fields. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#[$]match(exact: approved)#\$match(pattern: approved\|verified)#' examples/test_finance_user_11.json && sed -i 's#[$]match(exact: VRF-123456)#\$match(pattern: VRF-[0-9]{6})#; s#[$]match(exact: 2026-03-17)#\$match(dataType: date)#' examples/test_support_user_55.json" | PASS | `quick-start-api-testing/examples/test_finance_user_11.json`, `quick-start-api-testing/examples/test_support_user_55.json` | Replaced exact matchers with the intended pattern/date matchers. Corrected the command to preserve literal `$match` in replacements. |
+| `quick-start-async-contract-testing` | Fix `service/processor.py`: change emitted status from `STARTED` to `INITIATED`. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"status\": \"STARTED\"#\"status\": \"INITIATED\"#' service/processor.py" | PASS | `quick-start-async-contract-testing/service/processor.py` | Changed emitted status from `STARTED` to `INITIATED`. |
+| `quick-start-contract-testing` | Fix `service/server.py`: return response field `type` instead of `petType`. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"petType\": \"Golden Retriever\"#\"type\": \"Golden Retriever\"#' service/server.py" | PASS | `quick-start-contract-testing/service/server.py` | Changed response key from `petType` to `type`. |
+| `quick-start-mock` | No file-fix task. The attached learner task is operational: start consumer, observe failure, start mock, observe success, stop mock, then use Studio-generated examples. | No file edit command; this is an operational mock/consumer exercise. | Skipped | | No concrete command to run. |
+| `response-templating` | Fix mock examples under `examples/mock/`: direct-substitute order response fields from request body and add lookup-driven product search responses. | Not simple enough for a safe one-liner; it needs response-template and lookup-driven JSON edits. | Skipped | | No concrete command to run. |
+| `schema-design` | Update `specs/payment-api.yaml`: split `PaymentRequest` into card and bank-transfer schemas, then use `oneOf` with discriminator `paymentType`. | Not simple enough for a safe one-liner; it is a larger OpenAPI schema redesign. | Skipped | | No concrete command to run. |
+| `schema-resiliency-testing` | No bug-fix task. The attached learner task is config exploration: change `schemaResiliencyTests` from `none` to `positiveOnly`, then to `all`, and compare generated test counts. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#schemaResiliencyTests: none#schemaResiliencyTests: positiveOnly#' specmatic.yaml"docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#schemaResiliencyTests: positiveOnly#schemaResiliencyTests: all#' specmatic.yaml" | PASS | `schema-resiliency-testing/specmatic.yaml` | Sequentially changed `schemaResiliencyTests` from `none` to `positiveOnly`, then to `all`. Final diff shows `all`. |
+| `workflow-in-same-spec` | Add workflow ID propagation mapping in `specmatic.yaml` so IDs from `POST /tasks` flow into `GET`, `PUT`, and `DELETE`. | docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ workflow:\n ids:\n \"POST /tasks -> 200\":\n extract: \"BODY.tasks.[0].id\"\n \"GET /tasks/(task_id:string) -> 200\":\n use: \"PATH.task_id\"\n \"PUT /tasks/(task_id:string) -> 200\":\n use: \"PATH.task_id\"\n \"DELETE /tasks/(task_id:string) -> 204\":\n use: \"PATH.task_id\"\n' specmatic.yaml" | PASS | `workflow-in-same-spec/specmatic.yaml` | Added `workflow.ids` mapping under `systemUnderTest.service.runOptions.openapi`. |
diff --git a/api-coverage/README.md b/api-coverage/README.md
index f7a9c5c..43f9cb2 100644
--- a/api-coverage/README.md
+++ b/api-coverage/README.md
@@ -70,7 +70,9 @@ This lab uses FastAPI, which can publish OpenAPI dynamically at `/openapi.json`.
The important point for API coverage is not the specific library. What matters is that the application exposes a current OpenAPI document that Specmatic can fetch using `swaggerUrl`.
-## 1. Baseline run (intentional coverage failure)
+## Lab Implementation Phases
+
+### Baseline Phase
Run:
```shell
@@ -107,7 +109,6 @@ Expected gate failure highlight:
Failed the following API Coverage Report success criteria:
Total API coverage: 50% is less than the specified minimum threshold of 100%.
Total missed operations: 1 is greater than the maximum threshold of 0.
-
```
Clean up:
@@ -127,7 +128,7 @@ Example baseline CTRF HTML report:

-## 2. Fix the checked-in spec
+### Task A: Fix the checked-in spec
Open `specs/service.yaml`.
Find this path:
@@ -146,6 +147,12 @@ Change it to:
Do not change anything else in the operation.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's|/pets/search:|/pets/find:|' specs/service.yaml"
+```
+
## 3. Re-run the tests and coverage check
Run the same command again:
@@ -186,7 +193,8 @@ The HTML report is useful both before and after the fix:
- before the fix, it shows the coverage mismatch in a more readable UI
- after the fix, it confirms both paths are covered
-## Short Studio follow-up
+## Studio verification
+
Start Studio and the provider:
```shell
diff --git a/api-coverage/docker-compose.yaml b/api-coverage/docker-compose.yaml
index 5365dff..5d57c8e 100644
--- a/api-coverage/docker-compose.yaml
+++ b/api-coverage/docker-compose.yaml
@@ -14,7 +14,7 @@ services:
test:
container_name: api-coverage-openapi-test
- image: specmatic/enterprise:latest
+ image: ${SPECMATIC_ENTERPRISE_IMAGE:-specmatic/enterprise:latest}
volumes:
- ./:/usr/src/app
- ../license.txt:/specmatic/specmatic-license.txt:ro
@@ -25,7 +25,7 @@ services:
studio:
container_name: api-coverage-openapi-studio
- image: specmatic/enterprise:latest
+ image: ${SPECMATIC_ENTERPRISE_IMAGE:-specmatic/enterprise:latest}
profiles: ["studio"]
volumes:
- ./:/usr/src/app
diff --git a/api-security-schemes/README.md b/api-security-schemes/README.md
index 6a0e2d1..37f76dd 100644
--- a/api-security-schemes/README.md
+++ b/api-security-schemes/README.md
@@ -132,6 +132,12 @@ Update [`specmatic.yaml`](specmatic.yaml) to valid values again:
Do not change anything else. Fix only the above values.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#INVALID_OAUTH_TOKEN#OAUTH_TOKEN#g; s#dXNlcjppbnZhbGlkcGFzcw==#dXNlcjpwYXNzd29yZA==#g; s#INVALID_APIKEY1234#APIKEY1234#g' specmatic.yaml"
+```
+
## Verify the fix
Re-run:
diff --git a/continuous-integration/README.md b/continuous-integration/README.md
index 0525880..c5aa5b4 100644
--- a/continuous-integration/README.md
+++ b/continuous-integration/README.md
@@ -139,6 +139,12 @@ Keep:
Do not change anything else.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "awk 'BEGIN{done=0} !done && \$0==\" - priority\" {done=1; next} {print}' contracts/order_api.yaml > /tmp/order_api.yaml && mv /tmp/order_api.yaml contracts/order_api.yaml"
+```
+
## Part C: Re-run the CI simulation
Run the same command again:
diff --git a/data-adapters/README.md b/data-adapters/README.md
index 332e537..8613141 100644
--- a/data-adapters/README.md
+++ b/data-adapters/README.md
@@ -94,6 +94,12 @@ Why both hooks are needed:
- `pre_specmatic_request_processor`: adapts PascalCase request fields to camelCase before sending to mock.
- `post_specmatic_response_processor`: adapts camelCase mock response fields back to PascalCase before assertion.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ data:\n adapters:\n pre_specmatic_request_processor: ./hooks/pre_specmatic_request_processor.sh\n post_specmatic_response_processor: ./hooks/post_specmatic_response_processor.sh\n' specmatic.yaml && chmod +x hooks/pre_specmatic_request_processor.sh hooks/post_specmatic_response_processor.sh"
+```
+
## 5. Ensure hook scripts are executable
Run:
diff --git a/dictionary/README.md b/dictionary/README.md
index 86bd0fd..6935d4b 100644
--- a/dictionary/README.md
+++ b/dictionary/README.md
@@ -59,6 +59,13 @@ data:
path: specs/dictionary.yaml
```
+Alternative run command:
+
+```shell
+docker run --rm -v "$PWD:/usr/src/app" specmatic/enterprise examples dictionary --examples-dir examples --spec-file specs/simple-openapi-spec.yaml --out specs/dictionary.yaml
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ data:\n dictionary:\n path: specs/dictionary.yaml\n' specmatic.yaml"
+```
+
## 3. Re-run the suite after configuring dictionary
```shell
diff --git a/kafka-sqs-retry-dlq/README.md b/kafka-sqs-retry-dlq/README.md
index 10db3cb..ca79576 100644
--- a/kafka-sqs-retry-dlq/README.md
+++ b/kafka-sqs-retry-dlq/README.md
@@ -98,6 +98,12 @@ What to look for:
Do not change the contract, examples, or Compose wiring.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's@ # threading.Thread(target=self._run_retry_consumer, name=\"RetryConsumer\", daemon=True),@ threading.Thread(target=self._run_retry_consumer, name=\"RetryConsumer\", daemon=True),@' service/app.py"
+```
+
## Pass criteria
Re-run:
diff --git a/mcp-auto-test/README.md b/mcp-auto-test/README.md
index 6482440..e5aefcb 100644
--- a/mcp-auto-test/README.md
+++ b/mcp-auto-test/README.md
@@ -98,6 +98,12 @@ Make these two fixes:
Do not change anything else.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"damage\": 0.0#\"damaged\": 0.0#; s#order\\[\"shipment\"\\]#order[\"shipmentStatus\"]#' service/order_service.py"
+```
+
## Part C: Re-run tests (expected to pass)
Run:
diff --git a/quick-start-api-testing/README.md b/quick-start-api-testing/README.md
index 6694d33..cc49b36 100644
--- a/quick-start-api-testing/README.md
+++ b/quick-start-api-testing/README.md
@@ -53,7 +53,7 @@ This lab shows how to express each of those expectations with the right matcher
## Lab Implementation Phases
-### Baseline Phase
+## Baseline Phase
Your verification service returns:
- `handledBy`, which is always `verification-service`
- `decision`, which may be `approved` or `verified`
@@ -91,7 +91,7 @@ docker compose down -v
- `test_finance_user_11.json` expects `decision` to be exactly `approved`, but the service may return `approved` or `verified` for that request.
- `test_support_user_55.json` expects one hardcoded date and one exact reference code, but the service generates fresh valid values every time.
-### Intermediate Phase: Task A
+## Task A: Use pattern for flexible decision values
Edit `examples/test_finance_user_11.json`.
In `http-response.body`, change:
@@ -99,6 +99,12 @@ In `http-response.body`, change:
Do not change any other fields.
+Alternative run command for Task A:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#[$]match(exact: approved)#\$match(pattern: approved\|verified)#' examples/test_finance_user_11.json"
+```
+
Re-run:
```shell
@@ -117,7 +123,8 @@ Clean up:
docker compose down -v
```
-### Final Phase
+## Task B: Use dataType and pattern for dynamic values
+
Use `dataType` and `pattern` for dynamic values
Edit `examples/test_support_user_55.json`.
@@ -128,6 +135,12 @@ In `http-response.body`, change:
Keep `handledBy` and `decision` as exact matches.
+Alternative run command for Final Phase:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#[$]match(exact: VRF-123456)#\$match(pattern: VRF-[0-9]{6})#; s#[$]match(exact: 2026-03-17)#\$match(dataType: date)#' examples/test_support_user_55.json"
+```
+
Run:
```shell
diff --git a/quick-start-api-testing/docker-compose.yaml b/quick-start-api-testing/docker-compose.yaml
index 7b1dbaa..0c772a0 100644
--- a/quick-start-api-testing/docker-compose.yaml
+++ b/quick-start-api-testing/docker-compose.yaml
@@ -12,7 +12,7 @@ services:
api-test:
container_name: api-test
- image: specmatic/enterprise:latest
+ image: ${SPECMATIC_ENTERPRISE_IMAGE:-specmatic/enterprise:latest}
volumes:
- ./:/usr/src/app
- ../license.txt:/specmatic/specmatic-license.txt:ro
diff --git a/quick-start-async-contract-testing/README.md b/quick-start-async-contract-testing/README.md
index 5950bbb..1ad506b 100644
--- a/quick-start-async-contract-testing/README.md
+++ b/quick-start-async-contract-testing/README.md
@@ -73,6 +73,12 @@ To:
"status": "INITIATED",
```
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"status\": \"STARTED\"#\"status\": \"INITIATED\"#' service/processor.py"
+```
+
## Pass criteria
Re-run:
diff --git a/quick-start-contract-testing/README.md b/quick-start-contract-testing/README.md
index 90edf76..62cdc7b 100644
--- a/quick-start-contract-testing/README.md
+++ b/quick-start-contract-testing/README.md
@@ -108,6 +108,12 @@ with:
Do not change anything else.
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#\"petType\": \"Golden Retriever\"#\"type\": \"Golden Retriever\"#' service/server.py"
+```
+
## Part C: Re-run tests (expected to pass)
Run:
diff --git a/schema-resiliency-testing/README.md b/schema-resiliency-testing/README.md
index df1e1b2..ae6a732 100644
--- a/schema-resiliency-testing/README.md
+++ b/schema-resiliency-testing/README.md
@@ -69,6 +69,12 @@ The goal of this lab is to try different schema resiliency testing levels and se
### Positive Only Tests
In `specmatic.yaml` change `schemaResiliencyTests: none` to `schemaResiliencyTests: positiveOnly`
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#schemaResiliencyTests: none#schemaResiliencyTests: positiveOnly#' specmatic.yaml"
+```
+
#### Run Positive only Tests
Start docker containers
@@ -92,6 +98,12 @@ docker compose down -v
### Positive and Negative Tests (ALL)
In `specmatic.yaml` change `schemaResiliencyTests: positiveOnly` to `schemaResiliencyTests: all`
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i 's#schemaResiliencyTests: positiveOnly#schemaResiliencyTests: all#' specmatic.yaml"
+```
+
#### Run all Tests
Start docker containers
diff --git a/workflow-in-same-spec/README.md b/workflow-in-same-spec/README.md
index 2f47f9f..1e001aa 100644
--- a/workflow-in-same-spec/README.md
+++ b/workflow-in-same-spec/README.md
@@ -105,6 +105,12 @@ workflow:
use: "PATH.task_id"
```
+Alternative run command:
+
+```shell
+docker run --rm --entrypoint sh -v "$PWD:/work" -w /work specmatic/enterprise -lc "sed -i '/^specmatic:/i\ workflow:\n ids:\n \"POST /tasks -> 200\":\n extract: \"BODY.tasks.[0].id\"\n \"GET /tasks/(task_id:string) -> 200\":\n use: \"PATH.task_id\"\n \"PUT /tasks/(task_id:string) -> 200\":\n use: \"PATH.task_id\"\n \"DELETE /tasks/(task_id:string) -> 204\":\n use: \"PATH.task_id\"\n' specmatic.yaml"
+```
+
Re-run:
```shell