Skip to main content
Ankra converts an existing CI definition into an Ankra pipeline - the .ankra/pipeline.yaml its own engine runs inside your cluster. One converter reads four formats: GitHub Actions workflows, a .gitlab-ci.yml, a bitbucket-pipelines.yml and a .circleci/config.yml.
A converted pipeline is a starting point a person reviews, never a drop-in replacement. The rule the converter holds to: something it cannot map is either carried across as a stage that fails with an explanation, or reported as a note with the job or step it came from - never dropped silently. A converted pipeline that quietly skipped a toolchain step would go green having built nothing.

Migrate an existing application with one call

For an application already connected to Ankra, this is the fast path, and it takes effect the moment you ask. An application that still builds from a GitHub Actions workflow - the one Ankra generated at setup, or one your repository wrote itself - shows Convert to Ankra Pipelines on its journey card and Convert on its row in the Applications list. The same conversion is one command or one call:
One click does all of this:
  • Reads what builds the application today and converts it into .ankra/pipeline.yaml through the same converter Ankra AI uses, then stores that as the application’s pipeline definition of record.
  • Records the application as building on Ankra Pipelines (pipeline_source becomes ankra_pipeline) right away, so the next push to the tracked branch builds through the pipeline. Nothing waits on a merge.
  • Switches Ankra’s generated workflow off on GitHub, so the workflow and the pipeline never build the same commit twice. If the GitHub App lacks the Actions permission the answer says so, and the workflow stops building when the pull request that removes it merges.
  • Opens a pull request on the fixed branch ankra/pipeline-migration that commits the pipeline file and removes the generated workflow file. --keep-workflows ({"remove_workflows": false}) leaves the file in the repository, still switched off. A workflow your repository wrote itself is never touched either way.
The pull request is the repository catching up with what Ankra already runs: merge it when you are happy with the file. Converting again while it is still open answers the same conversion (already_open) and re-checks that the workflow is switched off; if you closed the pull request without merging, converting again proposes it afresh. An application with no detected CI, one already on Ankra Pipelines, or an organisation with no CI cluster to run the result on is refused with the reason rather than opening a pull request with nowhere to go. A service token may convert an application exactly as it may dispatch a run. The conversion needs the repository’s stored GitHub credential today. GET .../pipeline-migration reads what builds the application today and what a conversion would act on, without changing anything.

Convert with Ankra AI

Ask Ankra AI in the portal chat, or call the tool through the MCP server. Example prompts:
  • “Convert .github/workflows/ci.yml and .github/workflows/build-deploy.yml into an Ankra pipeline and show me every note.”
  • “Convert this .gitlab-ci.yml into .ankra/pipeline.yaml for the orders-api application.”
The tool behind it is cicd_convert_workflow_to_pipeline: It answers with the rendered pipeline_yaml, the notes naming every construct it could not carry across faithfully, and a validation verdict. It writes nothing to the repository: open the pull request yourself, or ask the AI to open one. Its twin, cicd_validate_pipeline_yaml, validates a definition you wrote or edited and lists every violation with its key and severity - see Validation.

What maps to what

Jobs become stages, needs becomes needs, conditions keep their ${{ }} delimiters, and the steps, matrix, inputs, env and needs references map one to one. A reference to the github context does not: the validator reports it with the Ankra equivalent (github.sha is ankra.sha, github.event_name is event.kind, and so on) for you to rewrite. See Expressions for the two deliberate departures. Third-party steps go through one mapping table, so a GitHub action, a Bitbucket pipe and a CircleCI orb command that do the same work become the same stage. A row matches every version of its identifier (actions/checkout@v4, atlassian/slack-notify:2.1.0).

Steps that become a stage

