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.

Use the following commands as the canonical hosted-validation stack:

  1. ota validate --json for repo contract syntax and structure
  2. ota doctor --json for repo readiness and actionable findings
  3. ota workspace validate --json for workspace contract syntax and structure
  4. ota workspace doctor --json for workspace readiness and per-repo findings
  5. ota workspace explain --json when you need ordered workspace remediation for blockers
  6. ota workspace list --json when 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
  • 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
  • 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

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: false in a JSON payload
  • summary.error_count > 0 in ota validate --json or ota workspace validate --json
  • any error or errors field from a contract-validation command
  • any severity: error finding from doctor or workspace doctor output
  • any severity: error finding from ota workspace explain --json when 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_blocker as the first check summary when present
  • emit one annotation per finding
  • map severity: error to failing annotations or a failed check
  • map severity: warn to non-blocking annotations unless policy says otherwise
  • use summary as the check-run headline
  • use why as the annotation body
  • use next as 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 --json
  • ota doctor --json
  • ota workspace validate --json
  • ota workspace doctor --json
  • ota workspace explain --json
  • ota workspace list --json

That keeps PR gating, local diagnostics, and editor feedback aligned.