From fb15c53920d20e2c1a31d61736ec86d940f5ae9d Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:20:38 +0800 Subject: [PATCH 1/8] docs(plan): add dev workflow optimization implementation plan --- .../2026-03-14-dev-workflow-optimization.md | 625 ++++++++++++++++++ 1 file changed, 625 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-14-dev-workflow-optimization.md diff --git a/docs/superpowers/plans/2026-03-14-dev-workflow-optimization.md b/docs/superpowers/plans/2026-03-14-dev-workflow-optimization.md new file mode 100644 index 00000000..cde70141 --- /dev/null +++ b/docs/superpowers/plans/2026-03-14-dev-workflow-optimization.md @@ -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 `` before the test dependencies: + +```xml + + org.springframework.boot + spring-boot-devtools + runtime + true + +``` + +- [ ] **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 `` 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 +``` From 797cfc52723a8639e310f2e01354aae7c9dbc418 Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:23:50 +0800 Subject: [PATCH 2/8] feat(dev): add Spring Boot DevTools for fast restart in local dev --- server/skillhub-app/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/skillhub-app/pom.xml b/server/skillhub-app/pom.xml index 3e9d6804..f7ddf6ca 100644 --- a/server/skillhub-app/pom.xml +++ b/server/skillhub-app/pom.xml @@ -73,6 +73,12 @@ org.springframework.session spring-session-data-redis + + org.springframework.boot + spring-boot-devtools + runtime + true + org.springframework.boot spring-boot-starter-test From 25477398b598d7b5407d2db1d032b2df7fcf8c4e Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:28:28 +0800 Subject: [PATCH 3/8] feat(dev): add dev-status and dev-logs make targets --- Makefile | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b266f3eb..2ab96494 100644 --- a/Makefile +++ b/Makefile @@ -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 DEV_DIR := .dev DEV_SERVER_PID := $(DEV_DIR)/server.pid @@ -105,6 +105,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 From 72b2802a21a23e37afe48a72218dfd81aa57ce81 Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:30:24 +0800 Subject: [PATCH 4/8] feat(staging): add hybrid staging docker-compose --- docker-compose.staging.yml | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docker-compose.staging.yml diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 00000000..864e5f02 --- /dev/null +++ b/docker-compose.staging.yml @@ -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 From 2f639f1098752b4e3dbc906db3a0d06b107d4dde Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:30:32 +0800 Subject: [PATCH 5/8] feat(staging): add staging and pr make targets --- Makefile | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2ab96494..0644b8f3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.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 +.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: ## 显示帮助 @@ -173,3 +176,76 @@ db-reset: ## 重置数据库 validate-release-config: ## 校验发布环境变量文件(默认 .env.release) ./scripts/validate-release-config.sh .env.release + +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 + +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 + From 175b6c44c91cf29abd449238f097753f25f64877 Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:31:47 +0800 Subject: [PATCH 6/8] docs: add dev-workflow guide covering local dev, staging, and PR creation --- README.md | 2 + docs/dev-workflow.md | 125 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/dev-workflow.md diff --git a/README.md b/README.md index 162a3836..077b0dda 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md new file mode 100644 index 00000000..609d2f50 --- /dev/null +++ b/docs/dev-workflow.md @@ -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 +``` From 5254cd155efd52dd3119571a3ef623ffb1c68191 Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 13:54:39 +0800 Subject: [PATCH 7/8] update --- .../skillhub/controller/portal/SkillRatingController.java | 3 +++ .../skillhub/controller/portal/SkillStarController.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java index c25713df..b9f1e022 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java @@ -37,6 +37,9 @@ public class SkillRatingController extends BaseApiController { public ApiResponse getUserRating( @PathVariable Long skillId, @AuthenticationPrincipal PlatformPrincipal principal) { + if (principal == null) { + return ok("response.success.read", new SkillRatingStatusResponse((short) 0, false)); + } Optional rating = skillRatingService.getUserRating(skillId, principal.userId()); return ok( "response.success.read", diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java index e9ce9709..4a3e77f1 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java @@ -40,6 +40,9 @@ public class SkillStarController extends BaseApiController { public ApiResponse 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); } From 5581bd6f06723ec2fce02e15f2ebdb47303450e0 Mon Sep 17 00:00:00 2001 From: wowo-zZ Date: Sat, 14 Mar 2026 14:21:06 +0800 Subject: [PATCH 8/8] feat(staging): optimize staging build with local Maven packaging - Add server/Dockerfile.dev for fast local builds (JRE-only, ~70MB) - Modify staging target to build JAR locally before Docker image - Update .dockerignore to allow target/ directory for Dockerfile.dev - Build time reduced from 17+ minutes to ~4 seconds - Reuses local Maven cache (~/.m2/repository) instead of downloading in Docker --- Makefile | 5 +++-- server/.dockerignore | 5 ++++- server/Dockerfile.dev | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 server/Dockerfile.dev diff --git a/Makefile b/Makefile index 0644b8f3..61e94697 100644 --- a/Makefile +++ b/Makefile @@ -178,8 +178,9 @@ validate-release-config: ## 校验发布环境变量文件(默认 .env.release ./scripts/validate-release-config.sh .env.release staging: ## 构建并启动 staging 环境,运行 smoke test(混合模式:后端镜像 + 前端静态文件) - @echo "=== [1/5] Building backend Docker image ===" - docker build -t $(STAGING_SERVER_IMAGE) -f server/Dockerfile server + @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 ===" diff --git a/server/.dockerignore b/server/.dockerignore index b7da8964..f993bd6a 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -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/ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev new file mode 100644 index 00000000..77f96fcc --- /dev/null +++ b/server/Dockerfile.dev @@ -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"]