# Hosted Validation Workflow 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](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](ci-pipeline-workflow.md). ## Recommended workflow Use the following commands as the canonical hosted-validation stack: 1. `ota validate --json` for repo contract syntax and structure 1. `ota doctor --json` for repo readiness and actionable findings 1. `ota workspace validate --json` for workspace contract syntax and structure 1. `ota workspace doctor --json` for workspace readiness and per-repo findings 1. `ota workspace explain --json` when you need ordered workspace remediation for blockers 1. `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. ```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. `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= sh` - PowerShell: `$env:OTA_VERSION=''; irm https://dist.ota.run/install.ps1 | iex` - `kind: git_rev` - shell: `curl -fsSL https://dist.ota.run/install.sh | OTA_GIT_REV= sh -s -- --from-git` - PowerShell: `$env:OTA_GIT_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= sh -s -- --from-git` - PowerShell: `$env:OTA_GIT_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: ```yaml - 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. ```yaml agent: bootstrap: ota: source: kind: git_rev rev: 756b2b982e42de1b09a76a6d53c59962a94c2a30 ``` ```yaml - 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 ```bash #!/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. ```yaml 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%`. ```yaml 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 ``` ```yaml 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. ```yaml 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: ```bash #!/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.