Files
at-container-registry/Makefile
T
Evan JarrettandClaude Opus 5 4b9d4bcbeb billing: retry a failed customer lookup instead of dropping the subscription
getCustomerDID returned "" for a FAILED customer.Get exactly as it does for a
customer carrying no user_did. handleSubscriptionChange read that as "not our
customer" and returned nil, so HandleWebhook recorded the event as processed
and answered 200. Stripe never redelivered. A transient Stripe API error
therefore dropped a paid upgrade permanently — the precise "paid but never
received tier" hole 12c55ed was written to close, left open one level down.

It now returns (string, error) so the two cases are distinguishable, and only
the subscription path propagates it. The invoice-failed and dispute handlers
use the DID for logging alone, so a lookup failure there is not worth failing a
webhook over and they ignore it deliberately.

Also fixed: handleSubscriptionChange dereferenced sub.Customer.ID four lines
after an ordering guard that explicitly checks sub.Customer != nil. Confirmed a
real panic, not a theoretical one — the new test panics against the old code.

Five tests, all mutation-verified, and all of them new ground: pkg/billing had
one test file and test/stripe-integration builds its manager with a nil
database, so every `m.db != nil` branch — which is all of the idempotency and
ordering work — was dead there. These use a real database.

The idempotency and ordering tests assert on whether Stripe was CALLED again,
not on row counts. That distinction matters: RecordStripeEvent is an idempotent
upsert, so deleting either guard outright leaves the table looking identical
and a row-count assertion passes. Counting API calls is the only thing that
separates "short-circuited" from "re-applied". My first draft got this wrong
and passed against both mutations.

Makefile: `make test` never ran any of this. The only -tags billing in the file
was a build line, so gate_test.go and checkout_gate_test.go had never executed
in the default target or in CI, and `make lint` never linted the package
either. Both now do; both are clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
2026-08-25 16:34:26 -05:00

215 lines
8.8 KiB
Makefile

