Merge pull request #14 from iflytek/feat/optimize-staging-build

feat: optimize development workflow with fast local dev and staging environments
This commit is contained in:
wowo 2026-03-14 14:27:48 +08:00 committed by GitHub
commit bd87e701cf
10 changed files with 969 additions and 2 deletions

107
Makefile
View file

@ -1,4 +1,4 @@
.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset validate-release-config
.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset dev-logs dev-status build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api db-reset validate-release-config staging staging-down staging-logs pr
DEV_DIR := .dev
DEV_SERVER_PID := $(DEV_DIR)/server.pid
@ -7,6 +7,9 @@ DEV_SERVER_LOG := $(DEV_DIR)/server.log
DEV_WEB_LOG := $(DEV_DIR)/web.log
DEV_WEB_URL := http://localhost:3000
DEV_API_URL := http://localhost:8080
STAGING_API_URL := http://localhost:8080
STAGING_WEB_URL := http://localhost
STAGING_SERVER_IMAGE := skillhub-server:staging
DEV_PROCESS := python3 scripts/dev_process.py
help: ## 显示帮助
@ -105,6 +108,34 @@ dev-all-reset: ## 重置本地开发环境(清理依赖数据卷后重新启
rm -rf $(DEV_DIR)
@$(MAKE) dev-all
dev-status: ## 查看本地开发服务状态
@echo "=== Dependency Services ==="
@docker compose ps
@echo ""
@echo "=== Backend ==="
@if $(DEV_PROCESS) status --pid-file $(DEV_SERVER_PID) >/dev/null 2>&1; then \
echo " Running (PID $$(cat $(DEV_SERVER_PID)))"; \
else \
echo " Not running"; \
fi
@echo "=== Frontend ==="
@if $(DEV_PROCESS) status --pid-file $(DEV_WEB_PID) >/dev/null 2>&1; then \
echo " Running (PID $$(cat $(DEV_WEB_PID)))"; \
else \
echo " Not running"; \
fi
dev-logs: ## 实时查看开发服务日志backend/frontend默认 backend
@SERVICE=$${SERVICE:-backend}; \
if [ "$$SERVICE" = "backend" ]; then \
tail -f $(DEV_SERVER_LOG); \
elif [ "$$SERVICE" = "frontend" ]; then \
tail -f $(DEV_WEB_LOG); \
else \
echo "Unknown service: $$SERVICE. Use SERVICE=backend or SERVICE=frontend"; \
exit 1; \
fi
build: ## 构建后端
cd server && ./mvnw clean package -DskipTests
@ -145,3 +176,77 @@ db-reset: ## 重置数据库
validate-release-config: ## 校验发布环境变量文件(默认 .env.release
./scripts/validate-release-config.sh .env.release
staging: ## 构建并启动 staging 环境,运行 smoke test混合模式后端镜像 + 前端静态文件)
@echo "=== [1/5] Building backend JAR and Docker image ==="
cd server && ./mvnw package -DskipTests -B -q
docker build -t $(STAGING_SERVER_IMAGE) -f server/Dockerfile.dev server
@echo "=== [2/5] Building frontend static files ==="
cd web && pnpm run build
@echo "=== [3/5] Starting dependency services ==="
docker compose up -d --wait
@echo "=== [4/5] Starting staging services ==="
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d --wait server web
@echo "=== [5/5] Running smoke tests ==="
@if bash scripts/smoke-test.sh $(STAGING_API_URL); then \
echo ""; \
echo "Staging passed. Environment is running:"; \
echo " Web UI: $(STAGING_WEB_URL)"; \
echo " Backend: $(STAGING_API_URL)"; \
echo ""; \
echo "Run 'make staging-down' to stop."; \
echo "Run 'make pr' to create a pull request."; \
else \
echo ""; \
echo "Smoke tests FAILED. Printing logs..."; \
docker compose -f docker-compose.yml -f docker-compose.staging.yml logs server; \
$(MAKE) staging-down; \
exit 1; \
fi
staging-down: ## 停止 staging 环境
docker compose -f docker-compose.yml -f docker-compose.staging.yml down --remove-orphans
staging-logs: ## 查看 staging 服务日志SERVICE=server|web默认 server
@SERVICE=$${SERVICE:-server}; \
docker compose -f docker-compose.yml -f docker-compose.staging.yml logs -f $$SERVICE
pr: ## 推送当前分支并创建 Pull Request需要 gh CLI仅限交互式终端
@if ! command -v gh >/dev/null 2>&1; then \
echo "Error: gh CLI not found. Install from https://cli.github.com/"; \
exit 1; \
fi
@if ! gh auth status >/dev/null 2>&1; then \
echo "Error: gh CLI not authenticated. Run: gh auth login"; \
exit 1; \
fi
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
if [ "$$BRANCH" = "main" ] || [ "$$BRANCH" = "master" ]; then \
echo "Error: Cannot create PR from main/master branch."; \
exit 1; \
fi
@if ! git diff --quiet || ! git diff --cached --quiet; then \
echo "You have uncommitted changes:"; \
git status --short; \
echo ""; \
printf "Commit all changes before creating PR? [y/N] "; \
read -r answer; \
if [ "$$answer" = "y" ] || [ "$$answer" = "Y" ]; then \
git add -A; \
git commit -m "chore: pre-PR commit"; \
else \
echo "Aborted. Commit or stash your changes first."; \
exit 1; \
fi; \
fi
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
echo "Pushing branch $$BRANCH to origin..."; \
git push -u origin "$$BRANCH"
@echo "Creating pull request..."
@if gh pr view >/dev/null 2>&1; then \
echo "A pull request already exists for this branch:"; \
gh pr view --json url -q '.url'; \
exit 0; \
fi
@gh pr create --fill --web || gh pr create --fill

