Runtime verification
Let Warpway run approved test profiles in your own GitHub Actions and use the results as evidence.
Runtime verification lets lenses use the results of real test runs as evidence. Warpway runs profiles you approve, in your own GitHub Actions, on the exact commit under review. A model never writes or chooses the commands.
Runtime verification is part of the Team plan and is off until an Admin turns it on for a repository.
How it stays safe
- Only approved profiles run. Profiles come from your configuration:
.warpway.ymlon the base branch, or the same settings in the dashboard. Warpway can dispatch a profile only if it is listed there; nothing in a pull request, a comment or a model's output can add one. - Commands live in your workflow. The workflow file maps each profile to the commands it runs. It sits in your repository and changes through review like any other code.
- Warpway sends four values. The profile's input, the commit to test, a correlation id and the review run id. No command text.
- A pull request cannot change its own verification. Warpway starts the copy of the workflow on the pull request's base branch; the workflow then checks out the commit under review and confirms it got exactly that commit. Pull requests from forks are not verified.
- Results are checked. A result counts only when the run name, the artifact name and
warpway-result.jsonall carry the correlation id of the dispatch, and the file names the dispatched commit and profile. A pass also needs the workflow run itself to succeed with no failed tests. A missing, oversized or inconsistent result is rejected, never treated as a pass. - Failure fails closed. Only a passing run for the exact commit under review satisfies a profile. If a required profile fails, cannot be dispatched, does not report a usable result, or exceeds its timeout, the lenses that need it cannot be completed, and the review does not pass.
Turn it on
- Enable it for the repository. In the dashboard, open the repository's Runtime verification settings. An Admin turns it on.
- Install Warpway Runtime. Warpway needs Actions: write to dispatch the workflow and read its results, and the Warpway app never asks for it. On the same settings page, choose Install Warpway Runtime: a separate GitHub app with only Actions and repository metadata access. Give it the repositories you verify. See GitHub permissions.
- Add the workflow below to your default branch, and to any other branch your pull requests target. GitHub only dispatches workflows that exist on the default branch, and Warpway runs the copy on each pull request's base branch.
- Define profiles in
.warpway.ymland merge it, so the base branch has them.
The workflow
Save this file as .github/workflows/warpway-verify.yml. Edit only the commands: each profile is a WARPWAY_COMMAND_<NAME> entry under Run approved profile plus a matching line in the case statement, which fails every profile it does not list. Everything else is the contract Warpway relies on: the four inputs, a run-name containing the correlation id, the checkout and check of the exact commit, and an artifact named warpway-result-<correlation id> that holds warpway-result.json.
name: Warpway verify
run-name: 'Warpway verify ${{ inputs.profile }} [${{ inputs.correlation_id }}]'
on:
workflow_dispatch:
inputs:
profile:
description: Approved verification profile to run
required: true
type: string
sha:
description: Full commit SHA to check out and verify
required: true
type: string
correlation_id:
description: Warpway correlation id (names the result artifact)
required: true
type: string
review_run_id:
description: Warpway review run id
required: true
type: string
permissions:
contents: read
jobs:
verify:
name: Verify ${{ inputs.profile }}
runs-on: ubuntu-latest
timeout-minutes: 45
defaults:
run:
shell: bash
env:
WARPWAY_PROFILE: ${{ inputs.profile }}
WARPWAY_SHA: ${{ inputs.sha }}
WARPWAY_CORRELATION_ID: ${{ inputs.correlation_id }}
WARPWAY_REVIEW_RUN_ID: ${{ inputs.review_run_id }}
steps:
- name: Validate inputs
id: validate
run: |
fail() { echo "::error::$1"; exit 1; }
[[ "$WARPWAY_SHA" =~ ^[0-9a-f]{40}$ ]] || fail "Input sha must be a full 40-character lowercase commit SHA."
[[ "$WARPWAY_CORRELATION_ID" =~ ^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$ ]] || fail "Input correlation_id is malformed."
[[ "$WARPWAY_PROFILE" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || fail "Input profile is malformed."
[[ "$WARPWAY_REVIEW_RUN_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || fail "Input review_run_id is malformed."
echo "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Check out the requested commit
id: checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
persist-credentials: false
- name: Verify the checked-out commit
id: verify_sha
run: |
actual="$(git rev-parse HEAD)"
if [ "$actual" != "$WARPWAY_SHA" ]; then
echo "::error::The checked-out commit does not match the requested SHA."
exit 1
fi
- name: Run approved profile
id: profile
timeout-minutes: 30
env:
# Approved commands, one per profile. Edit them through a normal pull request. Commands must not
# contain GitHub expressions; read inputs from the WARPWAY_* environment variables instead.
WARPWAY_COMMAND_UNIT: |-
npm ci
npm test
WARPWAY_COMMAND_INTEGRATION: |-
npm ci
npm run test:integration
run: |
case "$WARPWAY_PROFILE" in
unit) profile_command="$WARPWAY_COMMAND_UNIT" ;;
integration) profile_command="$WARPWAY_COMMAND_INTEGRATION" ;;
*)
echo "approved=false" >> "$GITHUB_OUTPUT"
echo "::error::The requested profile is not approved in this workflow."
exit 64
;;
esac
echo "approved=true" >> "$GITHUB_OUTPUT"
export WARPWAY_REPORT="$RUNNER_TEMP/warpway-report.json"
rm -f "$WARPWAY_REPORT"
set +e
bash --noprofile --norc -eo pipefail -c "$profile_command"
status=$?
set -e
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit "$status"
- name: Write warpway-result.json
if: always() && steps.validate.outcome == 'success'
env:
WARPWAY_STARTED_AT: ${{ steps.validate.outputs.started_at }}
WARPWAY_CHECKOUT_OUTCOME: ${{ steps.checkout.outcome }}
WARPWAY_VERIFY_OUTCOME: ${{ steps.verify_sha.outcome }}
WARPWAY_PROFILE_OUTCOME: ${{ steps.profile.outcome }}
WARPWAY_PROFILE_APPROVED: ${{ steps.profile.outputs.approved }}
WARPWAY_EXIT_CODE: ${{ steps.profile.outputs.exit_code }}
run: |
: "${RUNNER_TEMP:?RUNNER_TEMP is not set}"
result_dir="$RUNNER_TEMP/warpway-result"
rm -rf "$result_dir"
mkdir -p "$result_dir"
result="$result_dir/warpway-result.json"
finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
started_at="$WARPWAY_STARTED_AT"
if ! [[ "$started_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then started_at="$finished_at"; fi
exit_code="$WARPWAY_EXIT_CODE"
if ! [[ "$exit_code" =~ ^[0-9]{1,3}$ ]]; then exit_code=""; fi
failures="[]"
if [ "$WARPWAY_CHECKOUT_OUTCOME" != "success" ] || [ "$WARPWAY_VERIFY_OUTCOME" != "success" ]; then
status="error"
summary="Could not check out and verify commit $WARPWAY_SHA."
elif [ "$WARPWAY_PROFILE_APPROVED" != "true" ]; then
status="error"
summary="Profile $WARPWAY_PROFILE is not approved in this workflow."
elif [ "$WARPWAY_PROFILE_OUTCOME" = "success" ] && [ "$exit_code" = "0" ]; then
status="passed"
summary="Profile $WARPWAY_PROFILE passed."
elif [ "$WARPWAY_PROFILE_OUTCOME" = "failure" ] && [ -n "$exit_code" ]; then
status="failed"
summary="Profile $WARPWAY_PROFILE failed with exit code $exit_code."
failures="[{\"name\":\"$WARPWAY_PROFILE\",\"message\":\"Command exited with code $exit_code.\"}]"
else
status="error"
summary="Profile $WARPWAY_PROFILE did not finish (it was cancelled or timed out)."
fi
exit_field=""
if [ -n "$exit_code" ]; then exit_field=",\"exitCode\":$exit_code"; fi
printf '{"schemaVersion":1,"profile":"%s","sha":"%s","correlationId":"%s","reviewRunId":"%s","status":"%s","summary":"%s","tests":{"passed":0,"failed":0,"skipped":0},"failures":%s,"startedAt":"%s","finishedAt":"%s"%s}\n' \
"$WARPWAY_PROFILE" "$WARPWAY_SHA" "$WARPWAY_CORRELATION_ID" "$WARPWAY_REVIEW_RUN_ID" "$status" "$summary" \
"$failures" "$started_at" "$finished_at" "$exit_field" > "$result"
report="$RUNNER_TEMP/warpway-report.json"
if [ -f "$report" ]; then
merge='
def count: if type == "number" and . >= 0 and . < 1000000000 then floor else 0 end;
def text($n): (if type == "string" then . else tostring end) | .[0:$n];
$report[0] as $r
| if ($r | type) != "object" then error("the report must be a JSON object") else . end
| if ($r.tests | type) == "object" then
.tests = { passed: ($r.tests.passed | count), failed: ($r.tests.failed | count), skipped: ($r.tests.skipped | count) }
else . end
| if ($r.failures | type) == "array" and ($r.failures | length) > 0 then
.failures = [ $r.failures[0:50][] | select(type == "object")
| { name: ((.name // "unnamed") | text(200)), message: ((.message // "") | text(2000)) }
+ (if (.file | type) == "string" and (.file | length) > 0 then { file: (.file | text(300)) } else {} end)
+ (if (.line | type) == "number" and .line >= 1 and .line < 100000000 then { line: (.line | floor) } else {} end) ]
else . end
'
if ! command -v jq > /dev/null 2>&1; then
echo "::warning::The test report was ignored because jq is not installed on this runner."
elif [ "$(wc -c < "$report" | tr -d ' ')" -gt 1048576 ]; then
echo "::warning::The test report was ignored because it is larger than 1 MiB."
elif jq -c --slurpfile report "$report" "$merge" "$result" > "$result.tmp" 2> /dev/null; then
mv "$result.tmp" "$result"
else
rm -f "$result.tmp"
echo "::warning::The test report was ignored because it is not a JSON object in the documented format."
fi
fi
echo "Warpway result: $status"
- name: Upload the result
if: always() && steps.validate.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: warpway-result-${{ inputs.correlation_id }}
path: ${{ runner.temp }}/warpway-result/warpway-result.json
retention-days: 7
if-no-files-found: error
overwrite: trueCommands run with bash -eo pipefail in the checkout and can read WARPWAY_PROFILE, WARPWAY_SHA, WARPWAY_CORRELATION_ID and WARPWAY_REVIEW_RUN_ID. They must not contain GitHub expressions (${{ }}). You can add setup steps, such as actions/setup-node, before Run approved profile. The Run approved profile step stops a command after its timeout-minutes (30 in this template).
The repository's Runtime verification settings page shows the same template.
Tip
Read inputs from the WARPWAY_* environment variables, as the template does, rather than writing ${{ inputs.* }} inside run: lines. That keeps an unexpected input from being interpreted as shell code.
Test counts and failures
Without anything extra, the result records whether the profile passed and its exit code. To report test counts and failing tests as well, have the command write a JSON object to the path in WARPWAY_REPORT. The workflow merges it into the result with jq when the runner has it, keeping up to 50 failures, and ignores a report that is larger than 1 MiB or not in this format.
{
"tests": { "passed": 120, "failed": 1, "skipped": 3 },
"failures": [{ "name": "rejects expired tokens", "message": "expected 401", "file": "src/auth.test.ts", "line": 42 }]
}Define profiles
version: 1
runtime:
profiles:
unit:
workflow: warpway-verify.yml
input: unit
timeout_minutes: 20
required_for_lenses:
- testing
integration:
workflow: warpway-verify.yml
input: integration
timeout_minutes: 30
required_for_lenses:
- testing
run_when:
paths:
- services/**| Field | Meaning |
|---|---|
workflow | The workflow file name in .github/workflows. |
input | The value passed as the profile input. It must match a label in the workflow's case statement. |
timeout_minutes | How long the profile may run, 1–180 minutes (default 30). Warpway allows 15 more minutes for queueing, checkout and upload, then treats the run as timed out. Keep it within the workflow step's timeout-minutes. |
required_for_lenses | Lenses that need this profile. Until it passes for the commit under review, they cannot be completed. |
run_when | Require the profile only when the change matches, using the applicability fields. |
A lens can also list profiles it needs with its own runtime_profiles setting.
What happens during a review
- The review plan attaches to each lens the profiles it needs: those the lens lists in
runtime_profiles, and those whoserequired_for_lensesnames the lens and whoserun_whenmatches the change. - Warpway dispatches the workflow on the pull request's base branch with the profile's input, the head commit, a correlation id and the review run id, and records a verification run linked to the review.
- It follows the run through
workflow_runevents (which Warpway Runtime receives) and polling, matching it by the correlation id in the run name. - When the run completes, Warpway downloads the
warpway-result-<correlation id>artifact, checkswarpway-result.jsonagainst the dispatch, and stores the outcome as runtime evidence: the profile, workflow, run link, conclusion and commit, with any test counts and failing tests. Logs are never downloaded. - Lenses use that evidence. Failing tests can back a finding, and a lens that requires the profile cannot be Cleared until it passes; a missing or late result makes the lenses that required it Incomplete.
- If a new commit arrives, runs for the old commit are superseded and cannot affect the new review.
Keep the workflow safe
The workflow runs code from the pull request, like any CI job for pull requests:
- give it
permissions: contents: readand nothing more; - keep secrets out of it; verification should not need production credentials;
- use
persist-credentials: falseon checkout; - pin third-party actions to versions you trust;
- treat the result as evidence about tests, not as approval.
Something unclear or missing? Email marcus@cmglabs.ai.