Using ubc in CI/CD

ubc is a standalone binary that exposes the full ubCode engine on the command line. Use it locally as part of a pre-commit hook, in CI/CD pipelines to gate pull requests, or on the console to explore project insights.

ubc features explorative help pages. Add -h/--help to any command to see the available options. Many commands directly show the help page when executed without any options and arguments.

Some notes:

  • ubc check is the canonical project linter. It resolves the whole project and reports what the editor reports — parse and markup lints, toctree and reference problems, duplicate need IDs, and schema issues, for both reStructuredText and Markdown. Pass explicit paths to scope the report to them (the whole project is still resolved), --per-file for a fast, project-free per-file check, or --output-format json for machine-readable output. See Linting on the command line for the full command model.

  • ubc build index runs the same indexing over the whole project, reporting the same diagnostics through the same exit policy, while also building and caching the project index just like ubCode. Therefore it makes sense to keep ubCode and ubc versions in sync.

  • ubc build html renders the static site and applies the same exit policy to what it finds — both its render-phase build.* warnings and the index diagnostics. See Building for production.

A GitHub Actions job

The quality bar lives in ubproject.toml, so the workflow itself stays trivial:

# ubproject.toml
[build.html]
deny = "warning"
name: docs

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Install the `ubc` binary however you distribute it, then:
      - name: Build the site
        run: ubc build html

      # The site is written BEFORE the quality gate decides the exit code,
      # so `if: always()` uploads it even when the build came back red —
      # which is exactly when you want to look at it.
      - name: Upload the site
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: site
          path: _build/html

Reproduce or bypass the bar for a single run with --deny (ubc build html --deny none to build regardless, --deny error to accept warnings), and silence individual findings under Linting rather than lowering the bar for the whole project.

Gate AI-written work

ubc check does not read [workflow], so it passes a project whose workflow still has gaps. To gate what an AI assistant writes, add the agent gates (Experimental, like the rest of Pharaoh):

      - name: Check the agent setup
        run: ubc agent doctor

      - name: Gate new gaps and the reviews
        run: ubc agent release-check --baseline .pharaoh/gap-baseline.json --with-verdicts

ubc agent doctor is the pre-flight. It validates [workflow] against your need types and checks the installed skills, so a broken setup fails with its cause named rather than as a wall of gaps:

$ ubc agent doctor
{
  "ok": false,
  "checks": [
    {
      "name": "config-validity",
      "status": "error",
      "detail": "config-validate reports 1 cross-file inconsistency: type 'specification' referenced by stage 'spec' is not a declared [[needs.types]] directive (stage 'spec')",
      ...

ubc agent release-check --baseline fails on gaps that are not in the baseline file, and on baselined ones that have got worse, so a project can adopt the gate with the gaps it has today. Capture the baseline once, locally, from the gaps you accept, and commit it:

$ ubc agent gaps | jq '[.gaps[] | {key: "\(.id)|\(.category)|\(.link)", value: .have}] | from_entries' > .pharaoh/gap-baseline.json

A missing baseline file counts as empty, so every gap fails. Run the gate with --update-baseline on your machine after fixing gaps: on a passing run it drops the fixed ones from the file, so they cannot come back, and you commit the smaller file. A new need with no trace link then fails the gate:

$ ubc agent release-check --baseline .pharaoh/gap-baseline.json
{
  "ok": false,
  "stage": "gaps",
  "new_gaps": [
    ...
    {
      "id": "SPEC_TIMEOUT",
      "type": "spec",
      "link": "implements",
      "have": 0,
      "need": 1,
      "category": "trace_forward"
    },
    ...
  ],
  ...
  "summary": {
    "total_needs": 7,
    "total_gaps": 7,
    "new_gaps": 3,
    "baselined_debt": 4
  }
}
$ echo $?
1

--with-verdicts adds the review gate, which the baseline does not cover: each need of a stage you mark for review needs a passing verdict that is still current, so editing a reviewed need fails the gate (its verdict is outdated) until it is reviewed again. Commit .pharaoh/verdicts/ so that CI reads the reviews you ran.

Both commands exit non-zero exactly when their check fails, which is all a CI step needs. The JSON on stdout says why; when stdout is empty, the command failed before it had a result, and the reason is on stderr (see the conventions of ubc agent). Without a license, ubc agent commands that read your needs work on projects of up to 100 needs (CI and machines without Internet access covers licensing CI).

In a project that parses Markdown, ubc check also reads the skill files ubc agent install writes; leave them out of the project’s sources.

Every command and option

The Command-line reference lists every ubc command and option, with its exit codes, its maturity and what it does without a license. ubc agent-skill writes the same command tree as a SKILLS.md file for AI agents.