View file

@ -96,6 +96,8 @@ make dev-all-reset
Run `make help` to see all available commands.
For the full development workflow (local dev → staging → PR), see [docs/dev-workflow.md](docs/dev-workflow.md).
### API Contract Sync
OpenAPI types for the web client are checked into the repository.

View file

@ -0,0 +1,76 @@
# Staging environment: hybrid mode
# - Backend: locally built Docker image
# - Frontend: locally built static files mounted into Nginx
# - Dependencies: reuses docker-compose.yml (postgres, redis, minio)
#
# Usage: make staging
# Do NOT use directly — use the make target which handles build + deps + cleanup.
# NOTE: postgres, redis, minio services are defined in docker-compose.yml.
# When invoked via "docker compose -f docker-compose.yml -f docker-compose.staging.yml",
# Docker Compose merges both files so these services are available by hostname.
services:
server:
image: skillhub-server:staging
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/skillhub
SPRING_DATASOURCE_USERNAME: skillhub
SPRING_DATASOURCE_PASSWORD: skillhub_dev
REDIS_HOST: redis
REDIS_PORT: 6379
SESSION_COOKIE_SECURE: "false"
SKILLHUB_PUBLIC_BASE_URL: "http://localhost"
DEVICE_AUTH_VERIFICATION_URI: "http://localhost/api/device/activate"
SKILLHUB_STORAGE_PROVIDER: s3
STORAGE_BASE_PATH: /var/lib/skillhub/storage
SKILLHUB_STORAGE_S3_ENDPOINT: http://minio:9000
SKILLHUB_STORAGE_S3_PUBLIC_ENDPOINT: http://localhost:9000
SKILLHUB_STORAGE_S3_BUCKET: skillhub
SKILLHUB_STORAGE_S3_ACCESS_KEY: minioadmin
SKILLHUB_STORAGE_S3_SECRET_KEY: minioadmin
SKILLHUB_STORAGE_S3_REGION: us-east-1
SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE: "true"
SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET: "true"
BOOTSTRAP_ADMIN_ENABLED: "true"
BOOTSTRAP_ADMIN_USER_ID: staging-admin
BOOTSTRAP_ADMIN_USERNAME: admin
BOOTSTRAP_ADMIN_PASSWORD: "Admin@staging2026"
BOOTSTRAP_ADMIN_DISPLAY_NAME: Admin
BOOTSTRAP_ADMIN_EMAIL: admin@skillhub.local
OAUTH2_GITHUB_CLIENT_ID: local-placeholder
OAUTH2_GITHUB_CLIENT_SECRET: local-placeholder
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./web/dist:/usr/share/nginx/html:ro
- ./web/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
environment:
SKILLHUB_API_UPSTREAM: http://server:8080
SKILLHUB_WEB_API_BASE_URL: ""
SKILLHUB_PUBLIC_BASE_URL: ""
depends_on:
server:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/nginx-health"]
interval: 10s
timeout: 5s
retries: 6
start_period: 10s

