one day I will automate this part
c87387d · gmorell · 2026-05-15 02:07
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