diff --git a/.vibe/skills/gitea-client/scripts/gitea-client.sh b/.vibe/skills/gitea-client/scripts/gitea-client.sh index 5353a49..105dc3d 100755 --- a/.vibe/skills/gitea-client/scripts/gitea-client.sh +++ b/.vibe/skills/gitea-client/scripts/gitea-client.sh @@ -203,6 +203,31 @@ cmd_wait_job() { } # Comment on PR +# Create a pull request +cmd_create_pr() { + local owner="$1" + local repo="$2" + local title="$3" + local body="$4" + local head="$5" + local base="${6:-main}" + + if [[ -z "$owner" || -z "$repo" || -z "$title" || -z "$head" ]]; then + echo "Usage: $0 create-pr <body> <head_branch> [base_branch]" >&2 + exit 1 + fi + + local endpoint="/repos/${owner}/${repo}/pulls" + local data + data=$(jq -n \ + --arg title "$title" \ + --arg body "$body" \ + --arg head "$head" \ + --arg base "$base" \ + '{title: $title, body: $body, head: $head, base: $base}') + api_request "POST" "$endpoint" "$data" +} + cmd_comment_pr() { local owner="$1" local repo="$2" @@ -215,7 +240,8 @@ cmd_comment_pr() { fi local endpoint="/repos/${owner}/${repo}/issues/${pr_number}/comments" - local data="{\"body\": \"${comment}\"}" + local data + data=$(jq -n --arg body "$comment" '{body: $body}') api_request "POST" "$endpoint" "$data" } @@ -250,6 +276,7 @@ main() { monitor-workflow) cmd_monitor_workflow "$@" ;; diagnose-job) cmd_diagnose_job "$@" ;; recent-workflows) cmd_recent_workflows "$@" ;; + create-pr) cmd_create_pr "$@" ;; comment-pr) cmd_comment_pr "$@" ;; pr-status) cmd_pr_status "$@" ;; list-issues) cmd_list_issues "$@" ;; @@ -274,6 +301,7 @@ main() { echo " monitor-workflow <owner> <repo> <workflow_run_id> [interval_seconds]" >&2 echo " diagnose-job <owner> <repo> <job_id>" >&2 echo " recent-workflows <owner> <repo> [limit] [status_filter]" >&2 + echo " create-pr <owner> <repo> <title> <body> <head_branch> [base_branch]" >&2 echo " comment-pr <owner> <repo> <pr_number> <comment>" >&2 echo " pr-status <owner> <repo> <pr_number>" >&2 echo " list-issues <owner> <repo> [state]" >&2 diff --git a/README.md b/README.md index a1f03db..fe9c9f4 100644 --- a/README.md +++ b/README.md @@ -1,421 +1,98 @@ # dance-lessons-coach -[![Build Status](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml/badge.svg)](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml/badge.svg) -[![Go Report Card](https://goreportcard.com/badge/github.com/arcodange/dance-lessons-coach)](https://goreportcard.com/report/github.com/arcodange/dance-lessons-coach) +[![Build Status](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml/badge.svg)](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml) [![Version](https://img.shields.io/badge/version-1.4.0-blue.svg)](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/releases) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -[![BDD Coverage](https://img.shields.io/badge/BDD_Coverage-51.1%%-red?style=flat-square)](https://gitea.arcodange.lab/arcodange/dance-lessons-coach) -[![UNIT Coverage](https://img.shields.io/badge/UNIT_Coverage-8.9%%-red?style=flat-square)](https://gitea.arcodange.lab/arcodange/dance-lessons-coach) -A Go project demonstrating idiomatic package structure, CLI implementation, and JSON API with Chi router. -======= +Go web service demonstrating idiomatic package structure, versioned JSON API, and production-ready features. ## Features -- Greet function with default behavior -- Command-line interface -- JSON API with versioned endpoints -- Chi router integration -- Zerolog for high-performance logging -- Viper for configuration management -- Graceful shutdown with context -- Readiness endpoint for Kubernetes/service mesh integration -- OpenTelemetry integration with Jaeger support -- OpenAPI/Swagger documentation -- Unit tests -- Go 1.26.1 compatible +- Versioned JSON API (`/api/v1`, `/api/v2`) +- Chi router with graceful shutdown +- Zerolog structured logging (console and JSON modes) +- Viper configuration (file + env vars) +- Readiness endpoint for Kubernetes / service mesh +- OpenTelemetry / Jaeger distributed tracing +- OpenAPI / Swagger UI (embedded in binary) +- PostgreSQL user service with JWT auth +- BDD + unit tests -## Installation +## Quick Start ```bash -# Clone the repository git clone https://gitea.arcodange.lab/arcodange/dance-lessons-coach.git cd dance-lessons-coach - -# Build all binaries -./scripts/build.sh - -# Use the new Cobra CLI -./bin/dance-lessons-coach --help - -# Or use the legacy greet CLI -go run ./cmd/greet +./scripts/build.sh # produces ./bin/server and ./bin/greet +./scripts/start-server.sh start ``` -## CI/CD Pipeline - -dance-lessons-coach features an optimized CI/CD pipeline using GitHub Actions with container/services architecture: - -### Key Features -- ✅ **Container-based execution**: All steps run in pre-built Docker cache images -- ✅ **Service-based PostgreSQL**: Automatic database service provisioning -- ✅ **Smart caching**: Dependency-aware cache invalidation -- ✅ **Multi-platform**: Compatible with Gitea, GitHub, and GitLab -- ✅ **Fast execution**: No Docker Compose overhead -- ✅ **Reliable testing**: Full database connectivity with proper environment setup - -### Architecture - -The pipeline uses GitHub Actions' native `container` and `services` directives instead of Docker Compose: - -```yaml -jobs: - ci-pipeline: - container: - image: gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:${{ needs.build-cache.outputs.deps_hash }} - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: dance_lessons_coach_bdd_test -``` - -### Benefits - -1. **Performance**: Direct container execution without compose overhead -2. **Reliability**: Service containers managed by GitHub Actions -3. **Simplicity**: Cleaner workflow definition -4. **Portability**: Works across CI platforms -5. **Caching**: Intelligent dependency-based cache rebuilding - -### Workflow Steps - -1. **Build Cache**: Creates Docker image with Go tools and dependencies -2. **CI Pipeline**: Runs tests, builds binaries, and generates documentation -3. **Database Tests**: Connects to PostgreSQL service container -4. **Coverage Reporting**: Updates coverage badges automatically -5. **Artifact Publishing**: Builds and pushes Docker images (main branch only) - -### Environment Configuration - -The pipeline automatically sets up database environment variables: - ```bash -echo "DLC_DATABASE_HOST=postgres" >> $GITHUB_ENV -echo "DLC_DATABASE_PORT=5432" >> $GITHUB_ENV -echo "DLC_DATABASE_USER=postgres" >> $GITHUB_ENV -echo "DLC_DATABASE_PASSWORD=postgres" >> $GITHUB_ENV -echo "DLC_DATABASE_NAME=dance_lessons_coach_bdd_test" >> $GITHUB_ENV -echo "DLC_DATABASE_SSL_MODE=disable" >> $GITHUB_ENV +curl http://localhost:8080/api/health +curl http://localhost:8080/api/v1/greet/Alice ``` -### Status +Stop: `./scripts/start-server.sh stop` -[![Build Status](https://gitea.arcodange.fr/api/badges/arcodange/dance-lessons-coach/status)](https://gitea.arcodange.fr/arcodange/dance-lessons-coach) +## Greet CLI -======= -- ✅ **Linting**: Code quality checks with `go fmt` and `go vet` -- ✅ **Version Management**: Automatic version detection -- ✅ **Portable**: Uses standard GitHub Actions workflow format - -### Workflow File -```yaml -# .github/workflows/main.yml -jobs: - build-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 - with: - go-version: '1.26.1' - - run: go build ./... - - run: go test ./... -cover - - lint-format: - runs-on: ubuntu-latest - steps: - - run: go fmt ./... - - run: go vet ./... +```bash +go run ./cmd/greet # Hello world! +go run ./cmd/greet Alice # Hello Alice! ``` -### Setup Instructions -1. **Gitea**: Enable GitHub Actions compatibility in repo settings -2. **GitHub**: Push to mirror repository (workflow runs automatically) -3. **GitLab**: Convert workflow to `.gitlab-ci.yml` or use compatibility mode - -**See [ADR 0016](adr/0016-ci-cd-pipeline-design.md) for complete CI/CD design and [STATUS_BADGES.md](STATUS_BADGES.md) for badge setup.** - ## Configuration -Basic configuration options: +All options are available via `config.yaml` or `DLC_*` environment variables. -```bash -# Start with default configuration -./scripts/start-server.sh start +| Env var | Default | Description | +|---------|---------|-------------| +| `DLC_SERVER_PORT` | `8080` | Listening port | +| `DLC_SERVER_HOST` | `0.0.0.0` | Bind address | +| `DLC_LOGGING_JSON` | `false` | JSON log format | +| `DLC_LOGGING_OUTPUT` | stderr | Log file path | +| `DLC_SHUTDOWN_TIMEOUT` | `30s` | Graceful shutdown window | +| `DLC_API_V2_ENABLED` | `false` | Enable `/api/v2` routes | +| `DLC_CONFIG_FILE` | `./config.yaml` | Override config path | -# Custom port -export DLC_SERVER_PORT=9090 -./scripts/start-server.sh start +See `config.example.yaml` for a full template. -# JSON logging -export DLC_LOGGING_JSON=true -./scripts/start-server.sh start -``` +## API -**See [AGENTS.md](AGENTS.md#configuration-management) for comprehensive configuration guide including:** -- File-based configuration -- Environment variables -- Configuration priority rules -- OpenTelemetry setup -- Advanced scenarios - -## Usage - -### New Cobra CLI (Recommended) - -```bash -# Show help -./bin/dance-lessons-coach --help - -# Show version -./bin/dance-lessons-coach version - -# Greet someone -./bin/dance-lessons-coach greet John - -# Start server -./bin/dance-lessons-coach server -``` - -### Legacy CLI (Deprecated) - -```bash -# Default greeting -go run ./cmd/greet -# Output: Hello world! - -# Custom greeting -go run ./cmd/greet John -# Output: Hello John! -``` - -### Web Server - -**Using the server control script (recommended):** - -```bash -# Start the server -./scripts/start-server.sh start - -# Test API endpoints -./scripts/start-server.sh test - -# Access OpenAPI documentation -# Swagger UI: http://localhost:8080/swagger/ -# OpenAPI spec: http://localhost:8080/swagger/doc.json - -# Stop the server -./scripts/start-server.sh stop -``` - -**Manual server management:** - -```bash -# Start the server -go run ./cmd/server - -# Test API endpoints -curl http://localhost:8080/api/health -# Output: {"status":"healthy"} - -curl http://localhost:8080/api/ready -# Output: {"ready":true} - -curl http://localhost:8080/api/v1/greet -# Output: {"message":"Hello world!"} - -curl http://localhost:8080/api/v1/greet/John -# Output: {"message":"Hello John!"} -``` +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/health` | Liveness check | +| GET | `/api/ready` | Readiness check (503 during shutdown) | +| GET | `/api/version` | Version info (`?format=plain\|full\|json`) | +| GET | `/api/v1/greet/` | Default greeting | +| GET | `/api/v1/greet/{name}` | Named greeting | +| POST | `/api/v2/greet` | V2 greeting with validation | +| GET | `/swagger/` | Swagger UI | ## Testing ```bash -# Run all tests -go test ./... - -# Run specific package tests -go test ./pkg/greet/ +go test ./... # unit + integration tests +./scripts/test-graceful-shutdown.sh # lifecycle + JSON logging validation +./scripts/test-opentelemetry.sh # tracing end-to-end ``` -## CI/CD +## Gitea Client -dance-lessons-coach includes a comprehensive CI/CD pipeline with multiple testing options: +AI agent helper script at `.vibe/skills/gitea-client/scripts/gitea-client.sh`. -### Local Testing (No Gitea Required) +Auth setup: ```bash -# Validate workflow structure -./scripts/cicd.sh validate - -# Test workflow steps locally -./scripts/cicd.sh test-simple +echo "your_token" > ~/.gitea_token +chmod 600 ~/.gitea_token +export GITEA_API_TOKEN_FILE="$HOME/.gitea_token" ``` -### Gitea Integration -```bash -# Test local setup with Gitea configuration -./scripts/cicd.sh test-local - -# Check pipeline status on Gitea -./scripts/cicd.sh check-status -``` - -### Full CI/CD Testing -```bash -# Test with docker compose (requires Gitea runner) -./scripts/cicd.sh test-docker -``` - -**See [adr/0016-ci-cd-pipeline-design.md](adr/0016-ci-cd-pipeline-design.md) for complete CI/CD architecture.** - -## Project Structure - -``` -dance-lessons-coach/ -├── adr/ # Architecture Decision Records -├── cmd/ # Entry points (greet CLI, server) -├── pkg/ # Core packages (config, greet, server, telemetry) -│ └── server/docs/ # Generated OpenAPI documentation (gitignored) -├── config.yaml # Configuration file -├── scripts/ # Management scripts -└── go.mod # Go module definition -``` - -**See [AGENTS.md](AGENTS.md#project-structure) for detailed structure and component explanations.** -``` - -## Development - -### Generate OpenAPI Documentation - -The project uses [swaggo/swag](https://github.com/swaggo/swag) to generate OpenAPI/Swagger documentation from code annotations: - -```bash -# Generate documentation -go generate ./pkg/server/ - -# This creates: -# - pkg/server/docs/docs.go (swagger template) -# - pkg/server/docs/swagger.json (OpenAPI spec) -# - pkg/server/docs/swagger.yaml (YAML version) -``` - -**Note:** `pkg/server/docs/` is gitignored. Documentation is embedded in the binary at build time. - -### Documentation Annotations - -Add swagger annotations to handlers and models: - -```go -// @Summary Get personalized greeting -// @Description Returns a greeting with the specified name -// @Tags greet -// @Accept json -// @Produce json -// @Param name path string true "Name to greet" -// @Success 200 {object} GreetResponse "Successful response" -// @Failure 400 {object} ErrorResponse "Invalid name parameter" -// @Router /v1/greet/{name} [get] -func (h *apiV1GreetHandler) handleGreetPath(w http.ResponseWriter, r *http.Request) { - // handler implementation -} -``` +Get a token at https://gitea.arcodange.lab → Profile → Settings → Applications. ## Architecture -This project uses Architecture Decision Records (ADRs) to document key technical choices. See [adr/](adr/) for complete documentation including decisions on Go 1.26.1, Chi router, Zerolog, OpenTelemetry, interface-based design, graceful shutdown, configuration management, testing strategies, and OpenAPI documentation. - -**Adding new decisions?** See [adr/README.md](adr/README.md) for guidelines. - -## Gitea Integration - -dance-lessons-coach includes AI agent skills for Gitea integration to monitor CI/CD jobs and interact with pull requests. - -### Gitea Client Skill Setup - -The Gitea client skill enables AI agents to: -- Monitor CI/CD job status -- Fetch job logs for debugging -- Comment on pull requests -- Track PR status - -**Setup Instructions:** - -1. **Create a Personal Access Token:** - - Log in to https://gitea.arcodange.lab - - Go to Profile → Settings → Applications - - Generate token with `read:repository`, `write:repository`, and `read:user` scopes - -2. **Configure Authentication:** - ```bash - # Option 1: Environment variable - export GITEA_API_TOKEN="your_token" - - # Option 2: Token file (recommended) - echo "your_token" > ~/.gitea_token - chmod 600 ~/.gitea_token - export GITEA_API_TOKEN_FILE="$HOME/.gitea_token" - ``` - -3. **Add to shell configuration:** - ```bash - echo 'export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"' >> ~/.bashrc - source ~/.bashrc - ``` - -**Usage Examples:** -```bash -# List recent jobs -.vibe/skills/gitea-client/scripts/gitea-client.sh list-jobs owner repo workflow_id 5 - -# Wait for job completion -.vibe/skills/gitea-client/scripts/gitea-client.sh wait-job owner repo job_id 300 - -# Comment on PR -.vibe/skills/gitea-client/scripts/gitea-client.sh comment-pr owner repo 42 "Build completed!" -``` - -**Documentation:** See [.vibe/skills/gitea-client/README.md](.vibe/skills/gitea-client/README.md) for complete setup and usage guide. - -## 🤖 AI Agent Usage - -### Quick Launch Commands - -**Programmer Agent** (for code implementation, testing, CI/CD): -```bash -vibe start --agent dancelessonscoachprogrammer -``` - -**Product Owner Agent** (for requirements, interviews, documentation): -```bash -vibe start --agent dancelessonscoach-product-owner -``` - -### Full Documentation - -For complete agent usage guide including: -- Agent selection guidance -- Common workflow examples -- Configuration reference -- Best practices -- Troubleshooting tips - -See: [AGENT_USAGE_GUIDE.md](documentation/AGENT_USAGE_GUIDE.md) - -### Gitmoji Cheatsheet - -Quick reference for commit messages: -- **📝 `:memo:` docs** - Documentation -- **✨ `:sparkles:` feat** - New feature -- **🐛 `:bug:` fix** - Bug fix -- **♻️ `:recycle:` refactor** - Code refactoring -- **🔧 `:wrench:` chore** - Build/config changes - -Full cheatsheet: [GITMOJI_CHEATSHEET.md](documentation/GITMOJI_CHEATSHEET.md) +Key decisions are documented in [adr/](adr/). See [AGENTS.md](AGENTS.md) for the full development reference (commands, config, ADR index, commit conventions). ## License diff --git a/cmd/server/main.go b/cmd/server/main.go index 6f8cf38..abe8fa1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -48,8 +48,10 @@ func main() { log.Fatal().Err(err).Msg("Failed to load configuration") } - // Create readiness context to control readiness state - readyCtx, readyCancel := context.WithCancel(context.Background()) + // Create readiness context to control readiness state. + // CancelableContext exposes Cancel() so that Server.Run() can cancel + // readiness at the start of graceful shutdown (before the propagation sleep). + readyCtx, readyCancel := server.NewCancelableContext(context.Background()) defer readyCancel() // Create and run server @@ -57,4 +59,5 @@ func main() { if err := server.Run(); err != nil { log.Fatal().Err(err).Msg("Server failed") } + log.Trace().Msg("Server exited") } diff --git a/pkg/config/config.go b/pkg/config/config.go index 52210f7..f9cbdc1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -118,6 +118,34 @@ type SamplerConfig struct { Ratio float64 `mapstructure:"ratio"` } +// peekJSONLogging determines whether JSON logging should be used before the full +// config is loaded, solving the chicken-and-egg problem where the logger format +// must be known before any log is emitted, yet the format is stored in the config. +// +// Resolution order (mirrors Viper's own priority): +// 1. DLC_LOGGING_JSON env var — checked directly via os.Getenv (zero overhead) +// 2. logging.json key in the config file — read with a minimal throwaway Viper +// instance so we don't parse the whole config twice unnecessarily +func peekJSONLogging() bool { + // 1. Env var takes highest priority — check it first + if env := os.Getenv("DLC_LOGGING_JSON"); env != "" { + return strings.EqualFold(env, "true") || env == "1" + } + + // 2. Try to read logging.json from the config file + preV := viper.New() + preV.SetDefault("logging.json", false) + if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" { + preV.SetConfigFile(configFile) + } else { + preV.SetConfigName("config") + preV.SetConfigType("yaml") + preV.AddConfigPath(".") + } + _ = preV.ReadInConfig() // ignore errors — defaults apply on failure + return preV.GetBool("logging.json") +} + // LoadConfig loads configuration from file, environment variables, and defaults // Configuration priority: file > environment variables > defaults // To specify a custom config file path, set DLC_CONFIG_FILE environment variable @@ -129,9 +157,17 @@ func LoadConfig() (*Config, error) { v := viper.New() - // Set up initial console logging for config loading messages - consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} - log.Logger = log.Output(consoleWriter) + // Configure the logger format before emitting any log output. + // peekJSONLogging reads the JSON setting early (env var + config file pre-read) + // so that every log line — including those produced during config loading — is + // already in the correct format. + jsonLogging := peekJSONLogging() + if jsonLogging { + log.Logger = log.Output(os.Stderr) + } else { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + } + log.Info().Bool("json", jsonLogging).Msg("Logging configured") // Set default values v.SetDefault("server.host", "0.0.0.0") @@ -227,15 +263,9 @@ func LoadConfig() (*Config, error) { return nil, fmt.Errorf("config unmarshal error: %w", err) } - // Configure log output format (JSON or console) first - if config.Logging.JSON { - log.Logger = log.Output(os.Stderr) - } else { - consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} - log.Logger = log.Output(consoleWriter) - } - - // Setup logging based on configuration + // Setup logging based on configuration (level, output file, time format). + // The JSON/console format was already applied at the top of LoadConfig via + // peekJSONLogging, so SetupLogging only needs to handle the remaining knobs. config.SetupLogging() log.Info(). diff --git a/pkg/server/server.go b/pkg/server/server.go index 8b7286a..9c20539 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -33,6 +33,28 @@ import ( //go:embed docs/swagger.json var swaggerJSON embed.FS +// CancelableContext wraps a context.Context and exposes a Cancel() method so +// that Server.Run() can cancel readiness during graceful shutdown via the type +// assertion it already performs. Callers that don't need controlled cancellation +// (tests, CLI) can pass a plain context.Background() — the assertion silently +// fails and readiness is never explicitly cancelled, which is harmless. +type CancelableContext struct { + context.Context + cancel context.CancelFunc +} + +// NewCancelableContext creates a CancelableContext whose Cancel() method will +// be invoked by Server.Run() at the start of graceful shutdown, before the +// 1-second readiness propagation window. The returned CancelFunc is a no-op +// after Cancel() has been called, so it is safe to defer in main. +func NewCancelableContext(parent context.Context) (*CancelableContext, context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + return &CancelableContext{Context: ctx, cancel: cancel}, cancel +} + +// Cancel satisfies the interface checked in Run() and cancels the context. +func (c *CancelableContext) Cancel() { c.cancel() } + type Server struct { router *chi.Mux readyCtx context.Context diff --git a/scripts/start-server.sh b/scripts/start-server.sh index 98229e5..fd7ace8 100755 --- a/scripts/start-server.sh +++ b/scripts/start-server.sh @@ -4,7 +4,8 @@ # This script starts the server in the background and provides control functions # Configuration -PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach" +SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")") +PROJECT_DIR=$(dirname "$SCRIPTS_DIR") SERVER_CMD="go run ./cmd/server" LOG_FILE="server.log" PID_FILE="server.pid" diff --git a/scripts/test-graceful-shutdown.sh b/scripts/test-graceful-shutdown.sh index e3dd437..6b362bc 100755 --- a/scripts/test-graceful-shutdown.sh +++ b/scripts/test-graceful-shutdown.sh @@ -7,7 +7,8 @@ set -e # Configuration -PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach" +SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")") +PROJECT_DIR=$(dirname "$SCRIPTS_DIR") SERVER_CMD="./scripts/start-server.sh" LOG_FILE="server.log" PID_FILE="server.pid" @@ -59,11 +60,40 @@ echo "Response: $GREET_NAME_RESPONSE" echo "" echo "Stopping server gracefully..." -# Test readiness during shutdown (in background) -(curl -s http://localhost:8080/api/ready > /dev/null 2>&1 &) +# Send SIGTERM once and probe /api/ready during the 1-second propagation window +# the server holds open (pkg/server/server.go: time.Sleep(1s) after readiness +# cancel). Previously the curl fired *before* the signal — it always saw "ready". +# We also avoid calling "$SERVER_CMD stop" afterwards because that would send a +# second SIGTERM: after signal.NotifyContext is done, the default handler kicks in +# and the process terminates with a non-JSON "signal: terminated" on stderr. +SERVER_PID=$(cat "$PID_FILE" 2>/dev/null || echo "") +if [[ -z "$SERVER_PID" ]]; then + echo -e "\033[0;31m❌ FAIL: PID file not found\033[0m" + exit 1 +fi -$SERVER_CMD stop -sleep 3 +kill -TERM "$SERVER_PID" +# Brief yield so the signal handler runs and CancelableContext.Cancel() fires +sleep 0.2 +READY_DURING_SHUTDOWN=$(curl -s -w "\n[HTTP %{http_code}]" http://localhost:8080/api/ready 2>&1 || echo "[connection refused]") +echo "Readiness during shutdown: $READY_DURING_SHUTDOWN" + +# Wait for the process to exit cleanly (up to 30s) without sending another signal +echo "Waiting for server to exit..." +for i in {1..30}; do + if ! ps -p "$SERVER_PID" > /dev/null 2>&1; then + echo "Server stopped successfully" + rm -f "$PID_FILE" + break + fi + sleep 1 +done +if ps -p "$SERVER_PID" > /dev/null 2>&1; then + echo -e "\033[0;31m❌ FAIL: Server did not stop within 30s\033[0m" + kill -9 "$SERVER_PID" 2>/dev/null || true + exit 1 +fi +sleep 0.5 echo "" echo "Analyzing server logs..." @@ -201,6 +231,12 @@ fi echo "" echo -e "\033[0;32m🎉 GRACEFUL SHUTDOWN TEST PASSED!\033[0m" echo "All required logs are present and in correct order." + +echo "" +echo "📋 Full server log:" +echo "===============================" +cat "$LOG_FILE" | jq -r '"[\(.level | ascii_upcase)] \(.time | tostring) — \(.message)"' +echo "===============================" echo "" # Clean up diff --git a/scripts/test-opentelemetry.sh b/scripts/test-opentelemetry.sh index 750fc15..3d76cff 100755 --- a/scripts/test-opentelemetry.sh +++ b/scripts/test-opentelemetry.sh @@ -9,7 +9,8 @@ echo -e "\033[1;34m=== dance-lessons-coach OpenTelemetry Test ===\033[0m" echo "" # Configuration -PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach" +SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")") +PROJECT_DIR=$(dirname "$SCRIPTS_DIR") SERVER_CMD="./scripts/start-server.sh" LOG_FILE="server.log" PID_FILE="server.pid"