125
docs/dev-workflow.md Normal file
View file

@ -0,0 +1,125 @@
# Development Workflow
This document describes the recommended workflow for developing SkillHub locally.
## Prerequisites
- Docker Desktop (for dependency services and staging)
- Java 21 (for running the backend locally)
- Node.js 22 + pnpm (for running the frontend locally)
- `gh` CLI (for creating pull requests): https://cli.github.com/
## Stage 1: Local Development (fast iteration)
Use this stage for active development — writing code, fixing bugs, iterating quickly.
### Start the full local stack
```bash
make dev-all
```
This starts:
- Dependency services (Postgres, Redis, MinIO) via Docker
- Backend (Spring Boot) directly on your machine at http://localhost:8080
- Frontend (Vite) directly on your machine at http://localhost:3000
### Hot reload
**Frontend:** Vite HMR is enabled by default. Save a file and the browser updates instantly.
**Backend:** Spring Boot DevTools is configured. After editing Java code:
1. In IntelliJ IDEA: press `Cmd+F9` (Build Project)
2. The backend restarts automatically in 3-8 seconds
3. Watch the terminal running `make dev-server` for the restart log
### Mock authentication
Two mock users are available in local mode (no password needed):
| User ID | Role | Header |
|---------------|-------------|----------------------------------|
| `local-user` | Regular user | `X-Mock-User-Id: local-user` |
| `local-admin` | Super admin | `X-Mock-User-Id: local-admin` |
### Useful commands
| Command | Description |
|----------------------------------|----------------------------------|
| `make dev-all` | Start full local stack |
| `make dev-all-down` | Stop all local services |
| `make dev-status` | Check status of all services |
| `make dev-logs` | Tail backend logs |
| `SERVICE=frontend make dev-logs` | Tail frontend logs |
| `make dev-all-reset` | Full reset (clears data volumes) |
| `make db-reset` | Reset database only |
## Stage 2: Staging Regression (pre-PR validation)
Use this stage when a feature or bugfix is complete and you want to verify it works correctly in a Docker environment before pushing.
### What staging does
`make staging` runs a **hybrid** Docker environment:
- **Backend**: built as a Docker image from your local source
- **Frontend**: built as static files (`pnpm build`) and served by Nginx
- **Dependencies**: same Postgres/Redis/MinIO as local dev
This is faster than building both images but still validates the containerized backend and the production Nginx serving path.
### Run staging
```bash
make staging
```
This will:
1. Build the backend Docker image
2. Build the frontend static files
3. Start all services
4. Run smoke tests against the API
5. Print pass/fail summary
If all tests pass, the environment stays running at:
- Web UI: http://localhost
- Backend API: http://localhost:8080
### Stop staging
```bash
make staging-down
```
### View staging logs
```bash
make staging-logs # backend logs
SERVICE=web make staging-logs # nginx logs
```
## Stage 3: Create Pull Request
After staging passes:
```bash
make pr
```
This will:
1. Check for uncommitted changes (prompts to commit if any)
2. Push your branch to origin
3. Create a pull request using `gh pr create --fill`
The PR title and body are auto-populated from your commit messages.
> **Note:** `make pr` requires an interactive terminal. Do not use it in CI.
## Full workflow summary
```
make dev-all # start local dev
# ... write code, test in browser ...
make staging # regression test in Docker
make staging-down # stop staging
make pr # push + create PR
```

View file

