Compare commits

..

4 Commits

Author SHA1 Message Date
xiaoxia 1a5bb1ab43 feat(ci): optimize web image build with multi-stage Dockerfile + buildx cache
Tests / test (pull_request) Failing after 0s
Tests / lint (pull_request) Failing after 0s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1560h16m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1560h16m28s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1560h16m30s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1560h16m30s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1560h48m3s
2026-07-13 15:11:28 +08:00
xiaoxia bf47e69fa4 feat(ci): add node_modules cache for frontend-lint job 2026-07-13 15:10:44 +08:00
xiaoxia b947bf7569 feat(ci): add node_modules cache for frontend-lint job 2026-07-13 15:09:47 +08:00
xiaoxia f110bc6f16 feat: web.Dockerfile add buildkit cache for npm install 2026-07-13 15:08:49 +08:00
4 changed files with 188 additions and 15 deletions
+13 -2
View File
@@ -22,7 +22,7 @@ permissions:
jobs:
validate:
name: Validate Code Quality And Tests
runs-on: saas
runs-on: ubuntu-22.04
env:
DATABASE_URL: postgresql+psycopg://postgres:postgres@127.0.0.1:5433/xiaoxia_saas
@@ -191,7 +191,8 @@ jobs:
frontend-lint:
name: Frontend Lint
runs-on: saas
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Checkout code
@@ -217,7 +218,17 @@ jobs:
tar -xzf /tmp/repo.tar.gz --strip-components=1 -C .
rm -f /tmp/repo.tar.gz
- name: Cache node_modules
id: cache-node-modules
uses: actions/cache@v4
with:
path: apps/web/node_modules
key: ${{ runner.os }}-node-web-${{ hashFiles('apps/web/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-web-
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: sh
run: |
set -eu
+163
View File
@@ -0,0 +1,163 @@
name: Tests
on:
pull_request:
branches: [ main ]
jobs:
test:
runs-on: runtime-builder
steps:
- name: Checkout code
shell: sh
run: |
set -eu
python - <<'PY'
import io
import os
import tarfile
import time
import urllib.error
import urllib.request
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
# Retry up to 5 times with backoff for transient 5xx errors
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
PY
- name: Show Python version
shell: sh
run: |
set -eu
python --version
python -m pip --version
- name: Install dependencies
shell: sh
run: |
set -eu
python -m pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
python -m pip install -r requirements.txt -r requirements-dev.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
- name: Run unit tests
shell: sh
run: |
set -eu
PYTHONPATH="$PWD/apps/api:$PWD" python -m pytest tests/unit -q
- name: Run integration tests
shell: sh
run: |
set -eu
PYTHONPATH="$PWD/apps/api:$PWD" python -m pytest tests/integration -q --timeout=60 -x
lint:
runs-on: runtime-builder
steps:
- name: Checkout code
shell: sh
run: |
set -eu
python - <<'PY'
import io
import os
import tarfile
import time
import urllib.error
import urllib.request
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
# Retry up to 5 times with backoff for transient 5xx errors
last_err = None
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=120) as response:
archive = response.read()
break
except urllib.error.HTTPError as e:
last_err = e
if e.code >= 500 and attempt < 4:
wait = 2 ** attempt
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
except Exception as e:
last_err = e
if attempt < 4:
wait = 2 ** attempt
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
time.sleep(wait)
continue
raise
else:
raise last_err
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
for member in tar.getmembers():
name = member.name
if name == root_prefix[:-1]:
continue
if name.startswith(root_prefix):
member.name = name[len(root_prefix):]
if member.name:
tar.extract(member, '.')
PY
- name: Install dependencies
shell: sh
run: |
set -eu
python -m pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
python -m pip install -r requirements.txt -r requirements-dev.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
- name: Run Black (check only)
shell: sh
run: |
set -eu
python -m black --check alembic apps packages tests scripts
- name: Run Flake8
shell: sh
run: |
set -eu
python -m flake8 apps packages tests --count --statistics
+3 -1
View File
@@ -5,7 +5,9 @@ ARG VITE_API_URL=https://saas-api.xiaoxiajianji.com
ENV VITE_API_URL=$VITE_API_URL
COPY apps/web/package.json apps/web/package-lock.json ./apps/web/
WORKDIR /app/apps/web
RUN npm config set registry https://registry.npmmirror.com \
RUN \
--mount=type=cache,target=/root/.npm \
npm config set registry https://registry.npmmirror.com \
&& npm ci
COPY apps/web/ ./
RUN npm run build
+9 -12
View File
@@ -72,28 +72,25 @@ else
fi
echo "=== Building Web image (with buildx cache) ==="
# 先构建前端产物
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace/apps/web \
docker.m.daocloud.io/library/node:20 \
sh -lc "npm ci && npm run build"
test -f apps/web/dist/index.html
# 使用多阶段 Dockerfile 构建,配合 buildx cache 缓存 npm 安装和构建产物
WEB_NGINX_CONF="${WEB_NGINX_CONF:-infra/docker/nginx-production.conf}"
WEB_API_URL="${VITE_API_URL:-https://saas-api.xiaoxiajianji.com}"
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},mode=max" \
-f infra/docker/web-artifact.Dockerfile \
--build-arg NGINX_CONF=infra/docker/nginx-production.conf \
-f infra/docker/web.Dockerfile \
--build-arg NGINX_CONF="${WEB_NGINX_CONF}" \
--build-arg VITE_API_URL="${WEB_API_URL}" \
-t "$WEB_IMAGE" \
--load \
.
else
docker build --pull=false \
-f infra/docker/web-artifact.Dockerfile \
--build-arg NGINX_CONF=infra/docker/nginx-production.conf \
-f infra/docker/web.Dockerfile \
--build-arg NGINX_CONF="${WEB_NGINX_CONF}" \
--build-arg VITE_API_URL="${WEB_API_URL}" \
-t "$WEB_IMAGE" \
.
fi