No converter emits a uses: reference any more. Until the built-in step library ships, any uses: is refused outright at validation - “Reusable steps are not available yet; write the step as run:” - so a converted file carrying one could not be planned at all. Each construct that used to map onto an ankra/* step is therefore emitted as the run: stage above, with a note naming the library step it stands in for.

Steps that become the stage image

actions/setup-node, actions/setup-go, actions/setup-python, actions/setup-java, circleci/node/install and circleci/go/install do not become stages. Each becomes the image the job’s later stages run in - node:<version>, golang:<version>, python:<version>, eclipse-temurin:<version> - because a stage that runs in that image has the toolchain the step installed. 20.x and 20 both become node:20; a matrix expression such as ${{ matrix.node }} is carried into the image. lts/*, latest, a range or a version file names a resolution rule rather than a version, so no image is derived and the note tells you to pin one on the job’s stages. A setup step with no version at all leaves the job’s stages on the default image, which then has to ship the toolchain.

Steps that are dropped, with a note

Everything else

A third-party step the table does not know becomes a run stage that stops the run and says what has to be replaced:
Failing is the point: a dropped step would produce a pipeline that runs green having skipped whatever the step did, and you would have no way to tell a converted step from a discarded one.

What each source loses

Every item below arrives as a note with the file, job or step it came from. Read the notes before the YAML. GitHub Actions
  • The permissions block is not converted: a run’s authority is the intersection of the pipeline’s own permissions, the organisation’s CI policy and the target environment, not the workflow token’s scopes.
  • A trigger with no Ankra equivalent is not converted, and a trigger that narrows on something an Ankra trigger cannot (for example pull request types) loses the narrowing: the pipeline runs on more of those events than the workflow did.
  • defaults.run.shell is not converted; a stage runs its script with the shell its image provides.
  • Several workflows fold into one pipeline whose on: is their union - a path filter that one workflow declares and another does not leaves the union unfiltered, so every run the workflows started still starts. Each workflow’s own push and pull_request branches and paths stay on its stages as event-scoped when blocks wherever they are narrower than the union, and a workflow that does not listen to an event the pipeline does keeps its stages out of it with when.events. A note names the workflows this applied to.
  • A second concurrency block is not converted: a pipeline carries one concurrency group, and the first workflow claimed it.
  • A job that calls a reusable workflow (uses: ./.github/workflows/…) cannot be followed; its stage fails until it is replaced.
  • A workflow that is not valid YAML contributes nothing, and the note says which.
GitLab CI
  • workflow:rules is not converted, so runs it filtered out are not filtered here.
  • A top-level services: block, which gives every job a sidecar, is not converted: declare the services the stages need in the pipeline’s services block and list them on each stage, or the jobs that relied on them fail at runtime.
  • A job that triggers a downstream pipeline has no equivalent.
  • except: is not converted: an Ankra stage filters on the branches, paths and events it runs for, never on the ones it skips. A rules: block carrying when: never on a changes: list is not converted, and its paths go with it: that rule skips the paths it names, and an Ankra when.paths says which paths the stage runs for, so carrying them across would invert the meaning. Otherwise its changes: paths are carried, onto the stage’s flat when.paths unless the rule names an event. A rule whose if: tests $CI_PIPELINE_SOURCE == "merge_request_event" becomes when.pull_request.paths, one testing == "push" becomes when.push.paths (a matching rule with no changes: leaves that event unfiltered), and the rest go to when.paths. Any other if: condition, and the rules’ order, are not carried, and the note says so.
Bitbucket Pipelines
  • A step size of 1x, 2x, 4x or 8x becomes a memory request; any other value is reported and you declare resources.memory yourself.
  • bookmarks: pipelines belong to Mercurial and are not converted.
  • A parallel group’s fail-fast is not converted: an Ankra run lets the other stages of a group finish when one fails.
  • A pipeline that imports from another repository cannot be followed and contributes nothing.
  • runs-on labels are not converted: an Ankra stage is placed by the organisation’s CI cluster and the stage’s own runs_on block.
  • A manual trigger becomes an approval stage approvable by the admin and operator roles, because anyone with write access could press the Bitbucket button and no Ankra role maps onto that; narrow the roles to whoever actually held the button.
  • A pull-requests: pattern is not carried as a filter. Bitbucket matches it against the pull request’s source branch, while an Ankra pull_request trigger and a stage’s when.branches match the target, and nothing in a pipeline filters on the source branch. Each section therefore runs for every pull request (on.pull_request.branches: ['**']), and the note suggests a push trigger on the pattern if that is the filter you need. A '**' section gets no note.
  • A step that a branch section shares with a pull request, tag or custom section loses its branch filter, because a stage carries one branch list for all its events; the note names the filter that was dropped.
  • clone: lfs: true and clone: enabled: false are not converted: the checkout stage does not fetch Git LFS objects, and every stage shares the workspace it fills. A step’s own clone: settings are not converted either, because a run checks the repository out once.
Two Bitbucket settings carry across without a note: clone: depth becomes the checkout stage’s with.fetch_depth (full becomes "0", the whole history), and a step’s condition.changesets becomes its when.paths - the includePaths entries followed by each excludePaths entry negated with !. The planner lets the last matching entry decide, so a changed file counts exactly when Bitbucket would count it. As in Bitbucket, a step whose changesets match nothing is skipped on its own: the converted step after it lists it under skippable_needs, so it still runs, and waits on the filtered step only when that step runs. A manual step’s approval carries the same path filter, so a push that skips the step leaves no approval waiting. CircleCI
  • A version 2.0 configuration converts the way a 2.1 one does; any orb, command or parameter it cannot declare is simply absent. A setup configuration’s continuation pipeline cannot be followed; only the jobs declared in the file are converted.
  • An inline orb is not converted; every step that calls it becomes a stage that fails until it is replaced. An orb job the converter cannot follow is reported the same way.
  • A parameter whose name carries a dash is renamed (an input name may not carry one), and every reference to it is renamed the same way.
  • An approval job becomes an approval stage approvable by the admin and operator roles, with the same advice to narrow them.
  • Anchors and aliases are resolved before conversion, so an anchored executor or step list behaves like an inline one.

What to review after conversion

  1. The notes, first. Each is a decision the converter took on your behalf or could not take. Stages that fail by design (exit 1 with the reference) are in the YAML so you cannot miss them.
  2. Images. Every stage needs an image the organisation’s image policy allows - and so does every sidecar you declare in the pipeline’s services block, whose image and env are held to the same boundary a step’s own are, refusing the whole step rather than running beside it. A policy prefix is matched at a path boundary, and one ending in /, : or @ admits only that boundary (ghcr.io/my-org/ admits ghcr.io/my-org/api and not ghcr.io/my-org:v1) - the settings API refuses a trailing separator at write time anyway, so write the bare prefix. Pin the toolchain images the notes could not derive.
  3. Network. A run stage’s tier defaults to none, which denies all egress; give defaults.network: egress-https, or a network on the stage itself, to the ones that install packages or reach a registry. The converted checkout, build and scan stages need nothing said about them: those three kinds default to egress-https (the tier a stage gets by default). That tier reaches public addresses only, so a job that pulled from a VPC-internal mirror, an in-cluster registry or an internal Git host needs its range in ci_egress_allowed_cidrs before the converted stage runs, and without it fails on a connection timeout rather than a permission error. Remember that anything above egress-https is a protected section - as are a stage’s timeout, cache, resources and with: block, and the whole services block. A converted pipeline that declares any of them runs its pull requests under the last approved authority until an administrator approves the definition on your default branch; see Logic is open, authority is closed.
  4. Secrets and credentials. The converter declares what the workflow referenced; the secrets block names a source (app_env_secret, org_variable, registry, credential) that has to exist in the organisation. Delivery is live for secrets sourced app_env_secret, org_variable or registry, and for credentials sourced git or registry - each revealed once, at step start, to a file the step reads. secrets: [{from: credential}] still validates but is not deliverable yet, and a credentials binding sourced cloud or object_storage is refused by name until the run has its own scoped identity - reconsider any converted cloud-credential step against that gap.
  5. Filters. Where a rules:, except: or trigger types narrowing was lost, add when: or if: so the pipeline does not run more often than the workflow did.
  6. Resources and timeouts. A stage takes 500m CPU, 1Gi memory and 30 minutes unless it says otherwise; a Go build or a Playwright suite wants more of the first two. A pull request from a fork gets less than a converted workflow may expect, whatever the file asks for: 2 CPU, 4Gi, a 60-minute step timeout, 8 matrix legs per stage, no sidecar services and no caches, with 256 MiB of artifacts and logs for the whole run. The clamps are applied to the plan, so the pull request’s own status comment shows the smaller run rather than the one the file declared - see Forks.
  7. Caches, if you add one by hand. A cache: block does not behave the way actions/cache does across branches. An Ankra cache claim is scoped to the trust of the run that resolved it, so every run that is not on your default branch - a feature branch, a tag, any pull request - shares one non-default scope, and none of them can read or write the claim your default branch’s builds use. GitHub restores a default-branch cache into any branch; Ankra deliberately does not, because that is the path a less-trusted ref uses to poison what production builds from. Budget one cold build per cache key on each side of that line, and none at all on a fork’s pull request, which resolves no shared cache.
  8. What executes today, and what the converter never adds. checkout, run, build, scan, gate, publish and deploy all execute end to end; verify and the kinds with no executor yet (preview, agent, approval, webhook, automation, ankra, external) validate with a warning and are skipped, so a converted approval stage blocks nothing and a converted webhook notifies nobody. A deploy stage releases only to host targets outside Kubernetes, so a converted job that ran kubectl or helm does not become one; deploy to a cluster through the application’s push to deploy instead - see What is in place today. The converter itself never writes a gate or a publish stage - no source format has one to map from - so a workflow whose docker/build-push-action pushed straight to your registry converts into a pipeline whose build stage pushes a digest to Ankra’s own CI staging repository only, and stops there: without a gate stage after scan and a publish stage after gate, nothing ever reaches your real repository. Add both by hand (see the worked example) rather than assuming the converted file publishes what the workflow did.
Then validate it:

Run both side by side

Keep the workflow. A pipeline run and a workflow run on the same commit are independent: the workflow’s checks keep their names, the pipeline’s is Ankra pipeline, and both appear on the pull request. But know what actually decides which one a pull request’s preview deploys from, because it is not a setting you flip: .ankra/pipeline.yaml outranks a workflow the moment it is committed on the default branch and the organisation has a CI cluster to run it on - the application reads as ankra_pipeline-sourced from that point on, whether or not a workflow still runs alongside it, and its previews deploy the pipeline’s own digest from then on (one that has not built the head yet answers build missing rather than quietly falling back to the workflow’s image). A merge to your default branch is more forgiving: your own CI’s build observation always outranks the pipeline’s, so a workflow that keeps publishing keeps being what gets deployed, and the pipeline’s digest is only used once nothing higher-ranked has been observed for that commit. Retire the workflow deliberately, once you have watched the pipeline publish correctly on its own for a while. Two rules for the overlap:
  • Do not make Ankra pipeline a required check in branch protection until it has been green for as long as you would trust any new CI system, and never while its stages include a kind that does not execute yet. Such a stage is skipped, not failed, so the check goes green having done none of what the stage names - read the run’s skipped stages, not only its conclusion.
  • Retire the workflow deliberately. Ankra does not delete a workflow for you; when it proposes retiring a generated one, it will be a reviewable pull request that you merge.

Ankra Pipelines

Prerequisites, the first green run, the security model and limits.

pipeline.yaml reference

Every key, the expression language and the diagnostics the validator reports.