From aa040378a062dbc95ca71761ba17825df47739d6 Mon Sep 17 00:00:00 2001 From: "Adam C. Stephens" Date: Thu, 27 Aug 2026 22:21:01 -0400 Subject: [PATCH] fix: put git and ssh on the release PATH Refuse to start when a program a sync shells out to is missing, so a misconfigured deployment says so instead of failing every run with :enoent. Refs: GS-30 Assisted-By: Claude Opus 5 --- lib/git_sync/application.ex | 2 ++ lib/git_sync/tools.ex | 18 ++++++++++++++++++ nix/package.nix | 22 ++++++++++------------ nix/test.nix | 7 +++++++ test/git_sync/tools_test.exs | 22 ++++++++++++++++++++++ 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 lib/git_sync/tools.ex create mode 100644 test/git_sync/tools_test.exs diff --git a/lib/git_sync/application.ex b/lib/git_sync/application.ex index 961b6c1..3158bb7 100644 --- a/lib/git_sync/application.ex +++ b/lib/git_sync/application.ex @@ -7,6 +7,8 @@ defmodule GitSync.Application do @impl true def start(_type, _args) do + GitSync.Tools.check!() + children = [ GitSyncWeb.Telemetry, GitSync.Vault, diff --git a/lib/git_sync/tools.ex b/lib/git_sync/tools.ex new file mode 100644 index 0000000..4864dd3 --- /dev/null +++ b/lib/git_sync/tools.ex @@ -0,0 +1,18 @@ +defmodule GitSync.Tools do + @moduledoc """ + The external programs a sync shells out to. + + A deployment that is missing one of these can do nothing useful, so the + application refuses to start rather than failing every run with an + unreadable `:enoent`. + """ + + @required ~w(git sh ssh ssh-add ssh-agent ssh-keygen ssh-keyscan) + + def check!(tools \\ @required) do + case Enum.reject(tools, &System.find_executable/1) do + [] -> :ok + missing -> raise "not on PATH: #{Enum.join(missing, ", ")}" + end + end +end diff --git a/nix/package.nix b/nix/package.nix index 13a93d5..4d026ec 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -4,7 +4,6 @@ beamPackages, esbuild, git, - makeWrapper, openssh, }: @@ -32,8 +31,6 @@ beamPackages.mixRelease rec { hash = "sha256-dU5V1UQ/ztU5LQVR4e2/fCupJihKVEpZJOezdqK+s2M="; }; - nativeBuildInputs = [ makeWrapper ]; - # exqlite otherwise tries to download a precompiled NIF into a cache under $HOME. FORCE_BUILD = "1"; @@ -45,15 +42,16 @@ beamPackages.mixRelease rec { mix assets.deploy --no-deps-check ''; - postFixup = '' - wrapProgram $out/bin/git_sync \ - --prefix PATH : ${ - lib.makeBinPath [ - bash - git - openssh - ] - } + # mixRelease owns postFixup, so the tools we shell out to reach the release + # through its own environment hook instead of a wrapper. + postInstall = '' + echo 'export PATH=${ + lib.makeBinPath [ + bash + git + openssh + ] + }:$PATH' >> $out/releases/${version}/env.sh ''; meta = { diff --git a/nix/test.nix b/nix/test.nix index 4defbe4..612751e 100644 --- a/nix/test.nix +++ b/nix/test.nix @@ -25,5 +25,12 @@ machine.wait_for_open_port(4000) machine.succeed("curl --fail --show-error http://127.0.0.1:4000/") machine.succeed("test -f /var/lib/git-sync/git_sync.db") + + path = machine.succeed( + "tr '\\0' '\\n' < /proc/$(systemctl show --property MainPID --value git-sync.service)/environ" + " | sed -n 's/^PATH=//p'" + ) + for tool in ["git", "ssh", "ssh-agent", "ssh-keygen", "ssh-keyscan"]: + machine.succeed(f"PATH={path.strip()} command -v {tool}") ''; } diff --git a/test/git_sync/tools_test.exs b/test/git_sync/tools_test.exs new file mode 100644 index 0000000..e15d952 --- /dev/null +++ b/test/git_sync/tools_test.exs @@ -0,0 +1,22 @@ +defmodule GitSync.ToolsTest do + use ExUnit.Case, async: true + + alias GitSync.Tools + + test "passes when every program is on PATH" do + assert Tools.check!(["sh", "git"]) == :ok + end + + test "names the programs that are missing" do + error = + assert_raise RuntimeError, fn -> + Tools.check!(["sh", "not-a-real-program", "also-not-real"]) + end + + message = Exception.message(error) + + assert message =~ "not-a-real-program" + assert message =~ "also-not-real" + refute message =~ "sh," + end +end -- 2.51.2