excalidraw: migrate image build to GHA -> private ghcr (ADR-0002)

The image was still built by hand and pushed to DockerHub (v1..v4),
predating the all-builds-off-infra doctrine; Viktor chose to move it
onto the standard pipeline while shipping the export/rename feature
rather than keep the manual flow.

Mirrors the k8s-portal pattern: .github/workflows/build-excalidraw.yml
(go test + buildx linux/amd64, pushes ghcr latest+sha), excalidraw ns
added to the Kyverno ghcr-credentials allowlist (package is PRIVATE),
deployment now pins ghcr :latest with pullPolicy Always + pull secret,
Keel force/match-tag/5m annotations seed the metadata (live values win
via ignore_changes). DockerHub viktorbarzin/excalidraw-library:v4 stays
frozen as the rollback image. Docs: ci-cd.md + .claude/CLAUDE.md image
lists updated (also backfilled the missing k8s-portal rows in ci-cd.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-07-02 14:29:23 +00:00
parent 1cbc1e962b
commit 8fc657f431
5 changed files with 77 additions and 7 deletions

View file

@ -137,7 +137,7 @@ audiobook-search) now also land on ghcr.
chrome-service-novnc, android-emulator. chrome-service-novnc, android-emulator.
- **PRIVATE ghcr:** f1-stream, job-hunter, instagram-poster, payslip-ingest, - **PRIVATE ghcr:** f1-stream, job-hunter, instagram-poster, payslip-ingest,
wealthfolio-sync, fire-planner, recruiter-responder, tripit, infra-cli, wealthfolio-sync, fire-planner, recruiter-responder, tripit, infra-cli,
infra-ci, k8s-portal. Pulled via the Kyverno-synced `ghcr-credentials` allowlist infra-ci, k8s-portal, excalidraw-library. Pulled via the Kyverno-synced `ghcr-credentials` allowlist
(`stacks/kyverno/modules/kyverno/ghcr-credentials.tf`; NOT cluster-wide; cred (`stacks/kyverno/modules/kyverno/ghcr-credentials.tf`; NOT cluster-wide; cred
= Vault `secret/viktor/ghcr_pull_token`, a dedicated classic PAT scoped to = Vault `secret/viktor/ghcr_pull_token`, a dedicated classic PAT scoped to
`read:packages` (UI-minted 2026-06-15; no longer the admin `github_pat` `read:packages` (UI-minted 2026-06-15; no longer the admin `github_pat`
@ -153,7 +153,9 @@ github↔forgejo divergence was deliberately NOT reconciled):
`build-cli.yml` → DockerHub `viktorbarzin/infra` (kept) + `ghcr.io/viktorbarzin/infra-cli`; `build-cli.yml` → DockerHub `viktorbarzin/infra` (kept) + `ghcr.io/viktorbarzin/infra-cli`;
`build-infra-ci.yml``ghcr.io/viktorbarzin/infra-ci`; `build-k8s-portal.yml` `build-infra-ci.yml``ghcr.io/viktorbarzin/infra-ci`; `build-k8s-portal.yml`
PRIVATE `ghcr.io/viktorbarzin/k8s-portal` (Keel-deployed; the LAST in-cluster PRIVATE `ghcr.io/viktorbarzin/k8s-portal` (Keel-deployed; the LAST in-cluster
Woodpecker build, migrated 2026-06-13 — completes "no local builds"). **infra-ci** Woodpecker build, migrated 2026-06-13 — completes "no local builds"); `build-excalidraw.yml`
PRIVATE `ghcr.io/viktorbarzin/excalidraw-library` (Keel-deployed; replaced
manual DockerHub pushes 2026-07-02 — DockerHub `:v4` frozen as rollback). **infra-ci**
is the image the `.woodpecker/default.yml` apply step + `drift-detection.yml` run is the image the `.woodpecker/default.yml` apply step + `drift-detection.yml` run
in (proven by pipelines 165/166). chatterbox-tts is already built by tripit's GHA → ghcr. in (proven by pipelines 165/166). chatterbox-tts is already built by tripit's GHA → ghcr.
The Woodpecker `build-ci-image.yml` + `build-cli.yml` pipelines were REMOVED; The Woodpecker `build-ci-image.yml` + `build-cli.yml` pipelines were REMOVED;

42
.github/workflows/build-excalidraw.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Build excalidraw-library
# ADR-0002 / no-local-builds: excalidraw-library (infra-owned Go app behind
# draw.viktorbarzin.me) builds off-infra on GHA → private ghcr; Keel polls
# ghcr:latest and rolls the deployment. Replaces the manual DockerHub pushes
# (viktorbarzin/excalidraw-library:v4 stays frozen as the rollback image).
on:
push:
branches: [master]
paths:
- 'stacks/excalidraw/project/**'
workflow_dispatch: {}
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- run: go test ./...
working-directory: stacks/excalidraw/project
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: stacks/excalidraw/project
platforms: linux/amd64
provenance: false
push: true
tags: |
ghcr.io/viktorbarzin/excalidraw-library:latest
ghcr.io/viktorbarzin/excalidraw-library:${{ github.sha }}

View file

@ -94,7 +94,7 @@ can't reach Forgejo's public hairpin.
| Visibility | Packages | Pull mechanism | | Visibility | Packages | Pull mechanism |
|------------|----------|----------------| |------------|----------|----------------|
| **Public** | beadboard, nextcloud-todos, claude-agent-service, claude-memory-mcp, kms-website, freedify, tuya_bridge, x402-gateway, chrome-service-novnc, android-emulator | Anonymous | | **Public** | beadboard, nextcloud-todos, claude-agent-service, claude-memory-mcp, kms-website, freedify, tuya_bridge, x402-gateway, chrome-service-novnc, android-emulator | Anonymous |
| **Private** | f1-stream, job-hunter, instagram-poster, payslip-ingest, wealthfolio-sync, fire-planner, recruiter-responder, tripit, infra-cli, infra-ci | `ghcr-credentials` dockerconfigjson | | **Private** | f1-stream, job-hunter, instagram-poster, payslip-ingest, wealthfolio-sync, fire-planner, recruiter-responder, tripit, infra-cli, infra-ci, k8s-portal, excalidraw-library | `ghcr-credentials` dockerconfigjson |
Private-image pulls use the `ghcr-credentials` dockerconfigjson, cloned by the Private-image pulls use the `ghcr-credentials` dockerconfigjson, cloned by the
kyverno stack's `sync-ghcr-credentials` ClusterPolicy to an explicit kyverno stack's `sync-ghcr-credentials` ClusterPolicy to an explicit
@ -188,6 +188,8 @@ reconciled — the workflows were added to the GitHub lineage via PR):
| android-emulator | `build-android-emulator.yml` | public `ghcr.io/viktorbarzin/android-emulator` | | android-emulator | `build-android-emulator.yml` | public `ghcr.io/viktorbarzin/android-emulator` |
| infra CLI | `build-cli.yml` | DockerHub `viktorbarzin/infra` (kept) + `ghcr.io/viktorbarzin/infra-cli` | | infra CLI | `build-cli.yml` | DockerHub `viktorbarzin/infra` (kept) + `ghcr.io/viktorbarzin/infra-cli` |
| infra-ci | `build-infra-ci.yml` | private `ghcr.io/viktorbarzin/infra-ci` | | infra-ci | `build-infra-ci.yml` | private `ghcr.io/viktorbarzin/infra-ci` |
| k8s-portal | `build-k8s-portal.yml` | private `ghcr.io/viktorbarzin/k8s-portal` (Keel rolls `:latest` digests) |
| excalidraw-library | `build-excalidraw.yml` | private `ghcr.io/viktorbarzin/excalidraw-library` (Keel rolls `:latest` digests; DockerHub `:v4` frozen as rollback) |
**`infra-ci`** is the image the `.woodpecker/default.yml` apply step and **`infra-ci`** is the image the `.woodpecker/default.yml` apply step and
`drift-detection.yml` run in (proven by pipelines 165/166). `chatterbox-tts` is `drift-detection.yml` run in (proven by pipelines 165/166). `chatterbox-tts` is

View file

@ -45,6 +45,15 @@ resource "kubernetes_deployment" "excalidraw" {
app = "excalidraw" app = "excalidraw"
tier = local.tiers.aux tier = local.tiers.aux
} }
# Keel rolls new ghcr:latest digests (k8s-portal pattern). Values here are
# recreate-correct seeds only the keys are in ignore_changes below, so
# the live annotations win on an existing deployment.
annotations = {
"keel.sh/policy" = "force"
"keel.sh/trigger" = "poll"
"keel.sh/match-tag" = "true"
"keel.sh/pollSchedule" = "@every 5m"
}
} }
spec { spec {
replicas = 1 replicas = 1
@ -67,9 +76,19 @@ resource "kubernetes_deployment" "excalidraw" {
} }
} }
spec { spec {
# GHCR pull secret: the ghcr-credentials Secret in this namespace is
# cloned in by the kyverno stack's sync-ghcr-credentials ClusterPolicy
# (allowlisted private-ghcr namespaces only ADR-0002). Source of
# truth: stacks/kyverno/modules/kyverno/ghcr-credentials.tf.
image_pull_secrets {
name = "ghcr-credentials"
}
container { container {
image = "viktorbarzin/excalidraw-library:v4" # ADR-0002: GHA-built (.github/workflows/build-excalidraw.yml),
image_pull_policy = "IfNotPresent" # PRIVATE ghcr; Keel rolls new :latest digests. DockerHub
# viktorbarzin/excalidraw-library:v4 is the frozen rollback image.
image = "ghcr.io/viktorbarzin/excalidraw-library:latest"
image_pull_policy = "Always"
name = "excalidraw" name = "excalidraw"
port { port {
container_port = 8080 container_port = 8080

View file

@ -43,6 +43,11 @@ locals {
# ghcr.io/passionprojectsanca/book-plotter (built by GHA in Anca's repo, # ghcr.io/passionprojectsanca/book-plotter (built by GHA in Anca's repo,
# under her own org's ghcr). The deployment references the cloned secret. # under her own org's ghcr). The deployment references the cloned secret.
"plotting-book", "plotting-book",
# excalidraw: infra-owned image migrated from manual DockerHub pushes to
# PRIVATE ghcr.io/viktorbarzin/excalidraw-library (ADR-0002, built by
# .github/workflows/build-excalidraw.yml). The deployment references the
# cloned secret.
"excalidraw",
] ]
} }