Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions .github/workflows/trigger-labs-tests.yml
Original file line number Diff line number Diff line change
@@ -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
Loading