From 6e6788bea4e3467a326d69c82fcf3ece31492402 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Sun, 17 May 2026 23:22:53 -0400 Subject: [PATCH] make deploy remote resolution forgejo-aware --- .beads/issues.jsonl | 1 + ...5-17-forgejo-deploy-remote-resolution.html | 126 +++++++++++++++ scripts/deploy.ts | 152 +++++++++++++++--- 3 files changed, 258 insertions(+), 21 deletions(-) create mode 100644 docs/turns/2026-05-17-forgejo-deploy-remote-resolution.html diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index a7b04c0..e025c4d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -13,6 +13,7 @@ {"_type":"issue","id":"islandflow-ayo","title":"Drop stale backlog events from live fanout","description":"Follow-up to live freshness rollout: /ws/live was still fanning out stale backlog events for freshness-gated channels, which kept tape panes in Live feed behind despite active synthetic ingest. Gate fanout and cache ingest by freshness for options/nbbo/equities/flow.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:26:39Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:26:44Z","started_at":"2026-04-28T21:26:44Z","closed_at":"2026-04-28T21:26:44Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-0v6","title":"Fix tape freshness, NBBO coverage, pause controls, and filter popup","description":"Implement the tape fixes requested for synthetic options notional sizing, strict live freshness, live-mode pause/resume behavior, stronger NBBO snapshot coverage, and moving flow filters behind a popup. Includes server-side live cache changes, web terminal state/UI changes, and tests for synthetic pricing, live snapshot freshness/NBBO retention, and live pause/filter interactions.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T21:02:52Z","created_by":"dirtydishes","updated_at":"2026-04-28T21:13:38Z","started_at":"2026-04-28T21:02:57Z","closed_at":"2026-04-28T21:13:38Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-e4r","title":"Implement smart-money flow filtering and synthetic firehose modes","description":"Implement the approved multi-surface plan for named synthetic market profiles, options raw-vs-signal filtering, live/API filter contracts, Tape page client-side flow filters, firehose-readiness improvements, tests, and README updates.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-04-28T20:10:49Z","created_by":"dirtydishes","updated_at":"2026-04-28T20:29:29Z","started_at":"2026-04-28T20:10:53Z","closed_at":"2026-04-28T20:29:29Z","close_reason":"Implemented synthetic market profiles, options signal-path filtering, signal-aware API/replay contracts, Tape page filters, tests, and README updates. Follow-up tracked in islandflow-biq.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"islandflow-1ei","title":"Make deploy helper remote-aware for Forgejo","description":"Why: scripts/deploy.ts hardcodes git remote name origin for fetch/pull/push and branch verification, but this repository now uses forgejo/github remotes and may not have an origin remote. What: update deploy.ts to resolve the deploy git remote robustly (Forgejo-aware), use it across local prechecks, branch publish, and remote rollout git operations, and keep behavior explicit in output.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T03:20:12Z","created_by":"dirtydishes","updated_at":"2026-05-18T03:22:39Z","started_at":"2026-05-18T03:20:16Z","closed_at":"2026-05-18T03:22:39Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xod","title":"Add --fast mode to deploy helper","description":"Why: full main deploys rebuild all images and run full verification, which is slow for routine rollouts. What: add a --fast flag to scripts/deploy.ts with explicit behavior that short-circuits slow steps while preserving basic safety checks; update help text/docs for discoverability.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T02:50:47Z","created_by":"dirtydishes","updated_at":"2026-05-18T02:53:41Z","started_at":"2026-05-18T02:50:50Z","closed_at":"2026-05-18T02:53:41Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-cif","title":"hydrate alert evidence context from clickhouse","description":"Implement alert detail hydration from ClickHouse with a new context endpoint and frontend drawer evidence resolution. Includes storage lookup by alert trace_id/evidence refs, unresolved refs diagnostics, API route GET /flow/alerts/:trace_id/context, terminal evidence hydration + loading states/copy updates, and tests across storage/api/web.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-18T00:15:55Z","created_by":"dirtydishes","updated_at":"2026-05-18T00:17:38Z","started_at":"2026-05-18T00:16:00Z","closed_at":"2026-05-18T00:17:38Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-4e9","title":"Polish terminal view","description":"Improve the Islandflow web terminal view with a focused UI polish pass aligned to the product design system.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-17T15:18:18Z","created_by":"dirtydishes","updated_at":"2026-05-17T15:25:02Z","started_at":"2026-05-17T15:18:21Z","closed_at":"2026-05-17T15:25:02Z","close_reason":"Polished terminal shell styling, responsive Tape actions, and documented the turn.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/docs/turns/2026-05-17-forgejo-deploy-remote-resolution.html b/docs/turns/2026-05-17-forgejo-deploy-remote-resolution.html new file mode 100644 index 0000000..f0b14aa --- /dev/null +++ b/docs/turns/2026-05-17-forgejo-deploy-remote-resolution.html @@ -0,0 +1,126 @@ + + + + + + Turn Report: Forgejo-Aware Deploy Remote Resolution + + + +
+
+

Deploy helper now resolves Forgejo/GitHub remotes without hardcoded origin

+

Date: 2026-05-17 · Issue: islandflow-1ei · Files changed: scripts/deploy.ts

+ +

Summary

+

+ Updated scripts/deploy.ts so deploy operations no longer assume a git remote named origin. The deploy helper now auto-resolves an available remote (Forgejo-aware), uses it consistently across fetch/pull/push and remote checkout updates, and supports explicit override with DEPLOY_GIT_REMOTE. +

+ +

Changes Made

+
    +
  • Added DEPLOY_GIT_REMOTE environment override to force a specific remote when needed.
  • +
  • Added local helper functions to discover remotes, inspect branch upstream metadata, and resolve deploy remote candidates.
  • +
  • Changed local prechecks from hardcoded git fetch origin / origin/main to resolved remote values.
  • +
  • Changed branch publish from hardcoded pushes to remote-aware push commands.
  • +
  • Changed remote VPS git update steps from hardcoded origin fetch/pull/track to remote-aware commands.
  • +
  • Updated deploy CLI help/environment text and rollout log output to show selected git remote.
  • +
+ +

Context

+

+ The repository now includes forgejo and github remotes and may not define origin at all. Hardcoding origin caused deploy fragility in both local precheck and remote rollout flows. +

+ +

Important Implementation Details

+

+ Remote resolution prioritizes explicit operator intent and branch metadata, then falls back to a stable preference order and discovered remotes. +

+
candidates = [
+  DEPLOY_GIT_REMOTE,
+  branch.<name>.remote,
+  upstream remote,
+  branch.main.remote,
+  forgejo, origin, github,
+  all discovered remotes
+]
+

+ The selected remote is then threaded through all deploy git operations to avoid local/remote mismatch from hardcoded remote names. +

+ +

Expected Impact for End-Users

+

+ Operators should no longer see deploy failures caused solely by missing origin. Deploy commands should work in mixed Forgejo/GitHub environments with fewer manual fixes and less confusion. +

+ +

Validation

+
    +
  • Ran bun run scripts/deploy.ts --help to verify updated usage and environment output.
  • +
  • Ran bun test (232 passing, 0 failing) after code changes.
  • +
  • Searched the updated file to verify key origin hardcodes were removed from deploy flow paths.
  • +
+ +

Issues, Limitations, and Mitigations

+
    +
  • If local and VPS remote naming differ unexpectedly, deploy can still fail during remote git update.
  • +
  • Mitigation: DEPLOY_GIT_REMOTE allows explicit remote selection per run.
  • +
  • The current change does not rewrite deployment README examples; they may still mention origin in historical/manual sections.
  • +
+ +

Follow-up Work

+
    +
  • Optional: update deployment docs to describe dynamic remote resolution and DEPLOY_GIT_REMOTE usage examples.
  • +
  • No additional code follow-up required for the reported deploy.ts Forgejo mismatch.
  • +
+
+
+ + diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 70e54e1..68d260a 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -37,6 +37,7 @@ const PUBLIC_APP_URL = process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io"; const PUBLIC_API_HEALTH_URL = process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null; +const DEPLOY_GIT_REMOTE_OVERRIDE = process.env.DEPLOY_GIT_REMOTE?.trim() || null; const NATIVE_SYSTEMCTL_PREFIX = process.env.DEPLOY_NATIVE_SYSTEMCTL_PREFIX?.trim() || "sudo -n systemctl"; const NATIVE_UNITS = { @@ -75,7 +76,7 @@ function usage(exitCode = 1): never { ./deploy current branch [--runtime docker|native] [--web-only|--api-only|--services-only] [--fast] [--no-build] [--force-recreate] Modes: - main Deploy origin/main to the live server checkout. + main Deploy /main to the live server checkout. current-branch Push the current local branch, switch the server to it, and deploy it. Runtimes: @@ -96,6 +97,7 @@ Options: --help Show this help text. Environment: + DEPLOY_GIT_REMOTE Override git remote used for deploy fetch/pull/push (auto-detected by default). DEPLOY_PUBLIC_APP_URL Override the public app URL (default: https://flow.deltaisland.io). DEPLOY_PUBLIC_API_HEALTH_URL Optional separate public API health URL for two-origin deployments. DEPLOY_NATIVE_SYSTEMCTL_PREFIX Override systemctl invocation for native rollouts (default: sudo -n systemctl). @@ -155,6 +157,23 @@ function captureChecked( return result.stdout ?? ""; } +function tryCapture( + command: string, + args: string[], + options: SpawnSyncOptions = {} +): string | null { + const result = spawnSync(command, args, { + cwd: repoRoot, + encoding: "utf8", + stdio: ["inherit", "pipe", "pipe"], + ...options + }); + if (result.status !== 0) { + return null; + } + return result.stdout ?? ""; +} + function runRemoteScript( title: string, script: string, @@ -280,6 +299,83 @@ function shellPattern(value: string): string { return `'${value.replace(/'/g, `'"'"'`)}'`; } +function parseUpstreamRemote(upstreamRef: string | null): string | null { + if (!upstreamRef) { + return null; + } + const trimmed = upstreamRef.trim(); + if (!trimmed || !trimmed.includes("/")) { + return null; + } + return trimmed.split("/", 1)[0] ?? null; +} + +function localGitRemotes(): string[] { + const raw = tryCapture("git", ["remote"]); + if (!raw) { + return []; + } + return raw + .split("\n") + .map((value) => value.trim()) + .filter((value) => value.length > 0); +} + +function localHasRemote(name: string): boolean { + return spawnSync("git", ["remote", "get-url", name], { + cwd: repoRoot, + stdio: "ignore" + }).status === 0; +} + +function resolveDeployRemote(mode: DeployMode, branch: string | null): string { + const candidates: string[] = []; + + if (DEPLOY_GIT_REMOTE_OVERRIDE) { + candidates.push(DEPLOY_GIT_REMOTE_OVERRIDE); + } + + if (mode === "current-branch" && branch) { + const branchRemote = tryCapture("git", ["config", "--get", `branch.${branch}.remote`])?.trim(); + if (branchRemote) { + candidates.push(branchRemote); + } + + const upstreamRef = tryCapture("git", [ + "rev-parse", + "--abbrev-ref", + "--symbolic-full-name", + "@{u}" + ]); + const upstreamRemote = parseUpstreamRemote(upstreamRef); + if (upstreamRemote) { + candidates.push(upstreamRemote); + } + } + + const mainRemote = tryCapture("git", ["config", "--get", "branch.main.remote"])?.trim(); + if (mainRemote) { + candidates.push(mainRemote); + } + + candidates.push("forgejo", "origin", "github", ...localGitRemotes()); + + const deduped = Array.from(new Set(candidates.filter((value) => value.length > 0))); + const selected = deduped.find((name) => localHasRemote(name)); + + if (selected) { + return selected; + } + + console.error( + `Unable to resolve a deploy git remote. Checked candidates: ${deduped.join(", ")}` + ); + console.error( + "Set DEPLOY_GIT_REMOTE to a valid remote name or configure branch..remote." + ); + process.exit(1); +} + function describeRuntime(runtime: DeployRuntime): string { return runtime === "docker" ? "Docker Compose" : "experimental native systemd/Bun"; } @@ -404,12 +500,12 @@ function localRuntimePrecheck(runtime: DeployRuntime, noBuild: boolean): void { } } -function localMainPrecheck(runtime: DeployRuntime, noBuild: boolean): void { +function localMainPrecheck(remote: string, runtime: DeployRuntime, noBuild: boolean): void { section("Local Precheck"); - runChecked("git", ["fetch", "origin"]); + runChecked("git", ["fetch", remote]); runChecked("git", ["status", "--short", "--branch"]); runChecked("git", ["rev-parse", "--verify", "HEAD"]); - runChecked("git", ["rev-parse", "origin/main"]); + runChecked("git", ["rev-parse", `${remote}/main`]); localRuntimePrecheck(runtime, noBuild); } @@ -423,6 +519,7 @@ function currentBranchName(): string { } function localBranchPrecheck( + remote: string, branch: string, runtime: DeployRuntime, noBuild: boolean @@ -430,7 +527,7 @@ function localBranchPrecheck( section("Local Precheck"); runChecked("git", ["branch", "--show-current"]); runChecked("git", ["status", "--short", "--branch"]); - runChecked("git", ["fetch", "origin"]); + runChecked("git", ["fetch", remote]); const porcelain = captureChecked("git", ["status", "--porcelain=v1"]).trim(); if (porcelain) { @@ -443,7 +540,7 @@ function localBranchPrecheck( localRuntimePrecheck(runtime, noBuild); } -function publishCurrentBranch(branch: string): void { +function publishCurrentBranch(remote: string, branch: string): void { section("Local Publish"); const upstreamResult = spawnSync( "git", @@ -456,11 +553,11 @@ function publishCurrentBranch(branch: string): void { ); if (upstreamResult.status === 0) { - runChecked("git", ["push", "origin", branch]); + runChecked("git", ["push", remote, branch]); return; } - runChecked("git", ["push", "-u", "origin", branch]); + runChecked("git", ["push", "-u", remote, branch]); } function remoteGitPrecheck(): void { @@ -568,18 +665,20 @@ done ); } -function remoteGitUpdateScript(mode: DeployMode, branch: string | null): string { +function remoteGitUpdateScript(mode: DeployMode, remote: string, branch: string | null): string { const escapedBranch = branch ? shellEscape(branch) : null; + const escapedRemote = shellEscape(remote); const switchCommand = mode === "main" - ? `git switch main\ngit pull --ff-only origin main` - : `git switch ${escapedBranch} || git switch -c ${escapedBranch} --track origin/${escapedBranch}\ngit pull --ff-only origin ${escapedBranch}`; + ? `git switch main\ngit pull --ff-only ${escapedRemote} main` + : `git switch ${escapedBranch} || git switch -c ${escapedBranch} --track ${escapedRemote}/${escapedBranch}\ngit pull --ff-only ${escapedRemote} ${escapedBranch}`; - return `cd ${shellEscape(REMOTE_REPO)}\ngit fetch origin\n${switchCommand}`; + return `cd ${shellEscape(REMOTE_REPO)}\ngit remote get-url ${escapedRemote} >/dev/null\ngit fetch ${escapedRemote}\n${switchCommand}`; } function remoteDockerRollout( mode: DeployMode, + remote: string, branch: string | null, scope: DeployScope, forceRecreate: boolean, @@ -601,7 +700,7 @@ function remoteDockerRollout( `#!/usr/bin/env bash set -euo pipefail -${remoteGitUpdateScript(mode, branch)} +${remoteGitUpdateScript(mode, remote, branch)} cd ${shellEscape(REMOTE_DOCKER_DEPLOYMENT)} ${buildCommand ? `${buildCommand}\n` : ""}${upCommand} @@ -611,6 +710,7 @@ ${buildCommand ? `${buildCommand}\n` : ""}${upCommand} function remoteNativeRollout( mode: DeployMode, + remote: string, branch: string | null, scope: DeployScope, noBuild: boolean @@ -632,7 +732,7 @@ function remoteNativeRollout( `#!/usr/bin/env bash set -euo pipefail -${remoteGitUpdateScript(mode, branch)} +${remoteGitUpdateScript(mode, remote, branch)} cd ${shellEscape(REMOTE_REPO)} ${buildSteps.join("\n")} @@ -647,6 +747,7 @@ done function remoteRollout( mode: DeployMode, + remote: string, runtime: DeployRuntime, branch: string | null, scope: DeployScope, @@ -654,11 +755,11 @@ function remoteRollout( noBuild: boolean ): void { if (runtime === "docker") { - remoteDockerRollout(mode, branch, scope, forceRecreate, noBuild); + remoteDockerRollout(mode, remote, branch, scope, forceRecreate, noBuild); return; } - remoteNativeRollout(mode, branch, scope, noBuild); + remoteNativeRollout(mode, remote, branch, scope, noBuild); } function remoteDockerVerification(scope: DeployScope, fast: boolean): void { @@ -761,23 +862,27 @@ function publicVerification(scope: DeployScope, fast: boolean): void { function main(): void { const options = parseArgs(process.argv.slice(2)); const scope = effectiveScope(options.scope, options.fast); + const currentBranch = options.mode === "current-branch" ? currentBranchName() : null; + const deployRemote = resolveDeployRemote(options.mode, currentBranch); assertSshKeyExists(); printRuntimeAdvisory(options.runtime); console.log( - `Deploying ${options.mode === "main" ? "origin/main" : "the current local branch"} ` + + `Deploying ${options.mode === "main" ? `${deployRemote}/main` : "the current local branch"} ` + `via ${describeRuntime(options.runtime)} (${describeScope(scope)}${options.fast ? ", fast mode" : ""}).` ); + console.log(`[deploy] Using git remote: ${deployRemote}`); if (options.fast && options.scope === "full") { console.log("[deploy] Fast mode changed default full scope to --services-only."); } if (options.mode === "main") { - localMainPrecheck(options.runtime, options.noBuild); + localMainPrecheck(deployRemote, options.runtime, options.noBuild); remoteGitPrecheck(); remoteRuntimePrecheck(options.runtime, scope); remoteRollout( options.mode, + deployRemote, options.runtime, null, scope, @@ -785,13 +890,18 @@ function main(): void { options.noBuild ); } else { - const branch = currentBranchName(); - localBranchPrecheck(branch, options.runtime, options.noBuild); - publishCurrentBranch(branch); + const branch = currentBranch; + if (!branch) { + console.error("Unable to resolve current branch for current-branch deploy mode."); + process.exit(1); + } + localBranchPrecheck(deployRemote, branch, options.runtime, options.noBuild); + publishCurrentBranch(deployRemote, branch); remoteGitPrecheck(); remoteRuntimePrecheck(options.runtime, scope); remoteRollout( options.mode, + deployRemote, options.runtime, branch, scope,