Makefile
Rama main · réplica de lectura del repositorio original.
.PHONY: swagger-clean
swagger-clean:
rm -rf docs/swagger
# Swag v2 pin. Use `go run` for generation so CI never runs a random host `swag` from PATH while
# expecting $(GOPATH)/bin/swag (install-swag skipped when `which swag` succeeds).
SWAG_V2_PKG := github.com/swaggo/swag/v2/cmd/swag@v2.0.0-rc5
.PHONY: install-swag
install-swag:
go install $(SWAG_V2_PKG)
.PHONY: swagger
swagger: swagger-2-0 swagger-3-0
.PHONY: swagger-2-0-generate
swagger-2-0-generate:
@echo "go mod download (warm cache; swag v2 treats go list stderr as fatal)..."
go mod download
@echo "Running swag via go run $(SWAG_V2_PKG) ..."
go run $(SWAG_V2_PKG) init \
--generalInfo cmd/server/main.go \
--dir . \
--parseDependency \
--parseInternal \
--output docs/swagger \
--generatedTime=false \
--parseDepth 1 \
--instanceName swagger \
--parseVendor \
--outputTypes go,json,yaml
.PHONY: swagger-2-0-node
swagger-2-0-node:
node scripts/fix_swagger_internal_types.mjs
.PHONY: swagger-2-0
swagger-2-0: swagger-2-0-generate swagger-fix-refs swagger-2-0-node
.PHONY: swagger-3-0
swagger-3-0:
@echo "Converting Swagger 2.0 to OpenAPI 3.0..."
@curl -X 'POST' \
'https://converter.swagger.io/api/convert' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d @docs/swagger/swagger.json > docs/swagger/swagger-3-0.json
@echo "Conversion complete. Output saved to docs/swagger/swagger-3-0.json"
@node scripts/fix_swagger_internal_types.mjs
@./scripts/update_swagger_servers.sh
.PHONY: swagger-fix-refs
swagger-fix-refs:
@./scripts/fix_swagger_refs.sh
.PHONY: up
up:
docker compose up -d --build
.PHONY: down
down:
docker compose down
.PHONY: run-server
run-server:
go run cmd/server/main.go
.PHONY: run-e2eprobe
run-e2eprobe:
go run cmd/e2eprobe/main.go
E2EPROBE_IMAGE ?= flexprice/e2eprobe
# Single-platform build loaded into the local Docker engine for testing.
# (buildx cannot --load a multi-arch manifest into the default image store.)
.PHONY: e2eprobe-docker-build
e2eprobe-docker-build:
docker buildx build --load \
-f Dockerfile.e2eprobe -t $(E2EPROBE_IMAGE) .
# Multi-arch build pushed to a registry. Override the tag with
# E2EPROBE_IMAGE=<registry>/e2eprobe:<tag>. buildx discards a multi-platform
# result unless it is pushed, so --push is required here.
.PHONY: e2eprobe-docker-push
e2eprobe-docker-push:
docker buildx build --push --platform linux/amd64,linux/arm64 \
-f Dockerfile.e2eprobe -t $(E2EPROBE_IMAGE) .
.PHONY: run-server-local
run-server-local: run-server
.PHONY: run
run: run-server
.PHONY: dev-token
dev-token:
@tenant_id=''; \
while [ -z "$$tenant_id" ]; do \
printf 'Tenant ID: '; \
if ! IFS= read -r tenant_id; then \
printf '\nUnable to read tenant ID.\n'; \
exit 1; \
fi; \
if [ -z "$$tenant_id" ]; then \
echo 'Tenant ID is required.'; \
fi; \
done; \
printf 'Environment ID (optional): '; \
if ! IFS= read -r environment_id; then \
printf '\nUnable to read environment ID.\n'; \
exit 1; \
fi; \
go run ./scripts -cmd generate-dev-token -tenant-id "$$tenant_id" -environment-id "$$environment_id"
# ---------------------------------------------------------------------------
# Local development targets — load .env.local on top of .env so local Docker
# infra overrides take effect without touching production config.
# ---------------------------------------------------------------------------
# Run API server locally (loads .env then .env.local)
.PHONY: run-local-api
run-local-api:
@set -a && [ -f .env ] && . ./.env; [ -f .env.local ] && . ./.env.local; set +a; \
FLEXPRICE_DEPLOYMENT_MODE=api go run cmd/server/main.go
# Run Kafka consumer locally (loads .env then .env.local)
.PHONY: run-local-consumer
run-local-consumer:
@set -a && [ -f .env ] && . ./.env; [ -f .env.local ] && . ./.env.local; set +a; \
FLEXPRICE_DEPLOYMENT_MODE=consumer go run cmd/server/main.go
# Run all services in a single process locally (loads .env then .env.local)
.PHONY: run-local
run-local:
@set -a && [ -f .env ] && . ./.env; [ -f .env.local ] && . ./.env.local; set +a; \
FLEXPRICE_DEPLOYMENT_MODE=local go run cmd/server/main.go
# Run the admin portal API only (loads .env then .env.local)
.PHONY: run-local-admin
run-local-admin:
@set -a && [ -f .env ] && . ./.env; [ -f .env.local ] && . ./.env.local; set +a; \
FLEXPRICE_DEPLOYMENT_MODE=admin go run cmd/server/main.go
# ---------------------------------------------------------------------------
# Worktree-aware server — deterministic port from branch name, shared .env
# ---------------------------------------------------------------------------
# Run server on a port derived from the current branch name (8080 for main,
# 8100-8899 for other branches). Loads .env from the main worktree so there
# is only ever one .env file regardless of how many worktrees are active.
.PHONY: run-wt
run-wt:
@export PATH="/usr/local/go/bin:$$HOME/.local/bin:$$PATH"; \
BRANCH=$$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached); \
if [ "$$BRANCH" = "main" ] || [ "$$BRANCH" = "master" ]; then PORT=8080; \
else PORT=$$((8100 + $$(printf '%s' "$$BRANCH" | cksum | awk '{print $$1}') % 800)); fi; \
MAIN_WT=$$(git worktree list --porcelain | awk '/^worktree /{print $$2; exit}'); \
ENV_FILE="$$MAIN_WT/.env"; \
REPO_ROOT=$$(git rev-parse --show-toplevel); \
printf '\n+---------------------------------------------+\n'; \
printf '| branch : %-34s|\n' "$$BRANCH"; \
printf '| port : %-34s|\n' "$$PORT"; \
printf '| url : %-34s|\n' "http://localhost:$$PORT"; \
printf '| .env : %-34s|\n' "$$ENV_FILE"; \
printf '+---------------------------------------------+\n\n'; \
[ -f "$$ENV_FILE" ] || { printf 'ERROR: .env not found at %s\n' "$$ENV_FILE" >&2; exit 1; }; \
set -a && . "$$ENV_FILE" && set +a; \
export FLEXPRICE_SERVER_ADDRESS=":$$PORT"; \
exec go run "$$REPO_ROOT/cmd/server/main.go"
# Show all worktrees, their computed ports, and whether a server is running.
# Calls scripts/wt-ports.sh from the main worktree (works before and after commit).
.PHONY: wt-ports
wt-ports:
@MAIN_WT=$$(git worktree list --porcelain | awk '/^worktree /{print $$2; exit}'); \
bash "$$MAIN_WT/scripts/wt-ports.sh"
# Run Ent schema migrations against local Docker postgres
.PHONY: migrate-local
migrate-local:
@set -a && [ -f .env.local ] && . ./.env.local; set +a; \
go run ./cmd/migrate postgres
.PHONY: test test-verbose test-coverage
# Run go test, stream output, then print a short failure summary at the end.
# Usage: $(call run-go-test,<go test args...>)
define run-go-test
@bash -c 'set -o pipefail; \
tmp=$$(mktemp -t flexprice-test.XXXXXX); \
go test $(1) 2>&1 | tee "$$tmp"; \
status=$$?; \
echo ""; \
echo "======== FAILED TESTS ========"; \
grep -E "^--- FAIL:" "$$tmp" || echo "(none)"; \
echo "======== FAILED PACKAGES ====="; \
grep -E "^FAIL[[:space:]]" "$$tmp" || echo "(none)"; \
rm -f "$$tmp"; \
exit $$status'
endef
# Run all tests
test: install-typst
$(call run-go-test,-v -race ./internal/... ./cmd/server)
# Run tests with verbose output
test-verbose:
$(call run-go-test,-v ./internal/... ./cmd/server)
# Run tests with coverage report
test-coverage: install-typst
$(call run-go-test,-race -coverprofile=coverage.out ./internal/... ./cmd/server)
go tool cover -html=coverage.out -o coverage.html
# Database related targets
.PHONY: init-db migrate-postgres migrate-clickhouse seed-db migrate-ent
.PHONY: install-ent
install-ent:
@which ent > /dev/null || (go install entgo.io/ent/cmd/ent@latest)
.PHONY: generate-ent
generate-ent: install-ent
@echo "Generating ent code..."
@go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/execquery --template ./ent/template ./ent/schema
.PHONY: migrate-ent
migrate-ent:
@echo "Running Ent migrations..."
@go run ./cmd/migrate postgres --timeout 300
@echo "Ent migrations complete"
.PHONY: migrate-ent-dry-run
migrate-ent-dry-run:
@echo "Generating SQL migration statements (dry run)..."
@go run ./cmd/migrate postgres --dry-run --timeout 300
@echo "SQL migration statements generated"
# ── Versioned migrations (dbmate) ────────────────────────────────────────────
# Local scratch Postgres used by the checks below:
# docker run -d --name mig-pg -e POSTGRES_USER=flexprice \
# -e POSTGRES_PASSWORD=flexprice123 -e POSTGRES_DB=postgres \
# -p 5440:5432 postgres:16
MIGRATIONS_PG ?= migrations/versioned/postgres
MIGRATIONS_CH ?= migrations/versioned/clickhouse
.PHONY: migrate-up
migrate-up:
@./scripts/migrations/apply.sh $(MIGRATIONS_PG)
.PHONY: migrate-status
migrate-status:
@dbmate --migrations-dir $(MIGRATIONS_PG) status
.PHONY: migrate-new
migrate-new:
@test -n "$(name)" || (echo "usage: make migrate-new name=add_currency"; exit 1)
@dbmate --migrations-dir $(MIGRATIONS_PG) new $(name)
.PHONY: migrate-generate
migrate-generate:
@test -n "$(name)" || (echo "usage: make migrate-generate name=add_currency"; exit 1)
@./scripts/migrations/generate.sh $(name)
.PHONY: migrate-adopt
migrate-adopt:
@test -n "$(url)" || \
(echo "usage: make migrate-adopt url=postgres://... [version=head] [dry=1] [reference=postgres://...]"; exit 1)
@./scripts/migrations/adopt.sh "$(url)" $(MIGRATIONS_PG) $(or $(version),head) \
$(if $(reference),--reference "$(reference)",) \
$(if $(dry),--dry-run,)
.PHONY: migrate-fingerprint
migrate-fingerprint:
@test -n "$(url)" || (echo "usage: make migrate-fingerprint url=postgres://..."; exit 1)
@./scripts/migrations/fingerprint.sh "$(url)"
# ── CI gates ─────────────────────────────────────────────────────────────────
.PHONY: migrate-check
# migrate-check-clickhouse is deliberately NOT in this list — see the note in
# .github/workflows/migrations.yml. Run it by hand if you need it.
migrate-check: migrate-check-sync migrate-check-checksum migrate-check-order
@echo "all migration checks passed"
.PHONY: migrate-check-sync
migrate-check-sync:
@./scripts/migrations/synccheck.sh $(MIGRATIONS_PG)
.PHONY: migrate-check-checksum
migrate-check-checksum:
@./scripts/migrations/checksum-check.sh
.PHONY: migrate-check-order
migrate-check-order:
@./scripts/migrations/order-check.sh $(MIGRATIONS_PG)
.PHONY: migrate-check-clickhouse
migrate-check-clickhouse:
@./scripts/migrations/clickhouse-check.sh $(MIGRATIONS_CH)
.PHONY: generate-migration
generate-migration:
@echo "Generating SQL migration file..."
@mkdir -p migrations/ent
@go run ./cmd/migrate postgres --dry-run --timeout 300 > migrations/ent/migration_$(shell date +%Y%m%d%H%M%S).sql
@echo "SQL migration file generated in migrations/ent/"
# Initialize databases and required topics
init-db: up migrate-postgres migrate-clickhouse generate-ent migrate-ent seed-db
@echo "Database initialization complete"
# One-off: seed Svix event-types on a new/self-hosted env.
# Reads webhook.svix_config.base_url / auth_token (FLEXPRICE_WEBHOOK_SVIX_CONFIG_BASE_URL / FLEXPRICE_WEBHOOK_SVIX_CONFIG_AUTH_TOKEN).
# Usage: make seed-svix
.PHONY: seed-svix
seed-svix:
@go run ./cmd/migrate svix
# Same, run inside the flexprice image (no local Go toolchain needed).
# Usage: make seed-svix-docker [IMAGE=flexprice-app:local]
IMAGE ?= flexprice-app:local
.PHONY: seed-svix-docker
seed-svix-docker:
@docker run --rm --env-file .env --entrypoint ./migrate $(IMAGE) svix
# Run postgres migrations
migrate-postgres:
@echo "Running Postgres migrations..."
@for i in 1 2 3 4 5 6 7 8 9 10; do \
if docker compose exec -T postgres pg_isready -U flexprice -d flexprice >/dev/null 2>&1; then \
echo "Postgres is ready"; \
docker compose exec -T postgres psql -U flexprice -d flexprice -c "CREATE SCHEMA IF NOT EXISTS extensions;"; \
docker compose exec -T postgres psql -U flexprice -d flexprice -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\" SCHEMA extensions;"; \
echo "Postgres migrations complete"; \
exit 0; \
fi; \
echo "Postgres not ready yet (attempt $$i/10), waiting 3s..."; \
sleep 3; \
done; \
echo "Error: Postgres failed to become ready"; exit 1
# Run clickhouse migrations
migrate-clickhouse:
@echo "Running Clickhouse migrations..."
@for i in 1 2 3 4 5 6 7 8 9 10; do \
if docker compose exec -T clickhouse clickhouse-client --user=flexprice --password=flexprice123 --database=flexprice --query "SELECT 1" >/dev/null 2>&1; then \
echo "Clickhouse is ready"; \
for file in migrations/clickhouse/*.sql; do \
if [ -f "$$file" ]; then \
echo "Running migration: $$file"; \
docker compose exec -T clickhouse clickhouse-client --user=flexprice --password=flexprice123 --database=flexprice --multiquery < "$$file" || true; \
fi; \
done; \
echo "Clickhouse migrations complete"; \
exit 0; \
fi; \
echo "Clickhouse not ready yet (attempt $$i/10), waiting 3s..."; \
sleep 3; \
done; \
echo "Error: Clickhouse failed to become ready"; exit 1
# Seed initial data
seed-db:
@echo "Running Seed data migration..."
@docker compose exec -T postgres psql -U flexprice -d flexprice -f /docker-entrypoint-initdb.d/V1__seed.sql
@echo "Postgres seed data migration complete"
# Initialize kafka topics
.PHONY: init-kafka
init-kafka:
@echo "Creating Kafka topics..."
@for i in 1 2 3 4 5 6 7 8 9 10; do \
echo "Attempt $$i: Checking if Kafka is ready..."; \
if docker compose exec -T kafka kafka-topics --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then \
echo "Kafka is ready!"; \
for topic in \
events \
events_lazy \
events_dlq \
events_backfill \
events_post_processing \
events_post_processing_backfill \
system_events \
wallet_alert \
activity_logs \
activity_logs_dlq \
onboarding_events \
staging_benchmarking \
prod_events_v4 \
staging_events_backfill \
staging_events; do \
echo "Creating topic: $$topic"; \
docker compose exec -T kafka kafka-topics --create --if-not-exists \
--bootstrap-server kafka:9092 \
--topic $$topic \
--partitions 1 \
--replication-factor 1 \
--config cleanup.policy=delete \
--config retention.ms=604800000; \
done; \
echo "Kafka topics created successfully"; \
exit 0; \
fi; \
echo "Kafka not ready yet, waiting 5s..."; \
sleep 5; \
done; \
echo "Error: Kafka failed to become ready after 10 attempts"; \
exit 1
# Replay a dead-letter topic back to origin. Dry-run first, then drop DRY_RUN.
# make dlq-replay SOURCE=staging_events_dlq
# make dlq-replay SOURCE=production_event_processing_dlq DRY_RUN=
.PHONY: dlq-replay
DRY_RUN ?= --dry-run
# Read SOURCE/DRY_RUN from the environment, not make interpolation, so they can't inject shell.
dlq-replay: export SOURCE := $(SOURCE)
dlq-replay: export DRY_RUN := $(DRY_RUN)
dlq-replay:
@case "$$SOURCE" in ''|*[!A-Za-z0-9._-]*) echo "usage: make dlq-replay SOURCE=<dlq-topic> [DRY_RUN=]"; exit 1;; esac
@case "$$DRY_RUN" in --dry-run|'') ;; *) echo "DRY_RUN must be --dry-run or empty"; exit 1;; esac
go run ./cmd/dlq replay --source "$$SOURCE" $$DRY_RUN
# Clean all docker containers and volumes related to the project
.PHONY: clean-docker
clean-docker:
@echo "Cleaning all docker containers and volumes..."
@docker compose down -v
@docker container prune -f
@docker volume rm $$(docker volume ls -q | grep flexprice) 2>/dev/null || true
@echo "Docker cleanup complete"
# Full local setup
.PHONY: setup-local
setup-local: up init-db init-kafka
@echo "Local setup complete. You can now run 'make run-server-local' to start the server"
# Clean everything and start fresh
.PHONY: clean-start
clean-start:
@make down
@docker compose down -v
@make setup-local
# Build the flexprice image separately
.PHONY: build-image
build-image:
@echo "Building flexprice image..."
@docker compose build flexprice-build
@echo "Flexprice image built successfully"
# Start only the flexprice services
.PHONY: start-flexprice
start-flexprice:
@echo "Starting flexprice services..."
@docker compose up -d flexprice-api flexprice-consumer flexprice-worker
@echo "Flexprice services started successfully"
# Stop only the flexprice services
.PHONY: stop-flexprice
stop-flexprice:
@echo "Stopping flexprice services..."
@docker compose stop flexprice-api flexprice-consumer flexprice-worker
@echo "Flexprice services stopped successfully"
# Restart only the flexprice services
.PHONY: restart-flexprice
restart-flexprice: stop-flexprice start-flexprice
@echo "Flexprice services restarted successfully"
# Full developer setup with clear instructions
.PHONY: dev-setup
dev-setup:
@echo "Setting up FlexPrice development environment..."
@echo "Step 1: Starting infrastructure services..."
@docker compose up -d postgres kafka clickhouse redis temporal temporal-ui
@echo "Step 2: Building FlexPrice application image..."
@make build-image
@echo "Step 3: Running database migrations and initializing Kafka..."
@make migrate-postgres migrate-clickhouse migrate-ent seed-db init-kafka
@echo "Step 4: Starting FlexPrice services..."
@make start-flexprice
@echo ""
@echo "✅ FlexPrice development environment is now ready!"
@echo "📊 Available services:"
@echo " - API: http://localhost:8080"
@echo " - Temporal UI: http://localhost:8088"
@echo " - Kafka UI: http://localhost:8084 (with profile 'dev')"
@echo " - ClickHouse: http://localhost:8123"
@echo ""
@echo "🔑 Default API Key (for local testing):"
@echo " sk_local_flexprice_test_key"
@echo " (pass as: -H 'x-api-key: sk_local_flexprice_test_key')"
@echo ""
@echo "💡 Useful commands:"
@echo " - make restart-flexprice # Restart FlexPrice services"
@echo " - make down # Stop all services"
@echo " - make clean-start # Clean everything and start fresh"
.PHONY: apply-migration
apply-migration:
@if [ -z "$(file)" ]; then \
echo "Error: Migration file not specified. Use 'make apply-migration file=<path>'"; \
exit 1; \
fi
@echo "Applying migration file: $(file)"
@docker compose exec -T postgres psql -U flexprice -d flexprice < $(file)
@echo "Migration applied successfully"
.PHONY: docker-build-local
docker-build-local:
docker compose build flexprice-build
.PHONY: install-typst
install-typst:
@./scripts/install-typst.sh
# SDK Generation targets (Speakeasy pipeline; use make sdk-all)
.PHONY: clean-sdk update-sdk
# Update swagger and regenerate all SDKs/MCP
update-sdk: swagger sdk-all
@echo "Swagger updated and all SDKs/MCP regenerated."
# Clean all generated SDK/MCP output directories
clean-sdk:
@echo "Cleaning generated SDKs/MCP..."
@rm -rf api/go api/typescript api/python api/mcp
@echo "Generated SDKs/MCP cleaned"
# Show custom files status (api/custom/<lang>/)
show-custom-files:
@echo "Custom files status (api/custom/):"
@echo "================================"
@for dir in go typescript python mcp; do \
echo "$$dir:"; \
if [ -d "api/custom/$$dir" ]; then \
find api/custom/$$dir -type f | sed 's/^/ /' || echo " (none)"; \
else \
echo " No custom directory"; \
fi; \
echo ""; \
done
# Help for SDK management
help-sdk:
@echo "SDK Management Commands:"
@echo "======================="
@echo " make sdk-all - Validate + generate all SDKs/MCP + merge custom (uses existing swagger)"
@echo " make filter-mcp-spec - Build tag-filtered OpenAPI spec for MCP (allowed tags in .speakeasy/mcp/allowed-tags.yaml)"
@echo " make update-sdk - Regenerate swagger then run sdk-all"
@echo " make clean-sdk - Remove generated api/go, api/typescript, api/python, api/mcp"
@echo " make merge-custom - Copy api/custom/<lang>/ into api/<lang>/"
@echo " make ts-sdk-check - Build the merged TypeScript SDK in a temp dir and run its custom tests"
@echo " make sync-gen-to-output - Copy .speakeasy/gen/*.yaml to api/<lang>/.speakeasy/gen.yaml (run before generate)"
@echo " make show-custom-files - List files in api/custom/"
@echo " make mcp-check - Build the merged MCP server in a temp dir and run its custom tests"
@echo ""
@echo "Go SDK only:"
@echo " make go-sdk - Clean + generate Go SDK + merge custom + build"
@echo " make regenerate-go-sdk - Regenerate Go SDK (no clean) + merge custom"
@echo " make clean-go-sdk - Remove api/go only"
@echo ""
@echo "SDK integration tests (published SDKs, api/tests):"
@echo " make test-sdk / test-sdks - Run all SDK tests (Go, Python, TypeScript) in one command"
@echo ""
@echo "White-label SDK:"
@echo " make check-wl-templates - Verify WL templates match .speakeasy/gen/*.yaml"
# SDK publishing: done via GitHub Actions (.github/workflows/generate-sdks.yml). No api/publish.sh in repo.
sdk-publish-js sdk-publish-py sdk-publish-go sdk-publish-all sdk-publish-all-with-version:
@echo "Publishing is done via the Generate SDKs workflow. Push to main or run workflow_dispatch on .github/workflows/generate-sdks.yml"; exit 1
# Test Generate SDKs workflow locally using act
test-github-workflow:
@echo "Testing Generate SDKs workflow locally..."
@./scripts/ensure-act.sh
@if [ ! -f .secrets ] && [ ! -f .env ]; then \
echo "Error: Create .secrets or .env with SPEAKEASY_API_KEY, SDK_DEPLOY_GIT_TOKEN, NPM_TOKEN, PYPI_TOKEN"; \
exit 1; \
fi
@( [ -f .secrets ] && set -a && . ./.secrets && set +a ) || ( set -a && . ./.env && set +a ); \
act workflow_dispatch -W .github/workflows/generate-sdks.yml \
-s SPEAKEASY_API_KEY="$${SPEAKEASY_API_KEY}" \
-s SDK_DEPLOY_GIT_TOKEN="$${SDK_DEPLOY_GIT_TOKEN}" \
-s NPM_TOKEN="$${NPM_TOKEN:-$$NPM_AUTH_TOKEN}" \
-s PYPI_TOKEN="$${PYPI_TOKEN:-$$PYPI_API_TOKEN}" \
-P ubuntu-latest=catthehacker/ubuntu:act-latest \
--container-architecture linux/amd64
.PHONY: test-github-workflow show-custom-files help-sdk check-wl-templates
# Check that white-label templates are in sync with .speakeasy/gen/*.yaml
# Run this after Speakeasy CLI updates gen configs (it auto-bumps fields)
# Note: workflow.yaml.tmpl is intentionally excluded — it has no .speakeasy/gen/ counterpart.
check-wl-templates:
@bash -c '\
DRIFT=0; \
for lang in go typescript python mcp; do \
diff \
<(sed '"'"'s/$${WL_[^}]*}/PLACEHOLDER/g; s/"PLACEHOLDER"/PLACEHOLDER/g'"'"' configs/white-label/templates/$$lang.yaml.tmpl) \
<(sed \
-e '"'"'s/github\.com\/flexprice[^ ]*/PLACEHOLDER/g'"'"' \
-e '"'"'s/Flexprice/PLACEHOLDER/g'"'"' \
-e '"'"'s/FlexPrice/PLACEHOLDER/g'"'"' \
-e '"'"'s/flexprice/PLACEHOLDER/g'"'"' \
-e '"'"'s/"@PLACEHOLDER\/[^"]*"/PLACEHOLDER/g'"'"' \
.speakeasy/gen/$$lang.yaml) \
|| { echo "DRIFT DETECTED: configs/white-label/templates/$$lang.yaml.tmpl is out of sync with .speakeasy/gen/$$lang.yaml — update the template to match"; DRIFT=1; }; \
done; \
[ "$$DRIFT" -eq 0 ] && echo "All white-label templates are in sync." || exit 1 \
'
# =============================================================================
# Speakeasy SDK Generation (New Pipeline)
# =============================================================================
# Version is managed by Speakeasy (versioningStrategy: automatic in gen.yaml); do not pass --set-version.
.PHONY: speakeasy-install speakeasy-generate speakeasy-validate speakeasy-lint
speakeasy-install:
@echo "Installing Speakeasy CLI..."
@brew install speakeasy-api/homebrew-tap/speakeasy || curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | sh
@speakeasy --version
speakeasy-validate:
@echo "Validating OpenAPI spec..."
@speakeasy validate openapi --schema docs/swagger/swagger-3-0.json
# 413 on upload is expected for large specs; report is still written to ~/.speakeasy/temp/
# CI=true and TERM=dumb disable the interactive TUI so make does not hang
speakeasy-lint:
@echo "Linting OpenAPI spec..."
@CI=true TERM=dumb speakeasy openapi lint -s docs/swagger/swagger-3-0.json --non-interactive
speakeasy-clean:
@echo "Cleaning generated SDK files..."
@echo "Removing Go SDK generated files..."
@find api/go -type f -name "*.go" ! -path "*/examples/*" ! -path "*/custom/*" ! -name "helpers.go" -delete 2>/dev/null || true
@find api/go -type d -name ".speakeasy" -exec rm -rf {} + 2>/dev/null || true
@rm -f api/go/go.mod api/go/go.sum 2>/dev/null || true
@rm -rf api/go/.devcontainer api/go/.openapi-generator api/go/.travis.yml 2>/dev/null || true
@rm -rf api/go/docs api/go/models api/go/internal api/go/types api/go/optionalnullable api/go/retry api/go/speakeasyusagegen 2>/dev/null || true
@rm -f api/go/*.md api/go/.git* 2>/dev/null || true
@echo "Removing Python SDK generated files..."
@find api/python -type f -name "*.py" ! -path "*/examples/*" ! -name "async_utils.py" -delete 2>/dev/null || true
@rm -rf api/python/src api/python/dist api/python/build api/python/*.egg-info 2>/dev/null || true
@rm -f api/python/setup.py api/python/pyproject.toml api/python/poetry.lock 2>/dev/null || true
@rm -rf api/python/.devcontainer api/python/docs 2>/dev/null || true
@rm -f api/python/*.md api/python/.git* 2>/dev/null || true
@echo "Removing TypeScript SDK generated files..."
@rm -rf api/typescript 2>/dev/null || true
@echo "✓ SDK cleanup complete"
# MCP uses a tag-filtered spec (docs/swagger/swagger-3-0-mcp.json). Run this before sdk-all/speakeasy-generate.
# Allowed tags: .speakeasy/mcp/allowed-tags.yaml
.PHONY: filter-mcp-spec
filter-mcp-spec:
@echo "Applying scope overlay to base spec..."
@speakeasy overlay apply \
-s docs/swagger/swagger-3-0.json \
-o .speakeasy/overlays/scopes.yaml \
> docs/swagger/swagger-3-0-with-scopes.yaml
@echo "Converting YAML to JSON..."
@python3 -c "import yaml, json; print(json.dumps(yaml.safe_load(open('docs/swagger/swagger-3-0-with-scopes.yaml')), indent=2))" \
> docs/swagger/swagger-3-0-with-scopes.json 2>/dev/null || \
(pip3 install --break-system-packages pyyaml > /dev/null 2>&1 && \
python3 -c "import yaml, json; print(json.dumps(yaml.safe_load(open('docs/swagger/swagger-3-0-with-scopes.yaml')), indent=2))" \
> docs/swagger/swagger-3-0-with-scopes.json)
@echo "Filtering spec by allowed tags..."
@node scripts/filter-openapi-by-tags.mjs \
--spec docs/swagger/swagger-3-0-with-scopes.json \
--out docs/swagger/swagger-3-0-mcp.json
@rm -f docs/swagger/swagger-3-0-with-scopes.yaml docs/swagger/swagger-3-0-with-scopes.json
@echo "MCP spec created with scopes at docs/swagger/swagger-3-0-mcp.json"
# Copy central gen (.speakeasy/gen/*.yaml) into api/<lang>/.speakeasy/gen.yaml so Speakeasy CLI finds config.
.PHONY: sync-gen-to-output
sync-gen-to-output:
@./scripts/sync-gen-to-output.sh
# Local runs skip the registry upload. The release workflow overrides this to publish the spec and
# code samples behind the hosted docs spec (spec.speakeasy.com/flexprice/prod/swagger-json-with-code-samples).
SPEAKEASY_RUN_FLAGS ?= --skip-upload-spec --minimal
speakeasy-generate: speakeasy-validate filter-mcp-spec sync-gen-to-output
@echo "Generating SDKs with Speakeasy..."
@CI=true TERM=dumb speakeasy run --target all -y --skip-compile $(SPEAKEASY_RUN_FLAGS)
# =============================================================================
# Single command: Swagger + SDK/MCP generation + merge custom (no testing; use make test-sdk for integration tests)
# =============================================================================
# Run: make sdk-all
# Uses existing docs/swagger/swagger-3-0.json. Run 'make swagger' when you change the API.
# Does: (if VERSION unset) next patch version from .speakeasy/sdk-version.json → sync version to all gen.yaml → validate → generate → merge custom.
# Speakeasy reads version from gen.yaml (cannot use --set-version with --target all). Every run uses a unique version so publish never fails.
#
# Local auth: create a .secrets file (already gitignored) with:
# SPEAKEASY_API_KEY=spk_your_key_here
# Then run: make sdk-all-local (loads .secrets and runs sdk-all)
.PHONY: sdk-all sdk-all-local
sdk-all:
@VER="$${VERSION:-$$(./scripts/next-sdk-version.sh patch)}"; \
./scripts/sync-sdk-version-to-gen.sh "$$VER" && \
$(MAKE) speakeasy-validate speakeasy-generate merge-custom fix-mcp-package-name
@echo "✅ SDK/MCP generation complete. (Use make test-sdk to run SDK integration tests.)"
# Load SPEAKEASY_API_KEY from .secrets then run sdk-all. Use this when running locally.
sdk-all-local:
@if [ -f .secrets ]; then set -a && . ./.secrets && set +a; fi && $(MAKE) sdk-all
# =============================================================================
# Go SDK Generation with Speakeasy (Production Pipeline)
# =============================================================================
.PHONY: speakeasy-go-sdk merge-custom clean-go-sdk go-sdk regenerate-go-sdk sync-gen-to-output
# Generate Go SDK only with Speakeasy
speakeasy-go-sdk:
@echo "🔨 Generating Go SDK with Speakeasy..."
@bash -c 'set -o pipefail; CI=true TERM=dumb speakeasy run --target flexprice-go -y --skip-compile < /dev/null | cat'
@echo "✓ Go SDK generated successfully"
# Clean only Go SDK
clean-go-sdk:
@echo "🧹 Cleaning Go SDK..."
@rm -rf api/go
@echo "✓ Go SDK cleaned"
# Complete Go SDK pipeline: clean → validate → sync gen → generate → merge custom → build
go-sdk: clean-go-sdk speakeasy-validate sync-gen-to-output speakeasy-go-sdk merge-custom
@echo "🧪 Testing Go SDK compilation..."
@cd api/go && go mod tidy && go build ./...
@echo "✅ Go SDK ready for publishing!"
# Quick regeneration (no clean, faster for development)
regenerate-go-sdk: sync-gen-to-output speakeasy-go-sdk merge-custom
@echo "✓ Go SDK regenerated"
# Merge custom files from api/custom/<lang>/ into api/<lang>/ after generation.
# Add files under api/custom/go etc. with same relative paths as in api/go.
merge-custom:
@for dir in go typescript python mcp; do \
if [ -d "api/custom/$$dir" ]; then \
echo "Merging custom files into api/$$dir/..."; \
rsync -av --exclude='.gitkeep' --exclude='/tests/' "api/custom/$$dir/" "api/$$dir/" 2>/dev/null || true; \
fi; \
done
@if [ -f api/python/pyproject.toml ]; then \
sed 's/Generated by Speakeasy\./for the FlexPrice API./' api/python/pyproject.toml > api/python/pyproject.toml.tmp && mv api/python/pyproject.toml.tmp api/python/pyproject.toml; \
fi
@if [ -f api/mcp/package.json ]; then \
node scripts/patch-mcp-serve.mjs; \
fi
@if [ -f api/typescript/src/index.ts ] && [ -f api/typescript/src/index.extras.ts ]; then \
node scripts/patch-ts-sdk-index.mjs; \
fi
@if [ -f api/typescript/src/lib/config.ts ]; then \
node scripts/patch-ts-sdk-config.mjs; \
fi
@echo "✓ Custom merge complete"
# Build a scratch copy of the merged TS SDK and run api/custom/typescript/tests against it.
# The copy keeps node_modules and esm/ out of api/typescript, which is uploaded and published as is.
.PHONY: ts-sdk-check
ts-sdk-check:
@test -f api/typescript/package.json || { echo "api/typescript not found; run make sdk-all first"; exit 1; }
@tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; set -e; \
rsync -a --exclude='node_modules' --exclude='esm' api/typescript/ "$$tmp/"; \
rsync -a api/custom/typescript/tests/ "$$tmp/tests/"; \
cd "$$tmp"; \
npm install --ignore-scripts --no-audit --no-fund; \
npm run build; \
node --test tests/*.test.mjs
@echo "✓ TypeScript SDK build and tests passed"
# Force MCP package name so npm publish uses @flexprice/mcp-server.
.PHONY: fix-mcp-package-name
fix-mcp-package-name:
@if [ -f api/mcp/package.json ]; then \
jq '.name = "@flexprice/mcp-server"' api/mcp/package.json > api/mcp/package.json.tmp && mv api/mcp/package.json.tmp api/mcp/package.json; \
echo "✓ MCP package name set to @flexprice/mcp-server"; \
fi
# Build a scratch copy of the MCP server with api/custom/mcp merged in and run api/custom/mcp/tests against it.
# MCP_SRC is the generated server: api/mcp after make sdk-all, or a checkout of flexprice/mcp-server.
MCP_SRC ?= api/mcp
.PHONY: mcp-check
mcp-check:
@test -f "$(MCP_SRC)/package.json" || { echo "$(MCP_SRC) not found; run make sdk-all first or pass MCP_SRC=<generated MCP server>"; exit 1; }
@set -e; tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; \
rsync -a --exclude='.git' --exclude='node_modules' --exclude='esm' --exclude='bin' "$(MCP_SRC)/" "$$tmp/"; \
rsync -a --exclude='.gitkeep' --exclude='/tests/' api/custom/mcp/ "$$tmp/"; \
node scripts/patch-mcp-serve.mjs "$$tmp/src/mcp-server/cli/serve/impl.ts"; \
rsync -a api/custom/mcp/tests/ "$$tmp/tests/"; \
cd "$$tmp"; \
npm install --ignore-scripts --no-audit --no-fund; \
[ -f node_modules/bun/install.js ] && node node_modules/bun/install.js || true; \
npm run build; \
PATCH_SCRIPT="$(CURDIR)/scripts/patch-mcp-serve.mjs" node --test tests/*.test.mjs
@echo "✓ MCP server build and tests passed"
# =============================================================================
# SDK tests: single command runs all SDKs (published integration tests)
# =============================================================================
# Require FLEXPRICE_API_KEY and FLEXPRICE_API_HOST.
# Dependencies are installed automatically before each test run.
.PHONY: test-sdk test-sdks
# Run all SDK integration tests (Go, Python, TypeScript). Installs deps first to avoid missing-package issues.
# Requires FLEXPRICE_API_KEY and FLEXPRICE_API_HOST to be set (export them so tests can call the API).
test-sdk test-sdks:
@if [ -z "$$FLEXPRICE_API_KEY" ] || [ -z "$$FLEXPRICE_API_HOST" ]; then \
echo ""; \
echo "❌ SDK tests need API credentials. Set and export:"; \
echo " export FLEXPRICE_API_KEY=\"your-api-key\""; \
echo " export FLEXPRICE_API_HOST=\"us.api.flexprice.io/v1\" # or localhost:8080/v1 for local"; \
echo ""; \
exit 1; \
fi
@echo "Running SDK tests (Go, Python, TypeScript)..."
@echo " FLEXPRICE_API_HOST=$$FLEXPRICE_API_HOST"
@echo "--- Go (install deps + test) ---"; (cd api/tests/go && GOPRIVATE=github.com/flexprice/* go mod tidy && GOPRIVATE=github.com/flexprice/* go mod download && GOPRIVATE=github.com/flexprice/* go run -tags published test_sdk.go) || true
@echo "--- Python (install deps + test) ---"; (cd api/tests/python && \
PY=; \
for c in python3.13 python3.12 python3.11 python3.10 python3; do \
if command -v $$c >/dev/null 2>&1 && $$c -c 'import sys; sys.exit(0 if sys.version_info>=(3,10) else 1)' 2>/dev/null; then PY=$$c; break; fi; \
done; \
if [ -z "$$PY" ]; then \
echo "❌ Python 3.10+ required (PyPI flexprice). macOS: brew install python@3.12 (then re-run; we try python3.12 … python3.10 before python3)"; \
exit 1; \
fi; \
if [ ! -d .venv ] || ! [ -x .venv/bin/python ] || ! .venv/bin/python -c 'import sys; sys.exit(0 if sys.version_info>=(3,10) else 1)' 2>/dev/null || ! .venv/bin/python -m pip --version >/dev/null 2>&1; then \
rm -rf .venv && $$PY -m venv .venv; \
fi && \
echo " using $$(.venv/bin/python --version)" && \
.venv/bin/python -m pip install -q --upgrade pip setuptools wheel && \
.venv/bin/python -m pip install -q -r requirements.txt && \
.venv/bin/python test_sdk.py) || true
@echo "--- TypeScript (install deps + test) ---"; (cd api/tests/ts && npm install && npm test) || true
@echo "✓ All SDK tests finished"
# Run the orchestrated sanity integration test suite.
# Usage:
# export FLEXPRICE_API_KEY=sk_...
# make test-suite
# Host defaults to localhost:8080/v1 (http for localhost, https for remote).
test-suite:
@if [ -z "$$FLEXPRICE_API_KEY" ]; then \
echo ""; \
echo "❌ Need an API key:"; \
echo " export FLEXPRICE_API_KEY=sk_..."; \
echo " make test-suite"; \
echo ""; \
exit 1; \
fi
@cd integration-testing-suite/go && go run .
.PHONY: sdk-all test-sdk test-sdks test-suite
# -----------------------------------------------------------------------
# Loglint — ctx-first logger enforcement
# -----------------------------------------------------------------------
.PHONY: build-loglint lint lint-ci
## build-loglint: compile the loglint vettool binary into tools/bin/
build-loglint:
cd tools/loglint && go mod download && go build -o ../bin/loglint ./main.go
## lint: run loglint on internal/ (warnings + errors, non-blocking)
lint: build-loglint
go vet -vettool=./tools/bin/loglint ./internal/... ; echo "Loglint complete"
## lint-ci: run loglint on internal/ (errors only — LL008 warnings ignored)
lint-ci: build-loglint
@out=$$(go vet -vettool=./tools/bin/loglint ./internal/... 2>&1 | grep -v "^#" | grep -v "warning:"); if [ -n "$$out" ]; then echo "$$out"; exit 1; fi