mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-14 21:07:36 +00:00
refactor: 统一多前端工程架构、插件配置与 CI (#131)
* refactor: 重构项目架构 * refactor: 重构ci和测试 * fix: 修复测试 * feat: 新增更多E2E测试 * fix: 修复E2E测试
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
pytest==9.1.1
|
||||
PyYAML==6.0.3
|
||||
ruff==0.16.1
|
||||
@@ -0,0 +1,2 @@
|
||||
# CI 脚本和测试与后端使用相同的 Python 风格。
|
||||
extend = "../ruoyi-fastapi-backend/ruff.toml"
|
||||
@@ -0,0 +1,72 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CONFIGURATIONS = (
|
||||
('docker.env', 'docker-compose.my.yml', 'my'),
|
||||
('docker.env', 'docker-compose.pg.yml', 'pg'),
|
||||
('ruoyi-fastapi-test/docker.env', 'ruoyi-fastapi-test/docker-compose.test.my.yml', 'my'),
|
||||
('ruoyi-fastapi-test/docker.env', 'ruoyi-fastapi-test/docker-compose.test.pg.yml', 'pg'),
|
||||
)
|
||||
|
||||
|
||||
def validate_configuration(config: dict[str, Any], framework: str, engine: str, root: Path) -> None:
|
||||
"""
|
||||
核对 Compose 实际解析后的前后端框架及 nginx 来源。
|
||||
|
||||
:param config: docker compose config 的 JSON 结果
|
||||
:param framework: 本次选择的框架
|
||||
:param engine: my 或 pg
|
||||
:param root: 仓库根目录
|
||||
:return: None
|
||||
"""
|
||||
expected = (root / 'ruoyi-fastapi-frontend' / framework / 'web').resolve()
|
||||
services = config['services']
|
||||
frontend = services['ruoyi-frontend']
|
||||
build = frontend['build']
|
||||
if Path(build['context']).resolve() != expected:
|
||||
raise ValueError(f'{framework}: 前端构建目录不一致')
|
||||
if not (expected / build.get('dockerfile', 'Dockerfile')).is_file():
|
||||
raise ValueError(f'{framework}: 前端 Dockerfile 不存在')
|
||||
volumes = [
|
||||
volume for volume in frontend.get('volumes', []) if volume.get('target') == '/etc/nginx/conf.d/default.conf'
|
||||
]
|
||||
nginx = expected / 'bin' / f'nginx.docker{engine}.conf'
|
||||
if len(volumes) != 1 or Path(volumes[0]['source']).resolve() != nginx or not nginx.is_file():
|
||||
raise ValueError(f'{framework}: nginx 配置未选择同一框架')
|
||||
backend = services[f'ruoyi-backend-{engine}']
|
||||
if backend['environment'].get('RUOYI_PLUGIN_FRONTEND_FRAMEWORK') != framework:
|
||||
raise ValueError(f'{framework}: 后端插件框架与前端不一致')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
使用两种框架解析全部生产和测试 Compose,无需启动容器。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--docker', default='docker')
|
||||
args = parser.parse_args()
|
||||
for framework in ('vue2', 'vue3'):
|
||||
for env_file, compose_file, engine in CONFIGURATIONS:
|
||||
result = subprocess.run(
|
||||
[args.docker, 'compose', '--env-file', env_file, '-f', compose_file, 'config', '--format', 'json'],
|
||||
cwd=ROOT,
|
||||
env={**os.environ, 'FRONTEND_FRAMEWORK': framework},
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
timeout=60,
|
||||
)
|
||||
validate_configuration(json.loads(result.stdout), framework, engine, ROOT)
|
||||
print(f'{framework}: {compose_file} PASS')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_report(path: Path, *, allow_skips: bool = False) -> tuple[int, int]:
|
||||
"""
|
||||
验证测试确实执行,专用验收任务不允许缺少依赖而全部跳过。
|
||||
|
||||
:param path: pytest JUnit 报告路径
|
||||
:param allow_skips: 是否允许普通跨平台回归中的预期跳过
|
||||
:return: 实际执行及跳过的测试数量
|
||||
"""
|
||||
root = ET.parse(path).getroot()
|
||||
cases = root.findall('.//testcase')
|
||||
skipped = sum(case.find('skipped') is not None for case in cases)
|
||||
executed = len(cases) - skipped
|
||||
failures = sum(case.find('failure') is not None or case.find('error') is not None for case in cases)
|
||||
if executed == 0 or failures or (skipped and not allow_skips):
|
||||
raise ValueError(f'{path}: 执行 {executed},跳过 {skipped},失败 {failures}')
|
||||
return executed, skipped
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
检查报告并将执行数量写入 CI 摘要。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('report', type=Path)
|
||||
parser.add_argument('--allow-skips', action='store_true')
|
||||
args = parser.parse_args()
|
||||
executed, skipped = check_report(args.report, allow_skips=args.allow_skips)
|
||||
print(f'{args.report}: executed={executed}, skipped={skipped}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,245 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FRAMEWORKS = ('vue2', 'vue3')
|
||||
PYTHON_VERSIONS = ('3.10', '3.11', '3.12', '3.13')
|
||||
CATEGORIES = ('quality', 'backend', 'frontend', 'integration', 'e2e', 'native')
|
||||
FRONTEND_FILE_PATH_PARTS = 4
|
||||
|
||||
|
||||
# 将影响范围规则集中列出,便于逐条审核;拆散分支会掩盖任务之间的联动。
|
||||
def build_plan(paths: list[str], *, full: bool = False) -> dict[str, Any]: # noqa: PLR0912, PLR0915
|
||||
"""
|
||||
根据源码、共享依赖及测试的影响范围生成 CI 执行计划。
|
||||
|
||||
:param paths: 变更文件,重命名需要同时传入旧路径和新路径
|
||||
:param full: 是否执行完整兼容矩阵
|
||||
:return: 分类开关、矩阵和必跑任务
|
||||
"""
|
||||
projects: set[tuple[str, str]] = set()
|
||||
browsers: set[str] = set()
|
||||
networks: set[str] = set()
|
||||
backend = native = database = release = False
|
||||
normalized = [path.replace('\\', '/').removeprefix('./') for path in paths]
|
||||
shared = full or any(
|
||||
path.startswith(('.github/', 'ruoyi-fastapi-backend/requirements'))
|
||||
or path
|
||||
in (
|
||||
'ruoyi-fastapi-backend/pyproject.toml',
|
||||
'ruoyi-fastapi-backend/tests/conftest.py',
|
||||
'ruoyi-fastapi-test/requirements.txt',
|
||||
'.gitignore',
|
||||
)
|
||||
for path in normalized
|
||||
)
|
||||
if shared:
|
||||
projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
|
||||
browsers.update(FRAMEWORKS)
|
||||
networks.update(FRAMEWORKS)
|
||||
backend = native = database = release = True
|
||||
for path in normalized:
|
||||
if path.lower().endswith('.md'):
|
||||
continue
|
||||
parts = path.split('/')
|
||||
if path.startswith('ruoyi-fastapi-backend/'):
|
||||
backend = True
|
||||
relative = path.removeprefix('ruoyi-fastapi-backend/')
|
||||
if not relative.startswith(('tests/', 'ruff.toml')):
|
||||
browsers.update(FRAMEWORKS)
|
||||
if relative.startswith(('common/', 'config/', 'utils/', 'sql/', 'scripts/verify_timezone_database')):
|
||||
database = True
|
||||
if relative.startswith(
|
||||
(
|
||||
'plugins/',
|
||||
'module_plugin/',
|
||||
'cli/',
|
||||
'tests/plugins/',
|
||||
'tests/cli/',
|
||||
'tests/module_plugin/',
|
||||
'tests/sql/',
|
||||
)
|
||||
):
|
||||
native = release = True
|
||||
if relative == 'scripts/build_native_plugin.py':
|
||||
native = True
|
||||
if relative.startswith(
|
||||
('plugins/', 'module_plugin/', 'common/', 'config/', 'middlewares/', 'utils/')
|
||||
) or relative in ('server.py', 'app.py', 'module_admin/service/login_service.py'):
|
||||
networks.update(FRAMEWORKS)
|
||||
if relative.startswith(('config/', 'sql/', 'scripts/plugin_', 'tests/server/')) or relative in (
|
||||
'server.py',
|
||||
'app.py',
|
||||
):
|
||||
release = True
|
||||
if relative.startswith('tests/plugins/integration/') or relative in (
|
||||
'tests/scheduler_helpers.py',
|
||||
'tests/plugins/core/runtime/test_browser_session.py',
|
||||
):
|
||||
networks.update(FRAMEWORKS)
|
||||
if relative in (
|
||||
'plugins/core/frontend.py',
|
||||
'plugins/core/environment.py',
|
||||
'plugins/core/manifest/schema.py',
|
||||
):
|
||||
projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
|
||||
if relative.startswith('plugins/examples/python/bundle_demo/web/'):
|
||||
projects.update((framework, 'web') for framework in FRAMEWORKS)
|
||||
elif (
|
||||
len(parts) >= FRONTEND_FILE_PATH_PARTS
|
||||
and parts[0] == 'ruoyi-fastapi-frontend'
|
||||
and parts[1] in FRAMEWORKS
|
||||
and parts[2] in ('web', 'mobile')
|
||||
):
|
||||
framework, target = parts[1:3]
|
||||
projects.add((framework, target))
|
||||
if target == 'web':
|
||||
browsers.add(framework)
|
||||
networks.add(framework)
|
||||
if target == 'web' and (parts[-1].startswith('pluginBridge') or parts[-1] == 'package.json'):
|
||||
backend = native = release = True
|
||||
elif path.startswith('ruoyi-fastapi-frontend/'):
|
||||
projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
|
||||
browsers.update(FRAMEWORKS)
|
||||
networks.update(FRAMEWORKS)
|
||||
elif path.startswith('ruoyi-fastapi-test/frontend/'):
|
||||
projects.update((framework, 'mobile') for framework in FRAMEWORKS)
|
||||
elif path.startswith('ruoyi-fastapi-test/time-contract/'):
|
||||
projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
|
||||
database = True
|
||||
browsers.update(FRAMEWORKS)
|
||||
elif path.startswith('ruoyi-fastapi-test/') or path in (
|
||||
'docker.env',
|
||||
'docker-compose.my.yml',
|
||||
'docker-compose.pg.yml',
|
||||
):
|
||||
browsers.update(FRAMEWORKS)
|
||||
if 'docker' in path or path.endswith('.sql'):
|
||||
database = True
|
||||
|
||||
required = {
|
||||
'quality': True,
|
||||
'backend': backend,
|
||||
'frontend': bool(projects),
|
||||
'integration': database or release or bool(networks),
|
||||
'e2e': bool(browsers),
|
||||
'native': native,
|
||||
}
|
||||
return {
|
||||
'full': full,
|
||||
'required': required,
|
||||
'frontend_matrix': {
|
||||
'include': [{'framework': framework, 'target': target} for framework, target in sorted(projects)]
|
||||
},
|
||||
'e2e_matrix': {
|
||||
'include': [
|
||||
{'framework': framework, 'database': engine, 'python': version}
|
||||
for framework in sorted(browsers)
|
||||
for engine in ('mysql', 'postgresql')
|
||||
for version in (PYTHON_VERSIONS if full else ('3.12',))
|
||||
]
|
||||
},
|
||||
'network_frameworks': sorted(networks),
|
||||
'database': database,
|
||||
'release': release,
|
||||
'paths': normalized,
|
||||
}
|
||||
|
||||
|
||||
def check_results(plan: dict[str, Any], needs: dict[str, Any]) -> list[str]:
|
||||
"""
|
||||
核验必跑任务,拒绝失败、取消、缺失和意外跳过。
|
||||
|
||||
:param plan: 选路任务生成的计划
|
||||
:param needs: GitHub needs 上下文
|
||||
:return: 不满足门禁的原因
|
||||
"""
|
||||
errors = []
|
||||
if needs.get('plan', {}).get('result') != 'success':
|
||||
errors.append('CI 计划未成功生成')
|
||||
required = plan.get('required', {})
|
||||
if set(required) != set(CATEGORIES) or not all(isinstance(value, bool) for value in required.values()):
|
||||
return [*errors, 'CI 计划缺少完整的任务分类']
|
||||
for category in CATEGORIES:
|
||||
result = needs.get(category, {}).get('result')
|
||||
allowed = ('success',) if required[category] else ('success', 'skipped')
|
||||
if result not in allowed:
|
||||
errors.append(f'{category}: {result or "missing"},预期 {"/".join(allowed)}')
|
||||
return errors
|
||||
|
||||
|
||||
def changed_paths(base: str) -> list[str]:
|
||||
"""
|
||||
从 Git 获取变更文件,保留删除及重命名前后的路径。
|
||||
|
||||
:param base: 已检出的比较基准提交
|
||||
:return: 相对仓库根目录的路径
|
||||
"""
|
||||
result = subprocess.run(
|
||||
['git', 'diff', '--no-renames', '--name-only', '-z', base, 'HEAD'],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
)
|
||||
return [path for path in result.stdout.decode('utf-8').split('\0') if path]
|
||||
|
||||
|
||||
def write_outputs(plan: dict[str, Any]) -> None:
|
||||
"""
|
||||
输出 GitHub 任务参数及可审阅的执行摘要。
|
||||
|
||||
:param plan: 本次运行计划
|
||||
:return: None
|
||||
"""
|
||||
payload = {
|
||||
**plan['required'],
|
||||
**{key: value for key, value in plan.items() if key not in ('required', 'paths')},
|
||||
'plan_json': plan,
|
||||
}
|
||||
if output := os.environ.get('GITHUB_OUTPUT'):
|
||||
with Path(output).open('a', encoding='utf-8') as stream:
|
||||
for key, value in payload.items():
|
||||
stream.write(f'{key}={json.dumps(value, ensure_ascii=False, separators=(",", ":"))}\n')
|
||||
if summary := os.environ.get('GITHUB_STEP_SUMMARY'):
|
||||
with Path(summary).open('a', encoding='utf-8') as stream:
|
||||
stream.write('## CI 执行计划\n\n| 分类 | 执行 |\n|---|---|\n')
|
||||
for category, required in plan['required'].items():
|
||||
stream.write(f'| {category} | {"运行" if required else "无相关改动"} |\n')
|
||||
stream.write(f'\n完整矩阵:{plan["full"]}\n')
|
||||
print(json.dumps(plan, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
执行变更选路或最终结果校验。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('command', choices=('plan', 'gate'))
|
||||
args = parser.parse_args()
|
||||
if args.command == 'gate':
|
||||
plan = json.loads(os.environ.get('PLAN_JSON') or '{}')
|
||||
needs = json.loads(os.environ.get('NEEDS_JSON') or '{}')
|
||||
errors = check_results(plan, needs)
|
||||
if errors:
|
||||
raise SystemExit('\n'.join(errors))
|
||||
print('所有应执行的 CI 分类均已通过。')
|
||||
return
|
||||
event = json.loads(Path(os.environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8'))
|
||||
event_name = os.environ['GITHUB_EVENT_NAME']
|
||||
full = event_name in ('push', 'schedule') or (
|
||||
str(event.get('inputs', {}).get('full', 'true')).lower() == 'true' and event_name == 'workflow_dispatch'
|
||||
)
|
||||
base = event.get('pull_request', {}).get('base', {}).get('sha', '')
|
||||
paths = changed_paths(base) if base else ['.github/workflows/ci.yml']
|
||||
write_outputs(build_plan(paths, full=full))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
从 AI 插件清单安装测试依赖,避免 CI 维护第二份版本列表。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
backend = Path(__file__).resolve().parents[2] / 'ruoyi-fastapi-backend'
|
||||
manifest = yaml.safe_load((backend / 'plugins/ai/plugin.yaml').read_text(encoding='utf-8'))
|
||||
requirements = manifest['dependencies']['python']
|
||||
if not requirements or not all(isinstance(item, str) and not item.startswith('-') for item in requirements):
|
||||
raise ValueError('AI 插件 Python 依赖清单无效')
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', *requirements], check=True, timeout=900)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,184 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import partial
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from playwright.async_api import Route, async_playwright, expect
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TOKEN = 'isolated-mobile-smoke-token'
|
||||
USER = {
|
||||
'userId': 7,
|
||||
'userName': 'ci-user',
|
||||
'nickName': '移动端 CI',
|
||||
'avatar': '',
|
||||
'phonenumber': '13800000000',
|
||||
'email': 'mobile-ci@example.test',
|
||||
'createTime': '2026-01-01T00:00:00Z',
|
||||
'timeZone': 'America/New_York',
|
||||
}
|
||||
|
||||
|
||||
def create_api_handler(errors: list[str], observed: list[dict[str, Any]]) -> Callable[[Route], Awaitable[None]]:
|
||||
"""
|
||||
创建隔离 API 处理函数,向共享记录追加请求及异常。
|
||||
|
||||
:param errors: 本次验收收集的异常说明
|
||||
:param observed: 实际发出的 API 请求记录
|
||||
:return: 供浏览器路由使用的异步处理函数
|
||||
"""
|
||||
|
||||
async def api(route: Route) -> None:
|
||||
"""
|
||||
隔离 API 边界,记录真实构建发出的认证和时区请求。
|
||||
|
||||
:param route: 当前浏览器 API 请求
|
||||
:return: None
|
||||
"""
|
||||
request = route.request
|
||||
path = urlparse(request.url).path
|
||||
headers = {'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*'}
|
||||
if request.method == 'OPTIONS':
|
||||
await route.fulfill(status=204, headers=headers)
|
||||
return
|
||||
observed.append(
|
||||
{
|
||||
'path': path,
|
||||
'method': request.method,
|
||||
'authorization': request.headers.get('authorization'),
|
||||
'timezone': request.headers.get('x-timezone'),
|
||||
}
|
||||
)
|
||||
payload: dict[str, Any]
|
||||
if path == '/transport/crypto/frontend-config':
|
||||
payload = {
|
||||
'code': 200,
|
||||
'data': {
|
||||
'transportCryptoEnabled': False,
|
||||
'transportCryptoMode': 'off',
|
||||
'transportCryptoActive': False,
|
||||
'configExpireAt': int(time.time()) + 600,
|
||||
},
|
||||
}
|
||||
elif path == '/captchaImage':
|
||||
payload = {'code': 200, 'captchaEnabled': False}
|
||||
elif path == '/login':
|
||||
if 'ci-user' not in (request.post_data or ''):
|
||||
errors.append('登录请求未包含输入的用户名')
|
||||
payload = {'code': 200, 'token': TOKEN}
|
||||
elif path == '/getInfo':
|
||||
payload = {
|
||||
'code': 200,
|
||||
'user': USER,
|
||||
'roles': ['admin'],
|
||||
'permissions': ['*:*:*'],
|
||||
'appTimezone': 'Asia/Shanghai',
|
||||
}
|
||||
elif path == '/system/user/profile':
|
||||
payload = {'code': 200, 'data': USER, 'postGroup': 'CI', 'roleGroup': '管理员'}
|
||||
elif path == '/logout':
|
||||
payload = {'code': 200}
|
||||
else:
|
||||
errors.append(f'未预期的 API 请求:{path}')
|
||||
await route.fulfill(status=500, json={'code': 500, 'msg': 'unexpected smoke request'}, headers=headers)
|
||||
return
|
||||
await route.fulfill(json=payload, headers=headers)
|
||||
|
||||
return api
|
||||
|
||||
|
||||
async def run_smoke(framework: str, base_url: str, output: Path) -> None:
|
||||
"""
|
||||
使用真实 H5 构建验证登录、持久化身份和个人资料页。
|
||||
|
||||
:param framework: vue2 或 vue3
|
||||
:param base_url: 仅用于本次测试的静态文件服务器
|
||||
:param output: 浏览器报告目录
|
||||
:return: None
|
||||
"""
|
||||
logs: list[str] = []
|
||||
errors: list[str] = []
|
||||
observed: list[dict[str, Any]] = []
|
||||
api = create_api_handler(errors, observed)
|
||||
|
||||
async with async_playwright() as playwright:
|
||||
executable = os.environ.get('RUOYI_SMOKE_BROWSER_EXECUTABLE')
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=True, **({'executable_path': executable} if executable else {})
|
||||
)
|
||||
context = await browser.new_context(viewport={'width': 390, 'height': 844}, timezone_id='UTC')
|
||||
await context.tracing.start(screenshots=True, snapshots=True, sources=True)
|
||||
await context.route('http://localhost:9099/**', api)
|
||||
page = await context.new_page()
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
page.on('console', lambda message: logs.append(f'{message.type}: {message.text}'))
|
||||
failed = False
|
||||
try:
|
||||
await page.goto(f'{base_url}/#/pages/login')
|
||||
await expect(page.locator('input[type="text"]')).to_be_visible(timeout=30000)
|
||||
await expect(page.locator('input')).to_have_count(2, timeout=15000)
|
||||
await page.locator('input[type="text"]').fill('ci-user')
|
||||
await page.locator('input[type="password"]').fill('ci-test-password')
|
||||
await page.locator('uni-button').filter(has_text=re.compile(r'登\s*录')).click()
|
||||
await expect(page).to_have_url(re.compile(r'/pages/index'), timeout=30000)
|
||||
await page.goto(f'{base_url}/#/pages/mine/info/index')
|
||||
await expect(page.get_by_text(USER['nickName'], exact=True)).to_be_visible(timeout=30000)
|
||||
await expect(page.get_by_text('2025-12-31 19:00:00', exact=True)).to_be_visible(timeout=15000)
|
||||
profile_requests = [request for request in observed if request['path'] == '/system/user/profile']
|
||||
assert profile_requests, '个人资料页未发送真实 API 请求'
|
||||
assert all(request['authorization'] == f'Bearer {TOKEN}' for request in profile_requests)
|
||||
assert all(request['timezone'] == 'America/New_York' for request in profile_requests)
|
||||
assert not errors, errors
|
||||
(output / 'result.json').write_text(
|
||||
json.dumps({'framework': framework, 'ok': True, 'requests': observed}, ensure_ascii=False, indent=2),
|
||||
encoding='utf-8',
|
||||
)
|
||||
print(f'{framework}: H5 login, persisted session and timezone profile PASS')
|
||||
except BaseException:
|
||||
failed = True
|
||||
await page.screenshot(path=str(output / 'failure.png'), full_page=True)
|
||||
(output / 'page.html').write_text(await page.content(), encoding='utf-8')
|
||||
raise
|
||||
finally:
|
||||
(output / 'browser.log').write_text('\n'.join([*logs, *errors]), encoding='utf-8')
|
||||
await context.tracing.stop(**({'path': str(output / 'trace.zip')} if failed else {}))
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
在独立临时端口启动 H5 静态服务,结束后关闭自身创建的资源。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--framework', choices=('vue2', 'vue3'), required=True)
|
||||
args = parser.parse_args()
|
||||
distribution = ROOT / 'ruoyi-fastapi-frontend' / args.framework / 'mobile/dist/build/h5'
|
||||
if not (distribution / 'index.html').is_file():
|
||||
raise SystemExit(f'请先构建 H5:{distribution}')
|
||||
output = ROOT / 'target/ci/mobile-smoke' / args.framework
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
server = ThreadingHTTPServer(('127.0.0.1', 0), partial(SimpleHTTPRequestHandler, directory=str(distribution)))
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
asyncio.run(run_smoke(args.framework, f'http://127.0.0.1:{server.server_port}', output))
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
import importlib.util
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load_script(name: str) -> ModuleType:
|
||||
"""
|
||||
按文件路径加载 CI 脚本,避免依赖仓库外的模块搜索路径。
|
||||
|
||||
:param name: 不含扩展名的脚本名称
|
||||
:return: 已执行模块顶层定义的脚本模块
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / '.github/scripts' / f'{name}.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
compose = load_script('check_compose')
|
||||
reports = load_script('check_test_report')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configuration() -> dict[str, Any]:
|
||||
"""提供前端、nginx 和后端框架一致的 Compose 配置。"""
|
||||
frontend = ROOT / 'ruoyi-fastapi-frontend/vue2/web'
|
||||
return {
|
||||
'services': {
|
||||
'ruoyi-frontend': {
|
||||
'build': {'context': str(frontend), 'dockerfile': 'Dockerfile'},
|
||||
'volumes': [
|
||||
{'source': str(frontend / 'bin/nginx.dockermy.conf'), 'target': '/etc/nginx/conf.d/default.conf'}
|
||||
],
|
||||
},
|
||||
'ruoyi-backend-my': {'environment': {'RUOYI_PLUGIN_FRONTEND_FRAMEWORK': 'vue2'}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_compose_accepts_consistent_framework(configuration: dict[str, Any]) -> None:
|
||||
"""框架一致的 Compose 配置可以通过校验。"""
|
||||
compose.validate_configuration(configuration, 'vue2', 'my', ROOT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('component', ['build', 'nginx', 'backend'])
|
||||
def test_compose_rejects_mixed_framework_deployment(configuration: dict[str, Any], component: str) -> None:
|
||||
"""前端、nginx 或后端混用框架时拒绝部署配置。"""
|
||||
invalid = deepcopy(configuration)
|
||||
if component == 'build':
|
||||
invalid['services']['ruoyi-frontend']['build']['context'] = str(ROOT / 'ruoyi-fastapi-frontend/vue3/web')
|
||||
elif component == 'nginx':
|
||||
invalid['services']['ruoyi-frontend']['volumes'][0]['source'] = str(
|
||||
ROOT / 'ruoyi-fastapi-frontend/vue3/web/bin/nginx.dockermy.conf'
|
||||
)
|
||||
else:
|
||||
invalid['services']['ruoyi-backend-my']['environment']['RUOYI_PLUGIN_FRONTEND_FRAMEWORK'] = 'vue3'
|
||||
with pytest.raises(ValueError):
|
||||
compose.validate_configuration(invalid, 'vue2', 'my', ROOT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'cases',
|
||||
['', '<testcase><skipped /></testcase>', '<testcase><failure /></testcase>', '<testcase><error /></testcase>'],
|
||||
)
|
||||
def test_report_rejects_empty_skipped_or_failed_verification(tmp_path: Path, cases: str) -> None:
|
||||
"""空报告、全部跳过和失败用例都不能视为验收成功。"""
|
||||
report = tmp_path / 'results.xml'
|
||||
report.write_text(f'<testsuites><testsuite>{cases}</testsuite></testsuites>', encoding='utf-8')
|
||||
with pytest.raises(ValueError):
|
||||
reports.check_report(report)
|
||||
|
||||
|
||||
def test_expected_platform_skip_still_requires_executed_tests(tmp_path: Path) -> None:
|
||||
"""允许预期平台跳过时仍要求实际执行测试。"""
|
||||
report = tmp_path / 'results.xml'
|
||||
report.write_text(
|
||||
'<testsuites><testsuite><testcase/><testcase><skipped/></testcase></testsuite></testsuites>', encoding='utf-8'
|
||||
)
|
||||
assert reports.check_report(report, allow_skips=True) == (1, 1)
|
||||
|
||||
|
||||
def test_missing_report_is_not_success(tmp_path: Path) -> None:
|
||||
"""报告缺失时不能视为验收成功。"""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
reports.check_report(tmp_path / 'missing.xml')
|
||||
@@ -0,0 +1,169 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'scripts' / 'ci_plan.py'
|
||||
SPEC = importlib.util.spec_from_file_location('ci_plan', SCRIPT)
|
||||
ci = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(ci)
|
||||
FRONTEND_PROJECT_COUNT = 4
|
||||
STANDARD_E2E_COMBINATION_COUNT = 4
|
||||
FULL_E2E_COMBINATION_COUNT = 16
|
||||
|
||||
|
||||
def test_readme_change_only_runs_quality() -> None:
|
||||
"""仅文档变更时只执行质量检查。"""
|
||||
assert ci.build_plan(['README.md'])['required'] == {
|
||||
'quality': True,
|
||||
'backend': False,
|
||||
'frontend': False,
|
||||
'integration': False,
|
||||
'e2e': False,
|
||||
'native': False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-backend/cli/groups/plugin/options.py',
|
||||
'ruoyi-fastapi-backend/cli/groups/plugin/controller.py',
|
||||
'ruoyi-fastapi-backend/cli/runtime/plugin/service.py',
|
||||
'ruoyi-fastapi-backend/tests/module_plugin/controller/test_plugin_release_controller.py',
|
||||
'ruoyi-fastapi-backend/tests/sql/test_plugin_release_schema.py',
|
||||
],
|
||||
)
|
||||
def test_shared_plugin_changes_retain_windows_and_release_coverage(path: str) -> None:
|
||||
"""共享插件变更保留原生模块及发布验收覆盖。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['backend']
|
||||
assert plan['required']['native']
|
||||
assert plan['release']
|
||||
|
||||
|
||||
def test_native_builder_change_runs_native_verification() -> None:
|
||||
"""原生构建器变更必须触发原生验收。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-backend/scripts/build_native_plugin.py'])
|
||||
assert plan['required']['native']
|
||||
|
||||
|
||||
def test_bundle_example_change_runs_both_web_consumers() -> None:
|
||||
"""示例开发代理由两版 Web 测试直接引用,变更时均应执行。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/dev/mockRequest.js'])
|
||||
assert plan['frontend_matrix'] == {
|
||||
'include': [
|
||||
{'framework': 'vue2', 'target': 'web'},
|
||||
{'framework': 'vue3', 'target': 'web'},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-backend/module_admin/service/login_service.py',
|
||||
'ruoyi-fastapi-backend/tests/scheduler_helpers.py',
|
||||
'ruoyi-fastapi-backend/tests/plugins/core/runtime/test_browser_session.py',
|
||||
],
|
||||
)
|
||||
def test_shared_auth_and_test_helpers_keep_both_network_suites(path: str) -> None:
|
||||
"""共享鉴权和测试夹具变更触发两版网络验收。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['backend']
|
||||
assert plan['required']['integration']
|
||||
assert plan['network_frameworks'] == ['vue2', 'vue3']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path', ['.github/workflows/ci.yml', '.github/scripts/ci_plan.py', 'ruoyi-fastapi-backend/requirements-test.txt']
|
||||
)
|
||||
def test_ci_and_dependency_changes_select_every_category(path: str) -> None:
|
||||
"""CI 及共享依赖变更覆盖全部任务分类。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert all(plan['required'].values())
|
||||
assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
|
||||
assert len(plan['e2e_matrix']['include']) == STANDARD_E2E_COMBINATION_COUNT
|
||||
|
||||
|
||||
def test_mobile_change_does_not_build_other_projects_or_web_e2e() -> None:
|
||||
"""单个移动端变更只选中对应工程。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js'])
|
||||
assert plan['frontend_matrix'] == {'include': [{'framework': 'vue2', 'target': 'mobile'}]}
|
||||
assert not plan['required']['e2e']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-test/frontend/mobile-harness.mjs',
|
||||
'ruoyi-fastapi-test/frontend/mobile-behavior.mjs',
|
||||
],
|
||||
)
|
||||
def test_shared_mobile_harness_runs_both_mobile_projects(path: str) -> None:
|
||||
"""共享移动端测试工具变更同时验证两版工程。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['frontend']
|
||||
assert plan['frontend_matrix'] == {
|
||||
'include': [
|
||||
{'framework': 'vue2', 'target': 'mobile'},
|
||||
{'framework': 'vue3', 'target': 'mobile'},
|
||||
]
|
||||
}
|
||||
assert not plan['required']['e2e']
|
||||
|
||||
|
||||
def test_web_change_selects_matching_framework_and_both_databases() -> None:
|
||||
"""Web 变更使用对应框架并覆盖两类数据库。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/web/src/views/login.vue'])
|
||||
assert plan['network_frameworks'] == ['vue2']
|
||||
assert {(row['framework'], row['database'], row['python']) for row in plan['e2e_matrix']['include']} == {
|
||||
('vue2', 'mysql', '3.12'),
|
||||
('vue2', 'postgresql', '3.12'),
|
||||
}
|
||||
|
||||
|
||||
def test_shared_time_contract_runs_all_frontends_and_database_checks() -> None:
|
||||
"""共享时间契约触发全部前端及数据库检查。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-test/time-contract/fixtures.json'])
|
||||
assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
|
||||
assert plan['database']
|
||||
|
||||
|
||||
def test_framework_move_keeps_both_sides_in_plan() -> None:
|
||||
"""跨框架移动文件保留来源和目标两侧验证。"""
|
||||
plan = ci.build_plan(
|
||||
['ruoyi-fastapi-frontend/vue2/web/src/utils/demo.js', 'ruoyi-fastapi-frontend/vue3/web/src/utils/demo.js']
|
||||
)
|
||||
assert plan['network_frameworks'] == ['vue2', 'vue3']
|
||||
|
||||
|
||||
def test_full_run_preserves_python_database_and_framework_combinations() -> None:
|
||||
"""完整运行保留 Python、数据库和框架的全部组合。"""
|
||||
plan = ci.build_plan([], full=True)
|
||||
assert all(plan['required'].values())
|
||||
assert len(plan['e2e_matrix']['include']) == FULL_E2E_COMBINATION_COUNT
|
||||
assert {row['python'] for row in plan['e2e_matrix']['include']} == {'3.10', '3.11', '3.12', '3.13'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('result', ['failure', 'cancelled', 'skipped', None])
|
||||
def test_gate_rejects_unsuccessful_required_job(result: str | None) -> None:
|
||||
"""必跑任务失败、取消、跳过或缺失均不能通过门禁。"""
|
||||
plan = ci.build_plan(['README.md'])
|
||||
needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
|
||||
needs['quality']['result'] = result
|
||||
assert ci.check_results(plan, needs)
|
||||
|
||||
|
||||
def test_gate_allows_only_unselected_jobs_to_skip() -> None:
|
||||
"""只有未选中的任务允许跳过。"""
|
||||
plan = ci.build_plan(['README.md'])
|
||||
needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
|
||||
needs['quality']['result'] = 'success'
|
||||
assert ci.check_results(plan, needs) == []
|
||||
|
||||
|
||||
def test_gate_fails_closed_when_plan_or_job_is_missing() -> None:
|
||||
"""缺少执行计划或任务结果时门禁必须拒绝放行。"""
|
||||
assert ci.check_results({}, {})
|
||||
assert ci.check_results(ci.build_plan(['README.md']), {'plan': {'result': 'success'}})
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Backend Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Typer otherwise forces ANSI help output when GITHUB_ACTIONS is set.
|
||||
_TYPER_FORCE_DISABLE_TERMINAL: "1"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
ruoyi-fastapi-backend/requirements.txt
|
||||
ruoyi-fastapi-backend/plugins/ai/plugin.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt pytest pytest-asyncio aiosqlite
|
||||
|
||||
- name: Install AI plugin dependencies
|
||||
run: |
|
||||
pip install \
|
||||
agno==2.8.6 \
|
||||
anthropic==0.120.2 \
|
||||
cerebras-cloud-sdk==1.91.0 \
|
||||
cohere==7.0.8 \
|
||||
google-genai==2.16.0 \
|
||||
groq==1.6.0 \
|
||||
litellm==1.94.0 \
|
||||
llama-api-client==0.6.0 \
|
||||
mistralai==2.8.0 \
|
||||
ollama==0.6.2 \
|
||||
openai==2.52.0 \
|
||||
portkey-ai==2.3.4
|
||||
|
||||
- name: Run backend tests
|
||||
run: pytest -q
|
||||
|
||||
- name: Check timezone behavior under independent host timezones
|
||||
run: |
|
||||
for zone in UTC Asia/Shanghai America/New_York; do
|
||||
TZ="$zone" python -m pytest -q tests/common/test_types.py tests/common/test_query_time.py tests/utils/test_time_util.py tests/config/test_scheduler_database.py tests/utils/test_cron_util.py tests/module_admin/controller/test_job_controller.py tests/config/test_scheduler_job_persistence.py
|
||||
done
|
||||
@@ -0,0 +1,110 @@
|
||||
name: CI / Backend
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
_TYPER_FORCE_DISABLE_TERMINAL: '1'
|
||||
PYTHONUTF8: '1'
|
||||
|
||||
jobs:
|
||||
unit-contract:
|
||||
name: Python ${{ matrix.python }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ['3.10', '3.11', '3.12', '3.13']
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
ruoyi-fastapi-backend/requirements*.txt
|
||||
ruoyi-fastapi-backend/plugins/ai/plugin.yaml
|
||||
- name: Install test dependencies
|
||||
run: python -m pip install -r requirements.txt -r requirements-test.txt
|
||||
- name: Install AI plugin dependencies from its manifest
|
||||
run: python ../.github/scripts/install_ai_test_dependencies.py
|
||||
- name: Run backend unit and contract tests
|
||||
run: python -m pytest tests -q -m 'not browser and not native and not integration' --strict-markers --durations=20 --junitxml=target/ci/backend.xml
|
||||
- name: Verify tests were executed
|
||||
run: python ../.github/scripts/check_test_report.py target/ci/backend.xml --allow-skips
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: backend-python-${{ matrix.python }}
|
||||
path: ruoyi-fastapi-backend/target/ci/
|
||||
retention-days: 7
|
||||
|
||||
windows-contract:
|
||||
name: Windows CLI and paths
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- run: python -m pip install -r requirements.txt -r requirements-test.txt
|
||||
- name: Verify CLI, paths, artifacts and portable runtime contracts
|
||||
run: >-
|
||||
python -m pytest tests/cli
|
||||
tests/plugins/core/test_frontend_layout.py tests/plugins/core/validation/test_frontend_dependencies.py
|
||||
tests/plugins/core/native tests/plugins/core/runtime/test_host_services.py tests/plugins/core/runtime/test_job_dispatcher.py
|
||||
tests/plugins/core/artifacts tests/plugins/core/deployment
|
||||
tests/plugins/core/management/test_release_dao.py tests/plugins/core/management/test_artifact_views.py
|
||||
tests/module_plugin/controller/test_plugin_release_controller.py tests/sql/test_plugin_release_schema.py
|
||||
tests/plugins/examples/test_task_demo.py
|
||||
-m 'not native and not browser and not integration' -q --strict-markers --durations=15 --junitxml=target/ci/windows-contract.xml
|
||||
- run: python ../.github/scripts/check_test_report.py target/ci/windows-contract.xml --allow-skips
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: backend-windows-contract
|
||||
path: ruoyi-fastapi-backend/target/ci/
|
||||
retention-days: 7
|
||||
|
||||
timezone:
|
||||
name: Host timezone contracts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- run: python -m pip install -r requirements.txt -r requirements-test.txt
|
||||
- name: Verify UTC, Shanghai and New York in separate processes
|
||||
shell: bash
|
||||
run: |
|
||||
for zone in UTC Asia/Shanghai America/New_York; do
|
||||
report="target/ci/timezone-${zone//\//_}.xml"
|
||||
TZ="$zone" python -m pytest -q tests/common/test_types.py tests/common/test_query_time.py tests/utils/test_time_util.py tests/config/test_scheduler_database.py tests/utils/test_cron_util.py tests/module_admin/controller/test_job_controller.py tests/config/test_scheduler_job_persistence.py --junitxml="$report"
|
||||
python ../.github/scripts/check_test_report.py "$report"
|
||||
done
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: backend-timezones
|
||||
path: ruoyi-fastapi-backend/target/ci/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,111 @@
|
||||
name: CI / E2E
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
matrix_json:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
browser:
|
||||
name: ${{ matrix.framework }} / ${{ matrix.database }} / Python ${{ matrix.python }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(inputs.matrix_json) }}
|
||||
env:
|
||||
COMPOSE_PROJECT_NAME: ruoyi-ci-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}
|
||||
COMPOSE_FILE: ${{ github.workspace }}/ruoyi-fastapi-test/target/e2e/compose.json
|
||||
FRONTEND_FRAMEWORK: ${{ matrix.framework }}
|
||||
E2E_OUTPUT_DIR: ${{ github.workspace }}/ruoyi-fastapi-test/target/e2e/browser
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-test/requirements.txt
|
||||
- name: Install test runner and Chromium
|
||||
run: |
|
||||
python -m pip install -r ruoyi-fastapi-test/requirements.txt
|
||||
python -m playwright install --with-deps chromium
|
||||
- name: Verify reusable browser and isolated contexts
|
||||
working-directory: ruoyi-fastapi-test
|
||||
run: |
|
||||
python -m pytest -m harness -q --junitxml=target/e2e/harness.xml
|
||||
python ../.github/scripts/check_test_report.py target/e2e/harness.xml
|
||||
- name: Generate isolated Compose project
|
||||
run: |
|
||||
python ruoyi-fastapi-test/prepare_e2e_compose.py \
|
||||
--database '${{ matrix.database }}' --framework '${{ matrix.framework }}' \
|
||||
--python-version '${{ matrix.python }}' --project "$COMPOSE_PROJECT_NAME" \
|
||||
--output "$COMPOSE_FILE"
|
||||
docker compose config --quiet
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Build frontend with reusable layers
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ruoyi-fastapi-frontend/${{ matrix.framework }}/web
|
||||
file: ruoyi-fastapi-frontend/${{ matrix.framework }}/web/Dockerfile
|
||||
load: true
|
||||
tags: ${{ env.COMPOSE_PROJECT_NAME }}-ruoyi-frontend:ci
|
||||
cache-from: type=gha,scope=e2e-frontend-${{ matrix.framework }}
|
||||
cache-to: type=gha,scope=e2e-frontend-${{ matrix.framework }},mode=max
|
||||
- name: Select database Dockerfile
|
||||
id: database
|
||||
env:
|
||||
DATABASE: ${{ matrix.database }}
|
||||
run: |
|
||||
if [ "$DATABASE" = mysql ]; then
|
||||
echo 'suffix=my' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'suffix=pg' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Build backend with reusable layers
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ruoyi-fastapi-backend
|
||||
file: ruoyi-fastapi-backend/Dockerfile.${{ steps.database.outputs.suffix }}
|
||||
load: true
|
||||
tags: ${{ env.COMPOSE_PROJECT_NAME }}-ruoyi-backend-${{ steps.database.outputs.suffix }}:ci
|
||||
build-args: |
|
||||
PYTHON_VERSION=${{ matrix.python }}
|
||||
PIP_INDEX_URL=https://pypi.org/simple
|
||||
cache-from: type=gha,scope=e2e-backend-${{ matrix.database }}-py${{ matrix.python }}
|
||||
cache-to: type=gha,scope=e2e-backend-${{ matrix.database }}-py${{ matrix.python }},mode=max
|
||||
- name: Wait for healthy database, API and frontend proxy
|
||||
run: |
|
||||
docker compose up --detach --no-build --wait --wait-timeout 240
|
||||
echo "TEST_FRONTEND_URL=http://$(docker compose port ruoyi-frontend 80)" >> "$GITHUB_ENV"
|
||||
echo "TEST_BACKEND_URL=http://$(docker compose port ruoyi-backend-${{ steps.database.outputs.suffix }} 9099)" >> "$GITHUB_ENV"
|
||||
- name: Run all application pages and functional E2E checks
|
||||
working-directory: ruoyi-fastapi-test
|
||||
run: |
|
||||
python -m pytest -m e2e -v --durations=20 --junitxml=target/e2e/junit.xml
|
||||
python ../.github/scripts/check_test_report.py target/e2e/junit.xml
|
||||
- name: Collect service diagnostics
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p ruoyi-fastapi-test/target/e2e
|
||||
if [ -f "$COMPOSE_FILE" ]; then
|
||||
docker compose ps --all > ruoyi-fastapi-test/target/e2e/services.txt 2>&1 || true
|
||||
docker compose logs --no-color --timestamps > ruoyi-fastapi-test/target/e2e/services.log 2>&1 || true
|
||||
fi
|
||||
- name: Remove only this test project's containers and data
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f "$COMPOSE_FILE" ]; then
|
||||
docker compose down --volumes --remove-orphans --timeout 20
|
||||
fi
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-${{ matrix.framework }}-${{ matrix.database }}-py${{ matrix.python }}
|
||||
path: ruoyi-fastapi-test/target/e2e
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,116 @@
|
||||
name: CI / Frontend
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
matrix_json:
|
||||
description: 'JSON matrix with framework and target entries under include'
|
||||
required: true
|
||||
type: string
|
||||
full:
|
||||
description: 'Reserved for callers requesting the full project matrix'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
name: ${{ matrix.framework }} / ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(inputs.matrix_json) }}
|
||||
env:
|
||||
NODE_OPTIONS: ${{ matrix.framework == 'vue2' && '--openssl-legacy-provider' || '' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Install the Vue 2 Mobile package manager
|
||||
if: matrix.framework == 'vue2' && matrix.target == 'mobile'
|
||||
run: npm install --global yarn@1.22.22
|
||||
- uses: pnpm/action-setup@v5
|
||||
if: matrix.framework == 'vue3' && matrix.target == 'mobile'
|
||||
with:
|
||||
package_json_file: ruoyi-fastapi-frontend/vue3/mobile/package.json
|
||||
- name: Locate the package cache
|
||||
id: package-cache
|
||||
env:
|
||||
FRAMEWORK: ${{ matrix.framework }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: |
|
||||
if [ "$TARGET" = web ]; then
|
||||
echo "path=$(npm config get cache)" >> "$GITHUB_OUTPUT"
|
||||
elif [ "$FRAMEWORK" = vue2 ]; then
|
||||
echo "path=$(yarn cache dir --silent)" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.package-cache.outputs.path }}
|
||||
# Lockfiles are intentionally not tracked in these projects.
|
||||
key: ${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-${{ hashFiles(format('ruoyi-fastapi-frontend/{0}/{1}/package.json', matrix.framework, matrix.target)) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-
|
||||
- name: Install Web dependencies
|
||||
if: matrix.target == 'web'
|
||||
run: npm install --no-audit --no-fund --package-lock=false
|
||||
- name: Install Vue 2 Mobile dependencies
|
||||
if: matrix.framework == 'vue2' && matrix.target == 'mobile'
|
||||
run: yarn install --non-interactive
|
||||
- name: Install Vue 3 Mobile dependencies
|
||||
if: matrix.framework == 'vue3' && matrix.target == 'mobile'
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run unit and component behavior tests
|
||||
run: npm run test:unit
|
||||
- name: Verify time contracts in three host timezones
|
||||
run: |
|
||||
for zone in UTC Asia/Shanghai America/New_York; do
|
||||
TZ="$zone" npm run test:contract
|
||||
done
|
||||
- name: Verify the Vue 2 Mobile browser bundle without native Intl
|
||||
if: matrix.framework == 'vue2' && matrix.target == 'mobile'
|
||||
run: npm run test:bundle
|
||||
- name: Build Web production and Docker targets
|
||||
if: matrix.target == 'web'
|
||||
run: |
|
||||
npm run build:prod
|
||||
npm run build:docker
|
||||
- name: Build Mobile H5
|
||||
if: matrix.target == 'mobile'
|
||||
run: npm run build:h5
|
||||
- uses: actions/setup-python@v6
|
||||
if: matrix.target == 'mobile'
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install the Mobile smoke browser
|
||||
if: matrix.target == 'mobile'
|
||||
run: |
|
||||
python -m pip install playwright==1.62.0
|
||||
python -m playwright install --with-deps chromium
|
||||
- name: Verify Mobile H5 login and profile
|
||||
if: matrix.target == 'mobile'
|
||||
working-directory: .
|
||||
run: python .github/scripts/mobile_smoke.py --framework ${{ matrix.framework }}
|
||||
- name: Build Mobile WeChat target
|
||||
if: matrix.target == 'mobile'
|
||||
run: npm run build:mp-weixin
|
||||
- name: Upload frontend diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frontend-${{ matrix.framework }}-${{ matrix.target }}
|
||||
path: |
|
||||
target/ci/mobile-smoke/${{ matrix.framework }}/
|
||||
ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}/test-results/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,218 @@
|
||||
name: CI / Integration
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
database:
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
type: boolean
|
||||
default: false
|
||||
network_frameworks:
|
||||
type: string
|
||||
default: '[]'
|
||||
full:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
database-timezone:
|
||||
name: MySQL and PostgreSQL time contracts
|
||||
if: inputs.database
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
TZ: America/New_York
|
||||
PYTHONUTF8: '1'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: timezone_test
|
||||
TZ: Asia/Shanghai
|
||||
ports: ['3306:3306']
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h localhost -ptimezone_test"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: timezone_test
|
||||
TZ: America/New_York
|
||||
PGTZ: America/New_York
|
||||
ports: ['5432:5432']
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U postgres"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- run: python -m pip install -r requirements.txt -r requirements-pg.txt -r requirements-test.txt
|
||||
- name: Configure isolated database verification
|
||||
run: |
|
||||
mkdir -p target/ci/timezone
|
||||
python - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
for dialect, port, username, database in [('mysql', 3306, 'root', 'mysql'), ('postgresql', 5432, 'postgres', 'postgres')]:
|
||||
source = dict(db_type=dialect, db_host='127.0.0.1', db_port=port, db_username=username,
|
||||
db_password='timezone_test', db_database=database)
|
||||
Path(f'target/ci/timezone/{dialect}.json').write_text(json.dumps(source))
|
||||
PY
|
||||
- name: Verify drivers, CRUD and scheduler persistence
|
||||
run: |
|
||||
python -m scripts.verify_timezone_database --config target/ci/timezone/mysql.json --report target/ci/timezone/mysql-report.json
|
||||
python -m scripts.verify_timezone_database --config target/ci/timezone/postgresql.json --report target/ci/timezone/postgresql-report.json
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: integration-database-timezones
|
||||
path: ruoyi-fastapi-backend/target/ci/timezone/*-report.json
|
||||
retention-days: 7
|
||||
|
||||
task-bundle:
|
||||
name: Task plugin bundle
|
||||
if: inputs.release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/package.json
|
||||
- run: npm install --no-audit --no-fund --package-lock=false
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: integration-task-bundle
|
||||
path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
name: Plugin maintenance / Python ${{ matrix.python }}
|
||||
needs: task-bundle
|
||||
if: inputs.release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ${{ fromJSON(inputs.full && '["3.10","3.11","3.12","3.13"]' || '["3.12"]') }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: ci-only-mysql
|
||||
ports: ['3306:3306']
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h localhost -pci-only-mysql"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: ci-only-postgres
|
||||
ports: ['5432:5432']
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U postgres"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
redis:
|
||||
image: redis:6.2
|
||||
ports: ['6379:6379']
|
||||
options: >-
|
||||
--health-cmd="redis-cli ping"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
env:
|
||||
RUOYI_PLUGIN_IT_ALLOW: '1'
|
||||
RUOYI_IT_MYSQL_PASSWORD: ci-only-mysql
|
||||
RUOYI_IT_PG_PASSWORD: ci-only-postgres
|
||||
RUOYI_IT_REDIS_PASSWORD: ''
|
||||
PYTHONUTF8: '1'
|
||||
_TYPER_FORCE_DISABLE_TERMINAL: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: integration-task-bundle
|
||||
path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/dist
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- run: python -m pip install -r requirements.txt -r requirements-pg.txt
|
||||
- name: Verify MySQL and PostgreSQL maintenance releases
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p target/ci/release
|
||||
python scripts/plugin_release_integration.py --engine mysql 2>&1 | tee target/ci/release/mysql.log
|
||||
python scripts/plugin_release_integration.py --engine postgresql 2>&1 | tee target/ci/release/postgresql.log
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: integration-release-${{ matrix.python }}
|
||||
path: ruoyi-fastapi-backend/target/ci/release/
|
||||
retention-days: 7
|
||||
|
||||
network:
|
||||
name: ${{ matrix.framework }} proxy and plugin sessions
|
||||
if: inputs.network_frameworks != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
framework: ${{ fromJSON(inputs.network_frameworks) }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
env:
|
||||
RUOYI_PLUGIN_NETWORK_SMOKE: '1'
|
||||
RUOYI_PLUGIN_FRONTEND_FRAMEWORK: ${{ matrix.framework }}
|
||||
NODE_OPTIONS: ${{ matrix.framework == 'vue2' && '--openssl-legacy-provider' || '' }}
|
||||
DB_SOURCES: '{"primary":{"db_type":"mysql","db_host":"127.0.0.1","db_port":3306,"db_username":"network-smoke","db_password":"network-smoke","db_database":"network-smoke"}}'
|
||||
JWT_SECRET_KEY: isolated-plugin-network-smoke-ci-only
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: ruoyi-fastapi-frontend/${{ matrix.framework }}/web/package.json
|
||||
- run: python -m pip install -r requirements.txt -r requirements-test.txt playwright==1.62.0
|
||||
- run: python -m playwright install --with-deps chromium
|
||||
- run: npm --prefix ../ruoyi-fastapi-frontend/${{ matrix.framework }}/web install --no-audit --no-fund --package-lock=false
|
||||
- name: Verify HTTPS proxy, iframe sessions and long connections
|
||||
run: python -m pytest tests/plugins/integration/test_network_smoke.py -m browser --strict-markers -v --junitxml=target/plugin-network-smoke/${{ matrix.framework }}/results.xml
|
||||
- run: python ../.github/scripts/check_test_report.py target/plugin-network-smoke/${{ matrix.framework }}/results.xml
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: integration-network-${{ matrix.framework }}
|
||||
path: ruoyi-fastapi-backend/target/plugin-network-smoke/${{ matrix.framework }}/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,80 @@
|
||||
name: CI / Native
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHONUTF8: '1'
|
||||
_TYPER_FORCE_DISABLE_TERMINAL: '1'
|
||||
|
||||
jobs:
|
||||
rust-quality:
|
||||
name: Rust format and clippy
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: rustup toolchain install 1.99.0 --profile minimal --component rustfmt --component clippy
|
||||
- run: rustup default 1.99.0
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
ruoyi-fastapi-backend/plugins/examples/rust/rust_demo/target
|
||||
key: rust-quality-${{ runner.os }}-1.99.0-${{ hashFiles('**/Cargo.lock') }}
|
||||
- run: cargo fmt --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --check
|
||||
- run: cargo clippy --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --locked -- -D warnings
|
||||
|
||||
runtime:
|
||||
name: ${{ matrix.os }} / Python ${{ matrix.python }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python: ['3.10', '3.11', '3.12', '3.13']
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- run: python -m pip install -r requirements.txt -r requirements-test.txt 'maturin>=1.15,<2'
|
||||
- run: rustup toolchain install 1.99.0 --profile minimal
|
||||
- run: rustup default 1.99.0
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
ruoyi-fastapi-backend/plugins/examples/rust/rust_demo/target
|
||||
key: rust-native-${{ runner.os }}-${{ matrix.python }}-1.99.0-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Build and assemble source-free plugin
|
||||
run: python scripts/build_native_plugin.py --output target/native-package
|
||||
- name: Exercise real native runtime and signed artifact loading
|
||||
env:
|
||||
RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
|
||||
run: python -m pytest tests/plugins/native tests/plugins/core/deployment -m native --strict-markers -q --durations=15 --junitxml=target/ci/native.xml
|
||||
- name: Require native tests to run without missing-artifact skips
|
||||
run: python ../.github/scripts/check_test_report.py target/ci/native.xml
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: native-${{ runner.os }}-${{ matrix.python }}
|
||||
path: ruoyi-fastapi-backend/target/ci/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,54 @@
|
||||
name: CI / Quality
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: Code and CI contracts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: .github/requirements.txt
|
||||
- run: python -m pip install -r .github/requirements.txt
|
||||
- name: Python lint and format
|
||||
run: |
|
||||
ruff check ruoyi-fastapi-backend ruoyi-fastapi-test .github/scripts .github/tests
|
||||
ruff format --check ruoyi-fastapi-backend ruoyi-fastapi-test .github/scripts .github/tests
|
||||
- name: Test CI routing, failure gates and deployment contracts
|
||||
run: python -m pytest .github/tests -q --junitxml=target/ci/quality.xml
|
||||
- name: Validate every Compose framework selection
|
||||
run: python .github/scripts/check_compose.py
|
||||
- name: Install actionlint
|
||||
env:
|
||||
ACTIONLINT_VERSION: '1.7.12'
|
||||
run: |
|
||||
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
||||
curl --fail --location --retry 3 "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}" --output "${RUNNER_TEMP}/${archive}"
|
||||
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 ${RUNNER_TEMP}/${archive}" | sha256sum --check
|
||||
tar -xzf "${RUNNER_TEMP}/${archive}" -C "$RUNNER_TEMP" actionlint
|
||||
"$RUNNER_TEMP/actionlint" -color
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: ruoyi-fastapi-frontend/*/web/package.json
|
||||
- name: Check Web formatting with the existing project configurations
|
||||
run: |
|
||||
for framework in vue2 vue3; do
|
||||
(cd "ruoyi-fastapi-frontend/$framework/web" && npx --yes prettier@3.6.2 --check .)
|
||||
done
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: quality-reports
|
||||
path: target/ci/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,106 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '21 18 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
full:
|
||||
description: Run the full compatibility and end-to-end matrix
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
name: Plan
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
backend: ${{ steps.plan.outputs.backend }}
|
||||
frontend: ${{ steps.plan.outputs.frontend }}
|
||||
integration: ${{ steps.plan.outputs.integration }}
|
||||
e2e: ${{ steps.plan.outputs.e2e }}
|
||||
native: ${{ steps.plan.outputs.native }}
|
||||
full: ${{ steps.plan.outputs.full }}
|
||||
frontend_matrix: ${{ steps.plan.outputs.frontend_matrix }}
|
||||
e2e_matrix: ${{ steps.plan.outputs.e2e_matrix }}
|
||||
network_frameworks: ${{ steps.plan.outputs.network_frameworks }}
|
||||
database: ${{ steps.plan.outputs.database }}
|
||||
release: ${{ steps.plan.outputs.release }}
|
||||
plan_json: ${{ steps.plan.outputs.plan_json }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: plan
|
||||
run: python .github/scripts/ci_plan.py plan
|
||||
|
||||
quality:
|
||||
name: Quality
|
||||
needs: plan
|
||||
uses: ./.github/workflows/ci-quality.yml
|
||||
|
||||
backend:
|
||||
name: Backend
|
||||
needs: plan
|
||||
if: needs.plan.outputs.backend == 'true'
|
||||
uses: ./.github/workflows/ci-backend.yml
|
||||
|
||||
frontend:
|
||||
name: Frontend
|
||||
needs: plan
|
||||
if: needs.plan.outputs.frontend == 'true'
|
||||
uses: ./.github/workflows/ci-frontend.yml
|
||||
with:
|
||||
matrix_json: ${{ needs.plan.outputs.frontend_matrix }}
|
||||
full: ${{ needs.plan.outputs.full == 'true' }}
|
||||
|
||||
integration:
|
||||
name: Integration
|
||||
needs: plan
|
||||
if: needs.plan.outputs.integration == 'true'
|
||||
uses: ./.github/workflows/ci-integration.yml
|
||||
with:
|
||||
database: ${{ needs.plan.outputs.database == 'true' }}
|
||||
release: ${{ needs.plan.outputs.release == 'true' }}
|
||||
network_frameworks: ${{ needs.plan.outputs.network_frameworks }}
|
||||
full: ${{ needs.plan.outputs.full == 'true' }}
|
||||
|
||||
e2e:
|
||||
name: E2E
|
||||
needs: plan
|
||||
if: needs.plan.outputs.e2e == 'true'
|
||||
uses: ./.github/workflows/ci-e2e.yml
|
||||
with:
|
||||
matrix_json: ${{ needs.plan.outputs.e2e_matrix }}
|
||||
|
||||
native:
|
||||
name: Native
|
||||
needs: plan
|
||||
if: needs.plan.outputs.native == 'true'
|
||||
uses: ./.github/workflows/ci-native.yml
|
||||
|
||||
result:
|
||||
name: Result
|
||||
if: always()
|
||||
needs: [plan, quality, backend, frontend, integration, e2e, native]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Require every selected category to succeed
|
||||
env:
|
||||
PLAN_JSON: ${{ needs.plan.outputs.plan_json }}
|
||||
NEEDS_JSON: ${{ toJSON(needs) }}
|
||||
run: python .github/scripts/ci_plan.py gate
|
||||
@@ -1,97 +0,0 @@
|
||||
name: Native Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-backend/plugins/core/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/rust/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
|
||||
- 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
|
||||
- 'ruoyi-fastapi-backend/scripts/build_native_plugin.py'
|
||||
- 'ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/**'
|
||||
- 'ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/artifact*.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/artifact_controller.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/command.py'
|
||||
- 'ruoyi-fastapi-frontend/src/utils/pluginBridge*'
|
||||
- 'ruoyi-fastapi-backend/pyproject.toml'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold*.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_sdk.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
|
||||
- 'ruoyi-fastapi-backend/tests/plugins/**'
|
||||
- '.github/workflows/native-plugin.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-backend/plugins/core/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/rust/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
|
||||
- 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
|
||||
- 'ruoyi-fastapi-backend/scripts/build_native_plugin.py'
|
||||
- 'ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/**'
|
||||
- 'ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/commands/artifact*.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/artifact_controller.py'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/command.py'
|
||||
- 'ruoyi-fastapi-frontend/src/utils/pluginBridge*'
|
||||
- 'ruoyi-fastapi-backend/pyproject.toml'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold*.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_sdk.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
|
||||
- 'ruoyi-fastapi-backend/tests/plugins/**'
|
||||
- '.github/workflows/native-plugin.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
native:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python: ['3.10', '3.11', '3.12', '3.13']
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements.txt
|
||||
- name: Install host and build dependencies
|
||||
run: python -m pip install -r requirements.txt 'maturin>=1.15,<2' pytest pytest-asyncio aiosqlite
|
||||
- name: Install Rust
|
||||
run: rustup toolchain install 1.99.0 --profile minimal --component rustfmt --component clippy
|
||||
- name: Select Rust
|
||||
run: rustup default 1.99.0
|
||||
- name: Check Rust formatting
|
||||
run: cargo fmt --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --check
|
||||
- name: Verify v1 and v2 plugin scaffolds
|
||||
run: python -m pytest tests/cli/runtime/plugin/test_runtime_scaffold.py tests/cli/runtime/plugin/test_runtime_scaffold_v2.py -q
|
||||
- name: Verify SDK provenance and offline update commands
|
||||
run: python -m pytest tests/cli/runtime/plugin/test_runtime_sdk.py tests/cli/root/test_plugin_sdk_commands.py -q
|
||||
- name: Verify persistent task plugin
|
||||
run: python -m pytest tests/plugins/examples/test_task_demo.py -q
|
||||
- name: Build and assemble source-free plugin
|
||||
run: python scripts/build_native_plugin.py --output target/native-package
|
||||
- name: Check Rust code
|
||||
run: cargo clippy --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --locked -- -D warnings
|
||||
- name: Exercise native library through the host runtime
|
||||
env:
|
||||
RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
|
||||
run: python -m pytest tests/plugins/native tests/plugins/core/native tests/plugins/core/runtime/test_host_services.py tests/plugins/core/runtime/test_job_dispatcher.py -q
|
||||
- name: Verify signed artifacts and maintenance releases
|
||||
env:
|
||||
RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
|
||||
run: python -m pytest tests/plugins/core/artifacts tests/plugins/core/deployment tests/plugins/core/management/test_release_dao.py tests/plugins/core/management/test_artifact_views.py tests/module_plugin/controller/test_plugin_release_controller.py tests/cli/root/test_plugin_artifact_commands.py tests/cli/root/test_plugin_release_wait.py tests/sql/test_plugin_release_schema.py -q
|
||||
@@ -1,174 +0,0 @@
|
||||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
mysql-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.docker_cache
|
||||
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
|
||||
run: |
|
||||
# Save original Dockerfile.my
|
||||
cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
|
||||
# Create temporary Dockerfile with target Python version and default PyPI index
|
||||
sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
|
||||
-e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
|
||||
ruoyi-fastapi-backend/Dockerfile.my.bak > ruoyi-fastapi-backend/Dockerfile.my
|
||||
|
||||
- name: Start services with docker-compose
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.my.yml up -d --build
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
# Wait for backend to be running
|
||||
timeout 180 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-backend-my | grep -q "Up"; do sleep 5; done'
|
||||
# Wait for frontend to be running
|
||||
timeout 120 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
|
||||
# Additional wait for services to be fully ready and listening on ports
|
||||
sleep 30
|
||||
# Check that backend is actually responding on the API endpoint
|
||||
echo "Checking if backend service is ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service is ready!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for backend service to be ready... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
# Final check
|
||||
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service failed to start properly. Checking logs..."
|
||||
docker logs ruoyi-backend-my-test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
pip install -r requirements.txt
|
||||
playwright install
|
||||
pytest -v
|
||||
|
||||
- name: Stop services
|
||||
if: always()
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.my.yml down
|
||||
|
||||
- name: Restore original Dockerfile.my
|
||||
if: always()
|
||||
run: |
|
||||
# Restore original Dockerfile.my after test
|
||||
mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
|
||||
|
||||
pg-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: /tmp/.docker_cache
|
||||
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
|
||||
run: |
|
||||
# Save original Dockerfile.pg
|
||||
cp ruoyi-fastapi-backend/Dockerfile.pg ruoyi-fastapi-backend/Dockerfile.pg.bak
|
||||
# Create temporary Dockerfile with target Python version and default PyPI index
|
||||
sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
|
||||
-e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
|
||||
ruoyi-fastapi-backend/Dockerfile.pg.bak > ruoyi-fastapi-backend/Dockerfile.pg
|
||||
|
||||
- name: Start services with docker-compose
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.pg.yml up -d --build
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
# Wait for backend to be running
|
||||
timeout 180 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-backend-pg | grep -q "Up"; do sleep 5; done'
|
||||
# Wait for frontend to be running
|
||||
timeout 120 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
|
||||
# Additional wait for services to be fully ready and listening on ports
|
||||
sleep 30
|
||||
# Check that backend is actually responding on the API endpoint
|
||||
echo "Checking if backend service is ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service is ready!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for backend service to be ready... ($i/30)"
|
||||
sleep 5
|
||||
done
|
||||
# Final check
|
||||
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
|
||||
echo "Backend service failed to start properly. Checking logs..."
|
||||
docker logs ruoyi-backend-pg-test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
pip install -r requirements.txt
|
||||
playwright install
|
||||
pytest -v
|
||||
|
||||
- name: Stop services
|
||||
if: always()
|
||||
run: |
|
||||
cd ruoyi-fastapi-test
|
||||
docker compose -f docker-compose.test.pg.yml down
|
||||
|
||||
- name: Restore original Dockerfile.pg
|
||||
if: always()
|
||||
run: |
|
||||
# Restore original Dockerfile.pg after test
|
||||
mv ruoyi-fastapi-backend/Dockerfile.pg.bak ruoyi-fastapi-backend/Dockerfile.pg
|
||||
@@ -1,69 +0,0 @@
|
||||
name: Plugin Frontend Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-frontend/src/api/system/plugin.js'
|
||||
- 'ruoyi-fastapi-frontend/src/components/PluginFrame/**'
|
||||
- 'ruoyi-fastapi-frontend/src/views/system/plugin/**'
|
||||
- 'ruoyi-fastapi-frontend/src/utils/**'
|
||||
- 'ruoyi-fastapi-frontend/src/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/src/router/**'
|
||||
- 'ruoyi-fastapi-frontend/src/store/modules/permission.js'
|
||||
- 'ruoyi-fastapi-frontend/src/store/modules/user.js'
|
||||
- 'ruoyi-fastapi-frontend/src/permission.js'
|
||||
- 'ruoyi-fastapi-frontend/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/tests/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/tests/run-tests.js'
|
||||
- 'ruoyi-fastapi-frontend/package.json'
|
||||
- 'ruoyi-fastapi-frontend/package-lock.json'
|
||||
- 'ruoyi-fastapi-frontend/vite.config.js'
|
||||
- 'ruoyi-fastapi-frontend/vite/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/**'
|
||||
- '.github/workflows/plugin-frontend.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-frontend/src/api/system/plugin.js'
|
||||
- 'ruoyi-fastapi-frontend/src/components/PluginFrame/**'
|
||||
- 'ruoyi-fastapi-frontend/src/views/system/plugin/**'
|
||||
- 'ruoyi-fastapi-frontend/src/utils/**'
|
||||
- 'ruoyi-fastapi-frontend/src/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/src/router/**'
|
||||
- 'ruoyi-fastapi-frontend/src/store/modules/permission.js'
|
||||
- 'ruoyi-fastapi-frontend/src/store/modules/user.js'
|
||||
- 'ruoyi-fastapi-frontend/src/permission.js'
|
||||
- 'ruoyi-fastapi-frontend/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/tests/plugins/**'
|
||||
- 'ruoyi-fastapi-frontend/tests/run-tests.js'
|
||||
- 'ruoyi-fastapi-frontend/package.json'
|
||||
- 'ruoyi-fastapi-frontend/package-lock.json'
|
||||
- 'ruoyi-fastapi-frontend/vite.config.js'
|
||||
- 'ruoyi-fastapi-frontend/vite/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/**'
|
||||
- '.github/workflows/plugin-frontend.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
plugins:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: ruoyi-fastapi-frontend/package.json
|
||||
- name: Install frontend dependencies
|
||||
# This repository does not commit npm lockfiles, so npm ci is not available.
|
||||
run: npm install --no-audit --no-fund --package-lock=false
|
||||
- name: Verify plugin bridge, sessions and management views
|
||||
run: npm run test:plugin
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Plugin Network Smoke
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-backend/plugins/**'
|
||||
- 'ruoyi-fastapi-backend/module_plugin/**'
|
||||
- 'ruoyi-fastapi-backend/module_admin/service/login_service.py'
|
||||
- 'ruoyi-fastapi-backend/common/**'
|
||||
- 'ruoyi-fastapi-backend/config/**'
|
||||
- 'ruoyi-fastapi-backend/middlewares/**'
|
||||
- 'ruoyi-fastapi-backend/utils/**'
|
||||
- 'ruoyi-fastapi-backend/tests/**'
|
||||
- 'ruoyi-fastapi-backend/requirements.txt'
|
||||
- 'ruoyi-fastapi-backend/pyproject.toml'
|
||||
- 'ruoyi-fastapi-frontend/**'
|
||||
- '.github/workflows/plugin-network-smoke.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
browser:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
env:
|
||||
RUOYI_PLUGIN_NETWORK_SMOKE: '1'
|
||||
# The fixture replaces account, DB and Redis access; no external service is used.
|
||||
DB_SOURCES: '{"primary":{"db_type":"mysql","db_host":"127.0.0.1","db_port":3306,"db_username":"network-smoke","db_password":"network-smoke","db_database":"network-smoke"}}'
|
||||
JWT_SECRET_KEY: 'isolated-plugin-network-smoke-ci-only'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements.txt
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: ruoyi-fastapi-frontend/package.json
|
||||
- name: Install host and browser test dependencies
|
||||
run: python -m pip install -r requirements.txt pytest pytest-asyncio aiosqlite playwright
|
||||
- name: Install Chromium
|
||||
run: python -m playwright install --with-deps chromium
|
||||
- name: Install frontend dependencies
|
||||
# This repository does not commit npm lockfiles, so npm ci is not available.
|
||||
run: npm --prefix ../ruoyi-fastapi-frontend install --no-audit --no-fund --package-lock=false
|
||||
- name: Verify HTTPS proxy, iframe sessions and long connections
|
||||
run: python -m pytest tests/plugins/integration/test_network_smoke.py -v --junitxml=target/plugin-network-smoke/results.xml
|
||||
- name: Upload browser trace, screenshots and proxy log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-network-smoke
|
||||
path: ruoyi-fastapi-backend/target/plugin-network-smoke/
|
||||
retention-days: 7
|
||||
@@ -1,94 +0,0 @@
|
||||
name: Plugin Release Integration
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'ruoyi-fastapi-backend/plugins/core/**'
|
||||
- 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
|
||||
- 'ruoyi-fastapi-frontend/src/utils/pluginBridge.js'
|
||||
- 'ruoyi-fastapi-backend/config/**'
|
||||
- 'ruoyi-fastapi-backend/cli/groups/plugin/**'
|
||||
- 'ruoyi-fastapi-backend/cli/runtime/plugin/**'
|
||||
- 'ruoyi-fastapi-backend/module_plugin/**'
|
||||
- 'ruoyi-fastapi-backend/server.py'
|
||||
- 'ruoyi-fastapi-backend/app.py'
|
||||
- 'ruoyi-fastapi-backend/sql/ruoyi-fastapi*.sql'
|
||||
- 'ruoyi-fastapi-backend/scripts/plugin_release_integration.py'
|
||||
- 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
|
||||
- 'ruoyi-fastapi-backend/tests/plugins/**'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_diagnostics.py'
|
||||
- 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/**'
|
||||
- 'ruoyi-fastapi-backend/tests/module_plugin/**'
|
||||
- 'ruoyi-fastapi-backend/tests/server/test_plugin_runtime.py'
|
||||
- 'ruoyi-fastapi-backend/tests/sql/test_plugin_release_schema.py'
|
||||
- 'ruoyi-fastapi-backend/pyproject.toml'
|
||||
- 'ruoyi-fastapi-backend/requirements.txt'
|
||||
- 'ruoyi-fastapi-backend/requirements-pg.txt'
|
||||
- '.github/workflows/plugin-release-integration.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ['3.10', '3.11', '3.12', '3.13']
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: ci-only-mysql
|
||||
ports: ['3306:3306']
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h localhost"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: ci-only-postgres
|
||||
ports: ['5432:5432']
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U postgres"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
redis:
|
||||
image: redis:6.2
|
||||
ports: ['6379:6379']
|
||||
options: >-
|
||||
--health-cmd="redis-cli ping"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
env:
|
||||
RUOYI_PLUGIN_IT_ALLOW: '1'
|
||||
RUOYI_IT_MYSQL_PASSWORD: ci-only-mysql
|
||||
RUOYI_IT_PG_PASSWORD: ci-only-postgres
|
||||
RUOYI_IT_REDIS_PASSWORD: ''
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- name: Install host dependencies
|
||||
run: python -m pip install -r requirements.txt -r requirements-pg.txt
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Test and build task plugin bundle
|
||||
run: |
|
||||
npm --prefix plugins/examples/python/task_demo/web install --no-audit --no-fund --package-lock=false
|
||||
npm --prefix plugins/examples/python/task_demo/web test
|
||||
npm --prefix plugins/examples/python/task_demo/web run build
|
||||
- name: Verify MySQL and Redis maintenance releases
|
||||
run: python scripts/plugin_release_integration.py --engine mysql
|
||||
- name: Verify PostgreSQL and Redis maintenance releases
|
||||
run: python scripts/plugin_release_integration.py --engine postgresql
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Ruff Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install ruff
|
||||
|
||||
- name: Run linting
|
||||
run: |
|
||||
ruff check ruoyi-fastapi-backend
|
||||
ruff check ruoyi-fastapi-test
|
||||
|
||||
- name: Run format check
|
||||
run: |
|
||||
ruff format ruoyi-fastapi-backend --check
|
||||
ruff format ruoyi-fastapi-test --check
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Timezone Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
databases:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TZ: America/New_York
|
||||
PYTHONUTF8: '1'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: timezone_test
|
||||
TZ: Asia/Shanghai
|
||||
ports: ['3306:3306']
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h localhost -ptimezone_test"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: timezone_test
|
||||
TZ: America/New_York
|
||||
PGTZ: America/New_York
|
||||
ports: ['5432:5432']
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U postgres"
|
||||
--health-interval=10s --health-timeout=5s --health-retries=12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
|
||||
- name: Install verification dependencies
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
run: python -m pip install -r requirements.txt asyncpg psycopg2-binary pytest pytest-asyncio aiosqlite
|
||||
- name: Create isolated source configurations
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
run: |
|
||||
mkdir -p .cache/timezone-ci
|
||||
python - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
for dialect, port, username, database in [('mysql', 3306, 'root', 'mysql'), ('postgresql', 5432, 'postgres', 'postgres')]:
|
||||
source = dict(db_type=dialect, db_host='127.0.0.1', db_port=port, db_username=username,
|
||||
db_password='timezone_test', db_database=database)
|
||||
Path(f'.cache/timezone-ci/{dialect}.json').write_text(json.dumps(source))
|
||||
PY
|
||||
- name: Verify empty databases, drivers, generated CRUD and persistent schedulers
|
||||
working-directory: ruoyi-fastapi-backend
|
||||
run: |
|
||||
python -m scripts.verify_timezone_database --config .cache/timezone-ci/mysql.json --report .cache/timezone-ci/mysql-report.json
|
||||
python -m scripts.verify_timezone_database --config .cache/timezone-ci/postgresql.json --report .cache/timezone-ci/postgresql-report.json
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: timezone-database-reports
|
||||
path: ruoyi-fastapi-backend/.cache/timezone-ci/*-report.json
|
||||
include-hidden-files: true
|
||||
|
||||
web-app:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
# Lockfiles are ignored by this repository; cache by the committed manifest.
|
||||
cache-dependency-path: ruoyi-fastapi-frontend/package.json
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
package_json_file: ruoyi-fastapi-app/package.json
|
||||
- name: Install Web dependencies
|
||||
working-directory: ruoyi-fastapi-frontend
|
||||
run: npm install --no-package-lock
|
||||
- name: Install App dependencies
|
||||
working-directory: ruoyi-fastapi-app
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run time contracts in three host timezones
|
||||
run: node ruoyi-fastapi-test/time-contract/run-host-timezones.mjs
|
||||
- name: Build Web targets
|
||||
working-directory: ruoyi-fastapi-frontend
|
||||
run: |
|
||||
npm run build:prod
|
||||
npm run build:docker
|
||||
- name: Build App targets
|
||||
working-directory: ruoyi-fastapi-app
|
||||
run: |
|
||||
pnpm build:h5
|
||||
pnpm build:mp-weixin
|
||||
- name: Validate all Compose configurations
|
||||
run: |
|
||||
docker compose -f docker-compose.my.yml config --quiet
|
||||
docker compose -f docker-compose.pg.yml config --quiet
|
||||
docker compose -f ruoyi-fastapi-test/docker-compose.test.my.yml config --quiet
|
||||
docker compose -f ruoyi-fastapi-test/docker-compose.test.pg.yml config --quiet
|
||||
Reference in New Issue
Block a user