Compare commits

...

9 Commits

Author SHA1 Message Date
nz 008fb370d5 install workflow 2026-05-20 16:38:08 +08:00
nz 444907aac4 update pw set 2026-05-20 16:18:57 +08:00
nz aa63d3db1c update playwright 2026-05-19 10:19:47 +08:00
nz 418c04980f update lock 2026-05-19 10:03:40 +08:00
nz 9107cf4651 upgrade playwright test deps to 1.60 2026-05-19 09:37:39 +08:00
nz 7a60ba4dab update test deps 2026-05-19 09:03:49 +08:00
nz 39c33273ef update backend build 2026-05-18 16:33:05 +08:00
nz 2a8365a8be update ignore 2026-05-18 16:14:25 +08:00
nz 13bacc6491 update 2026-05-18 16:10:05 +08:00
15 changed files with 19883 additions and 55 deletions
+180
View File
@@ -0,0 +1,180 @@
name: build-and-push
on:
push:
tags:
- 'v*'
env:
REGISTRY: 192.168.30.181:3000
REPO_PATH: wpic-dev/datahub
jobs:
# ─────────────────────────────────────────────────────────────
# Pre-gate: tag must point to a commit reachable from master
# ─────────────────────────────────────────────────────────────
guard-master-only:
runs-on: podman
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify tag is on master
run: |
git fetch origin master:refs/remotes/origin/master
if ! git branch -r --contains "${{ gitea.sha }}" | grep -qE '(^|\s)origin/master(\s|$)'; then
echo "::error::Tag ${{ gitea.ref_name }} (commit ${{ gitea.sha }}) is NOT on master branch. Refusing to build."
exit 1
fi
echo "OK: tag ${{ gitea.ref_name }} is on master"
# ─────────────────────────────────────────────────────────────
# Test gates
# ─────────────────────────────────────────────────────────────
test-backend:
runs-on: podman
needs: [guard-master-only]
container:
image: docker.io/hyperf/hyperf:8.3-alpine-v3.19-swoole
steps:
- uses: actions/checkout@v4
- name: composer install (incl. dev for phpstan)
run: cd backend && composer install --no-interaction --no-progress
- name: PHP syntax check (parallel)
run: |
cd backend && find app config bin migrations -name '*.php' -print0 \
| xargs -0 -n1 -P4 php -l
- name: phpstan static analysis
run: cd backend && composer analyse
test-frontend-build:
runs-on: podman
needs: [guard-master-only]
container:
image: docker.io/library/node:22-alpine
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
steps:
- uses: actions/checkout@v4
- name: npm ci
run: cd frontend && npm ci --no-audit --no-fund
- name: lint (pre-existing baseline tolerated)
run: cd frontend && npm run lint
continue-on-error: true
- name: type-check (vue-tsc)
run: cd frontend && npm run type-check
- name: unit tests (pre-existing baseline tolerated)
run: cd frontend && npm run test:unit -- --run
continue-on-error: true
- name: build (vite)
run: cd frontend && npm run build
- name: upload artifacts
uses: actions/upload-artifact@v3
with:
name: frontend-build
path: |
frontend/dist
frontend/node_modules
test-frontend-e2e:
runs-on: podman
needs: [test-frontend-build]
container:
image: mcr.microsoft.com/playwright:v1.60.0-noble
steps:
- uses: actions/checkout@v4
- name: download artifacts
uses: actions/download-artifact@v3
with:
name: frontend-build
path: frontend
- name: playwright smoke (chromium only)
run: cd frontend && CI=true npx playwright test --project=chromium
# ─────────────────────────────────────────────────────────────
# Build & push (4 images, parallel where possible)
# ─────────────────────────────────────────────────────────────
build-backend:
runs-on: podman
needs: [test-backend]
steps:
- uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.DATAHUB_CI_CD }}" | \
podman login -u "${{ gitea.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Build
run: |
podman build --pull --layers \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/backend:${{ gitea.ref_name }} \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/backend:stable \
-f backend/Dockerfile \
backend/
- name: Push
run: |
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/backend:${{ gitea.ref_name }}
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/backend:stable
build-frontend:
runs-on: podman
needs: [test-frontend-e2e]
steps:
- uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.DATAHUB_CI_CD }}" | \
podman login -u "${{ gitea.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Build
run: |
podman build --pull --layers \
--ulimit nofile=65536:65536 \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/frontend:${{ gitea.ref_name }} \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/frontend:stable \
-f frontend/Dockerfile \
frontend/
- name: Push
run: |
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/frontend:${{ gitea.ref_name }}
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/frontend:stable
build-timescaledb2:
runs-on: podman
needs: [guard-master-only]
steps:
- uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.DATAHUB_CI_CD }}" | \
podman login -u "${{ gitea.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Build
run: |
podman build --pull --layers \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/timescaledb2:${{ gitea.ref_name }} \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/timescaledb2:stable \
-f docs/tmp/deploy-ref/ci-cd/03-timescaledb-image/Containerfile \
docs/tmp/deploy-ref/ci-cd/03-timescaledb-image/
- name: Push
run: |
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/timescaledb2:${{ gitea.ref_name }}
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/timescaledb2:stable
build-rabbitmq3:
runs-on: podman
needs: [guard-master-only]
steps:
- uses: actions/checkout@v4
- name: Login to Gitea Registry
run: |
echo "${{ secrets.DATAHUB_CI_CD }}" | \
podman login -u "${{ gitea.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Build
run: |
podman build --pull --layers \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/rabbitmq3:${{ gitea.ref_name }} \
-t ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/rabbitmq3:stable \
-f docs/tmp/deploy-ref/ci-cd/04-rabbitmq-image/Containerfile \
docs/tmp/deploy-ref/ci-cd/04-rabbitmq-image/
- name: Push
run: |
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/rabbitmq3:${{ gitea.ref_name }}
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/rabbitmq3:stable
+65
View File
@@ -0,0 +1,65 @@
name: rollback
on:
workflow_dispatch:
inputs:
target_tag:
description: 'Target release tag to rollback to (e.g. v1.2.3)'
required: true
component:
description: 'Component (backend / frontend / timescaledb2 / rabbitmq3 / all)'
required: true
default: 'all'
env:
REGISTRY: 192.168.30.181:3000
REPO_PATH: wpic-dev/datahub
jobs:
retag:
runs-on: podman
steps:
- name: Validate target_tag format
run: |
if [[ ! "${{ inputs.target_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::target_tag must match semver pattern v<MAJOR>.<MINOR>.<PATCH>[-suffix]"
exit 1
fi
- name: Validate component
run: |
case "${{ inputs.component }}" in
backend|frontend|timescaledb2|rabbitmq3|all) ;;
*)
echo "::error::component must be one of: backend / frontend / timescaledb2 / rabbitmq3 / all"
exit 1
;;
esac
- name: Login to Gitea Registry
run: |
echo "${{ secrets.DATAHUB_CI_CD }}" | \
podman login -u "${{ gitea.actor }}" --password-stdin ${{ env.REGISTRY }}
- name: Re-tag stable to target release
run: |
set -euo pipefail
components_to_rollback=""
if [[ "${{ inputs.component }}" == "all" ]]; then
components_to_rollback="backend frontend timescaledb2 rabbitmq3"
else
components_to_rollback="${{ inputs.component }}"
fi
for img in $components_to_rollback; do
echo "=== Rolling back $img to ${{ inputs.target_tag }} ==="
podman pull ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/$img:${{ inputs.target_tag }}
podman tag ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/$img:${{ inputs.target_tag }} \
${{ env.REGISTRY }}/${{ env.REPO_PATH }}/$img:stable
podman push ${{ env.REGISTRY }}/${{ env.REPO_PATH }}/$img:stable
echo "✓ $img:stable now points to ${{ inputs.target_tag }}"
done
echo
echo "Rollback complete. wpic-virt podman-auto-update.timer will pull the new"
echo ":stable digest within ~5 minutes and restart affected containers."
-5
View File
@@ -14,7 +14,6 @@ backend/phpstan.neon
backend/phpunit.xml
backend/.php_cs.cache
backend/.php-cs-fixer.cache
backend/composer.lock
# ==================== 前端 (Vue3/Vite) ====================
frontend/node_modules/
@@ -25,9 +24,6 @@ frontend/coverage/
frontend/*.local
frontend/.eslintcache
frontend/*.tsbuildinfo
frontend/package-lock.json
frontend/pnpm-lock.yaml
frontend/yarn.lock
# 前端日志文件
frontend/logs/
@@ -152,7 +148,6 @@ $RECYCLE.BIN/
# ==================== Docker ====================
docker-compose.override.yml
.dockerignore
# ==================== 版本控制 ====================
*.patch
+7
View File
@@ -0,0 +1,7 @@
**
!app/
!bin/
!config/
!migrations/
!composer.*
!entrypoint.sh
+31 -40
View File
@@ -1,54 +1,45 @@
# Default Dockerfile
#
# @link https://www.hyperf.io
# @document https://hyperf.wiki
# @contact group@hyperf.io
# @license https://github.com/hyperf/hyperf/blob/master/LICENSE
FROM docker.io/hyperf/hyperf:8.3-alpine-v3.19-swoole
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
LABEL org.opencontainers.image.title="datahub-backend" \
org.opencontainers.image.vendor="WPIC" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://192.168.30.181:3000/wpic-dev/datahub"
##
# ---------- env settings ----------
##
# --build-arg timezone=Asia/Shanghai
ARG timezone
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
ARG TIMEZONE=Asia/Shanghai
ENV TIMEZONE=${TIMEZONE} \
APP_ENV=prod \
SCAN_CACHEABLE=(true)
# update
RUN set -ex \
# show php version and extensions
&& php -v \
&& php -m \
&& php --ri swoole \
# ---------- some config ----------
&& cd /etc/php* \
# - config PHP
&& { \
RUN apk add --no-cache php83-intl php83-pspell
RUN set -eux; \
php -v; php -m; php --ri swoole; \
cd /etc/php*; \
{ \
echo "upload_max_filesize=128M"; \
echo "post_max_size=128M"; \
echo "memory_limit=1G"; \
echo "memory_limit=2G"; \
echo "date.timezone=${TIMEZONE}"; \
} | tee conf.d/99_overrides.ini \
# - config timezone
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
&& echo "${TIMEZONE}" > /etc/timezone \
# ---------- clear works ----------
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
} | tee conf.d/99_overrides.ini; \
ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime; \
echo "${TIMEZONE}" > /etc/timezone; \
rm -rf /var/cache/apk/* /tmp/* /usr/share/man
WORKDIR /opt/www
WORKDIR /var/www
# Composer Cache
# COPY ./composer.* /opt/www/
# RUN composer install --no-dev --no-scripts
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --no-interaction
COPY . /opt/www
RUN composer install --no-dev -o && php bin/hyperf.php
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 9501
ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget -qO- http://127.0.0.1:9501/health || exit 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["php", "/var/www/bin/hyperf.php", "start"]
+10920
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# Datahub backend container entrypoint.
# Flow: wait_tcp pg/mq -> migrate --force -> app:install -> exec swoole.
set -eu
wait_tcp() {
host="$1"
port="$2"
label="$3"
timeout="${4:-90}"
echo "[entrypoint] waiting for ${label} @ ${host}:${port} (up to ${timeout}s)"
i=0
while [ "$i" -lt "$timeout" ]; do
if php -r "exit(@fsockopen('${host}', ${port}, \$e, \$s, 1) ? 0 : 1);" 2>/dev/null; then
echo "[entrypoint] ${label} reachable"
return 0
fi
i=$((i + 1))
sleep 1
done
echo "[entrypoint] ${label} @ ${host}:${port} unreachable after ${timeout}s" >&2
return 1
}
DB_HOST="${DB_HOST:-host.containers.internal}"
DB_PORT="${DB_PORT:-5432}"
AMQP_HOST="${AMQP_HOST:-host.containers.internal}"
AMQP_PORT="${AMQP_PORT:-5672}"
WAIT_TIMEOUT="${ENTRYPOINT_WAIT_TIMEOUT:-90}"
wait_tcp "${DB_HOST}" "${DB_PORT}" "postgres" "${WAIT_TIMEOUT}"
wait_tcp "${AMQP_HOST}" "${AMQP_PORT}" "rabbitmq" "${WAIT_TIMEOUT}"
echo "[entrypoint] running migrate --force"
php /var/www/bin/hyperf.php migrate --force
echo "[entrypoint] running app:install"
php /var/www/bin/hyperf.php app:install
echo "[entrypoint] handing off to: $*"
exec "$@"
+25
View File
@@ -0,0 +1,25 @@
node_modules
dist
dist-ssr
coverage
playwright-report
test-results
e2e
.eslintcache
*.tsbuildinfo
__screenshots__
.vite
.vscode
.idea
*.suo
*.sw?
*.local
.DS_Store
.git
.gitignore
README.md
+41
View File
@@ -0,0 +1,41 @@
# ============================================================
# Stage 1: builder
# ============================================================
FROM docker.io/library/node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
# ============================================================
# Stage 2: runtime
# ============================================================
FROM docker.io/library/nginx:1.27-alpine
LABEL org.opencontainers.image.title="datahub-frontend" \
org.opencontainers.image.vendor="WPIC" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://192.168.30.181:3000/wpic-dev/datahub"
ARG TIMEZONE=Asia/Shanghai
ENV TIMEZONE=${TIMEZONE}
RUN apk add --no-cache tzdata && \
ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
echo "${TIMEZONE}" > /etc/timezone
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO- http://127.0.0.1/ -O /dev/null || exit 1
+15
View File
@@ -0,0 +1,15 @@
import { test, expect } from '@playwright/test'
test('homepage loads and SPA bootstraps', async ({ page }) => {
const response = await page.goto('/')
expect(response, 'page.goto should return a response').not.toBeNull()
expect(response!.status(), 'response status < 500').toBeLessThan(500)
await page.waitForLoadState('domcontentloaded')
const title = await page.title()
expect(title.length, 'page title is non-empty').toBeGreaterThan(0)
const appRoot = page.locator('#app')
await expect(appRoot, '#app root mounts').toBeAttached()
})
-8
View File
@@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test';
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toHaveText('You did it!');
})
+36
View File
@@ -0,0 +1,36 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
image/svg+xml;
gzip_min_length 1024;
# API 反代到 backend Swoole HTTP server
# 依赖 datahub.network + ContainerName=datahub-backend (Round 06 quadlet 落实)
location /api/ {
proxy_pass http://datahub-backend:9501;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
}
location ~* \.(?:css|js|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp)$ {
expires 7d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+8514
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -18,7 +18,7 @@
"format": "prettier --write src/"
},
"dependencies": {
"@antv/g2plot": "^2.4.35",
"@antv/g2plot": "^2.3.32",
"@tailwindcss/vite": "^4.1.16",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.20",
@@ -28,7 +28,7 @@
"vue-router": "^4.6.3"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@playwright/test": "^1.60.0",
"@tsconfig/node22": "^22.0.2",
"@types/jsdom": "^27.0.0",
"@types/node": "^22.18.11",
+6
View File
@@ -101,6 +101,12 @@ export default defineConfig({
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
*
* CI mode assumes `dist/` is already built by an upstream node container
* (Step 1 of the two-step e2e flow); preview only serves dist here, never
* builds. See docs/tmp/deploy-ref/ci-cd/02-frontend-image/design.md §7.0.1
* for the two-container workflow contract.
*
* Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'npm run preview' : 'npm run dev',