From 1b89dc98e2a4713b85c094459851659959543c91 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Dec 2025 00:45:08 +0100 Subject: [PATCH 1/4] replace the usage of MIX_ENV... with env arg to avoid the need for compiling envs --- lib/mix/tasks/secret_mana.edit.ex | 93 ++++++++++++++- lib/mix/tasks/secret_mana.encrypt.ex | 112 ++++++++++++++++-- lib/mix/tasks/secret_mana.ex | 51 ++++++++ lib/mix/tasks/secret_mana.gen.key.ex | 97 ++++++++++++++- lib/mix/tasks/secret_mana.install.ex | 2 +- lib/secret_mana/age_backend.ex | 2 +- lib/secret_mana/config.ex | 12 +- .../test/secret_mana_integration_test.exs | 8 +- 8 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 lib/mix/tasks/secret_mana.ex diff --git a/lib/mix/tasks/secret_mana.edit.ex b/lib/mix/tasks/secret_mana.edit.ex index 8b305de..8b64094 100644 --- a/lib/mix/tasks/secret_mana.edit.ex +++ b/lib/mix/tasks/secret_mana.edit.ex @@ -1,11 +1,96 @@ defmodule Mix.Tasks.SecretMana.Edit do use Mix.Task + @shortdoc "Edits SecretMana configuration for the given environment" + + @moduledoc """ + Opens the SecretMana configuration for editing for a given environment. + + ## Usage + + mix secret_mana.edit + + Where `` is typically one of: + + * dev + * test + * prod + + The task will: + + * Ensure the `:secret_mana` application is started. + * Build a `SecretMana.Config` for the given environment. + * Invoke `SecretMana.edit/1` with the config. + + ## Examples + + mix secret_mana.edit dev + mix secret_mana.edit prod + + ## Getting help + + You can display this help with: + + mix help secret_mana.edit + + Or by using: + + mix secret_mana.edit --help + mix secret_mana.edit -h + """ + @impl Mix.Task - def run(_) do - Application.ensure_all_started(:secret_mana) + def run(args) do + case parse_args(args) do + {:ok, env} -> + Application.ensure_all_started(:secret_mana) + + SecretMana.Config.new(env) + |> SecretMana.edit() + + :help -> + print_manual() + + {:error, message} -> + Mix.shell().error(message) + Mix.shell().info("") + print_manual() + Mix.raise("secret_mana.edit: invalid arguments") + end + end + + defp parse_args(["-h"]), do: :help + defp parse_args(["--help"]), do: :help + + defp parse_args([env]) when is_binary(env) and env != "" do + {:ok, env} + end + + defp parse_args([]) do + {:error, "Missing required argument: "} + end + + defp parse_args(_args) do + {:error, "Too many arguments were provided."} + end + + defp print_manual do + Mix.shell().info(""" + SecretMana Editor + + Usage: + mix secret_mana.edit + + Arguments: + The environment whose configuration to edit (e.g. dev, test, prod) + + Examples: + mix secret_mana.edit dev + mix secret_mana.edit test + mix secret_mana.edit prod - SecretMana.Config.new() - |> SecretMana.edit() + For more information: + mix help secret_mana.edit + """) end end diff --git a/lib/mix/tasks/secret_mana.encrypt.ex b/lib/mix/tasks/secret_mana.encrypt.ex index c22b0ef..54c0840 100644 --- a/lib/mix/tasks/secret_mana.encrypt.ex +++ b/lib/mix/tasks/secret_mana.encrypt.ex @@ -1,21 +1,109 @@ defmodule Mix.Tasks.SecretMana.Encrypt do use Mix.Task + @shortdoc "Encrypts a file using SecretMana for the given environment" + + @moduledoc """ + Encrypts a file using SecretMana for the given environment. + + ## Usage + + mix secret_mana.encrypt + + Where: + + * `` is typically one of: + * dev + * test + * prod + + * `` is the path to the file you want to encrypt. + + The task will: + + * Ensure the `:secret_mana` application is started. + * Build a `SecretMana.Config` for the given environment. + * Invoke `SecretMana.encrypt/2` with the config and file path. + + ## Examples + + mix secret_mana.encrypt dev config/secrets.yml + mix secret_mana.encrypt prod priv/secrets.txt + + ## Getting help + + You can show this help via: + + mix help secret_mana.encrypt + + Or by using: + + mix secret_mana.encrypt --help + mix secret_mana.encrypt -h + """ + @impl Mix.Task def run(args) do - Application.ensure_all_started(:secret_mana) + case parse_args(args) do + {:ok, env, file} -> + Application.ensure_all_started(:secret_mana) - args - |> List.first() - |> then(fn file -> - if file do - SecretMana.Config.new() + SecretMana.Config.new(env) |> SecretMana.encrypt(file) - else - raise """ - Usage: mix secret_mana.encrypt - """ - end - end) + + :help -> + print_manual() + + {:error, message} -> + Mix.shell().error(message) + Mix.shell().info("") + print_manual() + Mix.raise("secret_mana.encrypt: invalid arguments") + end + end + + # Argument parsing + + defp parse_args(["-h"]), do: :help + defp parse_args(["--help"]), do: :help + + defp parse_args([env, file]) + when is_binary(env) and env != "" and is_binary(file) and file != "" do + {:ok, env, file} + end + + defp parse_args([_env]) do + {:error, "Missing required argument: "} + end + + defp parse_args([]) do + {:error, "Missing required arguments: "} + end + + defp parse_args(_args) do + {:error, "Too many arguments were provided."} + end + + # Manual / help text + + defp print_manual do + Mix.shell().info(""" + SecretMana Encrypt + + Usage: + mix secret_mana.encrypt + + Arguments: + The environment to use (e.g. dev, test, prod) + The path to the file to encrypt + + Examples: + mix secret_mana.encrypt dev config/secrets.yml + mix secret_mana.encrypt test priv/credentials.txt + mix secret_mana.encrypt prod priv/secrets.env + + For more information: + mix help secret_mana.encrypt + """) end end diff --git a/lib/mix/tasks/secret_mana.ex b/lib/mix/tasks/secret_mana.ex new file mode 100644 index 0000000..368f501 --- /dev/null +++ b/lib/mix/tasks/secret_mana.ex @@ -0,0 +1,51 @@ +defmodule Mix.Tasks.SecretMana do + use Mix.Task + + @shortdoc "Lists available SecretMana Mix tasks" + + @moduledoc """ + A small index task for the SecretMana Mix tasks. + + ## Usage + + mix secret_mana + + This will print a short overview of all available `secret_mana.*` tasks + and how to get more detailed help. + + For detailed information on a specific task, use: + + mix help secret_mana.install + mix help secret_mana.edit + mix help secret_mana.encrypt + mix help secret_mana.gen.key + """ + + @impl Mix.Task + def run(_args) do + Mix.shell().info(""" + SecretMana Mix tasks + ==================== + + Configuration / Setup + mix secret_mana.install + Installs SecretMana for the given environment. + + mix secret_mana.edit + Opens the SecretMana configuration for editing. + + mix secret_mana.gen.key + Generates a SecretMana key for the given environment. + + Encryption + mix secret_mana.encrypt + Encrypts a file using SecretMana for the given environment. + + Usage hints + • is typically one of: dev, test, prod + • Use --help or -h on any task for a short usage reminder: + mix secret_mana.install --help + mix secret_mana.encrypt -h + """) + end +end diff --git a/lib/mix/tasks/secret_mana.gen.key.ex b/lib/mix/tasks/secret_mana.gen.key.ex index 3159d21..b4b6976 100644 --- a/lib/mix/tasks/secret_mana.gen.key.ex +++ b/lib/mix/tasks/secret_mana.gen.key.ex @@ -1,11 +1,100 @@ defmodule Mix.Tasks.SecretMana.Gen.Key do use Mix.Task + @shortdoc "Generates a SecretMana key for the given environment" + + @moduledoc """ + Generates a SecretMana key for the given environment. + + ## Usage + + mix secret_mana.gen.key + + Where `` is typically one of: + + * dev + * test + * prod + + The task will: + + * Ensure the `:secret_mana` application is started. + * Build a `SecretMana.Config` for the given environment. + * Invoke `SecretMana.gen_key/1` with the config. + + ## Examples + + mix secret_mana.gen.key dev + mix secret_mana.gen.key prod + + ## Getting help + + You can show this help via: + + mix help secret_mana.gen.key + + Or by using: + + mix secret_mana.gen.key --help + mix secret_mana.gen.key -h + """ + @impl Mix.Task - def run(_opts) do - Application.ensure_all_started(:secret_mana) + def run(args) do + case parse_args(args) do + {:ok, env} -> + Application.ensure_all_started(:secret_mana) + + SecretMana.Config.new(env) + |> SecretMana.gen_key() + + :help -> + print_manual() + + {:error, message} -> + Mix.shell().error(message) + Mix.shell().info("") + print_manual() + Mix.raise("secret_mana.gen.key: invalid arguments") + end + end + + # Argument parsing + + defp parse_args(["-h"]), do: :help + defp parse_args(["--help"]), do: :help + + defp parse_args([env]) when is_binary(env) and env != "" do + {:ok, env} + end + + defp parse_args([]) do + {:error, "Missing required argument: "} + end + + defp parse_args(_args) do + {:error, "Too many arguments were provided."} + end + + # Manual / help text + + defp print_manual do + Mix.shell().info(""" + SecretMana Key Generator + + Usage: + mix secret_mana.gen.key + + Arguments: + The environment to generate a key for (e.g. dev, test, prod) + + Examples: + mix secret_mana.gen.key dev + mix secret_mana.gen.key test + mix secret_mana.gen.key prod - SecretMana.Config.new() - |> SecretMana.gen_key() + For more information: + mix help secret_mana.gen.key + """) end end diff --git a/lib/mix/tasks/secret_mana.install.ex b/lib/mix/tasks/secret_mana.install.ex index 80d0745..0183651 100644 --- a/lib/mix/tasks/secret_mana.install.ex +++ b/lib/mix/tasks/secret_mana.install.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.SecretMana.Install do use Mix.Task @impl Mix.Task - def run(_opts) do + def run(_args) do Application.ensure_all_started(:secret_mana) SecretMana.Config.new() diff --git a/lib/secret_mana/age_backend.ex b/lib/secret_mana/age_backend.ex index 5bb63b5..4049721 100644 --- a/lib/secret_mana/age_backend.ex +++ b/lib/secret_mana/age_backend.ex @@ -415,7 +415,7 @@ defmodule SecretMana.AgeBackend do Application.app_dir(otp_app, "config/secrets") else # In development mode, use project root + secrets/ - env = Mix.env() + env = base_config.env base_path = Path.expand(config.secret_base_path) Path.join(base_path, to_string(env)) end diff --git a/lib/secret_mana/config.ex b/lib/secret_mana/config.ex index 4ede8f0..0af7f30 100644 --- a/lib/secret_mana/config.ex +++ b/lib/secret_mana/config.ex @@ -1,15 +1,15 @@ defmodule SecretMana.Config do @public_config_keys [ backend: SecretMana.AgeBackend, - otp_app: nil, - environment: nil + otp_app: nil ] @private_config_keys [ - backend_config: nil + backend_config: nil, + env: nil ] defstruct Keyword.merge(@public_config_keys, @private_config_keys) - def new() do + def new(env \\ "dev") do all_config = Application.get_all_env(:secret_mana) config_keys = Keyword.keys(@public_config_keys) @@ -28,7 +28,9 @@ defmodule SecretMana.Config do end # Merge defaults with provided config - final_config = Keyword.merge(@public_config_keys, valid_config) + final_config = + Keyword.merge(@public_config_keys, valid_config) + |> Keyword.put(:env, env) struct(__MODULE__, final_config) |> put_backend_config diff --git a/test_app/test/secret_mana_integration_test.exs b/test_app/test/secret_mana_integration_test.exs index 8e1e56b..4d14699 100644 --- a/test_app/test/secret_mana_integration_test.exs +++ b/test_app/test/secret_mana_integration_test.exs @@ -123,12 +123,9 @@ defmodule TestApp.SecretManaIntegrationTest do # Helper functions defp setup_secrets_for_env(env) do - original_env = Mix.env() - Mix.env(env) - # Use the SecretMana API directly instead of Mix tasks %{backend_config: %{secret_base_path: secret_base_path}} = - config = SecretMana.Config.new() + config = SecretMana.Config.new(env) # Install age binary SecretMana.install(config) @@ -145,9 +142,6 @@ defmodule TestApp.SecretManaIntegrationTest do # Clean up temporary file File.rm!(secrets_file) - - # Restore original environment - Mix.env(original_env) end defp cleanup_secrets do From 5a6e0dbd897ed99c7b0a2258178466c8b3d6d2ba Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Dec 2025 00:49:46 +0100 Subject: [PATCH 2/4] use windows-22, no support for 25 --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 585ad1e..8cbd84d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: matrix: otp: ["27.1.2"] elixir: ["1.18.1"] - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest] + os: [ubuntu-latest, ubuntu-24.04-arm, windows-2022] # mac is not yet supported by setup-beam # os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest, macos-13 ] @@ -24,7 +24,7 @@ jobs: - name: Check formatting run: mix format --check-formatted # skip on windows because different line endings (LF vs CRLF) - if: matrix.os != 'windows-latest' + if: matrix.os != 'windows-2022' - run: mix test test-test_app: @@ -32,7 +32,7 @@ jobs: matrix: otp: ["27.1.2"] elixir: ["1.18.1"] - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest] + os: [ubuntu-latest, ubuntu-24.04-arm, windows-2022] # mac is not yet supported by setup-beam # os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest, macos-13 ] @@ -72,7 +72,7 @@ jobs: - name: Smoke Test app # skip this on windows, cant figure out to start and curl the server, it simply never responds - if: matrix.os != 'windows-latest' + if: matrix.os != 'windows-2022' run: | _build/prod/rel/test_app/bin/test_app start & curl --connect-timeout 5 --max-time 10 --retry 5 --retry-max-time 40 --retry-connrefused --fail --silent --show-error http://127.0.0.1:4000/api/healthcheck && exit 0 From dead5bb8d918d342296cb80f5a073355cb6a7c58 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Dec 2025 00:54:13 +0100 Subject: [PATCH 3/4] one workflow run is enough --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cbd84d..8952c3e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ # @format -on: [push, pull_request] +on: [push] jobs: test: From 7348e5a77c0068e4601c38783dfe84efdec9912e Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 16 Dec 2025 23:01:15 +0100 Subject: [PATCH 4/4] fix smoke test --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8952c3e..4355564 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,9 +60,7 @@ jobs: choco: age.portable - name: Generate secrets - run: mix secret_mana.gen.key && mix secret_mana.encrypt secrets.json - env: - MIX_ENV: prod + run: mix secret_mana.gen.key prod && mix secret_mana.encrypt prod secrets.json working-directory: ./test_app - run: mix release