Skip to content

Type to search pages.

View .md

When one machine is not enough

The first two pages need no server. af up builds an environment on the machine it runs on, af ci does the same inside a workflow, and nothing calls home.

A control plane is what a team adds when one person’s laptop stops being the right place for the answer: environments that outlive a CI job, a reviewer who can open one, scheduling across a queue, quotas, and history.

AF-CP-001 The control plane at https://cp.example.com could not be reached.
Next: Antifailure works without it. Run af logout, or unset
AF_CONTROL_PLANE_URL, to work fully locally.

Events are buffered and delivered when it returns, environments keep running, and teardown still works, because teardown reads the local journal and not the control plane.

The first prepares the database, the second serves requests. They need different credentials, and the serving step has no migration credential at all.

Terminal window
# 1. Apply the schema, create the application role, grant it its membership.
docker run --rm \
-e AF_MIGRATION_DATABASE_URL=postgres://owner:...@db:5432/antifailure \
-e AF_DATABASE_URL=postgres://af_app:...@db:5432/antifailure \
ghcr.io/antifailure/control-plane:main-fa6c8aa node bootstrap.mjs
# 2. Serve.
docker run \
-e AF_DATABASE_URL=postgres://af_app:...@db:5432/antifailure \
-e AF_GITHUB_CLIENT_ID=... \
-e AF_GITHUB_CLIENT_SECRET=... \
-e AF_GITHUB_REDIRECT_URI=https://cp.example.com/auth/github/callback \
-p 8080:8080 ghcr.io/antifailure/control-plane:main-fa6c8aa

On Kubernetes, the chart in deploy/helm/antifailure-control-plane runs step 1 as a Job before the Deployment rolls.

The tag names the commit the image was built from. Pin a main-<sha>, not :latest or a version tag, because only a sha tag names anything checkable. Which tag to run has the details and the command that lists what is published.

Do not skip step 1, and do not trust a 200

Section titled “Do not skip step 1, and do not trust a 200”

Step 1 is what grants the application role its membership. Skip it and the failure is quiet instead of loud: the server starts, /health answers 200, the container reports healthy, and every query fails with

ERROR: relation "organizations" does not exist

which reads like a missing migration and is not one. Postgres does not tell a role that lacks USAGE on a schema that it lacks permission. It tells it the relation is not there.

So the check that means anything is the membership itself, not the health endpoint:

SELECT pg_has_role('af_app', 'antifailure_app', 'MEMBER');

The control plane is not a manifest key. It lives with the credential:

Terminal window
af login --control-plane https://cp.example.com

The token goes straight into the operating system’s credential store. It is never shown, never copied through a clipboard, and never written to a shell history file. AF_CONTROL_PLANE_URL sets the same thing for a runner that cannot open a browser, and af logout removes it and revokes it everywhere.

Then set github.mode to app in the manifest, which is a fact about the repository, so environments outlive the job and a reviewer can open one:

github:
mode: app

An environment appears in the console because the engine reported it, not because anything here created it. Every environment.* event carries the repository as owner/name, the branch, the pull request number when there is one, and the lifetime runtime.ttl declares, and the control plane creates the environment from whichever of those events reaches it first.

Each of those events also carries the instant the environment began existing, which is not the instant the event fired: an environment is reported ready after its build. Usage and the expiry are both measured from the earlier instant.

The repository name comes from GITHUB_REPOSITORY when the run is in GitHub Actions, and otherwise from the origin remote of the checkout. A checkout with neither reports no repository: the environment runs, and it does not appear in the console. The response says so on the event, and the control plane counts it as af_ingest_events_total{outcome="unprojected"}.

A repository the GitHub App has never mentioned is created from the name the engine reports rather than refused.

Related: the full control plane guide, every variable it reads, running it on Azure.