Skip to content

Commit 6214d3a

Browse files
h4x3rotabclaude
andcommitted
ci(consul-postgres-ha): publish stage 4 images to GHCR with build provenance
Adds .github/workflows/consul-postgres-ha-publish.yml — a matrix build that builds and pushes the six stage-4 images (mesh-conn, bootstrap-secrets, signaling, webdemo, sidecar, patroni) to ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-* on push to main, tagged with both the long-form commit SHA and `latest`. PRs build to verify but do not push. Each push is signed with a Sigstore-backed GitHub Build Provenance attestation via actions/attest-build-provenance@v2 — the workflow's GitHub OIDC token gets a short-lived Sigstore cert, no keys we manage. Consumers verify with `gh attestation verify oci://...@<digest> --repo Dstack-TEE/dstack-examples`, which proves the image came from this commit of this workflow. Replaces ttl.sh references in terraform.tfvars.example with the GHCR ones, fills in the previously-missing patroni_image and coordinator_replicas lines, and adds inline docs on pinning to a sha-tag for prod stability and on running the verification command. PUBLISHING.md walks through the three paths a stage-4 user actually hits: the CI publish (steady state), manual one-off ttl.sh / personal- GHCR builds for dev iteration, and the on-CVM hot-patch flow that sidesteps phala-cloud#246 when iterating on a running cluster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 823f6c9 commit 6214d3a

3 files changed

