Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
name: CI

on:
push:
branches:
- master
pull_request:
branches:
- master

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
NODE_VERSION: 24.18.0
PNPM_VERSION: 8.15.9

jobs:
backend-tests:
name: Backend tests
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: false

- name: Activate pnpm
shell: bash
run: |
set -Eeuo pipefail
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate

- name: Locate pnpm store
id: pnpm-store
shell: bash
run: |
set -Eeuo pipefail
echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Restore pnpm store
uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-backend-${{ hashFiles('wait-queue/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-backend-

- name: Install backend dependencies
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir wait-queue install --frozen-lockfile

- name: Run backend tests
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir wait-queue test

dashboard-typecheck:
name: Dashboard typecheck
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: false

- name: Activate pnpm
shell: bash
run: |
set -Eeuo pipefail
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate

- name: Locate pnpm store
id: pnpm-store
shell: bash
run: |
set -Eeuo pipefail
echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Restore pnpm store
uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-dashboard-${{ hashFiles('admin-dashboard/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-dashboard-

- name: Install dashboard dependencies
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir admin-dashboard install --frozen-lockfile

- name: Run dashboard typecheck
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir admin-dashboard typecheck

production-build:
name: Production build
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: false

- name: Activate pnpm
shell: bash
run: |
set -Eeuo pipefail
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate

- name: Locate pnpm store
id: pnpm-store
shell: bash
run: |
set -Eeuo pipefail
echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Restore pnpm store
uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-all-${{ hashFiles('wait-queue/pnpm-lock.yaml', 'admin-dashboard/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-node-${{ env.NODE_VERSION }}-pnpm-${{ env.PNPM_VERSION }}-all-

- name: Install all dependencies
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm install:all

- name: Build production artifacts
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm build

dependency-audit:
name: Dependency audit
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: false

- name: Activate pnpm
shell: bash
run: |
set -Eeuo pipefail
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate

- name: Audit backend production dependencies
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir wait-queue audit --prod --audit-level high

- name: Audit dashboard production dependencies
shell: bash
run: |
set -Eeuo pipefail
corepack pnpm --dir admin-dashboard audit --prod --audit-level high

compose-smoke:
name: Compose smoke
needs:
- backend-tests
- dashboard-typecheck
- production-build
- dependency-audit
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
COMPOSE_PROJECT_NAME: waitqueue-ci-${{ github.run_id }}-${{ github.run_attempt }}
BIND_ADDRESS: 127.0.0.1
API_PORT: 3000
DASHBOARD_PORT: 3001
DB_DATABASE: waitqueue_ci
DB_USER: waitqueue_ci
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Create ephemeral database credentials
shell: bash
run: |
set -Eeuo pipefail
db_password="$(openssl rand -hex 24)"
db_root_password="$(openssl rand -hex 24)"
echo "::add-mask::$db_password"
echo "::add-mask::$db_root_password"
{
echo "DB_PASSWORD=$db_password"
echo "DB_ROOT_PASSWORD=$db_root_password"
} >> "$GITHUB_ENV"

- name: Validate Compose configuration
shell: bash
run: |
set -Eeuo pipefail
docker compose config --quiet

- name: Build and start the stack
shell: bash
run: |
set -Eeuo pipefail
docker compose up --build --detach --wait --wait-timeout 300

- name: Verify HTTP health endpoints
shell: bash
run: |
set -Eeuo pipefail

health_json="$(curl --fail --silent --show-error --retry 10 --retry-all-errors --retry-delay 2 \
"http://127.0.0.1:${API_PORT}/waitqueue/health")"
jq --exit-status \
'.code == 0 and .data.status == "ok"' \
<<< "$health_json" > /dev/null

ready_json="$(curl --fail --silent --show-error --retry 10 --retry-all-errors --retry-delay 2 \
"http://127.0.0.1:${API_PORT}/waitqueue/ready")"
jq --exit-status \
'.code == 0 and .data.status == "ready" and .data.dependencies.mysql == "ok" and .data.dependencies.redis == "ok"' \
<<< "$ready_json" > /dev/null

dashboard_html="$(curl --fail --silent --show-error --retry 10 --retry-all-errors --retry-delay 2 \
"http://127.0.0.1:${DASHBOARD_PORT}/")"
grep --fixed-strings --quiet 'WaitQueue Control Room' <<< "$dashboard_html"

- name: Verify migration history and idempotency
shell: bash
run: |
set -Eeuo pipefail

expected_count=0
while IFS= read -r migration_path; do
migration_name="${migration_path##*/}"
if [[ "$migration_name" =~ ^V[1-9][0-9]*__.+\.sql$ ]]; then
expected_count=$((expected_count + 1))
fi
done < <(find wait-queue/sql -maxdepth 1 -type f -print)

if (( expected_count == 0 )); then
echo "No versioned migrations were discovered" >&2
exit 1
fi

before_count="$(docker compose exec -T mysql sh -eu -c '
MYSQL_PWD="$MYSQL_PASSWORD" mysql \
--batch --skip-column-names \
--user="$MYSQL_USER" "$MYSQL_DATABASE" \
--execute="SELECT COUNT(*) FROM waitqueue_schema_migrations"
')"

if [[ "$before_count" != "$expected_count" ]]; then
echo "Expected $expected_count migration records, found $before_count" >&2
exit 1
fi

rerun_output="$(docker compose run --rm --no-deps -T migrate)"
printf '%s\n' "$rerun_output"

if ! grep --extended-regexp --quiet '"applied":0([,}])' <<< "$rerun_output"; then
echo "The idempotency run did not report zero applied migrations" >&2
exit 1
fi

after_count="$(docker compose exec -T mysql sh -eu -c '
MYSQL_PWD="$MYSQL_PASSWORD" mysql \
--batch --skip-column-names \
--user="$MYSQL_USER" "$MYSQL_DATABASE" \
--execute="SELECT COUNT(*) FROM waitqueue_schema_migrations"
')"

if [[ "$after_count" != "$before_count" ]]; then
echo "Migration history changed on the idempotency run" >&2
exit 1
fi

- name: Print diagnostics on failure
if: failure()
shell: bash
run: |
set -Eeuo pipefail
docker compose ps --all || true
docker compose logs --no-color --timestamps || true

- name: Remove CI stack and volumes
if: always()
shell: bash
run: |
set -Eeuo pipefail
docker compose down --volumes --remove-orphans --timeout 20
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# waitqueue.js

[![CI](https://github.com/PeterGuy326/waitqueue.js/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/PeterGuy326/waitqueue.js/actions/workflows/ci.yml)

一个轻量的 HTTP 回调任务队列与并发调度器。业务系统只提交 `taskId`;WaitQueue 负责排队、并发占位和周期检查,真正的任务仍由业务回调服务执行。

![WaitQueue Control Room](docs/control-room.jpg)
Expand Down Expand Up @@ -243,6 +245,29 @@ Docker Compose 与这些快捷命令都会自动读取项目根目录的 `.env`

`wait-queue test` 会先重新编译,再使用 Node.js 内置 test runner 执行全部契约测试。

## CI 与贡献

所有提交到 `master` 的 push 和面向 `master` 的 Pull Request 都会运行以下稳定检查:

- `Backend tests`:安装后端锁定依赖并执行全部 Node.js 契约测试;
- `Dashboard typecheck`:独立安装控制台锁定依赖并执行 Next.js 类型生成与 TypeScript 检查;
- `Production build`:使用 Node.js 24.18.0 与 pnpm 8.15.9 构建前后端生产制品;
- `Dependency audit`:阻止前后端生产依赖中的 high / critical 已知漏洞;
- `Compose smoke`:等待以上四项并行检查通过后,在隔离的 Compose 项目和临时数据库凭据下启动完整栈,检查 API 存活/就绪、控制台可访问、迁移记录完整且重复迁移幂等,结束后删除测试容器与卷。

提交 PR 前建议先在仓库根目录运行:

```bash
corepack pnpm install:all
corepack pnpm test
corepack pnpm build
corepack pnpm --dir wait-queue audit --prod --audit-level high
corepack pnpm --dir admin-dashboard audit --prod --audit-level high
docker compose config --quiet
```

仓库管理员应在 GitHub `Settings → Branches → master` 的保护规则中启用 **Require status checks to pass before merging**,将 `Backend tests`、`Dashboard typecheck`、`Production build`、`Dependency audit` 与 `Compose smoke` 设为 required checks,并启用 **Require branches to be up to date before merging**。这样只有基于最新主干且全部检查通过的 PR 才能合入;CI 仅申请 `contents: read` 权限,不会发布制品或改写仓库。

## 配置

### 后端
Expand Down