Skip to main content

Deploy from Git

Push code, let Pagifier notice, and keep the CI plumbing out of your repository.

Your platform team connects GitHub, GitLab, or your company Git service once. You commit a small control manifest to a protected branch. After that, signed webhooks start work immediately and Pagifier's reconciliation loop catches anything a webhook misses.

What you need from the platform team

Ask for the Git connection IDs you may use, the templates available to your tenant, and a token with pipeline access. Provider tokens, webhook secrets, and the cluster stay on the platform side.

Choose the small template

You do not have to design a pipeline graph for normal applications. Pagifier ships four versioned templates:

TemplateUse it when
single-repo-serviceOne repository change should build and deploy the app
two-repo-serviceApplication changes build; an operations-repository pin deploys
build-onlyThis repository only produces immutable artifacts
deploy-onlyThis repository only promotes pinned artifacts

The two-repository template is the safest production default: merging application code produces an artifact, but changing production still requires an explicit, reviewable pin in the operations repository.

The two-repository flow

application repository operations repository
---------------------- ---------------------
src/** changes
|
v
Pagifier builds an immutable
pagifier:// artifact
release pin changes
|
v
approval, if required
|
v
Pagifier deploys that
exact artifact

No "latest successful build" lookup happens during deploy. The pin names the exact artifact, so a retry tomorrow promotes the same bits.

1. Keep the project file with the app

The version 1 project file describes what actually runs:

app.pagifier.toml
version = 1
name = "frontend"
template = "react-static"
environment = "prod"
ingress = "frontend.company.com"

This is the same application configuration used by the upload API. The Git control manifest simply points at it.

2. Render the control manifest

The web console has a Set up Git deployment form, or you can ask the API for a commit-ready file:

curl --fail-with-body \
-H "Authorization: Bearer $PAGIFIER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name":"frontend",
"buildConnection":"git_application",
"buildRepository":"acme/frontend",
"deployConnection":"git_operations",
"deployRepository":"acme/deployments",
"environment":"prod",
"projectFile":"app.pagifier.toml",
"pinFile":"apps/frontend/prod.toml",
"approvalRequired":true
}' \
"$PAGIFIER_URL/v1/pipeline-templates/two-repo-service/render"

Commit the returned pagifier.toml to a protected branch. It looks like this:

pagifier.toml
version = 2
name = "frontend"
template = "two-repo-service"
templateVersion = "1"

[repositories.application]
connection = "git_application"
slug = "acme/frontend"
ref = "refs/heads/main"

[repositories.operations]
connection = "git_operations"
slug = "acme/deployments"
ref = "refs/heads/main"

[build]
repository = "application"
projectFile = "app.pagifier.toml"
paths = ["src/**", "package.json", "package-lock.json"]

[deploy]
repository = "operations"
environment = "prod"
pinFile = "apps/frontend/prod.toml"

[environments.prod]
approvalRequired = true
approvalCount = 1
approverRoles = ["approver"]

The paths list keeps README-only changes from waking a builder.

3. Register it once

Registration fetches and validates the manifest, snapshots its commit, and starts watching every repository it names:

curl --fail-with-body \
-H "Authorization: Bearer $PAGIFIER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"connectionId":"git_operations",
"repository":"acme/deployments",
"ref":"refs/heads/main",
"manifestPath":"pagifier.toml"
}' \
"$PAGIFIER_URL/v1/pipelines"

Pagifier initializes its cursors here without doing a surprise first build or deployment. Later control-manifest edits are validated before they become active. A broken edit records an error while the last known good version keeps running.

4. Build, then pin

An application change creates an inactive Release and builds it with rootless BuildKit. Once it is built, the run exposes an immutable reference:

pagifier://artifacts/r20260725-120000-ab12cd34

Put that reference in the operations repository:

apps/frontend/prod.toml
version = 1
artifact = "pagifier://artifacts/r20260725-120000-ab12cd34"

Commit the pin. Only that file triggers the deploy stage. If the environment requires approval, the run pauses at pending_approval until the required approvers accept it:

curl --fail-with-body \
-H "Authorization: Bearer $PAGIFIER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"decision":"approved","comment":"release review complete"}' \
"$PAGIFIER_URL/v1/pipeline-runs/$RUN_ID/approval"

The token needs the deployment.approve permission and one of the roles required by the environment policy. Approval never quietly upgrades a builder or developer token into a deployer.

An image built elsewhere works too, as long as it is pinned by its full digest and the lock points to deployment configuration:

apps/frontend/prod.toml
version = 1
artifact = "oci://registry.example.com/acme/frontend@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
projectFile = "apps/frontend/app.pagifier.toml"

Tags such as latest are intentionally rejected. They are convenient right up until you need to explain what production is actually running.

See what is happening

Use the console, or list runs:

curl -H "Authorization: Bearer $PAGIFIER_TOKEN" \
"$PAGIFIER_URL/v1/pipeline-runs?pipelineId=$PIPELINE_ID"

A submitted run stays running while Kubernetes is building or rolling out the Release. It becomes succeeded only after the control plane reports the real build or deployment complete.

If your team still wants CI to own the trigger, that route is not going away—see CI integration.