From 8f7d927f108305290ea82ba9ece3c0bcbe70cea9 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 15:59:08 +0000 Subject: [PATCH 01/15] feat(agent): add --version and --help CLI flags --- agent/main.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/agent/main.go b/agent/main.go index 8e32293..32a3597 100644 --- a/agent/main.go +++ b/agent/main.go @@ -10,6 +10,32 @@ import ( ) func main() { + if len(os.Args) > 1 { + switch os.Args[1] { + case "--version", "-v": + fmt.Printf("vf-agent %s\n", agent.Version) + os.Exit(0) + case "--help", "-h": + fmt.Print(`VectorFlow Agent + +Usage: vf-agent [flags] + +Flags: + --version, -v Print version and exit + --help, -h Show this help + +Environment variables: + VF_URL Server URL (required) + VF_TOKEN Enrollment token + VF_DATA_DIR Data directory (default: /var/lib/vf-agent) + VF_VECTOR_BIN Path to Vector binary (default: vector) + VF_POLL_INTERVAL Poll interval duration (default: 15s) + VF_LOG_LEVEL Log level: debug|info|warn|error (default: info) +`) + os.Exit(0) + } + } + cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "config error: %v\n", err) From 3c9d0cddecb114ff15f7893cb118f3bff8366912 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 15:59:37 +0000 Subject: [PATCH 02/15] ci: publish dev agent binaries as rolling pre-release --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dafe379..2cd9f55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,49 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + agent-dev-binaries: + name: Agent Dev Binaries + needs: check + if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + cache-dependency-path: agent/go.sum + + - name: Build dev binaries + working-directory: agent + run: | + SHORT_SHA="${GITHUB_SHA::7}" + VERSION="dev-${SHORT_SHA}" + LDFLAGS="-s -w -X github.com/TerrifiedBug/vectorflow/agent/internal/agent.Version=${VERSION}" + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o ../vf-agent-linux-amd64 . + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o ../vf-agent-linux-arm64 . + echo "${VERSION}" > ../dev-version.txt + + - name: Generate checksums + run: sha256sum vf-agent-linux-* > checksums.txt + + - name: Publish dev pre-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Delete existing dev release if present (gh errors if not found, ignore) + gh release delete dev --yes --cleanup-tag 2>/dev/null || true + # Create fresh pre-release pointing at current commit + gh release create dev \ + --title "Development Build" \ + --notes "Rolling dev build from \`${GITHUB_SHA::7}\` on main. Not for production use." \ + --target "${GITHUB_SHA}" \ + --prerelease \ + vf-agent-linux-amd64 \ + vf-agent-linux-arm64 \ + checksums.txt \ + dev-version.txt + agent-binaries: name: Agent Binaries needs: check From d34e659d067617ad606b358210f331dfd8543e3d Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:00:12 +0000 Subject: [PATCH 03/15] feat(agent): add --channel dev flag to install script --- agent/install.sh | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/agent/install.sh b/agent/install.sh index 397c035..96426f3 100755 --- a/agent/install.sh +++ b/agent/install.sh @@ -17,6 +17,7 @@ VECTOR_VERSION="0.44.0" VF_URL="" VF_TOKEN="" VERSION="latest" +CHANNEL="stable" # ───────────────────────────────────────────────── # Helpers @@ -39,6 +40,7 @@ Options: --url VectorFlow server URL (e.g. https://vectorflow.example.com) --token One-time enrollment token from the VectorFlow UI --version Release version to install (default: latest) + --channel Release channel: stable or dev (default: stable) --help Show this help message Examples: @@ -50,6 +52,9 @@ Examples: # Install specific version curl -sSfL .../install.sh | sudo bash -s -- --version v0.3.0 + + # Install dev channel + curl -sSfL .../install.sh | sudo bash -s -- --channel dev --url https://vf.example.com --token abc123 EOF exit 0 } @@ -63,11 +68,16 @@ while [ $# -gt 0 ]; do --url) VF_URL="$2"; shift 2 ;; --token) VF_TOKEN="$2"; shift 2 ;; --version) VERSION="$2"; shift 2 ;; + --channel) CHANNEL="$2"; shift 2 ;; --help) usage ;; *) fatal "Unknown option: $1 (use --help for usage)" ;; esac done +if [ "${CHANNEL}" = "dev" ] && [ "${VERSION}" != "latest" ]; then + fatal "--channel dev and --version are mutually exclusive" +fi + # ───────────────────────────────────────────────── # Preflight checks # ───────────────────────────────────────────────── @@ -96,22 +106,30 @@ info "Detected architecture: ${ARCH}" # Resolve version # ───────────────────────────────────────────────── -if [ "${VERSION}" = "latest" ]; then - info "Resolving latest release..." - VERSION=$(curl -sSf "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') - [ -n "${VERSION}" ] || fatal "Could not determine latest release version" +if [ "${CHANNEL}" = "dev" ]; then + info "Using dev channel..." + VERSION="dev" + BINARY_NAME="vf-agent-linux-${ARCH}" + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/dev/${BINARY_NAME}" + CHECKSUM_URL="https://github.com/${REPO}/releases/download/dev/checksums.txt" +else + if [ "${VERSION}" = "latest" ]; then + info "Resolving latest release..." + VERSION=$(curl -sSf "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + [ -n "${VERSION}" ] || fatal "Could not determine latest release version" + fi + info "Target version: ${VERSION}" + + BINARY_NAME="vf-agent-linux-${ARCH}" + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}" + CHECKSUM_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt" fi -info "Target version: ${VERSION}" # ───────────────────────────────────────────────── # Download and verify agent binary # ───────────────────────────────────────────────── -BINARY_NAME="vf-agent-linux-${ARCH}" -DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}" -CHECKSUM_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt" - TMPDIR=$(mktemp -d) trap 'rm -rf "${TMPDIR}"' EXIT @@ -188,6 +206,7 @@ VF_URL=${VF_URL} VF_TOKEN=${VF_TOKEN} VF_DATA_DIR=${DATA_DIR} VF_VECTOR_BIN=${INSTALL_DIR}/vector +VF_CHANNEL=${CHANNEL} ENVEOF chmod 0600 "${ENV_FILE}" ok "Environment file written" From d7db0965552135a0dfcf0088ce45cbb2a93480eb Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:01:13 +0000 Subject: [PATCH 04/15] feat: dev-aware version comparison for agent updates --- src/lib/version.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/version.ts b/src/lib/version.ts index 21ea715..ba4bd7f 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1,9 +1,26 @@ /** - * Returns true if `current` is an older semver than `latest`. - * Handles multi-digit segments correctly (e.g., "0.9.0" < "0.10.0"). + * Returns true if `current` is an older version than `latest`. + * + * For release versions: standard semver comparison. + * For dev versions: true if SHAs differ (any difference = update available). + * Cross-channel (dev vs release): always false. */ export function isVersionOlder(current: string, latest: string): boolean { + const currentIsDev = current.startsWith("dev-"); + const latestIsDev = latest.startsWith("dev-"); + + // Plain "dev" (local build, no SHA) — not trackable if (current === "dev" || latest === "dev") return false; + + // Cross-channel: never suggest updates + if (currentIsDev !== latestIsDev) return false; + + // Dev-to-dev: different SHA means update available + if (currentIsDev && latestIsDev) { + return current !== latest; + } + + // Release-to-release: semver comparison const a = current.split(".").map(Number); const b = latest.split(".").map(Number); for (let i = 0; i < Math.max(a.length, b.length); i++) { From 7e8b1f5477c7648358593a3bbdf477442b6e43fd Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:01:25 +0000 Subject: [PATCH 05/15] db: add dev agent release tracking fields to SystemSettings --- prisma/schema.prisma | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3e68e44..111da15 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -415,6 +415,8 @@ model SystemSettings { latestServerReleaseCheckedAt DateTime? latestAgentRelease String? latestAgentReleaseCheckedAt DateTime? + latestDevAgentRelease String? + latestDevAgentReleaseCheckedAt DateTime? updatedAt DateTime @updatedAt } From 1e13f34445f76bc2d252fa5e87bb9017752107c6 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:02:35 +0000 Subject: [PATCH 06/15] feat: add dev channel agent version checking --- src/server/services/version-check.ts | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/server/services/version-check.ts b/src/server/services/version-check.ts index 00e149a..8970615 100644 --- a/src/server/services/version-check.ts +++ b/src/server/services/version-check.ts @@ -160,3 +160,76 @@ export async function checkAgentVersion(force = false): Promise<{ return { latestVersion, checksums, checkedAt }; } + +async function fetchDevRelease(): Promise { + try { + const res = await fetch( + `${GITHUB_API}/repos/${AGENT_REPO}/releases/tags/dev`, + { + headers: { Accept: "application/vnd.github.v3+json" }, + next: { revalidate: 0 }, + }, + ); + if (!res.ok) return null; + return res.json(); + } catch { + return null; + } +} + +async function fetchDevVersionString( + release: GitHubReleaseWithAssets, +): Promise { + const asset = release.assets.find((a) => a.name === "dev-version.txt"); + if (!asset) return null; + try { + const res = await fetch(asset.browser_download_url); + if (!res.ok) return null; + return (await res.text()).trim(); + } catch { + return null; + } +} + +export async function checkDevAgentVersion(force = false): Promise<{ + latestVersion: string | null; + checksums: Record; + checkedAt: Date | null; +}> { + const settings = await prisma.systemSettings.findUnique({ + where: { id: "singleton" }, + }); + + const lastChecked = settings?.latestDevAgentReleaseCheckedAt; + const needsCheck = + force || + !lastChecked || + Date.now() - lastChecked.getTime() > CHECK_INTERVAL_MS; + + let latestVersion = settings?.latestDevAgentRelease ?? null; + let checksums: Record = {}; + let checkedAt: Date | null = lastChecked ?? null; + + if (needsCheck) { + const release = await fetchDevRelease(); + if (release) { + latestVersion = await fetchDevVersionString(release) ?? "dev-unknown"; + checksums = await fetchChecksums(release); + checkedAt = new Date(); + await prisma.systemSettings.upsert({ + where: { id: "singleton" }, + update: { + latestDevAgentRelease: latestVersion, + latestDevAgentReleaseCheckedAt: checkedAt, + }, + create: { + id: "singleton", + latestDevAgentRelease: latestVersion, + latestDevAgentReleaseCheckedAt: checkedAt, + }, + }); + } + } + + return { latestVersion, checksums, checkedAt }; +} From 58c53369a279f2d415699bbd449dfa3db011323d Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:04:25 +0000 Subject: [PATCH 07/15] feat: expose dev agent version in fleet router --- src/server/routers/settings.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/server/routers/settings.ts b/src/server/routers/settings.ts index 52b675f..a5fd06a 100644 --- a/src/server/routers/settings.ts +++ b/src/server/routers/settings.ts @@ -5,7 +5,7 @@ import { prisma } from "@/lib/prisma"; import { encrypt, decrypt } from "@/server/services/crypto"; import { withAudit } from "@/server/middleware/audit"; import { invalidateAuthCache } from "@/auth"; -import { checkServerVersion, checkAgentVersion } from "@/server/services/version-check"; +import { checkServerVersion, checkAgentVersion, checkDevAgentVersion } from "@/server/services/version-check"; import { createBackup, listBackups, @@ -277,11 +277,20 @@ export const settingsRouter = router({ checkVersion: protectedProcedure .input(z.object({ force: z.boolean().optional() }).optional()) .query(async ({ input }) => { - const [server, agent] = await Promise.all([ + const [server, agent, devAgent] = await Promise.all([ checkServerVersion(input?.force), checkAgentVersion(input?.force), + checkDevAgentVersion(input?.force), ]); - return { server, agent }; + return { + server, + agent, + devAgent: { + latestVersion: devAgent.latestVersion, + checksums: devAgent.checksums, + checkedAt: devAgent.checkedAt, + }, + }; }), // ─── Backup & Restore ───────────────────────────────────────────────────── From 64b9a80b875a6b307f25fe755d8135045de96f80 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 16:06:00 +0000 Subject: [PATCH 08/15] feat: channel-aware agent update buttons in fleet UI --- src/app/(dashboard)/fleet/page.tsx | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index 445515e..b09284f 100644 --- a/src/app/(dashboard)/fleet/page.tsx +++ b/src/app/(dashboard)/fleet/page.tsx @@ -69,6 +69,15 @@ export default function FleetPage() { ); const latestAgentVersion = versionQuery.data?.agent.latestVersion ?? null; const agentChecksums = versionQuery.data?.agent.checksums ?? {}; + const latestDevAgentVersion = versionQuery.data?.devAgent?.latestVersion ?? null; + const devAgentChecksums = versionQuery.data?.devAgent?.checksums ?? {}; + + const getNodeLatest = (node: { agentVersion: string | null }) => { + if (node.agentVersion?.startsWith("dev-")) { + return { version: latestDevAgentVersion, checksums: devAgentChecksums, tag: "dev" }; + } + return { version: latestAgentVersion, checksums: agentChecksums, tag: `v${latestAgentVersion}` }; + }; const triggerUpdate = useMutation( trpc.fleet.triggerAgentUpdate.mutationOptions({ @@ -132,9 +141,9 @@ export default function FleetPage() { {node.agentVersion ?? "—"} - {latestAgentVersion && + {getNodeLatest(node).version && node.agentVersion && - isVersionOlder(node.agentVersion, latestAgentVersion) && ( + isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") && ( Update available @@ -165,9 +174,9 @@ export default function FleetPage() { Update pending... ) : node.deploymentMode === "DOCKER" ? ( - latestAgentVersion && + getNodeLatest(node).version && node.agentVersion && - isVersionOlder(node.agentVersion, latestAgentVersion) ? ( + isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( @@ -179,20 +188,21 @@ export default function FleetPage() { Update via Docker image pull ) : null - ) : latestAgentVersion && + ) : getNodeLatest(node).version && node.agentVersion && - isVersionOlder(node.agentVersion, latestAgentVersion) ? ( + isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? (