From ccff6fc5399746b398e318e7956ab66a1bece4c9 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Tue, 21 Jul 2026 15:46:49 +0300 Subject: [PATCH] ci: build and publish the remote-server image from this repo The build lived in lair/containers, which is for third-party images built from someone else's source. We own this fork now, so it belongs next to the code it builds: a source change and the image it produces become one commit rather than two repos that can drift. Differences from the lair/containers job it replaces: - Builds the checked-out tree instead of resolving a tag from the mirror and building `#`. There is no upstream release to track any more; what is on main is what ships. - Publishes an immutable ${version}-g${sha} tag alongside :latest, and prints the pin. A floating tag decides which build you are running based on whenever something last pulled, which is not a deployment strategy. - Drops the `skopeo inspect` already-published guard. skopeo is not installed on the metal runner, so that check silently failed and always fell through to a build -- an accident, not a policy. Building every push to main is the intent here anyway. - Adds a guard that greps the built bundle for the sunset page and refuses to publish if it reappears. This fork exists to remove it; a bad merge should fail the build, not ship a dead product. Verified against the currently-deployed image, where it correctly fires. concurrency keeps two builds from racing over :latest. --- .gitea/workflows/container.yml | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .gitea/workflows/container.yml diff --git a/.gitea/workflows/container.yml b/.gitea/workflows/container.yml new file mode 100644 index 00000000..22e522f0 --- /dev/null +++ b/.gitea/workflows/container.yml @@ -0,0 +1,110 @@ +name: container +# Build the self-hosted remote-server image from THIS repo and publish it to the +# Gitea registry as git.lair.cafe/lair/vibe-kanban-remote. +# +# 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. +# +# vibe-kanban ships its own Dockerfile (crates/remote/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 tag and the +# loser's digest would win at random. +concurrency: + group: container + cancel-in-progress: false + +jobs: + vibe-kanban-remote: + runs-on: + - metal + - podman + # The Rust workspace builds in release mode from a cold cache on a fresh + # runner; 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 "version=${version}" >> "$GITHUB_OUTPUT" + 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; empty + # disables the relay/tunnel features, which we do not deploy. Changing + # it requires a rebuild, not a restart. + podman build --pull=newer \ + -f crates/remote/Dockerfile \ + --build-arg VITE_RELAY_API_BASE_URL= \ + -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}"