@ -0,0 +1,625 @@
# Dev Workflow Optimization Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Optimize the development workflow with fast-restart backend (DevTools), a hybrid Docker staging environment, and automated PR creation.
**Architecture:** Two-stage workflow — local dev with Spring Boot DevTools for fast restarts + Vite HMR, then a hybrid staging mode that builds only the backend Docker image while mounting locally-built frontend static files into Nginx, followed by automated PR creation via `make pr`.
**Tech Stack:** Spring Boot DevTools, Docker Compose (staging overlay), Nginx (static file mount), GNU Make, gh CLI, bash
---
## Chunk 1: Spring Boot DevTools
### Task 1: Add Spring Boot DevTools to backend
**Files:**
- Modify: `server/skillhub-app/pom.xml`
- [ ] **Step 1: Add DevTools dependency**
In `server/skillhub-app/pom.xml`, add inside `<dependencies>` before the test dependencies:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
```
- [ ] **Step 2: Verify DevTools is excluded from production build**
DevTools is automatically excluded when running as a fully packaged JAR (the `optional` flag ensures it's not transitively included). The existing `spring-boot-maven-plugin` configuration in `server/skillhub-app/pom.xml` has no `<configuration>` block, so the default behavior (exclude DevTools from production) applies. No changes needed.
- [ ] **Step 3: Verify local profile still works**
First start the dependency services, then start the backend:
```bash
make dev
```
Expected: Docker services (Postgres, Redis, MinIO) start and become healthy.
Then in the same or a new terminal:
```bash
make dev-server
```
Expected: Backend starts, logs show `LiveReload server is running on port 35729` — this confirms DevTools is active.
- [ ] **Step 4: Test fast restart**
With backend running via `make dev-server`:
1. Edit any Java file (e.g., add a comment to a controller)
2. In IntelliJ: press `Cmd+F9` (Build Project)
3. Watch the terminal running `dev-server`
Expected: Application restarts in 3-8 seconds (vs 30-60 seconds for a cold start). Log line: `Restarting due to classpath changes`.
- [ ] **Step 5: Commit**
```bash
git add server/skillhub-app/pom.xml
git commit -m "feat(dev): add Spring Boot DevTools for fast restart in local dev"
```
---
## Chunk 2: Makefile dev-logs and dev-status targets
### Task 2: Add dev-logs and dev-status Makefile targets
**Files:**
- Modify: `Makefile` (will update `.PHONY` line in this chunk and again in Chunks 4 and 5 — each chunk appends new targets to the same line)
- [ ] **Step 1: Add targets to .PHONY line**
In `Makefile` line 1, add `dev-logs dev-status` to the `.PHONY` list after the existing targets.
- [ ] **Step 2: Add dev-status target**
After the `dev-all-reset` target (around line 106), add the following. Note: these targets reference variables (`DEV_PROCESS`, `DEV_SERVER_PID`, `DEV_WEB_PID`, `DEV_SERVER_LOG`, `DEV_WEB_LOG`) that are already defined at the top of the Makefile (lines 3-10).
```makefile
dev-status: ## 查看本地开发服务状态
@echo "=== Dependency Services ==="
@docker compose ps
@echo ""
@echo "=== Backend ==="
@if $(DEV_PROCESS) status --pid-file $(DEV_SERVER_PID) >/dev/null 2>&1; then \
echo " Running (PID $$(cat $(DEV_SERVER_PID)))"; \
else \
echo " Not running"; \
fi
@echo "=== Frontend ==="
@if $(DEV_PROCESS) status --pid-file $(DEV_WEB_PID) >/dev/null 2>&1; then \
echo " Running (PID $$(cat $(DEV_WEB_PID)))"; \
else \
echo " Not running"; \
fi
dev-logs: ## 实时查看开发服务日志backend/frontend默认 backend
@SERVICE=$${SERVICE:-backend}; \
if [ "$$SERVICE" = "backend" ]; then \
tail -f $(DEV_SERVER_LOG); \
elif [ "$$SERVICE" = "frontend" ]; then \
tail -f $(DEV_WEB_LOG); \
else \
echo "Unknown service: $$SERVICE. Use SERVICE=backend or SERVICE=frontend"; \
exit 1; \
fi
```
- [ ] **Step 3: Verify targets work**
```bash
make dev-status
```
Expected: Shows docker compose service status and backend/frontend process status.
```bash
# In a separate terminal while dev-all is running:
make dev-logs
# Or for frontend:
SERVICE=frontend make dev-logs
```
Expected: Tails the respective log file.
- [ ] **Step 4: Commit**
```bash
git add Makefile
git commit -m "feat(dev): add dev-status and dev-logs make targets"
```
---
## Chunk 3: Staging Docker Compose (hybrid mode)
### Task 3: Create docker-compose.staging.yml
**Files:**
- Create: `docker-compose.staging.yml`
The staging compose file uses:
- Backend: locally-built Docker image (`skillhub-server:staging`)
- Frontend: locally-built static files (`web/dist/`) mounted into an Nginx container using the existing `nginx.conf.template`
- Dependencies: same Postgres/Redis/MinIO as dev (reuses `docker-compose.yml` via `--file` flag)
**Important:** This file references `postgres`, `redis`, and `minio` services in `depends_on` clauses, but does NOT define them. When Docker Compose is invoked with `-f docker-compose.yml -f docker-compose.staging.yml`, it merges both files, so the dependency services from `docker-compose.yml` are available to the `server` and `web` services defined here.
- [ ] **Step 1: Create docker-compose.staging.yml**
```yaml
# Staging environment: hybrid mode
# - Backend: locally built Docker image
# - Frontend: locally built static files mounted into Nginx
# - Dependencies: reuses docker-compose.yml (postgres, redis, minio)
#
# Usage: make staging
# Do NOT use directly — use the make target which handles build + deps + cleanup.
services:
server:
image: skillhub-server:staging
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/skillhub
SPRING_DATASOURCE_USERNAME: skillhub
SPRING_DATASOURCE_PASSWORD: skillhub_dev
REDIS_HOST: redis
REDIS_PORT: 6379
SESSION_COOKIE_SECURE: "false"
SKILLHUB_PUBLIC_BASE_URL: "http://localhost"
DEVICE_AUTH_VERIFICATION_URI: "http://localhost/api/device/activate"
SKILLHUB_STORAGE_PROVIDER: s3
STORAGE_BASE_PATH: /var/lib/skillhub/storage
SKILLHUB_STORAGE_S3_ENDPOINT: http://minio:9000
SKILLHUB_STORAGE_S3_PUBLIC_ENDPOINT: http://localhost:9000
SKILLHUB_STORAGE_S3_BUCKET: skillhub
SKILLHUB_STORAGE_S3_ACCESS_KEY: minioadmin
SKILLHUB_STORAGE_S3_SECRET_KEY: minioadmin
SKILLHUB_STORAGE_S3_REGION: us-east-1
SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE: "true"
SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET: "true"
BOOTSTRAP_ADMIN_ENABLED: "true"
BOOTSTRAP_ADMIN_USER_ID: staging-admin
BOOTSTRAP_ADMIN_USERNAME: admin
BOOTSTRAP_ADMIN_PASSWORD: "Admin@staging2026"
BOOTSTRAP_ADMIN_DISPLAY_NAME: Admin
BOOTSTRAP_ADMIN_EMAIL: admin@skillhub.local
OAUTH2_GITHUB_CLIENT_ID: local-placeholder
OAUTH2_GITHUB_CLIENT_SECRET: local-placeholder
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./web/dist:/usr/share/nginx/html:ro
- ./web/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
environment:
SKILLHUB_API_UPSTREAM: http://server:8080
SKILLHUB_WEB_API_BASE_URL: ""
SKILLHUB_PUBLIC_BASE_URL: ""
depends_on:
server:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/nginx-health"]
interval: 10s
timeout: 5s
retries: 6
start_period: 10s
```
- [ ] **Step 2: Verify nginx.conf.template is compatible with nginx:alpine**
The existing `web/nginx.conf.template` uses `${SKILLHUB_API_UPSTREAM}` variable substitution. The official `nginx:alpine` image supports this via `envsubst` when templates are placed in `/etc/nginx/templates/` — files are processed at container startup. This is already the correct path in the compose file above.
No changes needed to `nginx.conf.template`.
- [ ] **Step 3: Commit**
```bash
git add docker-compose.staging.yml
git commit -m "feat(staging): add hybrid staging docker-compose"
```
---
## Chunk 4: Staging Makefile targets
### Task 4: Add staging targets to Makefile
**Files:**
- Modify: `Makefile` (will update `.PHONY` line again — appending to the changes from Chunk 2)
The `make staging` command will:
1. Build backend Docker image from source
2. Build frontend static files with `pnpm build`
3. Start dependency services (reuse dev docker-compose)
4. Start staging services (backend container + nginx with mounted dist)
5. Wait for health checks
6. Run smoke tests against `http://localhost:8080`
7. Print pass/fail summary
8. On failure: print logs and exit non-zero
**Note on Docker Compose project name:** Both `docker compose` invocations (step 3 and step 4) run from the same directory and use the same project name (default: directory name `skillhub`), so they share the same network and the `server` container can reach `postgres`/`redis`/`minio` by hostname.
- [ ] **Step 1: Add staging variables at top of Makefile**
After the existing `DEV_API_URL` line (line 9), add:
```makefile
STAGING_API_URL := http://localhost:8080
STAGING_WEB_URL := http://localhost
STAGING_SERVER_IMAGE := skillhub-server:staging
```
- [ ] **Step 1.5: Verify prerequisites**
Verify that `scripts/smoke-test.sh` exists and is executable:
```bash
ls -l scripts/smoke-test.sh
```
Expected: File exists and has execute permissions (`-rwxr-xr-x` or similar).
Also verify the backend Docker build context structure:
```bash
ls server/
```
Expected output should include: `pom.xml`, `mvnw`, `.mvn/`, `skillhub-app/`, `skillhub-domain/`, `skillhub-auth/`, `skillhub-search/`, `skillhub-infra/`, `skillhub-storage/`, and `Dockerfile`. These are all required by `server/Dockerfile`.
- [ ] **Step 2: Add staging targets to .PHONY line**
Add `staging staging-down staging-logs` to the `.PHONY` list on line 1 (appending to the changes from Chunk 2).
- [ ] **Step 3: Add staging targets after the dev-all-reset block**
After `dev-all-reset` target, add:
```makefile
staging: ## 构建并启动 staging 环境,运行 smoke test混合模式后端镜像 + 前端静态文件)
@echo "=== [1/5] Building backend Docker image ==="
docker build -t $(STAGING_SERVER_IMAGE) -f server/Dockerfile server
@echo "=== [2/5] Building frontend static files ==="
cd web && pnpm run build
@echo "=== [3/5] Starting dependency services ==="
docker compose up -d --wait
@echo "=== [4/5] Starting staging services ==="
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d --wait server web
@echo "=== [5/5] Running smoke tests ==="
@if bash scripts/smoke-test.sh $(STAGING_API_URL); then \
echo ""; \
echo "Staging passed. Environment is running:"; \
echo " Web UI: $(STAGING_WEB_URL)"; \
echo " Backend: $(STAGING_API_URL)"; \
echo ""; \
echo "Run 'make staging-down' to stop."; \
echo "Run 'make pr' to create a pull request."; \
else \
echo ""; \
echo "Smoke tests FAILED. Printing logs..."; \
docker compose -f docker-compose.yml -f docker-compose.staging.yml logs server; \
$(MAKE) staging-down; \
exit 1; \
fi
staging-down: ## 停止 staging 环境
docker compose -f docker-compose.yml -f docker-compose.staging.yml down --remove-orphans
staging-logs: ## 查看 staging 服务日志SERVICE=server|web默认 server
@SERVICE=$${SERVICE:-server}; \
docker compose -f docker-compose.yml -f docker-compose.staging.yml logs -f $$SERVICE
```
- [ ] **Step 4: Verify staging target syntax**
```bash
make help
```
Expected: `staging`, `staging-down`, `staging-logs` appear in the help output with their descriptions.
- [ ] **Step 5: Commit**
```bash
git add Makefile
git commit -m "feat(staging): add staging make targets for hybrid docker regression testing"
```
---
## Chunk 5: make pr target
### Task 5: Add make pr target to Makefile
**Files:**
- Modify: `Makefile` (will update `.PHONY` line again — appending to the changes from Chunks 2 and 4)
The `make pr` command will:
1. Check that `gh` CLI is installed and authenticated
2. Check for uncommitted changes and prompt to commit or abort
3. Push current branch to origin
4. Create a PR using `gh pr create` with auto-generated title from branch name and body template
**Note:** This target uses `read -r` for interactive prompts, so it requires an interactive terminal. It will not work in CI or non-interactive contexts.
- [ ] **Step 1: Add pr to .PHONY line**
Add `pr` to the `.PHONY` list on line 1 (appending to the changes from Chunks 2 and 4).
- [ ] **Step 2: Add pr target after staging-logs**
```makefile
pr: ## 推送当前分支并创建 Pull Request需要 gh CLI
@if ! command -v gh >/dev/null 2>&1; then \
echo "Error: gh CLI not found. Install from https://cli.github.com/"; \
exit 1; \
fi
@if ! gh auth status >/dev/null 2>&1; then \
echo "Error: gh CLI not authenticated. Run: gh auth login"; \
exit 1; \
fi
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
if [ "$$BRANCH" = "main" ] || [ "$$BRANCH" = "master" ]; then \
echo "Error: Cannot create PR from main/master branch."; \
exit 1; \
fi
@if ! git diff --quiet || ! git diff --cached --quiet; then \
echo "You have uncommitted changes:"; \
git status --short; \
echo ""; \
printf "Commit all changes before creating PR? [y/N] "; \
read -r answer; \
if [ "$$answer" = "y" ] || [ "$$answer" = "Y" ]; then \
git add -A; \
git commit -m "chore: pre-PR commit"; \
else \
echo "Aborted. Commit or stash your changes first."; \
exit 1; \
fi; \
fi
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
echo "Pushing branch $$BRANCH to origin..."; \
git push -u origin "$$BRANCH"
@echo "Creating pull request..."
@if gh pr view >/dev/null 2>&1; then \
echo "A pull request already exists for this branch:"; \
gh pr view --json url -q '.url'; \
exit 0; \
fi
@gh pr create --fill --web || gh pr create --fill
```
Note: `--fill` auto-populates title and body from commits. `--web` opens the browser for final editing; if that fails (non-interactive), falls back to CLI creation.
- [ ] **Step 3: Verify gh CLI is available**
```bash
gh --version
gh auth status
```
Expected: gh version output and authenticated status. If not installed, note in docs.
- [ ] **Step 4: Commit**
```bash
git add Makefile
git commit -m "feat(dev): add make pr target for automated pull request creation"
```
---
## Chunk 6: Documentation update
### Task 6: Update CONTRIBUTING / README with new workflow
**Files:**
- Modify: `README.md` (development workflow section)
- Create: `docs/dev-workflow.md`
- [ ] **Step 1: Create docs/dev-workflow.md**
Create the file `docs/dev-workflow.md` with the following content. The inner code blocks use standard triple-backtick fences; the outer `~~~` fence here is only for plan readability:
~~~markdown
# Development Workflow
This document describes the recommended workflow for developing SkillHub locally.
## Prerequisites
- Docker Desktop (for dependency services and staging)
- Java 21 (for running the backend locally)
- Node.js 22 + pnpm (for running the frontend locally)
- `gh` CLI (for creating pull requests): https://cli.github.com/
## Stage 1: Local Development (fast iteration)
Use this stage for active development — writing code, fixing bugs, iterating quickly.
### Start the full local stack
```bash
make dev-all
```
This starts:
- Dependency services (Postgres, Redis, MinIO) via Docker
- Backend (Spring Boot) directly on your machine at http://localhost:8080
- Frontend (Vite) directly on your machine at http://localhost:3000
### Hot reload
**Frontend:** Vite HMR is enabled by default. Save a file and the browser updates instantly.
**Backend:** Spring Boot DevTools is configured. After editing Java code:
1. In IntelliJ IDEA: press `Cmd+F9` (Build Project)
2. The backend restarts automatically in 3-8 seconds
3. Watch the terminal running `make dev-server` for the restart log
### Mock authentication
Two mock users are available in local mode (no password needed):
| User ID | Role | Header |
|---------------|-------------|----------------------------------|
| `local-user` | Regular user | `X-Mock-User-Id: local-user` |
| `local-admin` | Super admin | `X-Mock-User-Id: local-admin` |
### Useful commands
| Command | Description |
|----------------------|------------------------------------------|
| `make dev-all` | Start full local stack |
| `make dev-all-down` | Stop all local services |
| `make dev-status` | Check status of all services |
| `make dev-logs` | Tail backend logs |
| `SERVICE=frontend make dev-logs` | Tail frontend logs |
| `make dev-all-reset` | Full reset (clears data volumes) |
| `make db-reset` | Reset database only |
## Stage 2: Staging Regression (pre-PR validation)
Use this stage when a feature or bugfix is complete and you want to verify it works correctly in a Docker environment before pushing.
### What staging does
`make staging` runs a **hybrid** Docker environment:
- **Backend**: built as a Docker image from your local source
- **Frontend**: built as static files (`pnpm build`) and served by Nginx
- **Dependencies**: same Postgres/Redis/MinIO as local dev
This is faster than building both images but still validates the containerized backend and the production Nginx serving path.
### Run staging
```bash
make staging
```
This will:
1. Build the backend Docker image
2. Build the frontend static files
3. Start all services
4. Run smoke tests against the API
5. Print pass/fail summary
If all tests pass, the environment stays running at:
- Web UI: http://localhost
- Backend API: http://localhost:8080
### Stop staging
```bash
make staging-down
```
### View staging logs
```bash
make staging-logs # backend logs
SERVICE=web make staging-logs # nginx logs
```
## Stage 3: Create Pull Request
After staging passes:
```bash
make pr
```
This will:
1. Check for uncommitted changes (prompts to commit if any)
2. Push your branch to origin
3. Create a pull request using `gh pr create --fill`
The PR title and body are auto-populated from your commit messages.
> **Note:** `make pr` requires an interactive terminal. Do not use it in CI.
## Full workflow summary
```
make dev-all # start local dev
# ... write code, test in browser ...
make staging # regression test in Docker
make staging-down # stop staging
make pr # push + create PR
```
~~~
- [ ] **Step 2: Add workflow reference to README.md**
In `README.md`, find the `### Local Development` section (around line 67). After the `make help` line (around line 97), add:
```markdown
For the full development workflow (local dev → staging → PR), see [docs/dev-workflow.md](docs/dev-workflow.md).
```
- [ ] **Step 3: Commit**
```bash
git add docs/dev-workflow.md README.md
git commit -m "docs: add dev-workflow guide covering local dev, staging, and PR creation"
```
---
## Final verification
- [ ] **End-to-end test**
```bash
# 1. Start local dev
make dev-all
# Verify: http://localhost:3000 loads, http://localhost:8080/actuator/health returns UP
# 2. Edit a Java file, press Cmd+F9 in IntelliJ
# Verify: backend restarts in <10 seconds
# 3. Edit a React file and save
# Verify: browser updates without full reload
# 4. Stop local dev
make dev-all-down
# 5. Run staging
make staging
# Verify: smoke tests pass, http://localhost loads
# 6. Stop staging
make staging-down
# 7. Check help output
make help
# Verify: staging, staging-down, staging-logs, dev-status, dev-logs, pr all appear
```

