GitOps with liya

Export agents, providers, guardrails, policies and triggers as YAML, review changes as a diff, and apply them from CI.

liya export writes an instance's configuration as YAML files you can commit. liya apply makes an instance match those files, and --dry-run shows you the diff first.

Export

liya export -o infra/

This writes one file per resource:

infra/agents/<name>.yaml       kind: AgentBundle   the server's own agent bundle
infra/providers/<name>.yaml    kind: Provider      never an API key or other secret value
infra/guardrails/<name>.yaml   kind: Guardrail
infra/policies/<name>.yaml     kind: Policy        Cedar
infra/triggers/<name>.yaml     kind: Trigger

Every document has the same shape:

apiVersion: ticketiq.io/v1
kind: Trigger
metadata:
  name: nightly-digest
spec:
  agent: copilot
  kind: cron
  schedule: "0 2 * * *"
  timezone: Europe/London
  input: Summarise yesterday's tickets

Each spec holds exactly the fields that the server's own request schema accepts for that resource, which liya reads from the server's /openapi.json. Timestamps, etags and other server-managed fields are left out, so two exports diff cleanly. Fields that hold secret values (api_key, …secret, …password, …token, credentials_json) are never exported. A trigger's kind is the server's name for it: cron, ticket, webhook, email, teams, slack or google_chat. Fields that point to a stored secret (…_ref) are kept.

To export only some kinds, pass --kind Agent --kind Policy.

Plan, then apply

liya apply -f infra/ --dry-run      # a unified diff per resource; exit 5 if anything would change
liya apply -f infra/                # make it so
liya apply -f infra/triggers/nightly-digest.yaml

-f takes a single file or a directory. A directory is read recursively for *.yaml, *.yml and *.json, and multi-document YAML files work. Resources are applied in dependency order: providers, guardrails, policies, agents, then triggers.

  • Agents go through POST /api/agents/apply, in one transaction with every gate the console applies. A dry run shows the server's own plan: which sections would change, or why it would be refused.
  • Other kinds are compared with what is on the server over every field the PUT writes, and then written. A PUT replaces the whole record, so a field you leave out of the file is reset to its default: the diff shows it as a change rather than calling the resource unchanged. Fields the server manages (timestamps, etags) and secret values are never compared. A resource that already matches is reported as unchanged and not written.
  • If a change weakens a control, the server asks for a reason. Pass it with --reason "…".

A file that contains a secret value is refused before anything is sent. Store the secret with liya secrets set and refer to it with a …_ref field.

If the connected server has no endpoint for a kind, that kind is reported as not supported by this server version and skipped. The rest of the run continues.

In CI

- name: Plan
  run: liya apply -f infra/ --dry-run      # exit 5 = changes pending, 2 = invalid, 3 = auth, 1 = other
  env:
    LIYA_URL: https://ticketiq.example.com
    LIYA_TOKEN: ${{ secrets.LIYA_SERVICE_ACCOUNT }}   # svc/<name>:<secret>
- name: Apply
  if: github.ref == 'refs/heads/main'
  run: liya apply -f infra/ --reason "merged ${{ github.sha }}"

The tiq bundle commands (plan, apply, delete -f) still work through liya for pipelines that already use them. See examples/ci/ticketiq.yml.