Compare commits

...

4 Commits

Author SHA1 Message Date
2d6ca4b815 trigger CI
All checks were successful
check / check (push) Successful in 1m29s
2026-03-03 06:02:57 +01:00
4482529f6a Split Dockerfile: pre-built golangci-lint stage for faster CI (#26)
Closes [#22](#22)

## Changes

### Makefile
- Added `fmt-check` target: checks gofmt formatting without modifying files
- Added `hooks` target: installs pre-commit git hook
- Updated `check` target: now runs `fmt-check lint test`
- Removed redundant gofmt check from `lint` target (now in `fmt-check`)
- Added `.PHONY` declarations for all phony targets
- Updated `tools` target to use `go install`

### Dockerfile
- **Lint stage**: Uses pre-built `golangci/golangci-lint:v1.64.8` (sha256-pinned)
  - Runs `make fmt-check` and `make lint` for fast feedback
- **Build stage**: Uses `golang:1.24-bookworm` (sha256-pinned, matches go.mod 1.24.0)
  - `COPY --from=lint` forces BuildKit to actually run the lint stage
  - Runs `make test` then `make build`
- **Runtime stage**: Uses `debian:bookworm-slim` (sha256-pinned)
- All base images updated from ancient/unpinned versions to current sha256-pinned images
- Removed vendoring/source tarball per CLAUDE.md policy

### CI
- Added `.gitea/workflows/check.yml`: runs `docker build .` on push to main and PRs

## Image Versions
| Stage | Image | Digest |
|-------|-------|--------|
| lint | golangci/golangci-lint:v1.64.8 | sha256:2987913e...5cb8 |
| build | golang:1.24-bookworm | sha256:1a6d4452...77ac |
| runtime | debian:bookworm-slim | sha256:74d56e39...4421 |

## Verification
`docker build .` passes locally — all stages (lint, test, build) execute correctly.

<!-- session: agent:sdlc-manager:subagent:bcf4d5ff-f487-4dcb-aa85-1c0e039bbb3b -->

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #26
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-02 21:11:17 +01:00
0f53e8f659 instructions for LLMs 2026-01-11 04:05:39 -08:00
4d746027dc remove Buildarch from globals - is available at runtime 2026-01-11 04:05:11 -08:00
9 changed files with 154 additions and 65 deletions

View File

@@ -0,0 +1,12 @@
name: check
on:
push:
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: docker build .

80
CLAUDE.md Normal file
View File

@@ -0,0 +1,80 @@
# Repository Rules
Last Updated 2026-01-10
These rules MUST be followed at all times, it is very important.
* Do NOT stop working while there are still incomplete TODOs in the todo list
or in TODO.md. Continue implementing until all tasks are complete or you are
explicitly told to stop.
* Never use `git add -A` - add specific changes to a deliberate commit. A
commit should contain one change. After each change, make a commit with a
good one-line summary.
* NEVER modify the linter config without asking first.
* NEVER modify tests to exclude special cases or otherwise get them to pass
without asking first. In almost all cases, the code should be changed,
NOT the tests. If you think the test needs to be changed, make your case
for that and ask for permission to proceed, then stop. You need explicit
user approval to modify existing tests. (You do not need user approval
for writing NEW tests.)
* When linting, assume the linter config is CORRECT, and that each item
output by the linter is something that legitimately needs fixing in the
code.
* When running tests, use `make test`.
* Before commits, run `make check`. This runs `make lint` and `make test`
and `make check-fmt`. Any issues discovered MUST be resolved before
committing unless explicitly told otherwise.
* When fixing a bug, write a failing test for the bug FIRST. Add
appropriate logging to the test to ensure it is written correctly. Commit
that. Then go about fixing the bug until the test passes (without
modifying the test further). Then commit that.
* When adding a new feature, do the same - implement a test first (TDD). It
doesn't have to be super complex. Commit the test, then commit the
feature.
* When adding a new feature, use a feature branch. When the feature is
completely finished and the code is up to standards (passes `make check`)
then and only then can the feature branch be merged into `main` and the
branch deleted.
* Write godoc documentation comments for all exported types and functions as
you go along.
* ALWAYS be consistent in naming. If you name something one thing in one
place, name it the EXACT SAME THING in another place.
* Be descriptive and specific in naming. `wl` is bad;
`SourceHostWhitelist` is good. `ConnsPerHost` is bad;
`MaxConnectionsPerHost` is good.
* This is not prototype or teaching code - this is designed for production.
Any security issues (such as denial of service) or other web
vulnerabilities are P1 bugs and must be added to TODO.md at the top.
* As this is production code, no stubbing of implementations unless
specifically instructed. We need working implementations.
* NEVER silently fall back to a different setting when a user's parameter
explicitly specifies a value. If a user requests format=webp and WebP
encoding is not supported, return an error - do NOT silently output PNG
instead. If a user specifies fit=invalid and that fit mode doesn't exist,
return an error - do NOT silently default to "cover". Silent fallbacks
violate the principle of least surprise and mask bugs. The only acceptable
defaults are for OMITTED parameters, never for INVALID explicit values.
* Avoid vendoring deps unless specifically instructed to. NEVER commit
the vendor directory, NEVER commit compiled binaries. If these
directories or files exist, add them to .gitignore (and commit the
.gitignore) if they are not already in there. Keep the entire git
repository (with history) small - under 20MiB, unless you specifically
must commit larger files (e.g. test fixture example media files). Only
OUR source code and immediately supporting files (such as test examples)
goes into the repo/history.

View File

@@ -114,13 +114,11 @@ import (
var ( var (
Appname string = "CHANGEME" Appname string = "CHANGEME"
Version string Version string
Buildarch string
) )
func main() { func main() {
globals.Appname = Appname globals.Appname = Appname
globals.Version = Version globals.Version = Version
globals.Buildarch = Buildarch
fx.New( fx.New(
fx.Provide( fx.Provide(
@@ -453,6 +451,11 @@ func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
### Closure-Based Handler Pattern ### Closure-Based Handler Pattern
For JSON route handlers, both the request and the response structures are
defined in the scope of the method that returns the HandlerFunc. They can
be called simply `Request` and `Response` or slightly more descriptive
names.
All handlers return `http.HandlerFunc` using the closure pattern. This allows initialization logic to run once when the handler is created: All handlers return `http.HandlerFunc` using the closure pattern. This allows initialization logic to run once when the handler is created:
```go ```go
@@ -477,6 +480,11 @@ func (s *Handlers) HandleIndex() http.HandlerFunc {
```go ```go
// internal/handlers/now.go // internal/handlers/now.go
func (s *Handlers) HandleNow() http.HandlerFunc { func (s *Handlers) HandleNow() http.HandlerFunc {
type request struct {
// request format
}
// Response struct defined in closure scope // Response struct defined in closure scope
type response struct { type response struct {
Now time.Time `json:"now"` Now time.Time `json:"now"`
@@ -816,7 +824,6 @@ func (l *Logger) Identify() {
l.log.Info("starting", l.log.Info("starting",
"appname", l.params.Globals.Appname, "appname", l.params.Globals.Appname,
"version", l.params.Globals.Version, "version", l.params.Globals.Version,
"buildarch", l.params.Globals.Buildarch,
) )
} }
``` ```
@@ -938,20 +945,17 @@ import "go.uber.org/fx"
var ( var (
Appname string Appname string
Version string Version string
Buildarch string
) )
// Struct for DI // Struct for DI
type Globals struct { type Globals struct {
Appname string Appname string
Version string Version string
Buildarch string
} }
func New(lc fx.Lifecycle) (*Globals, error) { func New(lc fx.Lifecycle) (*Globals, error) {
n := &Globals{ n := &Globals{
Appname: Appname, Appname: Appname,
Buildarch: Buildarch,
Version: Version, Version: Version,
} }
return n, nil return n, nil
@@ -965,13 +969,11 @@ func New(lc fx.Lifecycle) (*Globals, error) {
var ( var (
Appname string = "CHANGEME" // Default, overridden by build Appname string = "CHANGEME" // Default, overridden by build
Version string // Set at build time Version string // Set at build time
Buildarch string // Set at build time
) )
func main() { func main() {
globals.Appname = Appname globals.Appname = Appname
globals.Version = Version globals.Version = Version
globals.Buildarch = Buildarch
// ... // ...
} }
``` ```
@@ -982,10 +984,9 @@ Use ldflags to inject version information at build time:
```makefile ```makefile
VERSION := $(shell git describe --tags --always) VERSION := $(shell git describe --tags --always)
BUILDARCH := $(shell go env GOARCH)
build: build:
go build -ldflags "-X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)" ./cmd/httpd go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd
``` ```
--- ---

View File

@@ -1,41 +1,35 @@
## lint image # Lint stage — fast feedback
FROM golangci/golangci-lint:v1.50.1 # golangci/golangci-lint:v1.64.8 (2025-03-17)
FROM golangci/golangci-lint@sha256:2987913e27f4eca9c8a39129d2c7bc1e74fbcf77f181e01cea607be437aa5cb8 AS lint
RUN mkdir -p /build WORKDIR /src
WORKDIR /build COPY go.mod go.sum ./
COPY ./ ./
RUN golangci-lint run
## build image:
FROM golang:1.19.3-bullseye AS builder
RUN apt update && apt install -y make bzip2
RUN mkdir -p /build
WORKDIR /build
COPY go.mod .
COPY go.sum .
RUN go mod download RUN go mod download
COPY . .
RUN make fmt-check
RUN make lint
COPY ./ ./ # Build stage
#RUN make lint # golang:1.24-bookworm (Go 1.24)
RUN make httpd && mv ./httpd /httpd FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS builder
RUN go mod vendor # Force BuildKit to run the lint stage
RUN tar -c . | bzip2 > /src.tbz2 COPY --from=lint /src/go.sum /dev/null
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN make test
RUN make build && cp ./httpd /httpd
## output image: # Runtime stage
FROM debian:bullseye-slim AS final # debian:bookworm-slim (2025-03)
FROM debian@sha256:74d56e3931e0d5a1dd51f8c8a2466d21de84a271cd3b5a733b803aa91abf4421 AS final
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /httpd /app/httpd COPY --from=builder /httpd /app/httpd
COPY --from=builder /src.tbz2 /usr/local/src/src.tbz2
WORKDIR /app WORKDIR /app
ENV HOME /app ENV HOME=/app
ENV PORT=8080
ENV PORT 8080 ENV DBURL=none
ENV DBURL none
EXPOSE 8080 EXPOSE 8080

View File

@@ -4,7 +4,6 @@ VERSION := $(shell git describe --always --dirty=-dirty)
ARCH := $(shell uname -m) ARCH := $(shell uname -m)
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Version=$(VERSION)
GOLDFLAGS += -X main.Buildarch=$(ARCH)
GOFLAGS := -ldflags "$(GOLDFLAGS)" GOFLAGS := -ldflags "$(GOLDFLAGS)"
default: clean debug default: clean debug
@@ -12,22 +11,30 @@ default: clean debug
commit: fmt lint commit: fmt lint
git commit -a git commit -a
# get golangci-lint with: # get gofumpt with:
# go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0 # go install mvdan.cc/gofumpt@latest
# get gofumports with:
# go get mvdan.cc/gofumpt/gofumports
fmt: fmt:
gofumpt -l -w . gofumpt -l -w .
golangci-lint run --fix golangci-lint run --fix
fmt-check:
@test -z "$$(gofmt -l .)" || { echo "gofmt found unformatted files:"; gofmt -l .; exit 1; }
lint: lint:
golangci-lint run golangci-lint run
sh -c 'test -z "$$(gofmt -l .)"'
test: test:
go test ./... go test ./...
check: lint test check: fmt-check lint test
build: ./$(FN)d
hooks:
@mkdir -p .git/hooks
@printf '#!/bin/sh\nmake fmt-check lint\n' > .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit
@echo "Pre-commit hook installed."
debug: ./$(FN)d debug: ./$(FN)d
DEBUG=1 GOTRACEBACK=all ./$(FN)d DEBUG=1 GOTRACEBACK=all ./$(FN)d
@@ -49,5 +56,6 @@ docker:
go build -o ../../$(FN)d $(GOFLAGS) . go build -o ../../$(FN)d $(GOFLAGS) .
tools: tools:
go get -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0 go install mvdan.cc/gofumpt@latest
go get -v mvdan.cc/gofumpt/gofumports
.PHONY: default commit fmt fmt-check lint test check build hooks debug debugger run clean docker tools

View File

@@ -86,3 +86,4 @@ WTFPL (aka public domain):
0. You just DO WHAT THE FUCK YOU WANT TO. 0. You just DO WHAT THE FUCK YOU WANT TO.
``` ```

View File

@@ -15,13 +15,11 @@ import (
var ( var (
Appname string = "CHANGEME" Appname string = "CHANGEME"
Version string Version string
Buildarch string
) )
func main() { func main() {
globals.Appname = Appname globals.Appname = Appname
globals.Version = Version globals.Version = Version
globals.Buildarch = Buildarch
fx.New( fx.New(
fx.Provide( fx.Provide(
@@ -36,5 +34,4 @@ func main() {
), ),
fx.Invoke(func(*server.Server) {}), fx.Invoke(func(*server.Server) {}),
).Run() ).Run()
// os.Exit(server.Run(Appname, Version, Buildarch))
} }

View File

@@ -8,19 +8,16 @@ import (
var ( var (
Appname string Appname string
Version string Version string
Buildarch string
) )
type Globals struct { type Globals struct {
Appname string Appname string
Version string Version string
Buildarch string
} }
func New(lc fx.Lifecycle) (*Globals, error) { func New(lc fx.Lifecycle) (*Globals, error) {
n := &Globals{ n := &Globals{
Appname: Appname, Appname: Appname,
Buildarch: Buildarch,
Version: Version, Version: Version,
} }
return n, nil return n, nil

View File

@@ -64,6 +64,5 @@ func (l *Logger) Identify() {
l.log.Info("starting", l.log.Info("starting",
"appname", l.params.Globals.Appname, "appname", l.params.Globals.Appname,
"version", l.params.Globals.Version, "version", l.params.Globals.Version,
"buildarch", l.params.Globals.Buildarch,
) )
} }