View file

@ -1,4 +1,7 @@
**/target/
# Exclude target for multi-stage build (Dockerfile)
# But allow it for runtime build (Dockerfile.runtime) which needs pre-built JAR
# **/target/
**/.idea/
**/*.iml
.git/

19
server/Dockerfile.dev Normal file
View file

@ -0,0 +1,19 @@
# Development Dockerfile
# Expects pre-built JAR at skillhub-app/target/*.jar
# Used by: make staging (local development workflow)
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# Copy pre-built JAR from local build (relative to server/ directory)
COPY ./skillhub-app/target/*.jar app.jar
RUN chown -R app:app /app
USER app
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

View file

@ -73,6 +73,12 @@
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View file

@ -37,6 +37,9 @@ public class SkillRatingController extends BaseApiController {
public ApiResponse<SkillRatingStatusResponse> getUserRating(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
if (principal == null) {
return ok("response.success.read", new SkillRatingStatusResponse((short) 0, false));
}
Optional<Short> rating = skillRatingService.getUserRating(skillId, principal.userId());
return ok(
"response.success.read",

View file

@ -40,6 +40,9 @@ public class SkillStarController extends BaseApiController {
public ApiResponse<Boolean> checkStarred(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
if (principal == null) {
return ok("response.success.read", false);
}
boolean starred = skillStarService.isStarred(skillId, principal.userId());
return ok("response.success.read", starred);
}