🔀 merge: integrate origin/main (PR #16 JSON logging fix) into restructure branch
PR #16 (commitc17fb4f) introduced 2 things while this restructure branch was in flight: 1. A fix in pkg/config/config.go (peekJSONLogging) so the very first log line is JSON when DLC_LOGGING_JSON=true. 2. Its own attempt to shorten AGENTS.md (1296 → 191 lines). Resolution: - Code/scripts changes from #16 (config.go, server.go, scripts/*, gitea-client.sh) accepted as-is via auto-merge. - AGENTS.md conflict resolved by keeping our version (130 lines, fully externalized to documentation/*.md). Our approach goes further in the lazy-loading direction (D-004, 128k context constraint), externalizing every detail instead of keeping minimal inline content. Caught up the missing /api/version endpoint in documentation/API.md (commitacebea3just before this merge) so we don't regress on the new endpoint introduced by #16. Pre-commit hooks already validated each upstream commit. Re-running on the merge to confirm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,31 @@ cmd_wait_job() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Comment on PR
|
# 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 <owner> <repo> <title> <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() {
|
cmd_comment_pr() {
|
||||||
local owner="$1"
|
local owner="$1"
|
||||||
local repo="$2"
|
local repo="$2"
|
||||||
@@ -215,7 +240,8 @@ cmd_comment_pr() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local endpoint="/repos/${owner}/${repo}/issues/${pr_number}/comments"
|
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"
|
api_request "POST" "$endpoint" "$data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +276,7 @@ main() {
|
|||||||
monitor-workflow) cmd_monitor_workflow "$@" ;;
|
monitor-workflow) cmd_monitor_workflow "$@" ;;
|
||||||
diagnose-job) cmd_diagnose_job "$@" ;;
|
diagnose-job) cmd_diagnose_job "$@" ;;
|
||||||
recent-workflows) cmd_recent_workflows "$@" ;;
|
recent-workflows) cmd_recent_workflows "$@" ;;
|
||||||
|
create-pr) cmd_create_pr "$@" ;;
|
||||||
comment-pr) cmd_comment_pr "$@" ;;
|
comment-pr) cmd_comment_pr "$@" ;;
|
||||||
pr-status) cmd_pr_status "$@" ;;
|
pr-status) cmd_pr_status "$@" ;;
|
||||||
list-issues) cmd_list_issues "$@" ;;
|
list-issues) cmd_list_issues "$@" ;;
|
||||||
@@ -274,6 +301,7 @@ main() {
|
|||||||
echo " monitor-workflow <owner> <repo> <workflow_run_id> [interval_seconds]" >&2
|
echo " monitor-workflow <owner> <repo> <workflow_run_id> [interval_seconds]" >&2
|
||||||
echo " diagnose-job <owner> <repo> <job_id>" >&2
|
echo " diagnose-job <owner> <repo> <job_id>" >&2
|
||||||
echo " recent-workflows <owner> <repo> [limit] [status_filter]" >&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 " comment-pr <owner> <repo> <pr_number> <comment>" >&2
|
||||||
echo " pr-status <owner> <repo> <pr_number>" >&2
|
echo " pr-status <owner> <repo> <pr_number>" >&2
|
||||||
echo " list-issues <owner> <repo> [state]" >&2
|
echo " list-issues <owner> <repo> [state]" >&2
|
||||||
|
|||||||
429
README.md
429
README.md
@@ -1,421 +1,98 @@
|
|||||||
# dance-lessons-coach
|
# dance-lessons-coach
|
||||||
|
|
||||||
[](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)
|
||||||
[](https://goreportcard.com/report/github.com/arcodange/dance-lessons-coach)
|
|
||||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/releases)
|
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://gitea.arcodange.lab/arcodange/dance-lessons-coach)
|
|
||||||
[](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
|
## Features
|
||||||
|
|
||||||
- Greet function with default behavior
|
- Versioned JSON API (`/api/v1`, `/api/v2`)
|
||||||
- Command-line interface
|
- Chi router with graceful shutdown
|
||||||
- JSON API with versioned endpoints
|
- Zerolog structured logging (console and JSON modes)
|
||||||
- Chi router integration
|
- Viper configuration (file + env vars)
|
||||||
- Zerolog for high-performance logging
|
- Readiness endpoint for Kubernetes / service mesh
|
||||||
- Viper for configuration management
|
- OpenTelemetry / Jaeger distributed tracing
|
||||||
- Graceful shutdown with context
|
- OpenAPI / Swagger UI (embedded in binary)
|
||||||
- Readiness endpoint for Kubernetes/service mesh integration
|
- PostgreSQL user service with JWT auth
|
||||||
- OpenTelemetry integration with Jaeger support
|
- BDD + unit tests
|
||||||
- OpenAPI/Swagger documentation
|
|
||||||
- Unit tests
|
|
||||||
- Go 1.26.1 compatible
|
|
||||||
|
|
||||||
## Installation
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
|
||||||
git clone https://gitea.arcodange.lab/arcodange/dance-lessons-coach.git
|
git clone https://gitea.arcodange.lab/arcodange/dance-lessons-coach.git
|
||||||
cd dance-lessons-coach
|
cd dance-lessons-coach
|
||||||
|
./scripts/build.sh # produces ./bin/server and ./bin/greet
|
||||||
# Build all binaries
|
./scripts/start-server.sh start
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
# Use the new Cobra CLI
|
|
||||||
./bin/dance-lessons-coach --help
|
|
||||||
|
|
||||||
# Or use the legacy greet CLI
|
|
||||||
go run ./cmd/greet
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
echo "DLC_DATABASE_HOST=postgres" >> $GITHUB_ENV
|
curl http://localhost:8080/api/health
|
||||||
echo "DLC_DATABASE_PORT=5432" >> $GITHUB_ENV
|
curl http://localhost:8080/api/v1/greet/Alice
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Status
|
Stop: `./scripts/start-server.sh stop`
|
||||||
|
|
||||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach)
|
## Greet CLI
|
||||||
|
|
||||||
=======
|
```bash
|
||||||
- ✅ **Linting**: Code quality checks with `go fmt` and `go vet`
|
go run ./cmd/greet # Hello world!
|
||||||
- ✅ **Version Management**: Automatic version detection
|
go run ./cmd/greet Alice # Hello Alice!
|
||||||
- ✅ **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 ./...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
## Configuration
|
||||||
|
|
||||||
Basic configuration options:
|
All options are available via `config.yaml` or `DLC_*` environment variables.
|
||||||
|
|
||||||
```bash
|
| Env var | Default | Description |
|
||||||
# Start with default configuration
|
|---------|---------|-------------|
|
||||||
./scripts/start-server.sh start
|
| `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
|
See `config.example.yaml` for a full template.
|
||||||
export DLC_SERVER_PORT=9090
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
|
|
||||||
# JSON logging
|
## API
|
||||||
export DLC_LOGGING_JSON=true
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
```
|
|
||||||
|
|
||||||
**See [AGENTS.md](AGENTS.md#configuration-management) for comprehensive configuration guide including:**
|
| Method | Path | Description |
|
||||||
- File-based configuration
|
|--------|------|-------------|
|
||||||
- Environment variables
|
| GET | `/api/health` | Liveness check |
|
||||||
- Configuration priority rules
|
| GET | `/api/ready` | Readiness check (503 during shutdown) |
|
||||||
- OpenTelemetry setup
|
| GET | `/api/version` | Version info (`?format=plain\|full\|json`) |
|
||||||
- Advanced scenarios
|
| GET | `/api/v1/greet/` | Default greeting |
|
||||||
|
| GET | `/api/v1/greet/{name}` | Named greeting |
|
||||||
## Usage
|
| POST | `/api/v2/greet` | V2 greeting with validation |
|
||||||
|
| GET | `/swagger/` | Swagger UI |
|
||||||
### 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!"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
go test ./... # unit + integration tests
|
||||||
go test ./...
|
./scripts/test-graceful-shutdown.sh # lifecycle + JSON logging validation
|
||||||
|
./scripts/test-opentelemetry.sh # tracing end-to-end
|
||||||
# Run specific package tests
|
|
||||||
go test ./pkg/greet/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
# Validate workflow structure
|
echo "your_token" > ~/.gitea_token
|
||||||
./scripts/cicd.sh validate
|
chmod 600 ~/.gitea_token
|
||||||
|
export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"
|
||||||
# Test workflow steps locally
|
|
||||||
./scripts/cicd.sh test-simple
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gitea Integration
|
Get a token at https://gitea.arcodange.lab → Profile → Settings → Applications.
|
||||||
```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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
## 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.
|
Key decisions are documented in [adr/](adr/). See [AGENTS.md](AGENTS.md) for the full development reference (commands, config, ADR index, commit conventions).
|
||||||
|
|
||||||
**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)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ func main() {
|
|||||||
log.Fatal().Err(err).Msg("Failed to load configuration")
|
log.Fatal().Err(err).Msg("Failed to load configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create readiness context to control readiness state
|
// Create readiness context to control readiness state.
|
||||||
readyCtx, readyCancel := context.WithCancel(context.Background())
|
// 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()
|
defer readyCancel()
|
||||||
|
|
||||||
// Create and run server
|
// Create and run server
|
||||||
@@ -57,4 +59,5 @@ func main() {
|
|||||||
if err := server.Run(); err != nil {
|
if err := server.Run(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Server failed")
|
log.Fatal().Err(err).Msg("Server failed")
|
||||||
}
|
}
|
||||||
|
log.Trace().Msg("Server exited")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,34 @@ type SamplerConfig struct {
|
|||||||
Ratio float64 `mapstructure:"ratio"`
|
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
|
// LoadConfig loads configuration from file, environment variables, and defaults
|
||||||
// Configuration priority: file > environment variables > defaults
|
// Configuration priority: file > environment variables > defaults
|
||||||
// To specify a custom config file path, set DLC_CONFIG_FILE environment variable
|
// To specify a custom config file path, set DLC_CONFIG_FILE environment variable
|
||||||
@@ -129,9 +157,17 @@ func LoadConfig() (*Config, error) {
|
|||||||
|
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
|
|
||||||
// Set up initial console logging for config loading messages
|
// Configure the logger format before emitting any log output.
|
||||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
// peekJSONLogging reads the JSON setting early (env var + config file pre-read)
|
||||||
log.Logger = log.Output(consoleWriter)
|
// 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
|
// Set default values
|
||||||
v.SetDefault("server.host", "0.0.0.0")
|
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)
|
return nil, fmt.Errorf("config unmarshal error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure log output format (JSON or console) first
|
// Setup logging based on configuration (level, output file, time format).
|
||||||
if config.Logging.JSON {
|
// The JSON/console format was already applied at the top of LoadConfig via
|
||||||
log.Logger = log.Output(os.Stderr)
|
// peekJSONLogging, so SetupLogging only needs to handle the remaining knobs.
|
||||||
} else {
|
|
||||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
|
||||||
log.Logger = log.Output(consoleWriter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup logging based on configuration
|
|
||||||
config.SetupLogging()
|
config.SetupLogging()
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ import (
|
|||||||
//go:embed docs/swagger.json
|
//go:embed docs/swagger.json
|
||||||
var swaggerJSON embed.FS
|
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 {
|
type Server struct {
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
readyCtx context.Context
|
readyCtx context.Context
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# This script starts the server in the background and provides control functions
|
# This script starts the server in the background and provides control functions
|
||||||
|
|
||||||
# Configuration
|
# 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"
|
SERVER_CMD="go run ./cmd/server"
|
||||||
LOG_FILE="server.log"
|
LOG_FILE="server.log"
|
||||||
PID_FILE="server.pid"
|
PID_FILE="server.pid"
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Configuration
|
# 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"
|
SERVER_CMD="./scripts/start-server.sh"
|
||||||
LOG_FILE="server.log"
|
LOG_FILE="server.log"
|
||||||
PID_FILE="server.pid"
|
PID_FILE="server.pid"
|
||||||
@@ -59,11 +60,40 @@ echo "Response: $GREET_NAME_RESPONSE"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Stopping server gracefully..."
|
echo "Stopping server gracefully..."
|
||||||
|
|
||||||
# Test readiness during shutdown (in background)
|
# Send SIGTERM once and probe /api/ready during the 1-second propagation window
|
||||||
(curl -s http://localhost:8080/api/ready > /dev/null 2>&1 &)
|
# 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
|
kill -TERM "$SERVER_PID"
|
||||||
sleep 3
|
# 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 ""
|
||||||
echo "Analyzing server logs..."
|
echo "Analyzing server logs..."
|
||||||
@@ -201,6 +231,12 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "\033[0;32m🎉 GRACEFUL SHUTDOWN TEST PASSED!\033[0m"
|
echo -e "\033[0;32m🎉 GRACEFUL SHUTDOWN TEST PASSED!\033[0m"
|
||||||
echo "All required logs are present and in correct order."
|
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 ""
|
echo ""
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ echo -e "\033[1;34m=== dance-lessons-coach OpenTelemetry Test ===\033[0m"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Configuration
|
# 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"
|
SERVER_CMD="./scripts/start-server.sh"
|
||||||
LOG_FILE="server.log"
|
LOG_FILE="server.log"
|
||||||
PID_FILE="server.pid"
|
PID_FILE="server.pid"
|
||||||
|
|||||||
Reference in New Issue
Block a user