one day I will automate this part

c87387d · gmorell · 2026-05-15 02:07

8 files +181 -1

Files changed

modified .gitignore
+1 −0
@@ -50,5 +50,6 @@ npm-debug.log
50 50 # placeholder copy.
51 51 /app-secrets.yml
52 52 /secret.yml
53 +/smtp-secret.yml
53 54 *.secret.yml
54 55
added app-secrets.example.yml
+39 −0
@@ -0,0 +1,39 @@
1 +# Example app-level secret for the git_gud deployment.
2 +#
3 +# Postgres credentials are NOT in here — they're managed by StackGres
4 +# and pulled from the `postgres` secret in the namespace (see pg.yml).
5 +# This file carries the application's own secrets only.
6 +#
7 +# Generate the secret_key_base with:
8 +#
9 +# mix phx.gen.secret
10 +#
11 +# Or, if you don't have Mix on the host:
12 +#
13 +# openssl rand -base64 64 | tr -d '\n'
14 +#
15 +# Don't commit the populated copy — `app-secrets.yml` and `*.secret.yml`
16 +# are gitignored.
17 +
18 +apiVersion: v1
19 +kind: Secret
20 +metadata:
21 + name: elixir-args
22 + namespace: git-gud
23 +type: Opaque
24 +stringData:
25 + # `mix phx.gen.secret` output. 64+ bytes of base64 entropy.
26 + secret_key_base: "REPLACE_ME_with_64_bytes_of_base64_entropy_from_mix_phx_gen_secret"
27 +
28 + # Erlang distribution cookie — every replica must agree so the nodes
29 + # can authenticate each other before clustering. Any 32+ char random
30 + # string works:
31 + #
32 + # openssl rand -hex 32
33 + #
34 + erlang_cookie: "REPLACE_ME_with_a_long_random_string"
35 +
36 + # Bearer token gate on the zot push/delete webhook (POST /_webhooks/zot).
37 + # Leave empty to disable the gate entirely (only OK in trusted-network
38 + # deploys); zot's `Authorization: Bearer <value>` must match this.
39 + zot_webhook_secret: "REPLACE_ME_with_a_long_random_string"
modified config/runtime.exs
+46 −0
@@ -151,6 +151,52 @@ if config_env() == :prod do
151 151 ],
152 152 secret_key_base: secret_key_base
153 153
154 + # Mailer adapter selection:
155 + #
156 + # * SMTP_RELAY set → Swoosh.Adapters.SMTP (via gen_smtp)
157 + # * otherwise → Swoosh.Adapters.Logger
158 + #
159 + # The Logger adapter doesn't actually send mail — it logs the
160 + # rendered email at :info — but it gets us off Swoosh.Adapters.Local
161 + # (which is what the dev mailbox uses), so the magic-link flow at
162 + # least leaves a trail in the pod log.
163 + smtp_tri = fn var, default ->
164 + case System.get_env(var, default) do
165 + v when v in ~w(always if_available never) -> String.to_existing_atom(v)
166 + other -> raise "#{var} must be one of always|if_available|never, got: #{inspect(other)}"
167 + end
168 + end
169 +
170 + mailer_opts =
171 + if relay = System.get_env("SMTP_RELAY") do
172 + [
173 + adapter: Swoosh.Adapters.SMTP,
174 + relay: relay,
175 + port: String.to_integer(System.get_env("SMTP_PORT", "587")),
176 + username: System.get_env("SMTP_USERNAME"),
177 + password: System.get_env("SMTP_PASSWORD"),
178 + tls: smtp_tri.("SMTP_TLS", "if_available"),
179 + ssl: System.get_env("SMTP_SSL", "false") == "true",
180 + auth: smtp_tri.("SMTP_AUTH", "if_available"),
181 + tls_options: [
182 + verify: :verify_peer,
183 + cacerts: :public_key.cacerts_get(),
184 + server_name_indication: String.to_charlist(relay),
185 + depth: 99
186 + ],
187 + retries: 1,
188 + no_mx_lookups: false
189 + ]
190 + else
191 + [adapter: Swoosh.Adapters.Logger, level: :info]
192 + end
193 +
194 + config :git_gud, GitGud.Mailer, mailer_opts
195 +
196 + config :git_gud, GitGud.Mailer,
197 + from_name: System.get_env("MAIL_FROM_NAME", "GitGud"),
198 + from_address: System.get_env("MAIL_FROM_ADDRESS") || "noreply@#{host}"
199 +
154 200 # ## SSL Support
155 201 #
156 202 # To get SSL working, you will need to add the `https` key
modified lib/git_gud/accounts/user_notifier.ex
+8 −1
@@ -9,7 +9,7 @@ defmodule GitGud.Accounts.UserNotifier do
9 9 email =
10 10 new()
11 11 |> to(recipient)
12 |> from({"GitGud", "contact@example.com"})
12 + |> from(sender())
13 13 |> subject(subject)
14 14 |> text_body(body)
15 15
@@ -18,6 +18,13 @@ defmodule GitGud.Accounts.UserNotifier do
18 18 end
19 19 end
20 20
21 + defp sender do
22 + cfg = Application.get_env(:git_gud, Mailer, [])
23 + name = Keyword.get(cfg, :from_name, "GitGud")
24 + addr = Keyword.get(cfg, :from_address, "noreply@example.com")
25 + {name, addr}
26 + end
27 +
21 28 @doc """
22 29 Deliver instructions to update a user email.
23 30 """
modified mix.exs
+5 −0
@@ -60,6 +60,11 @@ defmodule GitGud.MixProject do
60 60 compile: false,
61 61 depth: 1},
62 62 {:swoosh, "~> 1.16"},
63 + # gen_smtp powers Swoosh.Adapters.SMTP — selected in runtime.exs
64 + # when SMTP_RELAY is set; otherwise the mailer falls through to
65 + # Swoosh.Adapters.Logger so login-link emails land in the pod
66 + # log instead of being silently dropped.
67 + {:gen_smtp, "~> 1.2"},
63 68 {:req, "~> 0.5"},
64 69 {:telemetry_metrics, "~> 1.0"},
65 70 {:telemetry_poller, "~> 1.0"},
modified mix.lock
+2 −0
@@ -21,6 +21,7 @@
21 21 "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
22 22 "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
23 23 "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
24 + "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
24 25 "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
25 26 "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
26 27 "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
@@ -56,6 +57,7 @@
56 57 "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
57 58 "postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
58 59 "protobuf": {:hex, :protobuf, "0.16.0", "d1878725105d49162977cf3408ccc3eac4f3532e26e5a9e250f2c624175d10f6", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f0d0d3edd8768130f24cc2cfc41320637d32c80110e80d13f160fa699102c828"},
60 + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
59 61 "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
60 62 "rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"},
61 63 "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
added rel/env.sh.eex
+19 −0
@@ -0,0 +1,19 @@
1 +#!/bin/sh
2 +
3 +# Long-name distribution so libcluster can reach this node from peers
4 +# and Nebulex's Replicated cache (L2) can fan writes via `pg`.
5 +#
6 +# POD_IP is injected by the deployment from status.podIP. RELEASE_COOKIE
7 +# comes from the elixir-args secret so every replica agrees on the
8 +# Erlang cookie.
9 +#
10 +# Only enable name-distribution when POD_IP is actually populated —
11 +# otherwise RELEASE_NODE becomes "git_gud@" and the BEAM aborts before
12 +# Logger attaches, producing only a generic init crash.
13 +if [ -n "${POD_IP}" ]; then
14 + echo "[env.sh] starting with name distribution as git_gud@${POD_IP}" >&2
15 + export RELEASE_DISTRIBUTION=name
16 + export RELEASE_NODE="git_gud@${POD_IP}"
17 +else
18 + echo "[env.sh] POD_IP is empty — falling back to local-only sname distribution" >&2
19 +fi
added smtp-secret.example.yml
+61 −0
@@ -0,0 +1,61 @@
1 +# Example SMTP relay secret + non-secret config for the git_gud
2 +# deployment.
3 +#
4 +# git_gud uses Swoosh's gen_smtp adapter when `SMTP_RELAY` is set on
5 +# the pod — otherwise it falls back to `Swoosh.Adapters.Logger`, which
6 +# writes the rendered email to the pod log at :info level. That's fine
7 +# for development; for production you want a real relay so magic-link
8 +# logins and email-change confirmations land in real inboxes.
9 +#
10 +# Copy this file to `smtp-secret.yml` (gitignored), drop in the real
11 +# values, then `kubectl apply -f smtp-secret.yml`. The Deployment in
12 +# app.yml will pick the values up via `envFrom: secretRef` after a
13 +# `rollout restart`.
14 +#
15 +# Tested with: Postmark, Mailgun's SMTP endpoint, AWS SES, Fastmail.
16 +# Just about anything that speaks SMTP submission works.
17 +#
18 +# Two pieces:
19 +#
20 +# * `smtp-creds` (Opaque Secret) — username + password
21 +# * `smtp-config` (ConfigMap) — non-secret host/port/tls knobs
22 +
23 +apiVersion: v1
24 +kind: Secret
25 +metadata:
26 + name: smtp-creds
27 + namespace: git-gud
28 +type: Opaque
29 +stringData:
30 + # SMTP auth. Many providers use an API token or app password rather
31 + # than the real account password — check your provider's docs.
32 + SMTP_USERNAME: "REPLACE_ME"
33 + SMTP_PASSWORD: "REPLACE_ME"
34 +---
35 +apiVersion: v1
36 +kind: ConfigMap
37 +metadata:
38 + name: smtp-config
39 + namespace: git-gud
40 +data:
41 + # Host + port for the submission endpoint. 587 is the conventional
42 + # STARTTLS-submission port; 465 is implicit TLS (set SMTP_SSL=true
43 + # below if you use 465). 25 is plaintext and almost always blocked.
44 + SMTP_RELAY: "smtp.example.com"
45 + SMTP_PORT: "587"
46 +
47 + # TLS / SSL knobs. `tls` does STARTTLS upgrade; `ssl` is implicit
48 + # TLS from connect (port 465 style). One of: always | if_available |
49 + # never. `if_available` is a sensible default for submission-port
50 + # relays that may or may not advertise STARTTLS.
51 + SMTP_TLS: "always"
52 + SMTP_SSL: "false"
53 +
54 + # SMTP AUTH knob. `always` will fail if the server doesn't advertise
55 + # AUTH; `if_available` is the safe default for mixed setups.
56 + SMTP_AUTH: "always"
57 +
58 + # Optional From overrides. If unset the mailer defaults to
59 + # "GitGud" <noreply@<PHX_HOST>>.
60 + MAIL_FROM_NAME: "GitGud"
61 + # MAIL_FROM_ADDRESS: "noreply@example.com"

Parents: c9208ac