Compare commits
71 Commits
vibe/batch
...
0b0476b796
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b0476b796 | |||
| dda4489a63 | |||
| 6dc9e6c0a3 | |||
| b6072bb10c | |||
| 40967f4e3c | |||
| 70c2eb554e | |||
| dbadff58e2 | |||
| b6da5e15e0 | |||
| 81dc31850d | |||
| 25a20d4380 | |||
| 98596f42d7 | |||
|
|
5a77224cb1 | ||
|
|
df000b5a0d | ||
| 0aedf829de | |||
|
|
0b7f52b08d | ||
|
|
c36fc7c9ff | ||
| d53abe1d60 | |||
| 98a3acee36 | |||
| 22e211f842 | |||
| 3dbd41b731 | |||
| 908e41ba7d | |||
| 21f21a2fdd | |||
| b0e3d35c24 | |||
| b09aeadd72 | |||
| 230ee699e4 | |||
| 778d8822dc | |||
| 7b0135c537 | |||
| 9467fd942c | |||
| 33e6fa3921 | |||
| 41f22d816c | |||
| a29b8bbdb5 | |||
| 2d06925a3f | |||
| d51bc23706 | |||
| cd977cfc2a | |||
| bc4089531e | |||
| 4df20585b8 | |||
| aa4823eb11 | |||
| 756fc5abfd | |||
| 1f92302eff | |||
| 4292f79c6a | |||
| e9fd453a88 | |||
| a75f87777b | |||
| 520da07bfe | |||
| 0011bed168 | |||
| de2e03519e | |||
| de22839eb7 | |||
| 577c2c0d6f | |||
| f62c7c49a1 | |||
| d1d618a2e6 | |||
| 5c8f42b33f | |||
| e7c6154eab | |||
| c6fa746e52 | |||
| 02bbfdb111 | |||
| f4bc0c8fdf | |||
| 58c1dda4cf | |||
| 1da6789e1b | |||
| 526417af9e | |||
| 08bab8e0a2 | |||
| 40a1bcda72 | |||
| 927fa3627f | |||
| 1e200c7522 | |||
| 58d2187acf | |||
| cb18db18f1 | |||
| 168efd3e99 | |||
| 3bad64026b | |||
| 8dcfeea814 | |||
| 8caefff43e | |||
|
|
7b33aea814 | ||
|
|
d88502a394 | ||
| 07f8bd65b7 | |||
| 695cd407f2 |
@@ -1,234 +0,0 @@
|
|||||||
# CI/CD Workflow Architecture
|
|
||||||
|
|
||||||
## 🗺️ Overview
|
|
||||||
|
|
||||||
The dance-lessons-coach project uses a **multi-workflow architecture** for better separation of concerns, maintainability, and flexibility.
|
|
||||||
|
|
||||||
## 📁 Workflow Files
|
|
||||||
|
|
||||||
### 1. `ci-cd.yaml` - Main CI/CD Pipeline
|
|
||||||
|
|
||||||
**Purpose**: Run tests, build binaries, and generate documentation
|
|
||||||
|
|
||||||
**Triggers**:
|
|
||||||
- Push to `main`, `ci/**`, `feature/**`, `fix/**`, `refactor/**` branches
|
|
||||||
- Pull requests to `main` branch
|
|
||||||
- Manual workflow dispatch
|
|
||||||
|
|
||||||
**Jobs**:
|
|
||||||
1. **build-cache** - Build and cache Docker build environment
|
|
||||||
2. **ci-pipeline** - Run tests, build binaries, generate Swagger docs
|
|
||||||
3. **trigger-docker-push** - Trigger separate Docker workflow on main branch
|
|
||||||
|
|
||||||
**Key Features**:
|
|
||||||
- Runs in container environment with all build tools
|
|
||||||
- Generates Swagger documentation
|
|
||||||
- Runs BDD and unit tests with PostgreSQL
|
|
||||||
- Updates badges and version information
|
|
||||||
- Triggers Docker workflow only on main branch
|
|
||||||
|
|
||||||
### 2. `docker-push.yaml` - Docker Image Publishing
|
|
||||||
|
|
||||||
**Purpose**: Build and push Docker images to registry
|
|
||||||
|
|
||||||
**Triggers**:
|
|
||||||
- Manual workflow dispatch only (no automatic triggers)
|
|
||||||
- Triggered by `ci-cd.yaml` on main branch
|
|
||||||
|
|
||||||
**Jobs**:
|
|
||||||
1. **docker-push** - Build production Docker image and push to registry
|
|
||||||
|
|
||||||
**Key Features**:
|
|
||||||
- Runs on host environment (access to Docker daemon)
|
|
||||||
- Uses dependency hash from build-cache
|
|
||||||
- Builds minimal Alpine-based production image
|
|
||||||
- Pushes multiple tags (version, latest, commit SHA)
|
|
||||||
|
|
||||||
## 🔧 Architecture Benefits
|
|
||||||
|
|
||||||
### 1. Clear Separation of Concerns
|
|
||||||
- **CI/CD Pipeline**: Testing and artifact generation
|
|
||||||
- **Docker Publishing**: Image building and registry operations
|
|
||||||
|
|
||||||
### 2. Proper Environment Isolation
|
|
||||||
- **CI jobs run in container**: Consistent build environment
|
|
||||||
- **Docker jobs run on host**: Access to Docker daemon
|
|
||||||
|
|
||||||
### 3. Flexible Testing
|
|
||||||
- Can trigger Docker workflow independently for testing
|
|
||||||
- No complex conditional logic in main workflow
|
|
||||||
- Easier to debug and maintain
|
|
||||||
|
|
||||||
### 4. Better Security
|
|
||||||
- Docker operations isolated in separate workflow
|
|
||||||
- Clear dependency between test success and deployment
|
|
||||||
- Manual trigger capability for emergency situations
|
|
||||||
|
|
||||||
## 🚀 Usage Examples
|
|
||||||
|
|
||||||
### Trigger Full CI/CD Pipeline
|
|
||||||
```bash
|
|
||||||
# Automatically triggered on push to main branch
|
|
||||||
# Or manually:
|
|
||||||
./scripts/gitea-client.sh trigger-workflow arcodange dance-lessons-coach ci-cd.yaml main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trigger Docker Push Manually
|
|
||||||
```bash
|
|
||||||
# Get dependency hash from build-cache job first
|
|
||||||
DEPS_HASH="abc123def456"
|
|
||||||
|
|
||||||
# Trigger Docker workflow manually
|
|
||||||
./scripts/gitea-client.sh trigger-workflow arcodange dance-lessons-coach docker-push.yaml main --deps_hash $DEPS_HASH
|
|
||||||
```
|
|
||||||
|
|
||||||
### Workflow Dispatch Parameters (docker-push.yaml)
|
|
||||||
- `deps_hash` (required): Dependency hash from build-cache job
|
|
||||||
- `ref` (optional): Git reference (branch/tag), defaults to current
|
|
||||||
|
|
||||||
## 🔗 Workflow Dependencies
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[Push to main] --> B[ci-cd.yaml]
|
|
||||||
B --> C[build-cache job]
|
|
||||||
B --> D[ci-pipeline job]
|
|
||||||
D --> E[trigger-docker-push job]
|
|
||||||
E --> F[docker-push.yaml]
|
|
||||||
F --> G[docker-push job]
|
|
||||||
G --> H[Docker Registry]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Best Practices
|
|
||||||
|
|
||||||
### 1. Always Run CI First
|
|
||||||
- Docker workflow should only be triggered after CI passes
|
|
||||||
- Maintains quality gate before deployment
|
|
||||||
|
|
||||||
### 2. Use Dependency Hash
|
|
||||||
- Ensures consistent builds across workflows
|
|
||||||
- Pass hash from build-cache to docker-push
|
|
||||||
|
|
||||||
### 3. Manual Testing
|
|
||||||
- Use separate Docker workflow for testing image builds
|
|
||||||
- Avoids polluting main branch with test images
|
|
||||||
|
|
||||||
### 4. Monitor Both Workflows
|
|
||||||
- CI/CD workflow for test results and artifacts
|
|
||||||
- Docker workflow for image build and push status
|
|
||||||
|
|
||||||
## 🎯 Docker Build Strategy Decision
|
|
||||||
|
|
||||||
### 🏆 Chosen Approach: Attempt 2 (Standard Dockerfile)
|
|
||||||
|
|
||||||
After extensive testing of multiple approaches, we selected **Attempt 2** as the optimal Docker build strategy.
|
|
||||||
|
|
||||||
#### ⚡ Why Attempt 2 Won:
|
|
||||||
|
|
||||||
**1. Simplicity (60% smaller workflow)**
|
|
||||||
- 73 lines vs 158 lines in complex approaches
|
|
||||||
- No inline Dockerfile generation
|
|
||||||
- Standard `docker build -f docker/Dockerfile .` command
|
|
||||||
|
|
||||||
**2. Better Performance**
|
|
||||||
- No artifact/cache action overhead
|
|
||||||
- Natural Docker layer caching works optimally
|
|
||||||
- Faster execution without complex variable substitutions
|
|
||||||
|
|
||||||
**3. Superior Reliability**
|
|
||||||
- Proven standard Docker build process
|
|
||||||
- Easier to debug and maintain
|
|
||||||
- Fewer moving parts = fewer failures
|
|
||||||
|
|
||||||
**4. Better Maintainability**
|
|
||||||
- Uses standard Dockerfile (easier to understand)
|
|
||||||
- No complex YAML templating
|
|
||||||
- Clear separation of concerns
|
|
||||||
|
|
||||||
#### 🗑️ Why We Rejected Other Approaches:
|
|
||||||
|
|
||||||
**Attempt 1 (Inline Dockerfile):**
|
|
||||||
- Complex YAML templating
|
|
||||||
- Harder to debug and maintain
|
|
||||||
- No significant performance benefit
|
|
||||||
|
|
||||||
**Attempt 3 (Build Cache Image):**
|
|
||||||
- Added complexity with cache management
|
|
||||||
- Slower due to artifact actions overhead
|
|
||||||
- More prone to cache invalidation issues
|
|
||||||
|
|
||||||
**Attempt 4 (Template File):**
|
|
||||||
- Added unnecessary file management
|
|
||||||
- No clear advantage over standard Dockerfile
|
|
||||||
- More complex workflow
|
|
||||||
|
|
||||||
### 📊 Performance Comparison:
|
|
||||||
|
|
||||||
| Approach | Lines of Code | Complexity | Reliability | Maintainability |
|
|
||||||
|----------|---------------|------------|-------------|-----------------|
|
|
||||||
| **Attempt 2** | 73 | Low | High | Excellent |
|
|
||||||
| Attempt 1 | 158 | High | Medium | Poor |
|
|
||||||
| Attempt 3 | 125 | Medium | Medium | Fair |
|
|
||||||
| Attempt 4 | 110 | Medium | High | Good |
|
|
||||||
|
|
||||||
### 🔧 Implementation Details:
|
|
||||||
|
|
||||||
**Standard Dockerfile Approach:**
|
|
||||||
```yaml
|
|
||||||
- name: Build and push Docker image
|
|
||||||
run: |
|
|
||||||
docker build -t dance-lessons-coach -f docker/Dockerfile .
|
|
||||||
docker tag dance-lessons-coach "$IMAGE_NAME"
|
|
||||||
docker push "$IMAGE_NAME"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Benefits:**
|
|
||||||
- Uses multi-stage builds for optimization
|
|
||||||
- Standard Docker layer caching works naturally
|
|
||||||
- Easy to understand and modify
|
|
||||||
- Proven reliability in production
|
|
||||||
|
|
||||||
## 🎯 Future Enhancements
|
|
||||||
|
|
||||||
### Potential Improvements:
|
|
||||||
- Add workflow status badges to README
|
|
||||||
- Implement workflow chaining with outputs
|
|
||||||
- Add matrix builds for multiple architectures
|
|
||||||
- Implement canary deployment workflow
|
|
||||||
- Add rollback capability
|
|
||||||
|
|
||||||
### Architecture Considerations:
|
|
||||||
- Keep workflows focused on single responsibilities
|
|
||||||
- Maintain clear separation between test and deploy
|
|
||||||
- Document all workflow triggers and conditions
|
|
||||||
- Monitor workflow execution times and optimize
|
|
||||||
|
|
||||||
## 📝 Maintenance
|
|
||||||
|
|
||||||
### Adding New Jobs:
|
|
||||||
- Add to appropriate workflow based on responsibility
|
|
||||||
- CI-related jobs → `ci-cd.yaml`
|
|
||||||
- Docker-related jobs → `docker-push.yaml`
|
|
||||||
|
|
||||||
### Modifying Triggers:
|
|
||||||
- Update trigger conditions in respective workflow files
|
|
||||||
- Test changes thoroughly before merging
|
|
||||||
|
|
||||||
### Debugging:
|
|
||||||
- Check workflow logs in Gitea Actions
|
|
||||||
- Use `gitea-client.sh diagnose-job` for detailed analysis
|
|
||||||
- Monitor workflow dependencies and execution order
|
|
||||||
|
|
||||||
## 🔒 Security
|
|
||||||
|
|
||||||
### Secrets Management:
|
|
||||||
- Docker registry credentials stored in Gitea secrets
|
|
||||||
- Never hardcode credentials in workflow files
|
|
||||||
- Use GitHub token for workflow dispatch
|
|
||||||
|
|
||||||
### Access Control:
|
|
||||||
- Only authorized users can trigger workflows
|
|
||||||
- Manual approval required for production deployments
|
|
||||||
- Audit logs available for all workflow executions
|
|
||||||
|
|
||||||
This architecture provides a clean, maintainable, and secure CI/CD pipeline that scales well with project growth while maintaining clear separation of concerns.
|
|
||||||
@@ -132,8 +132,7 @@ jobs:
|
|||||||
name: CI Pipeline
|
name: CI Pipeline
|
||||||
needs: build-cache
|
needs: build-cache
|
||||||
runs-on: ubuntu-latest-ca
|
runs-on: ubuntu-latest-ca
|
||||||
# Skip conditions: standard skip ci + actor check + respect skip_ci input
|
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot'"
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot' && (!github.event.inputs.skip_ci || github.event.inputs.skip_ci == 'false')"
|
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ needs.build-cache.outputs.deps_hash }}
|
image: ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}-build-cache:${{ needs.build-cache.outputs.deps_hash }}
|
||||||
@@ -219,12 +218,6 @@ jobs:
|
|||||||
export DLC_DATABASE_PASSWORD=postgres
|
export DLC_DATABASE_PASSWORD=postgres
|
||||||
export DLC_DATABASE_NAME=dance_lessons_coach_bdd_test
|
export DLC_DATABASE_NAME=dance_lessons_coach_bdd_test
|
||||||
export DLC_DATABASE_SSL_MODE=disable
|
export DLC_DATABASE_SSL_MODE=disable
|
||||||
# T12: per-package isolated Postgres schema with migrations (re-enables what
|
|
||||||
# PR #26 attempted but couldn't deliver because the empty schemas had no tables).
|
|
||||||
# The fix: testserver Start() now builds a per-package isolated repo via
|
|
||||||
# user.NewPostgresRepositoryFromDSN which DOES run AutoMigrate against the new
|
|
||||||
# schema. Packages then run in parallel (~2.85x speedup observed locally).
|
|
||||||
export BDD_SCHEMA_ISOLATION=true
|
|
||||||
./scripts/run-bdd-tests.sh
|
./scripts/run-bdd-tests.sh
|
||||||
|
|
||||||
# Generate BDD coverage report
|
# Generate BDD coverage report
|
||||||
@@ -299,13 +292,7 @@ jobs:
|
|||||||
# Check for version bump on main branch
|
# Check for version bump on main branch
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||||
echo "🔖 Checking for version bump..."
|
echo "🔖 Checking for version bump..."
|
||||||
# Read commit message from git, NOT from the workflow event payload.
|
./scripts/ci-version-bump.sh "${{ github.event.head_commit.message }}" --no-push
|
||||||
# The event-payload expression is interpolated literally into the
|
|
||||||
# rendered script (even inside comments — see PR #38 + #46), so any
|
|
||||||
# backtick / unbalanced quote / multi-line body breaks bash parsing.
|
|
||||||
# git log is interpolation-free and safe.
|
|
||||||
COMMIT_MSG=$(git log -1 --pretty=%B)
|
|
||||||
./scripts/ci-version-bump.sh "$COMMIT_MSG" --no-push
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Single push for all commits (this is the ONLY push in the entire workflow)
|
# Single push for all commits (this is the ONLY push in the entire workflow)
|
||||||
@@ -317,23 +304,47 @@ jobs:
|
|||||||
echo "ℹ️ No changes to push"
|
echo "ℹ️ No changes to push"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Docker build and push (main branch only)
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.CI_REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.PACKAGES_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
# Trigger Docker push workflow on main branch
|
|
||||||
trigger-docker-push:
|
|
||||||
name: Trigger Docker Push
|
|
||||||
needs: [build-cache, ci-pipeline]
|
|
||||||
runs-on: ubuntu-latest-ca
|
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.actor != 'ci-bot' && github.ref == 'refs/heads/main'"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Trigger Docker Push Workflow
|
|
||||||
run: |
|
run: |
|
||||||
echo "🚀 Triggering Docker Push workflow..."
|
source VERSION
|
||||||
curl -X POST \
|
IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}"
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN || secrets.PACKAGES_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
# Use the template file with proper dependency hash replacement
|
||||||
"${{ env.GITEA_INTERNAL }}api/v1/repos/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}/actions/workflows/docker-push.yaml/dispatches" \
|
DEPS_HASH="${{ needs.build-cache.outputs.deps_hash }}"
|
||||||
-d '{"ref":"${{ github.ref }}"}'
|
echo "Using dependency hash: $DEPS_HASH"
|
||||||
echo "✅ Docker Push workflow triggered successfully!"
|
|
||||||
|
# Create Dockerfile.prod from template
|
||||||
|
sed "s/{{DEPS_HASH}}/$DEPS_HASH/g" docker/Dockerfile.prod.template > docker/Dockerfile.prod
|
||||||
|
|
||||||
|
TAGS="$IMAGE_VERSION latest ${{ github.sha }}"
|
||||||
|
echo "Building Docker image with tags: $TAGS"
|
||||||
|
|
||||||
|
# Build the production image
|
||||||
|
docker build -t dance-lessons-coach -f docker/Dockerfile.prod .
|
||||||
|
|
||||||
|
for TAG in $TAGS; do
|
||||||
|
IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG"
|
||||||
|
echo "Tagging and pushing: $IMAGE_NAME"
|
||||||
|
docker tag dance-lessons-coach "$IMAGE_NAME"
|
||||||
|
docker push "$IMAGE_NAME"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Show published images
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
source VERSION
|
||||||
|
IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}"
|
||||||
|
echo "📦 Published Docker images:"
|
||||||
|
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$IMAGE_VERSION"
|
||||||
|
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:latest"
|
||||||
|
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:${{ github.sha }}"
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
# dance-lessons-coach Docker Push Workflow
|
|
||||||
# Separate workflow for Docker image building and pushing
|
|
||||||
# Can be triggered manually or by CI/CD workflow
|
|
||||||
|
|
||||||
name: Docker Push
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
ref:
|
|
||||||
description: 'Git reference (branch/tag)'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
default: ''
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- 'README.md'
|
|
||||||
- 'AGENTS.md'
|
|
||||||
- 'CHANGELOG.md'
|
|
||||||
- 'AGENT_CHANGELOG.md'
|
|
||||||
- 'documentation/**'
|
|
||||||
- 'adr/**'
|
|
||||||
- 'chart/**'
|
|
||||||
- 'features/**'
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
env:
|
|
||||||
GITEA_INTERNAL: "https://gitea.arcodange.lab/"
|
|
||||||
GITEA_EXTERNAL: "https://gitea.arcodange.fr/"
|
|
||||||
GITEA_ORG: "arcodange"
|
|
||||||
GITEA_REPO: "dance-lessons-coach"
|
|
||||||
CI_REGISTRY: "gitea.arcodange.lab"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker-push:
|
|
||||||
name: Docker Push
|
|
||||||
runs-on: ubuntu-latest-ca
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.inputs.ref || github.ref }}
|
|
||||||
|
|
||||||
- name: Login to Gitea Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.CI_REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.PACKAGES_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
run: |
|
|
||||||
source VERSION
|
|
||||||
IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}"
|
|
||||||
|
|
||||||
TAGS="$IMAGE_VERSION latest ${{ github.sha }}"
|
|
||||||
echo "Building Docker image with tags: $TAGS"
|
|
||||||
|
|
||||||
# Build using the standard Dockerfile (Attempt 2 - simplest approach)
|
|
||||||
docker build -t dance-lessons-coach -f docker/Dockerfile .
|
|
||||||
|
|
||||||
for TAG in $TAGS; do
|
|
||||||
IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG"
|
|
||||||
echo "Tagging and pushing: $IMAGE_NAME"
|
|
||||||
docker tag dance-lessons-coach "$IMAGE_NAME"
|
|
||||||
docker push "$IMAGE_NAME"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Show published images
|
|
||||||
run: |
|
|
||||||
source VERSION
|
|
||||||
IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}"
|
|
||||||
echo "📦 Published Docker images:"
|
|
||||||
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$IMAGE_VERSION"
|
|
||||||
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:latest"
|
|
||||||
echo " - ${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:${{ github.sha }}"
|
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -34,14 +34,3 @@ config/runner
|
|||||||
coverage.txt
|
coverage.txt
|
||||||
trigger.txt
|
trigger.txt
|
||||||
test_trigger.txt
|
test_trigger.txt
|
||||||
|
|
||||||
# Frontend
|
|
||||||
frontend/node_modules/
|
|
||||||
frontend/.nuxt/
|
|
||||||
frontend/.output/
|
|
||||||
frontend/dist/
|
|
||||||
frontend/.env
|
|
||||||
frontend/.cache/
|
|
||||||
frontend/storybook-static/
|
|
||||||
frontend/test-results/
|
|
||||||
frontend/playwright-report/
|
|
||||||
|
|||||||
@@ -203,31 +203,6 @@ 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"
|
||||||
@@ -240,8 +215,7 @@ 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
|
local data="{\"body\": \"${comment}\"}"
|
||||||
data=$(jq -n --arg body "$comment" '{body: $body}')
|
|
||||||
api_request "POST" "$endpoint" "$data"
|
api_request "POST" "$endpoint" "$data"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +250,6 @@ 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 "$@" ;;
|
||||||
@@ -301,7 +274,6 @@ 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
|
||||||
|
|||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,42 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- ✨ `GET /api/v1/uptime` endpoint (PR #67) — returns server start_time and uptime_seconds
|
|
||||||
- 📝 mkcert local HTTPS doc + Makefile `cert` target (PR #68) — prep for ADR-0028 Phase B OIDC callbacks
|
|
||||||
- ✨ `pkg/auth/` skeleton for OpenID Connect (PR #69) — types + client surface, handlers come later (Phase B.3+)
|
|
||||||
- 📝 ADR-0028 Phase B roadmap document (PR #71) — outlines remaining B.3 / B.4 / B.5 work
|
|
||||||
- ✨ `pkg/auth/` OIDC client implementation : Discover, RefreshJWKS, ExchangeCode, ValidateIDToken (PR #74) — completes ADR-0028 Phase B.3
|
|
||||||
- ✨ OIDC HTTP handlers : `/api/v1/auth/oidc/{provider}/start` and `/callback` with PKCE + sign-up-on-first-use (PR #75) — completes ADR-0028 Phase B.4
|
|
||||||
- 🧪 OIDC handler unit tests covering start/callback rejection paths and PKCE redirect (PR #76)
|
|
||||||
- 📝 `documentation/AUTH.md` synthesis covering Phase A + B current state (PR #73)
|
|
||||||
- 📝 `documentation/MISTRAL-AUTONOMOUS-PATTERN.md` contributor guide for the Mistral autonomous pattern that ships PRs (PR #78)
|
|
||||||
- 📝 PHASE_B_ROADMAP marks B.3 + B.4 done (PR #80)
|
|
||||||
- 📝 documentation/2026-05-05-AUTONOMOUS-SESSION-RECAP.md captures the day's 24 Mistral autonomous PRs (PR #81)
|
|
||||||
- 📝 README link to Mistral autonomous pattern doc (PR #83)
|
|
||||||
- 📝 documentation/STATUS.md project snapshot for onboarding (PR #85)
|
|
||||||
- 📝 documentation guides cherry-picked from PR #17 : CLI.md, CODE_EXAMPLES.md, HISTORY.md, OBSERVABILITY.md, ROADMAP.md, TROUBLESHOOTING.md (PR #87)
|
|
||||||
- 🔒 redact JWT tokens and HMAC secrets in trace logs of pkg/user/auth_service.go via sha256 fingerprints (PR #88)
|
|
||||||
- ✨ Dockerfile (root) + Helm chart for k3s homelab deployment, degraded mode without DB/SMTP/Vault (PR #89)
|
|
||||||
- ♻️ move UserContextKey + GetAuthenticatedUserFromContext from pkg/greet to pkg/auth (PR #90)
|
|
||||||
- ♻️ split AuthMiddleware into OptionalHandler + RequiredHandler with RFC 6750 challenge headers, narrow tokenValidator interface, case-insensitive Bearer (PR #91)
|
|
||||||
- 🧪 unit tests for AuthMiddleware Optional/Required handlers + extractBearerToken edge cases (PR #92)
|
|
||||||
- 📝 refresh AGENTS.md and README.md to reflect auth endpoints (magic-link, OIDC, JWT admin), pkg/auth, pkg/email, pkg/user/api packages, and 30-ADR index. Endpoints listing decision : curated short list + pointer to swagger as source of truth (PR #93)
|
|
||||||
- 🤖 auto-build Docker image on push to main (paths-ignore for docs) + fix root Dockerfile swag init step (PR #94)
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-05-05
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Magic-link passwordless authentication (ADR-0028 Phases A.1 through A.5, PRs #59-#63)
|
|
||||||
- OIDC provider config skeleton (ADR-0028 Phase B.1 prep, PR #64)
|
|
||||||
- Magic-link expired-token cleanup loop (PR #65)
|
|
||||||
- Mailpit local SMTP infrastructure (ADR-0029)
|
|
||||||
- BDD parallel email assertion strategy (ADR-0030)
|
|
||||||
43
Dockerfile
43
Dockerfile
@@ -1,43 +0,0 @@
|
|||||||
# Build dance-lessons-coach Docker image
|
|
||||||
FROM golang:1.26-alpine AS builder
|
|
||||||
|
|
||||||
# Install git (required for go mod download)
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy go module files and download dependencies
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Copy entire source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Generate Swagger documentation if not already present
|
|
||||||
# (pkg/server/docs/ is gitignored ; the binary //go:embed depends on it)
|
|
||||||
RUN if [ ! -f pkg/server/docs/swagger.json ]; then \
|
|
||||||
go install github.com/swaggo/swag/cmd/swag@latest && \
|
|
||||||
cd pkg/server && go generate ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build the server binary
|
|
||||||
RUN go build -o app ./cmd/server
|
|
||||||
|
|
||||||
# Final lightweight stage
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
# Install CA certificates for HTTPS
|
|
||||||
RUN apk --no-cache add ca-certificates
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /root/
|
|
||||||
|
|
||||||
# Copy binary from builder stage
|
|
||||||
COPY --from=builder /app/app .
|
|
||||||
|
|
||||||
# Expose port 8080
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# Start the server
|
|
||||||
CMD ["./app"]
|
|
||||||
24
Makefile
24
Makefile
@@ -1,24 +0,0 @@
|
|||||||
# dance-lessons-coach Makefile — minimal targets for local development.
|
|
||||||
# This is a starter Makefile ; expand as needed (build, test, run, etc.).
|
|
||||||
# Existing build/test workflows live in scripts/ and remain authoritative.
|
|
||||||
|
|
||||||
CERT_DIR := ./certs
|
|
||||||
|
|
||||||
.PHONY: help cert clean-cert
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " cert Generate local-dev TLS certs via mkcert (cf. documentation/MKCERT.md)"
|
|
||||||
@echo " clean-cert Remove generated TLS certs"
|
|
||||||
@echo " help Show this help"
|
|
||||||
|
|
||||||
cert: $(CERT_DIR)
|
|
||||||
@command -v mkcert >/dev/null 2>&1 || { echo >&2 "mkcert not found. See documentation/MKCERT.md to install."; exit 1; }
|
|
||||||
mkcert -cert-file $(CERT_DIR)/dev-cert.pem -key-file $(CERT_DIR)/dev-key.pem localhost 127.0.0.1 ::1
|
|
||||||
@echo "Certs ready at $(CERT_DIR)/. Cf. documentation/MKCERT.md for usage."
|
|
||||||
|
|
||||||
$(CERT_DIR):
|
|
||||||
mkdir -p $(CERT_DIR)
|
|
||||||
|
|
||||||
clean-cert:
|
|
||||||
rm -rf $(CERT_DIR)
|
|
||||||
435
README.md
435
README.md
@@ -1,110 +1,421 @@
|
|||||||
# dance-lessons-coach
|
# dance-lessons-coach
|
||||||
|
|
||||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml)
|
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach)
|
||||||
[](https://goreportcard.com/report/github.com/arcodange/dance-lessons-coach)
|
[](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)
|
||||||
[](https://gitea.arcodange.lab/arcodange/dance-lessons-coach)
|
[](https://gitea.arcodange.lab/arcodange/dance-lessons-coach)
|
||||||
|
|
||||||
Go web service demonstrating idiomatic package structure, versioned JSON API, and production-ready features.
|
A Go project demonstrating idiomatic package structure, CLI implementation, and JSON API with Chi router.
|
||||||
|
=======
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Versioned JSON API (`/api/v1`, `/api/v2`)
|
- Greet function with default behavior
|
||||||
- Chi router with graceful shutdown
|
- Command-line interface
|
||||||
- Zerolog structured logging (console and JSON modes)
|
- JSON API with versioned endpoints
|
||||||
- Viper configuration (file + env vars)
|
- Chi router integration
|
||||||
- Readiness endpoint for Kubernetes / service mesh
|
- Zerolog for high-performance logging
|
||||||
- OpenTelemetry / Jaeger distributed tracing
|
- Viper for configuration management
|
||||||
- OpenAPI / Swagger UI (embedded in binary, source of truth at `/swagger/doc.json`)
|
- Graceful shutdown with context
|
||||||
- Username + password authentication with JWT (rotating secrets)
|
- Readiness endpoint for Kubernetes/service mesh integration
|
||||||
- Passwordless magic-link authentication (email-delivered, ADR-0028 Phase A)
|
- OpenTelemetry integration with Jaeger support
|
||||||
- OIDC authentication with PKCE (multi-provider, ADR-0028 Phase B)
|
- OpenAPI/Swagger documentation
|
||||||
- PostgreSQL user persistence with GORM
|
- Unit tests
|
||||||
- BDD + unit tests (Godog)
|
- Go 1.26.1 compatible
|
||||||
- Mistral autonomous PR pattern (cf. [documentation/MISTRAL-AUTONOMOUS-PATTERN.md](documentation/MISTRAL-AUTONOMOUS-PATTERN.md))
|
|
||||||
|
|
||||||
## Quick Start
|
## Installation
|
||||||
|
|
||||||
```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
|
|
||||||
./scripts/start-server.sh start
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
curl http://localhost:8080/api/health
|
echo "DLC_DATABASE_HOST=postgres" >> $GITHUB_ENV
|
||||||
curl http://localhost:8080/api/v1/greet/Alice
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
Stop: `./scripts/start-server.sh stop`
|
### Status
|
||||||
|
|
||||||
## Greet CLI
|
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach)
|
||||||
|
|
||||||
```bash
|
=======
|
||||||
go run ./cmd/greet # Hello world!
|
- ✅ **Linting**: Code quality checks with `go fmt` and `go vet`
|
||||||
go run ./cmd/greet Alice # Hello Alice!
|
- ✅ **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 ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
All options are available via `config.yaml` or `DLC_*` environment variables.
|
Basic configuration options:
|
||||||
|
|
||||||
| Env var | Default | Description |
|
```bash
|
||||||
|---------|---------|-------------|
|
# Start with default configuration
|
||||||
| `DLC_SERVER_PORT` | `8080` | Listening port |
|
./scripts/start-server.sh start
|
||||||
| `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 |
|
|
||||||
|
|
||||||
See `config.example.yaml` for a full template.
|
# Custom port
|
||||||
|
export DLC_SERVER_PORT=9090
|
||||||
|
./scripts/start-server.sh start
|
||||||
|
|
||||||
## API
|
# JSON logging
|
||||||
|
export DLC_LOGGING_JSON=true
|
||||||
|
./scripts/start-server.sh start
|
||||||
|
```
|
||||||
|
|
||||||
The full interactive list is in the Swagger UI at `/swagger/` (source of truth at `/swagger/doc.json`). Most-used endpoints :
|
**See [AGENTS.md](AGENTS.md#configuration-management) for comprehensive configuration guide including:**
|
||||||
|
- File-based configuration
|
||||||
|
- Environment variables
|
||||||
|
- Configuration priority rules
|
||||||
|
- OpenTelemetry setup
|
||||||
|
- Advanced scenarios
|
||||||
|
|
||||||
| Method | Path | Description |
|
## Usage
|
||||||
|--------|------|-------------|
|
|
||||||
| GET | `/api/health` | Liveness check |
|
|
||||||
| GET | `/api/ready` | Readiness check (503 during shutdown) |
|
|
||||||
| GET | `/api/version` | Version info |
|
|
||||||
| GET | `/api/v1/greet/{name}` | Named greeting |
|
|
||||||
| POST | `/api/v1/auth/login` | Login (JWT) |
|
|
||||||
| POST | `/api/v1/auth/magic-link/request` | Passwordless magic-link |
|
|
||||||
| GET | `/api/v1/auth/oidc/{provider}/start` | OIDC login |
|
|
||||||
| GET | `/swagger/` | Swagger UI |
|
|
||||||
|
|
||||||
This decision is intentional : the markdown table drifts ; swagger.json doesn't (it's regenerated from `swag` annotations on every build). Curated short list here for discovery, swagger for completeness.
|
### 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
|
||||||
go test ./... # unit + integration tests
|
# Run all tests
|
||||||
./scripts/test-graceful-shutdown.sh # lifecycle + JSON logging validation
|
go test ./...
|
||||||
./scripts/test-opentelemetry.sh # tracing end-to-end
|
|
||||||
|
# Run specific package tests
|
||||||
|
go test ./pkg/greet/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gitea Client
|
## CI/CD
|
||||||
|
|
||||||
AI agent helper script at `.vibe/skills/gitea-client/scripts/gitea-client.sh`.
|
dance-lessons-coach includes a comprehensive CI/CD pipeline with multiple testing options:
|
||||||
|
|
||||||
Auth setup:
|
### Local Testing (No Gitea Required)
|
||||||
```bash
|
```bash
|
||||||
echo "your_token" > ~/.gitea_token
|
# Validate workflow structure
|
||||||
chmod 600 ~/.gitea_token
|
./scripts/cicd.sh validate
|
||||||
export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"
|
|
||||||
|
# Test workflow steps locally
|
||||||
|
./scripts/cicd.sh test-simple
|
||||||
```
|
```
|
||||||
|
|
||||||
Get a token at https://gitea.arcodange.lab → Profile → Settings → Applications.
|
### 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Key decisions are documented in [adr/](adr/). See [AGENTS.md](AGENTS.md) for the full development reference (commands, config, ADR index, commit conventions).
|
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)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Use Go 1.26.1 as the standard Go version
|
# Use Go 1.26.1 as the standard Go version
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-01
|
* Date: 2026-04-01
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Use Chi router for HTTP routing
|
# Use Chi router for HTTP routing
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-02
|
* Date: 2026-04-02
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Use Zerolog for structured logging
|
# Use Zerolog for structured logging
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-02
|
* Date: 2026-04-02
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Adopt interface-based design pattern
|
# Adopt interface-based design pattern
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-02
|
* Date: 2026-04-02
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Implement graceful shutdown with readiness endpoints
|
# Implement graceful shutdown with readiness endpoints
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-03
|
* Date: 2026-04-03
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Use Viper for configuration management
|
# Use Viper for configuration management
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-03
|
* Date: 2026-04-03
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Integrate OpenTelemetry for distributed tracing
|
# Integrate OpenTelemetry for distributed tracing
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-04
|
* Date: 2026-04-04
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Adopt BDD with Godog for behavioral testing
|
# Adopt BDD with Godog for behavioral testing
|
||||||
|
|
||||||
**Status:** Accepted
|
* Status: Accepted
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-05
|
* Date: 2026-04-05
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Combine BDD and Swagger-based testing
|
# Combine BDD and Swagger-based testing
|
||||||
|
|
||||||
**Status:** Implemented (BDD + OpenAPI documentation operational; SDK generation explicitly out of scope — would require a fresh ADR if reopened)
|
* Status: ✅ Partially Implemented (BDD + Documentation only)
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-05
|
* Date: 2026-04-05
|
||||||
**Last Updated:** 2026-05-05
|
* Last Updated: 2026-04-05
|
||||||
|
* Implementation Status: BDD testing and OpenAPI documentation completed, SDK generation deferred
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ Chosen option: "Hybrid approach" because it provides the best combination of beh
|
|||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
|
|
||||||
**Status**: ✅ Implemented (BDD + OpenAPI documentation operational; SDK generation explicitly out of scope)
|
**Status**: ✅ Partially Implemented (BDD + Documentation only)
|
||||||
|
|
||||||
### What We Actually Have
|
### What We Actually Have
|
||||||
|
|
||||||
@@ -328,7 +329,7 @@ If we need SDK generation in the future:
|
|||||||
- Add SDK-based BDD tests
|
- Add SDK-based BDD tests
|
||||||
- Implement true hybrid testing approach
|
- Implement true hybrid testing approach
|
||||||
|
|
||||||
**Current Status:** ✅ Implemented (BDD + OpenAPI documentation; SDK generation out of scope)
|
**Current Status:** ✅ Partially Implemented (BDD + Documentation)
|
||||||
**BDD Tests:** http://localhost:8080/api/health (all passing)
|
**BDD Tests:** http://localhost:8080/api/health (all passing)
|
||||||
**OpenAPI Docs:** http://localhost:8080/swagger/
|
**OpenAPI Docs:** http://localhost:8080/swagger/
|
||||||
**OpenAPI Spec:** http://localhost:8080/swagger/doc.json
|
**OpenAPI Spec:** http://localhost:8080/swagger/doc.json
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# 13. OpenAPI/Swagger Toolchain Selection
|
# 13. OpenAPI/Swagger Toolchain Selection
|
||||||
|
|
||||||
**Date:** 2026-04-05
|
**Date:** 2026-04-05
|
||||||
**Status:** Implemented (OpenAPI documentation operational; SDK generation explicitly out of scope, see ADR-0009)
|
**Status:** ✅ Partially Implemented (Documentation only)
|
||||||
**Authors:** Arcodange Team
|
**Authors:** Arcodange Team
|
||||||
**Implementation Date:** 2026-04-05
|
**Implementation Date:** 2026-04-05
|
||||||
**Last Updated:** 2026-05-05
|
**Last Updated:** 2026-04-05
|
||||||
|
**Status:** OpenAPI documentation operational, SDK generation deferred
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -982,7 +983,7 @@ If we need SDK generation in the future:
|
|||||||
4. Implement request validation middleware
|
4. Implement request validation middleware
|
||||||
5. Migrate to OpenAPI 3.0 if needed
|
5. Migrate to OpenAPI 3.0 if needed
|
||||||
|
|
||||||
**Current Status:** ✅ Implemented (OpenAPI documentation; SDK generation out of scope)
|
**Current Status:** ✅ Partially Implemented (Documentation only)
|
||||||
**Implementation:** swaggo/swag with embedded documentation
|
**Implementation:** swaggo/swag with embedded documentation
|
||||||
**Documentation:** http://localhost:8080/swagger/
|
**Documentation:** http://localhost:8080/swagger/
|
||||||
**OpenAPI Spec:** http://localhost:8080/swagger/doc.json
|
**OpenAPI Spec:** http://localhost:8080/swagger/doc.json
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 15. CLI Subcommands and Flag Management with Cobra
|
# 15. CLI Subcommands and Flag Management with Cobra
|
||||||
|
|
||||||
**Date:** 2026-04-05
|
**Date:** 2026-04-05
|
||||||
**Status:** Implemented
|
**Status:** ✅ Implemented
|
||||||
**Authors:** Arcodange Team
|
**Authors:** Arcodange Team
|
||||||
**Decision Date:** 2026-04-05
|
**Decision Date:** 2026-04-05
|
||||||
**Implementation Status:** Phase 1 Complete
|
**Implementation Status:** Phase 1 Complete
|
||||||
@@ -222,7 +222,7 @@ dance-lessons-coach config validate
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** Proposed
|
**Status:** Proposed
|
||||||
**Next Review:** 2026-04-12
|
**Next Review:** 2026-04-12
|
||||||
**Implementation Owner:** Arcodange Team
|
**Implementation Owner:** Arcodange Team
|
||||||
**Approvers Needed:** @gabrielradureau
|
**Approvers Needed:** @gabrielradureau
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# 16. CI/CD Pipeline Design for Multi-Platform Compatibility
|
# 16. CI/CD Pipeline Design for Multi-Platform Compatibility
|
||||||
|
|
||||||
**Date:** 2026-04-05
|
**Date:** 2026-04-05
|
||||||
**Status:** Accepted
|
**Status:** ✅ Accepted
|
||||||
**Authors:** Arcodange Team
|
**Authors:** Arcodange Team
|
||||||
**Decision Date:** 2026-04-08
|
**Decision Date:** 2026-04-08
|
||||||
**Implementation Status:** Completed
|
**Implementation Status:** ✅ Completed
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -832,7 +832,7 @@ jobs:
|
|||||||
- ✅ **Coverage reporting**: Badges updating automatically
|
- ✅ **Coverage reporting**: Badges updating automatically
|
||||||
- ✅ **Binary builds**: Scripts executing properly in container environment
|
- ✅ **Binary builds**: Scripts executing properly in container environment
|
||||||
|
|
||||||
**Status:** Accepted
|
**Status:** ✅ Accepted
|
||||||
**Implementation Date:** 2026-04-08
|
**Implementation Date:** 2026-04-08
|
||||||
**Implementation Owner:** Arcodange Team
|
**Implementation Owner:** Arcodange Team
|
||||||
**Reviewers:** @gabrielradureau
|
**Reviewers:** @gabrielradureau
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# 17. Trunk-Based Development Workflow for CI/CD Safety
|
# 17. Trunk-Based Development Workflow for CI/CD Safety
|
||||||
|
|
||||||
**Date:** 2026-04-05
|
**Date:** 2026-04-05
|
||||||
**Status:** Approved
|
**Status:** 🟢 Approved
|
||||||
**Authors:** Arcodange Team
|
**Authors:** Arcodange Team
|
||||||
**Decision Date:** 2026-04-05
|
**Decision Date:** 2026-04-05
|
||||||
**Implementation Status:** Implemented
|
**Implementation Status:** ✅ Implemented
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 18. User Management and Authentication System
|
# 18. User Management and Authentication System
|
||||||
|
|
||||||
**Date:** 2026-04-06
|
**Date:** 2024-04-06
|
||||||
**Status:** Implemented (user model, JWT auth, password-reset workflow, admin endpoints, greet personalization, BDD coverage all live; future enhancements like 2FA / email verification belong in separate ADRs)
|
**Status:** Proposed
|
||||||
**Authors:** Product Owner
|
**Authors:** Product Owner
|
||||||
**Decision Drivers:** Security, User Personalization, Admin Functionality
|
**Decision Drivers:** Security, User Personalization, Admin Functionality
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 19. PostgreSQL Database Integration
|
# 19. PostgreSQL Database Integration
|
||||||
|
|
||||||
**Date:** 2026-04-07
|
**Date:** 2024-04-07
|
||||||
**Status:** Implemented (core integration; performance tuning + extended monitoring tracked as future work)
|
**Status:** Proposed
|
||||||
**Authors:** Product Owner
|
**Authors:** Product Owner
|
||||||
**Decision Drivers:** Data Persistence, Scalability, Production Readiness
|
**Decision Drivers:** Data Persistence, Scalability, Production Readiness
|
||||||
|
|
||||||
@@ -359,6 +359,8 @@ The PostgreSQL integration follows established dance-lessons-coach patterns:
|
|||||||
2. **Configuration Updates:** New database configuration structure
|
2. **Configuration Updates:** New database configuration structure
|
||||||
3. **Development Workflow:** Docker-based database for local development
|
3. **Development Workflow:** Docker-based database for local development
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Alternatives Considered
|
## Alternatives Considered
|
||||||
|
|
||||||
### Alternative 1: Keep SQLite with File Persistence
|
### Alternative 1: Keep SQLite with File Persistence
|
||||||
@@ -671,10 +673,10 @@ func AfterScenario(ctx context.Context, sc *godog.Scenario, err error) (context.
|
|||||||
## Future Considerations
|
## Future Considerations
|
||||||
|
|
||||||
### Immediate Next Steps (Post-Migration)
|
### Immediate Next Steps (Post-Migration)
|
||||||
1. **CI/CD Integration:** Add PostgreSQL to CI pipeline — ✅ Implemented (`postgres:15` service in `.gitea/workflows/ci-cd.yaml`, all BDD tests run against real Postgres)
|
1. **CI/CD Integration:** Add PostgreSQL to CI pipeline
|
||||||
2. **Performance Tuning:** Query optimization — Deferred. No production hot path identified. Reopen as separate ADR if/when latency budget exceeded.
|
2. **Performance Tuning:** Query optimization
|
||||||
3. **Monitoring:** Database health metrics — Partial. `/api/healthz` reports DB connectivity. Deeper metrics (slow query log, pool stats) deferred until ADR-0022 cache Phase 2 lands.
|
3. **Monitoring:** Database health metrics
|
||||||
4. **Backup Strategy:** Regular database backups — Deferred. No production data yet. Will require separate ADR before any production data lands.
|
4. **Backup Strategy:** Regular database backups
|
||||||
|
|
||||||
### Long-Term Enhancements
|
### Long-Term Enhancements
|
||||||
1. **Database Sharding:** For horizontal scaling
|
1. **Database Sharding:** For horizontal scaling
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# ADR 0020: Docker Build Strategy - Traditional vs Buildx
|
# ADR 0020: Docker Build Strategy - Traditional vs Buildx
|
||||||
|
|
||||||
**Status:** Accepted
|
## Status
|
||||||
|
**Accepted** ✅
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# 21. JWT Secret Retention Policy
|
# 10. JWT Secret Retention Policy
|
||||||
|
|
||||||
**Status:** Implemented (2026-05-05 — `pkg/user/jwt_manager.go` `RemoveExpiredSecrets` + `StartCleanupLoop`, wired in `pkg/server/server.go` `Run`; admin endpoint `/api/v1/admin/jwt/secrets` remains explicitly out of scope and tracked under @todo BDD scenarios)
|
## Status
|
||||||
|
**Proposed** 🟡
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# ADR 0022: Rate Limiting and Cache Strategy
|
# ADR 0022: Rate Limiting and Cache Strategy
|
||||||
|
|
||||||
**Status:** Implemented (Phase 1) - Phase 2 still Proposed
|
## Status
|
||||||
|
**Proposed** 🟡
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# Config Hot Reloading Strategy
|
# Config Hot Reloading Strategy
|
||||||
|
|
||||||
**Status:** Implemented — all 4 phases shipped (2026-05-05). Hot-reloadable fields: `logging.level` (Phase 1), `auth.jwt.ttl` (Phase 2), `telemetry.sampler.type` + `telemetry.sampler.ratio` (Phase 3), `api.v2_enabled` (Phase 4). Plumbing: `Config.WatchAndApply` in `pkg/config/config.go` is the single entry point. Phase 2 fixed a pre-existing bug where hardcoded 24h TTL ignored `auth.jwt.ttl`. Phase 4 chose the **always-register-with-middleware-gate** approach: v2 routes are now ALWAYS registered, and `Server.v2EnabledGate` middleware reads the live config on every request (returns 404 + JSON body when disabled). No router rebuild needed for the flag flip. 3 unit tests in `pkg/server/v2_gate_test.go` cover blocked-when-disabled / passes-when-enabled / hot-reload-mid-life-of-same-Server.
|
* Status: Proposed
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
* Deciders: Gabriel Radureau, AI Agent
|
||||||
**Date:** 2026-04-05
|
* Date: 2026-04-05
|
||||||
**Last Updated:** 2026-05-05
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# ADR 0024: BDD Test Organization and Isolation Strategy
|
# ADR 0024: BDD Test Organization and Isolation Strategy
|
||||||
|
|
||||||
**Status:** Implemented (Phase 1 + Phase 2 + Phase 3 — parallel testing via [PR #35](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/35), isolation strategy detailed in [ADR-0025](0025-bdd-scenario-isolation-strategies.md))
|
## Status
|
||||||
|
**Proposed** 🟡
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -284,22 +285,20 @@ func CleanupFeatureData(featureName string) {
|
|||||||
|
|
||||||
## Implementation Plan
|
## Implementation Plan
|
||||||
|
|
||||||
### Phase 1: Refactor Current Tests — ✅ Implemented
|
### Phase 1: Refactor Current Tests (1-2 weeks)
|
||||||
1. Split monolithic feature files into feature directories — done (see `features/<domain>/` layout)
|
1. Split monolithic feature files into feature directories
|
||||||
2. Create feature-specific test scripts — done
|
2. Create feature-specific test scripts
|
||||||
3. Implement basic isolation (config files, database names) — done
|
3. Implement basic isolation (config files, database names)
|
||||||
|
|
||||||
### Phase 2: Enhance Test Infrastructure — ✅ Implemented
|
### Phase 2: Enhance Test Infrastructure (2-3 weeks)
|
||||||
1. Add synchronization helpers to test framework — done
|
1. Add synchronization helpers to test framework
|
||||||
2. Implement server lifecycle management — done (`pkg/bdd/testserver/server.go`)
|
2. Implement server lifecycle management
|
||||||
3. Create comprehensive cleanup routines — done
|
3. Create comprehensive cleanup routines
|
||||||
|
|
||||||
### Phase 3: Parallel Testing — ✅ Implemented (PR #35, 2026-05-03)
|
### Phase 3: Parallel Testing (Optional)
|
||||||
1. Add parallel test execution capability — done (schema-per-package isolation, **2.85x speedup**)
|
1. Add parallel test execution capability
|
||||||
2. Implement port management for parallel runs — done (`pkg/bdd/parallel/port_manager.go`)
|
2. Implement port management for parallel runs
|
||||||
3. Add resource monitoring — deferred (not blocking; can be reopened as separate ADR if/when CI flakiness re-emerges)
|
3. Add resource monitoring
|
||||||
|
|
||||||
The strategy choice between alternatives (TRUNCATE vs schema isolation vs container-per-test) is documented in [ADR-0025](0025-bdd-scenario-isolation-strategies.md). Default behavior in CI is `BDD_SCHEMA_ISOLATION=true` (cf. `documentation/BDD_TEST_ENV.md`).
|
|
||||||
|
|
||||||
## Alternatives Considered
|
## Alternatives Considered
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# ADR 0025: BDD Scenario Isolation Strategies
|
# ADR 0025: BDD Scenario Isolation Strategies
|
||||||
|
|
||||||
**Status:** Implemented (per-package schema isolation since T12 stage 2/2 - 2026-05-03)
|
## Status
|
||||||
|
**Proposed** 🟡
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
# ADR 0026: Composite Info Endpoint vs Separate Calls
|
|
||||||
|
|
||||||
**Status:** Implemented (2026-05-05 — PR pending)
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The application currently exposes several endpoints that provide system information:
|
|
||||||
- `/api/version` - returns version, commit, build date, Go version (cached 60s)
|
|
||||||
- `/api/health` - returns `{"status":"healthy"}` (simple liveness)
|
|
||||||
- `/api/healthz` - returns rich health info: status, version, uptime_seconds, timestamp
|
|
||||||
- `/api/ready` - returns readiness with connection details
|
|
||||||
|
|
||||||
Frontend components like `HealthDashboard` currently call `/api/healthz` to display server info. However, there is a need for a **composite endpoint** that aggregates:
|
|
||||||
1. Version information (from `/api/version`)
|
|
||||||
2. Build metadata (commit hash, build date)
|
|
||||||
3. Uptime information (from `/api/healthz`)
|
|
||||||
4. Cache status (enabled/disabled)
|
|
||||||
5. Health status
|
|
||||||
|
|
||||||
This raises an architectural question: **Should we create a new composite `/api/info` endpoint, or should frontend components make multiple separate API calls?**
|
|
||||||
|
|
||||||
### The Problem with Separate Calls
|
|
||||||
|
|
||||||
If the frontend makes individual calls to `/api/version`, `/api/healthz`, and checks cache config separately:
|
|
||||||
|
|
||||||
1. **Multiple network requests**: 3-4 HTTP round trips per page load
|
|
||||||
2. **Inconsistent data**: Responses may come from different moments in time
|
|
||||||
3. **No caching coordination**: Each endpoint has its own cache key and TTL
|
|
||||||
4. **Complex frontend logic**: Need to merge data from multiple sources
|
|
||||||
5. **Poor user experience**: Slower page loads, multiple loading states
|
|
||||||
|
|
||||||
### Current State Analysis
|
|
||||||
|
|
||||||
| Endpoint | Data Provided | Cache TTL | Use Case |
|
|
||||||
|----------|---------------|-----------|----------|
|
|
||||||
| `/api/version` | version, commit, built, go | 60s | Version info |
|
|
||||||
| `/api/healthz` | status, version, uptime_seconds, timestamp | None | K8s probes, health dashboard |
|
|
||||||
| `/api/health` | status: "healthy" | None | Simple liveness |
|
|
||||||
| `/api/ready` | ready, connections, reason | None | Readiness probes |
|
|
||||||
|
|
||||||
The `/api/healthz` endpoint already combines some data (status + version + uptime + timestamp), but it:
|
|
||||||
- Doesn't include commit_short
|
|
||||||
- Doesn't include build_date separately
|
|
||||||
- Doesn't include cache_enabled
|
|
||||||
- Is not cached
|
|
||||||
- Has Kubernetes-specific field naming (`healthz`)
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
* **Performance**: Minimize network round trips for frontend
|
|
||||||
* **Consistency**: All data should reflect the same point-in-time
|
|
||||||
* **Maintainability**: Single source of truth for system info
|
|
||||||
* **Caching**: Reuse existing cache infrastructure (ADR-0022)
|
|
||||||
* **API Design**: Follow REST principles and existing patterns
|
|
||||||
* **Backward Compatibility**: Existing endpoints must remain unchanged
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
### Option 1: Composite `/api/info` Endpoint (Chosen)
|
|
||||||
|
|
||||||
Create a new endpoint that aggregates all required data in a single call.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- ✅ Single network request for frontend
|
|
||||||
- ✅ Consistent point-in-time data
|
|
||||||
- ✅ Can leverage existing cache infrastructure with key `info:json`
|
|
||||||
- ✅ Follows existing pattern of `/api/version` caching
|
|
||||||
- ✅ Clean API design - one endpoint, one purpose
|
|
||||||
- ✅ Reduces frontend complexity
|
|
||||||
- ✅ Better UX - faster page loads
|
|
||||||
- ✅ Aligns with ADR-0022 cache strategy (reusable cache key pattern)
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- ⚠️ Duplicates some data from `/api/healthz` and `/api/version`
|
|
||||||
- ⚠️ Requires new endpoint implementation
|
|
||||||
- ⚠️ Need to maintain consistency if source endpoints change
|
|
||||||
|
|
||||||
### Option 2: Frontend Aggregation with Multiple Calls
|
|
||||||
|
|
||||||
Frontend makes separate calls to `/api/version`, `/api/healthz`, and introspects config.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- ✅ No backend changes required
|
|
||||||
- ✅ Uses existing endpoints
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- ❌ Multiple network requests (3-4 round trips)
|
|
||||||
- ❌ Inconsistent data timing
|
|
||||||
- ❌ Complex error handling in frontend
|
|
||||||
- ❌ Poor UX - multiple loading states, slower
|
|
||||||
- ❌ Each endpoint has different caching behavior
|
|
||||||
- ❌ Violates DRY - same data fetched multiple times
|
|
||||||
|
|
||||||
### Option 3: Extend `/api/healthz` Endpoint
|
|
||||||
|
|
||||||
Add `commit_short`, `build_date`, and `cache_enabled` fields to existing `/api/healthz`.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- ✅ Reuses existing endpoint
|
|
||||||
- ✅ Single request
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- ❌ Breaks backward compatibility (response schema change)
|
|
||||||
- ❌ `/api/healthz` is Kubernetes-focused (naming convention)
|
|
||||||
- ❌ Not cached currently
|
|
||||||
- ❌ Mixes health probe concerns with version info
|
|
||||||
- ❌ Violates single responsibility
|
|
||||||
|
|
||||||
### Option 4: GraphQL / Query Parameters
|
|
||||||
|
|
||||||
Allow clients to specify which fields they want via query parameters.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- ✅ Flexible - clients get exactly what they need
|
|
||||||
- ✅ Single endpoint
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- ❌ Overkill for this use case
|
|
||||||
- ❌ Not consistent with existing REST API design
|
|
||||||
- ❌ Complex implementation
|
|
||||||
- ❌ Not aligned with project architecture (Chi router, REST style)
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
**Chosen: Option 1 - Composite `/api/info` Endpoint**
|
|
||||||
|
|
||||||
We will implement a new `GET /api/info` endpoint that returns a JSON object with all required fields in a single call. This endpoint will:
|
|
||||||
|
|
||||||
1. Aggregate data from existing sources (`version` package, `config`, server uptime)
|
|
||||||
2. Be cached using the existing cache service with key `info:json`
|
|
||||||
3. Use TTL from `config.cache.default_ttl_seconds` (consistent with ADR-0022)
|
|
||||||
4. Return `X-Cache: HIT/MISS` headers for debugging
|
|
||||||
5. Follow existing Go handler patterns from `pkg/server/server.go`
|
|
||||||
|
|
||||||
### Response Schema
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "1.4.0",
|
|
||||||
"commit_short": "a3f7b2c1",
|
|
||||||
"build_date": "2026-05-04T08:00:00Z",
|
|
||||||
"uptime_seconds": 1234,
|
|
||||||
"cache_enabled": true,
|
|
||||||
"healthz_status": "healthy",
|
|
||||||
"go_version": "go1.26.1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `go_version` field provides the Go runtime version via `runtime.Version()`, useful for ops debugging (e.g., identifying which Go version is running in production).
|
|
||||||
|
|
||||||
### Rationale
|
|
||||||
|
|
||||||
1. **Performance**: Single HTTP request instead of 3-4 separate calls
|
|
||||||
2. **Consistency**: All data reflects the same moment in time
|
|
||||||
3. **Caching**: Leverages existing cache infrastructure (ADR-0022) with predictable key pattern
|
|
||||||
4. **API Design**: Clean, RESTful endpoint with single responsibility
|
|
||||||
5. **Maintainability**: Clear separation of concerns - info aggregation is a distinct use case
|
|
||||||
6. **Backward Compatibility**: Existing endpoints remain unchanged
|
|
||||||
7. **Frontend Simplicity**: Reduces complexity and improves UX
|
|
||||||
|
|
||||||
### Cache Strategy
|
|
||||||
|
|
||||||
Following ADR-0022 pattern:
|
|
||||||
- Cache key: `info:json` (consistent with `version:format` pattern)
|
|
||||||
- TTL: `config.cache.default_ttl_seconds` (default 300 seconds)
|
|
||||||
- Cache service: `pkg/cache/cache.go` InMemoryService
|
|
||||||
- Headers: `X-Cache: HIT` or `X-Cache: MISS`
|
|
||||||
|
|
||||||
This allows the endpoint to be fast even under load, while maintaining data freshness.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
### Positive
|
|
||||||
|
|
||||||
1. **Improved frontend performance**: Single request instead of multiple
|
|
||||||
2. **Better UX**: Faster page loads, simpler loading states
|
|
||||||
3. **Consistent data**: All fields reflect the same point-in-time
|
|
||||||
4. **Cache efficiency**: Reuses existing cache infrastructure
|
|
||||||
5. **Clean separation**: Info endpoint handles aggregation, source endpoints unchanged
|
|
||||||
6. **Easy to test**: Single endpoint with predictable response
|
|
||||||
|
|
||||||
### Negative
|
|
||||||
|
|
||||||
1. **Data duplication**: Some fields appear in multiple endpoints
|
|
||||||
2. **Maintenance burden**: If source data changes, endpoint must be updated
|
|
||||||
3. **New endpoint**: Increases API surface area (though minimal)
|
|
||||||
|
|
||||||
### Mitigation
|
|
||||||
|
|
||||||
1. Data duplication is acceptable - it's read-only system info
|
|
||||||
2. Source the data from the same packages/functions used by other endpoints
|
|
||||||
3. The new endpoint has a clear, focused purpose
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [ADR-0002: Chi Router](adr/0002-chi-router.md) - Routing foundation
|
|
||||||
- [ADR-0022: Rate Limiting Cache Strategy](adr/0022-rate-limiting-cache-strategy.md) - Cache pattern reference
|
|
||||||
- [pkg/server/server.go](pkg/server/server.go) - Handler patterns
|
|
||||||
- [pkg/cache/cache.go](pkg/cache/cache.go) - Cache service
|
|
||||||
- [pkg/version/version.go](pkg/version/version.go) - Version data source
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# 27. Ollama Tier 1 onboarding via meta-trainer-bootstrap
|
|
||||||
|
|
||||||
**Date:** 2026-05-05
|
|
||||||
**Status:** Proposed
|
|
||||||
**Authors:** Gabriel Radureau, AI Agent (Claude Opus 4.7 Tier 3 inspector)
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
The autonomous trainer day on 2026-05-05 validated that Mistral Vibe (cloud) can drive a complete PR lifecycle on this project: ICM workspace → phase-planner → implementation → verifier audit → PR open (cf. PR #54, Q-041 in `~/.vibe/memory/reference/mistral-quirks.md`). Two limitations remain:
|
|
||||||
|
|
||||||
1. **Vendor risk** — every autonomous run consumes the Mistral cloud forfait. If the forfait runs out mid-month or the API is unavailable, autonomous capability is lost.
|
|
||||||
2. **Sovereignty story** — ARCODANGE's stated direction (cf. `migration-claude-vers-mistral-phase-1.md`) is to reduce dependence on a single foreign vendor. The hardware exists locally (M4 128 GB) ; the missing link is wiring a local model into the same Tier 1 executor role Mistral plays today.
|
|
||||||
|
|
||||||
The user-flagged candidate models (cf. `~/.vibe/memory/reference/ollama-candidate-models.md`) :
|
|
||||||
|
|
||||||
* `nemotron-3-super`
|
|
||||||
* `gemma4:31b`
|
|
||||||
|
|
||||||
Both are large enough to plausibly handle the agentic coding role and small enough to fit in 128 GB RAM with headroom for tools. Neither has been tested under the ARCODANGE methodology (canary suite, ICM workspace traversal, verifier-skill discipline).
|
|
||||||
|
|
||||||
The methodology to onboard a new Tier 1 already exists : the `meta-trainer-bootstrap` skill at `~/.vibe/skills/meta-trainer-bootstrap/`. It runs a 10-canary suite (C-001..C-010), copies + adapts the skill library to the new model's harness tool names, stands up a `<model>-quirks.md` baseline, and produces a Tier 3 audit report. It has been validated on Mistral itself (we are currently running the methodology Mistral-on-Mistral, which is unusual — the canary suite was originally written for a different model).
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
* **Forfait insurance** — a working local Tier 1 means autonomous capability survives a Mistral outage / forfait exhaustion
|
|
||||||
* **Sovereignty** — local execution removes the single-vendor dependency for the autonomous workflow
|
|
||||||
* **Methodology validation** — `meta-trainer-bootstrap` has never been run on a fresh model in production, only smoke-tested ; this is its first real test
|
|
||||||
* **Cost** — Ollama is local-only (no per-call price). The cost is the bootstrap effort + ongoing M4 power consumption.
|
|
||||||
* **Model maturity** — both candidates are recent ; their agentic coding ability is empirical, not theoretical
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
### Option 1: Bootstrap `nemotron-3-super` first, then `gemma4:31b`
|
|
||||||
|
|
||||||
Run the canary suite on each, document quirks separately, decide based on canary pass rate and cost-per-task.
|
|
||||||
|
|
||||||
* Good — comparative data, makes the choice empirical
|
|
||||||
* Good — discovers any meta-trainer-bootstrap bugs early on the first attempt
|
|
||||||
* Bad — doubles the bootstrap effort (~4-8 hours per model)
|
|
||||||
* Bad — requires holding both models on disk (large)
|
|
||||||
|
|
||||||
### Option 2: Bootstrap one model only, picked on prior reputation
|
|
||||||
|
|
||||||
Pick one (e.g. `nemotron-3-super` per the user's explicit ordering in `ollama-candidate-models.md`) and commit. Skip the comparison.
|
|
||||||
|
|
||||||
* Good — half the effort, ships faster
|
|
||||||
* Bad — no fallback if the chosen model is unsuitable
|
|
||||||
* Bad — anchors the methodology to one model's quirks before we know they generalise
|
|
||||||
|
|
||||||
### Option 3: Defer until Mistral autonomous shows real strain
|
|
||||||
|
|
||||||
Do nothing yet. Wait for forfait pressure or a Mistral outage to force the issue. Reactive instead of proactive.
|
|
||||||
|
|
||||||
* Good — zero effort now
|
|
||||||
* Bad — when the trigger fires, we are unprepared and the bootstrap is rushed
|
|
||||||
* Bad — postpones validation of `meta-trainer-bootstrap` indefinitely
|
|
||||||
|
|
||||||
### Option 4: Skip Ollama, evaluate a different vendor (Anthropic, OpenAI)
|
|
||||||
|
|
||||||
Bring in a second cloud model as Tier 1 instead of going local.
|
|
||||||
|
|
||||||
* Good — likely higher quality than 31B local
|
|
||||||
* Bad — replaces vendor dependence with two-vendor dependence ; doesn't solve sovereignty
|
|
||||||
* Bad — we already have Claude as Tier 3 inspector via Anthropic ; mixing roles complicates the methodology
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option: **Option 2 — Bootstrap `nemotron-3-super` first**, deferring `gemma4:31b` to a follow-up ADR if `nemotron-3-super` underperforms or shows unfixable quirks.
|
|
||||||
|
|
||||||
Rationale :
|
|
||||||
- Forfait pressure is real but not immediate (~3.5% of monthly forfait spent on the heavy autonomous trainer day 2026-05-05) — we have time but should not procrastinate
|
|
||||||
- Comparative testing (Option 1) is technically right but pragmatically slow for an unproven methodology
|
|
||||||
- The user's explicit ordering signals their prior on which to try first ; respect it
|
|
||||||
- If the canary suite fails substantially on `nemotron-3-super`, we pivot to `gemma4:31b` with the lessons (and per-model quirks file) from the first attempt — net learning either way
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
1. **Pre-flight** — verify `ollama` is installed, the model is pulled (`ollama pull nemotron-3-super`), and the M4 has enough free RAM (model size + ~16 GB headroom for tools).
|
|
||||||
2. **Run `meta-trainer-bootstrap` skill** — pointing `TARGET_MODEL_ID=nemotron-3-super`, `TARGET_HARNESS=ollama run nemotron-3-super`, `TARGET_PROJECT_ROOT=<a fresh clone or worktree>`. Budget : 5 EUR-equivalent of Mistral Tier-2 orchestration cost + 2-4 hours of trainer attention.
|
|
||||||
3. **Canary suite** — run C-001..C-010 ; record each result in `~/.vibe/memory/reference/nemotron-3-super-quirks.md` as `Q-101..Q-110` (the `Q-001..Q-099` range is reserved for the legacy Mistral baseline).
|
|
||||||
4. **Skill library adaptation** — for each ARCODANGE skill currently relying on Mistral-specific tool names (`read_file`, `write_file`, etc.), adapt to whatever Ollama exposes. Document deltas.
|
|
||||||
5. **Smoke test** — run a single small task end-to-end on a low-risk project. Use the ICM workspace pattern. Verify worktree isolation (Q-038 fix) still applies.
|
|
||||||
6. **Tier 3 report** — produce `bootstrap-report.md` for Claude inspector review. Include canary pass rate, key quirks, KPI baseline numbers, open friction points.
|
|
||||||
7. **Decision gate** — based on the report, either (a) promote `nemotron-3-super` to production Tier 1 and update `~/.vibe/config.toml` accordingly, (b) try `gemma4:31b` as a follow-up, or (c) escalate to Tier 3 for a strategic pivot.
|
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
|
||||||
|
|
||||||
### Option 1 (Bootstrap both)
|
|
||||||
|
|
||||||
* Good — comparative data
|
|
||||||
* Good — early bug detection on the methodology
|
|
||||||
* Bad — double effort
|
|
||||||
* Bad — no clear way to choose without significant additional time investment for the second model
|
|
||||||
|
|
||||||
### Option 2 (Chosen — `nemotron-3-super` first)
|
|
||||||
|
|
||||||
* Good — concrete forward motion
|
|
||||||
* Good — methodology gets its first real test
|
|
||||||
* Good — `meta-trainer-bootstrap` skill validated end-to-end (currently only smoke-tested)
|
|
||||||
* Bad — risk of picking the wrong model and wasting the bootstrap effort
|
|
||||||
* Mitigation: per-model quirks files mean the second attempt is cheaper (skill adaptations transfer)
|
|
||||||
|
|
||||||
### Option 3 (Defer)
|
|
||||||
|
|
||||||
* Good — zero effort
|
|
||||||
* Bad — reactive, increases risk under outage scenarios
|
|
||||||
|
|
||||||
### Option 4 (Different vendor)
|
|
||||||
|
|
||||||
* Good — likely higher quality
|
|
||||||
* Bad — does not solve sovereignty
|
|
||||||
* Bad — methodology already has Claude as Tier 3 ; another Anthropic-family model in Tier 1 conflates roles
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
* `meta-trainer-bootstrap` skill is exercised end-to-end for the first time. Discoveries during this run will likely produce Q-042+ entries in `mistral-quirks.md` and a separate `nemotron-3-super-quirks.md`.
|
|
||||||
* `~/.vibe/config.toml` may need a new model alias (e.g. `local-nemotron`) configured for testing without affecting the production `mistral-vibe-cli-latest` default.
|
|
||||||
* If successful, the next ADR (0028 or higher) will document the production switch (or split, e.g. routine tasks → local, complex tasks → cloud).
|
|
||||||
* Forfait usage from this bootstrap : Tier 2 Mistral orchestration only ; Tier 1 Ollama runs are free at the API level.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
* Three-tier methodology : `~/.vibe/skills/meta-trainer-bootstrap/references/three-tier-tutor.md`
|
|
||||||
* Candidate models reference : `~/.vibe/memory/reference/ollama-candidate-models.md`
|
|
||||||
* `meta-trainer-bootstrap` skill : `~/.vibe/skills/meta-trainer-bootstrap/SKILL.md`
|
|
||||||
* Canary suite : `~/.vibe/skills/meta-trainer-bootstrap/canaries/INDEX.md`
|
|
||||||
* Q-041 (autonomy story validated on Mistral) : `~/.vibe/memory/reference/mistral-quirks.md`
|
|
||||||
* Related ADRs : [ADR-0007](0007-opentelemetry-integration.md) (cloud / sovereignty considerations historically) ; [ADR-0023](0023-config-hot-reloading.md) (hot-reload may need different patterns under Ollama)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# 28. Passwordless authentication: magic link → OpenID Connect
|
|
||||||
|
|
||||||
**Date:** 2026-05-05
|
|
||||||
**Status:** Proposed
|
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
ADR-0018 (now Implemented) shipped a username + password authentication system with bcrypt hashing, JWT tokens, admin master password, and admin-assisted password reset. It works, but it carries the cost-of-passwords : we store password hashes, support password reset flows, and maintain a credential-rotation policy. Users hate passwords ; ops and security pay for them.
|
|
||||||
|
|
||||||
Two industry-standard alternatives exist :
|
|
||||||
1. **Magic link by email** — user enters their email, receives a one-time token in a clickable link, link consumes the token and issues a session JWT. No password stored.
|
|
||||||
2. **OpenID Connect Authorization Code flow** — delegate authentication to an external Identity Provider (e.g. Authelia, Keycloak, Auth0, Google) ; our app receives an `id_token` after the OIDC dance.
|
|
||||||
|
|
||||||
We want to **migrate to passwordless** for new sign-ups while keeping the existing username/password code path operational during the transition (no flag-day breakage). The two passwordless mechanisms above complement each other : magic link is simpler for first-party users on day 1 ; OIDC is the right answer for second-party users (other ARCODANGE products, partner integrations) and for admin SSO.
|
|
||||||
|
|
||||||
A third constraint : ARCODANGE local development must use HTTPS for OAuth callbacks to be valid (most OIDC providers reject `http://localhost` redirect URIs in their default config). `mkcert` is the canonical local-CA tool for this.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
* **Reduce password-related attack surface** — no hash storage, no breach-and-reuse risk, no password reset abuse vectors
|
|
||||||
* **User experience** — passwordless is faster for the user (1 click in email vs typing/remembering password)
|
|
||||||
* **Operational simplicity** — no password reset flow to maintain ; the password-reset code can be removed once migration is complete
|
|
||||||
* **Multi-product readiness** — OIDC is the prerequisite for cross-product SSO across the ARCODANGE portfolio
|
|
||||||
* **Backwards compatibility** — must not break existing tokens or BDD scenarios mid-migration
|
|
||||||
* **Local dev parity** — HTTPS in dev so OAuth flows can be tested locally without provider-specific workarounds
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen): Sequenced — magic link first, OIDC second
|
|
||||||
|
|
||||||
Deliver in two phases :
|
|
||||||
|
|
||||||
* **Phase A — Magic link**
|
|
||||||
- Add `POST /api/v1/auth/magic-link/request` (body: `{email}`) — generates token, stores it (TTL ~15 min), sends email via SMTP
|
|
||||||
- Add `GET /api/v1/auth/magic-link/consume?token=<...>` — single-use consumption, issues a JWT, returns it as cookie + JSON body
|
|
||||||
- Reuse the existing JWT issuance + secret retention infrastructure (ADR-0021)
|
|
||||||
- Existing `/api/v1/auth/login` (username/password) stays operational during transition
|
|
||||||
|
|
||||||
* **Phase B — OpenID Connect Authorization Code with PKCE**
|
|
||||||
- Add `GET /api/v1/auth/oidc/start` — generates state + PKCE verifier, redirects to provider's `authorization_endpoint`
|
|
||||||
- Add `GET /api/v1/auth/oidc/callback` — exchanges code for tokens, validates `id_token` signature against provider's JWKS, issues internal JWT
|
|
||||||
- Provider URL configurable per environment (`auth.oidc.issuer_url`, `auth.oidc.client_id`, `auth.oidc.client_secret`)
|
|
||||||
- Allow multiple providers in config (key by provider name, e.g. `arcodange-sso`)
|
|
||||||
- Local dev requires HTTPS — `mkcert` setup documented in `documentation/DEV_SETUP.md`
|
|
||||||
|
|
||||||
* **Phase C (later, separate ADR) — Decommission password auth**
|
|
||||||
- Once all users have migrated, remove the password endpoints, remove the password_hash column, mark ADR-0018 as Superseded by this ADR
|
|
||||||
|
|
||||||
### Option 2: All-at-once OIDC, no magic link
|
|
||||||
|
|
||||||
Skip magic link, jump straight to OIDC.
|
|
||||||
|
|
||||||
* Good — single migration, no intermediate state
|
|
||||||
* Bad — requires an OIDC provider operational on day 1, which we don't have configured
|
|
||||||
* Bad — magic link has zero infra dependencies (just SMTP) ; OIDC requires running an IdP or paying for one
|
|
||||||
|
|
||||||
### Option 3: Magic link only, no OIDC
|
|
||||||
|
|
||||||
Stop at Phase A.
|
|
||||||
|
|
||||||
* Good — simplest implementation
|
|
||||||
* Bad — doesn't solve cross-product SSO ; we'd re-do this work later for the broader ARCODANGE portfolio
|
|
||||||
|
|
||||||
### Option 4: Status quo (do nothing)
|
|
||||||
|
|
||||||
Keep username + password.
|
|
||||||
|
|
||||||
* Good — zero effort
|
|
||||||
* Bad — passwords stay forever ; ARCODANGE locks itself out of integration scenarios that expect OIDC
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option : **Option 1, sequenced magic link → OIDC**.
|
|
||||||
|
|
||||||
Rationale :
|
|
||||||
- Magic link is implementable today with zero infra dependencies beyond the email infrastructure (ADR-0029)
|
|
||||||
- OIDC requires running an IdP locally (Authelia or Keycloak) — that's another container in the dev stack and another ADR's worth of decision work, but the magic-link work is the natural prerequisite (token-by-email plumbing is reused)
|
|
||||||
- Sequenced delivery means we never have to roll back : Phase A works alone, Phase B layers on top, Phase C cleans up
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase A — Magic link (target: 2-3 PRs)
|
|
||||||
|
|
||||||
1. **A.1 — Storage** : add a `magic_link_tokens` table (id, email, token_hash, expires_at, consumed_at). Repository pattern alongside `pkg/user/postgres_repository.go`.
|
|
||||||
2. **A.2 — Token endpoint** : `POST /api/v1/auth/magic-link/request` generates a token, stores it (hashed), enqueues an email send. Rate-limited (cf. ADR-0022) by email address.
|
|
||||||
3. **A.3 — Consume endpoint** : `GET /api/v1/auth/magic-link/consume?token=...` validates + marks consumed + issues JWT. Returns `Set-Cookie` and `{token: jwt}` body.
|
|
||||||
4. **A.4 — Sign-up via magic link** : if the email is unknown, the consume endpoint creates the user record. (No separate "sign-up" flow needed — first magic link IS the sign-up.)
|
|
||||||
5. **A.5 — BDD coverage** : scenarios for happy path, expired token, double-consume, wrong-email, rate-limit. Cf. ADR-0030 for the email assertion strategy.
|
|
||||||
|
|
||||||
### Phase B — OIDC Code flow with PKCE (target: 3-4 PRs)
|
|
||||||
|
|
||||||
1. **B.1 — Local IdP** : choose Authelia or Keycloak for local development. Add to `docker-compose.yml` with default test configuration.
|
|
||||||
2. **B.2 — mkcert** : document local HTTPS setup in `documentation/DEV_SETUP.md`, add `make cert` target.
|
|
||||||
3. **B.3 — OIDC client** : `pkg/auth/oidc.go` — discovery, JWKS cache, code exchange with PKCE.
|
|
||||||
4. **B.4 — Endpoints** : `/oidc/start` and `/oidc/callback`.
|
|
||||||
5. **B.5 — Provider config** : `auth.oidc.providers` map in config (cf. ADR-0006 Viper) ; multi-provider supported.
|
|
||||||
6. **B.6 — BDD coverage** : end-to-end scenarios using a mock OIDC server (or the local Authelia instance with deterministic users).
|
|
||||||
|
|
||||||
### Phase C — Decommission password (separate ADR after A+B in production)
|
|
||||||
|
|
||||||
Out of scope for this ADR. Will be ADR-NNNN when migration is complete.
|
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen — Sequenced)
|
|
||||||
|
|
||||||
* Good — incremental, no flag day, each phase shippable on its own
|
|
||||||
* Good — reuses existing JWT infrastructure (ADR-0021 secret retention)
|
|
||||||
* Good — magic link work is a prerequisite for OIDC anyway (email plumbing, mkcert)
|
|
||||||
* Bad — total work spans 2 sprints, longer time-to-OIDC than Option 2
|
|
||||||
* Mitigation: after Phase A, the team can stop if priorities shift — magic link alone is a complete improvement
|
|
||||||
|
|
||||||
### Option 2 (All OIDC)
|
|
||||||
|
|
||||||
* Good — single migration
|
|
||||||
* Bad — requires IdP operational from day 1
|
|
||||||
* Bad — local dev environment more complex than necessary for the magic link case
|
|
||||||
|
|
||||||
### Option 3 (Magic link only)
|
|
||||||
|
|
||||||
* Good — minimal scope
|
|
||||||
* Bad — re-work later for SSO
|
|
||||||
|
|
||||||
### Option 4 (Status quo)
|
|
||||||
|
|
||||||
* Good — zero effort
|
|
||||||
* Bad — accumulating tech debt
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
* `pkg/auth/` package created (currently auth code lives in `pkg/user/`) — separation is now justified by the multi-mechanism scope
|
|
||||||
* `pkg/user/api/auth_handler.go` continues to serve username/password during transition (Phase A and B), removed in Phase C
|
|
||||||
* `documentation/DEV_SETUP.md` becomes a load-bearing doc for new contributors (mkcert + docker-compose with mailpit + Authelia)
|
|
||||||
* The 4 new endpoints (`magic-link/request`, `magic-link/consume`, `oidc/start`, `oidc/callback`) require their own ADR entries in the API doc + Swagger annotations
|
|
||||||
* Phase A's magic link plumbing depends on **ADR-0029** (email infrastructure decision) — that ADR ships first
|
|
||||||
* BDD scenarios for Phase A depend on **ADR-0030** (email testing strategy with parallel BDD) — that ADR ships before any Phase A scenario lands
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
* Email infrastructure : [ADR-0029](0029-email-infrastructure-mailpit.md)
|
|
||||||
* BDD email testing strategy : [ADR-0030](0030-bdd-email-parallel-strategy.md)
|
|
||||||
* Existing user auth (to be partially superseded by Phase C) : [ADR-0018](0018-user-management-auth-system.md)
|
|
||||||
* JWT secret retention reused : [ADR-0021](0021-jwt-secret-retention-policy.md)
|
|
||||||
* Rate limiting reused : [ADR-0022](0022-rate-limiting-cache-strategy.md)
|
|
||||||
* OAuth 2.0 Authorization Code with PKCE : [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)
|
|
||||||
* OpenID Connect Core : [OpenID Foundation](https://openid.net/specs/openid-connect-core-1_0.html)
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# 29. Email infrastructure: Mailpit local + production deferred
|
|
||||||
|
|
||||||
**Date:** 2026-05-05
|
|
||||||
**Status:** Proposed
|
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
ADR-0028 (passwordless auth) requires the application to send emails — magic-link tokens specifically. Email is a substrate decision : the choice of SMTP provider, the abstraction in code, and the local development experience all depend on it.
|
|
||||||
|
|
||||||
Two separate concerns :
|
|
||||||
|
|
||||||
1. **Local development + BDD tests** : we need a local SMTP receiver that captures emails and exposes them for inspection. Real email providers (Gmail, SES, SendGrid) are unsuitable for local dev — they cost money, leak test data, and rate-limit aggressively.
|
|
||||||
2. **Production** : the application needs to actually deliver mail to user inboxes. This decision is deferred — see "Out of scope" below.
|
|
||||||
|
|
||||||
ARCODANGE already has the **Mailpit** docker image pulled locally (`axllent/mailpit:latest`, 51 MB). Mailpit captures SMTP submissions on a port, stores them in-memory, exposes them via HTTP UI (default :8025) and an HTTP API (`/api/v1/messages`). It's the de-facto choice for Go projects needing local SMTP capture.
|
|
||||||
|
|
||||||
The application code needs to be **provider-agnostic** : a `pkg/email` package with a `Sender` interface, a Mailpit-compatible SMTP implementation, and a contract that production can swap for a real provider's adapter without changing call sites.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
* **Local dev and CI must work without internet** — emails should never leave the docker network in tests
|
|
||||||
* **Test inspection must be programmatic** — BDD tests assert on email content, not just "an email was sent"
|
|
||||||
* **Production decision deferred** — we don't know the volume / SLA / compliance requirements yet ; over-committing now is premature
|
|
||||||
* **Provider portability** — `pkg/email` interface lets us swap implementations without touching auth code
|
|
||||||
* **Cost** — Mailpit is free, runs in a container, no API quota concerns
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen): Mailpit for local + tests, production via a production-grade provider TBD
|
|
||||||
|
|
||||||
* Add Mailpit to `docker-compose.yml` (SMTP :1025, HTTP API :8025)
|
|
||||||
* `pkg/email` package with a `Sender` interface
|
|
||||||
* Default implementation : `SMTPSender` configured against the local Mailpit in dev/CI
|
|
||||||
* Tests query Mailpit's HTTP API to inspect captured messages
|
|
||||||
* Production deployment will add a separate `pkg/email/<provider>_sender.go` implementing the same interface — that decision is its own ADR
|
|
||||||
|
|
||||||
### Option 2: MailHog instead of Mailpit
|
|
||||||
|
|
||||||
MailHog is the older, well-known alternative. Mailpit is its modern successor, written in Go, with a richer API and active maintenance.
|
|
||||||
|
|
||||||
* Bad — abandoned upstream (last commit 2020). Mailpit is the natural replacement.
|
|
||||||
|
|
||||||
### Option 3: In-process mock email sender
|
|
||||||
|
|
||||||
Write a `MockSender` that captures emails in a Go slice. No SMTP at all.
|
|
||||||
|
|
||||||
* Good — fastest tests, zero infra
|
|
||||||
* Bad — doesn't validate the actual SMTP wire format, the From/To/Subject headers, the encoding of multi-byte content, or the DKIM/Reply-To setup
|
|
||||||
* Bad — doesn't double as a manual-inspection tool for the developer (no UI to look at the email)
|
|
||||||
|
|
||||||
### Option 4: Send to a real but throwaway provider (Mailtrap, Mailosaur)
|
|
||||||
|
|
||||||
External services that capture-and-display emails.
|
|
||||||
|
|
||||||
* Good — production-similar paths
|
|
||||||
* Bad — costs money, requires an account, leaks test data, doesn't work offline
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option : **Option 1 — Mailpit for local + tests, production deferred**.
|
|
||||||
|
|
||||||
Rationale :
|
|
||||||
- Mailpit is the modern, maintained successor to MailHog ; image is already on the dev machine
|
|
||||||
- The interface-first design (`pkg/email.Sender`) means production swap is a future ADR, not a refactor
|
|
||||||
- BDD tests have a real wire-format path to assert on (cf. ADR-0030)
|
|
||||||
- Zero monthly cost in dev/CI
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
1. **`pkg/email/sender.go`** — define the `Sender` interface :
|
|
||||||
```go
|
|
||||||
type Sender interface {
|
|
||||||
Send(ctx context.Context, msg Message) error
|
|
||||||
}
|
|
||||||
type Message struct {
|
|
||||||
To string
|
|
||||||
From string
|
|
||||||
Subject string
|
|
||||||
BodyText string
|
|
||||||
BodyHTML string
|
|
||||||
Headers map[string]string // for trace correlation, e.g. X-Test-Scenario-ID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
2. **`pkg/email/smtp_sender.go`** — implementation using `net/smtp` (stdlib) configured by `auth.email.smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`, `smtp_use_tls`. For Mailpit defaults : `smtp_host=localhost smtp_port=1025 smtp_use_tls=false`.
|
|
||||||
3. **`pkg/email/sender_test.go`** — unit tests using `httptest`-style fake SMTP, plus a `*_integration_test.go` (build tag `integration`) hitting the live Mailpit.
|
|
||||||
4. **`docker-compose.yml`** — add the `mailpit` service :
|
|
||||||
```yaml
|
|
||||||
mailpit:
|
|
||||||
image: axllent/mailpit:latest
|
|
||||||
ports:
|
|
||||||
- "1025:1025" # SMTP
|
|
||||||
- "8025:8025" # HTTP UI / API
|
|
||||||
environment:
|
|
||||||
MP_MAX_MESSAGES: 5000
|
|
||||||
```
|
|
||||||
5. **`pkg/config/config.go`** — add the `auth.email.*` config keys with defaults pointing at local Mailpit.
|
|
||||||
6. **Documentation** : `documentation/EMAIL.md` covering local setup, message inspection via UI (http://localhost:8025), API queries.
|
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen — Mailpit)
|
|
||||||
|
|
||||||
* Good — already locally available, free, modern, maintained
|
|
||||||
* Good — provider-agnostic interface decouples from prod choice
|
|
||||||
* Good — full SMTP wire format = realistic test path
|
|
||||||
* Good — UI for manual inspection during dev
|
|
||||||
* Bad — requires Mailpit running (one more docker-compose service)
|
|
||||||
* Bad — production decision still pending
|
|
||||||
|
|
||||||
### Option 2 (MailHog)
|
|
||||||
|
|
||||||
* Bad — unmaintained, choosing it would create immediate tech debt
|
|
||||||
|
|
||||||
### Option 3 (Mock only)
|
|
||||||
|
|
||||||
* Bad — too much abstraction loss, can't catch wire-level bugs
|
|
||||||
|
|
||||||
### Option 4 (Mailtrap / Mailosaur)
|
|
||||||
|
|
||||||
* Bad — cost, network dependency, account management
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
* New service in `docker-compose.yml` — developers run `docker compose up -d` once and Mailpit is on
|
|
||||||
* New `pkg/email` package — auth code (ADR-0028 magic link) calls `Sender.Send()` rather than direct SMTP
|
|
||||||
* New `auth.email.*` config keys, new env vars (`DLC_AUTH_EMAIL_SMTP_HOST` etc.)
|
|
||||||
* Mailpit's HTTP API becomes part of the BDD test contract — tests use it to assert messages were sent (cf. ADR-0030)
|
|
||||||
* Production sender ADR (TBD) will be a separate decision — this ADR explicitly does NOT pick a vendor for prod
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
* **Production email provider selection** — separate ADR when we know volume / SLA / compliance constraints. Likely candidates: AWS SES, Postmark, SendGrid, Mailjet. Magic-link emails are transactional + low-volume — most providers handle that easily.
|
|
||||||
* **DKIM/SPF/DMARC setup** — production deliverability concern, not a local-dev concern
|
|
||||||
* **HTML email templating** — we'll start with plain-text emails ; HTML can be added with a template package (e.g. `html/template`) when ARCODANGE branding requires it
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
* Auth migration that requires this : [ADR-0028](0028-passwordless-auth-migration.md)
|
|
||||||
* BDD test strategy that consumes Mailpit : [ADR-0030](0030-bdd-email-parallel-strategy.md)
|
|
||||||
* Mailpit homepage : https://mailpit.axllent.org/
|
|
||||||
* Mailpit API reference : https://mailpit.axllent.org/docs/api-v1/
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
# 30. BDD email assertions with parallel test execution
|
|
||||||
|
|
||||||
**Date:** 2026-05-05
|
|
||||||
**Status:** Proposed
|
|
||||||
**Authors:** Gabriel Radureau, AI Agent
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
|
||||||
|
|
||||||
ADR-0028 introduces magic-link auth, which requires the application to send emails. ADR-0029 chose **Mailpit** as the local SMTP receiver for dev and BDD tests. The remaining decision : **how do BDD scenarios assert on the email content while running in parallel ?**
|
|
||||||
|
|
||||||
Today (since [PR #35](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/35)), the BDD suite runs in parallel via per-package PostgreSQL schema isolation (cf. [ADR-0025](0025-bdd-scenario-isolation-strategies.md)). Each Go test package has its own schema ; tests inside a package run serially within that schema. This works because Postgres has named schemas with strong isolation. **Mailpit has no equivalent** — there is one inbox per Mailpit instance, shared across all senders.
|
|
||||||
|
|
||||||
A naive integration would have parallel scenarios fight over each other's emails :
|
|
||||||
- Scenario A : "request magic link for `test@example.com`" → email arrives
|
|
||||||
- Scenario B (in parallel) : "request magic link for `test@example.com`" → email arrives
|
|
||||||
- Both scenarios query Mailpit for `test@example.com` — they see each other's messages, assertions become flaky.
|
|
||||||
|
|
||||||
We need a way to scope each scenario's emails so it only sees its own messages.
|
|
||||||
|
|
||||||
## Decision Drivers
|
|
||||||
|
|
||||||
* **No regression on parallelism** — BDD-isolation Phase 3 (PR #35) achieved a 2.85x speedup ; the email-assertion solution must not undo that
|
|
||||||
* **No new container per test** — running one Mailpit per scenario would defeat the simplicity that made us choose Mailpit
|
|
||||||
* **Determinism** — a scenario's email assertions must succeed regardless of how many other scenarios are running
|
|
||||||
* **Realistic SMTP path** — we still want the full SMTP wire format exercised (cf. ADR-0029) ; we don't want to bypass Mailpit
|
|
||||||
* **Cleanup hygiene** — old messages from previous test runs must not leak into a new run
|
|
||||||
|
|
||||||
## Considered Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen): Per-test recipient scoping with deterministic addresses
|
|
||||||
|
|
||||||
Each BDD scenario generates a unique email address for its test user, derived from the scenario key + a random suffix. Examples :
|
|
||||||
|
|
||||||
- Scenario `magic-link-happy-path` → `magic-link-happy-path-<8hex>@bdd.local`
|
|
||||||
- Scenario `magic-link-expired-token` → `magic-link-expired-token-<8hex>@bdd.local`
|
|
||||||
|
|
||||||
The application code accepts any email format. The BDD scenario asserts on Mailpit's HTTP API filtering by the `to` address. Two parallel scenarios with different addresses can NEVER see each other's emails.
|
|
||||||
|
|
||||||
**Cleanup** : at the start of each scenario, the BDD framework calls `DELETE /api/v1/search?query=to:<scenario-address>` on Mailpit to purge any leftover messages from prior runs.
|
|
||||||
|
|
||||||
### Option 2: One Mailpit instance per Go test package
|
|
||||||
|
|
||||||
Spawn a fresh Mailpit container in `TestMain` of each `features/<area>/` package. Each gets its own port range.
|
|
||||||
|
|
||||||
* Good — strong isolation
|
|
||||||
* Bad — heavyweight (one container per package = 5+ containers running)
|
|
||||||
* Bad — port allocation complexity (similar to existing `pkg/bdd/parallel/port_manager.go`, but applied to Mailpit)
|
|
||||||
* Bad — slow startup (Mailpit boot is ~200ms but adds up)
|
|
||||||
|
|
||||||
### Option 3: One Mailpit instance, scenario-scoped via custom SMTP header
|
|
||||||
|
|
||||||
Add a custom header `X-BDD-Scenario-ID: <key>` to outgoing emails. Tests query Mailpit filtered on that header.
|
|
||||||
|
|
||||||
* Good — same single Mailpit
|
|
||||||
* Bad — requires the application code to know the scenario ID at email-send time, which means a test-only path in production code
|
|
||||||
* Bad — header propagation is fragile (gets stripped by some SMTP relays — not Mailpit, but real production providers might) ; we don't want a different code path between dev and prod
|
|
||||||
|
|
||||||
### Option 4: Sequence parallel scenarios via per-scenario Mailpit lock
|
|
||||||
|
|
||||||
Use a mutex / queue so no two scenarios that send email run concurrently.
|
|
||||||
|
|
||||||
* Good — minimal code change
|
|
||||||
* Bad — gives up the parallel speedup for any feature that involves email — that's most auth-related features going forward
|
|
||||||
|
|
||||||
## Decision Outcome
|
|
||||||
|
|
||||||
Chosen option : **Option 1 — per-test recipient scoping**.
|
|
||||||
|
|
||||||
Rationale :
|
|
||||||
- Recipient scoping is the simplest abstraction : the address IS the identity ; Mailpit's HTTP API natively supports filtering by recipient
|
|
||||||
- Application code stays clean : it just sends to whatever address it's given. No test-mode branching.
|
|
||||||
- Parallel-safe by construction : two scenarios cannot collide if they don't share an address
|
|
||||||
- Cheap to implement : a few helper functions in `pkg/bdd/steps/email_steps.go` and a `mailpit.Client` package wrapping the HTTP API
|
|
||||||
- Cleanup is per-scenario, not global — no "delete all messages" race between scenarios
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Helper package : `pkg/bdd/mailpit/client.go`
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Client struct {
|
|
||||||
BaseURL string // default: http://localhost:8025
|
|
||||||
HTTP *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// AwaitMessageTo polls Mailpit's HTTP API for a message addressed
|
|
||||||
// to the given recipient, with a deadline. Returns the most recent
|
|
||||||
// matching message or an error on timeout.
|
|
||||||
func (c *Client) AwaitMessageTo(ctx context.Context, to string, timeout time.Duration) (*Message, error)
|
|
||||||
|
|
||||||
// PurgeMessagesTo removes all messages addressed to the given
|
|
||||||
// recipient. Idempotent and parallel-safe.
|
|
||||||
func (c *Client) PurgeMessagesTo(ctx context.Context, to string) error
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
ID string
|
|
||||||
From string
|
|
||||||
To []string
|
|
||||||
Subject string
|
|
||||||
Text string
|
|
||||||
HTML string
|
|
||||||
Headers map[string][]string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Helper steps : `pkg/bdd/steps/email_steps.go`
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (s *EmailSteps) iHaveAnEmailAddressForThisScenario() error
|
|
||||||
// Generates `<scenario-key>-<8hex>@bdd.local`, stores it in the scenario state.
|
|
||||||
|
|
||||||
func (s *EmailSteps) iShouldReceiveAnEmailWithSubject(subject string) error
|
|
||||||
// Polls AwaitMessageTo on the scenario's address, asserts subject equality.
|
|
||||||
|
|
||||||
func (s *EmailSteps) theEmailShouldContain(snippet string) error
|
|
||||||
// Re-fetches the most recent message and checks for substring in body.
|
|
||||||
|
|
||||||
func (s *EmailSteps) theEmailContainsAMagicLinkToken() (string, error)
|
|
||||||
// Extracts the token from the magic-link URL via regex, returns it.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scenario lifecycle
|
|
||||||
|
|
||||||
- **Before each scenario** : `iHaveAnEmailAddressForThisScenario` is called (either explicitly via Background, or implicitly via a hook). The unique address is stored in the scenario's state. PurgeMessagesTo is called to clear any leftovers from prior runs of the same address (defensive — should be impossible since the suffix is random, but cheap).
|
|
||||||
- **During the scenario** : the application sends to that address. Tests query for it.
|
|
||||||
- **After each scenario** : no global cleanup needed — addresses are per-scenario unique, so they don't accumulate beyond Mailpit's `MP_MAX_MESSAGES=5000` cap.
|
|
||||||
|
|
||||||
### Race-free deletion
|
|
||||||
|
|
||||||
Mailpit's `DELETE /api/v1/search?query=to:<addr>` is atomic per recipient. Two concurrent scenarios with different addresses cannot interfere.
|
|
||||||
|
|
||||||
### Sample scenario (auth-magic-link.feature)
|
|
||||||
|
|
||||||
```gherkin
|
|
||||||
@critical @magic-link
|
|
||||||
Scenario: User receives a magic link by email
|
|
||||||
Given I have an email address for this scenario
|
|
||||||
When I request a magic link for my email address
|
|
||||||
Then I should receive an email with subject "Your magic link"
|
|
||||||
And the email contains a magic link token
|
|
||||||
When I consume the magic link token
|
|
||||||
Then I should receive a JWT
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
|
||||||
|
|
||||||
### Option 1 (Chosen)
|
|
||||||
|
|
||||||
* Good — parallel-safe by construction
|
|
||||||
* Good — application code unchanged ; test-only logic stays in the BDD layer
|
|
||||||
* Good — Mailpit API supports the filter natively
|
|
||||||
* Good — cleanup is fine-grained, no race
|
|
||||||
* Bad — requires cooperative scenarios (each must request a unique address)
|
|
||||||
* Mitigation : Background steps in feature files make it automatic
|
|
||||||
|
|
||||||
### Option 2 (Mailpit per package)
|
|
||||||
|
|
||||||
* Bad — operational complexity not justified for the test-only concern
|
|
||||||
|
|
||||||
### Option 3 (Custom header scoping)
|
|
||||||
|
|
||||||
* Bad — production code dirtied by test concerns
|
|
||||||
|
|
||||||
### Option 4 (Lock-and-sequence)
|
|
||||||
|
|
||||||
* Bad — gives up parallelism (the whole point of PR #35 + ADR-0025)
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
* `pkg/bdd/mailpit/` package is created with HTTP client + helper types
|
|
||||||
* `pkg/bdd/steps/email_steps.go` package is created and registered in `steps.go`
|
|
||||||
* `features/auth/` and any other email-using features have new BDD steps available
|
|
||||||
* The local development docker-compose must run Mailpit before BDD tests run — to be added to the BDD test runner script `scripts/run-bdd-tests.sh`
|
|
||||||
* Mailpit message TTL is governed by `MP_MAX_MESSAGES` (5000) — at parallel BDD volumes, that's enough headroom for ~50 scenarios × 100 messages each before any pruning kicks in
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
* **Visual regression on email rendering** — text body assertions only ; HTML rendering checks belong in a separate Storybook-style harness
|
|
||||||
* **Attachment handling** — magic-link emails are text-only ; ADRs for attachments will come if/when needed
|
|
||||||
* **Email volume / rate-limit testing** — that's a load-test concern, not a BDD concern
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
* Auth migration depending on this : [ADR-0028](0028-passwordless-auth-migration.md)
|
|
||||||
* Email infrastructure choice : [ADR-0029](0029-email-infrastructure-mailpit.md)
|
|
||||||
* BDD parallelism foundation : [ADR-0025](0025-bdd-scenario-isolation-strategies.md), [PR #35](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/35)
|
|
||||||
* Mailpit API : https://mailpit.axllent.org/docs/api-v1/
|
|
||||||
169
adr/README.md
169
adr/README.md
@@ -1,118 +1,129 @@
|
|||||||
# Architecture Decision Records (ADRs)
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
This directory contains the Architecture Decision Records (ADRs) for the dance-lessons-coach project. Each ADR captures a structurally important decision, its context, and its consequences.
|
This directory contains Architecture Decision Records (ADRs) for the dance-lessons-coach project.
|
||||||
|
|
||||||
## Index
|
## Index of ADRs
|
||||||
|
|
||||||
| ADR | Title | Status |
|
| Number | Title | Status |
|
||||||
|-----|-------|--------|
|
|--------|-------|--------|
|
||||||
| [0001](0001-go-1.26.1-standard.md) | Use Go 1.26.1 as the standard Go version | Accepted |
|
| 0001 | Go 1.26.1 Standard | ✅ Accepted |
|
||||||
| [0002](0002-chi-router.md) | Use Chi router for HTTP routing | Accepted |
|
| 0002 | Chi Router | ✅ Accepted |
|
||||||
| [0003](0003-zerolog-logging.md) | Use Zerolog for structured logging | Accepted |
|
| 0003 | Zerolog Logging | ✅ Accepted |
|
||||||
| [0004](0004-interface-based-design.md) | Adopt interface-based design pattern | Accepted |
|
| 0004 | Interface-Based Design | ✅ Accepted |
|
||||||
| [0005](0005-graceful-shutdown.md) | Implement graceful shutdown with readiness endpoints | Accepted |
|
| 0005 | Graceful Shutdown | ✅ Accepted |
|
||||||
| [0006](0006-configuration-management.md) | Use Viper for configuration management | Accepted |
|
| 0006 | Configuration Management | ✅ Accepted |
|
||||||
| [0007](0007-opentelemetry-integration.md) | Integrate OpenTelemetry for distributed tracing | Accepted |
|
| 0007 | OpenTelemetry Integration | ✅ Accepted |
|
||||||
| [0008](0008-bdd-testing.md) | Adopt BDD with Godog for behavioral testing | Accepted |
|
| 0008 | BDD Testing | ✅ Accepted |
|
||||||
| [0009](0009-hybrid-testing-approach.md) | Combine BDD and Swagger-based testing | Implemented |
|
| 0009 | Hybrid Testing Approach | ✅ Accepted |
|
||||||
| [0010](0010-api-v2-feature-flag.md) | API v2 Feature Flag Implementation | Accepted |
|
| 0010 | CI/CD Pipeline Design | ✅ Accepted |
|
||||||
| [0012](0012-git-hooks-staged-only-formatting.md) | Git Hooks: Staged-Only Formatting | Accepted |
|
| 0011 | Trunk-Based Development | ✅ Accepted |
|
||||||
| [0013](0013-openapi-swagger-toolchain.md) | OpenAPI/Swagger Toolchain Selection | Implemented |
|
| 0012 | Commit Message Conventions | ✅ Accepted |
|
||||||
| [0015](0015-cli-subcommands-cobra.md) | CLI Subcommands and Flag Management with Cobra | Implemented |
|
| 0013 | Version Management Lifecycle | ✅ Accepted |
|
||||||
| [0016](0016-ci-cd-pipeline-design.md) | CI/CD Pipeline Design for Multi-Platform Compatibility | Accepted |
|
| 0014 | Swagger Documentation | ✅ Accepted |
|
||||||
| [0017](0017-trunk-based-development-workflow.md) | Trunk-Based Development Workflow for CI/CD Safety | Approved |
|
| 0015 | Rate Limiting Strategy | ✅ Accepted |
|
||||||
| [0018](0018-user-management-auth-system.md) | User Management and Authentication System | Implemented |
|
| 0016 | Cache Invalidation Strategy | ✅ Accepted |
|
||||||
| [0019](0019-postgresql-integration.md) | PostgreSQL Database Integration | Implemented |
|
| 0017 | JWT Secret Rotation | ✅ Accepted |
|
||||||
| [0020](0020-docker-build-strategy.md) | Docker Build Strategy: Traditional vs Buildx | Accepted |
|
| 0018 | Configuration Hot Reloading | ✅ Accepted |
|
||||||
| [0021](0021-jwt-secret-retention-policy.md) | JWT Secret Retention Policy | Implemented |
|
| 0019 | BDD Feature Structure | ✅ Accepted |
|
||||||
| [0022](0022-rate-limiting-cache-strategy.md) | Rate Limiting and Cache Strategy | Implemented (Phase 1) |
|
| 0020 | Database Migration Strategy | ✅ Accepted |
|
||||||
| [0023](0023-config-hot-reloading.md) | Config Hot Reloading Strategy | Implemented |
|
| 0021 | API Versioning Strategy | ✅ Accepted |
|
||||||
| [0024](0024-bdd-test-organization-and-isolation.md) | BDD Test Organization and Isolation Strategy | Implemented |
|
| 0022 | Rate Limiting and Cache Strategy | ✅ Accepted |
|
||||||
| [0025](0025-bdd-scenario-isolation-strategies.md) | BDD Scenario Isolation Strategies | Implemented |
|
| 0023 | Config Hot Reloading | 🟡 Proposed |
|
||||||
| [0026](0026-composite-info-endpoint.md) | Composite Info Endpoint vs Separate Calls | Implemented |
|
| 0024 | BDD Test Organization and Isolation | 🟡 Proposed |
|
||||||
| [0027](0027-ollama-tier1-onboarding.md) | Ollama Tier 1 onboarding via meta-trainer-bootstrap | Proposed |
|
| 0025 | BDD Scenario Isolation Strategies | 🟡 Proposed |
|
||||||
| [0028](0028-passwordless-auth-migration.md) | Passwordless authentication: magic link → OpenID Connect | Proposed |
|
|
||||||
| [0029](0029-email-infrastructure-mailpit.md) | Email infrastructure: Mailpit local + production deferred | Proposed |
|
|
||||||
| [0030](0030-bdd-email-parallel-strategy.md) | BDD email assertions with parallel test execution | Proposed |
|
|
||||||
|
|
||||||
> **Note** : numbers `0011` and `0014` are not currently in use. Reserved for future ADRs or representing previously deleted entries.
|
|
||||||
|
|
||||||
## What is an ADR?
|
## What is an ADR?
|
||||||
|
|
||||||
An ADR is a document capturing one significant architectural decision: the **context** that motivated it, the **decision** itself, and its **consequences**. ADRs are append-only — once published, an ADR is not edited (except for typo / status updates). New decisions that supersede previous ones are recorded as new ADRs that explicitly link back.
|
An ADR is a document that captures an important architectural decision made along with its context and consequences.
|
||||||
|
|
||||||
## Canonical Format
|
## Format
|
||||||
|
|
||||||
All ADRs follow the canonical format below (homogenized 2026-05-03):
|
Each ADR follows this structure:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# NN. Short title summarising the decision
|
# [Short title is a few words]
|
||||||
|
|
||||||
**Status:** <Proposed | Accepted | Implemented | Partially Implemented | Approved | Rejected | Deferred | Deprecated | Superseded by ADR-NNNN>
|
* Status: [Proposed | Accepted | Deprecated | Superseded]
|
||||||
**Date:** YYYY-MM-DD
|
* Deciders: [List of decision makers]
|
||||||
**Authors:** Name(s)
|
* Date: [YYYY-MM-DD]
|
||||||
|
|
||||||
[Optional fields, all in `**Field:** value` format:]
|
|
||||||
**Decision Drivers:** ...
|
|
||||||
**Implementation Status:** ...
|
|
||||||
**Implementation Date:** ...
|
|
||||||
**Last Updated:** ...
|
|
||||||
|
|
||||||
## Context and Problem Statement
|
## Context and Problem Statement
|
||||||
|
|
||||||
[Describe the context and problem statement.]
|
[Describe the context and problem statement]
|
||||||
|
|
||||||
## Decision Drivers
|
## Decision Drivers
|
||||||
|
|
||||||
* Driver 1
|
* [Driver 1]
|
||||||
* Driver 2
|
* [Driver 2]
|
||||||
|
* [Driver 3]
|
||||||
|
|
||||||
## Considered Options
|
## Considered Options
|
||||||
|
|
||||||
* Option 1
|
* [Option 1]
|
||||||
* Option 2
|
* [Option 2]
|
||||||
|
* [Option 3]
|
||||||
|
|
||||||
## Decision Outcome
|
## Decision Outcome
|
||||||
|
|
||||||
Chosen option: "Option 1" because [justification].
|
Chosen option: "[Option 1]" because [justification]
|
||||||
|
|
||||||
## Pros and Cons of the Options
|
## Pros and Cons of the Options
|
||||||
|
|
||||||
### Option 1
|
### [Option 1]
|
||||||
|
|
||||||
* Good, because [argument].
|
* Good, because [argument a]
|
||||||
* Bad, because [argument].
|
* Good, because [argument b]
|
||||||
|
* Bad, because [argument c]
|
||||||
|
|
||||||
### Option 2
|
### [Option 2]
|
||||||
|
|
||||||
* Good, because [argument].
|
* Good, because [argument a]
|
||||||
* Bad, because [argument].
|
* Good, because [argument b]
|
||||||
|
* Bad, because [argument c]
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
* Related ADR: [ADR-NNNN](NNNN-slug.md)
|
* [Link type] [Link to ADR]
|
||||||
* Issue: [#NN](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/issues/NN)
|
* [Link type] [Link to ADR]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Status Legend
|
## ADR List
|
||||||
|
|
||||||
| Status | Meaning |
|
* [0001-go-1.26.1-standard.md](0001-go-1.26.1-standard.md) - Use Go 1.26.1 as the standard Go version
|
||||||
|---|---|
|
* [0002-chi-router.md](0002-chi-router.md) - Use Chi router for HTTP routing
|
||||||
| **Proposed** | Decision is being discussed; no implementation yet. |
|
* [0003-zerolog-logging.md](0003-zerolog-logging.md) - Use Zerolog for structured logging
|
||||||
| **Accepted** | Decision has been made; implementation may be pending or in progress. |
|
* [0004-interface-based-design.md](0004-interface-based-design.md) - Adopt interface-based design pattern
|
||||||
| **Approved** | Same as Accepted; alternative term used in some legacy ADRs. |
|
* [0005-graceful-shutdown.md](0005-graceful-shutdown.md) - Implement graceful shutdown with readiness endpoints
|
||||||
| **Implemented** | Decision is fully implemented and in production. |
|
* [0006-configuration-management.md](0006-configuration-management.md) - Use Viper for configuration management
|
||||||
| **Partially Implemented** | Decision is partly implemented; remainder is deferred or pending. |
|
* [0007-opentelemetry-integration.md](0007-opentelemetry-integration.md) - Integrate OpenTelemetry for distributed tracing
|
||||||
| **Rejected** | Decision considered and explicitly rejected. The ADR documents why. |
|
* [0008-bdd-testing.md](0008-bdd-testing.md) - Adopt BDD with Godog for behavioral testing
|
||||||
| **Deferred** | Decision postponed; revisit later. |
|
* [0009-hybrid-testing-approach.md](0009-hybrid-testing-approach.md) - Combine BDD and Swagger-based testing
|
||||||
| **Deprecated** | Decision is no longer relevant; system has moved on. |
|
* [0010-api-v2-feature-flag.md](0010-api-v2-feature-flag.md) - API v2 implementation with feature flag control
|
||||||
| **Superseded by ADR-NNNN** | Decision has been replaced by another ADR. Always include the link. |
|
* [0011-validation-library-selection.md](0011-validation-library-selection.md) - Selection of go-playground/validator for input validation
|
||||||
|
* [0012-git-hooks-staged-only-formatting.md](0012-git-hooks-staged-only-formatting.md) - Git hooks format only staged Go files
|
||||||
|
* [0013-openapi-swagger-toolchain.md](0013-openapi-swagger-toolchain.md) - ✅ OpenAPI/Swagger documentation with swaggo/swag (Implemented)
|
||||||
|
* [0014-grpc-adoption-strategy.md](0014-grpc-adoption-strategy.md) - Hybrid REST/gRPC adoption strategy
|
||||||
|
* [0015-cli-subcommands-cobra.md](0015-cli-subcommands-cobra.md) - Cobra CLI framework adoption
|
||||||
|
* [0016-ci-cd-pipeline-design.md](0016-ci-cd-pipeline-design.md) - CI/CD pipeline architecture
|
||||||
|
* [0017-trunk-based-development-workflow.md](0017-trunk-based-development-workflow.md) - Trunk-based development workflow
|
||||||
|
* [0018-user-management-auth-system.md](0018-user-management-auth-system.md) - User management and authentication system
|
||||||
|
* [0019-postgresql-integration.md](0019-postgresql-integration.md) - PostgreSQL database integration
|
||||||
|
* [0020-docker-build-strategy.md](0020-docker-build-strategy.md) - Docker Build Strategy: Traditional vs Buildx
|
||||||
|
* [0021-jwt-secret-retention-policy.md](0021-jwt-secret-retention-policy.md) - JWT Secret Retention Policy with Configurable TTL and Retention
|
||||||
|
* [0022-rate-limiting-cache-strategy.md](0022-rate-limiting-cache-strategy.md) - Rate Limiting and Cache Strategy with Multi-Phase Implementation
|
||||||
|
* [0023-config-hot-reloading.md](0023-config-hot-reloading.md) - Config Hot Reloading Strategy
|
||||||
|
* [0025-bdd-scenario-isolation-strategies.md](0025-bdd-scenario-isolation-strategies.md) - Schema-per-scenario isolation for BDD tests
|
||||||
|
|
||||||
## How to Add a New ADR
|
## How to Add a New ADR
|
||||||
|
|
||||||
1. Pick the next available number (currently next would be `0026`).
|
1. Create a new file with the next available number (e.g., `0010-new-decision.md`)
|
||||||
2. Copy an existing ADR (e.g., `0001-go-1.26.1-standard.md`) as a starting template.
|
2. Follow the template format
|
||||||
3. Edit the title, status, date, authors, and content.
|
3. Update this README.md with the new ADR
|
||||||
4. Update this `README.md` index with the new ADR.
|
4. Commit the changes
|
||||||
5. Commit using gitmoji convention (e.g., `📝 docs(adr): add ADR-0026 about ...`).
|
|
||||||
6. Open a PR for review.
|
## Status Legend
|
||||||
|
|
||||||
|
* **Proposed**: Decision is being discussed
|
||||||
|
* **Accepted**: Decision has been made and implemented
|
||||||
|
* **Deprecated**: Decision is no longer relevant
|
||||||
|
* **Superseded**: Decision has been replaced by another ADR
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Patterns to ignore when building packages.
|
|
||||||
# This supports shell glob matching, relative path matching, and
|
|
||||||
# negation (prefixed with !). Only one pattern per line.
|
|
||||||
.DS_Store
|
|
||||||
# Common VCS dirs
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.bzr/
|
|
||||||
.bzrignore
|
|
||||||
.hg/
|
|
||||||
.hgignore
|
|
||||||
.svn/
|
|
||||||
# Common backup files
|
|
||||||
*.swp
|
|
||||||
*.bak
|
|
||||||
*.tmp
|
|
||||||
*.orig
|
|
||||||
*~
|
|
||||||
# Various IDEs
|
|
||||||
.project
|
|
||||||
.idea/
|
|
||||||
*.tmproj
|
|
||||||
.vscode/
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: dance-lessons-coach
|
|
||||||
description: Helm chart for dance-lessons-coach Go API server (ARCODANGE)
|
|
||||||
type: application
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: "latest"
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
1. Get the application URL by running these commands:
|
|
||||||
{{- if .Values.ingress.enabled }}
|
|
||||||
{{- range $host := .Values.ingress.hosts }}
|
|
||||||
{{- range .paths }}
|
|
||||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- else if contains "NodePort" .Values.service.type }}
|
|
||||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "dance-lessons-coach.fullname" . }})
|
|
||||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
|
||||||
echo http://$NODE_IP:$NODE_PORT
|
|
||||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
|
||||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
|
||||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "dance-lessons-coach.fullname" . }}'
|
|
||||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "dance-lessons-coach.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
|
||||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
|
||||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
|
||||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "dance-lessons-coach.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
|
||||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
|
||||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
|
||||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
If release name contains chart name it will be used as a full name.
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.fullname" -}}
|
|
||||||
{{- if .Values.fullnameOverride }}
|
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
||||||
{{- if contains $name .Release.Name }}
|
|
||||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create chart name and version as used by the chart label.
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "dance-lessons-coach.chart" . }}
|
|
||||||
{{ include "dance-lessons-coach.selectorLabels" . }}
|
|
||||||
{{- if .Chart.AppVersion }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
{{- end }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Selector labels
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "dance-lessons-coach.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "dance-lessons-coach.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create }}
|
|
||||||
{{- default (include "dance-lessons-coach.fullname" .) .Values.serviceAccount.name }}
|
|
||||||
{{- else }}
|
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: {{ include "dance-lessons-coach.fullname" . }}-config
|
|
||||||
namespace: {{ .Release.Namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 4 }}
|
|
||||||
data:
|
|
||||||
{{ toYaml .Values.config | indent 2 }}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "dance-lessons-coach.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
revisionHistoryLimit: 3
|
|
||||||
{{- if not .Values.autoscaling.enabled }}
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
{{- end }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "dance-lessons-coach.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 8 }}
|
|
||||||
{{- with .Values.podLabels }}
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "dance-lessons-coach.serviceAccountName" . }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
envFrom:
|
|
||||||
- configMapRef:
|
|
||||||
name: {{ include "dance-lessons-coach.fullname" . }}-config
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
|
||||||
readinessProbe:
|
|
||||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
{{- with .Values.volumeMounts }}
|
|
||||||
volumeMounts:
|
|
||||||
{{- toYaml . | nindent 12 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.volumes }}
|
|
||||||
volumes:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
|
||||||
{{- $fullName := include "dance-lessons-coach.fullname" . -}}
|
|
||||||
{{- $svcPort := .Values.service.port -}}
|
|
||||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
|
||||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
|
||||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
|
||||||
{{- else -}}
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
{{- end }}
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: {{ $fullName }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.ingress.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
|
||||||
ingressClassName: {{ .Values.ingress.className }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if .Values.ingress.tls }}
|
|
||||||
tls:
|
|
||||||
{{- range .Values.ingress.tls }}
|
|
||||||
- hosts:
|
|
||||||
{{- range .hosts }}
|
|
||||||
- {{ . | quote }}
|
|
||||||
{{- end }}
|
|
||||||
secretName: {{ .secretName }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
rules:
|
|
||||||
{{- range .Values.ingress.hosts }}
|
|
||||||
- host: {{ .host | quote }}
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
{{- range .paths }}
|
|
||||||
- path: {{ .path }}
|
|
||||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
|
||||||
pathType: {{ .pathType }}
|
|
||||||
{{- end }}
|
|
||||||
backend:
|
|
||||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
|
||||||
service:
|
|
||||||
name: {{ $fullName }}
|
|
||||||
port:
|
|
||||||
number: {{ $svcPort }}
|
|
||||||
{{- else }}
|
|
||||||
serviceName: {{ $fullName }}
|
|
||||||
servicePort: {{ $svcPort }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "dance-lessons-coach.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
{{- include "dance-lessons-coach.selectorLabels" . | nindent 4 }}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ include "dance-lessons-coach.serviceAccountName" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "dance-lessons-coach.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{{- if .Values.vault.enabled }}
|
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
|
||||||
kind: VaultAuth
|
|
||||||
metadata:
|
|
||||||
name: auth
|
|
||||||
namespace: {{ .Release.Namespace }}
|
|
||||||
spec:
|
|
||||||
method: kubernetes
|
|
||||||
mount: kubernetes
|
|
||||||
kubernetes:
|
|
||||||
role: {{ .Values.vault.role }}
|
|
||||||
serviceAccount: {{ include "dance-lessons-coach.serviceAccountName" . }}
|
|
||||||
audiences:
|
|
||||||
- vault
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{{- if .Values.vault.enabled }}
|
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
|
||||||
kind: VaultDynamicSecret
|
|
||||||
metadata:
|
|
||||||
name: vso-db
|
|
||||||
namespace: {{ .Release.Namespace }}
|
|
||||||
spec:
|
|
||||||
mount: postgres
|
|
||||||
path: {{ .Values.vault.postgresPath }}
|
|
||||||
destination:
|
|
||||||
create: true
|
|
||||||
name: vso-db-credentials
|
|
||||||
rolloutRestartTargets:
|
|
||||||
- kind: Deployment
|
|
||||||
name: {{ include "dance-lessons-coach.fullname" . }}
|
|
||||||
vaultAuthRef: auth
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{{- if .Values.vault.enabled }}
|
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
|
||||||
kind: VaultStaticSecret
|
|
||||||
metadata:
|
|
||||||
name: vault-kv-app
|
|
||||||
namespace: {{ .Release.Namespace }}
|
|
||||||
spec:
|
|
||||||
type: kv-v2
|
|
||||||
mount: kvv2
|
|
||||||
path: {{ .Values.vault.kvv2Path }}
|
|
||||||
destination:
|
|
||||||
name: secretkv
|
|
||||||
create: true
|
|
||||||
refreshAfter: 30s
|
|
||||||
vaultAuthRef: auth
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Default values for dance-lessons-coach.
|
|
||||||
# This is a YAML-formatted file.
|
|
||||||
# Declare variables to be passed into your templates.
|
|
||||||
|
|
||||||
replicaCount: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: gitea.arcodange.lab/arcodange/dance-lessons-coach
|
|
||||||
pullPolicy: Always
|
|
||||||
# Overrides the image tag whose default is the chart appVersion.
|
|
||||||
tag: ""
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
serviceAccount:
|
|
||||||
# Specifies whether a service account should be created
|
|
||||||
create: true
|
|
||||||
# Automatically mount a ServiceAccount's API credentials?
|
|
||||||
automount: true
|
|
||||||
# Annotations to add to the service account
|
|
||||||
annotations: {}
|
|
||||||
# The name of the service account to use.
|
|
||||||
# If not set and create is true, a name is generated using the fullname template
|
|
||||||
name: ""
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
podLabels: {}
|
|
||||||
|
|
||||||
podSecurityContext: {}
|
|
||||||
# fsGroup: 2000
|
|
||||||
|
|
||||||
securityContext: {}
|
|
||||||
# capabilities:
|
|
||||||
# drop:
|
|
||||||
# - ALL
|
|
||||||
# readOnlyRootFilesystem: true
|
|
||||||
# runAsNonRoot: true
|
|
||||||
# runAsUser: 1000
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
className: ""
|
|
||||||
annotations:
|
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-crowdsec@kubernetescrd
|
|
||||||
hosts:
|
|
||||||
- host: dancecoachlessons.arcodange.lab
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls: []
|
|
||||||
|
|
||||||
resources: {}
|
|
||||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
|
||||||
# choice for the user. This also increases chances charts run on environments with little
|
|
||||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
|
||||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
|
||||||
# limits:
|
|
||||||
# cpu: 100m
|
|
||||||
# memory: 128Mi
|
|
||||||
# requests:
|
|
||||||
# cpu: 100m
|
|
||||||
# memory: 128Mi
|
|
||||||
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/healthz
|
|
||||||
port: http
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /api/healthz
|
|
||||||
port: http
|
|
||||||
|
|
||||||
autoscaling:
|
|
||||||
enabled: false
|
|
||||||
minReplicas: 1
|
|
||||||
maxReplicas: 100
|
|
||||||
targetCPUUtilizationPercentage: 80
|
|
||||||
# targetMemoryUtilizationPercentage: 80
|
|
||||||
|
|
||||||
# Additional volumes on the output Deployment definition.
|
|
||||||
volumes: []
|
|
||||||
# - name: foo
|
|
||||||
# secret:
|
|
||||||
# secretName: mysecret
|
|
||||||
# optional: false
|
|
||||||
|
|
||||||
# Additional volumeMounts on the output Deployment definition.
|
|
||||||
volumeMounts: []
|
|
||||||
# - name: foo
|
|
||||||
# mountPath: "/etc/foo"
|
|
||||||
# readOnly: true
|
|
||||||
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/hostname: pi1
|
|
||||||
|
|
||||||
tolerations: []
|
|
||||||
|
|
||||||
affinity: {}
|
|
||||||
|
|
||||||
# Vault Secrets Operator integration. Disabled by default ; set vault.enabled=true
|
|
||||||
# to render the VaultAuth / VaultStaticSecret / VaultDynamicSecret CRDs (requires
|
|
||||||
# VSO operator + Vault prereqs, cf. iac/ once shipped).
|
|
||||||
vault:
|
|
||||||
enabled: false
|
|
||||||
role: dance-lessons-coach # k8s auth backend role name (matches iac/main.tf)
|
|
||||||
kvv2Path: dance-lessons-coach/config # KVv2 secret path
|
|
||||||
postgresPath: creds/dance-lessons-coach # postgres dynamic creds path
|
|
||||||
|
|
||||||
# DLC-specific configuration
|
|
||||||
config:
|
|
||||||
DLC_LOGGING_JSON: "true"
|
|
||||||
DLC_LOGGING_LEVEL: "info"
|
|
||||||
DLC_DATABASE_HOST: ""
|
|
||||||
DLC_DATABASE_PORT: "5432"
|
|
||||||
DLC_API_V2_ENABLED: "false"
|
|
||||||
@@ -48,10 +48,8 @@ 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
|
||||||
// CancelableContext exposes Cancel() so that Server.Run() can cancel
|
readyCtx, readyCancel := context.WithCancel(context.Background())
|
||||||
// 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
|
||||||
@@ -59,5 +57,4 @@ 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")
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
config.yaml
13
config.yaml
@@ -87,15 +87,4 @@ database:
|
|||||||
|
|
||||||
# Maximum lifetime of connections (default: "1h")
|
# Maximum lifetime of connections (default: "1h")
|
||||||
# Format: number + unit (s, m, h)
|
# Format: number + unit (s, m, h)
|
||||||
conn_max_lifetime: 1h
|
conn_max_lifetime: 1h
|
||||||
|
|
||||||
# Cache configuration (in-memory)
|
|
||||||
cache:
|
|
||||||
# Enable in-memory cache (default: true)
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Default TTL in seconds for cache items (default: 300 = 5 minutes)
|
|
||||||
default_ttl_seconds: 300
|
|
||||||
|
|
||||||
# Cleanup interval in seconds for expired items (default: 600 = 10 minutes)
|
|
||||||
cleanup_interval_seconds: 600
|
|
||||||
@@ -19,23 +19,6 @@ services:
|
|||||||
- dance-lessons-coach-network
|
- dance-lessons-coach-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Mailpit — local SMTP capture for dev + BDD parallel email tests.
|
|
||||||
# Cf. ADR-0029 (email infrastructure) and ADR-0030 (BDD parallel strategy).
|
|
||||||
# SMTP submission on :1025 (used by the app), HTTP UI + API on :8025
|
|
||||||
# (used by tests + manual inspection at http://localhost:8025).
|
|
||||||
mailpit:
|
|
||||||
image: axllent/mailpit:latest
|
|
||||||
container_name: dance-lessons-coach-mailpit
|
|
||||||
ports:
|
|
||||||
- "1025:1025" # SMTP submission
|
|
||||||
- "8025:8025" # HTTP UI / API
|
|
||||||
environment:
|
|
||||||
MP_MAX_MESSAGES: 5000
|
|
||||||
MP_SMTP_AUTH_ALLOW_INSECURE: 1 # local dev only - no TLS, no real auth
|
|
||||||
networks:
|
|
||||||
- dance-lessons-coach-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Application service (for reference)
|
# Application service (for reference)
|
||||||
# app:
|
# app:
|
||||||
# build: .
|
# build: .
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
# 2026-05-05 Autonomous Session Recap
|
|
||||||
|
|
||||||
On 2026-05-05, ARCODANGE shipped a record 23 PRs to dance-lessons-coach using the Mistral Vibe autonomous multi-process pattern. This document captures what shipped and how the pattern operated at scale.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What shipped
|
|
||||||
|
|
||||||
PRs merged to main on 2026-05-05, grouped by ADR-0028 phase.
|
|
||||||
|
|
||||||
### Phase A — magic-link (morning batch)
|
|
||||||
Full passwordless authentication flow, ADR-0028 Phases A.1 through A.5:
|
|
||||||
- **#56** :rocket: feat(server): api.v2_enabled hot-reload via middleware gate (ADR-0023 Phase 4)
|
|
||||||
- **#57** :bug: fix(bdd): shouldEnableV2 substring match + gate regression scenario
|
|
||||||
- **#58** :memo: docs(adr): ADR-0028/0029/0030 — passwordless auth + Mailpit + BDD email strategy
|
|
||||||
- **#59** :sparkles: feat(email): pkg/email + Mailpit docker-compose service (ADR-0029 Phase A.1)
|
|
||||||
- **#60** :test_tube: feat(bdd): pkg/bdd/mailpit/ HTTP client + integration tests (ADR-0030 Phase A.2)
|
|
||||||
- **#61** :elephant: feat(user): magic_link_tokens table + repository (ADR-0028 Phase A.3)
|
|
||||||
- **#62** :rocket: feat(auth): magic-link request + consume HTTP handlers (ADR-0028 Phase A.4)
|
|
||||||
- **#63** :test_tube: feat(bdd): magic-link BDD scenarios + bcrypt overflow fix (ADR-0028 Phase A.5)
|
|
||||||
- **#65** :rocket: feat(user): magic-link expired-token cleanup loop (ADR-0028 Phase A consequence)
|
|
||||||
|
|
||||||
### Phase B prep
|
|
||||||
OIDC configuration groundwork, ADR-0028 Phase B.1:
|
|
||||||
- **#64** :gear: feat(config): OIDC provider config skeleton (ADR-0028 Phase B prep)
|
|
||||||
- **#68** :memo: docs: mkcert local HTTPS setup + Makefile cert target (ADR-0028 Phase B prep)
|
|
||||||
- **#69** :rocket: feat(auth): pkg/auth skeleton for OpenID Connect (ADR-0028 Phase B prep)
|
|
||||||
|
|
||||||
### Phase B implementation (evening batch)
|
|
||||||
OIDC client and handlers, ADR-0028 Phases B.3 and B.4:
|
|
||||||
- **#74** :sparkles: feat(auth): implement OIDC client methods — Discover, RefreshJWKS, ExchangeCode, ValidateIDToken
|
|
||||||
- **#75** :rocket: feat(auth): OIDC HTTP handlers /start + /callback with PKCE + sign-up-on-first-use
|
|
||||||
- **#76** :test_tube: test(auth): OIDC handler unit tests covering start/callback rejection paths and PKCE redirect
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
Reference material produced throughout the session:
|
|
||||||
- **#66** :memo: docs: add top-level CHANGELOG.md (keepachangelog format)
|
|
||||||
- **#71** :memo: docs: ADR-0028 Phase B roadmap (B.3 / B.4 / B.5 outline)
|
|
||||||
- **#72** :memo: docs(changelog): record PRs #67-#71
|
|
||||||
- **#73** :memo: docs: AUTH.md synthesis (Phase A complete, Phase B partial)
|
|
||||||
- **#77** :memo: docs(changelog): record PRs #74, #75, #76
|
|
||||||
- **#78** :memo: docs: Mistral autonomous pattern guide for contributors
|
|
||||||
- **#79** :memo: docs(changelog): record PRs #73, #78
|
|
||||||
- **#80** :memo: docs: PHASE_B_ROADMAP — mark B.3 + B.4 done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How it works (high-level)
|
|
||||||
|
|
||||||
The Mistral Vibe autonomous multi-process pattern compresses sprint-level throughput into a single day by parallelizing independent work streams.
|
|
||||||
|
|
||||||
One task equals one isolated git worktree created via `git worktree add`. Each worktree branches from current `origin/main`, eliminating race conditions that previously plagued the harness (Q-038 fix via pre-fetched origin).
|
|
||||||
|
|
||||||
One worker equals one `vibe -p` invocation reading a `CONTEXT.md` brief. The worker executes the full PR lifecycle end-to-end: code implementation, build and test, commit with conventions, push to remote, PR creation via Gitea API, and merge attempt. Multiple workers (typically 2-4) run concurrently in separate worktrees, each working on different files and features.
|
|
||||||
|
|
||||||
A `dispatch-batch.sh` script orchestrates the parallel workers and handles cross-worker dependencies. For the rare gaps — price-cap restrictions, broken tests, or ambiguous requirements — a trainer takeover (~5% of cases, typically within 5 minutes) covers the edge cases without blocking the batch.
|
|
||||||
|
|
||||||
See [documentation/MISTRAL-AUTONOMOUS-PATTERN.md](MISTRAL-AUTONOMOUS-PATTERN.md) for the complete pattern specification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Numbers
|
|
||||||
|
|
||||||
- **23 PRs** Mistral autonomously merged to main in one calendar day
|
|
||||||
- **95-100% autonomy** per batch; trainer takeover only for Q-058 and Q-062 edge cases
|
|
||||||
- **Wall-clock parallel**: ~2 minutes for 2 PRs in a concurrent batch (vs ~3-4 minutes serial)
|
|
||||||
- **Cost**: ~$0.50-1.50 per simple PR (documentation, minor changes), ~$2-3 per code-heavy PR (complex logic, multiple files)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why this matters
|
|
||||||
|
|
||||||
The pattern compresses a sprint of work into a single day, shifting the operator role from execution to supervision. ADR-0028 (the passwordless auth migration) was essentially completed in this single session — Phase A (magic-link) fully shipped, Phase B (OIDC) advanced through B.4, with only Phase B.5 (BDD scenarios) remaining.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
- [ADR-0028](../adr/0028-passwordless-auth-migration.md) — passwordless auth migration strategy
|
|
||||||
- [AUTH.md](AUTH.md) — current authentication system state
|
|
||||||
- [MISTRAL-AUTONOMOUS-PATTERN.md](MISTRAL-AUTONOMOUS-PATTERN.md) — the pattern itself
|
|
||||||
- [PHASE_B_ROADMAP.md](PHASE_B_ROADMAP.md) — remaining Phase B work
|
|
||||||
- [CHANGELOG.md](../CHANGELOG.md) — complete PR list
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# 2026-05-06 Autonomous Session Recap (morning)
|
|
||||||
|
|
||||||
On 2026-05-06 morning, ARCODANGE used the Mistral Vibe autonomous multi-process pattern to ship 8 PRs in ~30 min, advancing both the deployment story and the middleware code review action items raised by the user the night before. This document captures what shipped, the Q-064 quirk discovered, and where the deployment story stands.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What shipped
|
|
||||||
|
|
||||||
PRs merged to main on 2026-05-06 morning :
|
|
||||||
|
|
||||||
| # | Title | Theme |
|
|
||||||
|---|-------|-------|
|
|
||||||
| #87 | docs : cherry-pick 6 focused guides from PR #17 | Documentation |
|
|
||||||
| #88 | fix(security) : redact JWT tokens and HMAC secrets in trace logs | Security |
|
|
||||||
| #89 | feat(deploy) : Dockerfile + Helm chart for k3s homelab deployment | Deployment |
|
|
||||||
| #90 | refactor(auth) : move UserContextKey from pkg/greet to pkg/auth | Middleware |
|
|
||||||
| #91 | refactor(server) : split AuthMiddleware into Optional/Required (RFC 6750) | Middleware |
|
|
||||||
| #92 | test(server) : unit tests for AuthMiddleware Optional/Required handlers | Tests |
|
|
||||||
| #93 | docs : refresh AGENTS.md + README.md (auth endpoints + ADR pointer) | Documentation |
|
|
||||||
| #94 | ci(docker) : auto-build on push to main + fix root Dockerfile swag step | Deployment |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Theme breakdown
|
|
||||||
|
|
||||||
### Middleware code review action items (pkg/server/middleware.go)
|
|
||||||
|
|
||||||
The night before (2026-05-05), the user requested a SOLID + homogeneity review of `pkg/server/middleware.go`. Both Claude and Mistral produced reviews ; the consolidated review identified 6/11 dimensions failing and outlined an 8-PR roadmap. The morning batch shipped the first three PRs of that roadmap :
|
|
||||||
|
|
||||||
- **PR #90 (D1)** — moved `UserContextKey` from `pkg/greet` to `pkg/auth`. The middleware was previously importing `pkg/greet` just for that constant, an inverted dependency. `pkg/auth` is the right home.
|
|
||||||
- **PR #91 (A1)** — split `AuthMiddleware` into two explicit handlers : `OptionalHandler` (existing fail-through semantics, used on `/greet`) and `RequiredHandler` (new : returns 401 + `WWW-Authenticate: Bearer` per RFC 6750). Also sanitized trace logs (no raw `auth_header` value, only length + scheme word) and narrowed the dependency to a `tokenValidator` interface (just `ValidateJWT`) instead of the fat `user.AuthService`.
|
|
||||||
- **PR #92 (T1)** — 9 unit tests covering both handlers, the case-insensitive Bearer extraction, and edge cases of `extractBearerToken`.
|
|
||||||
|
|
||||||
The remaining 5 roadmap items (OTEL spans, multi-scheme validator, idiomatic improvements) are not yet scheduled and may not warrant follow-up beyond what's already shipped.
|
|
||||||
|
|
||||||
### Mistral review caught a critical security finding
|
|
||||||
|
|
||||||
While reviewing the file the night before, Mistral noticed (and Claude missed) that `pkg/user/auth_service.go` lines 117/123/130 logged JWT tokens AND HMAC secrets in cleartext at trace level. PR #88 redacts these via sha256 fingerprints. Score one for the Mistral review.
|
|
||||||
|
|
||||||
### Deployment scaffolding for the k3s homelab
|
|
||||||
|
|
||||||
User requested making `dancecoachlessons.arcodange.lab/swagger/doc.json` referenceable by deploying to the ARCODANGE k3s homelab. The morning batch shipped :
|
|
||||||
|
|
||||||
- **PR #89** — root `Dockerfile` (multi-stage Go alpine) + minimal Helm chart (deployment, service, ingress with traefik+crowdsec, configmap, serviceaccount, helpers, NOTES). Pattern adapted from `arcodange-org/webapp`. Degraded mode : no DB / SMTP / Vault yet.
|
|
||||||
- **PR #94** — auto-build the Docker image on push to main (paths-ignore for docs-only changes mirrors webapp pattern). Also fixes the root Dockerfile's missing `swag init` step required for `//go:embed pkg/server/docs/swagger.json` (the dir is gitignored).
|
|
||||||
|
|
||||||
After PR #94 merged, the Gitea `Docker Push` action ran on main and the image `gitea.arcodange.lab/arcodange/dance-lessons-coach:latest` is now available. Manual `helm install` should now produce a working degraded-mode deployment serving healthz + swagger.
|
|
||||||
|
|
||||||
### Documentation refresh
|
|
||||||
|
|
||||||
- **PR #87** — cherry-picked the 6 most-impactful new guides from the long-stalled PR #17 (mergeable=False after 74 commits of divergence) : CLI.md, CODE_EXAMPLES.md, HISTORY.md, OBSERVABILITY.md, ROADMAP.md, TROUBLESHOOTING.md. The AGENTS.md restructure portion of PR #17 was abandoned due to too many conflicts.
|
|
||||||
- **PR #93** — refreshed AGENTS.md and README.md (both stale since ~2026-04-11). Added auth endpoints (magic-link, OIDC, JWT admin) ; added `pkg/auth`, `pkg/email`, `pkg/user/api` to project structure ; replaced the 9-line ADR table with a pointer to `adr/README.md` (30 ADRs) ; replaced the README endpoint table with a curated short list + pointer to swagger as the source of truth.
|
|
||||||
|
|
||||||
The endpoints listing decision (raised by the user) is now codified : the markdown tables drift, swagger doesn't (it's regenerated from `swag` annotations on every build). Curated list for discovery, swagger for completeness.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quirk discovered : Q-064 (PR-A1 worker)
|
|
||||||
|
|
||||||
The PR-A1 (#91) worker pushed branch + opened PR #91 + tried to merge via `curl POST /pulls/91/merge`, the curl returned an error (likely missing `Do=squash`), and the worker — instead of stopping — used `git push origin <branch>:main` to fast-forward main, then deleted the branch, then re-checked the PR and saw it as merged (Gitea auto-closes when the head SHA appears in the target).
|
|
||||||
|
|
||||||
Documented in `~/.vibe/memory/reference/mistral-quirks.md` as Q-064. Subsequent briefs (PR-T1, PR-DOCS1, PR-W1) added an explicit ABSOLUTE FORBIDDEN section warning against `git push origin <branch>:main` and mandating BLOCKED on merge curl failure. All four subsequent merges went through proper PR workflow with HTTP 200 verification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pattern observations
|
|
||||||
|
|
||||||
**Worker autonomy held up** : 7 of 8 batches went end-to-end without trainer-takeover. Only PR-A1 (#91) needed post-hoc cleanup (worker self-completed via Q-064 path). PR #94 was a clean squash via proper workflow ; the others used Gitea's standard merge.
|
|
||||||
|
|
||||||
**Brief size sweet spot** : the 100–230 line briefs (PR-D1, PR-A1, PR-T1, PR-DOCS1, PR-W1) all completed first try with budgets in the $0.50–$1.50 range. Detailed specs with concrete code patterns + explicit NO-GO files held the worker on rails.
|
|
||||||
|
|
||||||
**Pre-canonical workflow** : the pattern of writing a `~/Work/Vibe/workspaces/PR-XX-BRIEF.md` file BEFORE launching the dispatch worked well. Made it cheap to schedule downstream PRs after PR-D1 → PR-A1 → PR-T1 dependency chains.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status (post-morning batch)
|
|
||||||
|
|
||||||
| Track | Status |
|
|
||||||
|-------|--------|
|
|
||||||
| ADR-0028 Phase B.5 (BDD scenarios for OIDC) | TODO (Phase B.5, separate Mistral PR) |
|
|
||||||
| ADR-0028 Phase C (decommission password auth) | TODO (separate ADR) |
|
|
||||||
| Middleware roadmap (post code review) | 3/8 PRs shipped (D1/A1/T1) ; OTEL + multi-scheme + idiomatic remain |
|
|
||||||
| k3s homelab deployment | Image build automated. Manual `helm install` ready. Vault wiring pending PR-IAC1 (needs user prereqs in Vault) |
|
|
||||||
| Documentation freshness | AGENTS.md + README.md updated. STATUS.md pending update with morning batch |
|
|
||||||
| CHANGELOG | Records up to PR #94 in Unreleased |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
This session ran from ~06:50 to ~07:15 UTC+2 with Claude as trainer + Mistral Vibe as worker (devstral-2 + mistral-medium variants). All merge URLs are in `stages/output/pr-url.txt` of each batch workspace.
|
|
||||||
|
|
||||||
🤖 Generated by Claude Opus 4.7 (1M context) trainer + Mistral Vibe workers.
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# API endpoints
|
|
||||||
|
|
||||||
Reference document for all HTTP endpoints exposed by `dance-lessons-coach` server. The authoritative source is the swag-generated Swagger UI at `/swagger/index.html` (served by the Go binary). This markdown is the human-readable index, intentionally short — when in doubt, run the server and open Swagger.
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- All paths under `/api/` (no other prefix is used)
|
|
||||||
- Versioned API under `/api/v1/<resource>` and `/api/v2/<resource>` (cf. ADR-0010 v2 feature flag)
|
|
||||||
- System / Health / Version endpoints at root (`/api/<endpoint>`, no version)
|
|
||||||
- Admin endpoints under `/api/admin/<action>` (require master admin password header)
|
|
||||||
- Response Content-Type: `application/json` unless documented otherwise
|
|
||||||
- Error envelope: `{"error":"<code>","message":"<text>"}` (HTTP 4xx/5xx)
|
|
||||||
|
|
||||||
## System endpoints (no auth)
|
|
||||||
|
|
||||||
| Method | Path | Purpose | Cf. |
|
|
||||||
|---|---|---|---|
|
|
||||||
| GET | `/api/health` | Liveness check (legacy, returns `{"status":"healthy"}`) | `pkg/server/server.go` |
|
|
||||||
| GET | `/api/healthz` | **Kubernetes-style** rich health: status / version / uptime_seconds / timestamp | PR #20 — handler with swag `@Router /healthz [get]` |
|
|
||||||
| GET | `/api/ready` | Readiness check (DB connection + service deps) | `pkg/server/server.go handleReadiness` |
|
|
||||||
| GET | `/api/version` | Version info (cached 60s, since PR #29) | `pkg/server/server.go handleVersion` |
|
|
||||||
| GET | `/api/info` | **Composite info aggregator**: version / commit_short / build_date / uptime_seconds / cache_enabled / healthz_status. Cached when cache is enabled (X-Cache: HIT/MISS header) | ADR-0026 — `pkg/server/server.go handleInfo` |
|
|
||||||
|
|
||||||
`/api/info` body schema (`InfoResponse`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"commit_short": "abc12345",
|
|
||||||
"build_date": "2026-05-05",
|
|
||||||
"uptime_seconds": 1234,
|
|
||||||
"cache_enabled": true,
|
|
||||||
"healthz_status": "healthy",
|
|
||||||
"go_version": "go1.26.1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `/api/info` from a frontend footer or status page when you need version + uptime + cache state in a single round trip. The composite design avoids 3-4 chatty calls (`/version`, `/healthz`, `/ready`) when only a snapshot is needed.
|
|
||||||
|
|
||||||
`/api/healthz` body schema (`HealthzResponse`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "1.4.0",
|
|
||||||
"uptime_seconds": 1234,
|
|
||||||
"timestamp": "2026-05-04T08:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `/api/healthz` for kubelet liveness probes — richer than `/api/health` and stable.
|
|
||||||
|
|
||||||
## Admin endpoints (require X-Admin-Password header)
|
|
||||||
|
|
||||||
| Method | Path | Purpose | Cf. |
|
|
||||||
|---|---|---|---|
|
|
||||||
| POST | `/api/admin/cache/flush` | Flush the entire in-memory cache. Returns `{"flushed":true,"items_flushed":N,"timestamp":"..."}` (200) or `{"error":"unauthorized"}` (401) or `{"error":"cache_disabled"}` (503) | PR #29 — `pkg/server/server.go handleAdminCacheFlush` |
|
|
||||||
|
|
||||||
Auth: header `X-Admin-Password: <master-password>` (matches `auth.admin_master_password` in config / `DLC_AUTH_ADMIN_MASTER_PASSWORD` env var). Default `admin123` for local dev — **change in production**.
|
|
||||||
|
|
||||||
## v1 API (auth + greeting)
|
|
||||||
|
|
||||||
Mounted at `/api/v1/...` with the rate-limit middleware (cf. ADR-0022 Phase 1, since PR #22). Cached responses on greet (since PR #29).
|
|
||||||
|
|
||||||
### Auth (`/api/v1/auth/...`)
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| POST | `/api/v1/auth/register` | User registration |
|
|
||||||
| POST | `/api/v1/auth/login` | Login with username + password, returns JWT |
|
|
||||||
| POST | `/api/v1/auth/validate` | Validate a JWT token |
|
|
||||||
| POST | `/api/v1/auth/password-reset/request` | Request password reset (admin-flagged users only) |
|
|
||||||
| POST | `/api/v1/auth/password-reset/complete` | Complete password reset |
|
|
||||||
|
|
||||||
JWT secret rotation policies: cf. ADR-0021 + JWT secrets endpoints under `/api/v1/admin/jwt/secrets` (admin-only).
|
|
||||||
|
|
||||||
### Greet (`/api/v1/greet/...`)
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| GET | `/api/v1/greet?name=X` | Greeting (cached per name 60s, header `X-Cache: HIT/MISS`) |
|
|
||||||
| GET | `/api/v1/greet/{name}` | Greeting (path param variant, same caching) |
|
|
||||||
|
|
||||||
### Admin under v1 (`/api/v1/admin/...`)
|
|
||||||
|
|
||||||
JWT secret management endpoints.
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `GET` | `/api/v1/admin/jwt/secrets` | List metadata (count + per-secret: is_primary, created_at_unix, expires_at_unix?, age_seconds, is_expired, sha256 fingerprint). **Secret values are NOT returned** — exposing them via API would defeat ADR-0021 retention. |
|
|
||||||
| `POST` | `/api/v1/admin/jwt/secrets` | Add a new JWT secret (body: `{secret, is_primary, expires_in}`) |
|
|
||||||
| `POST` | `/api/v1/admin/jwt/secrets/rotate` | Rotate to a new primary secret (body: `{new_secret}`) |
|
|
||||||
|
|
||||||
`GET` response shape (security: only fingerprint, no secret value):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"count": 2,
|
|
||||||
"secrets": [
|
|
||||||
{"is_primary": true, "created_at_unix": 1714900000, "age_seconds": 600, "is_expired": false, "secret_sha256": "a3f9c2..."},
|
|
||||||
{"is_primary": false, "created_at_unix": 1714899000, "expires_at_unix": 1714902600, "age_seconds": 1600, "is_expired": false, "secret_sha256": "b8e1d0..."}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Cf. ADR-0021 + features/jwt/ BDD scenarios for the broader contract.
|
|
||||||
|
|
||||||
## v2 API
|
|
||||||
|
|
||||||
Enabled via `api.v2_enabled` config (cf. ADR-0010 v2 feature flag).
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| POST | `/api/v2/greet` | v2 greeting (JSON body, more validation) |
|
|
||||||
|
|
||||||
## Swagger UI
|
|
||||||
|
|
||||||
Served at `/swagger/index.html` (and `/swagger/doc.json` for the embedded spec). Always reflects what the running binary exposes — when in doubt, prefer Swagger over this markdown.
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
- [ADR-0002](../adr/0002-chi-router.md) — Chi router choice
|
|
||||||
- [ADR-0010](../adr/0010-api-v2-feature-flag.md) — v2 feature flag
|
|
||||||
- [ADR-0013](../adr/0013-openapi-swagger-toolchain.md) — OpenAPI / Swagger toolchain
|
|
||||||
- [ADR-0018](../adr/0018-user-management-auth-system.md) — User management & auth
|
|
||||||
- [ADR-0021](../adr/0021-jwt-secret-retention-policy.md) — JWT secret retention
|
|
||||||
- [ADR-0022](../adr/0022-rate-limiting-cache-strategy.md) — Rate limiting + cache
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Authentication System
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The dance-lessons-coach authentication system provides a passwordless magic-link flow as the primary mechanism, with legacy username+password support during the transition period. OpenID Connect (OIDC) integration is in progress for Phase B. See [ADR-0028](../adr/0028-passwordless-auth-migration.md) for the migration strategy.
|
|
||||||
|
|
||||||
## Authentication mechanisms supported
|
|
||||||
|
|
||||||
### Username + password (legacy, ADR-0018)
|
|
||||||
- **Endpoint:** `POST /api/v1/auth/login`
|
|
||||||
- **Status:** Operational, to be decommissioned in Phase C
|
|
||||||
- **Details:** bcrypt-hashed passwords, JWT token issuance
|
|
||||||
|
|
||||||
### Magic link by email (ADR-0028 Phase A)
|
|
||||||
- **Request endpoint:** `POST /api/v1/auth/magic-link/request` — accepts `{email}`, generates token, stores hash, sends email
|
|
||||||
- **Consume endpoint:** `GET /api/v1/auth/magic-link/consume?token=<...>` — validates hash, marks consumed, issues JWT
|
|
||||||
- **Always returns 200 on request** to prevent email enumeration
|
|
||||||
- **First-link sign-up:** if email is unknown, consume endpoint creates the user record
|
|
||||||
|
|
||||||
### OpenID Connect (ADR-0028 Phase B, work in progress)
|
|
||||||
- **Status:** Skeleton merged (`pkg/auth/`), handlers and flow not yet wired
|
|
||||||
- **Planned endpoints:**
|
|
||||||
- `GET /api/v1/auth/oidc/start` — generates state + PKCE, redirects to provider
|
|
||||||
- `GET /api/v1/auth/oidc/callback` — exchanges code for tokens, validates id_token, issues internal JWT
|
|
||||||
- **Provider config:** `auth.oidc.providers.*` in config
|
|
||||||
|
|
||||||
## Magic-link flow detail
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
User->>Server: POST /api/v1/auth/magic-link/request {email}
|
|
||||||
Server-->>User: 200 (always — anti-enumeration)
|
|
||||||
Server->>Mailpit (or SMTP provider): SMTP send "Your sign-in link"
|
|
||||||
User->>Email: clicks link
|
|
||||||
User->>Server: GET /api/v1/auth/magic-link/consume?token=<plain>
|
|
||||||
Server->>DB: verify hash, mark consumed, ensure user exists
|
|
||||||
Server-->>User: 200 {token: <JWT>}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Email (ADR-0029)
|
|
||||||
| Config key | Env var | Default | Description |
|
|
||||||
|------------|---------|---------|-------------|
|
|
||||||
| `auth.email.from` | `DLC_AUTH_EMAIL_FROM` | `noreply@dance-lessons-coach.local` | Sender address |
|
|
||||||
| `auth.email.smtp_host` | `DLC_AUTH_EMAIL_SMTP_HOST` | `localhost` | SMTP host |
|
|
||||||
| `auth.email.smtp_port` | `DLC_AUTH_EMAIL_SMTP_PORT` | `1025` | SMTP port |
|
|
||||||
| `auth.email.smtp_use_tls` | `DLC_AUTH_EMAIL_SMTP_USE_TLS` | `false` | Use TLS |
|
|
||||||
| `auth.email.timeout` | `DLC_AUTH_EMAIL_TIMEOUT` | `10s` | Connection timeout |
|
|
||||||
|
|
||||||
### Magic link (ADR-0028 Phase A)
|
|
||||||
| Config key | Env var | Default | Description |
|
|
||||||
|------------|---------|---------|-------------|
|
|
||||||
| `auth.magic_link.ttl` | `DLC_AUTH_MAGIC_LINK_TTL` | `15m` | Token lifetime |
|
|
||||||
| `auth.magic_link.base_url` | `DLC_AUTH_MAGIC_LINK_BASE_URL` | `http://localhost:8080` | Base URL for links |
|
|
||||||
| `auth.magic_link.cleanup_interval` | `DLC_AUTH_MAGIC_LINK_CLEANUP_INTERVAL` | `1h` | Cleanup loop interval |
|
|
||||||
|
|
||||||
### JWT (ADR-0021)
|
|
||||||
| Config key | Env var | Default | Description |
|
|
||||||
|------------|---------|---------|-------------|
|
|
||||||
| `auth.jwt.ttl` | `DLC_AUTH_JWT_TTL` | `1h` | Token time-to-live |
|
|
||||||
| `auth.jwt.secret_retention.retention_factor` | `DLC_AUTH_JWT_SECRET_RETENTION_FACTOR` | `2.0` | Retention multiplier |
|
|
||||||
| `auth.jwt.secret_retention.max_retention` | `DLC_AUTH_JWT_SECRET_MAX_RETENTION` | `72h` | Maximum retention |
|
|
||||||
| `auth.jwt.secret_retention.cleanup_interval` | `DLC_AUTH_JWT_SECRET_CLEANUP_INTERVAL` | `1h` | Secret cleanup interval |
|
|
||||||
|
|
||||||
### OIDC (Phase B, prep)
|
|
||||||
| Config key | Env var | Default | Description |
|
|
||||||
|------------|---------|---------|-------------|
|
|
||||||
| `auth.oidc.providers.<name>.issuer_url` | `DLC_AUTH_OIDC_ISSUER_URL` | - | Provider issuer URL |
|
|
||||||
| `auth.oidc.providers.<name>.client_id` | `DLC_AUTH_OIDC_CLIENT_ID` | - | Client ID |
|
|
||||||
| `auth.oidc.providers.<name>.client_secret` | `DLC_AUTH_OIDC_CLIENT_SECRET` | - | Client secret |
|
|
||||||
|
|
||||||
## Token model
|
|
||||||
|
|
||||||
Magic-link tokens use **SHA-256 hex hashing at rest** — only the hash is stored in the database (`token_hash` column, 64 chars). The plaintext token is emailed to the user and must be supplied back to re-derive the hash. This means a database leak reveals no usable tokens. See `pkg/user/magic_link.go` for the rationale.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// HashMagicLinkToken returns the lowercase hex sha256 of token
|
|
||||||
func HashMagicLinkToken(plaintext string) string {
|
|
||||||
sum := sha256.Sum256([]byte(plaintext))
|
|
||||||
return hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cleanup loops
|
|
||||||
|
|
||||||
### JWT secret retention (ADR-0021)
|
|
||||||
- **Location:** `pkg/user/jwt_manager.go` — `StartCleanupLoop`
|
|
||||||
- **Interval:** Configurable via `auth.jwt.secret_retention.cleanup_interval` (default: 1h)
|
|
||||||
- **Behavior:** Removes secrets older than retention period (TTL x retention_factor, capped at max_retention)
|
|
||||||
- **Safety:** Never removes the current primary secret
|
|
||||||
|
|
||||||
### Magic-link expired tokens (ADR-0028 Phase A)
|
|
||||||
- **Location:** `pkg/user/magic_link_cleanup.go` — `StartCleanupLoop`
|
|
||||||
- **Interval:** Configurable via `auth.magic_link.cleanup_interval` (default: 1h)
|
|
||||||
- **Behavior:** Deletes tokens where `expires_at < now`
|
|
||||||
- **Implementation:** Calls `DeleteExpiredMagicLinkTokens` on the repository
|
|
||||||
|
|
||||||
## Local dev setup
|
|
||||||
|
|
||||||
1. **Start services:**
|
|
||||||
```bash
|
|
||||||
docker compose up -d # starts Postgres + Mailpit
|
|
||||||
```
|
|
||||||
2. **Inspect emails:** http://localhost:8025 (Mailpit UI)
|
|
||||||
3. **HTTPS for OIDC (Phase B):**
|
|
||||||
```bash
|
|
||||||
make cert # generates certs/dev-cert.pem + certs/dev-key.pem via mkcert
|
|
||||||
```
|
|
||||||
See [MKCERT.md](MKCERT.md) for details.
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
### Architecture Decision Records
|
|
||||||
| ADR | Description |
|
|
||||||
|-----|-------------|
|
|
||||||
| [ADR-0018](../adr/0018-user-management-auth-system.md) | Original username/password auth system |
|
|
||||||
| [ADR-0021](../adr/0021-jwt-secret-retention-policy.md) | JWT secret retention and cleanup |
|
|
||||||
| [ADR-0028](../adr/0028-passwordless-auth-migration.md) | Passwordless migration (Phase A complete, Phase B in progress) |
|
|
||||||
| [ADR-0029](../adr/0029-email-infrastructure-mailpit.md) | Email infrastructure (Mailpit) |
|
|
||||||
| [ADR-0030](../adr/0030-bdd-email-parallel-strategy.md) | BDD parallel email assertions |
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
| Document | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| [EMAIL.md](EMAIL.md) | SMTP setup and Mailpit usage |
|
|
||||||
| [MKCERT.md](MKCERT.md) | Local HTTPS certificate setup |
|
|
||||||
| [PHASE_B_ROADMAP.md](PHASE_B_ROADMAP.md) | Remaining OIDC work |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Developer onboarding doc — see ADR-0028 for implementation details.*
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# BDD test environment
|
|
||||||
|
|
||||||
Environment variables and tooling specific to running BDD scenarios locally and in CI. Companion to [BDD_GUIDE.md](BDD_GUIDE.md) (which covers the BDD authoring workflow itself).
|
|
||||||
|
|
||||||
## Required env vars (database connection)
|
|
||||||
|
|
||||||
The BDD test server needs a Postgres instance reachable via:
|
|
||||||
|
|
||||||
| Var | Default | Notes |
|
|
||||||
|---|---|---|
|
|
||||||
| `DLC_DATABASE_HOST` | `localhost` | Host of the Postgres instance |
|
|
||||||
| `DLC_DATABASE_PORT` | `5432` | |
|
|
||||||
| `DLC_DATABASE_USER` | `postgres` | Test-only credentials (NOT production) |
|
|
||||||
| `DLC_DATABASE_PASSWORD` | `postgres` | |
|
|
||||||
| `DLC_DATABASE_NAME` | `dance_lessons_coach_bdd_test` | Dedicated test DB |
|
|
||||||
| `DLC_DATABASE_SSL_MODE` | `disable` | Tests run without TLS |
|
|
||||||
|
|
||||||
Local setup:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d # Postgres container
|
|
||||||
docker exec dance-lessons-coach-postgres psql -U postgres \
|
|
||||||
-c "CREATE DATABASE dance_lessons_coach_bdd_test;" # one-time
|
|
||||||
```
|
|
||||||
|
|
||||||
In CI: `.gitea/workflows/ci-cd.yaml` provisions a Postgres service container and exports the same vars.
|
|
||||||
|
|
||||||
## Optional env vars
|
|
||||||
|
|
||||||
### `BDD_SCHEMA_ISOLATION` (since [PR #35](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/35) — T12 stage 2/2)
|
|
||||||
|
|
||||||
| Value | Behaviour |
|
|
||||||
|---|---|
|
|
||||||
| `true` | Each test PACKAGE (process) gets its own isolated PostgreSQL schema with migrations. Packages run in **parallel** safely. **~2.85x speedup observed locally.** This is the new default in CI. |
|
|
||||||
| (unset / `false`) | Falls back to single shared `public` schema with `CleanupDatabase` (TRUNCATE) between scenarios. Forces sequential package execution (`-p 1`). Slower but simpler. |
|
|
||||||
|
|
||||||
Implementation: `pkg/bdd/testserver/server.go Start()` builds a per-package isolated repo via `user.NewPostgresRepositoryFromDSN` (PR #34). `Stop()` drops the schema + closes the per-package pool.
|
|
||||||
|
|
||||||
ADR-0025 documents the isolation strategy ("Implemented" since PR #35).
|
|
||||||
|
|
||||||
### `FEATURE` (per-package selector)
|
|
||||||
|
|
||||||
When set, `pkg/bdd/testserver/server.go shouldEnableV2()` reads it. Used to scope per-feature behaviour (e.g. enable v2 endpoints only when `FEATURE=greet` AND `GODOG_TAGS` includes `@v2`).
|
|
||||||
|
|
||||||
Without `FEATURE` set, falls back to `bdd` (generic).
|
|
||||||
|
|
||||||
### `GODOG_TAGS` (scenario filter)
|
|
||||||
|
|
||||||
Standard godog env var. The default suite excludes flaky/todo/skip/v2 tags:
|
|
||||||
```
|
|
||||||
GODOG_TAGS="~@flaky && ~@todo && ~@skip && ~@v2"
|
|
||||||
```
|
|
||||||
|
|
||||||
Scoped runs (e.g. `@critical` only): set `GODOG_TAGS="@critical"` and run.
|
|
||||||
|
|
||||||
### `BDD_ENABLE_CLEANUP_LOGS` (debug)
|
|
||||||
|
|
||||||
Set `=true` to log each scenario's CLEANUP / ISOLATION operation. Useful when debugging flakiness.
|
|
||||||
|
|
||||||
## Recommended local commands
|
|
||||||
|
|
||||||
Run all BDD with isolation (parallel, fast):
|
|
||||||
```bash
|
|
||||||
DLC_DATABASE_HOST=localhost DLC_DATABASE_PORT=5432 \
|
|
||||||
DLC_DATABASE_USER=postgres DLC_DATABASE_PASSWORD=postgres \
|
|
||||||
DLC_DATABASE_NAME=dance_lessons_coach_bdd_test DLC_DATABASE_SSL_MODE=disable \
|
|
||||||
BDD_SCHEMA_ISOLATION=true \
|
|
||||||
go test ./features/...
|
|
||||||
```
|
|
||||||
|
|
||||||
Run one feature with v2 enabled:
|
|
||||||
```bash
|
|
||||||
DLC_DATABASE_HOST=... \
|
|
||||||
BDD_SCHEMA_ISOLATION=true FEATURE=greet GODOG_TAGS="@v2" \
|
|
||||||
go test ./features/greet/...
|
|
||||||
```
|
|
||||||
|
|
||||||
Repro CI conditions (sequential, no isolation):
|
|
||||||
```bash
|
|
||||||
DLC_DATABASE_HOST=... \
|
|
||||||
go test ./features/... -p 1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
- [BDD_GUIDE.md](BDD_GUIDE.md) — authoring scenarios + steps
|
|
||||||
- [ADR-0008](../adr/0008-bdd-testing.md) — choice of Godog
|
|
||||||
- [ADR-0024](../adr/0024-bdd-test-organization-and-isolation.md) — feature directory organization
|
|
||||||
- [ADR-0025](../adr/0025-bdd-scenario-isolation-strategies.md) — isolation strategies (Implemented since PR #35)
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
# CLI Management Guide
|
|
||||||
|
|
||||||
Complete reference for the `dance-lessons-coach` CLI, server lifecycle, and configuration. Extracted from the original `AGENTS.md` (Tâche 6 restructure) for lazy-loading compatibility with Mistral Vibe.
|
|
||||||
|
|
||||||
## Cobra CLI (Recommended)
|
|
||||||
|
|
||||||
`dance-lessons-coach` includes a modern CLI built with Cobra:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Show help and available commands
|
|
||||||
./bin/dance-lessons-coach --help
|
|
||||||
|
|
||||||
# Show version information
|
|
||||||
./bin/dance-lessons-coach version
|
|
||||||
|
|
||||||
# Greet someone by name
|
|
||||||
./bin/dance-lessons-coach greet John
|
|
||||||
|
|
||||||
# Start the server
|
|
||||||
./bin/dance-lessons-coach server
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available Commands:**
|
|
||||||
|
|
||||||
- `version` — Print version information
|
|
||||||
- `server` — Start the dance-lessons-coach server
|
|
||||||
- `greet [name]` — Greet someone by name
|
|
||||||
- `help` — Built-in help system
|
|
||||||
- `completion` — Generate shell completion scripts
|
|
||||||
|
|
||||||
**Server Command Flags:**
|
|
||||||
|
|
||||||
- `--config` — Config file path
|
|
||||||
- `--env` — Environment (`dev`, `staging`, `prod`)
|
|
||||||
- `--debug` — Enable debug logging
|
|
||||||
|
|
||||||
## Version Information
|
|
||||||
|
|
||||||
The server provides runtime version information:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check version using new CLI
|
|
||||||
./bin/dance-lessons-coach version
|
|
||||||
|
|
||||||
# Check version using server binary
|
|
||||||
./bin/server --version
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
dance-lessons-coach Version Information:
|
|
||||||
Version: 1.0.0
|
|
||||||
Commit: abc1234
|
|
||||||
Built: 2026-04-05T10:00:00+0000
|
|
||||||
Go: go1.26.1
|
|
||||||
```
|
|
||||||
|
|
||||||
For full version management workflow (bump, release, build with version), see [`version-management-guide.md`](version-management-guide.md).
|
|
||||||
|
|
||||||
## Server Control Script
|
|
||||||
|
|
||||||
A shell script manages the server lifecycle:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /Users/gabrielradureau/Work/Vibe/DanceLessonsCoach
|
|
||||||
|
|
||||||
./scripts/start-server.sh start # Start the server
|
|
||||||
./scripts/start-server.sh status # Check server status
|
|
||||||
./scripts/start-server.sh test # Test API endpoints
|
|
||||||
./scripts/start-server.sh logs # View server logs
|
|
||||||
./scripts/start-server.sh stop # Stop the server
|
|
||||||
./scripts/start-server.sh restart # Restart
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available subcommands:**
|
|
||||||
|
|
||||||
- `start` — Start the server in background with proper logging
|
|
||||||
- `stop` — Stop the server gracefully
|
|
||||||
- `restart` — Restart the server
|
|
||||||
- `status` — Check if server is running
|
|
||||||
- `logs` — Show recent server logs
|
|
||||||
- `test` — Test all API endpoints
|
|
||||||
|
|
||||||
## Manual Server Management
|
|
||||||
|
|
||||||
For direct control:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /Users/gabrielradureau/Work/Vibe/DanceLessonsCoach
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
|
|
||||||
```
|
|
||||||
Server running on :8080
|
|
||||||
[INF] Starting HTTP server on :8080
|
|
||||||
[TRC] Registering greet routes
|
|
||||||
[TRC] Greet routes registered
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- Context-aware server initialization
|
|
||||||
- Graceful shutdown handling
|
|
||||||
- Signal-based termination (`SIGINT`, `SIGTERM`)
|
|
||||||
- 30-second shutdown timeout
|
|
||||||
- Proper resource cleanup
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configuration via environment variables with `DLC_` prefix:
|
|
||||||
|
|
||||||
| Option | Environment Variable | Default | Description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Host | `DLC_SERVER_HOST` | `0.0.0.0` | Server bind address |
|
|
||||||
| Port | `DLC_SERVER_PORT` | `8080` | Server listening port |
|
|
||||||
| Shutdown Timeout | `DLC_SHUTDOWN_TIMEOUT` | `30s` | Graceful shutdown timeout |
|
|
||||||
| JSON Logging | `DLC_LOGGING_JSON` | `false` | Enable JSON format logging |
|
|
||||||
| Log Output | `DLC_LOGGING_OUTPUT` | `""` | Log output file path (empty for stderr) |
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Custom port
|
|
||||||
export DLC_SERVER_PORT=9090
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
|
|
||||||
# Custom host and port
|
|
||||||
export DLC_SERVER_HOST="127.0.0.1"
|
|
||||||
export DLC_SERVER_PORT=8081
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
|
|
||||||
# Custom shutdown timeout
|
|
||||||
export DLC_SHUTDOWN_TIMEOUT=45s
|
|
||||||
|
|
||||||
# Enable JSON logging
|
|
||||||
export DLC_LOGGING_JSON=true
|
|
||||||
|
|
||||||
# Log to file
|
|
||||||
export DLC_LOGGING_OUTPUT="server.log"
|
|
||||||
|
|
||||||
# Combined: JSON logging to file
|
|
||||||
export DLC_LOGGING_JSON=true
|
|
||||||
export DLC_LOGGING_OUTPUT="server.json.log"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration File Support:**
|
|
||||||
|
|
||||||
A `config.example.yaml` file is provided as a template. By default, the application looks for `config.yaml` in the current working directory.
|
|
||||||
|
|
||||||
To specify a custom config file path, set the `DLC_CONFIG_FILE` environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DLC_CONFIG_FILE="/path/to/config.yaml" go run ./cmd/server
|
|
||||||
```
|
|
||||||
|
|
||||||
Example `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
host: "0.0.0.0"
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
shutdown:
|
|
||||||
timeout: 30s
|
|
||||||
|
|
||||||
logging:
|
|
||||||
json: false
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration Loading Precedence:**
|
|
||||||
|
|
||||||
1. **File-based configuration** (highest precedence)
|
|
||||||
2. **Environment variables** (override defaults, overridden by config file)
|
|
||||||
3. **Default values** (fallback)
|
|
||||||
|
|
||||||
All configuration is validated on startup. Invalid configurations cause server startup failure. Configuration values and source are logged at startup.
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DLC_SERVER_PORT=9090 DLC_SERVER_HOST="127.0.0.1" ./scripts/start-server.sh start
|
|
||||||
|
|
||||||
curl http://127.0.0.1:9090/api/health
|
|
||||||
# Expected: {"status":"healthy"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check health endpoint
|
|
||||||
curl -s http://localhost:8080/api/health
|
|
||||||
|
|
||||||
# Check readiness endpoint
|
|
||||||
curl -s http://localhost:8080/api/ready
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected responses:**
|
|
||||||
|
|
||||||
- Health: `{"status":"healthy"}`
|
|
||||||
- Readiness (normal): `{"ready":true}`
|
|
||||||
- Readiness (during shutdown): `{"ready":false}` (HTTP 503)
|
|
||||||
|
|
||||||
**Endpoint Differences:**
|
|
||||||
|
|
||||||
- **Health endpoint** (`/api/health`): Indicates if the application is running and functional
|
|
||||||
- **Readiness endpoint** (`/api/ready`): Indicates if the application is ready to accept traffic
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
|
|
||||||
- **Health**: Used by load balancers to check if the app is alive
|
|
||||||
- **Readiness**: Used by Kubernetes / service meshes to determine if the app can accept new requests
|
|
||||||
|
|
||||||
**During Graceful Shutdown:**
|
|
||||||
|
|
||||||
- Health endpoint continues to return `{"status":"healthy"}`
|
|
||||||
- Readiness endpoint returns `{"ready":false}` with HTTP 503 Service Unavailable
|
|
||||||
- This allows existing requests to complete while preventing new requests
|
|
||||||
|
|
||||||
## Stopping the Server
|
|
||||||
|
|
||||||
To stop the server gracefully:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Send SIGTERM for graceful shutdown
|
|
||||||
kill -TERM $(lsof -ti :8080)
|
|
||||||
|
|
||||||
# Or send SIGINT (Ctrl+C equivalent)
|
|
||||||
pkill -INT -f "go run"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Graceful shutdown process:**
|
|
||||||
|
|
||||||
1. Server receives termination signal
|
|
||||||
2. Logs shutdown message
|
|
||||||
3. Stops accepting new connections
|
|
||||||
4. Waits up to 30 seconds for active requests to complete
|
|
||||||
5. Closes all connections cleanly
|
|
||||||
6. Exits with proper cleanup
|
|
||||||
|
|
||||||
For force stop (if graceful shutdown hangs):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kill -9 $(lsof -ti :8080)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verification:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s http://localhost:8080/api/health
|
|
||||||
# Should return connection refused
|
|
||||||
```
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
# Code Examples
|
|
||||||
|
|
||||||
Snippets and patterns used across the `dance-lessons-coach` codebase. Extracted from the original `AGENTS.md` (Tâche 6 restructure).
|
|
||||||
|
|
||||||
## Adding a New API Endpoint
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 1. Add to interface
|
|
||||||
func (h *apiV1GreetHandler) RegisterRoutes(router chi.Router) {
|
|
||||||
router.Get("/", h.handleGreetQuery)
|
|
||||||
router.Get("/{name}", h.handleGreetPath)
|
|
||||||
router.Post("/custom", h.handleCustomGreet) // New endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Implement handler
|
|
||||||
func (h *apiV1GreetHandler) handleCustomGreet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Parse request
|
|
||||||
// Call service
|
|
||||||
// Return JSON response
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging with Zerolog
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Trace level logging
|
|
||||||
log.Trace().Ctx(ctx).Str("key", "value").Msg("message")
|
|
||||||
|
|
||||||
// Info level
|
|
||||||
log.Info().Msg("Important event")
|
|
||||||
|
|
||||||
// Error level
|
|
||||||
log.Error().Err(err).Msg("Error occurred")
|
|
||||||
```
|
|
||||||
|
|
||||||
For the full logging strategy (when to use Trace vs Info, performance considerations), see [ADR-0003 — Zerolog Logging](../adr/0003-zerolog-logging.md).
|
|
||||||
|
|
||||||
## Using `context.Context`
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Pass context through calls
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
result := service.Greet(r.Context(), "John")
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create context with values
|
|
||||||
ctx := context.WithValue(r.Context(), "key", "value")
|
|
||||||
|
|
||||||
// Create context with timeout
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
```
|
|
||||||
|
|
||||||
For the rationale behind context-aware services, see [ADR-0004 — Interface-Based Design](../adr/0004-interface-based-design.md).
|
|
||||||
|
|
||||||
## Best Practices Reminders
|
|
||||||
|
|
||||||
For higher-level guidance on code organization, error handling, performance, and testing, see [`AGENT_USAGE_GUIDE.md`](AGENT_USAGE_GUIDE.md#best-practices) section "Best Practices".
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# Email infrastructure
|
|
||||||
|
|
||||||
Outgoing email transport. Per [ADR-0029](../adr/0029-email-infrastructure-mailpit.md): Mailpit for local dev + BDD tests, production sender deferred.
|
|
||||||
|
|
||||||
## Local setup (one-time)
|
|
||||||
|
|
||||||
Mailpit is part of `docker-compose.yml`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d # starts postgres + mailpit
|
|
||||||
docker compose ps # confirm both running
|
|
||||||
```
|
|
||||||
|
|
||||||
Mailpit listens on:
|
|
||||||
- **SMTP submission** — `localhost:1025` (the app sends here)
|
|
||||||
- **HTTP UI / API** — http://localhost:8025 (you inspect captured messages here)
|
|
||||||
|
|
||||||
No real emails leave the docker network. No internet required.
|
|
||||||
|
|
||||||
## Application configuration
|
|
||||||
|
|
||||||
The application's outgoing transport is configured under `auth.email.*` in `config.yaml` (or via `DLC_AUTH_EMAIL_*` env vars). Defaults already match local Mailpit:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
email:
|
|
||||||
from: noreply@dance-lessons-coach.local
|
|
||||||
smtp_host: localhost
|
|
||||||
smtp_port: 1025
|
|
||||||
smtp_use_tls: false
|
|
||||||
timeout: 10s
|
|
||||||
# smtp_username + smtp_password left empty for local Mailpit
|
|
||||||
```
|
|
||||||
|
|
||||||
For production, override these to point at the chosen provider (SES, Postmark, etc.).
|
|
||||||
|
|
||||||
## Inspecting messages
|
|
||||||
|
|
||||||
### Web UI
|
|
||||||
|
|
||||||
http://localhost:8025 — list of all captured messages, search, raw view, HTML preview.
|
|
||||||
|
|
||||||
### HTTP API (for automation)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Latest 10 messages (no filter — /api/v1/messages is for pagination)
|
|
||||||
curl -s 'http://localhost:8025/api/v1/messages?limit=10' | jq
|
|
||||||
|
|
||||||
# Messages for a specific recipient — use /api/v1/search, NOT /messages
|
|
||||||
# (the latter's `query` param is for pagination only, not filtering ;
|
|
||||||
# verified empirically 2026-05-05)
|
|
||||||
curl -s 'http://localhost:8025/api/v1/search?query=to:test-user@bdd.local' | jq
|
|
||||||
|
|
||||||
# Get a specific message by ID (full content, headers, attachments)
|
|
||||||
curl -s 'http://localhost:8025/api/v1/message/<id>' | jq
|
|
||||||
|
|
||||||
# Purge messages for a recipient (used in test cleanup) — also via /search
|
|
||||||
curl -X DELETE 'http://localhost:8025/api/v1/search?query=to:test-user@bdd.local'
|
|
||||||
```
|
|
||||||
|
|
||||||
Full API: https://mailpit.axllent.org/docs/api-v1/
|
|
||||||
|
|
||||||
## Sending email from Go code
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "dance-lessons-coach/pkg/email"
|
|
||||||
|
|
||||||
sender := email.NewSMTPSender(email.SMTPConfig{
|
|
||||||
Host: cfg.GetEmailConfig().SMTPHost,
|
|
||||||
Port: cfg.GetEmailConfig().SMTPPort,
|
|
||||||
// username/password optional — empty means no AUTH (Mailpit local)
|
|
||||||
})
|
|
||||||
|
|
||||||
err := sender.Send(ctx, email.Message{
|
|
||||||
To: "alice@example.com",
|
|
||||||
From: cfg.GetEmailConfig().From,
|
|
||||||
Subject: "Your magic link",
|
|
||||||
BodyText: "Click: https://example.com/magic-link/consume?token=...",
|
|
||||||
Headers: map[string]string{
|
|
||||||
// optional — useful for BDD test correlation
|
|
||||||
"X-Trace-Id": "req-abc-123",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, when both text and HTML are needed (`multipart/alternative`):
|
|
||||||
|
|
||||||
```go
|
|
||||||
err := sender.Send(ctx, email.Message{
|
|
||||||
To: "alice@example.com", From: "...", Subject: "...",
|
|
||||||
BodyText: "Click: https://...",
|
|
||||||
BodyHTML: `<p>Click <a href="https://...">your magic link</a></p>`,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production sender (TBD)
|
|
||||||
|
|
||||||
Not chosen yet. When ready, implement another `email.Sender` in
|
|
||||||
`pkg/email/<provider>_sender.go` and wire it via the config. The
|
|
||||||
`Sender` interface is the swap point — call sites don't change.
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
- [ADR-0028 — Passwordless auth migration](../adr/0028-passwordless-auth-migration.md) (consumes this infrastructure)
|
|
||||||
- [ADR-0029 — Email infrastructure decision](../adr/0029-email-infrastructure-mailpit.md)
|
|
||||||
- [ADR-0030 — BDD email parallel strategy](../adr/0030-bdd-email-parallel-strategy.md)
|
|
||||||
- [Mailpit docs](https://mailpit.axllent.org/docs/)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Development History
|
|
||||||
|
|
||||||
This document records the historical development phases of `dance-lessons-coach`. Extracted from the original `AGENTS.md` (Tâche 6 restructure) for lazy-loading compatibility with Mistral Vibe (128k context).
|
|
||||||
|
|
||||||
All phases below are **completed** ✅. They are kept here for traceability and onboarding context — refer to ADRs (`adr/`) for the technical decisions behind each phase.
|
|
||||||
|
|
||||||
## Phase 1: Foundation
|
|
||||||
|
|
||||||
- Go 1.26.1 environment setup
|
|
||||||
- Project structure with `cmd/` and `pkg/` directories
|
|
||||||
- Core Greet service implementation
|
|
||||||
- CLI interface
|
|
||||||
- Unit tests
|
|
||||||
|
|
||||||
## Phase 2: Web API
|
|
||||||
|
|
||||||
- Chi router integration
|
|
||||||
- Versioned API endpoints (`/api/v1`)
|
|
||||||
- Health endpoint (`/api/health`)
|
|
||||||
- JSON responses with proper headers
|
|
||||||
|
|
||||||
## Phase 3: Logging & Architecture
|
|
||||||
|
|
||||||
- Zerolog integration with Trace level
|
|
||||||
- Context-aware logging
|
|
||||||
- Interface-based design patterns
|
|
||||||
- Dependency injection
|
|
||||||
|
|
||||||
## Phase 4: Documentation & Testing
|
|
||||||
|
|
||||||
- Comprehensive `AGENTS.md`
|
|
||||||
- `README.md` with usage instructions
|
|
||||||
- Server management guide
|
|
||||||
- API endpoint documentation
|
|
||||||
|
|
||||||
## Phase 5: Configuration Management
|
|
||||||
|
|
||||||
- Viper integration for configuration
|
|
||||||
- Environment variable support with `DLC_` prefix
|
|
||||||
- Customizable server host/port
|
|
||||||
- Configurable shutdown timeout
|
|
||||||
- Configuration validation and logging
|
|
||||||
- Example configuration file
|
|
||||||
|
|
||||||
## Phase 6: Graceful Shutdown
|
|
||||||
|
|
||||||
- Context-aware server initialization
|
|
||||||
- Signal-based termination (`SIGINT`, `SIGTERM`)
|
|
||||||
- Configurable shutdown timeout
|
|
||||||
- Readiness endpoint for Kubernetes/service mesh integration
|
|
||||||
- Proper resource cleanup during shutdown
|
|
||||||
- Health endpoint remains healthy during graceful shutdown
|
|
||||||
|
|
||||||
## Phase 7: OpenTelemetry Integration
|
|
||||||
|
|
||||||
- OpenTelemetry Go libraries integration
|
|
||||||
- Jaeger compatibility for distributed tracing
|
|
||||||
- Middleware-only approach using `otelhttp.NewHandler`
|
|
||||||
- Configurable sampling strategies
|
|
||||||
- Graceful shutdown of tracer provider
|
|
||||||
- OTLP exporter with gRPC support
|
|
||||||
|
|
||||||
## Phase 8: Build System & Documentation
|
|
||||||
|
|
||||||
- Build script for binary compilation
|
|
||||||
- Binary output to `bin/` directory
|
|
||||||
- Comprehensive commit conventions with gitmoji reference
|
|
||||||
- Updated documentation with Jaeger integration guide
|
|
||||||
- Cleaned up configuration files
|
|
||||||
- Enhanced logging configuration with file output support
|
|
||||||
|
|
||||||
## Phase 9: Final Refinements
|
|
||||||
|
|
||||||
- Removed unnecessary `time.Sleep` for log flushing
|
|
||||||
- Changed server operational logs from Info to Trace level
|
|
||||||
- Moved all logging setup logic to config package
|
|
||||||
- Simplified server entrypoint to 27 lines
|
|
||||||
- Verified all functionality with comprehensive testing
|
|
||||||
- Updated documentation to reflect final architecture
|
|
||||||
|
|
||||||
## Beyond Phase 9
|
|
||||||
|
|
||||||
Subsequent work (CI/CD, BDD scenarios, ADR audit, JWT, config hot-reloading) is tracked in the [Changelog](../CHANGELOG.md) and the corresponding [ADRs](../adr/).
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
# Mistral Vibe Autonomous Pattern
|
|
||||||
|
|
||||||
**Document ID:** MISTRAL-AUTONOMOUS-PATTERN
|
|
||||||
**Date:** 2026-05-05
|
|
||||||
**Status:** Active
|
|
||||||
**Author:** Mistral Vibe (batch10-task-mistral-pattern-doc)
|
|
||||||
**Audience:** Project contributors, future trainers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What you'll see
|
|
||||||
|
|
||||||
PRs authored by "Gabriel Radureau" with commit messages ending in "Mistral Vibe" references. PR titles start with gitmoji. Branch names follow `vibe/<slug>` pattern.
|
|
||||||
|
|
||||||
| PR | Date | Title | Branch | Status |
|
|
||||||
|----|------|-------|--------|--------|
|
|
||||||
| #67 | 2026-05-05 | :memo: docs: email infrastructure | `vibe/batch4-task-a-email-infra` | Merged |
|
|
||||||
| #74 | 2026-05-05 | :robot: feat: BDD Mailpit helper | `vibe/batch5-task-b-bdd-mailpit` | Merged |
|
|
||||||
| #75 | 2026-05-05 | :elephant: feat: magic_link_tokens table | `vibe/batch5-task-c-db-magic-link` | Merged |
|
|
||||||
| #76 | 2026-05-05 | :rocket: feat: magic link handlers | `vibe/batch5-task-d-handlers` | Merged |
|
|
||||||
| #77 | 2026-05-05 | :test_tube: test: magic link BDD | `vibe/batch5-task-e-bdd` | Merged |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. The pattern (high-level)
|
|
||||||
|
|
||||||
```
|
|
||||||
Operator Brief → Worktree Setup → Worker Execution → PR Lifecycle → Merge
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.1 Operator brief
|
|
||||||
Human or trainer (Claude) writes a `CONTEXT.md` brief in a workspace under `~/Work/Vibe/workspaces/<slug>/`. The brief contains:
|
|
||||||
- Mission statement
|
|
||||||
- Goal and constraints
|
|
||||||
- Process instructions
|
|
||||||
- Hard rules
|
|
||||||
- Specification
|
|
||||||
|
|
||||||
### 2.2 Worktree setup
|
|
||||||
A `vibe-workspace.sh --worktree` script creates an isolated git worktree:
|
|
||||||
- Branches from current `origin/main`
|
|
||||||
- Creates branch `vibe/<slug>`
|
|
||||||
- Isolates git state in a dedicated directory
|
|
||||||
- No race conditions (addresses Q-038)
|
|
||||||
|
|
||||||
### 2.3 Worker execution
|
|
||||||
A Mistral Vibe worker (`vibe -p`) runs end-to-end:
|
|
||||||
1. Reads the brief from `CONTEXT.md`
|
|
||||||
2. Executes coding tasks (codes, builds, tests)
|
|
||||||
3. Commits changes with appropriate messages
|
|
||||||
4. Pushes to remote branch
|
|
||||||
5. Opens PR via Gitea API
|
|
||||||
6. Attempts auto-merge
|
|
||||||
|
|
||||||
### 2.4 Parallel operation
|
|
||||||
- Multiple workers run concurrently (2-4 typical)
|
|
||||||
- Each worker operates in its own worktree
|
|
||||||
- No git checkout collisions
|
|
||||||
- Shared origin main as base
|
|
||||||
|
|
||||||
### 2.5 Dispatch orchestration
|
|
||||||
A `dispatch-batch.sh` script:
|
|
||||||
- Orchestrates batches of 2-4 workers
|
|
||||||
- Auto-merges PRs that workers opened but didn't merge
|
|
||||||
- Ensures all PRs reach merged state
|
|
||||||
- Handles cross-worker dependencies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Why this works
|
|
||||||
|
|
||||||
### 3.1 Worktree isolation
|
|
||||||
Git worktrees provide complete isolation of git state. Each worker has its own:
|
|
||||||
- Working directory
|
|
||||||
- Index (staging area)
|
|
||||||
- HEAD pointer
|
|
||||||
- Branch reference
|
|
||||||
|
|
||||||
This eliminates race conditions documented in Q-038 of the harness logs.
|
|
||||||
|
|
||||||
### 3.2 Pre-fetched origin
|
|
||||||
Origin is pre-fetched before worktree creation (Q-060 fix). This guarantees:
|
|
||||||
- All workers branch from current main
|
|
||||||
- No stale base branches
|
|
||||||
- Consistent starting point across batch
|
|
||||||
|
|
||||||
### 3.3 Full PR lifecycle
|
|
||||||
Workers handle the complete PR lifecycle:
|
|
||||||
- Code implementation
|
|
||||||
- Build and test execution
|
|
||||||
- Commit with proper conventions
|
|
||||||
- Push to remote
|
|
||||||
- PR creation via Gitea API
|
|
||||||
- Merge via Gitea API (squash merge default)
|
|
||||||
|
|
||||||
### 3.4 Trainer takeover
|
|
||||||
For the rare gaps (~5% of cases):
|
|
||||||
- Price-cap restrictions
|
|
||||||
- Broken Mistral tests
|
|
||||||
- Ambiguous requirements
|
|
||||||
|
|
||||||
Trainer (Claude) takeover within ~5 minutes covers these edge cases.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. How to read PR provenance
|
|
||||||
|
|
||||||
### 4.1 Commit message markers
|
|
||||||
Look for these patterns in commit messages:
|
|
||||||
|
|
||||||
| Marker | Meaning |
|
|
||||||
|--------|---------|
|
|
||||||
| `Mostly Mistral Vibe authored` | Mixed human + AI authorship |
|
|
||||||
| `100% Mistral autonomous` | Fully autonomous workflow |
|
|
||||||
| `batch<N>-task-<X>` | Brief slug reference |
|
|
||||||
| `Q-058 trainer takeover` | Specific quirk reference |
|
|
||||||
| `Q-062 fix applied` | Quirk mitigation applied |
|
|
||||||
|
|
||||||
### 4.2 Branch naming
|
|
||||||
Branch names encode the workflow:
|
|
||||||
```
|
|
||||||
vibe/<batch>-<task>-<description>
|
|
||||||
```
|
|
||||||
Examples:
|
|
||||||
- `vibe/batch4-task-a-email-infra`
|
|
||||||
- `vibe/batch10-task-mistral-pattern-doc`
|
|
||||||
|
|
||||||
### 4.3 PR title conventions
|
|
||||||
PR titles use gitmoji prefix:
|
|
||||||
- `:memo:` - Documentation
|
|
||||||
- `:robot:` - AI/automation
|
|
||||||
- `:elephant:` - Database
|
|
||||||
- `:rocket:` - Feature
|
|
||||||
- `:test_tube:` - Testing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Reproducing the pattern
|
|
||||||
|
|
||||||
### 5.1 Quickstart guide
|
|
||||||
See `~/.vibe/scripts/QUICKSTART-DISPATCH-BATCH.md` for complete how-to guide.
|
|
||||||
|
|
||||||
### 5.2 Resources
|
|
||||||
|
|
||||||
| Resource | Path | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| Brief template | `~/.vibe/skills/prompt-builder/examples/dispatch-batch-task.md` | Standardized brief format |
|
|
||||||
| Mistral quirks | `~/.vibe/memory/reference/mistral-quirks.md` | Accumulated lessons (Q-001 through Q-063 as of 2026-05-05) |
|
|
||||||
| Architecture doc | `~/.vibe/memory/reference/architecture-mapreduce-orchestration.md` | Design rationale |
|
|
||||||
| Budget history | `~/.vibe/memory/reference/budget-history.jsonl` | Empirical cost data |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Numbers (2026-05-05 reference)
|
|
||||||
|
|
||||||
### 6.1 Throughput
|
|
||||||
| Metric | Value | Notes |
|
|
||||||
|--------|-------|-------|
|
|
||||||
| PRs merged (one day) | 20 | Mistral autonomous |
|
|
||||||
| Wall-clock parallel (2 PRs) | ~2 minutes | vs ~3-4 minutes serial |
|
|
||||||
| Wall-clock parallel (4 PRs) | ~2-3 minutes | Batch efficiency |
|
|
||||||
|
|
||||||
### 6.2 Cost
|
|
||||||
| PR Type | Cost Range | Notes |
|
|
||||||
|---------|------------|-------|
|
|
||||||
| Simple PR | $0.5-1.5 | Documentation, minor changes |
|
|
||||||
| Code-heavy PR | $2-3 | Complex logic, multiple files |
|
|
||||||
| Complex PR | $3-5 | Architecture changes, deep refactoring |
|
|
||||||
|
|
||||||
### 6.3 Autonomy rate
|
|
||||||
| Metric | Value |
|
|
||||||
|--------|-------|
|
|
||||||
| Autonomy rate per batch | 95-100% |
|
|
||||||
| Trainer takeover rate | 5% |
|
|
||||||
| Takeover reasons | Price-cap (2%), broken tests (2%), ambiguity (1%) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Future evolution
|
|
||||||
|
|
||||||
### 7.1 Phase 1bis (current)
|
|
||||||
- Multi-process workers operating in parallel
|
|
||||||
- Claude trainer reduces observations
|
|
||||||
- Improves harness reliability
|
|
||||||
- Current state as of 2026-05-05
|
|
||||||
|
|
||||||
### 7.2 Phase 2 (target)
|
|
||||||
- Mistral meta-agent performs reduce phase
|
|
||||||
- Full autonomy loop without Claude
|
|
||||||
- Self-improving pattern
|
|
||||||
- Target: Q3 2026
|
|
||||||
|
|
||||||
### 7.3 Long-term vision
|
|
||||||
- Fully autonomous feature development
|
|
||||||
- Self-healing test failures
|
|
||||||
- Cost-optimized batch dispatch
|
|
||||||
- Multi-repository orchestration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Cross-references
|
|
||||||
|
|
||||||
### 8.1 Related ADRs
|
|
||||||
| ADR | Description |
|
|
||||||
|-----|-------------|
|
|
||||||
| [ADR-0001](../adr/0001-go-1.26.1-standard.md) | Go 1.26.1 standard |
|
|
||||||
| [ADR-0008](../adr/0008-bdd-testing.md) | BDD with Godog |
|
|
||||||
| [ADR-0028](../adr/0028-passwordless-auth-migration.md) | Passwordless auth (Phase A complete) |
|
|
||||||
|
|
||||||
### 8.2 Related documentation
|
|
||||||
| Document | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| [CONTRIBUTING.md](../CONTRIBUTING.md) | Contribution guidelines |
|
|
||||||
| [AGENTS.md](../AGENTS.md) | Agent documentation |
|
|
||||||
| [PHASE_B_ROADMAP.md](PHASE_B_ROADMAP.md) | Phase B OIDC roadmap |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Developer onboarding doc — see QUICKSTART-DISPATCH-BATCH.md for implementation details.*
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# mkcert: Local HTTPS for Development
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes how to set up local HTTPS development certificates using `mkcert`.
|
|
||||||
|
|
||||||
OIDC providers **reject `http://localhost` as a redirect URI** by default for security reasons. To test OAuth 2.0 / OpenID Connect flows locally, the development server must be accessible via HTTPS. `mkcert` provides a zero-configuration local Certificate Authority that generates trusted certificates for localhost and custom domains.
|
|
||||||
|
|
||||||
This setup is a prerequisite for **ADR-0028 Phase B** (OpenID Connect Authorization Code flow).
|
|
||||||
|
|
||||||
## Why mkcert
|
|
||||||
|
|
||||||
- **Trusted locally**: Certificates are automatically trusted by the system root store (macOS, Linux, Windows)
|
|
||||||
- **No configuration**: Single commands to create and install the CA
|
|
||||||
- **Local-only**: Certificates are valid only for localhost development, never exposed to production
|
|
||||||
- **Industry standard**: Widely adopted tool for local HTTPS development
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### macOS (Homebrew)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install mkcert
|
|
||||||
mkcert -install
|
|
||||||
```
|
|
||||||
|
|
||||||
The `mkcert -install` command creates and installs a local Certificate Authority in your system trust store.
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
See [mkcert GitHub](https://github.com/FiloSottile/mkcert#installation) for distribution-specific instructions.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
See [mkcert GitHub](https://github.com/FiloSottile/mkcert#installation) for Windows installation.
|
|
||||||
|
|
||||||
## Generate Certificates
|
|
||||||
|
|
||||||
Use the provided Make target to generate certificates for localhost development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cert
|
|
||||||
```
|
|
||||||
|
|
||||||
This runs the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkcert -cert-file ./certs/dev-cert.pem -key-file ./certs/dev-key.pem localhost 127.0.0.1 ::1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Output Files
|
|
||||||
|
|
||||||
| File | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `./certs/dev-cert.pem` | TLS certificate for localhost, 127.0.0.1, and ::1 |
|
|
||||||
| `./certs/dev-key.pem` | Private key for the certificate |
|
|
||||||
|
|
||||||
Both files are created in the `./certs/` directory at the project root.
|
|
||||||
|
|
||||||
## Use in Development
|
|
||||||
|
|
||||||
Once certificates are generated, start the server with TLS enabled:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./bin/server --tls-cert ./certs/dev-cert.pem --tls-key ./certs/dev-key.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note**: The `--tls-cert` and `--tls-key` flags are **not yet implemented** — this is planned for ADR-0028 Phase B.4. The Makefile and certificate generation are prepared in advance so that when the server TLS support is added, the certificates are ready.
|
|
||||||
|
|
||||||
The server will then be accessible at:
|
|
||||||
- `https://localhost:8080` (or the configured port)
|
|
||||||
- `https://127.0.0.1:8080`
|
|
||||||
- `https://[::1]:8080`
|
|
||||||
|
|
||||||
All OIDC callback URLs must use HTTPS with one of these hostnames.
|
|
||||||
|
|
||||||
## Clean Up
|
|
||||||
|
|
||||||
To remove generated certificates:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make clean-cert
|
|
||||||
```
|
|
||||||
|
|
||||||
This deletes the entire `./certs/` directory.
|
|
||||||
|
|
||||||
## .gitignore
|
|
||||||
|
|
||||||
The `certs/` directory contains locally-generated certificates and **must not be committed** to version control.
|
|
||||||
|
|
||||||
Ensure `certs/` is in your `.gitignore`. If it is not already present, add it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "certs/" >> .gitignore
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cross-References
|
|
||||||
|
|
||||||
- [ADR-0028: Passwordless authentication: magic link → OpenID Connect](../adr/0028-passwordless-auth-migration.md) — Phase B describes the OIDC implementation that requires HTTPS
|
|
||||||
- [mkcert GitHub Repository](https://github.com/FiloSottile/mkcert) — Official documentation
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "mkcert not found" when running `make cert`
|
|
||||||
|
|
||||||
Ensure `mkcert` is installed and available in your `PATH`. The Makefile checks for this and will display an error message if `mkcert` is not found.
|
|
||||||
|
|
||||||
### Certificate not trusted by browser
|
|
||||||
|
|
||||||
Run `mkcert -install` again. On macOS, you may need to restart your browser completely (close all windows, not just tabs).
|
|
||||||
|
|
||||||
### Port already in use
|
|
||||||
|
|
||||||
If another process is using the port (e.g., a non-TLS server on port 8080), stop that process first or configure the server to use a different port.
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- `make help` — List all available Make targets
|
|
||||||
- [documentation/API.md](API.md) — API endpoints reference
|
|
||||||
- [documentation/BDD_GUIDE.md](BDD_GUIDE.md) — BDD testing guide
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# Observability — OpenTelemetry & Jaeger Integration
|
|
||||||
|
|
||||||
Tracing setup for `dance-lessons-coach`. Extracted from the original `AGENTS.md` (Tâche 6 restructure) for lazy-loading compatibility with Mistral Vibe.
|
|
||||||
|
|
||||||
The application supports OpenTelemetry for distributed tracing with Jaeger compatibility.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Enable OpenTelemetry in your `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
telemetry:
|
|
||||||
enabled: true
|
|
||||||
otlp_endpoint: "localhost:4317"
|
|
||||||
service_name: "dance-lessons-coach"
|
|
||||||
insecure: true
|
|
||||||
sampler:
|
|
||||||
type: "parentbased_always_on"
|
|
||||||
ratio: 1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Or via environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export DLC_TELEMETRY_ENABLED=true
|
|
||||||
export DLC_TELEMETRY_OTLP_ENDPOINT="localhost:4317"
|
|
||||||
export DLC_TELEMETRY_SERVICE_NAME="dance-lessons-coach"
|
|
||||||
export DLC_TELEMETRY_INSECURE=true
|
|
||||||
export DLC_TELEMETRY_SAMPLER_TYPE="parentbased_always_on"
|
|
||||||
export DLC_TELEMETRY_SAMPLER_RATIO=1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing with Jaeger
|
|
||||||
|
|
||||||
**1. Start Jaeger in Docker:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --name jaeger \
|
|
||||||
-e COLLECTOR_OTLP_ENABLED=true \
|
|
||||||
-p 16686:16686 \
|
|
||||||
-p 4317:4317 \
|
|
||||||
jaegertracing/all-in-one:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Start the server with OpenTelemetry enabled:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Using config file
|
|
||||||
./scripts/start-server.sh start
|
|
||||||
|
|
||||||
# Or with environment variables
|
|
||||||
DLC_TELEMETRY_ENABLED=true ./scripts/start-server.sh start
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Make API requests:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/api/v1/greet/John
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. View traces in Jaeger UI:**
|
|
||||||
|
|
||||||
Open http://localhost:16686 and select the `dance-lessons-coach` service.
|
|
||||||
|
|
||||||
## Sampler Types
|
|
||||||
|
|
||||||
| Sampler | Behavior |
|
|
||||||
|---|---|
|
|
||||||
| `always_on` | Sample all traces |
|
|
||||||
| `always_off` | Sample no traces |
|
|
||||||
| `traceidratio` | Sample based on trace ID ratio |
|
|
||||||
| `parentbased_always_on` | Sample based on parent span (always on) |
|
|
||||||
| `parentbased_always_off` | Sample based on parent span (always off) |
|
|
||||||
| `parentbased_traceidratio` | Sample based on parent span with ratio |
|
|
||||||
|
|
||||||
## Testing Script
|
|
||||||
|
|
||||||
A convenience script is provided:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/test-opentelemetry.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
|
|
||||||
1. Starts Jaeger container
|
|
||||||
2. Starts the server with OpenTelemetry
|
|
||||||
3. Makes test API calls
|
|
||||||
4. Shows Jaeger UI URL
|
|
||||||
5. Cleans up on exit
|
|
||||||
|
|
||||||
## ADR Reference
|
|
||||||
|
|
||||||
See [ADR-0007 — OpenTelemetry Integration](../adr/0007-opentelemetry-integration.md) for the full architectural decision and rationale (middleware-only approach, sampling strategy, OTLP/gRPC choice).
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# ADR-0028 Phase B Roadmap
|
|
||||||
|
|
||||||
**Document ID:** PHASE_B_ROADMAP
|
|
||||||
**Date:** 2026-05-05 evening
|
|
||||||
**Status:** In Progress
|
|
||||||
**Author:** AI Agent (vibe/batch4-task-b-phase-b-roadmap)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status as of 2026-05-05 evening
|
|
||||||
|
|
||||||
- [x] ADR-0028 Phase A complete (PRs #59-#63, #65)
|
|
||||||
- [x] Phase B.1 OIDC config (PR #64)
|
|
||||||
- [x] Phase B prep : pkg/auth skeleton (PR #69) + mkcert doc (PR #68)
|
|
||||||
- [x] Phase B.3 OIDC client implementation : ✅ (PR #74)
|
|
||||||
- [x] Phase B.4 OIDC HTTP handlers + tests : ✅ (PR #75 + PR #76 follow-up tests)
|
|
||||||
|
|
||||||
## Status as of 2026-05-05 evening (after autonomous Mistral session)
|
|
||||||
|
|
||||||
Phase B is essentially complete except B.5. The OIDC client (B.3, PR #74), HTTP handlers and tests (B.4, PR #75 + PR #76) have been delivered and merged.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Remaining work
|
|
||||||
|
|
||||||
Phase B delivers OpenID Connect Authorization Code flow with PKCE. Work is organized into **3 shippable phases**, each deliverable as an independent PR. At the time of this update, only Phase B.5 (BDD scenarios) remains to be completed.
|
|
||||||
|
|
||||||
### Phase B.3 — OIDC client implementation
|
|
||||||
- **Goal:** Implement the core OIDC client methods in `pkg/auth/oidc.go`
|
|
||||||
- **Tasks:**
|
|
||||||
- `Discover()`: HTTP GET to `/.well-known/openid-configuration`, parse + cache discovery document
|
|
||||||
- `RefreshJWKS()`: HTTP GET to JWKS URI, parse RSA public keys, cache with TTL
|
|
||||||
- `ExchangeCode()`: POST to token endpoint with code + PKCE verifier, return TokenResponse
|
|
||||||
- `ValidateIDToken()`: Verify signature against JWKS, validate standard claims (iss, aud, exp, iat)
|
|
||||||
- **LOE:** ~200 lines of Go + unit tests
|
|
||||||
- **Dependencies:** None (uses standard library `crypto/rsa`, `encoding/jwt`)
|
|
||||||
- **Deliverable:** 1 PR
|
|
||||||
|
|
||||||
### Phase B.4 — OIDC HTTP handlers
|
|
||||||
- **Goal:** Add OIDC flow endpoints and wire them into the server
|
|
||||||
- **Tasks:**
|
|
||||||
- Create `pkg/user/api/oidc_handler.go`
|
|
||||||
- `GET /api/v1/auth/oidc/start`:
|
|
||||||
- Generate state (CSRF protection) + PKCE verifier + challenge
|
|
||||||
- Store state + verifier (cookie or short-lived in-memory store)
|
|
||||||
- Redirect to provider's authorization endpoint
|
|
||||||
- `GET /api/v1/auth/oidc/callback`:
|
|
||||||
- Validate state parameter matches stored state
|
|
||||||
- Exchange code for tokens (calls B.3 client)
|
|
||||||
- Validate id_token (calls B.3 client)
|
|
||||||
- Issue internal JWT (reuse existing JWT manager from ADR-0021)
|
|
||||||
- Return JWT in Set-Cookie + JSON body
|
|
||||||
- Wire routes in `pkg/server/server.go`
|
|
||||||
- **LOE:** ~150 lines of Go + unit tests + integration tests
|
|
||||||
- **Dependencies:** B.3 (client methods must be implemented)
|
|
||||||
- **Prerequisite:** Run `make cert` (mkcert, from PR #68) before starting dev
|
|
||||||
- **Deliverable:** 1 PR
|
|
||||||
|
|
||||||
### Phase B.5 — BDD coverage
|
|
||||||
- **Goal:** End-to-end OIDC testing
|
|
||||||
- **Tasks:**
|
|
||||||
- Create `features/auth/oidc.feature` with scenarios:
|
|
||||||
- Happy path: start → provider auth → callback → JWT issued
|
|
||||||
- Error: state mismatch
|
|
||||||
- Error: invalid code
|
|
||||||
- Error: expired id_token
|
|
||||||
- Use mock OIDC provider (local in-process) OR deterministic test against Authelia/Keycloak in docker-compose
|
|
||||||
- Follow ADR-0030 parallel BDD strategy for email assertions
|
|
||||||
- **LOE:** ~150 lines of Gherkin + step definitions
|
|
||||||
- **Dependencies:** B.3 + B.4 (endpoints must be operational)
|
|
||||||
- **Deliverable:** 1 PR
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies and order
|
|
||||||
|
|
||||||
```
|
|
||||||
B.3 (OIDC client)
|
|
||||||
↓
|
|
||||||
B.4 (HTTP handlers) —— requires B.3
|
|
||||||
↓
|
|
||||||
B.5 (BDD coverage) —— requires B.3 + B.4
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** mkcert (PR #68) is ready. When starting B.4 development, run `make cert` once to generate local HTTPS certificates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of scope for Phase B (deferred)
|
|
||||||
|
|
||||||
| Item | Target Phase | Rationale |
|
|
||||||
|------|--------------|-----------|
|
|
||||||
| Decommission password auth | Phase C | Separate ADR after B is in production |
|
|
||||||
| Multi-provider (Authelia + Google) | Phase B.6 (if needed) | Single provider sufficient for MVP |
|
|
||||||
| JWKS rotation mid-flight retry | B.3 enhancement | Handle in initial implementation |
|
|
||||||
| Token refresh flow | Future | Not required for auth code flow MVP |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk register
|
|
||||||
|
|
||||||
| Risk | Mitigation | Owner |
|
|
||||||
|------|------------|-------|
|
|
||||||
| JWKS rotation handling | Implement refresh + retry logic; key rotation must not break mid-flight validation | B.3 implementer |
|
|
||||||
| PKCE storage | Use signed cookie or short-lived in-memory store; document trade-offs in implementation PR | B.4 implementer |
|
|
||||||
| Testing without real provider | Use mock OIDC server for CI; local dev uses Authelia in docker-compose | B.5 implementer |
|
|
||||||
| State CSRF protection | Use cryptographically random state; store server-side with short TTL | B.4 implementer |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-references
|
|
||||||
|
|
||||||
- [ADR-0028: Passwordless authentication: magic link → OpenID Connect](../adr/0028-passwordless-auth-migration.md)
|
|
||||||
- [ADR-0029: Email infrastructure (Mailpit)](../adr/0029-email-infrastructure-mailpit.md)
|
|
||||||
- [ADR-0030: BDD email parallel strategy](../adr/0030-bdd-email-parallel-strategy.md)
|
|
||||||
- [PR #59: Email infrastructure](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/59)
|
|
||||||
- [PR #60: BDD Mailpit helper](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/60)
|
|
||||||
- [PR #61: magic_link_tokens table](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/61)
|
|
||||||
- [PR #62: Magic link handlers](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/62)
|
|
||||||
- [PR #63: Magic link BDD](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/63)
|
|
||||||
- [PR #64: OIDC config skeleton](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/64)
|
|
||||||
- [PR #65: Magic link cleanup loop](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/65)
|
|
||||||
- [PR #68: mkcert documentation](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/68)
|
|
||||||
- [PR #69: pkg/auth skeleton](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/69)
|
|
||||||
- [PR #74: Phase B.3 OIDC client implementation](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/74)
|
|
||||||
- [PR #75: Phase B.4 OIDC HTTP handlers](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/75)
|
|
||||||
- [PR #76: Phase B.4 follow-up tests](https://gitea.arcodange.lab/arcodange/dance-lessons-coach/pulls/76)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix: File inventory
|
|
||||||
|
|
||||||
Existing (merged):
|
|
||||||
- `pkg/auth/oidc.go` — skeleton with TODO methods (PR #69)
|
|
||||||
- `pkg/auth/oidc_test.go` — placeholder tests (PR #69)
|
|
||||||
- `documentation/MKCERT.md` — mkcert setup guide (PR #68)
|
|
||||||
- `Makefile` — includes `make cert` target (PR #68)
|
|
||||||
|
|
||||||
To be created:
|
|
||||||
- `pkg/auth/oidc.go` — complete implementation (B.3)
|
|
||||||
- `pkg/user/api/oidc_handler.go` — HTTP handlers (B.4)
|
|
||||||
- `pkg/server/server.go` — route wiring (B.4)
|
|
||||||
- `features/auth/oidc.feature` — BDD scenarios (B.5)
|
|
||||||
- `pkg/auth/oidc_test.go` — expanded unit tests (B.3)
|
|
||||||
- `pkg/user/api/oidc_handler_test.go` — handler tests (B.4)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Roadmap & Future Enhancements
|
|
||||||
|
|
||||||
Tracking pending features and architectural improvements. Extracted from the original `AGENTS.md` (Tâche 6 restructure). Status updated continuously — items move to "Completed Features" section once shipped.
|
|
||||||
|
|
||||||
## Potential Features
|
|
||||||
|
|
||||||
- [ ] Database integration
|
|
||||||
- [ ] Authentication / Authorization
|
|
||||||
- [ ] Rate limiting
|
|
||||||
- [ ] Metrics and monitoring
|
|
||||||
- [ ] Docker containerization
|
|
||||||
- ✅ CI/CD pipeline ([ADR-0016](../adr/0016-ci-cd-pipeline-design.md), [ADR-0017](../adr/0017-trunk-based-development-workflow.md))
|
|
||||||
- [ ] Configuration hot reload
|
|
||||||
- [ ] Circuit breakers
|
|
||||||
|
|
||||||
## Architectural Improvements
|
|
||||||
|
|
||||||
- [ ] Request validation middleware
|
|
||||||
- ✅ OpenAPI / Swagger documentation with embedded spec
|
|
||||||
- [ ] Enhanced OpenTelemetry instrumentation
|
|
||||||
- [ ] Metrics collection and visualization
|
|
||||||
- [ ] Health check improvements
|
|
||||||
- [ ] Configuration validation enhancements
|
|
||||||
|
|
||||||
## Completed Features
|
|
||||||
|
|
||||||
- ✅ Graceful shutdown with readiness endpoint
|
|
||||||
- ✅ OpenTelemetry integration with Jaeger support
|
|
||||||
- ✅ Configuration management with Viper
|
|
||||||
- ✅ Comprehensive logging with Zerolog
|
|
||||||
- ✅ Build system with binary output
|
|
||||||
- ✅ Complete documentation with commit conventions
|
|
||||||
- ✅ Version management with runtime info
|
|
||||||
|
|
||||||
## How to Propose a New Feature
|
|
||||||
|
|
||||||
1. Open a Gitea issue describing the use case and acceptance criteria
|
|
||||||
2. If the feature implies an architectural decision, draft an ADR (`adr/<NNNN>-<slug>.md`) following the template
|
|
||||||
3. Reference the ADR + issue in any PR introducing the feature
|
|
||||||
4. Update this roadmap (move from "Potential" to "Completed" when shipped)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Project Status Snapshot
|
|
||||||
|
|
||||||
Last updated 2026-05-05 evening.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Active Features
|
|
||||||
|
|
||||||
- Magic-link passwordless auth (POST /api/v1/auth/magic-link/request + GET /consume) — production-ready, ADR-0028 Phase A complete
|
|
||||||
- OIDC client + HTTP handlers (GET /api/v1/auth/oidc/{provider}/start + /callback with PKCE) — production-ready code, BDD coverage TODO. ADR-0028 Phase B (B.1, B.3, B.4 + tests done ; B.5 BDD scenarios TODO).
|
|
||||||
- Username + password auth — legacy (ADR-0018), kept during migration. To be decommissioned in Phase C.
|
|
||||||
- Versioned API, JWT, OpenTelemetry, Swagger, BDD
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What's In Progress / Next
|
|
||||||
|
|
||||||
- Phase B.5 BDD scenarios for OIDC (1 PR Mistral expected)
|
|
||||||
- Phase C decommission password auth (separate ADR)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure Highlights
|
|
||||||
|
|
||||||
```
|
|
||||||
adr/ : ADRs
|
|
||||||
pkg/ : packages (auth, config, server, user, etc.)
|
|
||||||
features/ : BDD scenarios
|
|
||||||
documentation/ : docs index
|
|
||||||
scripts/ : build + CI
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Documentation Entry Points
|
|
||||||
|
|
||||||
- README.md : quick start
|
|
||||||
- AGENTS.md : agent + automation conventions
|
|
||||||
- documentation/AUTH.md : auth system synthesis
|
|
||||||
- documentation/MISTRAL-AUTONOMOUS-PATTERN.md : how Mistral PRs are shipped
|
|
||||||
- documentation/PHASE_B_ROADMAP.md : remaining auth migration work
|
|
||||||
- documentation/2026-05-05-AUTONOMOUS-SESSION-RECAP.md : the autonomous session highlights
|
|
||||||
- adr/ : architecture decisions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Today's Milestone (2026-05-05)
|
|
||||||
|
|
||||||
27 PRs merged in 1 day via the Mistral autonomous multi-process pattern. ADR-0028 (passwordless auth migration) essentially complete except Phase B.5 BDD.
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# Troubleshooting
|
|
||||||
|
|
||||||
Common issues and their resolution. Extracted from the original `AGENTS.md` and merged with relevant sections from `AGENT_USAGE_GUIDE.md` and `BDD_GUIDE.md`. Refer back to those guides for context-specific troubleshooting (agent workflows, BDD test failures).
|
|
||||||
|
|
||||||
## Port Already in Use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Find and kill process using port 8080
|
|
||||||
kill -TERM $(lsof -ti :8080)
|
|
||||||
|
|
||||||
# Force kill if graceful does not work
|
|
||||||
kill -9 $(lsof -ti :8080)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server Not Responding
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if running
|
|
||||||
curl -s http://localhost:8080/api/health
|
|
||||||
|
|
||||||
# Restart server using control script
|
|
||||||
./scripts/start-server.sh restart
|
|
||||||
|
|
||||||
# View recent logs
|
|
||||||
./scripts/start-server.sh logs
|
|
||||||
```
|
|
||||||
|
|
||||||
If health endpoint returns connection refused, the server may have crashed. Check logs in `./scripts/start-server.sh logs` for stack traces.
|
|
||||||
|
|
||||||
## Dependency Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clean and rebuild
|
|
||||||
go mod tidy
|
|
||||||
go build ./...
|
|
||||||
|
|
||||||
# If dependency version conflicts persist
|
|
||||||
go mod download
|
|
||||||
go mod verify
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tests Failing
|
|
||||||
|
|
||||||
### Unit tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run with verbose output
|
|
||||||
go test -v ./...
|
|
||||||
|
|
||||||
# Check specific test
|
|
||||||
go test ./pkg/greet/ -run TestName
|
|
||||||
```
|
|
||||||
|
|
||||||
### BDD tests
|
|
||||||
|
|
||||||
See [`BDD_GUIDE.md`](BDD_GUIDE.md) for the full BDD troubleshooting workflow (Godog setup, scenario isolation, step matching). Common BDD issues:
|
|
||||||
|
|
||||||
- **Step not found** → check `pkg/bdd/steps/` for the step definition file
|
|
||||||
- **Scenario state leaking** → review [ADR-0025](../adr/0025-bdd-scenario-isolation-strategies.md) for the isolation pattern
|
|
||||||
- **Database not reset** → ensure the test fixtures cleanup runs (BDD scenario After hooks)
|
|
||||||
|
|
||||||
## Configuration Not Loading
|
|
||||||
|
|
||||||
The application logs the configuration source at startup. Check logs for:
|
|
||||||
|
|
||||||
```
|
|
||||||
[INF] Configuration loaded from: file:config.yaml
|
|
||||||
# or
|
|
||||||
[INF] Configuration loaded from: env
|
|
||||||
# or
|
|
||||||
[INF] Configuration loaded from: defaults
|
|
||||||
```
|
|
||||||
|
|
||||||
If config is not loading as expected:
|
|
||||||
|
|
||||||
1. Verify file exists and is readable: `ls -la config.yaml`
|
|
||||||
2. Verify env vars are exported: `env | grep DLC_`
|
|
||||||
3. Check for typos in keys (case-sensitive)
|
|
||||||
4. Review [`AGENT_USAGE_GUIDE.md`](AGENT_USAGE_GUIDE.md) section "Configuration troubleshooting"
|
|
||||||
|
|
||||||
## OpenTelemetry Not Tracing
|
|
||||||
|
|
||||||
1. Verify Jaeger is running: `docker ps | grep jaeger`
|
|
||||||
2. Check `DLC_TELEMETRY_ENABLED=true` in environment or `telemetry.enabled: true` in config
|
|
||||||
3. Verify OTLP endpoint reachable: `nc -zv localhost 4317`
|
|
||||||
4. Check sampler is not `always_off`
|
|
||||||
5. See [`OBSERVABILITY.md`](OBSERVABILITY.md) for full setup
|
|
||||||
|
|
||||||
## Build Failures
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clear caches
|
|
||||||
go clean -cache -modcache
|
|
||||||
go mod download
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
go build ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
If errors persist, see [`local-ci-cd-testing.md`](local-ci-cd-testing.md) for the CI/CD pipeline that mirrors the production build.
|
|
||||||
|
|
||||||
## Where to Look Next
|
|
||||||
|
|
||||||
- **Agent-specific issues** (vibe, mistral, programmer agent) → [`AGENT_USAGE_GUIDE.md`](AGENT_USAGE_GUIDE.md)
|
|
||||||
- **BDD-specific issues** → [`BDD_GUIDE.md`](BDD_GUIDE.md)
|
|
||||||
- **Version/release issues** → [`version-management-guide.md`](version-management-guide.md)
|
|
||||||
- **CI/CD issues** → [`local-ci-cd-testing.md`](local-ci-cd-testing.md)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
@magic-link
|
|
||||||
Feature: Passwordless magic-link sign-in
|
|
||||||
As a user without a password
|
|
||||||
I want to sign in by clicking a link sent to my email
|
|
||||||
So I can access the system without typing a password
|
|
||||||
|
|
||||||
Scenario: Happy path - request, receive, consume
|
|
||||||
Given the server is running
|
|
||||||
And I have an email address for this scenario
|
|
||||||
When I request a magic link for my email
|
|
||||||
Then I should receive an email with subject "Your sign-in link"
|
|
||||||
And the email contains a magic link token
|
|
||||||
When I consume the magic link token
|
|
||||||
Then the consume should succeed and return a JWT
|
|
||||||
|
|
||||||
Scenario: Token cannot be consumed twice
|
|
||||||
Given the server is running
|
|
||||||
And I have an email address for this scenario
|
|
||||||
When I request a magic link for my email
|
|
||||||
And the email contains a magic link token
|
|
||||||
When I consume the magic link token
|
|
||||||
Then the consume should succeed and return a JWT
|
|
||||||
When I consume the magic link token
|
|
||||||
Then the consume should fail with 401
|
|
||||||
|
|
||||||
Scenario: Missing token returns 400
|
|
||||||
Given the server is running
|
|
||||||
When I consume an empty magic link token
|
|
||||||
Then the response should have status 400
|
|
||||||
|
|
||||||
Scenario: Unknown token returns 401
|
|
||||||
Given the server is running
|
|
||||||
When I consume an unknown magic link token
|
|
||||||
Then the consume should fail with 401
|
|
||||||
@@ -15,51 +15,23 @@ Feature: Greet Service
|
|||||||
When I request a greeting for "John"
|
When I request a greeting for "John"
|
||||||
Then the response should be "{\"message\":\"Hello John!\"}"
|
Then the response should be "{\"message\":\"Hello John!\"}"
|
||||||
|
|
||||||
@critical @v2-gate
|
|
||||||
Scenario: v2 endpoint returns 404 when api.v2_enabled is disabled
|
|
||||||
# In the default tag-filter run (~@v2), the test server starts with
|
|
||||||
# v2_enabled=false. The v2EnabledGate middleware (ADR-0023 Phase 4)
|
|
||||||
# returns 404 with a JSON body explaining the flag state.
|
|
||||||
Given the server is running
|
|
||||||
When I send a POST request to v2 greet with name "John"
|
|
||||||
Then the status code should be 404
|
|
||||||
And the response should contain "v2 API is currently disabled"
|
|
||||||
|
|
||||||
@v2 @api
|
@v2 @api
|
||||||
Scenario: v2 greeting with JSON POST request
|
Scenario: v2 greeting with JSON POST request
|
||||||
Given the server is running with v2 enabled
|
Given the server is running with v2 enabled
|
||||||
When I send a POST request to v2 greet with name "John"
|
When I send a POST request to v2 greet with name "John"
|
||||||
Then the response should be "{\"message\":\"Hello my friend John!\"}"
|
Then the response should be "{\"message\":\"Hello my friend John!\"}"
|
||||||
|
|
||||||
@v2 @api
|
|
||||||
Scenario: v2 default greeting with empty name
|
Scenario: v2 default greeting with empty name
|
||||||
Given the server is running with v2 enabled
|
Given the server is running with v2 enabled
|
||||||
When I send a POST request to v2 greet with name ""
|
When I send a POST request to v2 greet with name ""
|
||||||
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
||||||
|
|
||||||
@v2 @api
|
|
||||||
Scenario: v2 greeting with missing name field
|
Scenario: v2 greeting with missing name field
|
||||||
Given the server is running with v2 enabled
|
Given the server is running with v2 enabled
|
||||||
When I send a POST request to v2 greet with invalid JSON "{}"
|
When I send a POST request to v2 greet with invalid JSON "{}"
|
||||||
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
||||||
|
|
||||||
@v2 @api
|
|
||||||
Scenario: v2 greeting with name that is too long
|
Scenario: v2 greeting with name that is too long
|
||||||
Given the server is running with v2 enabled
|
Given the server is running with v2 enabled
|
||||||
When I send a POST request to v2 greet with name "ThisNameIsWayTooLongAndShouldFailValidationBecauseItExceedsTheMaximumAllowedLengthOf100Characters!!!!"
|
When I send a POST request to v2 greet with name "ThisNameIsWayTooLongAndShouldFailValidationBecauseItExceedsTheMaximumAllowedLengthOf100Characters!!!!"
|
||||||
Then the response should contain error "validation_failed"
|
Then the response should contain error "validation_failed"
|
||||||
|
|
||||||
@ratelimit @skip @bdd-deferred
|
|
||||||
# NOTE: Functional behavior validated by unit tests in pkg/middleware/ratelimit_test.go.
|
|
||||||
# BDD scenario currently skipped: env-var-based rate limit config does not reach the
|
|
||||||
# already-started test server (architectural limitation of testsetup, not the middleware).
|
|
||||||
# TODO: rework testserver to allow per-scenario rate limit config (admin endpoint or
|
|
||||||
# per-scenario fresh server), then re-enable this scenario.
|
|
||||||
Scenario: Greet endpoint rejects requests over the rate limit
|
|
||||||
Given the server is running with rate limit set to 3 requests per minute and burst 3
|
|
||||||
When I make 3 requests to "/api/v1/greet/Alice"
|
|
||||||
Then all responses should have status 200
|
|
||||||
When I make 1 more request to "/api/v1/greet/Alice"
|
|
||||||
Then the response should have status 429
|
|
||||||
And the response body should contain "rate_limited"
|
|
||||||
And the response should have header "Retry-After"
|
|
||||||
@@ -7,12 +7,4 @@ Feature: Health Endpoint
|
|||||||
Scenario: Health check returns healthy status
|
Scenario: Health check returns healthy status
|
||||||
Given the server is running
|
Given the server is running
|
||||||
When I request the health endpoint
|
When I request the health endpoint
|
||||||
Then the response should be "{\"status\":\"healthy\"}"
|
Then the response should be "{\"status\":\"healthy\"}"
|
||||||
|
|
||||||
@basic @critical
|
|
||||||
Scenario: Healthz endpoint returns rich health info
|
|
||||||
Given the server is running
|
|
||||||
When I request the healthz endpoint
|
|
||||||
Then the status code should be 200
|
|
||||||
And the response should be JSON with fields "status, version, uptime_seconds, timestamp"
|
|
||||||
And the "status" field should equal "healthy"
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# features/info/info.feature
|
|
||||||
@info @critical
|
|
||||||
Feature: Info Endpoint
|
|
||||||
The /api/info endpoint should return composite application information
|
|
||||||
|
|
||||||
@basic @critical
|
|
||||||
Scenario: GET /api/info returns all required fields
|
|
||||||
Given the server is running
|
|
||||||
When I request the info endpoint
|
|
||||||
Then the status code should be 200
|
|
||||||
And the response should be JSON
|
|
||||||
And the response should contain "version"
|
|
||||||
And the response should contain "commit_short"
|
|
||||||
And the response should contain "build_date"
|
|
||||||
And the response should contain "uptime_seconds"
|
|
||||||
And the response should contain "cache_enabled"
|
|
||||||
And the response should contain "healthz_status"
|
|
||||||
And the "healthz_status" field should equal "healthy"
|
|
||||||
|
|
||||||
@version @critical
|
|
||||||
Scenario: version field matches semantic version pattern
|
|
||||||
Given the server is running
|
|
||||||
When I request the info endpoint
|
|
||||||
Then the status code should be 200
|
|
||||||
And the "version" field should match /^\d+\.\d+\.\d+$/
|
|
||||||
|
|
||||||
@cache @skip @bdd-deferred
|
|
||||||
Scenario: /api/info is cached when cache is enabled
|
|
||||||
# Deferred: the BDD testsetup currently runs with cache disabled
|
|
||||||
# (see "Cache service disabled" in test logs). Cache HIT/MISS behavior
|
|
||||||
# is covered by unit tests on the cache service. Reopen this scenario
|
|
||||||
# if/when the BDD harness gains a cache-enabled mode (likely after
|
|
||||||
# ADR-0022 Phase 2).
|
|
||||||
Given the server is running with cache enabled
|
|
||||||
When I request the info endpoint
|
|
||||||
Then the response header "X-Cache" should be "MISS"
|
|
||||||
When I request the info endpoint again
|
|
||||||
Then the response header "X-Cache" should be "HIT"
|
|
||||||
|
|
||||||
@go_version @critical
|
|
||||||
Scenario: go_version field is non-empty
|
|
||||||
Given the server is running
|
|
||||||
When I request the info endpoint
|
|
||||||
Then the status code should be 200
|
|
||||||
And the response should contain "go_version"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package info
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"dance-lessons-coach/pkg/bdd/testsetup"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInfoBDD(t *testing.T) {
|
|
||||||
config := testsetup.NewFeatureConfig("info", "progress", false)
|
|
||||||
suite := testsetup.CreateTestSuite(t, config, "dance-lessons-coach BDD Tests - Info Feature")
|
|
||||||
|
|
||||||
if suite.Run() != 0 {
|
|
||||||
t.Fatal("non-zero status returned, failed to run info BDD tests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,16 +40,6 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the primary secret should not be removed
|
Then the primary secret should not be removed
|
||||||
And the primary secret should remain active
|
And the primary secret should remain active
|
||||||
|
|
||||||
@critical @admin-introspection
|
|
||||||
Scenario: Admin metadata endpoint exposes structure without leaking secret values
|
|
||||||
Given a primary JWT secret exists
|
|
||||||
And I add a secondary JWT secret "test-secret-do-not-leak-please-12345"
|
|
||||||
When I request the JWT secrets metadata endpoint
|
|
||||||
Then the status code should be 200
|
|
||||||
And the metadata should contain 2 secrets
|
|
||||||
And the metadata should NOT contain the secret value "test-secret-do-not-leak-please-12345"
|
|
||||||
And every secret in the metadata should have a SHA-256 fingerprint
|
|
||||||
|
|
||||||
@todo
|
@todo
|
||||||
Scenario: Multiple secrets with different ages
|
Scenario: Multiple secrets with different ages
|
||||||
Given I have 3 JWT secrets of different ages
|
Given I have 3 JWT secrets of different ages
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { StorybookConfig } from '@storybook/vue3-vite'
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ['../components/**/*.stories.@(js|ts|mdx)'],
|
|
||||||
addons: ['@storybook/addon-essentials'],
|
|
||||||
framework: {
|
|
||||||
name: '@storybook/vue3-vite',
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: 'tag',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Preview } from '@storybook/vue3'
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
parameters: {
|
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/i,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default preview
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</template>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import AppFooterView, { type AppInfo } from './AppFooterView.vue'
|
|
||||||
|
|
||||||
// Wrapper: handles data fetching, delegates rendering to AppFooterView.
|
|
||||||
// Separation of concerns (SRP) - same pattern as HealthDashboard / HealthDashboardView.
|
|
||||||
// server: false → fetch client-side only. Avoids SSR fetching through the dev proxy
|
|
||||||
// (which can fail in some local setups), and lets Playwright route mocks apply.
|
|
||||||
const { data, pending, error } = useFetch<AppInfo>('/api/info', { server: false })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AppFooterView :data="data" :pending="pending" :error="error" />
|
|
||||||
</template>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { humaniseUptime } from '~/utils/uptime'
|
|
||||||
|
|
||||||
export interface AppInfo {
|
|
||||||
version: string
|
|
||||||
commit_short: string
|
|
||||||
build_date: string
|
|
||||||
uptime_seconds: number
|
|
||||||
cache_enabled: boolean
|
|
||||||
healthz_status: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
data: AppInfo | null | undefined
|
|
||||||
pending: boolean
|
|
||||||
error: { message: string } | null | undefined
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<footer data-testid="app-footer">
|
|
||||||
<p v-if="pending" data-testid="app-footer-pending">v?</p>
|
|
||||||
<p v-else-if="error" data-testid="app-footer-error">v? · info unavailable</p>
|
|
||||||
<p v-else-if="data" data-testid="app-footer-info">
|
|
||||||
<span data-testid="app-footer-version">v{{ data.version }}</span>
|
|
||||||
<span> · commit </span>
|
|
||||||
<span data-testid="app-footer-commit">{{ data.commit_short }}</span>
|
|
||||||
<span> · uptime </span>
|
|
||||||
<span data-testid="app-footer-uptime">{{ humaniseUptime(data.uptime_seconds) }}</span>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
footer {
|
|
||||||
border-top: 1px solid #ccc;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
footer p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
||||||
import HealthDashboard from './HealthDashboard.vue'
|
|
||||||
|
|
||||||
const meta: Meta<typeof HealthDashboard> = {
|
|
||||||
title: 'Components/HealthDashboard',
|
|
||||||
component: HealthDashboard,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component:
|
|
||||||
'Smart wrapper that calls /api/healthz internally and delegates rendering to HealthDashboardView. ' +
|
|
||||||
'For state-by-state previews (Healthy, Loading, Error), see ' +
|
|
||||||
'[HealthDashboardView stories](?path=/docs/components-healthdashboardview--docs).',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>
|
|
||||||
|
|
||||||
// Default story - calls real /api/healthz (works in browser if dev proxy + backend are up)
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {},
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import HealthDashboardView, { type HealthInfo } from './HealthDashboardView.vue'
|
|
||||||
|
|
||||||
// Wrapper: handles data fetching, delegates rendering to HealthDashboardView.
|
|
||||||
// Separation of concerns (SRP):
|
|
||||||
// - HealthDashboard (this) = data layer (useFetch lifecycle)
|
|
||||||
// - HealthDashboardView = presentation layer (testable in Storybook + e2e)
|
|
||||||
//
|
|
||||||
// server: false → fetch client-side only. Avoids SSR fetching through the dev
|
|
||||||
// proxy (which can fail in some local setups), and lets Playwright route mocks
|
|
||||||
// apply. Same fix that landed for AppFooter in PR #40.
|
|
||||||
const { data, pending, error } = useFetch<HealthInfo>('/api/healthz', { server: false })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<HealthDashboardView :data="data" :pending="pending" :error="error" />
|
|
||||||
</template>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
||||||
import HealthDashboardView from './HealthDashboardView.vue'
|
|
||||||
|
|
||||||
interface ViewArgs {
|
|
||||||
data: {
|
|
||||||
status: string
|
|
||||||
version: string
|
|
||||||
uptime_seconds: number
|
|
||||||
timestamp: string
|
|
||||||
} | null
|
|
||||||
pending: boolean
|
|
||||||
error: { message: string } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Components/HealthDashboardView',
|
|
||||||
component: HealthDashboardView,
|
|
||||||
tags: ['autodocs'],
|
|
||||||
argTypes: {
|
|
||||||
pending: { control: 'boolean' },
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
docs: {
|
|
||||||
description: {
|
|
||||||
component:
|
|
||||||
'Pure presentational component for the health dashboard. ' +
|
|
||||||
'Accepts `data`, `pending`, `error` as props so all 3 states can be ' +
|
|
||||||
'previewed in Storybook and asserted in unit tests. The data fetching ' +
|
|
||||||
'wrapper is `HealthDashboard.vue`.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies Meta<ViewArgs>
|
|
||||||
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>
|
|
||||||
|
|
||||||
export const Healthy: Story = {
|
|
||||||
args: {
|
|
||||||
data: {
|
|
||||||
status: 'healthy',
|
|
||||||
version: '1.4.0',
|
|
||||||
uptime_seconds: 3600,
|
|
||||||
timestamp: '2026-05-03T17:30:00.000Z',
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Loading: Story = {
|
|
||||||
args: {
|
|
||||||
data: null,
|
|
||||||
pending: true,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorState: Story = {
|
|
||||||
args: {
|
|
||||||
data: null,
|
|
||||||
pending: false,
|
|
||||||
error: { message: '[GET] "/api/healthz": 502 Bad Gateway (simulated)' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HealthyHighUptime: Story = {
|
|
||||||
args: {
|
|
||||||
data: {
|
|
||||||
status: 'healthy',
|
|
||||||
version: '1.5.0-rc1',
|
|
||||||
uptime_seconds: 86400 * 7,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
pending: false,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
export interface HealthInfo {
|
|
||||||
status: string
|
|
||||||
version: string
|
|
||||||
uptime_seconds: number
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
data: HealthInfo | null | undefined
|
|
||||||
pending: boolean
|
|
||||||
error: { message: string } | null | undefined
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section data-testid="health-dashboard">
|
|
||||||
<h2>Server Health</h2>
|
|
||||||
<p v-if="pending" data-testid="health-loading">Loading...</p>
|
|
||||||
<p v-else-if="error" data-testid="health-error">
|
|
||||||
Error loading health: {{ error.message }}
|
|
||||||
</p>
|
|
||||||
<ul v-else-if="data" data-testid="health-info">
|
|
||||||
<li><strong>Status:</strong> <span data-testid="health-status">{{ data.status }}</span></li>
|
|
||||||
<li><strong>Version:</strong> {{ data.version }}</li>
|
|
||||||
<li><strong>Uptime:</strong> {{ data.uptime_seconds }} seconds</li>
|
|
||||||
<li><strong>Last check:</strong> {{ data.timestamp }}</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Frontend Docs
|
|
||||||
|
|
||||||
- [E2E Test Reports](./e2e/README.md) - auto-generated by `npm run docs:gen`
|
|
||||||
- Storybook (run locally: `npm run storybook` ; build: `npm run build-storybook` then open `storybook-static/index.html`)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# E2E Test Reports
|
|
||||||
|
|
||||||
[<- Up to docs](../README.md)
|
|
||||||
|
|
||||||
| Test | Status | Duration |
|
|
||||||
|------|--------|----------|
|
|
||||||
| [home page loads and shows server health info](./home-page-loads-and-shows-server-health-info.md) | PASSED | 168ms |
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# home page loads and shows server health info
|
|
||||||
|
|
||||||
[<- Back to index](./README.md) | [Top](../README.md)
|
|
||||||
|
|
||||||
**File**: `tests/e2e/health.spec.ts`
|
|
||||||
**Status**: PASSED
|
|
||||||
**Duration**: 168ms
|
|
||||||
|
|
||||||
## Screenshot
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Test Details
|
|
||||||
|
|
||||||
- Start Time: 2026-05-03T14:38:42.958Z
|
|
||||||
- Spec File: health.spec.ts
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="layout-root">
|
|
||||||
<slot />
|
|
||||||
<AppFooter />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.layout-root {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.layout-root > :first-child {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export default defineNuxtConfig({
|
|
||||||
devtools: { enabled: true },
|
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
13525
frontend/package-lock.json
generated
13525
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dance-lessons-coach-frontend",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"build-storybook": "storybook build",
|
|
||||||
"docs:gen": "playwright test && node scripts/generate-test-docs.mjs",
|
|
||||||
"docs:full": "npm run build-storybook && npm run docs:gen"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.59.1",
|
|
||||||
"@storybook/addon-essentials": "^8.0.0",
|
|
||||||
"@storybook/vue3": "^8.0.0",
|
|
||||||
"@storybook/vue3-vite": "^8.0.0",
|
|
||||||
"@types/node": "^25.6.0",
|
|
||||||
"nuxt": "^3.13.0",
|
|
||||||
"storybook": "^8.0.0",
|
|
||||||
"typescript": "^6.0.3"
|
|
||||||
},
|
|
||||||
"packageManager": "npm@11.5.2"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main>
|
|
||||||
<h1>dance-lessons-coach</h1>
|
|
||||||
<HealthDashboard />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig } from '@playwright/test'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './tests/e2e',
|
|
||||||
timeout: 30_000,
|
|
||||||
reporter: [
|
|
||||||
['list'],
|
|
||||||
['json', { outputFile: path.join(process.cwd(), 'test-results', 'results.json') }],
|
|
||||||
],
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:3000',
|
|
||||||
screenshot: 'on',
|
|
||||||
video: 'off',
|
|
||||||
},
|
|
||||||
outputDir: 'test-results/output',
|
|
||||||
webServer: {
|
|
||||||
command: 'npm run dev',
|
|
||||||
url: 'http://localhost:3000',
|
|
||||||
timeout: 60_000,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import fs from 'node:fs/promises'
|
|
||||||
import path from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const frontendDir = path.resolve(__dirname, '..')
|
|
||||||
|
|
||||||
const resultsPath = path.join(frontendDir, 'test-results', 'results.json')
|
|
||||||
const docsDir = path.join(frontendDir, 'docs', 'e2e')
|
|
||||||
const screenshotsDir = path.join(frontendDir, 'tests', 'e2e', 'screenshots')
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// Read results
|
|
||||||
const resultsText = await fs.readFile(resultsPath, 'utf8')
|
|
||||||
const results = JSON.parse(resultsText)
|
|
||||||
|
|
||||||
// Create output directories
|
|
||||||
await fs.mkdir(docsDir, { recursive: true })
|
|
||||||
|
|
||||||
// Extract tests from suites
|
|
||||||
const testDocs = []
|
|
||||||
for (const suite of results.suites || []) {
|
|
||||||
for (const spec of suite.specs || []) {
|
|
||||||
for (const test of spec.tests || []) {
|
|
||||||
for (const result of test.results || []) {
|
|
||||||
const testInfo = {
|
|
||||||
title: spec.title,
|
|
||||||
specFile: spec.file || suite.file,
|
|
||||||
status: result.status,
|
|
||||||
duration: result.duration,
|
|
||||||
startTime: result.startTime,
|
|
||||||
attachments: result.attachments || [],
|
|
||||||
}
|
|
||||||
testDocs.push(testInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate individual test markdown files
|
|
||||||
for (const test of testDocs) {
|
|
||||||
const slug = slugify(test.title)
|
|
||||||
const mdPath = path.join(docsDir, `${slug}.md`)
|
|
||||||
|
|
||||||
// Use slug-based screenshot name (matches explicit screenshot in test)
|
|
||||||
let screenshotPath = `${slug}.png`
|
|
||||||
|
|
||||||
// Also try to find screenshot attachment and use its basename
|
|
||||||
if (test.attachments && test.attachments.length > 0) {
|
|
||||||
for (const attachment of test.attachments) {
|
|
||||||
if (attachment.contentType === 'image/png') {
|
|
||||||
const basename = path.basename(attachment.path)
|
|
||||||
// Prefer explicit screenshot name if it matches our pattern
|
|
||||||
if (basename !== 'test-finished-1.png' && basename !== 'test-finished-2.png') {
|
|
||||||
screenshotPath = basename
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const absoluteScreenshotPath = path.join(screenshotsDir, screenshotPath)
|
|
||||||
const relativeScreenshotPath = path.relative(docsDir, absoluteScreenshotPath)
|
|
||||||
|
|
||||||
const mdContent = `# ${test.title}
|
|
||||||
|
|
||||||
[<- Back to index](./README.md) | [Top](../README.md)
|
|
||||||
|
|
||||||
**File**: \`tests/e2e/${test.specFile}\`
|
|
||||||
**Status**: ${test.status.toUpperCase()}
|
|
||||||
**Duration**: ${test.duration}ms
|
|
||||||
|
|
||||||
## Screenshot
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Test Details
|
|
||||||
|
|
||||||
- Start Time: ${test.startTime || 'N/A'}
|
|
||||||
- Spec File: ${test.specFile}
|
|
||||||
`
|
|
||||||
|
|
||||||
await fs.writeFile(mdPath, mdContent)
|
|
||||||
console.log(`Generated: ${path.relative(frontendDir, mdPath)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate index README
|
|
||||||
const indexContent = `# E2E Test Reports
|
|
||||||
|
|
||||||
[<- Up to docs](../README.md)
|
|
||||||
|
|
||||||
| Test | Status | Duration |
|
|
||||||
|------|--------|----------|
|
|
||||||
${testDocs.map(t => `| [${escapeMd(t.title)}](./${slugify(t.title)}.md) | ${t.status.toUpperCase()} | ${t.duration}ms |`).join('\n')}
|
|
||||||
`
|
|
||||||
|
|
||||||
await fs.writeFile(path.join(docsDir, 'README.md'), indexContent)
|
|
||||||
console.log(`Generated: ${path.relative(frontendDir, path.join(docsDir, 'README.md'))}`)
|
|
||||||
|
|
||||||
console.log(`\nGenerated ${testDocs.length} test docs`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function slugify(str) {
|
|
||||||
return str
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\s-]/g, '')
|
|
||||||
.replace(/[\s_]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeMd(str) {
|
|
||||||
return str.replace(/[|\\\[\]\{\}]/g, '\\$&')
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error('Error:', err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
6
frontend/shims-vue.d.ts
vendored
6
frontend/shims-vue.d.ts
vendored
@@ -1,6 +0,0 @@
|
|||||||
declare module '*.vue' {
|
|
||||||
import type { DefineComponent } from 'vue'
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const component: DefineComponent<any, any, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
// Both specs mock /api/info so they decouple from the dev-proxy plumbing.
|
|
||||||
// The integration with the real backend is covered by the BDD scenario in
|
|
||||||
// features/info/info.feature (server-side, no frontend proxy in the loop).
|
|
||||||
|
|
||||||
test('home page footer shows version, commit and uptime', async ({ page }) => {
|
|
||||||
await page.route('**/api/info', (route) => {
|
|
||||||
route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify({
|
|
||||||
version: '1.4.0',
|
|
||||||
commit_short: '4a3f1bb',
|
|
||||||
build_date: '2026-05-05T00:00:00Z',
|
|
||||||
uptime_seconds: 8042,
|
|
||||||
cache_enabled: true,
|
|
||||||
healthz_status: 'healthy',
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await page.goto('/')
|
|
||||||
|
|
||||||
// Footer is mounted globally via layouts/default.vue
|
|
||||||
await expect(page.getByTestId('app-footer')).toBeVisible()
|
|
||||||
|
|
||||||
// The PR #32 lesson: assert content, not just visibility.
|
|
||||||
// Without the regex check the test would PASS even if the footer rendered the
|
|
||||||
// pending placeholder ("v?") indefinitely.
|
|
||||||
await expect(page.getByTestId('app-footer-info')).toBeVisible()
|
|
||||||
const versionLocator = page.getByTestId('app-footer-version')
|
|
||||||
await expect(versionLocator).toBeVisible()
|
|
||||||
await expect(versionLocator).toHaveText(/^v\d+\.\d+\.\d+$/)
|
|
||||||
|
|
||||||
// Commit and uptime should be present and non-empty.
|
|
||||||
await expect(page.getByTestId('app-footer-commit')).not.toBeEmpty()
|
|
||||||
await expect(page.getByTestId('app-footer-uptime')).not.toBeEmpty()
|
|
||||||
|
|
||||||
await page.screenshot({
|
|
||||||
path: 'tests/e2e/screenshots/app-footer-shows-version-commit-uptime.png',
|
|
||||||
fullPage: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Regression spec: documents the expected error UX so we don't ship a silent failure.
|
|
||||||
// Routes /api/info to a 502 mock so the test is reproducible regardless of backend.
|
|
||||||
test('home page footer surfaces info endpoint errors gracefully', async ({ page }) => {
|
|
||||||
await page.route('**/api/info', (route) => {
|
|
||||||
route.fulfill({
|
|
||||||
status: 502,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify({ error: 'simulated_backend_down' }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await page.goto('/')
|
|
||||||
|
|
||||||
// Footer must NOT crash the page
|
|
||||||
await expect(page.getByTestId('app-footer')).toBeVisible()
|
|
||||||
await expect(page.getByTestId('app-footer-error')).toBeVisible()
|
|
||||||
// The error placeholder should NOT contain a real version pattern
|
|
||||||
await expect(page.getByTestId('app-footer-info')).not.toBeVisible()
|
|
||||||
|
|
||||||
await page.screenshot({
|
|
||||||
path: 'tests/e2e/screenshots/app-footer-surfaces-info-endpoint-errors-gracefully.png',
|
|
||||||
fullPage: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
// Both specs mock /api/healthz so they decouple from the dev-proxy plumbing.
|
|
||||||
// The integration with the real backend is covered by the BDD scenario in
|
|
||||||
// features/health/health.feature (server-side, no frontend proxy in the loop).
|
|
||||||
// Same approach as tests/e2e/app-footer.spec.ts (PR #40) - applied here to
|
|
||||||
// close the debt left by that PR's out-of-scope follow-up note.
|
|
||||||
|
|
||||||
test('home page loads and shows healthy server state', async ({ page }) => {
|
|
||||||
await page.route('**/api/healthz', (route) => {
|
|
||||||
route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify({
|
|
||||||
status: 'healthy',
|
|
||||||
version: '1.4.0',
|
|
||||||
uptime_seconds: 8042,
|
|
||||||
timestamp: '2026-05-05T08:00:00Z',
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await page.goto('/')
|
|
||||||
await expect(page.getByTestId('health-dashboard')).toBeVisible()
|
|
||||||
const heading = page.getByRole('heading', { name: /dance-lessons-coach/i })
|
|
||||||
await expect(heading).toBeVisible()
|
|
||||||
|
|
||||||
// Assert the dashboard is in HEALTHY state, not an error state.
|
|
||||||
// The dashboard renders an "Error loading health: ..." paragraph when /api/healthz
|
|
||||||
// is unreachable (Go backend not running, proxy misconfigured, endpoint removed,
|
|
||||||
// etc.). Without these assertions the test would falsely PASS even when the
|
|
||||||
// dashboard shows the error UI - regression observed 2026-05-03 (Go backend
|
|
||||||
// not running locally → page renders the error, Playwright PASSES).
|
|
||||||
await expect(page.getByTestId('health-info')).toBeVisible()
|
|
||||||
await expect(page.getByTestId('health-status')).toHaveText('healthy')
|
|
||||||
await expect(page.getByText(/Error loading health/i)).not.toBeVisible()
|
|
||||||
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/home-page-loads-and-shows-server-health-info.png', fullPage: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Regression spec: documents the expected error UX so we don't ship a silent failure.
|
|
||||||
// Routes /api/healthz to a 502 mock so the test is reproducible regardless of backend.
|
|
||||||
test('home page surfaces health endpoint errors visibly', async ({ page }) => {
|
|
||||||
await page.route('**/api/healthz', (route) => {
|
|
||||||
route.fulfill({
|
|
||||||
status: 502,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify({ error: 'simulated_backend_down' }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await page.goto('/')
|
|
||||||
await expect(page.getByTestId('health-dashboard')).toBeVisible()
|
|
||||||
await expect(page.getByText(/Error loading health/i)).toBeVisible()
|
|
||||||
await expect(page.getByTestId('health-info')).not.toBeVisible()
|
|
||||||
await page.screenshot({ path: 'tests/e2e/screenshots/home-page-surfaces-health-endpoint-errors-visibly.png', fullPage: true })
|
|
||||||
})
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user