Lines changed: 283 additions & 7 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Publish consul-postgres-ha images
2+
3+
# Builds and publishes the six container images that the stage-4
4+
# consul-postgres-ha example needs (mesh-conn, bootstrap-secrets,
5+
# signaling, webdemo, sidecar, patroni). On push to main, images are
6+
# tagged with the commit SHA *and* `latest`, pushed to GHCR, and
7+
# attested with Sigstore-backed GitHub Build Provenance so consumers
8+
# can verify "this image came from this commit of this repo" without
9+
# us managing any keys. PRs build to verify but do not push or attest.
10+
#
11+
# Why six images on one workflow: the example needs all of them in
12+
# lockstep — bumping mesh-conn alone but leaving the rest stale leads
13+
# to mixed-version clusters that are hard to reason about. One workflow
14+
# means one set of tags moves together.
15+
#
16+
# Verifying a published image (consumer side):
17+
#
18+
# gh attestation verify \
19+
# oci://ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-mesh-conn:latest \
20+
# --repo Dstack-TEE/dstack-examples
21+
22+
on:
23+
push:
24+
branches: [main]
25+
paths:
26+
- 'consul-postgres-ha/stage4/mesh-conn/**'
27+
- 'consul-postgres-ha/stage4/bootstrap-secrets/**'
28+
- 'consul-postgres-ha/stage4/patroni/**'
29+
- 'consul-postgres-ha/stage3b/webdemo/**'
30+
- 'consul-postgres-ha/stage3b/sidecar/**'
31+
- 'consul-postgres-ha/phase0/icetest/**'
32+
- '.github/workflows/consul-postgres-ha-publish.yml'
33+
pull_request:
34+
paths:
35+
- 'consul-postgres-ha/stage4/mesh-conn/**'
36+
- 'consul-postgres-ha/stage4/bootstrap-secrets/**'
37+
- 'consul-postgres-ha/stage4/patroni/**'
38+
- 'consul-postgres-ha/stage3b/webdemo/**'
39+
- 'consul-postgres-ha/stage3b/sidecar/**'
40+
- 'consul-postgres-ha/phase0/icetest/**'
41+
- '.github/workflows/consul-postgres-ha-publish.yml'
42+
workflow_dispatch:
43+
44+
env:
45+
REGISTRY: ghcr.io
46+
47+
jobs:
48+
build:
49+
runs-on: ubuntu-latest
50+
permissions:
51+
contents: read
52+
packages: write
53+
# id-token + attestations are required for Sigstore-backed
54+
# GitHub Build Provenance via actions/attest-build-provenance.
55+
id-token: write
56+
attestations: write
57+
58+
strategy:
59+
fail-fast: false
60+
matrix:
61+
include:
62+
- name: mesh-conn
63+
context: consul-postgres-ha/stage4/mesh-conn
64+
- name: bootstrap-secrets
65+
context: consul-postgres-ha/stage4/bootstrap-secrets
66+
- name: patroni
67+
context: consul-postgres-ha/stage4/patroni
68+
- name: signaling
69+
context: consul-postgres-ha/phase0/icetest
70+
- name: webdemo
71+
context: consul-postgres-ha/stage3b/webdemo
72+
- name: sidecar
73+
context: consul-postgres-ha/stage3b/sidecar
74+
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Set up Docker Buildx
79+
uses: docker/setup-buildx-action@v3
80+
81+
- name: Log in to GHCR
82+
if: github.event_name != 'pull_request'
83+
uses: docker/login-action@v3
84+
with:
85+
registry: ${{ env.REGISTRY }}
86+
username: ${{ github.actor }}
87+
password: ${{ secrets.GITHUB_TOKEN }}
88+
89+
- name: Extract image metadata
90+
id: meta
91+
uses: docker/metadata-action@v5
92+
with:
93+
# Image namespace lives one level under the repo so all six
94+
# images sit side-by-side: ghcr.io/<owner>/<repo>/consul-postgres-ha-<name>
95+
images: ${{ env.REGISTRY }}/${{ github.repository }}/consul-postgres-ha-${{ matrix.name }}
96+
tags: |
97+
type=sha,format=long
98+
type=raw,value=latest,enable={{is_default_branch}}
99+
type=ref,event=pr
100+
101+
- name: Build and push
102+
id: push
103+
uses: docker/build-push-action@v6
104+
with:
105+
context: ${{ matrix.context }}
106+
platforms: linux/amd64
107+
push: ${{ github.event_name != 'pull_request' }}
108+
tags: ${{ steps.meta.outputs.tags }}
109+
labels: ${{ steps.meta.outputs.labels }}
110+
cache-from: type=gha,scope=consul-postgres-ha-${{ matrix.name }}
111+
cache-to: type=gha,scope=consul-postgres-ha-${{ matrix.name }},mode=max
112+
113+
# Sigstore-backed build provenance. Binds {image digest, repo,
114+
# workflow, commit SHA, runner identity} into an attestation
115+
# signed with a short-lived Sigstore cert obtained via this
116+
# workflow's GitHub OIDC token — no keys we have to rotate. The
117+
# attestation is uploaded to GitHub *and* (via push-to-registry)
118+
# written next to the image on GHCR so `gh attestation verify
119+
# oci://...` and `cosign verify-attestation` both work.
120+
- name: Attest build provenance
121+
if: github.event_name != 'pull_request'
122+
uses: actions/attest-build-provenance@v2
123+
with:
124+
subject-name: ${{ env.REGISTRY }}/${{ github.repository }}/consul-postgres-ha-${{ matrix.name }}
125+
subject-digest: ${{ steps.push.outputs.digest }}
126+
push-to-registry: true
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Stage 4 — image publishing & verification
2+
3+
The stage-4 example needs six container images deployed in lockstep:
4+
`mesh-conn`, `bootstrap-secrets`, `signaling`, `webdemo`, `sidecar`,
5+
`patroni`. CI publishes them to GHCR with Sigstore-backed GitHub Build
6+
Provenance; consumers pin by tag (or, better, by digest) and verify
7+
provenance with `gh attestation verify`.
8+
9+
This doc covers the three paths you'll actually use:
10+
11+
1. **CI publish** (the steady-state)
12+
2. **Manual one-off publish** (dev iteration / breaking glass)
13+
3. **Hot-patch on a live cluster** (debugging without a redeploy)
14+
15+
## 1. CI publish — the steady-state
16+
17+
`.github/workflows/consul-postgres-ha-publish.yml` runs on push to `main`
18+
when any of the six image build contexts (or the workflow itself)
19+
change, and on PRs touching the same paths. Each run:
20+
21+
- Builds all six images via a matrix job.
22+
- On `main`, pushes to `ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-<name>` with two tags: the long-form commit SHA (`sha-<40-hex>`) and `latest`.
23+
- Generates a GitHub Build Provenance attestation per image via
24+
`actions/attest-build-provenance@v2`. The attestation is signed by
25+
Sigstore using a short-lived cert obtained through the workflow's
26+
GitHub OIDC token — no keys we manage. It binds the image digest to
27+
the commit SHA, workflow file, and runner identity.
28+
- Pushes the attestation to GHCR alongside the image, so consumers can
29+
fetch and verify it via either GitHub's API or any cosign-style tool.
30+
- On PRs, builds without pushing or attesting (verification only).
31+
32+
### Verifying a published image as a consumer
33+
34+
```bash
35+
# By tag (lower assurance — `latest` floats):
36+
gh attestation verify \
37+
oci://ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-mesh-conn:latest \
38+
--repo Dstack-TEE/dstack-examples
39+
40+
# By digest (preferred — pinned, won't drift):
41+
gh attestation verify \
42+
oci://ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-mesh-conn@sha256:<digest> \
43+
--repo Dstack-TEE/dstack-examples
44+
```
45+
46+
A successful verification proves: this image's digest was attested in a
47+
GitHub Actions run on `Dstack-TEE/dstack-examples`, with a workflow
48+
file and commit SHA you can inspect to decide whether to trust it.
49+
Failed or absent attestations should fail your deploy.
50+
51+
For prod-style deploys, pin every image in `terraform.tfvars` to its
52+
`sha-<40-hex>` tag (or a digest) rather than `latest`, so a CI rebuild
53+
of `latest` doesn't silently swap your cluster's bits.
54+
55+
## 2. Manual one-off publish — dev iteration
56+
57+
When iterating fast on `mesh-conn` (or any other component) you don't
58+
want to round-trip through CI for every byte. Two equivalent shortcuts:
59+
60+
### a) `ttl.sh` (24h-disposable, no auth)
61+
62+
```bash
63+
TS=$(date +%s)
64+
TAG=ttl.sh/dstack-mesh-conn-${TS}:24h
65+
docker build -t $TAG consul-postgres-ha/stage4/mesh-conn
66+
docker push $TAG
67+
```
68+
69+
Then point the running cluster at it via `terraform.tfvars`'s
70+
`mesh_conn_image = ...` (and `terraform apply`), or hot-patch the
71+
running CVM (see §3). `ttl.sh` images expire 24h after push.
72+
73+
### b) Personal GHCR namespace (persistent, requires PAT)
74+
75+
If you want a longer-lived dev image without going through main:
76+
77+
```bash
78+
echo "$GITHUB_TOKEN" | docker login ghcr.io -u <your-user> --password-stdin
79+
TAG=ghcr.io/<your-user>/consul-postgres-ha-mesh-conn:dev-$(date +%s)
80+
docker build -t $TAG consul-postgres-ha/stage4/mesh-conn
81+
docker push $TAG
82+
```
83+
84+
These manual builds do **not** carry a build-provenance attestation —
85+
that comes from CI's OIDC identity. For anything user-facing, run the
86+
real CI workflow.
87+
88+
## 3. Hot-patch on a live cluster — debugging without a redeploy
89+
90+
Sometimes you need to swap a binary on a running CVM right now,
91+
without re-applying terraform (because `terraform-provider-phala` has
92+
a known bug where `env`-block in-place updates silently no-op — see
93+
`Phala-Network/phala-cloud#246` — and you don't want to recreate CVMs
94+
just to roll a new image).
95+
96+
```bash
97+
GW=dstack-pha-prod5.phala.network
98+
APP_ID=<cvm-app-id>
99+
NEW=ttl.sh/dstack-mesh-conn-<ts>:24h
100+
OLD=$(ssh ... root@${APP_ID}-22.${GW} \
101+
"docker inspect dstack-mesh-conn-1 --format '{{.Config.Image}}'")
102+
103+
ssh ... root@${APP_ID}-22.${GW} "
104+
docker pull $NEW
105+
docker tag $NEW $OLD
106+
cd /tapp && docker compose \
107+
--env-file /dstack/.host-shared/.decrypted-env \
108+
-p dstack -f /tapp/docker-compose.yaml \
109+
up -d --force-recreate mesh-conn
110+
"
111+
```
112+
113+
The retag tricks compose into using the new bits without touching the
114+
declared image string. This bypasses dstack's attestation hashes —
115+
**fine for dev/smoke, not for prod**. Next CVM reboot re-renders the
116+
compose from the platform-encrypted env and reverts to whatever's in
117+
your tfstate.
118+
119+
## What to bump after a CI publish
120+
121+
When CI publishes a new `latest` and you want to roll it to a running
122+
cluster:
123+
124+
1. Decide whether you're pinning to `:latest` (drifts) or to the
125+
`:sha-...` tag from the new run (recommended). Find the new SHA by
126+
inspecting the workflow run's output or `gh run view`.
127+
2. Edit `consul-postgres-ha/stage4/cluster-example/terraform.tfvars`
128+
to that pin.
129+
3. `terraform apply`. Per-CVM compose re-renders and the dstack agent
130+
recreates each service. (Or hot-patch per §3 if you want to verify
131+
on one CVM first.)
132+
4. Verify with `gh attestation verify oci://...@<digest>` if you want
133+
to be sure the image you're pinning was built by this repo.
Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
# Copy to terraform.tfvars and fill in.
2+
#
3+
# Defaults below point at the GHCR-published, Sigstore-attested images
4+
# produced by .github/workflows/consul-postgres-ha-publish.yml. Pin to a
5+
# specific commit by replacing `:latest` with `:sha-<full-sha>` (40-char
6+
# git sha) — preferred for prod since `:latest` floats. Verify a tag
7+
# came from this repo with:
8+
#
9+
# gh attestation verify \
10+
# oci://ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-mesh-conn:latest \
11+
# --repo Dstack-TEE/dstack-examples
12+
#
13+
# For dev iteration, replace any single line with a `ttl.sh/...:24h`
14+
# tag from `docker push ttl.sh/<name>-$(date +%s):24h`.
215

