skillhub/docs/superpowers/plans/2026-03-14-dev-workflow-optimization.md

20 KiB
Raw Permalink Blame History

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:

<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:

make dev

Expected: Docker services (Postgres, Redis, MinIO) start and become healthy.

Then in the same or a new terminal:

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
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).

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
make dev-status

Expected: Shows docker compose service status and backend/frontend process status.

# 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
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
# 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
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:

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:

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:

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:

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
make help

Expected: staging, staging-down, staging-logs appear in the help output with their descriptions.

  • Step 5: Commit
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
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
gh --version
gh auth status

Expected: gh version output and authenticated status. If not installed, note in docs.

  • Step 4: Commit
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:

# 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:

For the full development workflow (local dev → staging → PR), see [docs/dev-workflow.md](docs/dev-workflow.md).
  • Step 3: Commit
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
# 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