Skip to main content
.ankra/pipeline.yaml is the one file an Ankra pipeline is planned from. It sits beside .ankra/ankra.yaml in the repository, and its parser, validator and planner live in exactly one place on the platform, so the dry run you get from ankra pipeline validate is the run.

The document

The parser is lenient about anything that does not change what the pipeline does, and strict about anything that would:
  • Unknown keys are ignored. A single scalar stands in for a one-element list (branches: main), a quoted number reads as a number (retention_days: "30"), and a flag accepts true/false, yes/no, on/off and 1/0. YAML anchors are resolved, so an anchored services or defaults block behaves like an inline one.
  • An empty document, a document that is not YAML, an apiVersion other than ankra.io/v1, a kind other than Pipeline, or a stages block that is not a list is fatal: the pipeline cannot be planned.
  • metadata.name and at least one stage are required.
A pipeline mistakenly committed under .ankra/manifests/ is refused by the manifest gate, because the file carries a kind and an apiVersion and would otherwise be applied to a cluster.

Top-level keys

Stage names must be unique across stages, on_failure and finally, because the three share one run and one output namespace.

Triggers

A manual input has a name (letters, digits and underscores, so it can be addressed as ${{ inputs.<name> }}), a type (string, the default, boolean, number or choice), an optional default, an enum (required for choice, and the default must be one of its values), required and description. A dispatch whose inputs do not match is refused with the planner’s diagnostics and records nothing. A path filter is decided on the event’s changed files, and never excludes a run when the diff could not be read or was truncated - a file the planner could not see is not a file that did not change; the run records that it passed on incomplete information.

Stages

Every stage has a name (lower-case letters, digits, dashes and underscores, starting with a letter or digit - it becomes an expression key, a Job name component and a check-run title) and a kind. The kinds are a closed vocabulary; an unknown kind is fatal rather than a stage the planner skips. A stage of a platform-settled kind is left exactly as the dispatcher found it; a run containing one does not conclude until that kind’s evaluator ships, and ankra pipeline cancel is how to release its concurrency group in the meantime.

Fields common to every stage

Kind-specific blocks

What every step carries

A step’s container mounts the run’s workspace at /workspace and runs with these variables set: ANKRA_RUN_ID, ANKRA_STEP_ID, ANKRA_STEP_KEY, ANKRA_STEP_KIND, ANKRA_ORGANISATION_ID, ANKRA_CLUSTER_ID, ANKRA_REPOSITORY_ID, ANKRA_APPLICATION_ID, ANKRA_HEAD_SHA, ANKRA_REF, ANKRA_IS_FORK and ANKRA_WORKSPACE. ANKRA_OUTPUT names a file on a memory-backed volume: append key=value lines to it (at most 64 KiB) and they are recorded as the step’s outputs when the script exits, whether it succeeded or not. Declared secrets are delivered as files under /run/agent-secrets or as environment variables once secret delivery ships.

Expressions

Any string field may embed expressions between ${{ and }}; the text inside is CEL. Evaluation is deterministic and performs no I/O: everything an expression can reach arrives in its context, and hashFiles is served by the agent, which has the workspace. Functions: hashFiles(pattern, …), contains(haystack, needle) (substring or list membership), startsWith, endsWith, fromJSON, toJSON (compact, keys sorted), format(template, args…) with GitHub-style {0} placeholders, join(list) and join(list, separator), and the status functions always(), success(), failure(), cancelled() and skipped(). The CEL standard library - size, matches, the bounded all/exists/map/filter macros, arithmetic and comparisons - is available on top. Rules worth knowing:
  • A stage condition is compiled at plan time against a schema without steps.*, so a reference to a step that has not run is reported as a diagnostic rather than resolving to nothing; the dispatcher evaluates the same definition with the concluded steps in scope.
  • has() is refused on inputs, matrix, vars, env, steps, needs and secrets: an absent key on those roots resolves to the zero value, so a converted env.DEPLOY == 'true' keeps its meaning when the variable is unset. Compare the value instead. has() still works on ankra and event, whose paths are fixed.
  • Converted GitHub expressions keep working - single-quoted strings, ==, !=, &&, ||, !, the functions above, and the steps, matrix, inputs, env and needs references map one to one. A reference to the github, runner, job or strategy context is a diagnostic naming the Ankra equivalent (github.sha is ankra.sha, github.event_name is event.kind). Two deliberate departures: toJSON emits compact JSON, and comparisons follow the typed rules of CEL, so '1' == 1 is a type error rather than true.
  • A template is at most 8,192 bytes with at most 64 expressions; an expression is at most 2,048 bytes and 24 levels deep; the evaluation cost is capped. Each bound is refused with a fixed sentence at validation time.

Triggers and reasons

Every event that reaches the trigger is decided in order, and the answer is recorded as one of these tokens. Where a run row is written, ankra pipeline list shows it; where none is, the reason is in the platform’s logs and metrics. Dispatches (manual, api, agent, rerun) are exempt from the recent-run deduplication: a person who presses run twice asked for two runs. Inside a run, a stage that is not planned carries a skip reason: event_filter, branch_filter, path_filter, schedule_subset, condition_false, condition_unreadable (a condition nobody can read is not a condition that passed), fork_policy or dependency_skipped. A run refused as a whole carries one fixed sentence, for example “Every stage this pipeline declares is filtered out for this event, so the run would do nothing.” or “A stage fans out over more matrix legs than the number one run may plan.”

Validation

The command reads .ankra/pipeline.yaml (or the file you name), validates it, and plans it for a synthetic push to the default branch and a synthetic pull request, printing for each the steps that would run, the stages that would be skipped with their reason, and every diagnostic. Its exit code is non-zero when the severity is fatal. Without a file and without a stored definition there is nothing to validate. The same validator answers POST …/pipeline/validate on the API and the cicd_validate_pipeline_yaml tool in chat, which needs no repository at all. A result has a severity - ok, warn (the pipeline runs, but not the way its author probably meant; reported on the pull request, never blocking) or fatal (the pipeline cannot be planned) - and a list of violations, each prefixed with a fixed key: The planner adds diagnostics with plan_ keys (plan_trigger:, plan_paths:, plan_matrix:, plan_condition:, plan_fork:, plan_concurrency:, plan_interpolation:, plan_timeout:, plan_needs:, plan_spec:), and the step renderer records what it decided on incomplete information: image_policy_open when the organisation set no image policy, resources_defaulted, service_without_readiness, service_undeclared, build_without_registry_auth, registry_push_withheld, build_namespace_workspace and multi_platform_build. A step the renderer refuses outright - an image outside the policy, more compute than one step may have, a missing timeout, a services tier without services, host privilege - is a fixed sentence naming the field, and the same refusal at dispatch time is what a step that never started shows.

A complete example

This is the pipeline Ankra’s own documentation repository is written against - a bare repository with nothing to build, which is the case that proves pipelines are repository-scoped with an optional application link.
Reading it against what executes today: the four run stages execute in the cluster; checkout and the verify probe are dispatched but wait on runner images that have not shipped; the agent triage and the webhook notification are platform-settled kinds that stay pending. The schedule is stored but not fired. The definition validates as ok today, which is the point of the closed vocabulary: the file describes the pipeline the platform will run, and says nothing it will not do.

Ankra Pipelines

How a run happens, the security model, failure handling and limits.

Migrate from GitHub Actions

Convert an existing definition and review the notes.