-
Notifications
You must be signed in to change notification settings - Fork 0
feat: dev binary channel with CLI flags and auto-update #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8f7d927
3c9d0cd
d34e659
d7db096
7e8b1f5
1e13f34
58c5336
64b9a80
f8f47bc
40a264f
966631f
3efbfc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,7 @@ VECTOR_VERSION="0.44.0" | |||||||||||||||||||||||||||||||||||||||||||
| VF_URL="" | ||||||||||||||||||||||||||||||||||||||||||||
| VF_TOKEN="" | ||||||||||||||||||||||||||||||||||||||||||||
| VERSION="latest" | ||||||||||||||||||||||||||||||||||||||||||||
| CHANNEL="stable" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # ───────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||
| # Helpers | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -39,6 +40,7 @@ Options: | |||||||||||||||||||||||||||||||||||||||||||
| --url <url> VectorFlow server URL (e.g. https://vectorflow.example.com) | ||||||||||||||||||||||||||||||||||||||||||||
| --token <token> One-time enrollment token from the VectorFlow UI | ||||||||||||||||||||||||||||||||||||||||||||
| --version <tag> Release version to install (default: latest) | ||||||||||||||||||||||||||||||||||||||||||||
| --channel <name> 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 | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
68
to
82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unknown The
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: agent/install.sh
Line: 68-82
Comment:
**Unknown `--channel` values silently fall through to stable**
The `--channel` flag only rejects the combination of `dev` + `--version`, but does not validate that the channel value itself is one of the supported options (`stable` | `dev`). Passing `--channel foo` silently treats the install as a stable-channel install, which can confuse operators who mistype the channel name.
```suggestion
if [ "${CHANNEL}" = "dev" ] && [ "${VERSION}" != "latest" ]; then
fatal "--channel dev and --version are mutually exclusive"
fi
if [ "${CHANNEL}" != "stable" ] && [ "${CHANNEL}" != "dev" ]; then
fatal "Unknown channel '${CHANNEL}'. Valid values are: stable, dev"
fi
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||
| # ───────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | |
| <span className="font-mono text-sm text-muted-foreground"> | ||
| {node.agentVersion ?? "—"} | ||
| </span> | ||
| {latestAgentVersion && | ||
| {getNodeLatest(node).version && | ||
| node.agentVersion && | ||
| isVersionOlder(node.agentVersion, latestAgentVersion) && ( | ||
| isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") && ( | ||
| <Badge variant="outline" className="text-amber-600"> | ||
| Update available | ||
| </Badge> | ||
|
|
@@ -165,9 +174,9 @@ export default function FleetPage() { | |
| Update pending... | ||
| </Badge> | ||
| ) : node.deploymentMode === "DOCKER" ? ( | ||
| latestAgentVersion && | ||
| getNodeLatest(node).version && | ||
| node.agentVersion && | ||
| isVersionOlder(node.agentVersion, latestAgentVersion) ? ( | ||
| isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <span> | ||
|
|
@@ -179,20 +188,21 @@ export default function FleetPage() { | |
| <TooltipContent>Update via Docker image pull</TooltipContent> | ||
| </Tooltip> | ||
| ) : null | ||
| ) : latestAgentVersion && | ||
| ) : getNodeLatest(node).version && | ||
| node.agentVersion && | ||
| isVersionOlder(node.agentVersion, latestAgentVersion) ? ( | ||
| isVersionOlder(node.agentVersion, getNodeLatest(node).version ?? "") ? ( | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| disabled={triggerUpdate.isPending} | ||
| onClick={(e) => { | ||
| e.preventDefault(); | ||
| const latest = getNodeLatest(node); | ||
| triggerUpdate.mutate({ | ||
| nodeId: node.id, | ||
| targetVersion: latestAgentVersion, | ||
| downloadUrl: `https://github.com/${AGENT_REPO}/releases/download/v${latestAgentVersion}/vf-agent-linux-amd64`, | ||
| checksum: `sha256:${agentChecksums["vf-agent-linux-amd64"] ?? ""}`, | ||
| targetVersion: latest.version!, | ||
| downloadUrl: `https://github.com/${AGENT_REPO}/releases/download/${latest.tag}/vf-agent-linux-amd64`, | ||
| checksum: `sha256:${latest.checksums["vf-agent-linux-amd64"] ?? ""}`, | ||
|
Comment on lines
+204
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded The download URL and checksum key both hardcode The
Then use the per-node architecture in the update command: const arch = node.arch ?? "amd64"; // e.g. "amd64" | "arm64"
triggerUpdate.mutate({
nodeId: node.id,
targetVersion: latest.version!,
downloadUrl: `https://github.com/${AGENT_REPO}/releases/download/${latest.tag}/vf-agent-linux-${arch}`,
checksum: `sha256:${latest.checksums[`vf-agent-linux-${arch}`] ?? ""}`,
});Prompt To Fix With AIThis is a comment left during a code review.
Path: src/app/(dashboard)/fleet/page.tsx
Line: 204-205
Comment:
Hardcoded `amd64` architecture breaks arm64 agent updates
The download URL and checksum key both hardcode `vf-agent-linux-amd64`, but the CI workflow explicitly builds and publishes both `vf-agent-linux-amd64` and `vf-agent-linux-arm64` for the dev channel (`.github/workflows/ci.yml` lines 169–170, 188–189). An arm64 agent (e.g., running on AWS Graviton or Raspberry Pi) that is offered an update will download and attempt to execute the `amd64` binary, causing the agent to crash or fail to restart.
The `VectorNode` model includes an `os` field but no architecture information. To fix this, either:
1. Add an `arch` field to `VectorNode` (persisted from agent heartbeat metadata)
2. Infer the architecture from `agentVersion` or `metadata` if available
3. Default to `amd64` with an override mechanism for known arm64 deployments
Then use the per-node architecture in the update command:
```typescript
const arch = node.arch ?? "amd64"; // e.g. "amd64" | "arm64"
triggerUpdate.mutate({
nodeId: node.id,
targetVersion: latest.version!,
downloadUrl: `https://github.com/${AGENT_REPO}/releases/download/${latest.tag}/vf-agent-linux-${arch}`,
checksum: `sha256:${latest.checksums[`vf-agent-linux-${arch}`] ?? ""}`,
});
```
How can I resolve this? If you propose a fix, please make it concise. |
||
| }); | ||
| }} | ||
| > | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dev release race condition on concurrent pushes
The
agent-dev-binariesjob has noconcurrencygroup. If two commits land onmainin quick succession, two workflow runs will overlap. The delete → create sequence is not atomic, so the following interleaving is possible:gh release delete dev← removes devgh release create dev← publishes commit-A binariesgh release delete dev← removes commit-A binariesgh release create dev← publishes commit-B binaries ✓ (correct final state)…or, if scheduling flips steps 2 and 3:
gh release delete devgh release delete dev(already gone — silently ignored with|| true)gh release create dev← publishes commit-B binariesgh release create dev← FAILS with "release already exists"In scenario 4,
gh release createexits non-zero and the step fails, causing the newer commit's release to persist — but subsequent CI runs will surface a noisy failure that needs to be retried manually.Add a job-level
concurrencykey to cancel in-progress runs:With
cancel-in-progress: true, only the latest push's job ever reaches the delete/create steps, making the operation effectively atomic.Prompt To Fix With AI