Skip to content

Deploy from GitHub Actions

A CI job deploys with the same CLI a person uses, holding its own credential. Nothing about the installation has to be administered to set it up: an owner of an organization creates a deploy token, puts it in a repository secret, and the job deploys.

A project created by ankka init already carries the workflows described here. This page is what they do and how to point them at your installation — and how to do the same thing from any other CI system.

What a generated project carries

File Runs on What it does
.github/workflows/ci.yml every push and pull request sbt test. Needs no secrets, so it is green from the first push.
.github/workflows/deploy.yml a v* tag, or run by hand Builds the image, pushes it to the repository's GitHub Container Registry, and deploys. Declines to run when its secrets are absent, so a first push shows no failure.

The snippets below are the pieces those files are built from, written out so they can be adapted. The files themselves are not quoted here. In the template each GitHub expression carries an escape that the project generator removes on expansion, so quoting the source would put that escape on the page and mislead anyone copying it. What keeps the files correct is the template's own test, which expands a project and asserts every expression survived.

Create a deploy token

ankka organizations tokens create acme --label github-deploy
Deploy token 'github-deploy' created. This is the only time the secret is shown.

  ankka_3f9a1c2e7b4d8f01_9c2e…a1f0

Expires 2026-12-24T10:00:00Z. Store it as a secret named ANKKA_TOKEN.

The secret is shown once and stored only as a one-way digest, so a lost token is replaced rather than recovered. It authenticates as a member of that organization: it can apply, pause, resume, restart, expose and delete services and read their logs and history, and it cannot invite members, rename or delete the organization, or manage deploy tokens — including itself, so a leaked credential cannot mint a replacement or revoke the one that would stop it.

A workflow that deploys into one project is better served by a token for that project alone:

ankka organizations tokens create acme --label github-deploy --project storefront

That token is a member of storefront and of no other project, which it is told does not exist, and it can also make and end storefront's own grants. ankka organizations tokens list acme shows each token's project in its PROJECT column, - for a token of the whole organization.

A token expires after 90 days unless you say otherwise (--expires-in 30d, up to 365 days, or --never-expires). See Identity and machine accounts for the whole model, including the Keycloak-client alternative.

Add three secrets

In the repository's Settings → Secrets and variables → Actions:

Secret Value Where it comes from
ANKKA_URL the control plane's address, https://api.<base domain> ankka config get url
ANKKA_TOKEN the deploy token's secret the command above
ANKKA_PROJECT the project to deploy into ankka projects list
ANKKA_CA optional: the installation's certificate authority, as PEM cat ~/.ankka/local-ca.crt, for an installation whose certificate is not publicly trusted

ANKKA_URL and ANKKA_PROJECT are not secret, but keeping all four in one place means one screen to fill in rather than a workflow to edit.

Use the action

- uses: thinkmorestupidless/ankka-action@v1
  with:
    url: ${{ secrets.ANKKA_URL }}
    token: ${{ secrets.ANKKA_TOKEN }}
    project: ${{ secrets.ANKKA_PROJECT }}
    ca: ${{ secrets.ANKKA_CA }}
- run: |
    ankka services deploy orders ghcr.io/acme/orders:1.4.2
    ankka services get orders

The action installs the CLI, verifies it against the checksum published beside the release, and sets the environment for the rest of the job — so every later step runs any ankka command with no further setup. It does not wrap commands: services deploy, services logs, projects list and everything else work exactly as they do in a terminal.

No Java is needed. The action installs the CLI's native executable for the runner: Linux or macOS, x64 or ARM64. A Windows runner is refused before anything is downloaded, with a message naming it.

The build step before it, in the workflow a generated project carries, builds the image for both linux/amd64 and linux/arm64, so the tag it deploys serves a cluster of either; remove one from ANKKA_IMAGE_PLATFORMS in that workflow to build for one. See Build an image.

Deploy the image the build just pushed

ankka services deploy takes the image on the command line and everything else from the descriptor:

ankka services deploy orders ghcr.io/acme/orders:1.4.2

