mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
chore(plans): add deploy-notify-owner plan (completed)
Plan + 2 phase files for the deploynotify feature. Both phases marked completed.
This commit is contained in:
1 parent
22c817b0db
commit
ddb9d61bde
3 files changed
+305
No files matched your search
+141
@@ -0,0 +1,141 @@
|
||||
---
|
||||
phase: 1
|
||||
title: Implement deploynotify package + main.go hook
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 1h
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Implement deploynotify package + main.go hook
|
||||
|
||||
## Overview
|
||||
|
||||
Create `internal/deploynotify/` — a small package that compares the
|
||||
baked-in `gitSHA` against a `last_notified_sha` value in KV and DMs the
|
||||
bot owner if (and only if) it changed. Wire it from `cmd/server/main.go`
|
||||
right after `modules.Install`.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Send exactly one Telegram DM to `BOT_OWNER_ID` per *new* gitSHA observed.
|
||||
- On subsequent cold starts with the same SHA, send nothing.
|
||||
- Skip silently when: `gitSHA` empty (local build), `BOT_OWNER_ID == 0`,
|
||||
or KV operation fails.
|
||||
|
||||
**Non-functional**
|
||||
- Never panic; never return an error that aborts startup.
|
||||
- ≤3s wall time on the happy path (network DM + 1 KV read + 1 KV write).
|
||||
- No new env vars, no new IAM permissions (DynamoDB read/write already
|
||||
granted to the partition).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
cmd/server/main.go
|
||||
│
|
||||
│ after modules.Install(b, reg, auth):
|
||||
├─→ deploynotify.Run(ctx, deploynotify.Config{
|
||||
│ Bot: b,
|
||||
│ KV: provider.For("deploynotify"),
|
||||
│ OwnerID: cfg.BotOwnerID,
|
||||
│ GitSHA: gitSHA, // package-level var, ldflags-injected
|
||||
│ Timeout: 3 * time.Second,
|
||||
│ })
|
||||
│ │
|
||||
│ ├─ skipReason() short-circuit (no SHA / no owner)
|
||||
│ ├─ kv.GetJSON("last_notified_sha", &prev)
|
||||
│ ├─ if prev.SHA == gitSHA → return (silent)
|
||||
│ ├─ bot.SendMessage(owner, "🚀 miti99bot deployed: <code>SHA</code>")
|
||||
│ └─ kv.PutJSON("last_notified_sha", {SHA: gitSHA, At: now})
|
||||
```
|
||||
|
||||
KV namespace: `deploynotify` (new partition, isolated from module data).
|
||||
Key: `last_notified_sha`.
|
||||
Value shape:
|
||||
```go
|
||||
type notifyRecord struct {
|
||||
SHA string `json:"sha"`
|
||||
At int64 `json:"at"` // ms-since-epoch, for debug only
|
||||
}
|
||||
```
|
||||
|
||||
Telegram message (plain text — no parse_mode dependency on formatting
|
||||
edge cases):
|
||||
```
|
||||
🚀 miti99bot deployed: <SHORT_SHA>
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/deploynotify/deploy_notify.go` (snake_case per Go conventions)
|
||||
- Create: `internal/deploynotify/deploy_notify_test.go`
|
||||
- Modify: `cmd/server/main.go` — declare `var gitSHA string`, add
|
||||
`deploynotify.Run(...)` call after `modules.Install(b, reg, auth)` and
|
||||
before the `go func() { srv.ListenAndServe() }()` block.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Create `internal/deploynotify/deploy_notify.go`** with:
|
||||
- `type Config struct { Bot *bot.Bot; KV storage.KVStore; OwnerID int64; GitSHA, Timeout }`.
|
||||
- `func Run(ctx context.Context, cfg Config)` — fire-and-forget, no
|
||||
error return; all failures logged via `internal/log`.
|
||||
- Internal helper `shouldNotify(ctx, kv, sha) (bool, error)` so dedup
|
||||
is unit-testable without a real Telegram bot.
|
||||
- Internal helper `markNotified(ctx, kv, sha) error`.
|
||||
- Internal `renderMessage(sha string) string` — single line, easy to test.
|
||||
|
||||
2. **Wire into `cmd/server/main.go`**:
|
||||
- Add `var gitSHA string` at package level (alongside `factories()`).
|
||||
- After `modules.Install(b, reg, auth)` and the existing `log.Info("modules loaded", ...)`:
|
||||
```go
|
||||
deploynotify.Run(rootCtx, deploynotify.Config{
|
||||
Bot: b,
|
||||
KV: provider.For("deploynotify"),
|
||||
OwnerID: cfg.BotOwnerID,
|
||||
GitSHA: gitSHA,
|
||||
Timeout: 3 * time.Second,
|
||||
})
|
||||
```
|
||||
- Import: `"github.com/tiennm99/miti99bot/internal/deploynotify"`.
|
||||
|
||||
3. **Tests** (`deploy_notify_test.go`):
|
||||
- `TestShouldNotify_FirstRun` — empty KV → returns true.
|
||||
- `TestShouldNotify_SameSHA` — KV holds current SHA → returns false.
|
||||
- `TestShouldNotify_DifferentSHA` — KV holds old SHA → returns true.
|
||||
- `TestRun_SkipsWhenSHAEmpty` — gitSHA="" → no KV access, no send.
|
||||
- `TestRun_SkipsWhenNoOwner` — OwnerID=0 → no KV access, no send.
|
||||
- `TestRenderMessage_ContainsSHA` — output includes the SHA.
|
||||
- Use `storage.NewMemoryKVStore()` for KV.
|
||||
- For the Telegram send path: skip end-to-end Telegram tests — the
|
||||
existing `testutil.RecordingBot` pattern is heavier than needed.
|
||||
Cover send via an indirection: `Config.sender` field of type
|
||||
`func(ctx, chatID, text) error` defaulting to `b.SendMessage`
|
||||
wrapper. Tests inject a recorder.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `go build ./...` succeeds.
|
||||
- [ ] `go test ./internal/deploynotify/...` passes.
|
||||
- [ ] `go test ./...` passes (no regressions).
|
||||
- [ ] `cmd/server/main.go` still under reasonable size; deploynotify call
|
||||
adds ≤6 LOC.
|
||||
- [ ] Manual review: a `Run` invocation with empty SHA touches neither KV
|
||||
nor Telegram (traceable in code, not just behaviour).
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Cold-start storm sends duplicate DMs (2+ instances boot concurrently after deploy) | Documented in plan Out-of-Scope; race window narrow; failure mode is annoyance not corruption. |
|
||||
| KV write succeeds, Telegram send fails | Order is reversed: send first, write only on send success. So a failed send doesn't permanently silence retries. |
|
||||
| KV read fails (DynamoDB throttle) | Treat as "not notified yet" → fall through to send. Worst case: extra DM. |
|
||||
| Test for Run-with-bot leaks a goroutine | Run is synchronous (uses Timeout via context.WithTimeout). No goroutine spawned. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Owner ID is already in env; no new secret material.
|
||||
- Telegram message body contains only the short git SHA — public
|
||||
information (the repo is public). No env/secrets leak risk.
|
||||
- KV partition `deploynotify` is read/write under existing IAM scope.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
phase: 2
|
||||
title: Wire git SHA into build
|
||||
status: completed
|
||||
priority: P3
|
||||
effort: 20m
|
||||
dependencies:
|
||||
- 1
|
||||
---
|
||||
|
||||
# Phase 2: Wire git SHA into build
|
||||
|
||||
## Overview
|
||||
|
||||
Inject the short git SHA into the binary at link time via
|
||||
`-X main.gitSHA=…`. Without this, Phase 1's `Run` short-circuits and the
|
||||
feature is dormant. Touches Makefile only; GitHub Actions already runs
|
||||
`make build-lambda`, so no workflow change is required.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- `make build-lambda` produces a binary whose `main.gitSHA` equals
|
||||
`git rev-parse --short HEAD` at build time.
|
||||
- `make build` (local host binary) does the same — useful for dogfooding.
|
||||
- Both targets degrade gracefully if `git` is unavailable: empty SHA →
|
||||
Phase 1's `Run` silently skips.
|
||||
|
||||
**Non-functional**
|
||||
- No new tools or actions added to CI.
|
||||
- `actions/checkout@v6` default depth (shallow) must support
|
||||
`git rev-parse --short HEAD` — it does, HEAD is always present.
|
||||
|
||||
## Architecture
|
||||
|
||||
```makefile
|
||||
GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
|
||||
LDFLAGS := -s -w -X main.gitSHA=$(GIT_SHA)
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o ./bin/server ./cmd/server
|
||||
|
||||
build-lambda:
|
||||
@mkdir -p $(dir $(LAMBDA_OUT))
|
||||
CGO_ENABLED=0 GOOS=$(LAMBDA_GOOS) GOARCH=$(LAMBDA_GOARCH) \
|
||||
go build -tags lambda.norpc -ldflags="$(LDFLAGS)" \
|
||||
-o $(LAMBDA_OUT) ./cmd/server
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `Makefile` — add `GIT_SHA` + `LDFLAGS` vars, swap inline
|
||||
`-ldflags="-s -w"` for `-ldflags="$(LDFLAGS)"` in both `build` and
|
||||
`build-lambda` targets.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Add Makefile variables** near the top (after existing `LAMBDA_OUT` etc):
|
||||
```makefile
|
||||
GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
LDFLAGS := -s -w -X main.gitSHA=$(GIT_SHA)
|
||||
```
|
||||
|
||||
2. **Update `build` target** (`Makefile:51-52`):
|
||||
- Change `-ldflags="-s -w"` → `-ldflags="$(LDFLAGS)"`.
|
||||
|
||||
3. **Update `build-lambda` target** (`Makefile:54-60`):
|
||||
- Change `-ldflags="-s -w"` → `-ldflags="$(LDFLAGS)"`.
|
||||
|
||||
4. **Verify**:
|
||||
- `make build` then `strings ./bin/server | grep -E '^[0-9a-f]{7,}$'`
|
||||
should reveal the SHA.
|
||||
- Or: add a no-op `--version`-style log line behind a build flag —
|
||||
skip for now (YAGNI).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `make build` builds successfully.
|
||||
- [ ] `make build-lambda` builds successfully.
|
||||
- [ ] Resulting binary has `main.gitSHA` populated (verified once via
|
||||
`strings` grep or a temporary debug log — not a permanent test).
|
||||
- [ ] No change to `.github/workflows/deploy.yml` — `make build-lambda`
|
||||
in CI picks up the new ldflags automatically.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| Dockerfile builds (CI `docker build -t miti99bot`) bypass Makefile and won't inject SHA | Out of scope — Lambda deploy uses `make build-lambda`, not Docker. CI's `docker build` is just a smoke check. Document the gap; revisit only if Cloud Run path is reactivated. |
|
||||
| Local `make build` in a tarball download with no `.git/` → SHA empty | Acceptable — feature silently disables on non-git builds. |
|
||||
| `git rev-parse` outputs spaces / unexpected chars → breaks ldflags | `git rev-parse --short HEAD` output is `[0-9a-f]{7,}` only. Safe. |
|
||||
| Reproducible builds tooling complains about non-deterministic SHA | Not relevant for this project. |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Short git SHA is published on every GitHub commit page — not sensitive.
|
||||
- No build-time secrets touched.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Deploy notification to bot owner with git SHA
|
||||
description: >-
|
||||
On startup the bot DMs BOT_OWNER_ID with the deployed git SHA. Dedup via
|
||||
DynamoDB so only real new versions notify, not every Lambda cold start.
|
||||
status: completed
|
||||
priority: P3
|
||||
branch: main
|
||||
tags:
|
||||
- ops
|
||||
- observability
|
||||
- telegram
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
created: '2026-05-22T04:10:07.522Z'
|
||||
createdBy: 'ck:plan'
|
||||
source: skill
|
||||
---
|
||||
|
||||
# Deploy notification to bot owner with git SHA
|
||||
|
||||
## Overview
|
||||
|
||||
Operator awareness of deploys. After `make build-lambda` + `sam deploy`, the
|
||||
owner currently has no in-Telegram signal that the new code is running on
|
||||
Lambda (only the GitHub Actions log). Add a startup hook that DMs the owner
|
||||
with the baked-in short git SHA, deduped by KV so subsequent cold starts of
|
||||
the same version stay silent.
|
||||
|
||||
Confirmation that the **new code is running**, not just that the deploy
|
||||
script finished — that's why this lives in the bot binary, not in the deploy
|
||||
workflow.
|
||||
|
||||
## Design Decisions (locked)
|
||||
|
||||
- **Dedup**: DynamoDB KV stores `last_notified_sha`. Send only when baked
|
||||
`gitSHA != stored`, then write. One `GetItem` per cold start (~free tier).
|
||||
- **Code placement**: new `internal/deploynotify/` package — testable in
|
||||
isolation, ~50 LOC, main.go just calls `deploynotify.Run(...)`.
|
||||
- **Build wiring**: `-ldflags "-X main.gitSHA=<short-sha>"` in Makefile.
|
||||
Empty `gitSHA` (local non-make build) → silently skip.
|
||||
- **Failure policy**: log + continue. Never block server startup. KV error,
|
||||
Telegram error, missing owner — all non-fatal.
|
||||
- **Timing**: synchronous, ≤3s timeout, runs after `modules.Install` and
|
||||
before `srv.ListenAndServe()`. Lambda init phase has 10s headroom.
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | Name | Status |
|
||||
|-------|------|--------|
|
||||
| 1 | [Implement deploynotify package + main.go hook](./phase-01-implement-deploynotify-package-main-go-hook.md) | Completed |
|
||||
| 2 | [Wire git SHA into build](./phase-02-wire-git-sha-into-build.md) | Completed |
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Multi-environment routing (prod vs staging) — single owner, single env today.
|
||||
- Notify on rollback — same dedup mechanism naturally handles it; rollback to
|
||||
a previously-notified SHA just resends because stored value moved forward.
|
||||
Acceptable.
|
||||
- Conditional KV write to prevent concurrent-cold-start dupes — KV interface
|
||||
has no CAS today; the race window is narrow and the failure mode is
|
||||
"two identical DMs", not data corruption.
|
||||
Reference in new issue
Block a user