From 6e507186b1df0725e932594540ccdccb9e72b99b Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Tue, 21 Jul 2026 15:54:54 +0300 Subject: [PATCH] vibe-kanban-remote: move the build to lair/vibe-kanban MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repo is for third-party images built from someone else's source. We now own a fork at lair/vibe-kanban, so its image is built there, next to the code, by .gitea/workflows/container.yml. The quadlets move with it. Leaving this job here was actively harmful, not just redundant: it built from the BloopAI mirror and pushed to the same git.lair.cafe/lair/vibe-kanban-remote:latest on a daily 07:00 cron. The mirror still contains upstream's product sunset, which replaced the kanban board with an export-only page — so the next scheduled run would have republished a sunset image over :latest. bob is pinned to an immutable tag and would not have picked it up, but the tag would have been wrong for anything that did. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0182wzZE8DguMPWhxD21gfP2 --- .gitea/workflows/images.yml | 59 ------------ images/vibe-kanban-remote/build.sh | 50 ----------- images/vibe-kanban-remote/readme.md | 90 ------------------- .../vibe-kanban-db.container | 40 --------- .../vibe-kanban-electric.container | 45 ---------- .../vibe-kanban-remote/vibe-kanban.container | 48 ---------- images/vibe-kanban-remote/vibe-kanban.network | 8 -- 7 files changed, 340 deletions(-) delete mode 100755 images/vibe-kanban-remote/build.sh delete mode 100644 images/vibe-kanban-remote/readme.md delete mode 100644 images/vibe-kanban-remote/vibe-kanban-db.container delete mode 100644 images/vibe-kanban-remote/vibe-kanban-electric.container delete mode 100644 images/vibe-kanban-remote/vibe-kanban.container delete mode 100644 images/vibe-kanban-remote/vibe-kanban.network diff --git a/.gitea/workflows/images.yml b/.gitea/workflows/images.yml index 2b85bde..b8599f7 100644 --- a/.gitea/workflows/images.yml +++ b/.gitea/workflows/images.yml @@ -80,62 +80,3 @@ jobs: podman push "${IMAGE}:${VERSION}" podman push "${IMAGE}:latest" echo "published ${IMAGE}:${VERSION} (and :latest)" - - vibe-kanban-remote: - runs-on: - - metal - - podman - steps: - - uses: actions/checkout@v4 - - # Resolved from OUR MIRROR, not GitHub: BloopAI has announced a sunset and - # the mirror exists so this build outlives them — which is pointless if the - # build asks github.com what to build. Gitea mirrors carry tags but not - # releases, so filter to the strict release pattern and sort on the trailing - # datestamp (also skips malformed historical tags like `vv.20250708094151`). - - name: resolve latest upstream release (from the mirror) - id: rel - run: | - tag=$(for p in 1 2 3 4 5; do - curl -fsS "https://git.lair.cafe/api/v1/repos/BloopAI/vibe-kanban/tags?limit=100&page=${p}" \ - | jq -r '.[].name' - done \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-[0-9]{14}$' \ - | sort -t- -k2 -n \ - | tail -1) - if [ -z "$tag" ]; then - echo "ERROR: could not resolve a vibe-kanban tag from the mirror"; exit 1 - fi - echo "mirror latest: $tag" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "version=${tag#v}" >> "$GITHUB_OUTPUT" - - - name: login to registry - run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe - - - name: build & push (release-triggered, self-healing) - env: - TAG: ${{ steps.rel.outputs.tag }} - VERSION: ${{ steps.rel.outputs.version }} - FORCE: ${{ github.event.inputs.force }} - run: | - IMAGE=git.lair.cafe/lair/vibe-kanban-remote - if [ "$FORCE" != "true" ] && skopeo inspect "docker://${IMAGE}:${VERSION}" >/dev/null 2>&1; then - echo "${IMAGE}:${VERSION} already published — nothing to build" - exit 0 - fi - # Upstream ships the Dockerfile; context is the repo root, so -f points - # into it. FEATURES is deliberately unset — the Dockerfile strips the - # private billing crate only when it is empty (the self-host path), and - # we have no access to BloopAI/vibe-kanban-private. - # VITE_RELAY_API_BASE_URL is baked into the SPA; empty = relay disabled. - echo "building ${IMAGE}:${VERSION} from the mirror at ${TAG}" - podman build --pull=newer \ - -f crates/remote/Dockerfile \ - --build-arg VITE_RELAY_API_BASE_URL= \ - -t "${IMAGE}:${VERSION}" \ - -t "${IMAGE}:latest" \ - "https://git.lair.cafe/BloopAI/vibe-kanban.git#${TAG}" - podman push "${IMAGE}:${VERSION}" - podman push "${IMAGE}:latest" - echo "published ${IMAGE}:${VERSION} (and :latest)" diff --git a/images/vibe-kanban-remote/build.sh b/images/vibe-kanban-remote/build.sh deleted file mode 100755 index 26f33c6..0000000 --- a/images/vibe-kanban-remote/build.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# Build the vibe-kanban remote-server image locally, mirroring the `images` workflow. -# -# vibe-kanban ships its own Dockerfile (crates/remote/Dockerfile, context = repo -# root), so there is no vendored Containerfile here. -# -# Unlike hermes, the build context is OUR MIRROR (git.lair.cafe/BloopAI/vibe-kanban) -# rather than GitHub, and the version is resolved from the mirror's tags rather than -# GitHub's releases API. BloopAI has announced a sunset; the whole point of the -# mirror is that this build keeps working after upstream disappears, so it must not -# depend on GitHub at build time. See the BloopAI org description on git.lair.cafe. -# -# Gitea mirrors do not carry GitHub *releases*, only tags — hence tag resolution by -# strict pattern (v-<14-digit datestamp>) sorted on the datestamp. That -# filter also skips the malformed historical tags in this repo (e.g. `vv.2025…`). -# -# Override the ref with VK_REF (e.g. v0.1.44-20260424091429); empty resolves latest. -set -euo pipefail - -REGISTRY="${REGISTRY:-git.lair.cafe}" -IMAGE_NAME="${REGISTRY}/lair/vibe-kanban-remote" -MIRROR_API="https://git.lair.cafe/api/v1/repos/BloopAI/vibe-kanban/tags" -MIRROR_GIT="https://git.lair.cafe/BloopAI/vibe-kanban.git" -VK_REF="${VK_REF:-}" - -# The frontend bakes this in at build time; empty disables the relay/tunnel -# features, which we do not deploy. Changing it requires a rebuild, not a restart. -VITE_RELAY_API_BASE_URL="${VITE_RELAY_API_BASE_URL:-}" - -if [ -z "${VK_REF}" ]; then - VK_REF=$(for p in 1 2 3 4 5; do - curl -fsS "${MIRROR_API}?limit=100&page=${p}" | jq -r '.[].name' - done \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-[0-9]{14}$' \ - | sort -t- -k2 -n \ - | tail -1) -fi -[ -n "${VK_REF}" ] || { echo "could not resolve a vibe-kanban ref from the mirror"; exit 1; } -VERSION="${VK_REF#v}" - -echo "building ${IMAGE_NAME}:${VERSION} from ${MIRROR_GIT}#${VK_REF}" -podman build --pull=newer \ - -f crates/remote/Dockerfile \ - --build-arg "VITE_RELAY_API_BASE_URL=${VITE_RELAY_API_BASE_URL}" \ - -t "${IMAGE_NAME}:${VERSION}" \ - -t "${IMAGE_NAME}:latest" \ - "${MIRROR_GIT}#${VK_REF}" - -echo "built ${IMAGE_NAME}:${VERSION} and :latest" -echo "push with: podman push ${IMAGE_NAME}:${VERSION} && podman push ${IMAGE_NAME}:latest" diff --git a/images/vibe-kanban-remote/readme.md b/images/vibe-kanban-remote/readme.md deleted file mode 100644 index fa564e2..0000000 --- a/images/vibe-kanban-remote/readme.md +++ /dev/null @@ -1,90 +0,0 @@ -# vibe-kanban-remote - -The **remote-server** from [vibe-kanban](https://git.lair.cafe/BloopAI/vibe-kanban) — -the self-hostable server half of the suite. Published as -`git.lair.cafe/lair/vibe-kanban-remote:{version,latest}`. - -## What it is - -A single Rust binary (`/usr/local/bin/remote`) serving both an API and the built -`remote-web` SPA from `/srv/static`, listening on `:8081` as uid 10001. Upstream -ships the Dockerfile (`crates/remote/Dockerfile`, context = repo root), so nothing -is vendored here — `build.sh` and the workflow build straight from that. - -## Built from our mirror, deliberately - -Unlike `hermes`, this builds from **`git.lair.cafe/BloopAI/vibe-kanban`**, not -GitHub, and resolves its version from the mirror's tags rather than GitHub's -releases API. - -BloopAI has announced a sunset. The mirror exists so this suite survives upstream -going away, which is worthless if the build still calls GitHub to find out what to -build. Nothing in this image's build path touches github.com. - -Gitea mirrors carry tags but **not** GitHub releases, so "latest" is resolved by -filtering tags to the strict release pattern `v-<14-digit datestamp>` and -sorting on the datestamp. The filter also excludes malformed historical tags in the -upstream repo (`vv.20250708094151`, `vv0.0.40-nbump.2.…`) that would otherwise sort -unpredictably. - -## No private access required - -`crates/remote/Cargo.toml` declares a `billing` dependency on the private -`BloopAI/vibe-kanban-private` repo. The Dockerfile deliberately strips it — and -deletes `crates/remote/Cargo.lock` — whenever the `FEATURES` build arg is empty, -which is the documented self-host path. **Never set `FEATURES`**; we have no access -to that repo and the build would fail trying to reach it over SSH. - -## Build args - -| Arg | Value here | Why | -|-----|-----------|-----| -| `FEATURES` | *(unset)* | Strips the private billing crate. Setting it breaks the build. | -| `VITE_RELAY_API_BASE_URL` | *(empty)* | Baked into the SPA at build time; empty disables relay/tunnel, which we don't deploy. Changing it needs a rebuild, not a restart. | - -## Runtime - -Consumed by the `vibe-kanban` quadlets on `bob` (`kanban.internal`). Needs a -PostgreSQL with `wal_level=logical` and an ElectricSQL sync service alongside; -`SERVER_DATABASE_URL`, `ELECTRIC_URL` and `VIBEKANBAN_REMOTE_JWT_SECRET` are -mandatory, and at least one auth provider must be configured or the server refuses -to start. It runs its own sqlx migrations on startup. - -Note ElectricSQL **cannot** use client-certificate auth to Postgres, which is why -this deployment runs its own Postgres rather than using magrathea's mTLS-only -instance. - -## Deployment (bob) - -The four quadlets in this directory are the deployed configuration, copied to -`/etc/containers/systemd/` on `bob`: - -| Unit | What | -|------|------| -| `vibe-kanban.network` | private bridge; only remote-server publishes a host port | -| `vibe-kanban-db.container` | PostgreSQL 16 with `wal_level=logical` | -| `vibe-kanban.container` | remote-server, published on `27180` | -| `vibe-kanban-electric.container` | ElectricSQL sync | - -**Start order is load-bearing, not cosmetic.** remote-server's sqlx migrations are -what create the `electric_sync` role, its grants and the publication that Electric -connects with — and remote-server re-`ALTER`s that role's password from -`ELECTRIC_ROLE_PASSWORD` on every start. So `db → remote-server → electric`, with -`Notify=healthy` on the first two so systemd waits for readiness rather than mere -process start. - -Secrets live in `/etc/vibe-kanban/{env,electric.env}` (0600 root), mirroring -`pass lair/vibe-kanban/*`. Electric gets its own file purely so its variable can be -named `DATABASE_URL` without colliding with remote-server's `SERVER_DATABASE_URL`, -and so nothing depends on systemd expanding one `Environment=` into another. - -Fronted by nginx on `hanzalova.internal` as `https://kanban.internal` (internal CA, -renewed by `step@kanban.timer`). Upstream's Caddy service is dropped entirely — TLS -terminates at the proxy, per `architecture/reverse-proxies.md`. - -## Local build - -```sh -./build.sh # latest tag from the mirror -VK_REF=v0.1.44-20260424091429 ./build.sh # a specific tag -``` diff --git a/images/vibe-kanban-remote/vibe-kanban-db.container b/images/vibe-kanban-remote/vibe-kanban-db.container deleted file mode 100644 index 526f93e..0000000 --- a/images/vibe-kanban-remote/vibe-kanban-db.container +++ /dev/null @@ -1,40 +0,0 @@ -# PostgreSQL for vibe-kanban. -# -# Deliberately NOT magrathea. magrathea is mTLS-only (`hostssl ... cert -# clientcert=verify-full map=cert_cn`, with `hostnossl ... reject`) and -# ElectricSQL cannot present a client certificate — it supports only -# sslmode=require/disable plus a CA file for verifying the *server*. Electric is -# not optional either: remote-server treats ELECTRIC_URL as mandatory. magrathea -# is also wal_level=replica, and Electric needs logical, which would mean -# restarting the shared primary. So this stack runs its own Postgres on a private -# podman network; nothing here is reachable from the LAN. -# -# Version tracks upstream's compose (postgres:16-alpine), not magrathea's 18. -# wal_level=logical is required by Electric's logical replication. -[Unit] -Description=vibe-kanban PostgreSQL -After=network-online.target -Wants=network-online.target - -[Container] -Image=docker.io/library/postgres:16-alpine -ContainerName=vibe-kanban-db -AutoUpdate=registry -Network=vibe-kanban.network -Exec=postgres -c wal_level=logical -Volume=/var/lib/vibe-kanban/postgres:/var/lib/postgresql/data:Z -Environment=POSTGRES_DB=remote -Environment=POSTGRES_USER=remote -EnvironmentFile=/etc/vibe-kanban/env -HealthCmd=pg_isready -U remote -d remote -HealthInterval=5s -HealthTimeout=5s -HealthRetries=10 -HealthStartPeriod=5s - -[Service] -Restart=always -TimeoutStartSec=300 - -[Install] -WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban-electric.container b/images/vibe-kanban-remote/vibe-kanban-electric.container deleted file mode 100644 index bb08f87..0000000 --- a/images/vibe-kanban-remote/vibe-kanban-electric.container +++ /dev/null @@ -1,45 +0,0 @@ -# ElectricSQL sync service for vibe-kanban. -# -# Electric streams Postgres logical replication to the browser. remote-server -# treats ELECTRIC_URL as mandatory, so this is not optional. -# -# Starts AFTER vibe-kanban.service because the `electric_sync` role, its grants -# and the publication are created by remote-server's migrations — Electric cannot -# connect until they exist, and remote-server also sets that role's password on -# start. vibe-kanban.service uses Notify=healthy, so this really does wait. -# -# ELECTRIC_INSECURE mirrors upstream's own production compose and is contained: -# Electric publishes no host port and is reachable only by container name on the -# private vibe-kanban bridge. An `electric-secret` exists in pass if we later want -# to set ELECTRIC_SECRET here and on remote-server. -[Unit] -Description=vibe-kanban ElectricSQL sync -After=network-online.target vibe-kanban.service -Wants=network-online.target -Requires=vibe-kanban.service - -[Container] -Image=docker.io/electricsql/electric:1.4.13 -ContainerName=vibe-kanban-electric -AutoUpdate=registry -Network=vibe-kanban.network -Volume=/var/lib/vibe-kanban/electric:/app/persistent:Z -# Its own env file (not the shared one) so the variable can simply be named -# DATABASE_URL — what Electric reads — without colliding with remote-server's -# SERVER_DATABASE_URL, and with no reliance on systemd expanding one Environment= -# value into another, which quadlets do not do dependably. -EnvironmentFile=/etc/vibe-kanban/electric.env -Environment=PG_PROXY_PORT=65432 -Environment=LOGICAL_PUBLISHER_HOST=vibe-kanban-electric -Environment=AUTH_MODE=insecure -Environment=ELECTRIC_INSECURE=true -Environment=ELECTRIC_MANUAL_TABLE_PUBLISHING=true -Environment=ELECTRIC_USAGE_REPORTING=false -Environment=ELECTRIC_FEATURE_FLAGS=allow_subqueries,tagged_subqueries - -[Service] -Restart=always -TimeoutStartSec=300 - -[Install] -WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban.container b/images/vibe-kanban-remote/vibe-kanban.container deleted file mode 100644 index f1563e3..0000000 --- a/images/vibe-kanban-remote/vibe-kanban.container +++ /dev/null @@ -1,48 +0,0 @@ -# vibe-kanban remote-server — the self-hosted server half of the suite. -# Serves both the API and the built SPA (from /srv/static in the image) on :8081 -# as uid 10001; published to the LAN on 27180 (agent-zero=5080, open-webui=5090, -# hermes=5100). Fronted by nginx on hanzalova.internal as https://kanban.internal. -# -# Image is built by lair/containers from OUR mirror of BloopAI/vibe-kanban, never -# from GitHub — upstream is sunsetting. AutoUpdate=registry picks up rebuilds. -# -# Startup order matters and is not merely cosmetic: this unit's sqlx migrations -# are what CREATE the `electric_sync` role and the publication that Electric then -# connects with, and it ALTERs that role's password from ELECTRIC_ROLE_PASSWORD on -# every start. So db -> this -> electric. Notify=healthy makes systemd hold the -# unit "starting" until /v1/health answers, so electric genuinely waits for the -# migrations rather than racing them. -# -# Auth: local single-account mode only (SELF_HOST_LOCAL_AUTH_*), deliberately no -# OAuth — the server refuses to start unless at least one provider is configured. -# Swapping to a real IdP later means adding a provider, not changing this file. -[Unit] -Description=vibe-kanban remote-server -After=network-online.target vibe-kanban-db.service -Wants=network-online.target -Requires=vibe-kanban-db.service - -[Container] -Image=git.lair.cafe/lair/vibe-kanban-remote:latest -ContainerName=vibe-kanban -AutoUpdate=registry -Network=vibe-kanban.network -PublishPort=27180:8081 -EnvironmentFile=/etc/vibe-kanban/env -Environment=SERVER_LISTEN_ADDR=0.0.0.0:8081 -Environment=ELECTRIC_URL=http://vibe-kanban-electric:3000 -Environment=SERVER_PUBLIC_BASE_URL=https://kanban.internal -Environment=RUST_LOG=info,remote=info -Notify=healthy -HealthCmd=wget --spider -q http://127.0.0.1:8081/v1/health -HealthInterval=10s -HealthTimeout=5s -HealthRetries=12 -HealthStartPeriod=20s - -[Service] -Restart=always -TimeoutStartSec=300 - -[Install] -WantedBy=multi-user.target diff --git a/images/vibe-kanban-remote/vibe-kanban.network b/images/vibe-kanban-remote/vibe-kanban.network deleted file mode 100644 index dd54e76..0000000 --- a/images/vibe-kanban-remote/vibe-kanban.network +++ /dev/null @@ -1,8 +0,0 @@ -# Private bridge for the vibe-kanban stack (postgres + electric + remote-server). -# Only remote-server publishes a host port (27180); postgres and electric are -# reachable only by container name on this network, never from the LAN. -[Network] -NetworkName=vibe-kanban - -[Install] -WantedBy=multi-user.target