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.
164 lines
6.1 KiB
YAML
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)"
|