fix: address project review findings
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 48s
ci / api (push) Successful in 51s
ci / recommender (push) Canceled after 2s
ci / ts (packages + web) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (apps/web/Dockerfile, web, ) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, api, api) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s

Security and robustness:
- storage-agent rejects an empty INTERNAL_TOKEN (config notEmpty + middleware)
- api takes the client IP only from X-Real-IP (chi RealIP trusted True-Client-IP)
- /readyz reports ok/fail and logs the cause; checks run in parallel with a timeout
- storage-agent shuts down gracefully; shared httpx.NewServer/Serve with IdleTimeout
- goose migrations take a Postgres advisory lock
- recommender readyz: per-check timeout, redis socket timeouts

Clients:
- Android release allows cleartext HTTP for LAN servers (expo-build-properties)
- checkServer times out after 10s; connect button is disabled while checking

Deploy and CI:
- install.sh hands cache/uploads/updates/models to the non-root service users
- update.sh refuses downgrades unless PIPU_ALLOW_DOWNGRADE=1
- recommender/analyzer get only the env they need; required secrets fail fast
- nginx security headers and CSP; strip True-Client-IP
- dev compose binds ports to 127.0.0.1; postgres/redis images pinned
- version.mjs fails on a malformed explicit tag instead of releasing 0.0.0-dev
- contract check catches untracked generated files
- final releases fail without a release keystore; changelog skips rc tags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 00:46:46 +03:00
1 parent 1324cad2af
commit 4fa1add80d
37 files changed
+503 -199

No files matched your search

+31 -11
View File
@@ -3,6 +3,8 @@ package system
import (
"context"
"log/slog"
"sync"
"time"
"pipu/api/internal/api/gen"
@@ -29,13 +31,17 @@ type Info struct {
// Handler реализует системную часть gen.StrictServerInterface.
type Handler struct {
info Info
log *slog.Logger
checks map[string]Checker
checkTimeout time.Duration
}
// NewHandler создаёт обработчик. checks — именованные проверки для /readyz.
func NewHandler(info Info, checks map[string]Checker) *Handler {
return &Handler{info: info, checks: checks, checkTimeout: 2 * time.Second}
func NewHandler(info Info, log *slog.Logger, checks map[string]Checker) *Handler {
if log == nil {
log = slog.New(slog.DiscardHandler)
}
return &Handler{info: info, log: log, checks: checks, checkTimeout: 2 * time.Second}
}
// GetHealthz сообщает, что процесс жив; зависимости не проверяет.
@@ -43,21 +49,35 @@ func (h *Handler) GetHealthz(context.Context, gen.GetHealthzRequestObject) (gen.
return gen.GetHealthz200JSONResponse{Status: gen.HealthStatusStatusOk}, nil
}
// GetReadyz проверяет все зависимости; 503, если хотя бы одна недоступна.
// GetReadyz параллельно проверяет все зависимости; 503, если хотя бы одна недоступна.
// /readyz доступен снаружи, поэтому текст ошибки пишется только в лог, а в ответ — "fail".
func (h *Handler) GetReadyz(ctx context.Context, _ gen.GetReadyzRequestObject) (gen.GetReadyzResponseObject, error) {
ctx, cancel := context.WithTimeout(ctx, h.checkTimeout)
defer cancel()
results := make(map[string]string, len(h.checks))
healthy := true
var (
mu sync.Mutex
wg sync.WaitGroup
results = make(map[string]string, len(h.checks))
healthy = true
)
for name, c := range h.checks {
if err := c.Check(ctx); err != nil {
results[name] = err.Error()
healthy = false
continue
}
results[name] = "ok"
wg.Add(1)
go func() {
defer wg.Done()
err := c.Check(ctx)
mu.Lock()
defer mu.Unlock()
if err != nil {
h.log.WarnContext(ctx, "readiness check failed", "check", name, "err", err)
results[name] = "fail"
healthy = false
return
}
results[name] = "ok"
}()
}
wg.Wait()
if !healthy {
return gen.GetReadyz503JSONResponse{Status: gen.ReadyStatusStatusDegraded, Checks: results}, nil
+2 -2
View File
@@ -20,7 +20,7 @@ func newServer(checks map[string]system.Checker) http.Handler {
Version: "1.2.3",
SupportedLocales: []string{"ru", "en"},
MinAndroidVersionCode: 10203,
}, checks)
}, nil, checks)
return server.NewRouter(server.Deps{System: h})
}
@@ -68,7 +68,7 @@ func TestReadyz(t *testing.T) {
assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
assert.Equal(t, "degraded", body["status"])
assert.Equal(t, map[string]any{"postgres": "ok", "redis": "connection refused"}, body["checks"])
assert.Equal(t, map[string]any{"postgres": "ok", "redis": "fail"}, body["checks"])
})
}