From ec64d7c2ff0ca7f37e5c82f9c39d6b0b2dc471d3 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 20 May 2026 16:21:11 -0400 Subject: [PATCH] chore: add mise git:worksync (gws) task to reconcile worktrees with main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://linear.app/speakeasy/issue/AGE-2441/reconcile-existing-worktrees-with-main-port-mappings-and-db-migrations git:workinit (gwi) is one-shot, so existing worktrees drift as main evolves: new _PORT dependents added to mise.toml never propagate into mise.local.toml (silent breakage — e.g. dashboard OAuth redirects to the default port instead of the remapped one), and new DB migrations aren't applied locally. This adds git:worksync (alias gws), safe to run repeatedly: - Extends zero:remap-ports with --preserve mode: keeps already-assigned _PORT values, randomizes any newly-introduced ports, emits dependent declarations only when absent from mise.local.toml (preserves manual edits). Dedupe via insertion-order Map ensures dependents land after the latest port they reference. - Applies pending Postgres + ClickHouse migrations (--no-migrate to skip). - ./zero detects worktrees and invokes gws --no-migrate so users who re-run the bootstrap script also get port reconcile (migrations stay in ./zero's existing flow). --- .mise-tasks/git/worksync.sh | 56 ++++++++++++++++++++++++++++++++ .mise-tasks/zero/remap-ports.mts | 55 ++++++++++++++++++++++++++----- zero | 13 ++++++++ 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100755 .mise-tasks/git/worksync.sh diff --git a/.mise-tasks/git/worksync.sh b/.mise-tasks/git/worksync.sh new file mode 100755 index 0000000000..f7716f7e66 --- /dev/null +++ b/.mise-tasks/git/worksync.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +#MISE dir="{{ config_root }}" +#MISE alias="gws" +#MISE description="Sync an existing worktree with main: re-runs port remapping (preserving assigned ports, adding new dependents) and applies pending database migrations. Safe to run repeatedly." + +#USAGE flag "--no-migrate" help="Skip applying database migrations." + +set -e + +main_worktree=$(cd "$(git rev-parse --git-common-dir)/.." && pwd) +current_worktree=$(git rev-parse --show-toplevel) + +if [ -z "$main_worktree" ] || [ "$main_worktree" = "$current_worktree" ]; then + echo "Error: this task must be run from a git worktree, not the main working tree." + exit 1 +fi + +if [ ! -f "mise.local.toml" ]; then + echo "Error: mise.local.toml not found. Initialize this worktree first with 'mise gwi'." + exit 1 +fi + +echo "⏳ Syncing port mappings..." +added=0 +remap=$(mise run zero:remap-ports --preserve --format flat --file -) +for line in $remap; do + if [ -z "$line" ]; then continue; fi + key="${line%%=*}" + mise set --file mise.local.toml "$line" + echo " + ${key}" + added=$((added + 1)) +done + +if [ "$added" -eq 0 ]; then + echo "✅ Port mappings already in sync." +else + echo "✅ Added ${added} env var declaration(s) to mise.local.toml." +fi + +if [ "${usage_no_migrate:-false}" = "true" ]; then + echo + echo "ℹ️ Skipping database migrations (--no-migrate)." + exit 0 +fi + +echo +echo "⏳ Applying Postgres migrations..." +mise run db:migrate + +echo +echo "⏳ Applying ClickHouse migrations..." +mise run clickhouse:migrate + +echo +echo "✅ Worktree synced." diff --git a/.mise-tasks/zero/remap-ports.mts b/.mise-tasks/zero/remap-ports.mts index 60cccc7e46..00a168c0f0 100755 --- a/.mise-tasks/zero/remap-ports.mts +++ b/.mise-tasks/zero/remap-ports.mts @@ -6,6 +6,7 @@ //USAGE flag "--format " default="mise" { choices "mise" "flat" } //USAGE flag "--file " default="mise.worktree.local.toml" help="The file to write the environment variables to. If set to '-', the output will be written to stdout." +//USAGE flag "--preserve" help="Preserve existing port assignments and dependent declarations already present in mise.local.toml. Only emit newly-introduced ports (randomized) and newly-introduced dependent declarations." /** * This script is responsible for finding available ports for any environment @@ -15,6 +16,13 @@ * environment variables that depend on the `_PORT` variables will also need to * be picked up and redeclared since env var declarations are sensitive to * config loading precedence and order dependent within each config file. + * + * When `--preserve` is set the script reads `mise.local.toml` and skips any + * `_PORT` or dependent declaration that already has a value there. This is + * what `mise git:worksync` (alias `gws`) uses to bring an existing worktree + * up to date with new ports / dependents added on `main` without + * re-randomizing ports that are already assigned and without clobbering + * manual edits the user may have made to dependent values. */ import { readFileSync, writeFileSync } from "node:fs"; @@ -26,23 +34,52 @@ async function main() { env: Record; }; + const preserve = process.env["usage_preserve"] === "true"; + + let existing: Record = {}; + if (preserve) { + try { + const localConfig = parseTOML( + await readFileSync("mise.local.toml", "utf-8"), + ) as { env?: Record }; + existing = localConfig.env ?? {}; + } catch { + // mise.local.toml is missing — treat as empty and emit everything. + } + } + const portEnvVars = Object.keys(config.env).filter((key) => key.endsWith("_PORT"), ); - const finalVars: [string, string][] = []; + const emitted = new Map(); + const emit = (key: string, value: string) => { + // delete-then-set moves the key to the end of insertion order, matching + // the unset+set semantics of `mise set` so dependents end up after the + // latest port they reference. + emitted.delete(key); + emitted.set(key, value); + }; for (const portEnvVar of portEnvVars) { - const port = await getPort({ - name: portEnvVar, - random: true, - }); - finalVars.push( - [portEnvVar, `${port}`], - ...findDependentEnvVars(config.env, portEnvVar), - ); + if (preserve && portEnvVar in existing) { + // Port is already assigned in mise.local.toml — keep it. + } else { + const port = await getPort({ + name: portEnvVar, + random: true, + }); + emit(portEnvVar, `${port}`); + } + + for (const [key, value] of findDependentEnvVars(config.env, portEnvVar)) { + if (preserve && key in existing) continue; + emit(key, value); + } } + const finalVars = Array.from(emitted.entries()); + const format = process.env["usage_format"] ?? "mise"; let out = ""; switch (format) { diff --git a/zero b/zero index 07675954a7..6a8984ec75 100755 --- a/zero +++ b/zero @@ -50,6 +50,19 @@ if [ -f "mise.worktree.local.toml" ]; then export MISE_ENV=worktree fi +# Detect git worktree (a working tree distinct from the main one). When in a +# worktree, sync port mappings against mise.toml so newly-added _PORT vars +# and dependents land in mise.local.toml. Migrations are skipped here because +# ./zero applies them later. +if command -v git &> /dev/null && git rev-parse --is-inside-work-tree &> /dev/null; then + git_main_worktree=$(cd "$(git rev-parse --git-common-dir)/.." && pwd 2>/dev/null || true) + git_current_worktree=$(git rev-parse --show-toplevel 2>/dev/null || true) + if [ -n "$git_main_worktree" ] && [ -n "$git_current_worktree" ] && [ "$git_main_worktree" != "$git_current_worktree" ]; then + echo -e "\n⏳ Syncing worktree port mappings" + mise run git:worksync --no-migrate + fi +fi + check_command() { local cmd=$1 local instructions=$2