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