316
cluster_name = "demo"
17+
coordinator_replicas = 3
418
worker_replicas = 3
519
gateway_domain = "dstack-pha-prod5.phala.network"
620

7-
# Image refs. For experiments use ttl.sh; for production use a
8-
# permanent registry (GHCR, Phala internal, etc.).
9-
bootstrap_secrets_image = "ttl.sh/dstack-bootstrap-secrets-XXXXXXXX:24h"
10-
mesh_conn_image = "ttl.sh/dstack-mesh-conn-XXXXXXXX:24h"
11-
signaling_image = "ttl.sh/dstack-icetest-XXXXXXXX:24h" # signaling mode
12-
webdemo_image = "ttl.sh/dstack-webdemo3b-XXXXXXXX:24h"
13-
sidecar_image = "ttl.sh/dstack-consul-sidecar-XXXXXXXX:24h"
21+
bootstrap_secrets_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-bootstrap-secrets:latest"
22+
mesh_conn_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-mesh-conn:latest"
23+
signaling_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-signaling:latest"
24+
webdemo_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-webdemo:latest"
25+
sidecar_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-sidecar:latest"
26+
patroni_image = "ghcr.io/dstack-tee/dstack-examples/consul-postgres-ha-patroni:latest"
27+
28+
# external_coordinator_host = "<vultr-ip>"
29+
# external_signaling_url = "http://<vultr-ip>:7000"
30+
# external_turn_secret = "<hex>"

0 commit comments

Comments
 (0)