1 Commits
Author SHA1 Message Date
sneak 0d5b3b23ea Rewrite the -count=1 note to match the current files (closes #77)
check / check (push) Successful in 50s
The note under the canonical Go `make test` example in `prompts/REPO_POLICIES.md` still named the cache-busting build argument that `--no-cache` replaced, and said Go's cache was baked into earlier image layers.

It now says where Go's test result cache can replay a pass: on a developer's machine, where the Makefile target runs, so `-count=1` stays on both invocations. The `test` phase of the `Dockerfile` has nothing to replay: its base image holds no result for the repo's tests and no earlier step runs one.

The first paragraph no longer says the rerun would replay a failure: Go stores only passes.

Model: opus-5-5
2026-10-04 03:20:01 +00:00
10 changed files with 131 additions and 289 deletions
+1 -13
View File
@@ -17,17 +17,7 @@
# stage that compiles runs `git describe --tags --always` on .git, which # stage that compiles runs `git describe --tags --always` on .git, which
# does not need .git/config; that file can hold a credential, such as a # does not need .git/config; that file can hold a credential, such as a
# password in a remote URL or the token the CI checkout step stores there. # password in a remote URL or the token the CI checkout step stores there.
# Each submodule keeps a config with the same exposure in its git directory .git/config
# under .git/modules/, nested again for a submodule's own submodules, or in
# its own .git directory when it keeps one.
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
# `deploy/config`, `config/lib`) loses its whole git directory, because
# `**/.git/modules/**/config` also matches that segment's directory
# under .git/modules/. Go's version stamping then fails the build;
# nothing leaks. Name such a submodule without that segment:
# `git submodule add --name`.
**/.git/config
**/.git/modules/**/config
# Agent scratch: one full checkout of the repo per in-flight agent. # Agent scratch: one full checkout of the repo per in-flight agent.
# Anchored because it occurs once where agents run at the repo root. # Anchored because it occurs once where agents run at the repo root.
@@ -51,9 +41,7 @@
**/[iI][dD]_[rR][sS][aA] **/[iI][dD]_[rR][sS][aA]
**/[iI][dD]_[dD][sS][aA] **/[iI][dD]_[dD][sS][aA]
**/[iI][dD]_[eE][cC][dD][sS][aA] **/[iI][dD]_[eE][cC][dD][sS][aA]
**/[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
**/[iI][dD]_[eE][dD]25519 **/[iI][dD]_[eE][dD]25519
**/[iI][dD]_[eE][dD]25519_[sS][kK]
# Dependencies: restored inside the image, never copied in. # Dependencies: restored inside the image, never copied in.
**/node_modules **/node_modules
-4
View File
@@ -1,4 +0,0 @@
# Every PR adds an entry at the top of TODO.md's Completed Steps; union keeps
# both sides instead of conflicting. Git never reports a conflict here: read
# the merged entries after every merge or rebase.
TODO.md merge=union
-2
View File
@@ -42,6 +42,4 @@ node_modules/
[iI][dD]_[rR][sS][aA] [iI][dD]_[rR][sS][aA]
[iI][dD]_[dD][sS][aA] [iI][dD]_[dD][sS][aA]
[iI][dD]_[eE][cC][dD][sS][aA] [iI][dD]_[eE][cC][dD][sS][aA]
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
[iI][dD]_[eE][dD]25519 [iI][dD]_[eE][dD]25519
[iI][dD]_[eE][dD]25519_[sS][kK]
-78
View File
@@ -21,90 +21,12 @@ fmt-check, and commit.
# Completed Steps # Completed Steps
- 2026-10-04: Went through the fleet findings recorded on 2026-08-09 (issue 62)
and added the two rules `REPO_POLICIES.md` did not yet state: a new or changed
check is proven by planting a defect it must catch; and a change to a separate
workflow limited to `main` is first run from the feature branch, added to that
workflow's `branches` list and removed again before merging. The other
findings were already stated, replaced by `--no-cache`, about git worktrees,
or about how agents work together. The warning against
`golangci-lint config verify` is dropped because sneak ruled on
https://git.eeqj.de/sneak/prompts/issues/40 (2026-08-10) that there is no
config check step and the config is assumed valid; a vendored `.golangci.yml`
stays byte-identical to the canonical copy. The issue gives each reason.
- 2026-10-04: `REPO_POLICIES.md` now says which `Dockerfile` stages run
`script/bootstrap` (issue 90). The gate phases and the build stage start from
their pinned base images and install what those images lack either inline, as
the canonical Go `Dockerfile` does for `git`, or by running
`script/bootstrap`, as this repo's own `Dockerfile` does for its yarn
packages. The development environment stage, the final stage of a non-server
repo, runs `script/bootstrap`. The new repo checklist says the same.
- 2026-10-04: Added a root `.gitattributes` that merges `TODO.md` with git's
union merge (issue 98), so two branches that each add an entry at the top of
Completed Steps merge without a conflict. Git now never reports a conflict in
`TODO.md`: a real conflict elsewhere keeps both versions of the line, and when
two new entries share an identical line, one is inserted into the middle of
the other, which a rebase can do to an entry already on `next`. Read the
merged entries after every merge or rebase. This applies to this repository
only; no canonical file changed.
- 2026-10-04: The canonical `.dockerignore` now also keeps out the git `config`
of a submodule that keeps its own `.git` directory, which still reached the
image (issue 88): both git patterns now carry the `**/` prefix. A submodule
whose name has a `config` segment (`config`, `deploy/config`, `config/lib`)
still loses its whole git directory, so Go's version stamping fails the build;
the file records this as a `KNOWN GAP:` with the remedy,
`git submodule add --name`. Closing it would take a wildcard re-include, which
makes BuildKit walk every excluded directory, such as `node_modules`, on every
build. `REPO_POLICIES.md` and both checklists say so in the same words.
- 2026-10-04: The note under the canonical Go `Dockerfile` example in
`REPO_POLICIES.md` now installs lint-phase system libraries with `apt-get`
under their Debian package names (issue 83). The `golangci/golangci-lint`
image is Debian-based and has no `apk`, so the old `apk add` instruction
failed as written. Nothing is pinned or unpinned; that is still open on
issue 72.
- 2026-10-04: Fixed the server lifecycle example in
`prompts/GO_HTTP_SERVER_CONVENTIONS.md` (issue 86). Only fx handles SIGINT and
SIGTERM, and `Run()` in `main` exits with the shutdown's exit code. A listen
error asks fx to shut down with exit code 1 through `fx.Shutdowner`; a Sentry
start failure is returned from the server's start hook instead of calling
`os.Exit` from a goroutine, so the stop hooks of what had started still run.
The server's stop hook shuts the HTTP server down within 5 seconds and fails
when requests are still running. A new paragraph says who owns signals and the
exit code.
- 2026-10-04: `REPO_POLICIES.md` now says how a Go tool a repo needs on the host
is pinned (issue 37): installed with `go install` pinned to a commit hash,
never tracked as a `go.mod` tool dependency or through a `tools.go` file.
golangci-lint is unaffected, since no repo installs it on the host.
- 2026-10-04: The canonical `.gitignore` and `.dockerignore` now also keep out
`id_ecdsa_sk` and `id_ed25519_sk`, the private key files `ssh-keygen` writes
for keys backed by a hardware security key (issue 81). Their `.pub` halves
stay trackable.
- 2026-10-04: `package.json` now has `"license": "MIT"`, matching `LICENSE`, so
yarn no longer prints "No license field" when `script/bootstrap` runs it
inside the Docker phases (issue 76). That was the only yarn warning there.
- 2026-10-04: `REPO_POLICIES.md` now states that guidance for coding agents
lives in one `AGENTS.md` at the repository root, never under a file or
directory named after one agent tool and never in separate memory files (issue
31). This retires the rule, still present in older vendored copies, that kept
agent memory as committed files under `.claude/memory/`. `AGENTS.md` joins the
list of files allowed in the root, and both checklists say so.
- 2026-10-04: Rewrote the note under the canonical Go `make test` example in - 2026-10-04: Rewrote the note under the canonical Go `make test` example in
`REPO_POLICIES.md` (issue 77), which still named the cache-busting build `REPO_POLICIES.md` (issue 77), which still named the cache-busting build
argument that `--no-cache` replaced. It now says where Go's test result cache argument that `--no-cache` replaced. It now says where Go's test result cache
can replay a pass: on a developer's machine, where the Makefile target runs, can replay a pass: on a developer's machine, where the Makefile target runs,
and not in the `test` phase of the `Dockerfile`, whose base image and earlier and not in the `test` phase of the `Dockerfile`, whose base image and earlier
steps hold no result for the repo's tests. steps hold no result for the repo's tests.
- 2026-10-04: The Makefile examples in the Go styleguide and the HTTP server
conventions now fall back to `dev` when `git describe` prints nothing (outside
a git checkout, or where git is missing or refuses the checkout), instead of
stamping an empty version (issue 74). The canonical `Dockerfile` already fails
on a `dev` version when `.git` is in the build context.
- 2026-10-04: The canonical `.dockerignore` now also keeps out each submodule's
`config` (issue 75). A submodule's git directory lives under `.git/modules/`,
nested again for its own submodules, and its `config` can hold a credential
just like `.git/config`. The pattern `.git/modules/**/config` covers every
depth and leaves the top-level `.git` that `git describe` reads untouched.
`REPO_POLICIES.md` and both checklists say so in the same words.
- 2026-10-03: Fixed two defects in the canonical Go `Dockerfile` example (issue - 2026-10-03: Fixed two defects in the canonical Go `Dockerfile` example (issue
73). The test phase now uses the Debian Go image, since `-race` needs cgo and 73). The test phase now uses the Debian Go image, since `-race` needs cgo and
the alpine image has no C compiler, so the phase failed before running a test. the alpine image has no C compiler, so the phase failed before running a test.
-1
View File
@@ -1,5 +1,4 @@
{ {
"license": "MIT",
"devDependencies": { "devDependencies": {
"prettier": "3.8.1" "prettier": "3.8.1"
} }
+3 -5
View File
@@ -1,6 +1,6 @@
--- ---
title: Code Styleguide — Go title: Code Styleguide — Go
last_modified: 2026-10-04 last_modified: 2026-10-02
--- ---
1. Try to hard wrap long lines at 77 characters or less. 1. Try to hard wrap long lines at 77 characters or less.
@@ -51,10 +51,8 @@ last_modified: 2026-10-04
# ?= rather than := so that a `VERSION` build argument takes precedence: # ?= rather than := so that a `VERSION` build argument takes precedence:
# where a build stage invokes make, `ARG VERSION` puts it in the # where a build stage invokes make, `ARG VERSION` puts it in the
# environment and `?=` defers to it. Otherwise `git describe` runs, in a # environment and `?=` defers to it. Otherwise `git describe` runs, in a
# build stage on the `.git` the build context carries. When it prints # build stage on the `.git` the build context carries.
# nothing (outside a git checkout, or where git is missing or refuses the VERSION ?= $(shell git describe --tags --always)
# checkout), the version falls back to `dev`.
VERSION ?= $(or $(shell git describe --tags --always 2>/dev/null),dev)
GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Version=$(VERSION)
+21 -32
View File
@@ -1,6 +1,6 @@
--- ---
title: Existing Repo Checklist title: Existing Repo Checklist
last_modified: 2026-10-04 last_modified: 2026-10-03
--- ---
Use this checklist when beginning work in a repo that may not yet conform to our Use this checklist when beginning work in a repo that may not yet conform to our
@@ -24,10 +24,6 @@ with your task.
- [ ] `LICENSE` file exists and matches the README - [ ] `LICENSE` file exists and matches the README
- [ ] `REPO_POLICIES.md` exists and version date is current — fetch from - [ ] `REPO_POLICIES.md` exists and version date is current — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md` `https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
- [ ] Guidance for coding agents, if the repo has any, is one `AGENTS.md` at the
root — never a file or directory named after one agent tool, such as
`CLAUDE.md` or `.claude/`, and never separate memory files. Move what any
such committed file says into `AGENTS.md` and delete it.
- [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, language - [ ] `.gitignore` is comprehensive (OS, editor, agent scratch, language
artifacts, secrets) — fetch from artifacts, secrets) — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing. `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` if missing.
@@ -63,33 +59,26 @@ with your task.
here run anywhere other than the repo root, the anchored entry misses here run anywhere other than the repo root, the anchored entry misses
`services/api/.claude/`: add anchored entries for those directories. `services/api/.claude/`: add anchored entries for those directories.
- [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into - [ ] If the repo embeds a version in a binary: `.dockerignore` lets `.git` into
the build context. It keeps out every git `config` at any depth the build context. It keeps out `.git/config`, which `git describe` does
(`**/.git/config`, `**/.git/modules/**/config`): the repository's own, not need and which can hold a credential: a password in a remote URL, or
each submodule's under `.git/modules/`, and that of a submodule keeping the token the CI checkout step stores there. The stage that compiles has
its own `.git` directory. `git describe` does not need them, and each can `git` (the Debian Go image has it; an alpine one needs
hold a credential: a password in a remote URL, or the token the CI `apk add --no-cache git`) and takes the version from the `VERSION` build
checkout step stores there. A submodule whose name has a `config` segment argument when one is given, otherwise from `git describe --tags --always`.
(`config`, `deploy/config`, `config/lib`) loses its whole git directory to That gives the tag on a tagged commit; on a later commit, the tag, the
`**/.git/modules/**/config`, and Go's version stamping then fails the number of commits since it and the short commit (`v1.2.3-4-gabc1234`); and
build: give it a name without that segment (`git submodule add --name`). the short commit when no tag is reachable. The stage that compiles also
The stage that compiles has `git` (the Debian Go image has it; an alpine marks its working directory safe for git
one needs `apk add --no-cache git`) and takes the version from the (`git config --system --add safe.directory /src`): a context sent as a tar
`VERSION` build argument when one is given, otherwise from stream keeps the sender's file owners, and git refuses a checkout owned by
`git describe --tags --always`. That gives the tag on a tagged commit; on another user, so the version would come out empty. `ARG VERSION` has no
a later commit, the tag, the number of commits since it and the short default, and the build fails if the context carries `.git` and the version
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is still comes out empty, `dev` or `unknown`. A plain `docker build .` with
reachable. The stage that compiles also marks its working directory safe no build arguments must succeed; a Dockerfile that refuses an empty build
for git (`git config --system --add safe.directory /src`): a context sent argument drops that refusal and keeps the argument. `script/docker` and
as a tar stream keeps the sender's file owners, and git refuses a checkout `script/cibuild` pass the version they compute on the host; it takes
owned by another user, so the version would come out empty. `ARG VERSION` precedence. A tag-derived version additionally needs `fetch-depth: 0` on
has no default, and the build fails if the context carries `.git` and the the CI checkout step, which clones shallow and fetches no tags by default.
version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument.
`script/docker` and `script/cibuild` pass the version they compute on the
host; it takes precedence. A tag-derived version additionally needs
`fetch-depth: 0` on the CI checkout step, which clones shallow and fetches
no tags by default.
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
push — reference push — reference
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
+60 -60
View File
@@ -1,6 +1,6 @@
--- ---
title: Go HTTP Server Conventions title: Go HTTP Server Conventions
last_modified: 2026-10-04 last_modified: 2026-10-02
--- ---
This document defines the architectural patterns, design decisions, and This document defines the architectural patterns, design decisions, and
@@ -106,9 +106,6 @@ project-root/
package main package main
import ( import (
"os/signal"
"syscall"
"yourproject/internal/config" "yourproject/internal/config"
"yourproject/internal/database" "yourproject/internal/database"
"yourproject/internal/globals" "yourproject/internal/globals"
@@ -129,9 +126,6 @@ func main() {
globals.Appname = Appname globals.Appname = Appname
globals.Version = Version globals.Version = Version
// A write to a closed stdout or stderr must not end the process.
signal.Ignore(syscall.SIGPIPE)
fx.New( fx.New(
fx.Provide( fx.Provide(
config.New, config.New,
@@ -204,8 +198,7 @@ Providers are resolved automatically by fx, but conceptually follow this order:
Database) Database)
6. `middleware.New` - Middleware (depends on Logger, Globals, Config) 6. `middleware.New` - Middleware (depends on Logger, Globals, Config)
7. `handlers.New` - Handlers (depends on Logger, Globals, Database, Healthcheck) 7. `handlers.New` - Handlers (depends on Logger, Globals, Database, Healthcheck)
8. `server.New` - Server (depends on all above, and on `fx.Shutdowner`, which fx 8. `server.New` - Server (depends on all above)
provides itself)
--- ---
@@ -224,14 +217,16 @@ type ServerParams struct {
Config *config.Config Config *config.Config
Middleware *middleware.Middleware Middleware *middleware.Middleware
Handlers *handlers.Handlers Handlers *handlers.Handlers
Shutdowner fx.Shutdowner
} }
type Server struct { type Server struct {
startupTime time.Time startupTime time.Time
port int port int
exitCode int
sentryEnabled bool sentryEnabled bool
log *slog.Logger log *slog.Logger
ctx context.Context
cancelFunc context.CancelFunc
httpServer *http.Server httpServer *http.Server
router *chi.Mux router *chi.Mux
params ServerParams params ServerParams
@@ -253,15 +248,13 @@ func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
lc.Append(fx.Hook{ lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error { OnStart: func(ctx context.Context) error {
s.startupTime = time.Now() s.startupTime = time.Now()
if err := s.enableSentry(); err != nil { go s.Run()
return err return nil
} },
s.SetupRoutes() OnStop: func(ctx context.Context) error {
s.httpServer = s.newHTTPServer() // Server shutdown logic
go s.serveUntilShutdown()
return nil return nil
}, },
OnStop: s.cleanShutdown,
}) })
return s, nil return s, nil
} }
@@ -271,25 +264,23 @@ func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
```go ```go
// internal/server/http.go // internal/server/http.go
func (s *Server) newHTTPServer() *http.Server { func (s *Server) serveUntilShutdown() {
return &http.Server{ listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
Addr: fmt.Sprintf(":%d", s.params.Config.Port), s.httpServer = &http.Server{
Addr: listenAddr,
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
Handler: s, Handler: s,
} }
}
// serveUntilShutdown returns when the stop hook shuts the HTTP server down. s.SetupRoutes()
// If it stops for any other reason, such as its port being taken, it asks fx
// to shut down with exit code 1. s.log.Info("http begin listen", "listenaddr", listenAddr)
func (s *Server) serveUntilShutdown() {
s.log.Info("http begin listen", "listenaddr", s.httpServer.Addr)
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
s.log.Error("listen error", "error", err) s.log.Error("listen error", "error", err)
if err := s.params.Shutdowner.Shutdown(fx.ExitCode(1)); err != nil { if s.cancelFunc != nil {
s.log.Error("shutdown request failed", "error", err) s.cancelFunc()
} }
} }
} }
@@ -301,30 +292,43 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
## Signal Handling and Graceful Shutdown ## Signal Handling and Graceful Shutdown
fx owns SIGINT, SIGTERM and the exit code. `Run()` in `main` waits for one of
those signals or for a call to `Shutdown()` on `fx.Shutdowner`, runs the stop
hooks, and exits 0 after a signal, or with the code the call gave in
`fx.ExitCode`. It exits 1 instead when a start hook or a stop hook returns an
error; when a start hook fails, fx first runs the stop hooks of everything
already started. No other code calls `signal.Notify` or `os.Exit`: the listen
error above asks fx to shut down with `fx.ExitCode(1)`, and a Sentry start
failure is returned from the start hook. Each component releases its own
resources in its own stop hook, which fx runs in the reverse order of start.
```go ```go
// cleanShutdown is the server's stop hook. It fails when requests are still func (s *Server) serve() int {
// running after 5 seconds. s.ctx, s.cancelFunc = context.WithCancel(context.Background())
func (s *Server) cleanShutdown(ctx context.Context) error {
ctxShutdown, shutdownCancel := context.WithTimeout(ctx, 5*time.Second) // Signal watcher
defer shutdownCancel() go func() {
err := s.httpServer.Shutdown(ctxShutdown) c := make(chan os.Signal, 1)
signal.Ignore(syscall.SIGPIPE)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
sig := <-c
s.log.Info("signal received", "signal", sig)
if s.cancelFunc != nil {
s.cancelFunc()
}
}()
go s.serveUntilShutdown()
for range s.ctx.Done() {
}
s.cleanShutdown()
return s.exitCode
}
func (s *Server) cleanShutdown() {
s.exitCode = 0
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
s.log.Error("server clean shutdown failed", "error", err)
}
if shutdownCancel != nil {
shutdownCancel()
}
s.cleanupForExit()
if s.sentryEnabled { if s.sentryEnabled {
sentry.Flush(2 * time.Second) sentry.Flush(2 * time.Second)
} }
if err != nil {
return fmt.Errorf("http server shutdown: %w", err)
}
return nil
} }
``` ```
@@ -983,10 +987,8 @@ Use ldflags to inject version information at build time:
# ?= rather than := so that a `VERSION` build argument takes precedence: # ?= rather than := so that a `VERSION` build argument takes precedence:
# where a build stage invokes make, `ARG VERSION` puts it in the # where a build stage invokes make, `ARG VERSION` puts it in the
# environment and `?=` defers to it. Otherwise `git describe` runs, in a # environment and `?=` defers to it. Otherwise `git describe` runs, in a
# build stage on the `.git` the build context carries. When it prints # build stage on the `.git` the build context carries.
# nothing (outside a git checkout, or where git is missing or refuses the VERSION ?= $(shell git describe --tags --always)
# checkout), the version falls back to `dev`.
VERSION ?= $(or $(shell git describe --tags --always 2>/dev/null),dev)
build: build:
go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd
@@ -1156,11 +1158,11 @@ s.router.Get("/.well-known/healthcheck", s.h.HandleHealthCheck())
Sentry is conditionally enabled based on `SENTRY_DSN` environment variable: Sentry is conditionally enabled based on `SENTRY_DSN` environment variable:
```go ```go
func (s *Server) enableSentry() error { func (s *Server) enableSentry() {
s.sentryEnabled = false s.sentryEnabled = false
if s.params.Config.SentryDSN == "" { if s.params.Config.SentryDSN == "" {
return nil return
} }
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
@@ -1168,17 +1170,15 @@ func (s *Server) enableSentry() error {
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version), Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
}) })
if err != nil { if err != nil {
return fmt.Errorf("sentry init failure: %w", err) s.log.Error("sentry init failure", "error", err)
os.Exit(1)
return
} }
s.log.Info("sentry error reporting activated") s.log.Info("sentry error reporting activated")
s.sentryEnabled = true s.sentryEnabled = true
return nil
} }
``` ```
The server's start hook calls `enableSentry()` and returns its error, so a DSN
Sentry rejects stops startup and fx exits 1.
Sentry middleware with repanic (bubbles panics to chi's Recoverer): Sentry middleware with repanic (bubbles panics to chi's Recoverer):
```go ```go
@@ -1190,7 +1190,7 @@ if s.sentryEnabled {
} }
``` ```
Flush Sentry in the server's stop hook, `cleanShutdown()`: Flush Sentry on shutdown:
```go ```go
if s.sentryEnabled { if s.sentryEnabled {
+20 -31
View File
@@ -1,6 +1,6 @@
--- ---
title: New Repo Checklist title: New Repo Checklist
last_modified: 2026-10-04 last_modified: 2026-10-03
--- ---
Use this checklist when creating a new repository from scratch. Follow the steps Use this checklist when creating a new repository from scratch. Follow the steps
@@ -54,9 +54,6 @@ Template files can be fetched from:
- [ ] `LICENSE` file matching the chosen license - [ ] `LICENSE` file matching the chosen license
- [ ] `REPO_POLICIES.md` — fetch from - [ ] `REPO_POLICIES.md` — fetch from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md` `https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
- [ ] Guidance for coding agents, if the repo has any, is one `AGENTS.md` at the
root — never a file or directory named after one agent tool, such as
`CLAUDE.md` or `.claude/`, and never separate memory files
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from - [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
- Extend `.dockerignore` with the repo's own host-built artifacts, giving - Extend `.dockerignore` with the repo's own host-built artifacts, giving
@@ -71,29 +68,23 @@ Template files can be fetched from:
will run them in subdirectories, `services/api/.claude/` needs its own will run them in subdirectories, `services/api/.claude/` needs its own
anchored entry. anchored entry.
- If the image embeds a version in a binary: `.dockerignore` lets `.git` - If the image embeds a version in a binary: `.dockerignore` lets `.git`
into the build context. It keeps out every git `config` at any depth into the build context. It keeps out `.git/config`, which `git describe`
(`**/.git/config`, `**/.git/modules/**/config`): the repository's own, does not need and which can hold a credential: a password in a remote URL,
each submodule's under `.git/modules/`, and that of a submodule keeping or the token the CI checkout step stores there. The stage that compiles
its own `.git` directory. `git describe` does not need them, and each can has `git` (the Debian Go image has it; an alpine one needs
hold a credential: a password in a remote URL, or the token the CI `apk add --no-cache git`) and takes the version from the `VERSION` build
checkout step stores there. A submodule whose name has a `config` segment argument when one is given, otherwise from `git describe --tags --always`.
(`config`, `deploy/config`, `config/lib`) loses its whole git directory to That gives the tag on a tagged commit; on a later commit, the tag, the
`**/.git/modules/**/config`, and Go's version stamping then fails the number of commits since it and the short commit (`v1.2.3-4-gabc1234`); and
build: give it a name without that segment (`git submodule add --name`). the short commit when no tag is reachable. The stage that compiles also
The stage that compiles has `git` (the Debian Go image has it; an alpine marks its working directory safe for git
one needs `apk add --no-cache git`) and takes the version from the (`git config --system --add safe.directory /src`): a context sent as a tar
`VERSION` build argument when one is given, otherwise from stream keeps the sender's file owners, and git refuses a checkout owned by
`git describe --tags --always`. That gives the tag on a tagged commit; on another user, so the version would come out empty. `ARG VERSION` has no
a later commit, the tag, the number of commits since it and the short default, and the build fails if the context carries `.git` and the version
commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is still comes out empty, `dev` or `unknown`. A plain `docker build .` with
reachable. The stage that compiles also marks its working directory safe no build arguments must succeed; a Dockerfile that refuses an empty build
for git (`git config --system --add safe.directory /src`): a context sent argument drops that refusal and keeps the argument.
as a tar stream keeps the sender's file owners, and git refuses a checkout
owned by another user, so the version would come out empty. `ARG VERSION`
has no default, and the build fails if the context carries `.git` and the
version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument.
- The Dockerfile carries a `lint` phase and a `test` phase, each invoking - The Dockerfile carries a `lint` phase and a `test` phase, each invoking
its tool directly rather than through `make` or `script/`, and the final its tool directly rather than through `make` or `script/`, and the final
stage carries a `COPY --from=` of a harmless file from each so the image stage carries a `COPY --from=` of a harmless file from each so the image
@@ -124,10 +115,8 @@ are thin shims calling them. Model scripts:
- [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies, - [ ] `script/bootstrap` / `make bootstrap` — installs all dependencies,
idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk; idempotently, assuming nothing (pkg manager detection nix/apt/brew/apk;
node used if present, else pinned version via nvm from a hash-verified node used if present, else pinned version via nvm from a hash-verified
archive; pinned yarn via corepack); a non-server repo's development archive; pinned yarn via corepack); Dockerfile runs it instead of inline
environment stage runs it instead of inline installs; a gate phase or the installs
build stage installs what its base image lacks either inline or by running
it
- [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`, - [ ] `script/setup` / `make setup` — readies a fresh clone: runs `bootstrap`,
then `install-precommit`, plus repo-specific init then `install-precommit`, plus repo-specific init
- [ ] `script/test` / `make test` — `docker build --no-cache --target test .`, - [ ] `script/test` / `make test` — `docker build --no-cache --target test .`,
+26 -63
View File
@@ -104,14 +104,10 @@ style conventions are in separate documents:
`lint` phase and a `test` phase, with the final stage depending on both so the `lint` phase and a `test` phase, with the final stage depending on both so the
image cannot be built unless they pass. For non-server repos the final stage image cannot be built unless they pass. For non-server repos the final stage
brings up a development environment; for server repos it is the runtime image. brings up a development environment; for server repos it is the runtime image.
The gate phases and the build stage start from their pinned base images and Dockerfiles install development prerequisites by running `script/bootstrap`
install what those images lack either inline, as the canonical Go `Dockerfile` rather than duplicating installs inline; COPY `script/` and the dependency
below does for `git`, or by running `script/bootstrap`, as the `prompts` manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before
repo's own `Dockerfile` does for its yarn packages. The development running it.
environment stage installs development prerequisites by running
`script/bootstrap` rather than duplicating its installs inline. A stage that
runs `script/bootstrap` COPYs `script/` and the dependency manifests
(`package.json` + `yarn.lock`, `go.mod` + `go.sum`, etc.) before running it.
- **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is - **Linting and testing run in Docker, as phases of the `Dockerfile`.** There is
no separate lint file. `script/lint` and `script/test` each build one phase no separate lint file. `script/lint` and `script/test` each build one phase
@@ -160,9 +156,6 @@ style conventions are in separate documents:
not evidence that anything ran: a sub-second build reporting success is a not evidence that anything ran: a sub-second build reporting success is a
cache hit, not a result. Never invalidate by pruning — `docker builder prune` cache hit, not a result. Never invalidate by pruning — `docker builder prune`
and friends destroy a build cache shared with every other build on the host. and friends destroy a build cache shared with every other build on the host.
When a check is added or changed, prove it works by planting a defect it must
catch and watching the run fail on it, then revert the defect. A green run
alone shows neither that the check ran nor that it covers what it should.
- **The gate phases are separate stages, and the build stage depends on both.** - **The gate phases are separate stages, and the build stage depends on both.**
The lint phase is based on the `golangci/golangci-lint` image (pinned by The lint phase is based on the `golangci/golangci-lint` image (pinned by
@@ -243,39 +236,23 @@ style conventions are in separate documents:
(e.g. a web frontend compiled in a separate stage), the lint phase must (e.g. a web frontend compiled in a separate stage), the lint phase must
create placeholder files so the embed directives resolve. Example: create placeholder files so the embed directives resolve. Example:
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`. `RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
- If the project requires CGO or system libraries for linting, install them - If the project requires CGO or system libraries for linting (e.g.
in the lint phase. The `golangci/golangci-lint` image is Debian-based and `vips-dev`), install them in the lint phase with `apk add`.
has no `apk`, so install with `apt-get` under the Debian package name - `.dockerignore` lets `.git` into the build context. It keeps out
(`libvips-dev`, where alpine says `vips-dev`), and delete the package `.git/config`, which `git describe` does not need and which can hold a
lists in the same `RUN`, so the layer does not keep them: credential: a password in a remote URL, or the token the CI checkout step
stores there. The stage that compiles has `git` (the Debian Go image has
```dockerfile it; an alpine one needs `apk add --no-cache git`) and takes the version
RUN apt-get update \ from the `VERSION` build argument when one is given, otherwise from
&& apt-get install -y --no-install-recommends libvips-dev \ `git describe --tags --always`. That gives the tag on a tagged commit; on
&& rm -rf /var/lib/apt/lists/* a later commit, the tag, the number of commits since it and the short
``` commit (`v1.2.3-4-gabc1234`); and the short commit when no tag is
reachable. The stage that compiles also marks its working directory safe
- `.dockerignore` lets `.git` into the build context. It keeps out every git for git (`git config --system --add safe.directory /src`): a context sent
`config` at any depth (`**/.git/config`, `**/.git/modules/**/config`): the as a tar stream keeps the sender's file owners, and git refuses a checkout
repository's own, each submodule's under `.git/modules/`, and that of a owned by another user, so the version would come out empty. `ARG VERSION`
submodule keeping its own `.git` directory. `git describe` does not need has no default, and the build fails if the context carries `.git` and the
them, and each can hold a credential: a password in a remote URL, or the version still comes out empty, `dev` or `unknown`. A plain
token the CI checkout step stores there. A submodule whose name has a
`config` segment (`config`, `deploy/config`, `config/lib`) loses its whole
git directory to `**/.git/modules/**/config`, and Go's version stamping
then fails the build: give it a name without that segment
(`git submodule add --name`). The stage that compiles has `git` (the
Debian Go image has it; an alpine one needs `apk add --no-cache git`) and
takes the version from the `VERSION` build argument when one is given,
otherwise from `git describe --tags --always`. That gives the tag on a
tagged commit; on a later commit, the tag, the number of commits since it
and the short commit (`v1.2.3-4-gabc1234`); and the short commit when no
tag is reachable. The stage that compiles also marks its working directory
safe for git (`git config --system --add safe.directory /src`): a context
sent as a tar stream keeps the sender's file owners, and git refuses a
checkout owned by another user, so the version would come out empty.
`ARG VERSION` has no default, and the build fails if the context carries
`.git` and the version still comes out empty, `dev` or `unknown`. A plain
`docker build .` with no build arguments must succeed; a Dockerfile that `docker build .` with no build arguments must succeed; a Dockerfile that
refuses an empty build argument drops that refusal and keeps the argument. refuses an empty build argument drops that refusal and keeps the argument.
@@ -286,12 +263,7 @@ style conventions are in separate documents:
carry the same guarantee, because its gate phases may come from the cache. The carry the same guarantee, because its gate phases may come from the cache. The
image build is uncached and so runs the gate phases a second time. That is the image build is uncached and so runs the gate phases a second time. That is the
price of the rule above, and it is worth paying: the image that ships is built price of the rule above, and it is worth paying: the image that ships is built
from a run of its own gates rather than from a cache entry. A separate from a run of its own gates rather than from a cache entry.
workflow limited to `main` by a `branches` list under `on: push` cannot be
checked by review: to try a change to it, add the feature branch to that list
and push, then remove the branch from the list again before merging. Keep any
job in it that publishes behind `if: github.ref_name == 'main'`, so the run
from the feature branch publishes nothing.
- Use platform-standard formatters: `black` for Python, `prettier` for - Use platform-standard formatters: `black` for Python, `prettier` for
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
@@ -522,11 +494,6 @@ style conventions are in separate documents:
Keep it POSIX sh: no arrays, no `[[`, no `grep -P`. Keep it POSIX sh: no arrays, no `[[`, no `grep -P`.
A Go tool a repo needs on the host is installed with `go install` pinned to
a commit hash (`go install <package>@<commit hash>`). It is never tracked as
a `go.mod` tool dependency or through a `tools.go` file, either of which
pulls the tool's own dependencies into the repo's `go.mod` and `go.sum`.
- When pinning images or packages by hash, add a comment above the reference - When pinning images or packages by hash, add a comment above the reference
with the version and date (YYYY-MM-DD). with the version and date (YYYY-MM-DD).
@@ -639,10 +606,10 @@ style conventions are in separate documents:
settings. settings.
- Avoid putting files in the repo root unless necessary. Root should contain - Avoid putting files in the repo root unless necessary. Root should contain
only project-level config files (`README.md`, `AGENTS.md`, `Makefile`, only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
`Dockerfile`, `LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, `LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
and language-specific config). Everything else goes in a subdirectory. language-specific config). Everything else goes in a subdirectory. Canonical
Canonical subdirectory names: subdirectory names:
- `bin/` — executable scripts and tools - `bin/` — executable scripts and tools
- `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose - `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose
body is a single call into `internal/` or `pkg/`, no project logic in body is a single call into `internal/` or `pkg/`, no project logic in
@@ -673,7 +640,3 @@ style conventions are in separate documents:
- Go: `go.mod`, `go.sum`, `.golangci.yml` - Go: `go.mod`, `go.sum`, `.golangci.yml`
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore` - JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
- Python: `pyproject.toml` - Python: `pyproject.toml`
- Guidance for coding agents lives in one `AGENTS.md` at the repository root. It
is never committed under a file or directory named after one agent tool, such
as `CLAUDE.md` or `.claude/`, and never split into separate memory files.