Guides

Continuous integration

Two commands catch most contract mistakes before they ship: check, which validates every contract, and the decision tests.

.github/workflows/policy.ymlexample project
name: policy
on: [push, pull_request]

jobs:
  contracts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Validate contracts
        run: node vendor/ulang-lite/bin/ulang-lite.ts check policy/*.ul
      - name: Decision tests
        run: node --test test/*.test.ts

The same two commands, run in the example project when this site was built:

example projectreal output
$ node vendor/ulang-lite/bin/ulang-lite.ts check policy/*.ul
policy/deploy.ul: ok ProductionDeploy (6 clauses; actions deploy, rollback; sha256 686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059)
example projectreal output · timings elided
$ node --test test/*.test.ts
TAP version 13
# Subtest: the reviewed contract is the one being tested
ok 1 - the reviewed contract is the one being tested
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: a green, reviewed deploy is permitted
ok 2 - a green, reviewed deploy is permitted
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: deny: change freeze in production
ok 3 - deny: change freeze in production
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: deny: author approved their own change
ok 4 - deny: author approved their own change
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: deny: error budget exhausted
ok 5 - deny: error budget exhausted
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: deny: tests failing
ok 6 - deny: tests failing
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: deny: one approval
ok 7 - deny: one approval
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: the freeze applies to production only
ok 8 - the freeze applies to production only
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: rollbacks are not blocked by the freeze
ok 9 - rollbacks are not blocked by the freeze
  ---
  duration_ms …
  type: 'test'
  ...
# Subtest: bad facts are rejected, not decided
ok 10 - bad facts are rejected, not decided
  ---
  duration_ms …
  type: 'test'
  ...
1..10
# tests 10
# suites 0
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms …

check exits with status 1 if any file is rejected, and prints each problem as file:line:column: kind[code]: message:

example projectreal output
$ ./ulang-lite check scratch/typo.ul
scratch/typo.ul:29:24: unsupported[unknown-field]: unknown field DeployRequest.error_rate
[exit status 1]