Publish
What publish does
sklp run publish runs .sklp/pipelines/publish.yaml: it diffs the branch,
picks the impacted services, and for each one builds the image daemonless,
scans it, and pushes it. Pushing an image is where publish stops — rolling it
out is the node's job.
sklp run publish # impacted services only
sklp run publish -resolve-only # list what would ship, push nothing
sklp run publish -only web,core # just these scopes
sklp run publish -force # every service, whatever the diff
sklp run publish -base-ref HEAD~3 # widen the diff window| Flag | Effect |
|---|---|
-resolve-only | print the impacted services and changed paths, then exit |
-only a,b | only the steps scoped to these names; wins over -force |
-force | every scoped step is in scope |
-base-ref <ref> | base of the diff (default HEAD~1); widen it when a merge was missed |
-no-cache | rebuild even if an identical build is cached |
Only from a clean main
The pipeline guards itself:
kind: pipeline
name: publish
space: false
on:
branches: [main]
clean_tree: trueFrom any other branch, or with anything in git status --porcelain, the run is
refused before the first build. An image built from uncommitted work matches no
commit. Merge the PR, git pull main, then publish.
Impacted services
Each image is a kind: publish step with a scope:. A service ships when a
changed file matches its scope; the rest are skipped.
scopes:
core:
- "apps/core/**"
- "Dockerfile"
- "go.work"
web:
- "apps/web/**"
- "pnpm-lock.yaml"
- "Dockerfile.web"Run -resolve-only first when unsure; it honours -only and -force.
The publish step
steps:
- name: core-image
kind: publish
platform: linux/amd64
scope: [core]
context: .
dockerfile: Dockerfile
image: rg.fr-par.scw.cloud/skalpai/core
tag: "{{ publish_tag }}"
secrets:
- id: github_token
env: GITHUB_TOKEN| Field | Meaning |
|---|---|
image | registry reference without the tag |
tag | literal, $VAR, or {{ publish_tag }} |
context | build context, relative to the repo root (required) |
dockerfile | defaults to <context>/Dockerfile |
platform | linux/amd64 or linux/arm64; defaults to the build host's |
build_args | --build-arg values, templated; non-secret (they land in history) |
build_contexts | extra named contexts (dir, docker-image://, target:) |
secrets | build secrets: {id, env} or {id, file} |
scan | false, or {severity, ignore_unfixed}; default HIGH/CRITICAL fixable |
push | false builds without pushing |
The builder is in-process (runc, no Docker daemon, no BuildKit). It supports
FROM … AS, RUN (with --mount=type=secret), COPY, COPY --from, ENV,
ARG, WORKDIR, USER, EXPOSE, ENTRYPOINT, CMD; anything else is
rejected loudly.
Runs on the host
A pipeline with publish steps needs space: false. A rootless Space container
cannot create the containers a build runs; inside a Space the failure looks
like a corrupt cache when it is not.
Platform
The image carries a real platform. Without platform: it is the build host's,
so a build on an arm64 Mac produces an arm64 image. Pin
platform: linux/amd64 for amd64 nodes; foreign RUN steps go through the
host's binfmt qemu handler.
build_args
build_args:
VITE_SKALPAI_API_KEY: "${VITE_SKALPAI_API_KEY}"
VITE_SKALPAI_SAAS_URL: "${VITE_SKALPAI_SAAS_URL}"Values are resolved from the run's env before reaching the builder. Anything
sensitive goes in secrets:, never build_args:.
Image tags
tag:
strategy: date-shaThe tag is computed once per run and exported as $SKLP_PUBLISH_TAG and
{{ publish_tag }}:
| Strategy | Shape |
|---|---|
date-sha | YYYYMMDD-HHMM-<short-sha>, e.g. 20260516-1856-bf44d9d |
sha | <short-sha> |
date | YYYYMMDD-HHMM |
The date is UTC build time, the sha is HEAD. Every service of one run shares
the same tag.
Registries and credentials
Images are named <registry>/<project>/<service>, e.g.
rg.fr-par.scw.cloud/skalpai/core. Where the push credential comes from
depends on where the run executes, never both:
| Where | Source |
|---|---|
| Dev machine | ~/.config/sklp/registries.yaml, written by sklp registry login |
CI node (SKLP_CI=1) | the pipeline registries: block, ${VAR} from secrets: |
sklp registry login --provider scaleway # rg.fr-par.scw.cloud, user nologin
sklp registry login --provider github # ghcr.io, user oauth2
printf "$TOKEN" | sklp registry login --provider scaleway --password-stdin
sklp registry ls
sklp registry logout rg.fr-par.scw.cloudregistries:
rg.fr-par.scw.cloud:
username: nologin
password: ${SCW_SECRET_KEY}sklp never reads ~/.docker/config.json; docker login does nothing for it. A
host with no credential fails the run before the first build. See
sklp registry login.
One-off push
sklp image push builds and pushes a single
Dockerfile outside any pipeline, with the same builder:
sklp image push --image rg.fr-par.scw.cloud/skalpai/core \
--context . --dockerfile Dockerfile --platform linux/amd64 \
--secret id=github_token,env=GITHUB_TOKEN --scanWithout --tag it uses YYYYMMDD-HHMM-<short-sha>. --build-arg NAME=value
is repeatable.
From pushed image to running service
A push only puts an image in the registry. Rollout happens on the node:
# .sklp/image-watches.yaml
watches:
- name: app-core
service: core
space: app-production
image_repo: rg.fr-par.scw.cloud/skalpai/core- On a push to the default branch, core reconciles the repo and makes the
node's image watches match
.sklp/image-watches.yaml. - The watch sees the new tag in the registry and rolls the service on the node.
- The node starts the new generation next to the old one, cuts traffic over once it is Ready, and keeps the old one alive through a soak (60 s by default). A crash during the soak rolls back (ADR-013).
- The node keeps the rolled image across later
deploy.yamlapplies until Git changes that service's tag itself: it tracksImageandDeclaredImageand arbitrates with a three-way merge (ADR-018). A spec it just rolled back is reportedrejected, and core records the roll as failed.
Pushed is not deployed
A green publish means the image is in the registry, nothing more. Before
calling a release live, check that the node runs the new tag and that the roll
survived its soak — in the Skalpai app or through the node. A watch that
reports rolled with no soak outcome is acceptance, not success.