Ankra Pipelines runs a repository’s CI as Kubernetes Jobs inside a cluster you already operate through Ankra. One file,
.ankra/pipeline.yaml, declares the triggers and the stages; a push, a pull request or a tag starts a run, every in-cluster stage runs as a hardened Job in the ankra-ci namespace of your CI cluster, and the result lands as an Ankra pipeline check on the commit and a run you can follow with ankra pipeline logs --follow. There are no GitHub-hosted or self-hosted runners to keep alive.Before your first run
The first four are preconditions, not preferences, and the failure modes differ: three of them stop a run before anything is dispatched, and one lets it run and then quietly strip every protected section out of it. The two vault entries at the end are not preconditions at all - they are where a run’s logs and reports go if you do nothing, and why you might want somewhere else.- A connected repository. The repository is connected through the Ankra GitHub App, GitLab, or Bitbucket Cloud, so Ankra receives its webhooks - and then connected to Ankra Pipelines itself, which is self-service: automatically when it backs an Ankra Application, or with one call for a bare repository (below). Pipelines are repository-scoped with an optional link to an Application; a repository with nothing to build can have a pipeline too. A Bitbucket Cloud repository gets its webhook when you connect it to Ankra Pipelines with a Bitbucket credential, and its pull requests start runs only once that credential grants Bitbucket’s
pullrequestscope - see Granting the pull request permission. - A CI cluster. An organisation administrator sets
ci_cluster_idin the organisation’s CI settings. Without one, the staging cluster the organisation nominated for AI work is used; with neither, a provider event recordsno_ci_clusterand nothing runs. - An agent that advertises pipeline steps. The CI cluster’s agent runs pipeline steps in a third scheduler that is off by default -
ci_worker_count(AGENT_CI_WORKER_COUNT) is0on every cluster until someone raises it. An agent with the scheduler off does not advertise the pipeline job names at all, so the platform refuses a step for that cluster up front instead of dispatching it. Set the count through Ankra, per cluster, and Ankra re-renders that agent’s own release; neverhelm upgradethe agent by hand (below). - An approved definition. A pipeline’s protected sections - its
secrets,credentials,permissions, network tier, placement, resources, timeout, caches,servicesand agatestage’swith:- carry no authority until an organisation administrator approves the definition on the default branch. Until one does, a run still executes, with none of them: the check run and the pull request comment carry “No administrator has approved any authority for this repository yet, so this run executes with none.” Approve before the first run that needs any of it (below). - For check runs,
checks:write. Without it the GitHub App cannot create a check run, and the run’s status is carried by a comment on the pull request instead - the comment says so and names the permission. - Somewhere to keep the objects - and you already have it. A
scanstage’s reports, the findings agatereads, the complete step log and every declared artifact are objects, and they need a bucket. An organisation with no ready backup vault of its own is lent one: Ankra keeps them in a vault the platform owns, under a key prefix of your organisation’s own, with the same retention, the same per-object download links and the same reaper. Nothing to provision before a first run. - A vault of your own, when where the objects live matters. Register one for data residency, for a bucket only your own credentials reach, or simply because cluster backups, restore points and
ankra migratedata need one - those never use the platform’s vault. A ready vault of your own takes precedence automatically, from the next step onward (below).
Connect a repository, push, see the run
1
Connect the repository
Two paths, both self-service:
-
An Ankra Application. Creating or reconciling an Application connects its repository and decides its pipeline automatically. For a genuinely new application, the setup pull request now commits
.ankra/pipeline.yamlinstead of a generated GitHub Actions workflow - unless the repository already carries an Ankra-authored workflow (which keeps healing rather than switching underneath you) or you asked forgithub_actionsexplicitly. A monorepo gets one file with each component’s stages suffixed by its name, not one workflow per app. Ankra AI’s proposal tool on the MCP server takes the same choice astarget: "ankra_pipeline"or"github_actions". -
A bare repository - nothing to build, or you’d rather write the file yourself:
Needs
pipelines.manage. For a GitHub repository, Ankra reads.ankra/pipeline.yamlatdefault_branchitself, through the organisation’s stored Git credential, and stores a valid file as the definition of record in the same call - connect a repository that already commits one and the first webhook after connecting has something to run. GitLab and Bitbucket Cloud repositories connect the same way but needankra pipeline definition putafterwards (step three, below): reading the committed file at connect time is GitHub-only today.default_branchdefaults tomain;credential_nameis optional for a public repository. Connecting a repository the organisation already connected answers409with the existing repository’s id, not a second row. For a Bitbucket Cloud repository, send"provider": "bitbucket_cloud", the workspace asowner, the repository slug asname, and the Bitbucket credential ascredential_name, public repository or not: once the connect commits, Ankra registers the repository’s webhook with that credential, unless the organisation already has one on it. Connected without a credential, it gets no webhook from the connect, so unless the organisation already has one on the repository, no push, tag or pull request reaches its pipeline. Two more fields on the same call, both optional and both addressed by id rather than name:application_idlinks the repository to an existing Ankra Application without changing what that application deploys, andcluster_idoverrides the organisation’s CI cluster for this repository alone, instead of the organisation-wideci_cluster_id(below). Disconnect a repository withDELETE /org/pipelines/repositories/{id}-pipelines.manage,204on success,409while a run is still live; it is a soft delete, so reconnecting the same identity later revives the row rather than creating a second one.
GET /org/pipelines/repositories lists what is connected, keyset paginated; GET /org/pipelines/repositories/{id} reads one. The CLI does the same with ankra pipeline repositories connect|list|get|disconnect (CLI v0.15.0 or later), and the portal’s Settings → Pipelines page lists connected repositories.2
Optionally, give the organisation a vault of its own
Every report a Every argument is an override: Either way the vault has to reach
scan stage writes, every finding a gate reads, every step log archive and every declared artifact is a presigned upload into the organisation’s oldest ready backup vault - or, for an organisation that has none, into the Ankra-managed default vault. So this step is optional: skip it and the first run still keeps its logs and its reports. Do it when the objects should live in a bucket you own, or when you want cluster backups and ankra migrate as well, which never use the platform’s vault. One vault serves the whole organisation, and one it already uses for cluster backups serves pipelines too - if ankra backup vaults list already shows a ready row, this step is done.Let Ankra create the bucket from a Hetzner, UpCloud, DigitalOcean or Scaleway credential:ankra backup vaults provision on its own names the vault, picks the only credential it can provision from and takes that provider’s usual region, printing all three before it creates anything. A Hetzner credential is the one exception - Hetzner issues Object Storage keys in its Cloud Console (Object Storage > Manage credentials) and its API cannot mint them, so the command prompts for the pair.Or register a bucket you already run, on any S3-compatible service:ready before it counts: a vault still provisioning, or one whose last verification failed, has no bucket a presigned URL would reach, so the platform treats it as absent and keeps using its own default vault. Registering a vault changes nothing about what is already stored - objects stay where they were written, and are still downloadable and still reaped; the next step’s objects go to yours. ankra backup vaults get ci-artifacts shows the status, and Backups covers the rest.3
Choose the cluster that runs pipelines, and turn on its agent's pipeline-step workers
An organisation administrator sets the CI cluster. Any member can read the settings; changing them answers The same request takes the rest of the CI settings - see Organisation CI settings. Leave This is a setting Ankra owns on the agent’s behalf, not a chart value you apply yourself: the write is stored, and Ankra hands it to the agent through the agent’s own upgrade lane so the running release re-renders with it. An online agent new enough to take chart values picks it up immediately, an older one at its next upgrade, and an offline one when it reconnects - the response says which. Never
403 for everyone else.ci_cluster_id unset and the organisation’s AI staging cluster is used instead.Then raise that cluster’s pipeline-step worker count above zero - the number of pipeline steps its agent runs at once, in slots separate from its read and write pools. It is 0 on every cluster until you change it, and an agent at zero does not advertise the pipeline job names at all, so a step for that cluster is refused before it is dispatched.helm upgrade the agent by hand to set it: Ankra re-renders that release, so a hand-set value is overwritten by the next reconcile.ci_storage_class on the same call names the StorageClass for cache volumes and for a workspace claim that names none; leave it empty for the cluster default. The agent creates the ankra-ci and ankra-ci-build namespaces on first use, with their Pod Security admission labels, a ResourceQuota and a LimitRange. See the agent Helm values for what the release carries.4
Write and validate .ankra/pipeline.yaml
This is the shape Ankra’s own setup pull request commits for a new Go application - reuse it as a starting point. Every stage below now executes (see What is in place today): The Reads
checkout clones the commit, test runs your script, build produces a real digest in a rootless BuildKit pod, scan runs Trivy, Semgrep and Checkov against that digest and your source, gate judges what they found against your organisation’s policy, and publish re-tags the one digest gate approved into your real repository. The image is never rebuilt and never a mutable tag until that last step - the same digest the whole way from build to the registry your application deploys from.fail_on map above reads per scanner and only ever tightens the gate: semgrep: "error" raises Semgrep’s own floor to error, while checkov: "none" and trivy: "app" are not severities in those tools’ vocabularies, so they carry no floor at all and leave the organisation’s ci_image_gate policy to decide. Nothing a pull request writes here can loosen that policy.Ask Ankra AI to validate it - in the portal chat or through the MCP server, the cicd_validate_pipeline_yaml tool answers with a severity (ok, warn or fatal) and every violation with its key. A fatal definition cannot be planned; fix it before committing. A warn is worth reading rather than skipping past: a stage whose kind has no executor on this build reports one, and that stage will be skipped..ankra/pipeline.yaml from the current directory (or the file you name), plans it for a synthetic push and a synthetic pull request, and prints the steps that would run, the stages that would be skipped and why, and every diagnostic - no repository connection required for this step alone. --repository <repository-id> validates for a bare repository instead of --application.5
Store the definition, commit and push
definition put needs pipelines.manage and stores the definition server-side; it does not touch the repository. On GitHub, a run no longer needs that stored copy to be current: the planner fetches .ankra/pipeline.yaml at the run’s own head commit and plans the run’s logic from it, so editing the file in a pull request takes effect on that pull request’s next run. On GitLab and Bitbucket Cloud the stored definition is still what runs, so keep it identical to the committed file there. Either way the run’s authority - the protected sections - still comes from the default branch’s approved definition, never from the head file (see Logic is open, authority is closed).6
Approve the definition's protected authority
An organisation administrator does this once per definition, and again whenever a protected section changes on the default branch. The id to approve is the repository’s current default-branch definition, and the run’s own status is where it is named: the check run summary and the pull request status comment carry it inside the approve command they suggest.
ankra pipeline get <run> reports the authority state and says where to read the id, deliberately without guessing one - the definition a run drew its current authority from is rarely the one waiting to be approved, and approving that would answer 409. With the id in hand, read what you are about to grant, then approve it:definitions get prints the definition’s protected hash and whether it is the approved one; when it is not, it names the exact approve command to run. approve needs pipelines.manage and a human actor - a service token or agent token is refused, because approving grants whatever permissions, credentials, secrets scope and network tier the definition declares. It confirms first; --yes skips the prompt for scripted use. Only the repository’s current default-branch definition can be approved, and only once: a definition the default branch has since moved past, or one already approved, answers 409. The same act is POST /api/v1/org/pipelines/definitions/{definition_id}/approve on the API. In the portal, an administrator approves from Settings → Pipelines → Pending approvals or from the run page, and an administrator whose GitHub account is linked to their Ankra profile can press Approve authority on the GitHub check run. Approving re-runs the runs that waited for it.Skipping this does not stop a run - it strips it. A pipeline whose default-branch definition declares any protected section executes with none of them until an administrator approves, and the check run and pull request comment say so verbatim: “No administrator has approved any authority for this repository yet, so this run executes with none.” A definition that declares no protected section at all needs no approval, because there is nothing to grant.7
Watch the run
The push you just made triggers a run on a connected repository. To dispatch one by hand, name the commit - and it has to be one of the repository’s own. Ankra resolves Run from inside the checkout, the CLI reads what it can see: the application is the one bound to the repository your
--ref (or the repository’s default branch when you name none) through the repository’s own host before writing anything, and accepts your --sha only when it is that reference’s current tip or - on GitHub, through a compare read - a provable ancestor of it. Anything else is refused by name, and so is a question the host could not answer at all. GitLab and Bitbucket Cloud have no compare read, so a commit you name there has to be the exact tip.origin remote points at, and the commit is your HEAD. It says which it used. Name them yourself with --application and --sha when you are outside a checkout, when the repository has more than one application, or when you mean a commit other than the one in front of you:--wait follows the run to its conclusion and exits non-zero on anything but success. Every stage runs for real: checkout clones the commit, test runs your script, build produces a digest in a rootless BuildKit pod, scan runs Trivy, Semgrep and Checkov against it, gate judges what they found, and publish re-tags the digest gate approved to sha-<7> in your real repository - nothing is published that was not scanned and gated first. Because --wait already blocked until the run concluded, the logs --step checkout --follow command above is reading checkout’s log back after the fact, not watching it live - from the CLI’s next release that reads the step’s complete, archived output; today it reads only whatever a fresh connection to the live relay happens to catch from an already-finished step, which for a step this far in the past is usually nothing. --follow earns its name against a step that is still running, which is the case whenever you check on a pipeline without also passing --wait. On GitHub the commit carries an Ankra pipeline check with a step table and a Cancel button, and on a pull request the same run populates the pipeline section of Ankra’s one status comment; the portal’s Pipelines section shows the same run with a timeline and an in-app log viewer, and Settings → Pipelines lists the repository you connected in step one.8
Wait on a run a push or pull request started
Most runs are not dispatched by you: the push or pull request webhook starts them, so a CI job that has to wait for the result holds no run id. With
ankra pipeline get selects the run by what the job does know instead - --head-sha (the full sha), --branch and --trigger narrow it, and --latest takes the newest match (on its own, the pipeline’s newest run). Several matches without --latest are refused and listed, exit 2; no match is exit 3.--wait blocks until the run concludes, then prints its final detail, exactly as pipeline run --wait does. The webhook can land a moment after the job asks, so under --wait or --watch a selection that matches nothing yet waits for the run to appear, up to --timeout or ten minutes when none is set. --timeout takes a duration and bounds pipeline run --wait and pipeline rerun --wait the same way. On GitHub Actions, a pull_request job’s checkout is a merge commit rather than the head the run was started for, so pass ${{ github.event.pull_request.head.sha }} there instead of git rev-parse HEAD.To react while the run moves instead of only when it ends, --watch prints each run and step state change until it concludes:-o json that is one JSON object per line: event (run or step), observed_at, pipeline_run_id, run_number, step_key, status, previous_status (null the first time a run or step is seen), outcome, and the whole run or step object. The first lines are the full current state, and the run’s conclusion is always the last line.With --wait or --watch the exit code is the verdict: 0 when the run succeeded, 1 when it concluded any other way or the platform could not be read, and 5 when --timeout ran out. A plain get still exits 0 whatever the outcome; add --exit-code to a single read to branch on it without waiting - 0 or 1 for a concluded run, 5 while it has not concluded yet.How a run happens
- Trigger. A verified delivery from GitHub, GitLab or Bitbucket Cloud - a branch push, a pull request opened, synchronised or reopened, or a tag - is normalised into one event and decided in a fixed order: is the repository connected, is the head trusted, is there a cluster to run on, is there a definition, does the definition’s
on:block accept the event. A dispatch from the CLI, the API or an agent goes through the same decision, after one of its own: a delivery’s commit arrives proven by the provider, a dispatch’s does not, so the ref it names is resolved against the repository’s own host and a caller-suppliedhead_shais accepted only as that reference’s tip or a provable ancestor of it. Every answer is recorded, so a push that started nothing still says why (the reason tokens). - Plan. The planner first decides which definition it is planning: on GitHub it fetches
.ankra/pipeline.yamlat the run’s own head commit and uses that for the run’s logic, falling back to the stored definition (with a diagnostic) if the read fails, and recording which source won; on GitLab and Bitbucket Cloud the stored definition is always what runs. A dispatch that carried its own definition (--spec-file,spec_yaml) outranks both: the override is stored verbatim on the run, and the planner and the dispatcher both render from it. Protected authority is resolved separately and never from the head file - nor from an override, which is content nobody committed. It then turns that definition and the event into the step DAG: which stages survive the trigger and stage filters, how a matrix fans out (at most 64 legs per stage), what each step depends on, whichif:conditions can be settled now, and - for a pull request that touches a protected section - which authority the run actually executes under (see Logic is open, authority is closed). Nothing is dropped silently - a filtered stage is recorded with a reason code, and a run whose every stage is filtered out is recorded as concluded with outcomeskipped. A stage whosekindhas no executor on this build -preview,agent,approval,webhook,automation,ankra,externalandverify- is skipped right here, with reasonkind_unavailableand a sentence naming the kind and the wave that ships its executor, so a run that declares one still concludes instead of holding itself and its concurrency group open forever. - Dispatch. Steps are claimed org-fairly, never global FIFO: organisations are served in order of how many steps they already have running, and a run already at its own step cap is passed over for the next run’s step rather than holding the queue (see Capacity and concurrency). The dispatcher resolves the cluster per step - the organisation’s CI cluster, otherwise the staging cluster it already nominated for AI work - and probes that the cluster’s agent advertises the
run_pipeline_stepjob before sending anything. Dispatch is also the point where a step’s deferredif:is evaluated and its remaining${{ steps.<key>.outputs.<name> }}andneeds.<stage>.*references are interpolated intorun,env,with,image,working_directoryand the stage’s ownoutputs:, now that the steps it waited for have concluded: a condition that reads false concludes the stepskipped, and a reference that still cannot be resolved concludes itinfra_errornaming the field, rather than handing a container a literal${{ }}. Both happen before anything is revealed, so a step about to be skipped spends no secret. Declared secrets and credentials are resolved into one-time, per-step reveal grants at this point, never written into the payload itself. - Run. The agent renders the step’s own Kubernetes objects from the signed payload - it never applies what the platform sent verbatim - and runs a hardened batch/v1 Job. A
checkoutstep clones the run’s commit with a pinned public git image, reading a revealed credential only if the host it is cloning from matches; arunstep executes your script. Output streams live while the step runs; the complete log and the declared artifacts are uploaded to your backup vault when it concludes, through presigned URLs minted at dispatch. The vault is the organisation’s own oldest ready one, and an organisation with none is granted uploads into the Ankra-managed default vault instead; the step’s execution says which -pipeline_artifacts: "granted"orpipeline_artifacts: "granted (Ankra-managed default vault)". Only when there is no ready vault on either side does a step get no upload grants at all, recordingpipeline_artifacts: "unavailable: This organisation has no ready backup vault, so pipeline step logs and artifacts have nowhere to be stored"- live output is unaffected, and everything archival is lost. - Settle. The settle loop records the verdict, unblocks the steps that waited on it and concludes the run when nothing is left. Each pass re-stamps the run’s own heartbeat, so a build that legitimately takes hours is no longer failed out from under you by the platform’s stale-run reaper at twenty minutes. Only Ankra’s own failures are retried, once; a non-zero exit is the step’s verdict and is final. A concluded
buildstep’s digest is stamped as the application’s build observation, ranked below your own CI’s, so push-to-deploy, previews and rollback already read a pipeline-built image.gateandpublishsettle in the same pass:gatereads the run’s own persisted scan findings against your organisation’s policy, tightened per scanner by thefail_oneach requiredscanstage declared and recording the digest it actually judged, andpublishre-tags that digest into your real repository assha-<7>- refusing outright if the build’s digest is a different one, or if the gate recorded none. - Report. The
Ankra pipelinecheck run (or commit status) is updated on every change, naming the authority the run executed under when it differs from what the head asked for. On Bitbucket Cloud each kind of run keeps a commit status key of its own, so a tag, pull request or dispatched run on a commit no longer overwrites the branch run’s result there:ankra/pipelinefor a push,ankra/pipeline/pull-request,ankra/pipeline/tag,ankra/pipeline/dispatchfor a manual, API or agent dispatch, andankra/pipeline/schedule, and a re-run keeps its original’s key. On a pull request the same run state and the same authority sentence populate thepipelinesection of Ankra’s one status comment. The run’s outcome also leaves the pull request:pipeline_run_failed,pipeline_run_succeeded,image_gate_blocked,preview_readyandpreview_failedare ordinary Ankra notification kinds, so they reach whichever Slack, Teams, PagerDuty or webhook destination your routing rules send them to.
publish just did: an Ankra Application deploys the sha-<7> image publish tagged into your real repository, exactly as it would deploy an image your own CI published. A pull request gets its own preview from the same pipeline, before publish ever runs (publish fires only on push, per the example above) - the application’s preview deploys the digest build produced for that head, by digest, never a mutable tag, and GitLab and Bitbucket Cloud pull requests now get previews the same way GitHub ones always could. The generated GitHub Actions workflow still works and is still what an application with no pipeline connected uses; nothing forces you off it.
A build you start by hand does not go through the gate. The two paths publish to different places, deliberately:
- a pipeline build stages the digest under
ankra-ci/<application>/<component>, andpublishre-tags it into<application>/<component>only aftergatehas judged it; ankra application build startpublishes straight to<application>/<component>- noscan, nogate.
Which component a build belongs to
Akind: build stage of an application’s pipeline belongs to one of the application’s components, and that decides where it pushes and where publish re-tags it. Ankra reads it in this order:
build.component, when the stage declares it. The value is the name of a component the application records.- The stage name, otherwise: a stage called
build-<component>belongs to that component. - The application’s one component, for a single-component application, whatever the stage is called.
build.component when one component has more than one build, or when you want to name a stage for what it builds. A typical case is a component that ships both a container image and a release bundle:
sha-<7> tag:
An image has to belong to a component, because a deploy of the application only reads component repositories. A pipeline that declares no
build.component anywhere keeps working exactly as before. In a repository with no linked application, build.component has no effect, and every stage still publishes as <repository>/<stage suffix>.
ankra pipeline validate checks the stages against the application before a run. It reports a stage that belongs to no component, a build.component the application does not record, and two stages that would publish to the same repository. That last finding is fatal when both stages always run, because each run’s commit tag would be overwritten by whichever stage publishes last, and a warning when one of them runs only under a condition. The components an application records come from its .ankra/ankra.yaml when it declares them. When a run reaches a stage it cannot attribute, the step concludes step_refused, and the message lists the components the application records.
Monorepo: PR-only checks, full builds on push
A monorepo usually wants a pull request to check only the components it touches, while a push to the default branch builds everything. Write one stage per component, and give each stage an event-scopedwhen block: the pull request filters on paths, and the push does not. Do not duplicate stages per event.
web/ runs checkout and test-web. test-api is skipped as path_filter and recorded as skipped on the run. A push to main runs every stage. Put the pipeline file (.ankra/**) in each list so a change to the pipeline checks everything it touches.
- A pull request’s changed files are read from your provider when the run is triggered, up to 300 paths. When they cannot be read, or the change is larger than that, no stage is skipped for its paths, and the run’s
paths_unverifiedsays why. - A stage that needs a component stage keeps the usual rule: it is skipped when that stage is left out. To run it anyway, name the stage in
skippable_needs, so it waits on what the skipped stage waited on, or give it anif:with a status function such asalways()(above), so its condition decides andneeds.test-api.resultreadsskipped. - Check the shape before you commit it.
ankra pipeline validate --changed-file web/app.tsprints each stage against push and pull request, so you can see the plan for that change. - Write each list once. When several stages share a list, or every list carries
.ankra/**, name it underpath_sets:and write{set: <name>}where the list was, for examplepath_sets: {pipeline: [".ankra/**"], web: ["web/**", "packages/ui/**", {set: pipeline}]}andpaths: [{set: web}].
Affected-only stages: skip what a change did not touch
A change that touches onlyweb/ should not wait for the API’s tests, and it should still build, scan and gate, and on a push publish. Give each component’s test stage paths:. A stage with paths: is affected-only: when no changed file matches, it is skipped as path_filter and the stages that need it run without it, waiting on what it waited on.
web/app.ts runs checkout, test-web, build, scan, gate and publish; test-api is skipped and build waits on test-web and checkout. A pull request with the same change runs the same stages except publish, which runs on pushes only. A push that changes only README.md skips both tests and still builds and publishes; that is deliberate here, because the image is built from the whole repository either way. If a documentation change should not publish, give build and the stages after it a when.paths filter, which skips them together. A change to .ankra/pipeline.yaml runs everything.
paths:orwhen.paths.when.pathsskips the stages behind a stage it leaves out, so a filtered test stops the publish behind it; that is the right rule for a component’s whole chain (its build, scan, gate and publish skipped together), and it is what the generated monorepo pipeline uses.paths:is for a stage whose absence should not hold anything back. One stage declares one of them, never both.- Every event.
paths:applies to pushes as well as pull requests, so a push tomainalso runs only the affected tests. For “pull requests check what they touch, pushes check everything”, use the event-scopedwhenrecipe withskippable_needs. - What it refuses. A stage that is left out produced nothing, so
ankra pipeline validaterefuses (pipeline_paths:) a stage that reads its outputs, abuildthat another stage needs (what needs a build reads its image), acheckout,gateorapprovalwithpaths:, and a scan a gate names inrequire_stages. It warns about an affected-only scan (a change outside its paths is not scanned) and an affected-onlydeploy,publishorpreviewthat another stage needs. - When the change cannot be seen (no change list, or more than 300 files), no stage is left out, and the run’s
paths_unverifiedsays why. - Check a change before you commit it:
ankra pipeline validate --changed-file web/app.tsprints which stages run and which are skipped.
Affected from the dependency graph: affected:
Path globs have one blind spot: a package a stage’s tests import from somewhere else in the repository. test-api lists api/**, a change to libs/auth that api imports matches nothing, and the tests that would have caught the break are skipped. affected: names the packages instead of the files, and Ankra follows their imports.
affected: runs when a changed file is in the dependency graph of what it names, its tests included, or matches its paths:. Otherwise it is skipped as not affected, and the stages that need it run without it, exactly as for an affected-only stage left out by its paths:.
gonames Go packages the way the stage’s owngo testdoes:./...and./internal/apiare read from the stage’sworking_directory, and an import path such asexample.com/shop/libs/...works too. Ankra reads everygo.modin the repository, so a module another one pulls in through a localreplacedirective counts. A change togo.mod,go.sumorvendor/reaches every package of that module.pnpmnames workspace packages by name (@acme/web) or by directory (./apps/web). A dependency on another workspace package, by name,workspace:alias orfile:path, is followed. A change to a file at the repository root, such aspnpm-lock.yaml, the rootpackage.jsonorpnpm-workspace.yaml, or to a pnpm patch (any file underpatches/and any.patchfile), reaches every pnpm stage. Documentation and Go files at the root do not:*.md,LICENSE,CHANGELOG,.gitignore,.gitattributes,*.go,go.mod,go.sum,go.workandgo.work.sum.paths:besideaffected:adds files no import graph sees: migrations, fixtures a test opens by path, a script the stage runs. A matching file runs the stage.- Where the graph comes from. The run’s checkout step reads the graph of the exact commit it checked out, in a few seconds, and records an answer for each stage. Nothing is cached and there is no extra step. The planner decides only what it can without the code: a change list it could not read in full, a change to
.ankra/pipeline.yamlor a matchingpaths:glob runs the stage, and a change of no files leaves it out. - A skip needs an answer. The stage is skipped only when the checkout answered that no changed file reaches it. No answer at all (a pattern that names nothing, an analysis that failed or took over two minutes) runs it, and the checkout’s log says why. A pipeline with no checkout stage never reads the graph: its
affected:stages run on every change,ankra pipeline validatewarns about it (pipeline_paths:), and the run’s plan records it (plan_paths:). - A skip never waits for capacity. A not-affected stage is concluded as soon as the checkout has answered, without waiting for a free CI slot on a busy cluster, and the stages that need it start as soon as they have one of their own. The time it saved is recorded with reason
not_affected. - Promotion. A merge that promotes the pull request run’s image counts a stage that run skipped as not affected, when the merge changes no file that run’s graph was not asked about. Otherwise the merge builds in full and reads the graph itself.
- The same rules as
paths:.affected:makes a stage affected-only, so it is refused where a skip would be unsafe: a stage whose outputs another reads, abuildanother stage needs, acheckout,gateorapproval, a scan a gate judges. A falseif:is still the condition’s own skip and skips what needs the stage. - Check a change before you commit it: inside your checkout,
ankra pipeline validate --changed-file libs/auth/token.goreads the same graph from your working tree and shows which stages the change reaches.
Newest commit wins
Aconcurrency.group decides which runs serialise. cancel_in_progress decides what happens when a newer one arrives: with it, the new commit supersedes the run it replaces; without it, the new run waits for the older one to finish - and on a group that varies per branch, that is a branch queueing behind itself while the older commit’s run keeps your step slots.
ankra pipeline validate(andankra application pipeline validate) reportspipeline_concurrency_queues_behind:at severityadvice, naming the group and the one line that changes it. Advice never blocks a run and never fails the command.- A queued run whose
Waiting:line names an older run of the same branch or pull request that will not be superseded carries the same recommendation. A holder on another branch does not - that is the group serialising across branches, which is what a group is for. - A pipeline converted from a GitHub Actions workflow keeps the workflow’s own queue-behind behaviour, with a conversion note recommending the flag for a per-branch group on a
pull_requesttrigger.
What a pull request gets
A connected pull request carries one comment, not several: a run, its preview and Ankra AI’s own review each write into that comment’s ownpipeline, preview and review sections in place rather than posting separately - see One status comment per pull request. The pipeline section is the run’s own state, plus the authority sentence whenever the run executed under anything but the plainly approved definition; the preview section is the throwaway environment deployed from the run’s own digest; the review section is Ankra AI’s verdict on the diff.
The review reads your pipeline, not just GitHub Actions
Ankra’s pull request review used to read a GitHub check run for its “is this commit passing” signal, so a repository already running on Ankra Pipelines still got a review that could not see its own pipeline. The review now resolves the newest pipeline run of the reviewed commit first, and falls back to GitHub’s own check runs only for an application whose real build authority is still its own CI or the generated workflow, or for a head no pipeline run has touched yet. When a pipeline run answers, the review’s prompt carries a bounded set of facts about it, each independently sourced so an absent one never reads as a clean one:- the failing step’s name, kind and error message, when the run is failing - not its log, which stays behind
ankra pipeline logs - the run’s scan findings, by tool and severity, with the highest-severity individual findings named
- the
gatestep’s own verdict and blocking count, once one has concluded - the
buildstep’s reported image digest, once one has concluded - the authority sentence, when the run executed under anything other than the plainly approved definition
A human can say “merge when green”
Mention@ankraai merge when green in a pull request comment, ask Ankra AI in chat, or call the API, and Ankra records the request and watches the pull request, merging it (squash) the moment every condition holds:
- the requester is an active member of your Ankra organisation holding
pipelines.operate, and the binding’s AI mode is Agent - in Ask mode the request is refused, naming the setting to change - the GitHub login that commented is linked to that member through an OAuth-verified identity. A login claimed by hand, or auto-filled from an Auth0 GitHub sign-in, proves the account exists and not that the commenter owns it, so it no longer counts as verified; the refusal says to link GitHub again. A service principal is refused outright - merging is a human act
- the requester still holds
pipelines.operate, is still an active member and is still OAuth-verified when the sweep actually merges, not only when the comment was written. A request can wait up to 24 hours, and an administrator can revoke the permission inside that window - the pull request’s head is not a fork
- the newest pipeline run for the head concluded successfully, and its
gatestep, if the definition declares one, passed - the run executed under plainly approved authority - one that fell back to an earlier approval, or none, is refused rather than merged under it
- Ankra’s own review has not requested changes on that exact commit
- no human reviewer’s most recent review is Changes requested - unlike the AI review’s check, this one is not scoped to the current commit, so a changes-requested left on an earlier push still blocks until the same reviewer clears it with a later review
The merge train: test every pull request as it will merge
A pull request that is behind its base branch merges as a tree nobody tested: the pull request’s own run tested its head, not its head on top of everything that landed since. Turn on the merge train for a repository and Ankra tests each pull request as the commit its merge will produce before it merges it - the way GitLab’s merge trains work, and without GitHub’s merge queue, which Team-plan private repositories do not have.ankra-merge-train/<entry>). A pipeline run tests that commit. It is planned as a pull request into the base branch, so it runs only when the pipeline declares on.pull_request and its branches match the base branch, when.events: [pull_request] stages run and push-only publish and deploy stages do not, under the base branch’s approved authority. When the car’s run passes and the base branch has not moved, Ankra squash-merges the pull request through GitHub’s own merge - branch protection, required reviews and required checks apply exactly as for a person’s merge, and Ankra never pushes to or force-pushes the base branch.
Because the merge is exactly the tree the car tested, a definition whose publish declares promote_from: pull_request publishes the car run’s tested image on the merge instead of building and testing it again: merge to published is a re-tag.
What happens when something moves:
- A car fails. The pull request leaves the train with the failing run linked; every car behind it, which included it, is rebuilt without it. Each car tests everything ahead of it, so the pull request that failed is the one that broke, with no bisection.
- Somebody merges around the train. Every car built on the old base branch head is rebuilt on the new one, and nothing is merged onto a base its car did not test.
- The pull request is pushed to, retargeted or closed. It leaves the train: the train merges the head a person put in it, never one that arrived later.
- GitHub declines the merge - usually because the pull request’s own required check is still running. The car waits, passed, and the merge is retried every pass; after three hours it leaves the train with GitHub’s reason.
- A fork. Refused, as for merge when green.
success only immediately before it merges. Make that check required on the base branch, and every merge goes through the train.
The train writes with a GitHub App token minted for that one repository with only contents, pull_requests and checks permissions. It works on GitHub only today.
Chat watches a run, and a failed build gets a fix
Ask Ankra AI to watch a run - or dispatch one, which registers the watch itself - and thewatch_pipeline_run tool wakes the same conversation the moment it concludes, with the failing step, a bounded tail of its log, the findings and the gate verdict already in the prompt. Separately and unattended, a failed build step (never infra_error, a stranded step, or one a protected policy refused - only a genuine script failure) on a direct push to the tracked branch, in Agent mode, dispatches the same auto-fix mission the generated GitHub Actions workflow’s own failed-build webhook already dispatches, with a log excerpt as evidence. A failed build on a pull request never gets an unattended commit from this lane, in either mode - it gets its diagnosis (the failing step, its error class, a log excerpt) posted into the status comment’s pipeline section instead, pointing at Agent mode’s Propose a fix on the pull request review as the actual route to a fix.
Why CI belongs in your cluster
The generated GitHub Actions workflow Ankra Applications ship with works, and keeps working - see Bring your own CI below. Ankra Pipelines exists because of what that model cannot fix:- Runners are a second fleet. GitHub-hosted runners are shared and rate limited; a self-hosted runner pool is one more set of machines to size, patch and pay for. A pipeline step is a Job on nodes you already have, scheduled under the quota and node selectors you set, and never at the expense of a deploy - the agent runs pipeline work in its own worker pool.
- Credentials live in CI secrets. A workflow needs registry logins, cloud credentials and deploy tokens as repository secrets, copied per repository and readable by any workflow that names them. In a pipeline definition those are references the platform resolves and delivers as a one-time, per-step reveal - never written into a job spec or a log - and the sections that grant them are protected: a pull request can rewrite what a pipeline does, and cannot widen what it may touch.
- Publish happens before the gate. A workflow that pushes an image and scans it afterwards has already published a vulnerable image by the time the scan fails. The pipeline vocabulary separates
build,scan,gateandpublishso that nothing is published that the gate did not pass - enforced end to end today: apublishstep refuses by name when its run has no successfulgatestep immediately upstream, and it re-tags only the exact digest that gate evaluated. - The evidence is somewhere else. Run history, logs, scan artifacts and the check on the commit all lived on the CI provider. A pipeline run is an Ankra object:
ankra pipeline get, live logs, artifacts in a backup vault - the organisation’s own, or the Ankra-managed default one - and the same status on GitHub, GitLab or Bitbucket.
What is in place today
Ankra Pipelines is landing in pieces, and this page is honest about the line. Everything in the first list is on the platform and the agent now; everything in the second is not, and is described in What comes next. In place- The definition:
.ankra/pipeline.yaml(apiVersion: ankra.io/v1,kind: Pipeline) with its parser, validator and dry-run planner - see the pipeline.yaml reference. - Conversion of an existing GitHub Actions, GitLab CI, Bitbucket Pipelines or CircleCI definition into a pipeline, from chat or the MCP server - see Migrate from GitHub Actions.
- The organisation’s CI settings (which cluster runs pipelines, parallelism, image allow-list, retention, the image gate) and its pipeline policy (the authority ceiling an administrator sets). The image gate is no longer only a setting:
gatesteps (below) evaluate every run against it. - Connecting a repository, with an optional link to an application and a CI-cluster override, and disconnecting one.
POST /org/pipelines/repositoriesconnects a bare repository and, on GitHub, reads its committed.ankra/pipeline.yamlas the definition of record in the same call;DELETE /org/pipelines/repositories/{id}disconnects it,409while a run is still live. An Ankra Application connects and decides its own repository automatically on create and on every reconcile. A new application’s setup pull request commits.ankra/pipeline.yamlby default instead of a generated workflow. - Migrating an existing application onto a pipeline with one call - see Migrate an existing application.
- The runs API and the
ankra pipelinecommands: dispatch, list, get, cancel, re-run, live step logs, artifacts, the stored definition, schedules. - Triggers from push, pull request and tag deliveries on GitHub, GitLab and Bitbucket Cloud, deduplicated across redeliveries, with a recorded reason whenever nothing runs. A Bitbucket Cloud push that updates several branches or tags at once starts each branch’s and each tag’s pipelines, and a Bitbucket pull request’s abbreviated head commit is resolved to the full commit through the repository’s credential before its run is created; a head that cannot be resolved starts no run. A private GitLab repository builds through the organisation’s stored source credential the same way GitHub and Bitbucket Cloud ones do, and a GitLab project under a nested group (
group/subgroup/project) resolves to a clone URL instead of being refused by the owner grammar. - A stage kind nothing can run is skipped, not left pending. The kinds this build has no executor for -
preview,agent,approval,webhook,automation,ankra,externalandverify- now report a validation warning naming the stage, its kind and the wave that ships its executor (severitywarn, so the pipeline still plans and every other stage still runs), and the planner skips exactly those stages with reasonkind_unavailable. A run that declares one concludes on its own; before this it satpendingforever, holding its concurrency group with it. Apendingstep whose run has outlived the sum of every step’stimeoutis additionally reaped asinfra_error/step_stranded, so a step that stranded for a reason no static check can see - a cluster that never resolved, an agent that never advertised support - cannot pin a run open either. uses:is refused by name, at validate time. Anyuses:reference -ankra/*,custom_tool/*orrepo:<owner>/<name>@<ref>#<path>, however it is spelled - is fatal with one sentence, “Reusable steps are not available yet; write the step asrun:”, until the step library ships. That is deliberately louder than the old behaviour, where auses:stage validated and then failed at dispatch with an error that read as “you forgot to write a script”.- Step outputs and deferred conditions are resolved at dispatch. A stage’s
if:that depends on a step’s result, and every${{ steps.<key>.outputs.<name> }}orneeds.<stage>.*reference inrun,env,with,image,working_directoryandoutputs:, are evaluated once the steps they wait on have concluded - a false condition concludes the stepskippedbefore any secret is revealed, and a reference dispatch cannot resolve concludes itinfra_errornaming the field rather than passing a literal${{ }}into a script. on_failureandfinallystages run when their names say. Anon_failurestage runs after the main stages have all concluded and only when one of them failed; after a green run it is skipped with its reason. Afinallystage runs after everything before it has concluded, whatever the outcome, and also after a run that had started is cancelled (ankra pipeline cancel --forceskips it). See Failure and cleanup stages for the exact rules.- In-cluster execution of
checkoutandrunstages on the organisation’s CI cluster: a hardened Job per step inankra-ci, a per-run workspace volume, cache volumes, service sidecars, network tiers, resource ceilings, timeouts, live logs, and one automatic retry of Ankra’s own failures.checkoutclones with a pinned public git image, a shallow fetch of the exact commit with a fallback to fetching its ref, and refuses anlfs:request by name rather than silently checking out pointer files. - Caches survive between runs, inside their own trust scope. A stage’s
cache:block resolves to the same PersistentVolumeClaim on every run that declares the same key, scoped to the repository, cluster, cache path and the trust of the run resolving it - the second run’steststage mounts what the first run’s left behind, not an empty volume. A miss on the exact key that matches arestore_keysprefix restores the most recently used claim under that prefix instead of starting cold, the same semantics as GitHub Actions’ own cache action, and that prefix search stays inside the caller’s own scope too. Retention is unchanged by any of this: a claim nothing has resolved to forci_cache_retention_days(below) is still deleted by the same sweep, per claim. - A fork’s run resolves no shared cache at all, and a branch never resolves the default branch’s. Two holes, closed together. A pull request from a fork that runs restricted - the ordinary
fork_policy: read_onlycase - is refused a shared cache at dispatch, on the same predicate that already withholds its secrets and credentials: every declared cache degrades to the pod-lifetime volume a cluster with no usable StorageClass already gets, and the step’s execution metadata carriespipeline_caches: "caches are not shared with fork runs"so an operator can see why the build was cold rather than guessing. Such a step never reaches the cache store at all. Separately, every claim now carries a trust scope:default_branchfor a run on the repository’s own recorded default branch,branchfor everything else - a feature branch push, a pull request (fork or not), a tag, a manual dispatch of a non-default ref. The two scopes never share a claim, and neither an exact-key hit nor arestore_keysprefix match crosses between them, so a less-trusted ref can no longer poison what a production build restores. It is deliberately coarser than per-branch isolation: the threat is an untrusted ref writing into what a trusted one reads, not one feature branch reading another’s warm cache. Claims that existed before this deploy were backfilled intobranch, the untrusted scope - nothing on a row records which run wrote it, so the true value cannot be recovered, and defaulting the other way would have carried the hole forward for every cache written before the fix. The cost is one cold build per cache key on your default branch, once. - A restricted fork run is budgeted, not only restricted. It is clamped to at most 8 matrix legs per stage, a 60-minute timeout per step, 2 CPU and 4Gi of memory per step, no sidecar services, no cache volumes and 256 MiB of artifacts and logs across the whole run - fractions of the platform-wide ceilings of 64 legs, 6 hours, 8 cores, 32 GiB and 2 GiB, because a drive-by pull request triggers this with nobody having reviewed it first. Matrix legs, timeout, compute and sidecars are clamped in the planner, so the plan
ankra pipeline validate, the dry run and the pull request’s status comment render is the run that actually executes, rather than one silently rendered smaller at dispatch; caches are refused at dispatch, and the artifact budget bounds what the run is granted, from the sizes its steps declare. The matching ceiling on what actually arrived - the one number a step cannot lie about - is written into the artifact store but has no settle lane calling it yet, so today the budget is enforced on the grant rather than on the upload. Aplan_fork_budget:diagnostic states the ceilings once per run, plus one more for each stage that actually hit one, and a declared quantity the planner cannot read is clamped rather than given the benefit of the doubt. An organisation’s own policy may lower these numbers further; nothing raises them for a fork. buildproduces a real digest. A rootless BuildKit pod in its ownankra-ci-buildnamespace resolves its own builder, source and push target at dispatch and pushes the image it builds, by digest, to the organisation’s CI staging repository; a bare repository with no application link, or a component with no resolvable registry, gets a named refusal instead of a guess.provenance: trueandsbom: trueask BuildKit to attest the digest it pushes.- Ankra platform builders build when your cluster cannot. When the CI cluster cannot build, the organisation set
ci_build_fallback: platform_builders, and Ankra has granted the organisation platform builds, abuildstep runs on Ankra platform builders instead - the same staging-repository push and the samescan/gate/publishdownstream, with the step’sexecutorrecorded as whichever lane actually ran it. What “cannot build” covers, how to confirm and request the grant, and what a fallback build looks like from your side are all in Ankra platform builders below. An application that publishes to a registry you operate yourself is pushed by this lane with the push robot Ankra minted for it there, exactly as the in-cluster build is. scanruns three tools for real, one step per tool. Trivy (filesystem, a CycloneDX SBOM, and - once a build produced one - the image itself), Semgrep and Checkov, each from a pinned, digest-referenced public image the organisation’s ownci_allowed_image_prefixescannot turn off, writing the reportsgateand the application’s Security tab both read.with.semgrep_confignames the registry packs to run (p/default p/docker …); undeclared, Semgrep runs its own--config=auto.gatejudges whatscanfound, andscan.fail_onis part of the judgement. It reads the run’s own persisted findings against the organisation’sci_image_gatepolicy (below), which agatestage’s ownwith:may only tighten, never loosen; a declared scanner that recorded nothing fails the gate by name rather than passing as clean. Each requiredscanstage’s ownfail_onis folded in on top, per scanner and tighten-only: a pull request head may add scrutiny to its own pipeline, never remove the organisation’s. Afail_onvalue that is not that scanner’s own severity vocabulary at all - Checkov’snone, a Trivy image-gate word such asapp- carries no floor rather than the loosest one.publishre-tags only the digestgateapproved, and proves it. Apublishstep refuses by name when its run has no successfulgatestep; otherwise it re-tags the exact digestbuildpushed to staging assha-<7>in the application’s real repository - never rebuilt, never a different digest than the onescanandgatejudged. The gate now records the digest it actually judged on its own step outputs, andpublishrefuses when the build’s digest is anything else - including when the gate recorded none at all, because “no record” is unknown, not agreement. Before this the guarantee rested on ordering alone: a successful gate existed upstream, so surely it had judged this image.deployrolls a release out to your own hosts. Akind: deploystage releases the signed release bundle apublishstage of the same run produced to the active host targets of itsenvironment: machines outside Kubernetes that runankra-host-agentand connect outbound only (seeankra targets). The platform settles the step from what each host reports, andankra deploymentsshows how every host fared. The stage’senvironmentanddeployblock are protected, a pull request or fork run never deploys (deploy_untrusted_event), and the step refuses an artifact this run did not publish. It does not deploy to a Kubernetes cluster: an application’s own push to deploy does that. See thedeployrow of the reference.- A monorepo’s components are tracked separately. A
publishstep resolves thebuildandgatesteps carrying its own stage suffix rather than whichever succeeded first, sopublish-webcan no longer re-tagapi’s digest into web’s repository, and a stage name following neither convention is refused by name instead of guessed at. Ascanstage honoursworking_directory, so a component’s scan reads the tree its build and test stages do rather than the whole checkout. Build observation is recorded per component, with only the primary component also moving the application-level record, so push-to-deploy, previews and rollback keep converging on the image they always did. - A run plans from
.ankra/pipeline.yamlat its own commit (GitHub). The planner fetches the file at the run’s head sha and plans the run’s logic from it, records which source won (definition_source:head_file,storedordefault), and falls back to the stored definition with a diagnostic when GitHub is briefly unreachable - a transient read failure must never stop a push running its own pipeline. Only a head file that really was fetched and is then unparseable or fatally invalid refuses the run. Protected authority is unaffected: it still resolves from the default branch’s approved definition, never from the head file. A re-run re-plans from the original run’s definition, so re-running an old run reproduces it rather than silently executing whatever the pipeline has become since. - Previews deploy the pipeline’s own digest. An application whose pipeline source is
ankra_pipelinegets its pull-request preview from thebuildstep’s own digest for that head - by digest, never a mutable tag - on GitHub, GitLab and Bitbucket Cloud alike; the gateway also re-drives a stalled preview the moment a pipeline run concludes, on top of the existing poll fallback. - Fork pull requests run, restricted.
checkoutandbuildclone the fork’s own head anonymously - never the pipeline’s own repository -buildrenders with no push target so nothing it produces can reach a registry, and every stage butcheckout,buildandscanruns with no network at all regardless of what the fork’s own copy of the definition asks for. Such a run also resolves no shared cache and is held to fixed matrix, timeout, compute and artifact budgets (above). See Forks. - Secrets and credentials delivered into steps as one-time, per-step reveal grants collected by the agent at step start: a
secretsbinding sourcedapp_env_secret,org_variableorregistryresolves, and acredentialsbinding sourcedgitorregistryresolves;secrets: [{from: credential}]validates but is not deliverable yet, andcloudandobject_storagecredentials are refused by name until the run gets its own scoped identity (below). - Schedules fire. The cron trigger dispatches through the same trigger every push and pull request uses, advancing from a stored watermark so a loop that was down for a while does not replay every occurrence it missed.
- Protected authority is enforced, not only modelled. Whichever definition supplied a run’s logic, its authority resolves from the default branch’s admin-approved definition; a pull request that changes a protected section (below) executes under the last approved authority instead, and the check run, commit status, the definition response and the pull request’s own status comment all say so. The last-approved fallback only ever offers a definition that was itself, at some point, the default branch’s own head - proved from the repository’s run history, never from the approval columns on the definition row alone.
- One approval route, whichever id you hold. The application-addressed approval (
POST /org/applications/{id}/pipeline/approve-definition) is now an alias for the definition-addressed one, so both run the same checks - a human actor rather than a service principal, the definition must be the repository’s current default-branch definition, a second approval of an already-approved definition is a conflict rather than a silent yes - and both write the same audit row. Before this the application-addressed route had none of them: a caller holding that application’spipelines.managecould approve any definition row belonging to its repository, including one sourced from a pull request head that never was, and might never be, the default branch’s own content. - A dispatch proves its commit belongs to the repository.
POST …/pipeline-runsresolves therefit was given - or the repository’s default branch - through the repository’s own host before anything is written, and accepts a caller-suppliedhead_shaonly when it is that reference’s current tip or, on GitHub, a provable ancestor of it through a compare read. Anything else is refused by name, and so is a transport failure that leaves the question unanswered: an unanswerable question is not a yes. GitLab and Bitbucket Cloud have no compare read, so a commit named there is held to the exact tip. Re-runs keep working and are deliberately exempt - a re-run’s commit is one Ankra itself recorded when the original run was created, not a claim arriving with this call, which is what lets a fork pull request’s run be repeated at all. - A dispatch’s own definition is what actually runs.
--spec-file(spec_yamlon the API) is persisted verbatim on the run, and both the planner and the dispatcher render every step from it, instead of shaping the plan and then silently re-rendering each step from the stored definition. Only the logic moves: an override is content nobody committed, so its protected sections are compared against the default branch’s approved authority exactly as a pull request head’s are. - Five more sections are protected authority.
services,cache,resources.cpuandresources.memory,timeoutand a stage’swith:block now hash, resolve in both directions and surface by name, so a head can no longer ask for far more compute than the organisation meant to grant, remove its own or the trusted definition’s timeout, change which run’s output a cache restores into the workspace, or rewrite agatestage’s own scan-tool configuration. This moves every definition’s protected hash - see the re-approval note under Logic is open, authority is closed. - A sidecar is held to the step’s own boundary. A named
services:entry’s image goes through the organisation’s image policy and itsenvthrough the reserved-name check, both as a refusal of the whole step rather than a diagnostic that runs it anyway;ankra pipeline validatenames the same violation atservices[<name>].image. A sidecar could previously pull any image and setANKRA_*, the loader andPATHvariables, or the image builder’s ownBUILDCTL,BUILDKIT_HOSTandDOCKER_CONFIG. - Two organisation ceilings that nothing could set now bite.
ci_ignore_unfixed(below) is an organisation-wide floor agatestage’s ownwith:may only tighten, andmax_cache_sizeon the pipeline policy is stored and enforced - every declared cache had been held to it since the ceiling was written, with nothing able to set it, so every organisation silently resolved “no ceiling”. egress-httpsreaches the public internet, and nothing private. The tier’s 443 rule now excludes RFC1918 (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), the whole of link-local169.254.0.0/16rather than only the169.254.169.254endpoint that used to be its single exception (AWS ECS serves task metadata at169.254.170.2), and carrier-grade NAT100.64.0.0/10(Alibaba Cloud answers at100.100.100.200, inside it). There is an IPv6 rule at all now, excludingfd00::/8,fe80::/10and AWS’sfd00:ec2::254. DNS stopped being “port 53 to any pod in the cluster”: it is thekube-systemnamespace plus169.254.0.0/16for a cluster running NodeLocal DNSCache, and theservicestier - which carried no DNS rule at all - gained the same one. This breaks a pipeline that reaches a private HTTPS endpoint, and the symptom is a connection timeout rather than a refusal, so name the range inci_egress_allowed_cidrs(below) before the step runs: abuildthat pushes to a registry on a private address - an in-cluster registry, a Harbor inside your VPC - cannot reach it until that entry exists. The agent renders a step’s NetworkPolicy itself, so the tightened tier arrives with the agent release of 5 September 2026; a cluster still on an older agent keeps the previous tier until the fleet roll reaches it.- An image policy prefix ending in a separator means that separator. An entry ending in
/,:or@used to match nothing at all, because the matcher requires one of those three characters after the prefix - the boundary check that stopsghcr.io/my-orgadmittingghcr.io/my-org-evil/api. Each now admits exactly its own boundary:ghcr.io/my-org/admitsghcr.io/my-org/apiand refusesghcr.io/my-organdghcr.io/my-org:v1,ghcr.io/my-org:admits a tag and not a path,ghcr.io/my-org@admits a digest, andregistry.example.com/admits paths on that host and neverregistry.example.com:5000/anything, which is a different endpoint. Trimming the separator away would have widened each of those rather than narrowing it, so that is not what happens, and an entry that is only separators - or ends in two of them - matches nothing rather than becoming the empty prefix every image starts with. The settings API refuses a trailing separator at write time and always has, so a storedci_allowed_image_prefixescannot carry one: this is defence in depth for the prefixes Ankra sets in code and for the copy of the policy a step’s payload carries, not a change to what an administrator can store. - SCM feedback: one
Ankra pipelinecheck run per run on GitHub with a real conclusion and a Cancel button, a commit status on GitLab and Bitbucket, a comment fallback when the GitHub App lackschecks:write, and - on a pull request - the same run state and authority sentence in thepipelinesection of Ankra’s one status comment. - The AI review reads your pipeline, and a human can say “merge when green”. See What a pull request gets above: a bounded pipeline-facts block in the review prompt, a
merge when greenpull request comment gated on the run, its gate, both reviews and its authority, and awatch_pipeline_runchat tool whose unattended fix loop reacts to a failedbuildstep. - Step logs and declared artifacts stored in the organisation’s oldest ready backup vault - or, for an organisation with none, in the Ankra-managed default vault - listed and downloaded through the API today;
ankra pipeline artifactsandartifacts downloadspeak the same routes and read the same way from the CLI’s next release (see Commands and API). A vault is no longer something to arrange first - ascanstage’s reports travel the same path, so a fresh organisation records findings and passes its gate without provisioning a bucket; bringing your own decides where the objects live, not whether they are kept (see Before your first run). - Long runs stay alive. Every settle pass re-stamps the run’s heartbeat, so the platform’s twenty-minute stale-run reaper no longer fails a pipeline that is still working. A run whose steps genuinely stopped reporting is still reaped, and the reason is recorded.
- Run history is retained by policy, not forever.
ci_run_retention_days(default 90, 7 to 365, below) bounds how long a concluded run is kept, and an hourly sweep deletes past the window in batches. It never deletes a live run, never a repository’s own newest concluded run, and never a run still carrying an artifact the artifact reaper has yet to remove from the vault. - Run outcomes reach your destinations.
pipeline_run_failed,pipeline_run_succeeded,image_gate_blocked,preview_readyandpreview_failedare notification kinds like any other, produced once per run (or once per gate verdict, or when the preview row itself flips), so notification routing sends them to Slack, Teams, PagerDuty or a webhook alongside everything else Ankra reports. - A truncated log says so, and the failing step names its own error. The agent reports how many log lines it had to drop to backpressure; that count is recorded on the step and shown as a notice under its row on both the check run and the pull request status comment, so a step whose output was silently cut no longer looks like one whose log arrived whole. The check run’s step table also carries the failing step’s own error message (truncated to 200 characters) in its Outcome cell, instead of only the run’s error in one blockquote at the end.
- The portal. The application’s Pipelines section shows Ankra pipeline runs with a timeline and an in-app log viewer beside the GitHub Actions view; organisation Settings → Pipelines lists connected repositories; the application’s Security section reads a pipeline’s own findings once a run has concluded for its repository, falling back to the GitHub-artifact reader until one has.
- The runner for
verify. No runner image exists, so aprobe,smoke,load_test,assert_promqlorrestore_verifysub-block never runs. The stage is skipped withkind_unavailablerather than dispatched into an image pull that cannot succeed, so it no longer holds the run open - but it also verifies nothing, and a pipeline that relies on it is not being verified. - The executors for the remaining stage kinds -
preview(a per-pull-request environment deployed from inside the pipeline DAG, not to be confused with an Application’s own pull-request preview, which already deploys a pipeline’s digest, above),agent,approval,webhook,automation,ankraandexternal. Each validates with a warning and is skipped, so a run containing one concludes; none of them does the work its name promises. In particular anapprovalstage does not block a run on a human today - it is skipped, and everything downstream of it runs. - The per-run identity
permissionsmints, so a step can call theankraCLI with exactly the authority the default branch granted - a convertedgithub.tokenreference already diagnoses itself as “the run-scopedANKRA_TOKEN”, naming what does not exist yet rather than resolving to nothing;cloudandobject_storagecredentials wait on the same identity. - The built-in
ankra/*step library behinduses:, organisation step templates, andcustom_tool/<name>steps. Until it ships, anyuses:is fatal at validate time with one sentence telling you to write the step asrun:. - Nothing pins the step keys an expression names.
${{ steps.<key>.outputs.<name> }}resolves at dispatch against the run’s real steps, but a key that does not exist - a typo, or a matrix leg the writer guessed at - interpolates to empty text rather than being refused. The validator declares expression roots, not key sets, and a step key is a matrix-expansion product it cannot know; closing this belongs in the planner, which does know the expanded keys. - The user-declared
retryblock is validated and carried to the agent, not applied. A step’s ownretry:is parsed and stored on the plan, but nothing dispatches a second attempt from it - only Ankra’s own infrastructure failure classes retry automatically (see When something fails); a script that exits non-zero is final today regardless ofretry.max_attempts. Dynamic matrices (matrix: ${{ fromJSON(steps.plan.outputs.legs) }}, fanning out over a previous step’s own output) are not available either - every axis must be written out in the file. - Attestations and SBOM referrers on the image you actually deploy.
build’sprovenance/sbomflags attest the digest it pushes to the CI staging repository;publish’s re-tag copies that one image manifest, not the attestation manifests beside it, so they are not on the digest your application deploys. Trivy’s SBOM is an artifact in the vault today, not an OCI referrer on any image. - Merge-when-green and automatic disposition re-evaluation are GitHub only. A GitLab or Bitbucket Cloud
merge when greenrequest is accepted and then refused, because neither provider adapter reports the capability to merge yet; a push to a GitLab or Bitbucket Cloud default branch that adds or removes.ankra/pipeline.yamlanswersinsufficient evidencerather than re-evaluating the application’s pipeline source, because the raw-file read that evaluation needs has no GitLab or Bitbucket equivalent wired in yet. - A dedicated log stream for pipeline output, on the agent side. The platform half is in place: the
pipeline_outputJetStream stream exists with its own capacity and retention, the agent is granted publish on its own cluster’s subject, dispatch renders that subject onto every step, and the log relay reads the dedicated stream first and falls back to the shared one. What has not shipped is the agent release that actually publishes there, so live output still rides the platform-wideexecution_outputstream until your cluster’s agent is upgraded to a release carrying that change. Nothing aboutankra pipeline logsor the check run changes when it does - the relay already reads either. - Planning from the committed file is GitHub-only. A GitLab or Bitbucket Cloud run plans from the stored definition permanently, because the bounded raw-file read the planner uses has no equivalent wired in for those providers yet; the run records
definition_source: storedand says so in a diagnostic rather than pretending it read your commit. - Annotations and metrics. A step’s stdout can already carry
$ANKRA_OUTPUTkey=value lines, read back onto the step; the equivalent for check-run annotations (::ankra::notice file=…,line=…::msg) and custom metrics (::ankra::metric name=…,value=…) is not ingested anywhere yet. Test reports are: see Test results. - Environments hydrated from restore points, verification stages, promotions with approvals, and canary rollouts - see What comes next.
Test results
A stage that runs tests can declare the reports its test command writes, and Ankra reads every test’s outcome from them:ankra-tests.<n>.<format> in the run’s artifact list), and Ankra parses it. The step’s own exit code stays its verdict: a report Ankra cannot read never fails or passes the step.
Formats.
Your command writes the report.
test_results only names where the report is; the stage’s own command has to write it there, or there is nothing to read and the run’s test summary lists the report as never uploaded. For example: gotestsum --jsonfile test-results.json -- ./... (or go test -json ./... > test-results.json, which leaves the step’s log as raw JSON), pytest --junitxml=reports/junit.xml, vitest run --reporter=junit --outputFile=reports/junit.xml. A path is relative to the workspace, not to a directory the command cds into or the stage’s working_directory. A pipeline Ankra generates for you declares a report only when its test command writes it.
Flaky tests. A test that failed and then passed on a retry is recorded as flaky, not passed, with the number of attempts. Ankra reads retries from Playwright’s JSON reporter, from a JUnit file’s Surefire flakyFailure/rerunFailure elements, and from a test that appears more than once in one report (gotestsum --rerun-fails). Playwright’s junit reporter only writes retries with includeRetries: true (or PLAYWRIGHT_JUNIT_INCLUDE_RETRIES=1); without it a flaky test reads as passed, so use the JSON reporter or turn that on.
What you get.
- The run page’s Tests section: failed tests first, then flaky ones, then the slowest, each with its duration, attempts and failure message. A report that was declared but not uploaded or could not be read is listed with the reason.
GET …/pipeline-runs/{run_id}/tests: the same summary over the API.GET …/pipeline-tests/history?test_key=…: one test’s outcomes in the newest runs of your default branch (orbranch=), with its flaky rate.GET …/pipeline-tests/timings?stage=…&group_by=file|test: each file’s or test’s mean recorded duration on the default branch, to balance shards by time.runs_sampled: 0means there is no data yet; fall back to an even split.
pipelines.read, and each is addressed by repository or by application, for example /api/v1/org/pipeline-repositories/{repository_id}/pipeline-tests/history and /api/v1/org/applications/{application_id}/pipeline-runs/{run_id}/tests.
When a report is missing. Declaring test_results never fails a step that passed. Today Ankra uploads a declared report only when the stage also lists the same path under artifacts:, as the example above does: a path the stage already declares is one the step promised to write. A report that is declared only under test_results is not uploaded, and the run’s test summary says so and why. If your test command never wrote the report (the suite crashed before its reporter ran), the summary says that too, rather than reading as a run with no tests.
Quarantining a flaky test
A test that keeps failing on and off should not fail every run while someone fixes it. Each repository can quarantine such tests. A quarantined test still runs and is still reported; what changes is that a step whose only failed tests are quarantined passes. Quarantine is off on every repository until you turn it on, and only a member withpipelines.manage can change it.
When a failure is forgiven. A failing step passes because of the quarantine only when all of these hold. Otherwise it fails exactly as it would without one:
- It is a
kind: runstep that failed on its own (step_failed). A timeout, an infrastructure error, a refusal, and any build, scan, publish, deploy or verify step always fail as before. - Quarantine is on for the repository.
- The step declared at least one test report, and every one was uploaded and read whole.
- The reports hold at least one failed test, and every failed test is quarantined. A test that passed on a retry counts as
flaky, not failed.
; instead of &&) fails the step on its own, because the quarantine only reads the step’s test reports.
Which tests can be quarantined. A test qualifies when, over your default branch’s own runs in the last 30 days (never pull request checks), it ran in at least min_samples runs and was flaky in at least min_flaky_rate of them.
Leaving quarantine. A quarantined test is released when someone releases it, or on its own after it passes
release_after_passes default-branch runs in a row; a failure or a flake on the default branch starts the count again. Turning quarantine on or changing a setting, quarantining a test and releasing one are each recorded in the organisation’s audit log, an automatic release under system.
Commands.
GET …/pipeline-tests/quarantine, PUT …/pipeline-tests/quarantine/settings, GET …/pipeline-tests/quarantine/candidates, POST …/pipeline-tests/quarantine with {"test_key", "reason"}, and POST …/pipeline-tests/quarantine/{id}/release. Quarantining a test that does not qualify, or while quarantine is off, is refused with the reason, naming the test’s flaky rate and runs against your settings.
Limits. A report is at most 32 MiB, and a directory at most 256 report files and 64 MiB unpacked. At most 100,000 tests are read from one report, and 20,000 per report and 50,000 per run are stored, failed and flaky ones first; the counts always cover every test read. Failure messages are kept to 2,000 characters, and anything that looks like a credential is redacted. A report with a DOCTYPE is refused. Test results are kept as long as the run they belong to.
Organisation CI settings
GET and PUT /api/v1/org/ci-settings (and /org/ci-settings in the portal session) carry the organisation-level settings every pipeline runs under. Reading them is open to every member; changing them is an organisation administrator’s act.
Only the members you send are changed, so re-opening one private range is a single call that leaves the rest of the table alone:
/api/v1/org/pipeline-policy, pipelines.manage for read and write) is the ceiling above these: allowed network tiers, credentials, node selectors, tolerations and runtime classes, maximum CPU, memory and GPU per step, the largest a declared cache volume may ask for (max_cache_size, which the route now stores and every declared cache is held to), the hosts a webhook stage may post to, and the external CI providers a pipeline may wait on.
Capacity and concurrency
How much pipeline work runs at once is decided by two organisation limits and one property of the cluster the steps run on. The narrowest one wins.
The two organisation limits need an organisation administrator, and the portal’s Pipelines settings page edits both. The same page edits the pipeline cluster’s CI slots and workspace storage class in its Workers and step placement card. The run limit is the one that caps throughput: every run past it waits as
queued. The step limit only shapes how wide one run fans out. It is not an organisation-wide total (it was until 14 September 2026), so raising it never drains a queue of other runs.
A CI slot is not a node. A slot is one step the agent on the CI cluster watches at once, from the moment it creates the step’s Job until the step concludes. Nodes are a separate question, answered by the cluster’s node-group autoscaler: when steps ask for more CPU and memory than the current nodes have free, it adds nodes, up to the group’s autoscaling maximum. So the slot count is how many steps may be live, and the node group is whether there is room to place them. A cluster with plenty of slots and a node group at its maximum still queues, and so does a large node group behind a small slot count.
Changing the slot count. An agent new enough to resize live applies a new ci_worker_count within a few seconds, without a restart, and takes up to 128 slots; ankra cluster agent ci set says it was applied live. An older agent takes up to 32, and a change re-renders its release instead: at once when it is online, at its next upgrade otherwise, and the command says which happened. Turning pipelines on or off (to or from 0) always re-renders the agent.
Ankra does not over-fill a cluster. Ankra stops handing a cluster steps once its agent has as many in flight as it has slots, so the extra steps wait in Ankra’s own queue, where organisations are served fairly and the run can say why it is waiting. Time spent waiting for a slot is not time spent running: a step’s timeout starts when the agent begins it, and the step’s duration on the run page and in ankra pipeline get counts from the same moment. The run page shows the wait separately as the step’s queue wait, and ankra pipeline get prints it beside the step.
Steps of one run share a node. A run’s workspace is one rwo volume (see Limits and defaults), so every step of one run, and the helpers that restore and save its caches and upload its artifacts, is pinned to the node that volume is attached to. A step that cannot fit there waits for its siblings to finish and is then queued again, rather than landing on another node and waiting for the volume to detach and re-attach. When the node is gone, cordoned or not Ready the pin is dropped and the step log says so; when a step does wait on a volume held by another node, its log names that node. That node’s free CPU and memory against what each step reserves is a physical limit of its own: more nodes elsewhere do not widen one run, while a larger node does, and so does a stage that declares what it needs rather than a round number. When a run fans out, its first step prefers the emptiest node so its siblings have room beside it.
A step reserves half of its resources.cpu and all of its resources.memory (250m and 1Gi when the stage sets no resources). The CPU figure is a ceiling the step grows into while its node has CPU free, so a lint step that declares two cores and uses a fraction of one does not keep a build off the node; when every step on a node is busy at once, the node’s CPU is shared in proportion to what the stages declared and the steps run slower instead of waiting. Memory is different: a node cannot take memory back from a running step without killing it, so each step’s memory is set aside in full, and on most nodes memory is what decides how many steps fit. A stage whose memory is padded costs you a step’s worth of concurrency; one whose memory is too low is killed with exit status 137.
Sizing a stage from what it used. Ankra records, for every step the agent runs, what its container asked for and what it used: the highest memory the kernel charged it, how often it met its memory limit, whether a process was killed for memory, its CPU time and how long it was held back at its CPU limit. ankra pipeline get <run> -o json carries these per step (memory_peak_bytes, cpu_usage_usec and the rest), next to its requests (memory_request_bytes, cpu_request_millicores). The API’s resource suggestions for a run (GET .../pipeline-runs/<run>/resource-suggestions) turn them into one suggestion per stage, from that stage’s latest 20 successful attempts under the same pipeline definition: the 95th percentile plus 25% headroom, rounded up, and only once a stage has at least 5 attempts to go on. Each suggestion says how many attempts it rests on. Nothing is ever applied for you; change resources in your pipeline file if you agree.
Two readings need care. A memory peak counts page cache, which the kernel uses to fill whatever room a step has, so a stage whose peak reached its limit is reported at_limit with no suggestion: its peak says it used the room, not that it needed it. CPU has no peak the kernel keeps, so the CPU suggestion is the request a stage should reserve, sized from the cores it used on average, written as the resources.cpu that reserves it; a stage that spent a tenth or more of its demand held at its limit is at_limit instead. Lowering resources.cpu lowers the ceiling too, so a stage that uses its CPU in bursts may run slower on the suggestion.
A step that cannot fit beside its run goes back to the queue. When the node holding a run’s workspace is full, a step that has to run there would otherwise sit waiting on that node while holding a CI slot. After a minute of that, the agent gives the slot back and Ankra returns the step to the queue; it is handed out again once the run’s other live steps have finished and the node has room. The run page says how many times a step was returned to the queue, and ankra pipeline get prints the count.
A run can reserve its room before it starts. With the run reservation on for a pipeline cluster (ankra cluster agent ci set --run-reservation on --cluster <name>), Ankra works out from the plan how much CPU and memory the run’s steps will need side by side at their peak. Before the first step starts, it asks the cluster to hold that much room on one node with a placeholder pod. The run’s steps then take that room over as they start, so another run cannot fill the node between two steps. When no node has that much room free, the placeholder waits like any pod, and the cluster autoscaler can add a node for it. When the run concludes, the room is released.
That has a cost. While a reserved run is in progress it keeps its room on the node even between steps, and that can add a node or keep one from scaling down that an unreserved run would have let go. shadow works out and records each run’s reservation without holding any room, so you can compare the estimate with what the steps used before you turn it on. off reserves nothing. Each run’s reservation is on its detail as reservation (mode, the estimated CPU and memory, and the peak its steps were observed to use), and the run page shows it under Reservation.
A step that can never be scheduled fails fast. If no node the step may run on is large enough for what it asks for, and the autoscaler cannot add one that is, the step fails after about two minutes with the scheduler’s own reason, instead of holding a slot until its timeout and reporting a timeout for a step that never ran. Lower the stage’s resources, or add a node group with larger nodes.
Why a run is waiting
A queued run says which limit it is behind. The run page shows it under the run’s status,ankra pipeline get prints a Waiting: line (CLI v0.17.0 or newer), and the run detail carries it as queue_reason with a sentence in queue_reason_message:
Once a run’s first step has started it shows no
Waiting: line, whichever of these limits holds its next step back: its pending steps start as the limit frees up. A step that Ankra has handed to the agent and that is waiting for a free slot shows as waiting for a CI slot on the run page.
A run waiting for room for its reservation has been admitted and is already running, so it carries no queue_reason. It says so in its own field instead. While the run’s first step waits for a node with room for the run’s reservation, the run page shows Waiting for a node with room for this run, with the room it is holding out for, how long it has waited, and what the autoscaler is doing about it (adding a node, at its node group’s maximum, refused by the provider). ankra pipeline get prints the same sentence on its Waiting: line and a Reserving: line with the room and the wait. The run detail carries it as reservation_wait (state waiting or returned_to_queue, since, waited_seconds, the reserved cpu_millicores and memory_bytes, and the message). If no node has room within 10 minutes, the first step goes back to the queue and is sent again, and the state reads returned_to_queue with the count. When Ankra could not read the cluster, reservation_wait_unavailable says so, and while it is set the wait is unknown: a missing reservation_wait then does not mean nothing is holding the run back. The step itself carries the diagnosis run_reservation_pending (see Diagnosis codes).
Seeing and raising capacity
The Capacity panel on the portal’s Pipelines settings page shows, refreshed every 15 seconds: the CI slots on your pipeline cluster, the steps in flight there, your organisation’s queued runs, its runs in flight againstci_max_parallel_runs, its pending steps, and how much of its work is waiting for a CI slot. It also names the limit that is full.
To raise capacity, change the limit that is full:
- Runs queue as
waiting_on_run_cap.ankra org ci-settings set --max-parallel-runs N(up to 64). - Runs queue as
waiting_on_ci_slots.ankra cluster agent ci set --workers N --cluster <name>(up to 128 on current agents). - Steps are slow to start although slots are free. The cluster has no room for the pods: raise the node group’s autoscaling maximum (
ankra cluster node-group autoscaling getshows it), or use larger nodes. More slots without more nodes only means more steps waiting for a node.
ankra org ci-settings get -o json carries the same list per pool member as scale_up_blockers, with scale_up_blockers_state saying whether an empty list means nothing is wrong (none_observed) or the cluster’s events could not be read (unknown). The codes are listed in Diagnosis codes.
Ankra also watches every pipeline cluster for you. When a cluster’s steps in flight have stayed at or above its CI slots for fifteen minutes, Ankra raises a saturation alert for that cluster: pipelines there are queueing for slots while everything else reads healthy. The remedy is the same as above, more slots, or a larger node group maximum when the nodes are what is full.
Step placement
By default a pipeline pod runs on any node of the CI cluster that will take it. To keep CI on nodes of its own, for example a dedicated node group tainted so nothing else lands there, give the cluster a step placement:ankra-ci-build. Naming a node group is enough on a cluster Ankra built: the pods select the group’s nodes by the ankra.cloud/node-group label every node Ankra spawns carries (or by the labels the group declares, on a group from before that label), and tolerate every taint the group has. A group the cluster does not have is refused, and the refusal lists the groups it does have. On a cluster without Ankra node groups, name the nodes yourself:
--node-selector key=value and --toleration key[=value]:Effect repeat, and add to --node-group when you give both. A toleration without a value tolerates any value of the key; the effect is NoSchedule, PreferNoSchedule or NoExecute.
Set the mode with
--placement required|preferred, and remove the placement with --clear-placement. A placement flag replaces the whole placement; the worker count and storage class are left alone, and --workers is not needed to change only the placement. The change applies to the next step Ankra sends the cluster, with no agent restart. ankra cluster agent ci get prints the placement and how many Ready nodes it admits right now, so a placement no node matches is visible before a run waits on it. The portal’s Workers and step placement card on the Pipelines settings page edits the same setting, with the cluster’s node groups to choose from.
The placement is set by whoever may manage the cluster’s agent, outside any pipeline definition, so it needs no authority approval and is not held to the pipeline policy’s node selector and toleration allow-lists - those bound what a pipeline’s own runs_on may ask for. A stage’s runs_on still applies on top of it: its node selector is merged with a required placement’s, and a stage asking for a different value of a key the placement sets is refused before it runs, naming both. On a cluster where a Kyverno or Gatekeeper policy mutated pipeline pods onto a dedicated group, the placement replaces that policy, and unlike it also covers the helper pods.
To create such a group, ankra cluster node-group add <cluster> --name pipelines --instance-type <type> --taints dedicated=ci:NoSchedule taints it from its first node.
An agent that predates the placement ignores it and places pods by each stage’s own runs_on alone, so upgrade the cluster’s agent (ankra cluster agent upgrade) before relying on it.
Ankra platform builders
Ankra platform builders are the Ankra-operated build lane abuild step falls back to when the pipeline cluster cannot build it. The fallback covers two causes, and nothing else:
- The cluster’s agent does not run pipeline steps. It predates the capability, or no agent has checked in at all. Every stage of the run is affected;
buildis the one kind that has somewhere else to go. - The cluster’s node runtime confines the rootless image builder. Typical for AppArmor on k3s or Ubuntu nodes, and for a kubelet that defaults seccomp to
RuntimeDefault. Every other stage runs in-cluster as normal and onlybuildis routed away;build_runtime_confinedhas the mechanics.
ci_build_fallback reads platform_builders (the default), platform builds are on for the organisation (the default), and the stage is one the lane can accept (below). With platform builds switched off, the step concludes build_fallback_none with a sentence saying Ankra’s platform-operated build fallback is not enabled for this organisation yet, whatever the setting says.
Confirm the grant. ankra org ci-settings get prints Platform builds enabled: yes or no (CLI v0.17.0 or newer; -o json carries it as platform_builds_enabled), and when the answer is no while the fallback reads platform_builders, the command says the grant is what is missing, not the setting. GET /api/v1/org/ci-settings returns the same field as platform_builds_enabled. Any member of the organisation can read it; nobody in the organisation can write it.
If the grant reads no. Platform builds are on for every organisation unless Ankra has switched them off for yours. Open a support ticket naming the organisation and the pipeline cluster (ci_cluster_id, or the cluster’s name) whose builds should fall back, and the next run uses the grant once it is back on, with no change on your side.
Sharing the pool. Ankra platform builders are shared by every organisation, so one organisation runs at most three builds on them at a time. A fallback build beyond that waits in order and starts when one of your running builds finishes; it is not failed and does not use the step’s retry. Waiting for a builder has the details.
Cost. Platform builds carry no charge. Nothing about the grant or the setting changes your billing: no usage is metered for a build that runs on Ankra platform builders, and a build that runs in-cluster costs what your cluster already costs.
What you see during a fallback build. The step’s executor records platform_builders, and the run’s step table shows the step running on Ankra platform builders rather than on your cluster. There is no live log stream to tail, because the step was never dispatched to an agent: ankra pipeline logs --follow has nothing to attach to while the build runs, and the archived log appears when the build concludes. The image is pushed by digest to the organisation’s CI staging repository, exactly where an in-cluster build pushes, so the pipeline’s own scan, gate and publish steps still decide whether it is promoted.
What the lane refuses. Ankra platform builders build what the in-cluster pod would have, from the same stage, but two things about a stage’s layout have to be right, or the step concludes without a build and its sentence names the fix:
- In a multi-component (monorepo) application, a build stage must belong to a component the application records: either it declares
build.component, or it is calledbuild-<component>(which component a build belongs to). A stage that does neither, or that declares a component the application does not record (a typo, or a component renamed since the run was planned), is refused asplatform_build_unavailable, and the message lists the components the application knows. A single-component application’s build stage may be called anything. A repository with no linked application is refused the same way, because it has nowhere to push whoever builds it. - Every
build_argsentry must carry a literal value, writtenNAME=value. In-cluster, a bareNAMElets BuildKit read the argument from the build pod’s own environment; the platform builder has no such environment, so a bare entry is refused asbuild_fallback_unsupportedrather than built silently without the argument.
Logic is open, authority is closed
Anyone who can push can change what a pipeline does: add stages, rewrite scripts, pick any image the organisation’s policy allows, change conditions, matrices,needs, caches, artifacts and timeouts. What a run may touch is not the pull request’s to decide. These sections are protected - they are resolved from the organisation’s policy and the definition on the default branch, never from a pull request’s copy of the file:
permissions- the API scopes the run identity is minted with.secretsandcredentials- each declared binding, by name, source and key.on.pull_request.fork_policy.defaults.networkand a stage’snetworkwhen it is aboveegress-https.runs_onon every stage - cluster, node selector, tolerations, runtime class - and the whole ofresources:gpu,cpuandmemory, so a head cannot ask for far more compute than the organisation meant to grant.- A stage’s
timeout, so a head cannot remove its own or the trusted definition’s and let a step run far past what was approved. - A stage’s
cacheblock -key,paths,size,restore_keysandfallback- which decides whose earlier output is restored into this run’s workspace. - A stage’s
with:block, which is where agatestage’s own scan-tool configuration lives. - The
servicesblock - each sidecar’s image, environment and ports. - A
gatestage’s approval roles and required stages, anapprovalstage’s roles and minimum approvals, anagentstage inagentmode and its tool profile, awebhookstage’s URL, anexternalstage’s provider and workflow. - The
environmentsblock - data policy, verification, rollout and gate per environment.
approved- the head declares exactly the trusted authority. An ordinary pull request that changed no protected section reads this way.unapproved- a push to the default branch itself changed a protected section nobody has approved yet; the run executes under the last approved authority (or none) until an administrator does.changed_on_head- a pull request added a credential, widened its egress, or moved a step onto another node pool. The change is ignored for this run and surfaced, never silently granted because a commit landed.
approved, naming what changed and what to do about it; GET /org/pipelines/definitions/{id} reports the definition’s protected_hash and whether it is the currently approved one. An administrator approves a repository’s current protected sections with:
pipelines.manage and a human actor - a service token cannot approve. Answers 409 rather than silently agreeing when the definition is no longer the repository’s current default-branch definition (a later push replaced it) or is already approved. POST /org/applications/{id}/pipeline/approve-definition is the same act addressed by the application whose repository owns the definition, for a caller who holds the application’s id and not the definition’s; it resolves that address form and then runs exactly the checks above, mapping their three refusals onto the same 403 and 409. ankra pipeline definitions get <id> and ankra pipeline definitions approve <id> are the CLI form of the same two acts, and are the ones the platform’s own call to action names when a run executes under anything but plainly approved authority. The portal approves the same way, from Settings → Pipelines → Pending approvals or the run page, and so does the Approve authority button on the GitHub check run. Narrowing the image allow-list afterwards does not invalidate an approval, because the policy is applied at plan time and is not part of the hash.
What a step runs as
Every step is a hardened batch/v1 Job in theankra-ci namespace, which enforces the Pod Security Standards’ Restricted profile: uid and gid 65532, the RuntimeDefault seccomp profile, no privilege escalation, a read-only root filesystem, every capability dropped, no service account token, no service links, one attempt, an explicit deadline and a TTL. A pipeline cannot ask for host privilege, host namespaces, host paths or control-plane placement; the request is refused at plan time. The one relaxation is the rootless BuildKit pod a build stage runs, in its own ankra-ci-build namespace under the Baseline profile - never privileged, never root, and no pipeline can name its own build or checkout image: both are Ankra’s own, pinned by digest. That build pod names no seccomp or AppArmor profile at all, which is the only spelling of “leave the builder alone” Baseline admits, and nothing can change it to Unconfined. On nodes whose runtime reads that as a confining default the builder cannot start, and the build is routed to Ankra’s own builders instead (build_runtime_confined).
With the root filesystem read-only, a step can write to three places, and they differ in size and lifetime:
/workspaceis the run’s shared volume: the checkout lands there, and it carries files from one step to the next. Anything large belongs here./tmpis a 256 MiB memory-backed tmpfs, private to the step’s pod and empty again at the next step. It counts against the step’s memory, and a compiler or linker that stages intermediate files in/tmpruns out of space long before the workspace would. PointTMPDIRinto the workspace for anything that writes big temporary files, withexport TMPDIR=/workspace/.tmp && mkdir -p "$TMPDIR"at the top of the step’srunscript./dev/shmis 64 MiB unless the stage setsshm_size.
/etc/passwd entry in most images, so every step gets USER and LOGNAME set to ankra, which is what lets getpass.getuser(), git and similar lookups resolve. HOME is /tmp, so a tool that writes a per-user config there works, but the 256 MiB limit above applies to it: point package caches and build output at /workspace instead. HOME and TMPDIR cannot be set in a stage’s env (see reserved names below), so export them in the run script when a tool needs them elsewhere, where the change is visible in the step you wrote.
Beyond containment:
- Network tiers.
nonedenies all egress, and is what a stage gets when neither it nor the pipeline’sdefaultsnames a tier - except forcheckout,buildandscan, whose only job is outside the cluster and which default toegress-httpsinstead (the tier a stage gets by default).egress-httpspermits 53/UDP and 53/TCP to the cluster’s own DNS - thekube-systemnamespace, plus169.254.0.0/16for a cluster running NodeLocal DNSCache - and 443/TCP to public addresses only: RFC1918,169.254.0.0/16,100.64.0.0/10,fd00::/8,fe80::/10and AWS’sfd00:ec2::254are all excluded, so a step on it reaches neither your private networks nor any cloud metadata endpoint that hands out the node’s identity. Acheckout,buildorscanstage that explicitly declaresnetwork: "none"is honoured rather than widened back - the tier is clamped only on a restricted fork run (below) - but the validator warns what that stage loses, because a step with no way to reach its source, its base images or its vulnerability database is not one broken step, it is a pipeline that cannot run at all.servicesis deny-all plus the run’s own pods and that same DNS rule; the sidecars a step declares are containers in its own pod, reached over loopback by name. A private destination a pipeline legitimately needs is named inci_egress_allowed_cidrs, which renders as an extra peer on the 443 rule of theegress-httpstier alone, is re-validated when the step is rendered rather than trusted from the settings row (an entry that fails that check is dropped and the rest of the list is still honoured), and rides on the step’s payload so the agent renders the same policy. The agent is what renders that policy in your cluster, so until the agent release of 5 September 2026 reaches it, a step still gets the previous tier - 443 to every address but169.254.169.254, and 53 to any pod. - Image policy. A
runorscanstage’s image is checked againstci_allowed_image_prefixeson the platform and again by the agent before anything runs;checkoutandbuildalways run Ankra’s own pinned images regardless of the organisation’s list. A named service’s image goes through the same check, and a sidecar outside the policy refuses the whole step rather than running beside it. The platform’s check is the one matched at a path boundary, and an entry ending in/,:or@admits exactly that boundary rather than matching nothing (above). The agent’s own fallback check is still a plain textual prefix match, so a neighbouring registry path can satisfy it and a separator-terminated entry is read as plain text rather than as the boundary it names, until the agent release of 5 September 2026 reaches your cluster. - Resources. A stage’s
cpuandmemoryare the step’s limits. The step reserves half of the CPU (never under 100m, or the whole figure when the stage declares 100m or less) and all of the memory on its node, so it can use CPU up to its ceiling when the node has it free and can never take memory another step was given. It may ask for at most 8 CPU cores, 32 GiB of memory and 4 GPU devices, and a GPU step must name aruns_on.node_selector, so it is refused here rather than left Pending until it times out. - Reserved names. A step may not set environment variables beginning
ANKRA_, the loader andPATHvariables,HOME,TMPDIR,XDG_RUNTIME_DIRandXDG_CACHE_HOME, or the image builder’s ownBUILDKIT*,BUILDCTL,ROOTLESSKITandDOCKER_CONFIG. A service the step names may not set them either - those variables decide what a container actually executes, and a sidecar’s process is no exception. - Secrets and credentials. A declared binding is revealed once, to a file under
/run/agent-secretsnamed after the binding, by the agent calling back with its own session token - never written into the step’s payload or a log line. Acheckoutstep’s own git credential is granted the same way and read by a credential helper that answers only for the host it was minted for, so a malicious submodule URL cannot walk off with it. - Forks. A pull request whose head lives in a fork - or whose provenance the delivery could not establish - runs with no secrets, no credentials and no identity beyond reading itself (
fork_policy: read_only, the default).checkoutandbuildboth clone the fork’s own head anonymously - never the pipeline’s own repository - reading the head repository the trigger recorded on the run;checkoutrefuses outright, rather than guessing a remote, on the rare delivery whose fork head repository could not be established at all.buildstill runs (so a maintainer can see whether a contributor’s change compiles) but renders with no push target, so nothing it produces can reach a registry;scanmay still pull the image it built to report on, never a private one. Every stage butcheckout,buildandscanruns with no network at all, regardless of what the fork’s own copy of the definition asks for - the tier is clamped, not merely left alone, so a fork-controllednetwork: egress-httpson some other stage is not honoured just because these three need it. Beyond what such a run may touch, it is bounded in what it may spend: at most 8 matrix legs per stage, a 60-minute step timeout, 2 CPU and 4Gi per step, no sidecar services, no shared cache volumes, and 256 MiB of artifacts and logs for the whole run. The first four are imposed when the run is planned, so the plan a reviewer reads is the run that executes; caches are withheld at dispatch, and the artifact budget bounds what the run is granted from its declared sizes (the same ceiling on what actually arrived is in the artifact store, waiting on the settle lane that will call it). Aplan_fork_budget:diagnostic names the ceilings, and one more diagnostic names each stage that hit one. - Permissions.
pipelines.readcovers listing and reading runs, logs, artifacts, the stored definition, schedules and validation;pipelines.operatecovers dispatch, cancel and re-run;pipelines.managecovers connecting a repository, replacing the stored definition, creating, changing and deleting schedules, the pipeline policy and its approvals, and a dispatch with a--spec-fileoverride.
When something fails
Every failed step carries an error class, and the class decides whose failure it is.- Yours, never retried automatically.
step_failed(the script exited non-zero),step_refused(the definition asked for something the policy or the renderer refuses - an image outside the allow-list, a missing script, too much compute, or abuildstep whose push target cannot be resolved: no application link, a component with no resolvable registry, or a stage that belongs to no component of the application), andtimeout. A step that ran out of time did run; running it again is a retry policy to declare. Theretry:block is validated and carried to the agent but not applied yet, so today a non-zero exit is final. - Ankra’s, retried once.
agent_infra(the cluster could not create the namespace, bind the volume or pull the image),worker_lost(the agent claimed the step and stopped reporting),result_malformed,agent_offline,agent_never_started(no agent claimed the step inside its timeout - check that the cluster is connected andci_worker_countis above zero),build_runtime_confined(below),execution_vanishedandstranded. Each of these writes the next attempt instead of a conclusion, up to two attempts in total; the second failure concludes the step asinfra_error, and the check run settles asaction_requirednaming the class - Ankra failing never reads as your code failing. build_runtime_confined: your nodes confine the image builder, and no setting relaxes it. Abuildstage runs rootless BuildKit in a pod that deliberately names no seccomp or AppArmor profile, because the Pod Security Baseline profile theankra-ci-buildnamespace enforces forbidsUnconfined- naming it would leave the pod refused by the very namespace admitting it. On a node whose runtime resolves “names none” to a confining profile - containerd on an Ubuntu host applying its default AppArmor profile, or a kubelet with seccomp defaulting applyingRuntimeDefault- rootlesskit cannot make its mount namespace private, and the builder dies before BuildKit starts and before anything in your Dockerfile is read. The pod exits non-zero, so the agent reports it as your failure; it is not, and the settle loop re-classes it. There is no cluster, organisation or pipeline setting that renders the build podUnconfined, and that is by design rather than an omission - nothing about a pod spec can defeat the node’s own default from inside Baseline. The supported answer is Ankra platform builders, which takes two things:ci_build_fallback: platform_builders, which is the default, and the platform builds grant, which Ankra makes per organisation on request (that section says how to confirm it and how to ask). With both, the retry, and every later build on that cluster, is routed to Ankra platform builders and the step’sexecutorrecords it. The verdict is remembered per cluster against the agent version and builder image it was observed under, so a cluster pays this failure once rather than on every run - and re-probes by itself when either moves. With the fallback set tononethe build is refused instead, naming the containment and telling you to set the fallback or run the pipeline on a cluster whose runtime leaves the builder unconfined. With the fallback alreadyplatform_buildersbut the grant not yet made, the retry concludesbuild_fallback_nonewith a sentence saying Ankra’s platform-operated build fallback is not enabled for this organisation yet: no setting of yours changes that, so ask Ankra for the grant, then re-run. Every other stage kind is unaffected:checkout,test,scanandgaterun in-cluster on a confining node exactly as anywhere else.- Ankra’s, never retried.
step_stranded- a step stillpendingafter its run has outlived the sum of every step’s owntimeout(capped at the same 6-hour ceiling one step is held to, plus a grace window). It concludes asinfra_errorwith a sentence saying it never started and pointing at the cluster’s agent and itsci_worker_count. A fresh attempt is not written, because it would face the identical exhausted budget; the value of the sweep is that the run concludes rather than sitting open forever.report_unreadablejoins it: ascanstep that concluded successfully but whose report Ankra could not download or parse is revised toinfra_errorcarrying “Ankra could not read the<report>report this step produced, so its findings were not recorded.” Ankra failing to read what a scanner said is not the same fact as the scanner having passed, so it is never recorded as clean.report_unreadableused to be what an organisation with no backup vault got on every scan step it ran; now that such an organisation’s artifacts go to the Ankra-managed default vault, it means what it says - the object was never uploaded, or came back unparseable. The remaining causes are a run that had already spent its 2 GiB artifact budget before the report was due, and a vault whose bucket stopped answering. With no ready vault on either side, ascanstep is refused before it runs instead, with error classartifact_store_unavailableand the sentence “This step’s reports need a ready backup vault in the organisation, and there is none.” A step that had already failed, timed out or hitinfra_errorkeeps its own verdict - that is a more specific answer than this one. - Timeouts. A stage runs under its
timeout, ordefaults.timeout, or 30 minutes; the ceiling is 6 hours and a longer value is clamped and recorded. The agent stops a step at its deadline with a 60-second grace, and a reaper on the platform sweeps every 30 seconds for steps two watchdogs missed - a step five minutes past its deadline, an agent that has not checked in for ten. The run itself is heartbeated while any of this is going on, so a long pipeline is not failed by the platform’s own stale-run reaper for taking longer than twenty minutes. - Cancel.
ankra pipeline cancel <run>,POST …/pipeline-runs/{run_id}/cancel, or the Cancel button on the check run while the run is live. A run that is cancelled concludes with outcomecancelled. - Re-run.
ankra pipeline rerun <run>(add--failed-onlyto re-run only the steps that did not succeed, whatever depends on them, and the steps they build on),POST …/pipeline-runs/{run_id}/rerun, or GitHub’s own Re-run button on the check. A re-run is a new run withrerun_of_run_idpointing at the old one; the old run’s outcome stays the record of what happened. It re-plans from the original run’s own definition, not from whatever the pipeline has become since, so re-running a month-old run reproduces it rather than quietly executing a different pipeline. - Failed-only re-runs start from an empty workspace. A re-run gets a workspace of its own - the original’s is removed once it concludes - so
--failed-onlyre-runs the checkout and every upstream step whose output a repeated step reads (throughneeds, and for agatethe stages in itsrequire_stages) together with the failed steps and their dependents. Steps that succeeded and that nothing repeated needs, such as a publish after the failure, are not repeated. A failed-only re-run that could not run a step that failed - a stage limited bywhen.eventstopush, which a re-run’sreruntrigger filters out - is refused with the reason for each such step, rather than queued to pass without it.
Diagnosis codes
A step that failed for a reason Ankra recognises, or that has waited more than two minutes for its pod to start, carries adiagnosis: a stable code, a title, a next_step and, where there is one, the cluster’s or the provider’s own words in detail (cleaned of hidden characters and anything that looks like a credential, and cut to 300 characters). The run carries the one a reader should act on first in its own diagnosis, and the run page shows it above the steps. A step that failed in its own script has no diagnosis, because its log is the answer. A running step whose cluster events could not be read says so in diagnosis_unavailable rather than showing nothing.
Ankra reads the evidence it already has, so nothing extra runs on your cluster: the step’s error class and the agent’s message, the Kubernetes events on the step’s pod and its workspace volume, the Cluster Autoscaler’s events, and, for node groups Ankra scales, its own record of the node create the provider refused. It reads only events of clusters your organisation owns.
Why a step, or a CI cluster, is waiting
Why a step or run failed
A notice on a passing step
The codes are a fixed vocabulary that monitoring and scripts can key on; the sentences may be reworded.
Status is the lifecycle, outcome is the verdict
A run and each of its steps carry two fields, and only one of them says how the work ended.status is where the work is in its life; outcome is how it finished, and it is null until status is concluded. This is the same split as GitHub Actions’ status / conclusion: a monitor that filters steps on status == "failure" matches nothing, because every finished step is concluded - read outcome for the verdict.
The outcome vocabulary is one set for runs and steps alike:
A run’s outcome folds its steps’ outcomes:
success when every step succeeded or was skipped, otherwise the worst of what its steps reported. ankra pipeline get <run> prints the outcome in place of the status once there is one, with a glyph per state - ✓ success, ✗ failure / timed_out / infra_error, ⊘ cancelled, ○ skipped, and ⟳ only for a run or step that has not concluded (○ for a blocked step) - and -o json carries both fields verbatim.
A run also records the protected authority it executed under, as authority_state (the Authority: line in ankra pipeline get):
authority_hash and authority_definition_id sit beside it and are null in the same cases. See Logic is open, authority is closed for what each state means for the run’s protected sections.
Limits and defaults
Bring your own CI (GitHub Actions)
You do not have to delete your existing CI to try Ankra Pipelines - the generated GitHub Actions workflow keeps building, scanning and publishing images to your registry, and a pipeline run on the same commit is a second, independent check,Ankra pipeline beside the workflow’s own. But connecting the repository is not inert: the moment .ankra/pipeline.yaml is the definition of record and the organisation has a CI cluster, the application is read as pipeline-sourced and its pull-request previews switch to the pipeline’s own digest from then on - not when you decide you trust it. A merge to your default branch is more forgiving, because your own CI’s build observation always outranks the pipeline’s for it, so the workflow stays what actually deploys production until the pipeline’s publish catches up or you retire the workflow. Retirement is deliberate either way: Ankra will not delete a workflow for you; when it does propose one, it will be a reviewable pull request.
build and publish now run in the cluster, so a pipeline can publish your image on its own once you trust it - review what to check after conversion first, and keep the workflow live until you have watched the pipeline publish correctly a few times. Migrate from GitHub Actions explains how a converted pipeline maps onto it and what to review; an application already connected to Ankra can skip the manual conversion entirely and migrate with one call.
Commands and API
Every command addresses one pipeline with--application <name-or-id> or --repository <repository-id>. Run from inside a checkout, both are optional: the CLI resolves the application from the repository your origin remote points at, and says which one it used. It asks for the flag when the working directory answers nothing, or names the candidates when the repository carries more than one application. ankra application pipeline … is the same set of commands with a leading <application-id> argument. Tables by default, -o json for the wire shape.
ankra pipeline repositories connect|list|get|disconnect, ankra pipeline definitions get|approve, ankra pipeline findings, the concluded-step behaviour of logs and artifact listings that carry a status have all merged and ship in v0.15.0, which is a pre-release today - enable the beta channel to get them, or use the API and the portal until v0.15.0 is stable. ankra cluster agent ci get|set, which turns a cluster’s pipeline-step workers on, lands in v0.15.0-rc3; until then that setting is PUT /api/v1/org/clusters/{cluster_id}/agent/ci-settings on the API.
The same run and repository operations are on the API under /api/v1/org/applications/{application_id}/pipeline-runs, /api/v1/org/pipeline-repositories/{repository_id}/pipeline-runs and /api/v1/org/pipelines/repositories with a bearer token: list and create runs, …/{run_id}, …/{run_id}/rerun, …/{run_id}/cancel, …/{run_id}/steps/{step_id}/logs (server-sent events, resumable with from_seq), …/{run_id}/artifacts, …/artifacts/{artifact_id}/download (a 302 to a presigned URL), and beside them …/pipeline (GET, PUT), …/pipeline/validate, …/pipeline/schedules, …/merge-train (GET), …/merge-train/settings (PUT), …/merge-train/entries (POST) and …/merge-train/entries/{entry_id} (DELETE), and …/pipelines/definitions/{id}[/approve]. Creating a run answers 202 with the run id and its number; connecting a repository answers 201 with the repository and what happened to its committed definition, or 409 naming the one that already exists.
What comes next
These are on the plan of record and not available yet. None of them is usable today, and this page will say so when that changes.- The runner for
verify(HTTP probes, smoke scripts, k6 load tests, PromQL assertions, restore verification). - The per-run identity that
permissionsmints, so a step can call theankraCLI with exactly the authority the default branch granted;cloudandobject_storagecredentials once it exists. - The built-in
ankra/*step library behinduses:, organisation step templates, andcustom_tool/<name>steps. - The executors for the
preview(inside the pipeline DAG),agent,approval,webhook,automation,ankraandexternalstage kinds;on.webhookinbound dispatch tokens. Each of those stages validates with a warning and is skipped today, so declaring one costs you the stage, not the run. - The user-declared
retryblock being applied; dynamic matrices that fan out over a previous step’s output; a dedicated run page in the portal (the Pipelines section shows runs today; a standalone page with more depth is still to come). - Attestations and SBOM referrers carried onto the digest
publishre-tags into your real repository, not only the onebuildpushes to staging. - Merge-when-green, automatic disposition re-evaluation, and planning from the committed file at a run’s own commit, on GitLab and Bitbucket Cloud (GitHub only today).
- The agent release that publishes live step output on the dedicated
pipeline_outputstream the platform already reads. (The narroweregress-httpswith itsci_egress_allowed_cidrsallow-list, and the image-prefix boundary check, were listed here and have since shipped on both the platform and the agent - see What is in place today.) - Environments hydrated from restore points, verification stages, promotions with approvals, and canary rollouts through Traefik weighted traffic.
Next steps
pipeline.yaml reference
Every key, the expression language, the trigger table and the diagnostics the validator reports.
Migrate from GitHub Actions
Convert a workflow, GitLab CI file, Bitbucket pipeline or CircleCI config and review what changed.
Application CI/CD
The generated GitHub Actions workflow, registry authentication and image retention.
Backup vaults
Where pipeline logs, artifacts and scan reports are kept - a ready one is a precondition for a run.