Files
vibe-kanban/.gitea/workflows/container.yml
rob thijssen e06652a57a
All checks were successful
container / relay (push) Successful in 17m20s
container / remote (push) Successful in 36m11s
relay: build and deploy the relay under a path of the existing host
Pairing needs a third service we never deployed. Hosts run the ordinary
local vibe-kanban server (the `server` crate, shipped as `npx
vibe-kanban`); it registers with a relay over a websocket control channel,
and the browser reaches the host's local API through that relay over
WebRTC. The remote server we deploy contains none of it -- its hosts.rs is
a single read-only GET /hosts -- so the UI could list hosts and never pair
one.

Mount it on a path of the existing name (https://kanban.internal/relay-api/)
rather than giving it a domain. Every relay URL on both sides is composed
as {base}/v1/..., the host turns the base into wss:// by stripping only the
scheme, and RelayServerConfig is just database_url/listen_addr/jwt_secret
with no notion of its own public address and no redirects -- so a prefix is
invisible to it. That buys same-origin (no CORS), one less certificate and
one less renewal timer, and when kanban.l4ir.net lands it inherits the
relay by copying one nginx location block.

For that second hostname to work from the SAME build, the SPA's relay base
is origin-relative ("/relay-api") rather than an absolute URL baked at
build time. Two consequences:

- The previous empty value was not "relay disabled", as the comment
  inherited from lair/containers claimed. Empty makes Bootstrap.tsx fall
  back to window.location.origin, aiming relay calls at the remote API,
  which does not serve them. Comment corrected.
- fetch() paths take a relative base fine (plain concatenation), but
  relayHostApi built its websocket by string-replacing http->ws, which
  silently leaves a relative URL that the WebSocket constructor rejects.
  It now uses openBrowserWebSocket, already imported in that file, which
  resolves wss://, https:// and relative alike.

The relay shares the remote's JWT secret -- that is how it trusts tokens
the remote issued -- so both read /etc/vibe-kanban/env.
2026-07-21 17:29:27 +03:00

164 lines
6.1 KiB
YAML

name: container
# Build the self-hosted images from THIS repo and publish them to the Gitea
# registry:
#
# git.lair.cafe/lair/vibe-kanban-remote crates/remote/Dockerfile :8081
# git.lair.cafe/lair/vibe-kanban-relay crates/relay-tunnel/Dockerfile :8082
#
# This build used to live in lair/containers, which is for *third-party* images
# built from someone else's source. We own this fork now, so the build belongs
# next to the code it builds — a source change and its image are one commit.
#
# Both crates ship their own Dockerfile (context = repo root), so there is no
# vendored Containerfile here. Nothing in this workflow reaches out to GitHub:
# upstream is sunset and may disappear, and a build that asks github.com what to
# build would disappear with it.
on:
push:
branches: [main]
paths:
- "crates/**"
- "packages/**"
- "shared/**"
- "assets/**"
- "patches/**"
- "Cargo.toml"
- "Cargo.lock"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "rust-toolchain.toml"
- ".gitea/workflows/container.yml"
workflow_dispatch:
# Never let two builds race: they would fight over the :latest tags and the
# loser's digest would win at random.
concurrency:
group: container
cancel-in-progress: false
jobs:
remote:
runs-on:
- metal
- podman
# The Rust workspace builds in release mode; from a cold cache the default
# job timeout is not enough headroom.
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: derive image tags
id: meta
run: |
set -euo pipefail
version=$(jq -r .version package.json)
short=$(git rev-parse --short HEAD)
# Immutable tag first. :latest is a convenience for humans, never a
# deployment target -- a floating tag decides which build you are
# running based on whenever something last pulled.
echo "immutable=${version}-g${short}" >> "$GITHUB_OUTPUT"
- name: login to registry
run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe
- name: build
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
# 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 at build time and is
# ORIGIN-RELATIVE on purpose. It is not a switch that disables the
# relay -- leaving it empty makes Bootstrap.tsx fall back to
# window.location.origin, which aims relay calls at the remote API,
# which does not serve them. "/relay-api" instead means the SPA calls
# whichever host served it, so one build works unchanged behind both
# kanban.internal and a future public name, with no CORS and no second
# certificate. nginx strips the prefix and forwards to the relay.
podman build --pull=newer \
-f crates/remote/Dockerfile \
--build-arg VITE_RELAY_API_BASE_URL=/relay-api \
-t "${IMAGE}:${IMMUTABLE}" \
-t "${IMAGE}:latest" \
.
- name: verify the sunset really is gone
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
# This fork exists because upstream replaced the board with an
# export-only page. If a merge or a bad rebase ever reinstates it, fail
# here rather than publish an image that greets us with a dead product.
if podman run --rm --entrypoint sh "${IMAGE}:${IMMUTABLE}" -c \
'grep -rlq "Project functionality has been retired" /srv/static' 2>/dev/null; then
echo "ERROR: the sunset page is present in the built bundle."
echo "The revert of #3387 has been lost -- refusing to publish."
exit 1
fi
echo "ok: no sunset page in the bundle"
- name: push
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
podman push "${IMAGE}:${IMMUTABLE}"
podman push "${IMAGE}:latest"
echo "published ${IMAGE}:${IMMUTABLE} (and :latest)"
echo
echo "pin the quadlet to the immutable tag:"
echo " Image=${IMAGE}:${IMMUTABLE}"
relay:
runs-on:
- metal
- podman
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: derive image tags
id: meta
run: |
set -euo pipefail
version=$(jq -r .version package.json)
short=$(git rev-parse --short HEAD)
echo "immutable=${version}-g${short}" >> "$GITHUB_OUTPUT"
- name: login to registry
run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe
- name: build
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-relay
# No build args: the relay takes all of its configuration from the
# environment at runtime (DATABASE_URL, RELAY_LISTEN_ADDR and the JWT
# secret it shares with the remote server). It has no notion of its own
# public address, which is what makes mounting it under a path safe.
podman build --pull=newer \
-f crates/relay-tunnel/Dockerfile \
-t "${IMAGE}:${IMMUTABLE}" \
-t "${IMAGE}:latest" \
.
- name: push
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-relay
podman push "${IMAGE}:${IMMUTABLE}"
podman push "${IMAGE}:latest"
echo "published ${IMAGE}:${IMMUTABLE} (and :latest)"