diff --git a/.env.example b/.env.example index 8eef2fc..3de6771 100644 --- a/.env.example +++ b/.env.example @@ -23,8 +23,9 @@ ADMIN_IDS= # Only the twentyq module needs this. Leave blank to disable that command. GEMINI_API_KEY= -# SOURCE_COMMIT is injected as a build arg by Coolify (it sets it automatically) -# for the deploynotify owner DM. Leave unset for local `docker compose up` — +# SOURCE_COMMIT is injected into the container at runtime by Coolify (a +# predefined env var holding the commit SHA); read at startup for the +# deploynotify owner DM. Leave unset for local `docker compose up` — # deploynotify just stays silent. # SOURCE_COMMIT= diff --git a/Dockerfile b/Dockerfile index 47e3b60..7e3560a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,15 +6,11 @@ RUN go mod download COPY . . -# SOURCE_COMMIT is baked into the binary so internal/deploynotify can DM the -# owner once per new version (parity with the Makefile build). Coolify exposes -# the commit SHA as the SOURCE_COMMIT build arg automatically — no manual -# wiring needed. For a manual build, pass it with -# --build-arg SOURCE_COMMIT=$(git rev-parse --short HEAD) -# When unset, deploynotify treats the empty SHA as "stay silent". -ARG SOURCE_COMMIT="" +# The deploy-notify commit SHA comes from the SOURCE_COMMIT runtime env that +# Coolify injects into the container (see docker-compose.yml), not from a build +# arg — Coolify does not pass build args here. The binary is built plain. RUN CGO_ENABLED=0 GOOS=linux go build \ - -ldflags="-s -w -X main.gitSHA=${SOURCE_COMMIT}" \ + -ldflags="-s -w" \ -o /out/server \ ./cmd/server diff --git a/cmd/server/main.go b/cmd/server/main.go index e9c4899..872c7d7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -36,11 +36,22 @@ import ( "github.com/tiennm99/miti99bot/internal/telegram" ) -// gitSHA is populated at build time via `-ldflags "-X main.gitSHA="` -// (see Makefile). Empty value means the binary was built without that flag — -// deploynotify treats it as a signal to stay silent. +// gitSHA is the local-build fallback, populated via `-ldflags "-X +// main.gitSHA="` (see Makefile). On Coolify the commit comes from the +// SOURCE_COMMIT runtime env instead (resolveCommitSHA prefers it). Empty from +// both sources means deploynotify stays silent. var gitSHA string +// resolveCommitSHA returns the commit identifier for the deploy notification. +// Coolify injects SOURCE_COMMIT into the container environment at runtime, so +// prefer it; fall back to the ldflags-baked gitSHA for local builds. +func resolveCommitSHA(envSourceCommit string) string { + if s := strings.TrimSpace(envSourceCommit); s != "" { + return s + } + return gitSHA +} + // factories is the static module catalog. Adding a new module is a one-line // change here. Lives in main rather than the modules package to avoid an // import cycle (modules → util → modules). @@ -157,7 +168,7 @@ func main() { Bot: b, Store: deploynotify.NewStore(provider.Collection("deploynotify")), OwnerID: cfg.BotOwnerID, - GitSHA: gitSHA, + GitSHA: resolveCommitSHA(cfg.SourceCommit), }) handler := server.New() @@ -260,6 +271,7 @@ func buildProvider(ctx context.Context, cfg config) (storage.Provider, func(), e type config struct { Port string TelegramBotToken string + SourceCommit string // Coolify-injected commit SHA (runtime env) for deploynotify GeminiAPIKey string GoldPriceAPIURL string GoldFXAPIURL string @@ -299,6 +311,7 @@ func loadConfig() config { return config{ Port: port, TelegramBotToken: envMap["TELEGRAM_BOT_TOKEN"], + SourceCommit: envMap["SOURCE_COMMIT"], GeminiAPIKey: envMap["GEMINI_API_KEY"], GoldPriceAPIURL: envMap["GOLD_PRICE_API_URL"], GoldFXAPIURL: envMap["GOLD_FX_API_URL"], diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index b15745e..b1918d7 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -7,6 +7,28 @@ import ( "github.com/tiennm99/miti99bot/internal/storage" ) +func TestResolveCommitSHA(t *testing.T) { + prev := gitSHA + defer func() { gitSHA = prev }() + + // Coolify runtime env wins over the baked fallback. + gitSHA = "baked" + if got := resolveCommitSHA(" runtime-sha "); got != "runtime-sha" { + t.Errorf("env present: got %q, want trimmed runtime-sha", got) + } + + // Empty env falls back to the ldflags-baked value. + if got := resolveCommitSHA(""); got != "baked" { + t.Errorf("env empty: got %q, want baked fallback", got) + } + + // Neither source set → empty (deploynotify stays silent). + gitSHA = "" + if got := resolveCommitSHA(" "); got != "" { + t.Errorf("both empty: got %q, want empty", got) + } +} + func TestFactoriesIncludesGoldAndCoin(t *testing.T) { catalog := factories() if catalog["gold"] == nil { diff --git a/docker-compose.yml b/docker-compose.yml index aaa7a4a..5c88dcb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,6 @@ services: bot: build: context: . - args: - # Coolify sets SOURCE_COMMIT automatically; passed so deploynotify DMs - # the owner on each new version. Optional — empty SHA just stays silent. - SOURCE_COMMIT: ${SOURCE_COMMIT:-} # Or pin a prebuilt image instead of building: # image: ghcr.io/tiennm99/miti99bot:latest restart: unless-stopped @@ -20,6 +16,10 @@ services: ADMIN_IDS: ${ADMIN_IDS} # CSV of admin Telegram user ids # --- Optional --- GEMINI_API_KEY: ${GEMINI_API_KEY} # only the twentyq module needs it + # Coolify injects SOURCE_COMMIT (commit SHA) at deploy time; passed to the + # container so deploynotify DMs the owner once per new version. Empty (e.g. + # local `docker compose up` without it) just stays silent. + SOURCE_COMMIT: ${SOURCE_COMMIT:-} # Storage auto-selects mongodb because MONGO_URL is set — no KV_PROVIDER. # The in-process cron scheduler runs by default — no CRON_MODE. # PORT defaults to 8080 (internal health server) — omit unless overriding. diff --git a/docs/deploy-coolify-selfhosted.md b/docs/deploy-coolify-selfhosted.md index 80570fb..f03fc1b 100644 --- a/docs/deploy-coolify-selfhosted.md +++ b/docs/deploy-coolify-selfhosted.md @@ -88,10 +88,11 @@ SSM/AWS lookup that fails with no AWS creds and bricks startup), `KV_PROVIDER`, bot token; a second poller gets HTTP 409, and a second in-process scheduler double-fires crons. Prefer **stop-first redeploys** so two containers never overlap near a cron time. -5. **Build arg for deploynotify:** Coolify sets `SOURCE_COMMIT` automatically - and the compose build forwards it, so the owner gets the "new version" DM - with no manual wiring. Without it, `deploynotify` stays silent (no crash) — - but you lose that notification. +5. **deploynotify commit SHA:** Coolify injects `SOURCE_COMMIT` (a predefined + runtime env var) into the container, and the compose `environment` forwards + it, so the bot reads it at startup and DMs the owner the "new version" + notice — no manual wiring. Without it, `deploynotify` stays silent (no + crash) — but you lose that notification. 6. **Health check:** use Coolify's HTTP monitor against `GET /` (returns `text/plain` `miti99bot ok`). Do **not** use a compose `healthcheck` — the distroless image has no shell/curl and `cmd/server` has no `-healthcheck`