diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dafe379..5f9c1d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,52 @@ 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 + concurrency: + group: dev-release + cancel-in-progress: true + 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 diff --git a/agent/install.sh b/agent/install.sh index 397c035..04795b1 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 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) diff --git a/prisma/migrations/20260305000000_add_dev_agent_version/migration.sql b/prisma/migrations/20260305000000_add_dev_agent_version/migration.sql new file mode 100644 index 0000000..1f9c7d4 --- /dev/null +++ b/prisma/migrations/20260305000000_add_dev_agent_version/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "SystemSettings" ADD COLUMN "latestAgentChecksums" TEXT; +ALTER TABLE "SystemSettings" ADD COLUMN "latestDevAgentRelease" TEXT; +ALTER TABLE "SystemSettings" ADD COLUMN "latestDevAgentReleaseCheckedAt" TIMESTAMP(3); +ALTER TABLE "SystemSettings" ADD COLUMN "latestDevAgentChecksums" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3e68e44..605b4b4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -411,10 +411,14 @@ model SystemSettings { lastBackupStatus String? lastBackupError String? - latestServerRelease String? - latestServerReleaseCheckedAt DateTime? - latestAgentRelease String? - latestAgentReleaseCheckedAt DateTime? + latestServerRelease String? + latestServerReleaseCheckedAt DateTime? + latestAgentRelease String? + latestAgentReleaseCheckedAt DateTime? + latestAgentChecksums String? // JSON: {"vf-agent-linux-amd64":"sha256..."} + latestDevAgentRelease String? + latestDevAgentReleaseCheckedAt DateTime? + latestDevAgentChecksums String? // JSON: {"vf-agent-linux-amd64":"sha256..."} updatedAt DateTime @updatedAt } diff --git a/src/app/(dashboard)/fleet/page.tsx b/src/app/(dashboard)/fleet/page.tsx index 445515e..e74d648 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: latestAgentVersion ? `v${latestAgentVersion}` : null }; + }; 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 ?? "") ? (