Clone
deploy.service.ex
defmodule Mix.Tasks.Deploy.Service do
  use Mix.Task

  @shortdoc "Update systemd service on server"
  @moduledoc """
  Update systemd service configuration for the deployed Phoenix application.

  ## Usage

      mix deploy.service                # Update service for production
      mix deploy.service staging        # Update service for staging

  This task will:
  1. Upload the service file from deploy/
  2. Replace template variables with actual values
  3. Reload systemd daemon
  4. Restart the service
  """

  require Logger

  @impl Mix.Task
  def run(args) do
    # Load deployment config
    Mix.Task.run("loadconfig", ["config/deploy.exs"])
    
    env = case args do
      [] -> :production
      [env] -> String.to_atom(env)
      _ -> 
        Logger.error("Too many arguments. Usage: mix deploy.service [environment]")
        exit(1)
    end
    
    config = get_deploy_config(env)
    
    Logger.info("Updating systemd service for #{env} environment...")
    
    with :ok <- check_requirements(config),
         :ok <- upload_service_file(config),
         :ok <- reload_and_restart_service(config) do
      Logger.info("✅ Systemd service updated successfully!")
    else
      {:error, reason} ->
        Logger.error("❌ Failed to update systemd service: #{reason}")
        exit(1)
    end
  end

  defp get_deploy_config(env), do: Deploy.SSH.get_deploy_config(env)

  defp check_requirements(config) do
    required_keys = [:user, :domain, :deploy_to, :app_port, :app_name]
    missing = Enum.filter(required_keys, &(not Map.has_key?(config, &1)))
    
    if Enum.empty?(missing) do
      :ok
    else
      {:error, "Missing required config keys: #{inspect(missing)}"}
    end
  end

  defp upload_service_file(config) do
    # Look for service file
    service_files = Path.wildcard("deploy/*.service")

    service_file = case service_files do
      [] -> nil
      [file] -> file
      files ->
        # Prefer one that matches app name
        Enum.find(files, hd(files), &String.contains?(&1, config.app_name))
    end

    if service_file do
      # Service name is {app_name}-{env} (e.g., testapp-staging, testapp-production)
      service_name = "#{config.app_name}-#{config.env}"

      Logger.info("Uploading service file from #{service_file} as #{service_name}...")

      # Read template
      template = File.read!(service_file)

      # Extract host from URL
      host = case URI.parse(Map.get(config, :url, "")) do
        %{host: nil} -> "localhost"
        %{host: h} -> h
      end

      # Replace variables
      content = template
      |> String.replace("${DEPLOY_TO}", Map.get(config, :deploy_to, "/var/www/#{config.app_name}"))
      |> String.replace("${APP_PORT}", to_string(config.app_port))
      |> String.replace("${PHX_HOST}", host)

      # Ensure PHX_SERVER=true is set
      content = if not String.contains?(content, "PHX_SERVER") do
        String.replace(content, "Environment=\"PORT=", "Environment=\"PHX_SERVER=true\"\nEnvironment=\"PORT=")
      else
        content
      end

      # Create temp file
      temp_file = Path.join(System.tmp_dir!(), "#{service_name}.service")
      File.write!(temp_file, content)

      # Upload to server
      remote_temp = "/tmp/#{service_name}.service"

      case scp_upload(config, temp_file, remote_temp) do
        {_, 0} ->
          # Install service
          install_cmd = """
          sudo cp #{remote_temp} /etc/systemd/system/#{service_name}.service && \
          sudo systemctl daemon-reload
          """

          case ssh_exec(config, install_cmd) do
            {_, 0} ->
              File.rm(temp_file)
              :ok
            {output, _} ->
              File.rm(temp_file)
              {:error, "Failed to install service: #{output}"}
          end
        {output, _} ->
          File.rm(temp_file)
          {:error, "Failed to upload service file: #{output}"}
      end
    else
      {:error, "No service file found in deploy/"}
    end
  end

  defp reload_and_restart_service(config) do
    # Service name is {app_name}-{env} (e.g., testapp-staging, testapp-production)
    service_name = "#{config.app_name}-#{config.env}"

    Logger.info("Restarting service: #{service_name}")

    restart_cmd = """
    sudo systemctl daemon-reload && \
    sudo systemctl restart #{service_name} && \
    sudo systemctl status #{service_name} | head -n 10
    """

    case ssh_exec(config, restart_cmd) do
      {output, 0} ->
        Logger.info(output)
        :ok
      {output, _} ->
        {:error, "Failed to restart service: #{output}"}
    end
  end

  defp ssh_exec(config, command), do: Deploy.SSH.ssh_exec(config, command)

  defp scp_upload(config, local_file, remote_file), do: Deploy.SSH.scp_upload(config, local_file, remote_file)
end