vibe-kanban-remote: move the build to lair/vibe-kanban
Some checks failed
images / hermes (push) Failing after 8m4s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182wzZE8DguMPWhxD21gfP2
This commit is contained in:
2026-07-21 15:54:54 +03:00
parent 3619470d79
commit 6e507186b1
7 changed files with 0 additions and 340 deletions

View File

@@ -80,62 +80,3 @@ jobs:
podman push "${IMAGE}:${VERSION}" podman push "${IMAGE}:${VERSION}"
podman push "${IMAGE}:latest" podman push "${IMAGE}:latest"
echo "published ${IMAGE}:${VERSION} (and :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)"

View File

@@ -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<semver>-<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"

View File

@@ -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<semver>-<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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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