Hosted Validation Workflow
August 26, 2026 · View on GitHub
This document defines the V7 hosted-validation shape for using ota in CI and pull-request gating without mutating repo state.
The goal is to keep validation deterministic, non-mutating, and easy to consume by hosted systems.
Purpose
Hosted validation should:
- prove contract correctness
- surface readiness blockers early
- stay read-only
- emit stable JSON for automation
- avoid hidden repo or workspace mutation
Official GitHub Actions wrapper
If the CI runner is GitHub Actions, prefer ota-run/action@v1 when you want a read-only
readiness receipt, annotations, pull-request comments, and uploaded receipt artifacts without
writing your own JSON adapter.
The action is the GitHub-native wrapper around ota JSON. It does not replace direct ota up,
ota run, or workspace execution steps.
See github-action-workflow.md for the canonical action contract. For provider-neutral runners such as GitLab CI, Jenkins, or CircleCI, see ci-pipeline-workflow.md.
Recommended workflow
Use the following commands as the canonical hosted-validation stack:
ota validate --jsonfor repo contract syntax and structureota doctor --jsonfor repo readiness and actionable findingsota workspace validate --jsonfor workspace contract syntax and structureota workspace doctor --jsonfor workspace readiness and per-repo findingsota workspace explain --jsonwhen you need ordered workspace remediation for blockersota workspace list --jsonwhen you want lightweight workspace inventory and readiness
For workspace inventory and readiness summaries, ota workspace list --json can be used as a
lightweight preflight signal. For ticketing or automated follow-up, ota workspace explain --json
gives an ordered plan without mutating state.
Contract-owned GitHub Actions install
If the repo already declares agent.bootstrap.ota.source, prefer ota-run/setup@v1 in
source: contract mode instead of restating install truth in workflow YAML.
- uses: actions/checkout@v6
- uses: ota-run/setup@v1
with:
source: contract
That keeps ota.yaml as the single install source of truth for both agent bootstrap and GitHub
Actions jobs that later run direct ota commands, while keeping GitHub install behavior on the
single public ota-run/setup action surface.
When the contract declares metadata.ota.minimum_version, a released
agent.bootstrap.ota.source.version must meet that floor before CI can consume it. This prevents
one repository from declaring a newer contract surface while its contract-owned GitHub install
still selects an older release. Git revisions and named pressure branches are intentionally not
compared to a release floor.
ota doctor now warns when workflows restate ota install truth through explicit installer
commands, explicit ota-run/setup / ota-run/action mode, or source-install helpers while the
repo already declares agent.bootstrap.ota.source. Treat that as install-governance drift and
move the job back onto source: contract unless the workflow is an intentional unreleased
pressure lane.
The contract-owned bootstrap mapping is the same one agents should use outside GitHub Actions:
kind: version- shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_VERSION=<version> sh - PowerShell:
$env:OTA_VERSION='<version>'; irm https://dist.ota.run/install.ps1 | iex
- shell:
kind: git_rev- shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_GIT_REV=<rev> sh -s -- --from-git - PowerShell:
$env:OTA_GIT_REV='<rev>'; & ([scriptblock]::Create((irm https://dist.ota.run/install.ps1))) -FromGit
- shell:
kind: branch- shell:
curl -fsSL https://dist.ota.run/install.sh | OTA_GIT_BRANCH=<branch> sh -s -- --from-git - PowerShell:
$env:OTA_GIT_BRANCH='<branch>'; & ([scriptblock]::Create((irm https://dist.ota.run/install.ps1))) -FromGit
- shell:
When a GitHub Actions workflow intentionally wants the reporting wrapper to own installation too,
ota-run/action@v1 can consume the same repo truth directly instead of restating version or
source-install inputs:
- uses: actions/checkout@v6
- uses: ota-run/action@v1
with:
command: receipt
source: contract
contract-path: ota.yaml
Prefer ota-run/setup@v1 plus ota-run/action@v1 install: never as the steady-state split when
later steps also need direct ota commands. Use standalone ota-run/action@v1 source: contract
when the job only needs the GitHub-native wrapper.
Testing unreleased ota builds
When a case-study matrix or maintainer branch needs to test an unreleased ota change, keep that
truth in agent.bootstrap.ota.source and let the contract-owned install action consume it.
agent:
bootstrap:
ota:
source:
kind: git_rev
rev: 756b2b982e42de1b09a76a6d53c59962a94c2a30
- uses: actions/checkout@v6
- uses: ota-run/setup@v1
with:
source: contract
Use a workflow-owned explicit ota-run/setup@v1 input or other maintainer-specific source-install
lane only when the workflow intentionally needs install truth outside repo-owned contract truth.
Infrastructure boundary
ota does not replace the CI runner or its service provisioning layer.
For example, if your GitHub Actions job uses a Postgres service container, GitHub Actions still starts that container. ota removes the repo-specific duplication above it:
- contract validation
- readiness diagnosis
- task execution
- env and service intent declared once in
ota.yaml
That means the CI workflow stays thin, while the repo contract carries the real requirements.
Gating rules
Hosted validation should treat the following as failures:
ok: falsein a JSON payloadsummary.error_count > 0inota validate --jsonorota workspace validate --json- any
errororerrorsfield from a contract-validation command - any
severity: errorfinding fromdoctoror workspace doctor output - any
severity: errorfinding fromota workspace explain --jsonwhen the plan is being used as a gate - non-zero process exit when the command is expected to validate successfully
Warnings should be surfaced to humans, but they do not necessarily fail the gate unless policy requires it.
What hosted validation should not do
Hosted validation must not:
- run
ota init - run
ota detect --write - run
ota workspace init --bootstrap - mutate repo or workspace contracts as part of the validation step
- infer execution behavior from human-readable text output
Example CI flow
#!/usr/bin/env bash
set -euo pipefail
ota validate --json | tee .ota-validate.json
ota doctor --json | tee .ota-doctor.json
ota workspace validate --json | tee .ota-workspace-validate.json
ota workspace doctor --json | tee .ota-workspace-doctor.json
ota workspace list --json | tee .ota-workspace-list.json
ota workspace explain --json | tee .ota-workspace-explain.json
Example with a Postgres service container
GitHub Actions still provisions the database container; ota removes the duplicate repo setup above it.
name: ci
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v5
- name: Install ota
run: curl -fsSL https://dist.ota.run/install.sh | sh
- name: Validate contract
run: ota validate
- name: Diagnose readiness
run: ota doctor --json
- name: Prepare repo
run: ota up
- name: Run lint
run: ota run lint
- name: Run tests
run: ota run test
Example with ota-provisioned Postgres
In this shape, the repo contract owns the database service and the CI job stays thin.
The repo is expected to carry the matching Compose definition; ota only models and runs the
service boundary declared in ota.yaml.
In GitHub Actions, prefer the plain installer command without extra path glue. The hosted install
scripts now export the resolved ota bin directory to GITHUB_PATH automatically, including the
Windows install path, so workflows do not need to guess ~/.local/bin versus %LOCALAPPDATA%.
version: 1
project:
name: app
execution:
default_context: host
contexts:
host:
backend: native
services:
postgres:
required: true
manager:
kind: compose
name: local
file: compose.yaml
service: postgres
endpoints:
host:
address: 127.0.0.1
port: 5432
readiness:
from: host
kind: tcp
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
env:
vars:
DATABASE_URL:
required: true
tasks:
lint:
run: npm run lint
test:
run: npm test
name: ci
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install ota
run: curl -fsSL https://dist.ota.run/install.sh | sh
- name: Validate contract
run: ota validate
- name: Prepare repo
run: ota up
- name: Run lint
run: ota run lint
- name: Run tests
run: ota run test
In this model, ota starts and validates the Compose service declared in ota.yaml; the runner
does not duplicate the Postgres setup.
Example PR policy:
- fail the job on any
ok: false - fail the job on any
severity: error - post warnings as annotations
- keep JSON artifacts for traceability
Example with the official GitHub Action
Use this when the runner is GitHub Actions and you want step summaries, annotations, a sticky pull-request comment, and an archived receipt without writing adapter glue in the workflow.
name: readiness
on:
pull_request:
push:
permissions:
actions: read
contents: read
pull-requests: write
jobs:
readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Publish ota readiness
uses: ota-run/action@v1
with:
command: receipt
archive: true
fail-on-new-blockers: true
github-token: ${{ github.token }}
If a later job needs to run ota directly, install ota in that job or run ota-run/action@v1
there as well. Job-level PATH changes do not cross into a different job.
CI and PR annotation delivery
The annotation layer should be a thin consumer of JSON, not a second diagnosis engine.
Recommended mapping:
- use
summary.primary_blockeras the first check summary when present - emit one annotation per finding
- map
severity: errorto failing annotations or a failed check - map
severity: warnto non-blocking annotations unless policy says otherwise - use
summaryas the check-run headline - use
whyas the annotation body - use
nextas the suggested fix or link target
For workspace commands, keep the same mapping but scope annotations to the repo name and path in the workspace payload. That keeps PR feedback aligned with the same JSON fields used by local editor integrations.
Portable adapter:
ota annotations is the canonical JSON-to-CI adapter. It turns ota JSON into either plain CI log
lines, GitHub Actions annotations, or compact markdown summaries. Use --format plain when you
want a provider-neutral output stream, --format github when the CI platform understands
annotation syntax, or --format markdown when you need a step summary or pull-request comment.
For repo-local usage, the canonical entrypoint is ota run doctor-annotations, which now shells
out to ota annotations under the hood with the --render-format input set to plain or
github.
Example shell adapter for GitHub Actions:
#!/usr/bin/env bash
set -euo pipefail
ota doctor --json | ota annotations --mode doctor --format github --input -
ota workspace doctor --json | ota annotations --mode workspace-doctor --format github --input -
ota doctor --json | ota annotations --mode doctor --format markdown --input - >> "$GITHUB_STEP_SUMMARY"
ota receipt --json --baseline .ota/receipts/latest.json . \
| ota annotations --mode receipt-diff --format markdown --input - >> "$GITHUB_STEP_SUMMARY"
Any GitHub-specific wrapper should reuse those same ota annotations entrypoints instead of
rebuilding summary or comment text from raw JSON fields.
Editor and hosted validation overlap
Hosted validation systems and editor integrations should consume the same JSON shapes:
ota validate --jsonota doctor --jsonota workspace validate --jsonota workspace doctor --jsonota workspace explain --jsonota workspace list --json
That keeps PR gating, local diagnostics, and editor feedback aligned.