host: build and publish the local server as a signed RPM
Some checks failed
Some checks failed
The banner on a host comes from `npx vibe-kanban` fetching upstream's published 0.1.44, which embeds upstream's frontend. Our fork already fixes that UI -- the revert restored LocalProjectKanban and showCloudShutdownBanner is pinned false in SharedAppLayout, which local-web uses -- it just was not in the binary anyone was running. npx cannot be pointed at us. npx-cli resolves its download from R2_BASE_URL, a placeholder substituted at npm-publish time to BloopAI's bucket, with no runtime override. That bucket serves the sunset binary and dies with them. So build our own. crates/server embeds packages/local-web/dist via rust-embed and crates/server/build.rs reads VK_SHARED_API_BASE at compile time, so one build yields the fixed UI and our endpoints as defaults, with no environment on the command line -- std::env::var still wins at runtime for overrides. Packaged as an RPM to rpm.lair.cafe, mirroring lair/claude-desktop-package: package and publish on the `rpm` runner, sign, rsync to oolon, createrepo under the shared flock. The compile runs first on a podman runner instead, so this never depends on the rpm host's toolchain matching the nightly pinned in rust-toolchain.toml. Built on bookworm although the target is Fedora 43/44: glibc compatibility is forward-only, so one binary covers both. The build fails closed if the sunset strings reappear or the baked endpoint is missing. Needs RPM_SIGNING_KEY, RPM_SIGNING_KEY_ID and RSYNC_SSH_KEY on this repo; they currently exist only on lair/claude-desktop-package.
This commit is contained in:
198
.gitea/workflows/host-release.yml
Normal file
198
.gitea/workflows/host-release.yml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
name: host-release
|
||||||
|
# Build the LOCAL vibe-kanban server and publish it as a signed RPM to
|
||||||
|
# rpm.lair.cafe, so a host installs it with dnf instead of `npx vibe-kanban`.
|
||||||
|
#
|
||||||
|
# npx is not an option here: npx-cli resolves its download from R2_BASE_URL, a
|
||||||
|
# placeholder substituted at npm-publish time to BloopAI's bucket, with no
|
||||||
|
# runtime override (npx-cli/src/download.ts). That serves upstream's binary --
|
||||||
|
# which still carries the product sunset -- from infrastructure that disappears
|
||||||
|
# when they do.
|
||||||
|
#
|
||||||
|
# Convention mirrors lair/claude-desktop-package: package and publish on the
|
||||||
|
# `rpm` runner, sign with RPM_SIGNING_KEY, rsync to oolon, createrepo under
|
||||||
|
# flock. The compile happens first on a podman runner rather than on the rpm
|
||||||
|
# runner, so this never depends on a host toolchain matching the pinned
|
||||||
|
# nightly in rust-toolchain.toml.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "crates/**"
|
||||||
|
- "packages/local-web/**"
|
||||||
|
- "packages/web-core/**"
|
||||||
|
- "packages/ui/**"
|
||||||
|
- "shared/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- "rust-toolchain.toml"
|
||||||
|
- "rpm/**"
|
||||||
|
- ".gitea/workflows/host-release.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: host-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on:
|
||||||
|
- metal
|
||||||
|
- podman
|
||||||
|
timeout-minutes: 120
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: derive version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: build the host binary
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# --target export writes the binary straight out of the scratch stage,
|
||||||
|
# so nothing has to be extracted from an image afterwards.
|
||||||
|
mkdir -p out
|
||||||
|
podman build --pull=newer \
|
||||||
|
-f rpm/Containerfile \
|
||||||
|
--target export \
|
||||||
|
--output type=local,dest=out \
|
||||||
|
.
|
||||||
|
test -s out/vibe-kanban
|
||||||
|
chmod 0755 out/vibe-kanban
|
||||||
|
file out/vibe-kanban || true
|
||||||
|
|
||||||
|
- name: sanity-check the binary
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# The whole point of building our own: upstream's carries the sunset.
|
||||||
|
if grep -qa "Project functionality has been retired" out/vibe-kanban; then
|
||||||
|
echo "ERROR: the sunset page is embedded in the host binary."
|
||||||
|
echo "The revert of #3387 has been lost -- refusing to publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -qa "Cloud is shutting down" out/vibe-kanban; then
|
||||||
|
echo "ERROR: the cloud-shutdown banner is embedded in the host binary."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# And it must point at us, not at api.vibekanban.com.
|
||||||
|
grep -qa "kanban.internal" out/vibe-kanban \
|
||||||
|
|| { echo "ERROR: baked API base missing from the binary"; exit 1; }
|
||||||
|
echo "ok: no sunset UI, lair endpoints baked in"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: host-binary
|
||||||
|
path: out/vibe-kanban
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.meta.outputs.version }}
|
||||||
|
commit: ${{ steps.meta.outputs.commit }}
|
||||||
|
|
||||||
|
package:
|
||||||
|
needs: build
|
||||||
|
runs-on: rpm
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
fedora_version: ["43", "44"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: host-binary
|
||||||
|
path: bin/
|
||||||
|
|
||||||
|
- name: rpmbuild
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
find bin/ -name vibe-kanban -exec mv --target-directory=bin/ {} + 2>/dev/null || true
|
||||||
|
mkdir -p ~/rpmbuild/SOURCES
|
||||||
|
install -m 0755 bin/vibe-kanban ~/rpmbuild/SOURCES/vibe-kanban
|
||||||
|
rpmbuild -bb rpm/vibe-kanban.spec \
|
||||||
|
--define "vk_version ${{ needs.build.outputs.version }}" \
|
||||||
|
--define "vk_commit ${{ needs.build.outputs.commit }}" \
|
||||||
|
--define "dist .fc${{ matrix.fedora_version }}"
|
||||||
|
ls -l ~/rpmbuild/RPMS/x86_64/
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: rpm-fc${{ matrix.fedora_version }}
|
||||||
|
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: package
|
||||||
|
runs-on: rpm
|
||||||
|
env:
|
||||||
|
RPM_REPO_HOST: oolon.kosherinata.internal
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
fedora_version: ["43", "44"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: rpm-fc${{ matrix.fedora_version }}
|
||||||
|
path: rpms/
|
||||||
|
|
||||||
|
- name: Flatten RPM artifacts
|
||||||
|
run: |
|
||||||
|
find rpms/ -name '*.rpm' -exec mv --target-directory=rpms/ {} +
|
||||||
|
find rpms/ -mindepth 1 -type d -empty -delete
|
||||||
|
|
||||||
|
- name: Import signing key
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
|
||||||
|
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
|
||||||
|
echo "${fpr}:6:" | gpg --batch --import-ownertrust
|
||||||
|
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
|
||||||
|
|
||||||
|
- name: Sign RPMs
|
||||||
|
run: |
|
||||||
|
for rpm in rpms/*.rpm; do
|
||||||
|
echo "signing ${rpm}..."
|
||||||
|
rpm --addsign "${rpm}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
install --directory --mode 700 ~/.ssh
|
||||||
|
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
|
||||||
|
env:
|
||||||
|
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
|
||||||
|
|
||||||
|
- name: Test SSH connectivity
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
|
||||||
|
|
||||||
|
- name: Sync RPMs to repo
|
||||||
|
run: |
|
||||||
|
rsync \
|
||||||
|
--archive \
|
||||||
|
--verbose \
|
||||||
|
--chmod D755,F644 \
|
||||||
|
rpms/*.rpm \
|
||||||
|
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/"
|
||||||
|
|
||||||
|
- name: Update repo metadata
|
||||||
|
run: |
|
||||||
|
# flock guards createrepo against concurrent publishes into the
|
||||||
|
# shared repo tree (other package repos publish here too).
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"flock /var/www/rpm/.publish.lock -c 'cd /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64 && createrepo_c --update .'"
|
||||||
|
|
||||||
|
- name: Generate packages.json
|
||||||
|
run: |
|
||||||
|
scp rpm/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
|
||||||
|
ssh "gitea_ci@${RPM_REPO_HOST}" \
|
||||||
|
"flock /var/www/rpm/.publish.lock -c 'python3 /tmp/generate-packages-json.py \
|
||||||
|
--repodata-dir /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/repodata \
|
||||||
|
--output /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/packages.json \
|
||||||
|
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64'"
|
||||||
64
rpm/Containerfile
Normal file
64
rpm/Containerfile
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Builder for the LOCAL vibe-kanban server (crates/server) — the binary a host
|
||||||
|
# runs to pair with our remote. Produces /out/vibe-kanban.
|
||||||
|
#
|
||||||
|
# `npx vibe-kanban` cannot be used for this: npx-cli resolves its download from
|
||||||
|
# R2_BASE_URL, a placeholder substituted at npm-publish time to BloopAI's bucket
|
||||||
|
# (npx-cli/src/download.ts) with no runtime override. That bucket ships upstream's
|
||||||
|
# binary -- which carries the product sunset -- and disappears when they do.
|
||||||
|
#
|
||||||
|
# Built on bookworm although the RPM targets Fedora 43/44. glibc compatibility is
|
||||||
|
# forward-only, so a binary linked against bookworm's 2.36 runs on Fedora's newer
|
||||||
|
# glibc; the reverse would not. One binary therefore serves both Fedora targets.
|
||||||
|
|
||||||
|
FROM node:20-alpine AS fe-builder
|
||||||
|
WORKDIR /repo
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
# Lockfile-first so a source-only change does not re-resolve ~900 packages.
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
|
COPY packages/local-web/package.json packages/local-web/package.json
|
||||||
|
COPY packages/remote-web/package.json packages/remote-web/package.json
|
||||||
|
COPY packages/ui/package.json packages/ui/package.json
|
||||||
|
COPY packages/web-core/package.json packages/web-core/package.json
|
||||||
|
COPY patches/ patches/
|
||||||
|
RUN --mount=type=cache,id=pnpm-local,target=/pnpm/store \
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY packages/ packages/
|
||||||
|
COPY shared/ shared/
|
||||||
|
# PostHog/Sentry VITE_ vars are deliberately unset: unset means no telemetry.
|
||||||
|
RUN pnpm -C packages/local-web build
|
||||||
|
|
||||||
|
FROM rust:1.93-slim-bookworm AS builder
|
||||||
|
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||||
|
ENV CARGO_TARGET_DIR=/app/target
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Whole-tree copy rather than enumerating workspace manifests: crates/server
|
||||||
|
# pulls in most of the workspace, and an enumerated list silently breaks when a
|
||||||
|
# crate is added. The cargo cache mounts below carry the incremental build.
|
||||||
|
COPY . .
|
||||||
|
COPY --from=fe-builder /repo/packages/local-web/dist packages/local-web/dist
|
||||||
|
|
||||||
|
# Baked at compile time by crates/server/build.rs, so the installed binary points
|
||||||
|
# at our instance with no environment on the command line. std::env::var still
|
||||||
|
# takes precedence at runtime, so either can be overridden per-invocation.
|
||||||
|
ARG VK_SHARED_API_BASE=https://kanban.internal
|
||||||
|
ARG VK_SHARED_RELAY_API_BASE=https://kanban.internal/relay-api
|
||||||
|
ENV VK_SHARED_API_BASE=${VK_SHARED_API_BASE}
|
||||||
|
ENV VK_SHARED_RELAY_API_BASE=${VK_SHARED_RELAY_API_BASE}
|
||||||
|
|
||||||
|
RUN --mount=type=cache,id=cargo-registry,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,id=cargo-git,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,id=host-target,target=/app/target \
|
||||||
|
cargo build --release --manifest-path crates/server/Cargo.toml \
|
||||||
|
&& mkdir -p /out \
|
||||||
|
&& cp /app/target/release/server /out/vibe-kanban
|
||||||
|
|
||||||
|
FROM scratch AS export
|
||||||
|
COPY --from=builder /out/vibe-kanban /vibe-kanban
|
||||||
154
rpm/generate-packages-json.py
Normal file
154
rpm/generate-packages-json.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Parse RPM repodata and emit a packages.json manifest for the UI."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
RPM_NS = "http://linux.duke.edu/metadata/common"
|
||||||
|
OTHER_NS = "http://linux.duke.edu/metadata/other"
|
||||||
|
REPO_NS = "http://linux.duke.edu/metadata/repo"
|
||||||
|
|
||||||
|
|
||||||
|
def find_repodata_file(repodata_dir, data_type):
|
||||||
|
"""Read repomd.xml and return the path to a specific data type's file."""
|
||||||
|
repomd_path = os.path.join(repodata_dir, "repomd.xml")
|
||||||
|
tree = ET.parse(repomd_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
for data in root.findall(f"{{{REPO_NS}}}data"):
|
||||||
|
if data.get("type") == data_type:
|
||||||
|
location = data.find(f"{{{REPO_NS}}}location")
|
||||||
|
if location is not None:
|
||||||
|
href = location.get("href", "")
|
||||||
|
return os.path.join(os.path.dirname(repodata_dir), href)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def open_compressed(path):
|
||||||
|
"""Open a gzip or zstd compressed file for reading."""
|
||||||
|
if path.endswith(".zst"):
|
||||||
|
result = subprocess.run(
|
||||||
|
["zstdcat", path], capture_output=True, check=True
|
||||||
|
)
|
||||||
|
import io
|
||||||
|
return io.BytesIO(result.stdout)
|
||||||
|
else:
|
||||||
|
return gzip.open(path, "rb")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_primary(repodata_dir):
|
||||||
|
"""Parse primary.xml.{gz,zst} and return package metadata."""
|
||||||
|
path = find_repodata_file(repodata_dir, "primary")
|
||||||
|
if not path:
|
||||||
|
print("error: primary metadata not found in repomd.xml", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
packages = {}
|
||||||
|
with open_compressed(path) as f:
|
||||||
|
tree = ET.parse(f)
|
||||||
|
|
||||||
|
for pkg in tree.getroot().findall(f"{{{RPM_NS}}}package"):
|
||||||
|
if pkg.get("type") != "rpm":
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = pkg.findtext(f"{{{RPM_NS}}}name", "")
|
||||||
|
version_el = pkg.find(f"{{{RPM_NS}}}version")
|
||||||
|
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||||
|
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||||
|
arch = pkg.findtext(f"{{{RPM_NS}}}arch", "")
|
||||||
|
|
||||||
|
size_el = pkg.find(f"{{{RPM_NS}}}size")
|
||||||
|
size = int(size_el.get("package", "0")) if size_el is not None else 0
|
||||||
|
|
||||||
|
time_el = pkg.find(f"{{{RPM_NS}}}time")
|
||||||
|
build_time = int(time_el.get("build", "0")) if time_el is not None else 0
|
||||||
|
|
||||||
|
location_el = pkg.find(f"{{{RPM_NS}}}location")
|
||||||
|
filename = os.path.basename(location_el.get("href", "")) if location_el is not None else ""
|
||||||
|
|
||||||
|
key = f"{name}-{ver}-{rel}"
|
||||||
|
packages[key] = {
|
||||||
|
"name": name,
|
||||||
|
"version": ver,
|
||||||
|
"release": rel,
|
||||||
|
"arch": arch,
|
||||||
|
"summary": pkg.findtext(f"{{{RPM_NS}}}summary", ""),
|
||||||
|
"size": size,
|
||||||
|
"buildTime": build_time,
|
||||||
|
"rpmFilename": filename,
|
||||||
|
"changelog": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
|
||||||
|
|
||||||
|
def parse_other(repodata_dir, packages):
|
||||||
|
"""Parse other.xml.gz and attach changelog entries to packages."""
|
||||||
|
path = find_repodata_file(repodata_dir, "other")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open_compressed(path) as f:
|
||||||
|
tree = ET.parse(f)
|
||||||
|
|
||||||
|
for pkg in tree.getroot().findall(f"{{{OTHER_NS}}}package"):
|
||||||
|
name = pkg.get("name", "")
|
||||||
|
version_el = pkg.find(f"{{{OTHER_NS}}}version")
|
||||||
|
ver = version_el.get("ver", "") if version_el is not None else ""
|
||||||
|
rel = version_el.get("rel", "") if version_el is not None else ""
|
||||||
|
key = f"{name}-{ver}-{rel}"
|
||||||
|
|
||||||
|
if key not in packages:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for entry in pkg.findall(f"{{{OTHER_NS}}}changelog"):
|
||||||
|
packages[key]["changelog"].append({
|
||||||
|
"author": entry.get("author", ""),
|
||||||
|
"date": int(entry.get("date", "0")),
|
||||||
|
"text": (entry.text or "").strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repodata-dir",
|
||||||
|
required=True,
|
||||||
|
help="path to the repodata/ directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
required=True,
|
||||||
|
help="path to write packages.json",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base-url",
|
||||||
|
required=True,
|
||||||
|
help="public base URL for the repo (e.g. https://rpm.lair.cafe/fedora/43/x86_64)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
packages = parse_primary(args.repodata_dir)
|
||||||
|
parse_other(args.repodata_dir, packages)
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"generated": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"baseUrl": args.base_url,
|
||||||
|
"packages": list(packages.values()),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(args.output, "w") as f:
|
||||||
|
json.dump(manifest, f, indent=2)
|
||||||
|
|
||||||
|
print(f"wrote {len(packages)} packages to {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
rpm/rpmmacros
Normal file
1
rpm/rpmmacros
Normal file
@@ -0,0 +1 @@
|
|||||||
|
%_openpgp_sign_id @GPG_NAME@
|
||||||
53
rpm/vibe-kanban.spec
Normal file
53
rpm/vibe-kanban.spec
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Prebuilt Rust binary produced by rpm/Containerfile — no compile here, no
|
||||||
|
# debuginfo (matches the house convention in lair/claude-desktop-package).
|
||||||
|
%global _build_id_links none
|
||||||
|
%global debug_package %{nil}
|
||||||
|
%global __strip /usr/bin/true
|
||||||
|
|
||||||
|
# Passed in via --define at rpmbuild time (see .gitea/workflows/host-release.yml).
|
||||||
|
%{!?vk_version: %global vk_version 0.0.0}
|
||||||
|
%{!?vk_commit: %global vk_commit unknown}
|
||||||
|
|
||||||
|
Name: vibe-kanban
|
||||||
|
Version: %{vk_version}
|
||||||
|
Release: 1.g%{vk_commit}%{?dist}
|
||||||
|
Summary: Local vibe-kanban server — pairs a host with the lair remote
|
||||||
|
|
||||||
|
License: Apache-2.0
|
||||||
|
URL: https://git.lair.cafe/lair/vibe-kanban
|
||||||
|
Source0: vibe-kanban
|
||||||
|
ExclusiveArch: x86_64
|
||||||
|
|
||||||
|
# git is invoked at runtime for worktree and branch operations.
|
||||||
|
Requires: git-core
|
||||||
|
|
||||||
|
%description
|
||||||
|
The local half of vibe-kanban: runs on a development host, registers with the
|
||||||
|
relay at https://kanban.internal/relay-api and serves its own UI, so the host's
|
||||||
|
repositories and coding agents can be driven from the remote board.
|
||||||
|
|
||||||
|
Built from lair/vibe-kanban, our fork of BloopAI/vibe-kanban. Upstream sunset
|
||||||
|
the product in April 2026, replacing the kanban board with an export-only page
|
||||||
|
and adding a cloud-shutdown banner; both are reverted in this fork. It is not
|
||||||
|
`npx vibe-kanban`, which downloads upstream's binary from a bucket that carries
|
||||||
|
the sunset and disappears with them.
|
||||||
|
|
||||||
|
The remote and relay endpoints are compiled in, so no configuration is needed on
|
||||||
|
a lair host. Override per-invocation with VK_SHARED_API_BASE and
|
||||||
|
VK_SHARED_RELAY_API_BASE, which take precedence over the baked defaults.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
# Source0 is the bare binary; nothing to unpack.
|
||||||
|
|
||||||
|
%build
|
||||||
|
# Built upstream of rpmbuild, in a container. See rpm/Containerfile.
|
||||||
|
|
||||||
|
%install
|
||||||
|
install -D -m 0755 %{SOURCE0} %{buildroot}%{_bindir}/vibe-kanban
|
||||||
|
|
||||||
|
%files
|
||||||
|
%{_bindir}/vibe-kanban
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Tue Jul 21 2026 rob thijssen <rthijssen@gmail.com> - 0.1.44-1
|
||||||
|
- First lair build: sunset reverted, remote and relay endpoints baked in.
|
||||||
Reference in New Issue
Block a user