From dff283c4688f075624f0042c761a2b1678ff8c5a Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 20 Jul 2026 18:18:43 +0300 Subject: [PATCH] feat(vibe-kanban-remote): build the self-hosted remote-server image Builds crates/remote/Dockerfile from our mirror at git.lair.cafe rather than from GitHub, and resolves the version from the mirror's tags rather than GitHub's releases API. BloopAI has announced a sunset; the mirror exists so this build outlives them, which is pointless if the build still asks github.com what to build. Nothing in this image's path touches GitHub. Gitea mirrors carry tags but not releases, so latest is resolved by filtering to the strict release pattern v-<14-digit datestamp> and sorting on the datestamp -- which also skips the malformed historical tags in the upstream repo (vv.20250708094151, vv0.0.40-nbump.2....). FEATURES is deliberately left unset: the Dockerfile strips the private billing crate only when it is empty, which is the documented self-host path. Setting it would send the build looking for BloopAI/vibe-kanban-private over SSH, which we cannot reach. Consumed by the vibe-kanban quadlets on bob (kanban.internal). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TsmUEtbyTkgQ18tCFYXo1h --- .gitea/workflows/images.yml | 59 +++++++++++++++++++++++++++ images/vibe-kanban-remote/build.sh | 50 +++++++++++++++++++++++ images/vibe-kanban-remote/readme.md | 62 +++++++++++++++++++++++++++++ readme.md | 1 + 4 files changed, 172 insertions(+) create mode 100755 images/vibe-kanban-remote/build.sh create mode 100644 images/vibe-kanban-remote/readme.md diff --git a/.gitea/workflows/images.yml b/.gitea/workflows/images.yml index b8599f7..2b85bde 100644 --- a/.gitea/workflows/images.yml +++ b/.gitea/workflows/images.yml @@ -80,3 +80,62 @@ 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 new file mode 100755 index 0000000..26f33c6 --- /dev/null +++ b/images/vibe-kanban-remote/build.sh @@ -0,0 +1,50 @@ +#!/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 new file mode 100644 index 0000000..99fc56e --- /dev/null +++ b/images/vibe-kanban-remote/readme.md @@ -0,0 +1,62 @@ +# 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. + +## Local build + +```sh +./build.sh # latest tag from the mirror +VK_REF=v0.1.44-20260424091429 ./build.sh # a specific tag +``` diff --git a/readme.md b/readme.md index 8c647d5..266334e 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,7 @@ images// one directory per image | Image | Published as | Source | |-------|--------------|--------| | [hermes](images/hermes/readme.md) | `git.lair.cafe/lair/hermes:{version,latest}` | built from NousResearch/hermes-agent's Dockerfile at the latest release tag | +| [vibe-kanban-remote](images/vibe-kanban-remote/readme.md) | `git.lair.cafe/lair/vibe-kanban-remote:{version,latest}` | built from **our mirror** `BloopAI/vibe-kanban`'s `crates/remote/Dockerfile` at the latest release tag — deliberately never touches GitHub, since upstream is sunsetting | ## How builds work