service.json stays as it is in the repository — the file is never rewritten, and the one field that is genuinely different on every build comes from the build. The command refuses when the descriptor names a different service, so a workflow pointed at the wrong file stops rather than deploying something unexpected. See Deploy a service and Build an image.

Tag each build. A cluster runs imagePullPolicy: IfNotPresent, so re-pushing the same tag does not give a node the new image; a new tag is a new descriptor, a new generation and a rolling update. See Tag by version for anything real.

The cluster has to be able to pull

The job pushes the image; the cluster pulls it, with no access to the job's credentials. A package on a registry that requires authentication therefore needs a credential registered for the project, once:

ankka projects registry set checkout --server ghcr.io --username octocat --password-stdin

A GitHub Container Registry package is private by default, so either make the package public or register a credential whose password is a personal access token with the read:packages scope. The token the job itself pushes with is not usable here: it exists only while that job runs. See a private registry.

A missing or wrong credential shows as ImagePullBackOff in the detail of:

ankka services get orders

Exposure is not a build step

A service is reachable inside the cluster as soon as it is Ready. Giving it a public hostname is a separate decision, taken once:

ankka services expose orders

Exposure is desired state and survives every later deploy, so no workflow needs to repeat it — and no workflow decides on its own that a service should face the internet. See Expose a service.

A project of several services

A project started with ankka init <project> --project keeps several services in one repository and releases each on its own. Its deploy.yml runs on a service tag, <service>/v<version> such as orders/v1.4.2, and on nothing else:

  • The tag names the service, and the version is what follows /v. A tag naming no service of the project fails the job, saying so; a plain v1.4.2 tag starts nothing.
  • The service's checks run first, through the project's reusable service-checks.yml, the same checks a pull request runs for that service. A service whose checks fail is not deployed.
  • One image is built and pushed, for that service, and one ankka services deploy names it, with the service's own services/<service>/service.json. No other service is built or deployed.
  • The secrets are the same four, set once for the repository: ANKKA_URL, ANKKA_TOKEN, ANKKA_PROJECT and, when needed, ANKKA_CA.

The project's ci.yml runs on every push to a branch and every pull request, never on a tag, checks only the services the change touched (every Scala service when the shared module changed), runs the tests that cross services, and needs no secret. See Build a project of several services.

Any other CI system

Nothing above needs GitHub beyond the action, which is a convenience. The CLI resolves each setting from a flag, then an environment variable, then its configuration file, and a CI job normally has no configuration file at all:

Variable Flag Meaning
ANKKA_URL --url the control plane's address
ANKKA_TOKEN --token the token to present
ANKKA_PROJECT --project, -p the project to act on
ANKKA_CA none a PEM file of the certificate authority to trust
ANKKA_CONFIG none an alternative configuration file path

Prefer ANKKA_TOKEN to --token: a flag's value appears in process listings and often in the job's log. Installing the CLI by hand is a download and an unzip:

curl -sSLO "https://github.com/thinkmorestupidless/ankka/releases/download/v$ANKKA_VERSION/ankka-cli-$ANKKA_VERSION.zip"
curl -sSLO "https://github.com/thinkmorestupidless/ankka/releases/download/v$ANKKA_VERSION/ankka-cli-$ANKKA_VERSION.zip.sha256"
sha256sum --check "ankka-cli-$ANKKA_VERSION.zip.sha256"
unzip -q "ankka-cli-$ANKKA_VERSION.zip"
export PATH="$PWD/ankka-cli-$ANKKA_VERSION/bin:$PATH"

On macOS, brew install thinkmorestupidless/tap/ankka instead.

Exit codes

Code Meaning
0 the command succeeded
1 the command failed: refused, not authorized, or the control plane could not be reached
2 the command was misused: an unknown command, option or argument

A refused command prints error: <reason> on standard error. A token that has expired or been revoked is reported as the token was rejected; a valid token for an action its holder may not take is not permitted — the two are different answers and the CLI keeps them apart. Use -o json for output a script reads:

ankka services get orders -o json | jq -r .lifecycle

Every change names its actor

A deploy from CI is attributed to the token that made it, distinguishably from the person who created it:

ankka services history orders

See Status and history.