# ATCR Makefile
# Build targets for the ATProto Container Registry
.PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \
build-trixie \
generate test test-billing test-race test-verbose integration-test stripe-integration-test \
lint lex-lint clean help install-credential-helper \
develop develop-detached develop-down dev \
docker docker-appview docker-hold docker-scanner
.DEFAULT_GOAL := help
help: ## Show this help message
@echo "ATCR Build Targets:"
@echo ""
@awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-28s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
all: generate build ## Generate assets and build all binaries (default)
# Generated asset files
GENERATED_ASSETS = \
pkg/appview/public/js/htmx.min.js \
pkg/appview/public/js/lucide.min.js \
pkg/appview/licenses/spdx-licenses.json
generate: ## Run go generate ./... (always — regenerates cbor_gen, icon sprites, vendor assets)
@echo "→ Running go generate ./..."
go generate ./...
# File rule: lazily download missing vendor assets for fast incremental local builds.
# Production builds depend on the phony `generate` target instead so generated code
# (cbor_gen.go, icon sprites, etc.) is always up to date.
$(GENERATED_ASSETS):
@echo "→ Generating vendor assets and code..."
go generate ./...
##@ Build Targets
build: build-appview build-hold build-credential-helper ## Build all binaries
# Legal page "Last updated" dates come from the git commit date of the page
# templates. Empty values (e.g., Docker builds without .git) fall back to the
# hardcoded default in legal.go.
LEGAL_PKG := atcr.io/pkg/appview/handlers
PRIVACY_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/privacy.html 2>/dev/null)
TERMS_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/terms.html 2>/dev/null)
APPVIEW_LDFLAGS := -X '$(LEGAL_PKG).privacyLastUpdated=$(PRIVACY_DATE)' -X '$(LEGAL_PKG).termsLastUpdated=$(TERMS_DATE)'
build-appview: $(GENERATED_ASSETS) ## Build appview binary only
@echo "→ Building appview..."
@mkdir -p bin
go build -ldflags="$(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview
build-hold: $(GENERATED_ASSETS) ## Build hold binary only
@echo "→ Building hold..."
@mkdir -p bin
go build -o bin/atcr-hold ./cmd/hold
build-credential-helper: ## Build credential helper only (atcr brand)
@echo "→ Building credential helper..."
@mkdir -p bin
cd cmd/credential-helper/atcr && go build -ldflags="-X main.version=$(shell git describe --tags --always 2>/dev/null || echo dev) -X main.commit=$(shell git rev-parse HEAD 2>/dev/null || echo none)" -o ../../../bin/docker-credential-atcr .
build-oauth-helper: ## Build OAuth helper only
@echo "→ Building OAuth helper..."
@mkdir -p bin
go build -o bin/oauth-helper ./cmd/oauth-helper
# Trixie cross-build (Debian 13, glibc 2.41) — produces binaries that run on
# any glibc ≥ 2.41 target, even when the host glibc is newer (e.g. Fedora's
# 2.43, which otherwise stamps sqrtf@GLIBC_2.43 onto cgo-linked output).
TRIXIE_BUILDER_IMAGE ?= golang:1-trixie
build-trixie: generate ## Build all production binaries (appview, hold, credential-helper, scanner, labeler) for linux/amd64 in a Debian 13 (glibc 2.41) container
@echo "→ Building in $(TRIXIE_BUILDER_IMAGE) for glibc 2.41 compatibility..."
@mkdir -p bin
docker run --rm \
--user $$(id -u):$$(id -g) \
-v "$(CURDIR)":/src \
-w /src \
-e HOME=/tmp \
-e GOCACHE=/tmp/.gocache \
-e GOMODCACHE=/tmp/.gomodcache \
-e CGO_ENABLED=1 \
-e GOOS=linux \
-e GOARCH=amd64 \
$(TRIXIE_BUILDER_IMAGE) \
bash -c '\
set -e && \
go build -trimpath -tags billing -ldflags="-s -w $(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview && \
go build -trimpath -ldflags="-s -w" -o bin/atcr-hold ./cmd/hold && \
(cd cmd/credential-helper/atcr && go build -trimpath -ldflags="-s -w" -o ../../../bin/docker-credential-atcr .) && \
go build -trimpath -ldflags="-s -w" -o bin/atcr-labeler ./cmd/labeler && \
cd scanner && go build -trimpath -ldflags="-s -w" -o ../bin/atcr-scanner ./cmd/scanner'
@echo "✓ Built to bin/ (glibc ≥ 2.41 compatible)"
##@ Test Targets
test: test-billing ## Run all tests
@echo "→ Running tests..."
go test -cover ./...
# pkg/billing is behind the `billing` build tag, so `go test ./...` never
# compiles it, let alone runs it. Its tests covered the money path and had
# never executed in this target or in CI.
test-billing: ## Run the billing-tagged tests (skipped by plain `go test ./...`)
@echo "→ Running billing-tagged tests..."
go test -tags billing -cover ./pkg/billing/...
test-race: ## Run tests with race detector
@echo "→ Running tests with race detector..."
go test -race ./...
test-verbose: ## Run tests with verbose output
@echo "→ Running tests with verbose output..."
go test -v ./...
integration-test: ## Run in-process smoke test (no docker, fake PDS + gofakes3 + hold + appview)
@echo "→ Running integration smoke test..."
go test -tags=integration -count=1 -race -timeout=120s ./test/integration/...
stripe-integration-test: ## Run Stripe sandbox-backed billing tests (needs STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY)
@echo "→ Running Stripe sandbox integration tests..."
@echo " Required env: STRIPE_SECRET_KEY (sk_test_...), STRIPE_WEBHOOK_SECRET (whsec_...),"
@echo " STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY"
@echo " Optional env: STRIPE_TEST_TIER_NAME (default 'Supporter'),"
@echo " STRIPE_TEST_EXISTING_CUSTOMER_DID (skips portal search-lag wait)"
go test -tags="billing stripe_integration" -count=1 -timeout=180s ./test/stripe-integration/...
##@ Quality Targets
.PHONY: check-golangci-lint
check-golangci-lint:
@LINT_PKG=github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \
CUR_GO=$$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1 | sed 's/^go//'); \
if ! command -v golangci-lint > /dev/null 2>&1; then \
echo "→ Installing golangci-lint..."; \
go install $$LINT_PKG; \
else \
LINT_GO=$$(golangci-lint --version 2>&1 | grep -oE 'built with go[0-9]+\.[0-9]+' | head -1 | sed 's/^built with go//'); \
if [ -n "$$LINT_GO" ] && [ "$$LINT_GO" != "$$CUR_GO" ] && \
[ "$$(printf '%s\n%s\n' $$LINT_GO $$CUR_GO | sort -V | head -1)" = "$$LINT_GO" ]; then \
echo "→ golangci-lint built with go$$LINT_GO but project targets go$$CUR_GO — reinstalling..."; \
go install $$LINT_PKG; \
fi; \
fi
lint: check-golangci-lint ## Run golangci-lint
@echo "→ Running golangci-lint..."
golangci-lint run ./...
@echo "→ Running golangci-lint (billing tag)..."
golangci-lint run --build-tags=billing ./pkg/billing/...
lex-lint: ## Lint ATProto lexicon schemas
goat lex lint ./lexicons/
##@ Install Targets
install-credential-helper: build-credential-helper ## Install credential helper to /usr/local/sbin
@echo "→ Installing credential helper to /usr/local/sbin..."
install -m 755 bin/docker-credential-atcr /usr/local/sbin/docker-credential-atcr
@echo "✓ Installed docker-credential-atcr to /usr/local/sbin/"
##@ Development Targets
dev: $(GENERATED_ASSETS) ## Run AppView locally with Air hot reload
@which air > /dev/null || (echo "→ Installing Air..." && go install github.com/air-verse/air@latest)
air -c .air.toml
##@ Docker Targets
docker: docker-appview docker-hold docker-scanner ## Build all Docker images
docker-appview: ## Build appview Docker image
@echo "→ Building appview Docker image..."
docker build -f Dockerfile.appview \
--build-arg PRIVACY_DATE=$(PRIVACY_DATE) \
--build-arg TERMS_DATE=$(TERMS_DATE) \
-t atcr.io/atcr.io/appview:latest .
docker-hold: ## Build hold Docker image
@echo "→ Building hold Docker image..."
docker build -f Dockerfile.hold -t atcr.io/atcr.io/hold:latest .
docker-scanner: ## Build scanner Docker image
@echo "→ Building scanner Docker image..."
docker build -f Dockerfile.scanner -t atcr.io/atcr.io/scanner:latest .
develop: ## Build and start docker-compose with Air hot reload
@echo "→ Building Docker images..."
docker-compose build
@echo "→ Starting docker-compose with hot reload..."
docker-compose up
develop-detached: ## Build and start docker-compose with hot reload (detached)
@echo "→ Building Docker images..."
docker-compose build
@echo "→ Starting docker-compose with hot reload (detached)..."
docker-compose up -d
@echo "✓ Services started in background with hot reload"
@echo " AppView: http://localhost:5000"
@echo " Hold: http://localhost:8080"
develop-down: ## Stop docker-compose services
@echo "→ Stopping docker-compose..."
docker-compose down
##@ Utility Targets
clean: ## Remove built binaries and generated assets
@echo "→ Cleaning build artifacts..."
rm -rf bin/
rm -f pkg/appview/licenses/spdx-licenses.json
@echo "✓ Clean complete"