diff --git a/flake.nix b/flake.nix index 03460e4..623772a 100644 --- a/flake.nix +++ b/flake.nix @@ -118,6 +118,11 @@ touch $out ''; + fish-deploy-to = pkgs.runCommand "fish-deploy-to-check" { nativeBuildInputs = [ pkgs.fish pkgs.git ]; } '' + bash ${./tests/deploy-to.bash} ${./modules/fish/functions/deploy-to.fish} + touch $out + ''; + copyparty-reverse-proxy-trust-config = let pikachuConfig = self.nixosConfigurations.pikachu.config; diff --git a/modules/fish/fish.nix b/modules/fish/fish.nix index ae11c1b..0aec84a 100644 --- a/modules/fish/fish.nix +++ b/modules/fish/fish.nix @@ -120,6 +120,11 @@ in description = "Copy a file, or recursively copy a source directory into a destination"; body = builtins.readFile ./functions/copy.fish; }; + + deploy-to = { + description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild"; + body = builtins.readFile ./functions/deploy-to.fish; + }; }; # Rendered by home-manager into ~/.config/fish/config.fish. diff --git a/modules/fish/functions/deploy-to.fish b/modules/fish/functions/deploy-to.fish new file mode 100644 index 0000000..0c8f6e0 --- /dev/null +++ b/modules/fish/functions/deploy-to.fish @@ -0,0 +1,53 @@ +if test (count $argv) -lt 1; or test (count $argv) -gt 2 + echo "usage: deploy-to [branch]" >&2 + return 2 +end + +set host $argv[1] +set branch $argv[2] + +if test -z "$branch" + set branch (git branch --show-current) +end + +if test -z "$branch" + echo "deploy-to: could not determine the current branch" >&2 + return 2 +end + +git rev-parse --show-toplevel >/dev/null 2>&1 +or begin + echo "deploy-to: run this from a git checkout" >&2 + return 2 +end + +set dirty_lines (git status --porcelain) +if test (count $dirty_lines) -gt 0 + echo "deploy-to: refusing to deploy with a dirty local worktree" >&2 + return 1 +end + +git fetch origin +or return $status + +set remote_ref refs/remotes/origin/$branch +git rev-parse --verify --quiet $remote_ref >/dev/null +or begin + echo "deploy-to: origin/$branch does not exist" >&2 + return 1 +end + +set local_head (git rev-parse HEAD) +set remote_head (git rev-parse $remote_ref) + +if test "$local_head" != "$remote_head" + echo "deploy-to: local HEAD differs from origin/$branch" >&2 + echo "deploy-to: push or reset the branch before deploying" >&2 + return 1 +end + +set escaped_branch (string escape -- $branch) +set escaped_host (string escape -- $host) +set remote_command "cd ~/wrk/dotfiles && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs" + +ssh $host $remote_command diff --git a/tests/deploy-to.bash b/tests/deploy-to.bash new file mode 100644 index 0000000..c5a6ba9 --- /dev/null +++ b/tests/deploy-to.bash @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +function_path=$1 +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +export HOME="$workdir/home" +mkdir -p "$HOME" + +git config --global user.email test@example.invalid +git config --global user.name 'Deploy Test' + +origin="$workdir/origin.git" +repo="$workdir/repo" +ssh_log="$workdir/ssh.log" +mock_bin="$workdir/bin" + +git init --bare "$origin" >/dev/null +git clone "$origin" "$repo" >/dev/null 2>&1 + +cd "$repo" +git switch -c impl/test >/dev/null +echo test >README.md +git add README.md +git commit -m 'test fixture' >/dev/null +git push -u origin impl/test >/dev/null 2>&1 + +mkdir -p "$mock_bin" +bash_path=${BASH_PATH:-$(command -v bash)} +cat >"$mock_bin/ssh" <"\$SSH_LOG" +printf '%s\n' "\$2" >>"\$SSH_LOG" +SSH +chmod +x "$mock_bin/ssh" + +export SSH_LOG="$ssh_log" +export PATH="$mock_bin:$PATH" + +fish_definition="function deploy-to; source '$function_path' \$argv; end" +default_command="$fish_definition; deploy-to pikachu" +explicit_command="$fish_definition; deploy-to pikachu impl/test" + +fish -c "$default_command" + +expected_command='cd ~/wrk/dotfiles && git fetch origin && git switch -- impl/test && git reset --hard origin/impl/test && sudo nixos-rebuild switch --flake .#pikachu --print-build-logs' + +mapfile -t ssh_call <"$ssh_log" + +test "${ssh_call[0]}" = pikachu +test "${ssh_call[1]}" = "$expected_command" + +rm "$ssh_log" +fish -c "$explicit_command" +mapfile -t ssh_call <"$ssh_log" + +test "${ssh_call[0]}" = pikachu +test "${ssh_call[1]}" = "$expected_command" + +rm "$ssh_log" +echo dirty >>README.md + +if fish -c "$default_command" 2>"$workdir/dirty.err" +then + echo 'deploy-to unexpectedly allowed a dirty worktree' >&2 + exit 1 +fi + +grep -F 'dirty local worktree' "$workdir/dirty.err" >/dev/null + +git checkout -- README.md + +git commit --allow-empty -m 'unpushed fixture' >/dev/null + +if fish -c "$default_command" 2>"$workdir/unpushed.err" +then + echo 'deploy-to unexpectedly allowed a branch behind origin' >&2 + exit 1 +fi + +grep -F 'local HEAD differs from origin/impl/test' "$workdir/unpushed.err" >/dev/null