From a28c88f2a73f8c86d784bc56172b32326bbe6163 Mon Sep 17 00:00:00 2001 From: zaodonganqi <3193156071@qq.com> Date: Sun, 12 Oct 2025 20:36:38 +0800 Subject: [PATCH] =?UTF-8?q?js=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/jsValidation.yml | 225 +++++++++++++++++++++ build/validate_js.py | 313 +++++++++++++++++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 .github/workflows/jsValidation.yml create mode 100644 build/validate_js.py diff --git a/.github/workflows/jsValidation.yml b/.github/workflows/jsValidation.yml new file mode 100644 index 000000000..3e95d799f --- /dev/null +++ b/.github/workflows/jsValidation.yml @@ -0,0 +1,225 @@ +name: JS Script Validation + +on: + pull_request_target: + types: [opened, synchronize, reopened, edited] + branches: + - main + paths: + - 'repo/js/**/*.js' + - 'repo/js/**/*.json' + workflow_dispatch: + inputs: + path: + description: '要验证的路径 (repo/js下的文件或目录)' + required: true + default: 'repo/js' + type: string + auto_fix: + description: '是否自动修复问题' + required: false + default: true + type: boolean + +jobs: + validate-js: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip install packaging semver + pip install chardet + + - name: Setup Git + run: | + git config --global user.name "GitHub Actions Bot" + git config --global user.email "actions@github.com" + git config --global core.quotepath false + + # 设置upstream远程仓库指向当前仓库 + CURRENT_REPO="${{ github.repository }}" + git remote remove upstream 2>/dev/null || true + git remote add upstream https://github.com/${CURRENT_REPO}.git + + # 获取upstream分支 + git fetch upstream main + + echo "远程仓库配置:" + git remote -v + + - name: Get changed files + id: changed_files + if: ${{ github.event_name == 'pull_request_target' }} + run: | + # 获取修改的JS和JSON文件 + echo "当前分支: $(git branch --show-current)" + echo "HEAD指向: $(git rev-parse HEAD)" + echo "获取修改的文件..." + + # 方法1:使用git diff检测变化 + CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD | grep -E '^repo/js/.*\.(js|json)$' || true) + echo "方法1找到的文件:" + echo "$CHANGED_FILES" + + # 方法2:如果方法1没找到,使用GitHub API + if [ -z "$CHANGED_FILES" ]; then + echo "方法1未找到文件,尝试使用GitHub API" + CHANGED_FILES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" | \ + jq -r '.[] | select(.filename | test("^repo/js/.*\\.(js|json)$")) | .filename' || true) + echo "方法2找到的文件:" + echo "$CHANGED_FILES" + fi + + # 回退方案:如果还是没找到,验证整个JS目录 + if [ -z "$CHANGED_FILES" ]; then + echo "未找到修改的文件,验证整个JS目录" + CHANGED_FILES="repo/js" + fi + + echo "最终文件列表:" + echo "$CHANGED_FILES" + echo "changed_files=$(echo "$CHANGED_FILES" | base64 -w 0)" >> $GITHUB_OUTPUT + + - name: Run JS validation + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + CHANGED_FILES_B64: ${{ steps.changed_files.outputs.changed_files }} + run: | + set -e + + # 设置验证路径 + if [ "${{ github.event_name }}" = "pull_request_target" ]; then + CHANGED_FILES=$(echo "$CHANGED_FILES_B64" | base64 --decode) + echo "PR触发模式,验证修改的文件:" + echo "$CHANGED_FILES" + + # 直接处理文件列表,不创建临时文件 + if [ "$CHANGED_FILES" = "repo/js" ]; then + # 如果回退到整个目录,直接验证目录 + python build/validate_js.py "repo/js" --fix + else + # 处理每个修改的文件 + echo "$CHANGED_FILES" | while read -r file_path; do + if [ -n "$file_path" ] && [ -f "$file_path" ]; then + echo "处理文件: $file_path" + python build/validate_js.py "$file_path" --fix + + # 如果是JS文件,检查对应目录的manifest.json + if [[ "$file_path" == *.js ]]; then + file_dir=$(dirname "$file_path") + manifest_path="$file_dir/manifest.json" + if [ -f "$manifest_path" ]; then + echo "检查对应目录的manifest.json: $manifest_path" + python build/validate_js.py "$manifest_path" --fix + fi + fi + fi + done + fi + else + VALIDATE_PATH="${{ github.event.inputs.path }}" + echo "手动触发模式,验证路径: ${VALIDATE_PATH}" + python build/validate_js.py "${VALIDATE_PATH}" --fix + fi + + # 检查是否有文件被修改 + if [ -n "$(git status --porcelain)" ]; then + echo "发现修改,检查修改的文件:" + git status --porcelain + + # 只添加manifest.json文件的修改 + MANIFEST_FILES=$(git status --porcelain | grep "manifest.json" | awk '{print $2}') + if [ -n "$MANIFEST_FILES" ]; then + echo "提交manifest.json版本号修改:" + echo "$MANIFEST_FILES" + + # 检查是否只有版本号被修改 + for manifest_file in $MANIFEST_FILES; do + echo "检查文件修改内容: $manifest_file" + git diff "$manifest_file" | head -20 + + # 只添加这个特定的manifest.json文件 + git add "$manifest_file" + done + + git commit -m "自动更新manifest.json版本号 [ci skip]" + + # 推送到PR分支 + if [ "${{ github.event_name }}" = "pull_request_target" ] && [ -n "$HEAD_REF" ]; then + echo "PR触发,推送到PR分支: ${HEAD_REF}" + # 先拉取最新代码,然后推送 + git fetch origin ${HEAD_REF} + git push origin HEAD:${HEAD_REF} --force-with-lease + elif [ -n "$PR_NUMBER" ] && [ -n "$HEAD_REF" ]; then + echo "提交更改到PR: #$PR_NUMBER" + + # 确定当前分支 + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "当前分支: ${CURRENT_BRANCH}" + + if [ "$CURRENT_BRANCH" = "HEAD" ]; then + # 如果在detached HEAD状态,使用正确的方式推送 + echo "在detached HEAD状态,使用HEAD_REF推送: ${HEAD_REF}" + git push origin HEAD:${HEAD_REF} + else + # 常规推送 + echo "推送到分支: ${CURRENT_BRANCH}" + git push origin ${CURRENT_BRANCH} + fi + else + echo "未关联PR或无法确定分支,直接提交到main分支" + git push origin HEAD + fi + else + echo "没有manifest.json文件被修改,无需提交" + fi + else + echo "没有文件被修改,无需提交" + fi + + - name: Add PR comment + if: ${{ github.event_name == 'pull_request_target' }} + continue-on-error: true + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { execSync } = require('child_process'); + let commitMessage = ''; + + try { + const lastCommit = execSync('git log -1 --pretty=%B').toString().trim(); + if (lastCommit.includes('自动修复')) { + commitMessage = '✅ JS脚本校验完成并自动修复了一些问题。修改已提交到PR中。'; + } else { + commitMessage = '✅ JS脚本校验完成,没有发现需要修复的问题'; + } + } catch (error) { + commitMessage = '✅ JS脚本校验完成,没有发现需要修复的问题'; + } + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: commitMessage + }); \ No newline at end of file diff --git a/build/validate_js.py b/build/validate_js.py new file mode 100644 index 000000000..7a7ece961 --- /dev/null +++ b/build/validate_js.py @@ -0,0 +1,313 @@ +import json +import os +import subprocess +import re +from packaging.version import parse +from semver import VersionInfo + +# ==================== 配置和常量 ==================== + +DEFAULT_VERSION = "1.0.0" + +# ==================== 文件操作 ==================== + +def load_json_file(file_path): + """加载 JSON 文件""" + try: + with open(file_path, encoding='utf-8') as f: + return json.load(f), None + except Exception as e: + return None, f"❌ JSON 格式错误: {str(e)}" + +def save_json_file(file_path, old_version, new_version): + """保存 JSON 文件,只修改版本号,保持原有格式""" + try: + # 读取原文件内容 + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 使用简单的字符串替换 + old_version_str = f'"{old_version}"' + new_version_str = f'"{new_version}"' + + if old_version_str in content: + new_content = content.replace(old_version_str, new_version_str, 1) + + # 写回文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content) + return True + else: + print(f"未找到版本号: {old_version}") + return False + except Exception as e: + print(f"保存文件失败: {str(e)}") + return False + +# ==================== 版本处理 ==================== + +def get_original_manifest_version(file_path): + """从上游仓库获取原始manifest.json的版本号""" + try: + # 直接从upstream/main获取原始版本 + result = subprocess.run(['git', 'show', f'upstream/main:{file_path}'], + capture_output=True, text=True, encoding='utf-8') + if result.returncode == 0 and result.stdout.strip(): + data = json.loads(result.stdout) + version = data.get('version', DEFAULT_VERSION) + print(f"🔍 获取到原始版本号: {version} (来自 upstream/main:{file_path})") + return version + else: + print(f"⚠️ 无法从upstream/main获取原始版本: {file_path}") + print(f"错误信息: {result.stderr}") + except Exception as e: + print(f"⚠️ 获取原始版本时出错: {str(e)}") + + # 如果无法获取,返回None表示无法比较 + print(f"⚠️ 无法获取原始版本号,跳过版本号检查") + return None + +def increment_version(version_str): + """增加版本号,在最后一个数字上+1""" + try: + # 解析版本号 + version_parts = version_str.split('.') + if len(version_parts) >= 2: + # 将最后一个部分转换为数字并+1 + last_part = int(version_parts[-1]) + version_parts[-1] = str(last_part + 1) + return '.'.join(version_parts) + else: + # 如果版本号格式不正确,返回默认版本 + return DEFAULT_VERSION + except (ValueError, IndexError): + return DEFAULT_VERSION + +def process_manifest_version(manifest_path, auto_fix=False): + """处理manifest.json的版本号""" + corrections = [] + + try: + print(f"🔍 处理manifest.json版本号: {manifest_path}") + + # 加载当前manifest.json + data, error = load_json_file(manifest_path) + if error: + return error, corrections + + current_version = data.get('version', DEFAULT_VERSION) + print(f"📋 当前版本号: {current_version}") + + # 获取原始版本号 + original_version = get_original_manifest_version(manifest_path) + + # 如果无法获取原始版本号,跳过版本号检查 + if original_version is None: + print(f"⚠️ 无法获取原始版本号,跳过版本号检查") + return None, corrections + + print(f"📋 原始版本号: {original_version}") + + # 检查版本号是否增加 + if current_version == original_version: + print(f"⚠️ 版本号未增加: {current_version} == {original_version}") + if auto_fix: + # 自动增加版本号 + new_version = increment_version(current_version) + print(f"🔄 自动增加版本号: {current_version} → {new_version}") + corrections.append(f"版本号已自动更新: {current_version} → {new_version}") + + # 保存文件 + if save_json_file(manifest_path, current_version, new_version): + print(f"✅ {manifest_path}: 版本号已自动更新: {current_version} → {new_version}") + else: + corrections.append(f"保存文件失败: {manifest_path}") + else: + corrections.append(f"版本号未增加: {current_version} (与原始版本相同)") + else: + print(f"✅ {manifest_path}: 版本号已更新: {original_version} → {current_version}") + + except Exception as e: + error_msg = f"处理manifest.json版本号时出错: {str(e)}" + print(f"❌ {error_msg}") + return error_msg, corrections + + return None, corrections + +# ==================== JS语法校验 ==================== + +def validate_js_syntax(js_file_path): + """校验JS文件语法""" + errors = [] + try: + # 使用Node.js -c 参数进行语法检查 + result = subprocess.run(['node', '-c', js_file_path], capture_output=True, text=True, check=True) + if result.returncode == 0: + print(f"✅ {js_file_path}: JS语法正确") + else: + errors.append(f"❌ {js_file_path}: JS语法错误: {result.stderr.strip()}") + print(f"❌ {js_file_path}: JS语法错误: {result.stderr.strip()}") + except subprocess.CalledProcessError as e: + errors.append(f"❌ {js_file_path}: JS语法错误: {e.stderr.strip()}") + print(f"❌ {js_file_path}: JS语法错误: {e.stderr.strip()}") + except FileNotFoundError: + errors.append(f"❌ Node.js 未安装或不在PATH中,无法校验JS语法。") + print(f"❌ Node.js 未安装或不在PATH中,无法校验JS语法。") + return errors + +# ==================== 核心校验逻辑 ==================== + +def validate_js_file(file_path, auto_fix=False): + """校验单个JS或JSON文件""" + errors = [] + corrections = [] + + if file_path.lower().endswith('.json'): + if os.path.basename(file_path).lower() == 'manifest.json': + # 处理manifest.json的版本号 + version_error, version_corrections = process_manifest_version(file_path, auto_fix) + if version_error: + errors.append(version_error) + corrections.extend(version_corrections) + + # 校验JSON格式 + data, json_error = load_json_file(file_path) + if json_error: + errors.append(f"❌ {file_path}: {json_error}") + print(f"❌ {file_path}: {json_error}") + else: + print(f"✅ {file_path}: JSON格式正确") + + elif file_path.lower().endswith('.js'): + # 校验JS语法 + js_errors = validate_js_syntax(file_path) + errors.extend(js_errors) + + return errors, corrections + +def validate_js_directory(dir_path, auto_fix=False): + """校验JS目录下的所有文件""" + all_errors = [] + all_corrections = [] + + try: + for root, dirs, files in os.walk(dir_path): + for file in files: + file_path = os.path.join(root, file) + file_ext = os.path.splitext(file)[1].lower() + + # 处理所有JS和JSON文件 + if file_ext in ['.js', '.json']: + errors, corrections = validate_js_file(file_path, auto_fix) + all_errors.extend(errors) + all_corrections.extend(corrections) + + except Exception as e: + error_msg = f"❌ 遍历目录时出错: {str(e)}" + all_errors.append(error_msg) + print(error_msg) + + return all_errors, all_corrections + +def validate_js_files_from_list(file_list_path, auto_fix=False): + """从文件列表校验JS文件,并处理对应目录的manifest.json""" + all_errors = [] + all_corrections = [] + processed_manifests = set() # 避免重复处理同一个manifest.json + + try: + print(f"📋 读取文件列表: {file_list_path}") + with open(file_list_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + print(f"📋 文件列表内容 ({len(lines)} 行):") + for i, line in enumerate(lines): + file_path = line.strip() + print(f" {i+1}: {file_path}") + if file_path and os.path.exists(file_path): + print(f" ✅ 文件存在,开始校验") + errors, corrections = validate_js_file(file_path, auto_fix) + all_errors.extend(errors) + all_corrections.extend(corrections) + + # 如果是JS文件,检查对应目录的manifest.json + if file_path.lower().endswith('.js'): + # 获取文件所在目录 + file_dir = os.path.dirname(file_path) + manifest_path = os.path.join(file_dir, 'manifest.json') + + # 检查manifest.json是否存在且未被处理过 + if os.path.exists(manifest_path) and manifest_path not in processed_manifests: + print(f" 🔍 检查对应目录的manifest.json: {manifest_path}") + processed_manifests.add(manifest_path) + errors, corrections = validate_js_file(manifest_path, auto_fix) + all_errors.extend(errors) + all_corrections.extend(corrections) + elif not os.path.exists(manifest_path): + print(f" ⚠️ 未找到对应的manifest.json: {manifest_path}") + else: + print(f" ❌ 文件不存在或为空") + + except Exception as e: + error_msg = f"❌ 读取文件列表时出错: {str(e)}" + all_errors.append(error_msg) + print(error_msg) + + return all_errors, all_corrections + +# ==================== 主函数 ==================== + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='校验 JS 脚本文件') + parser.add_argument('path', help='要校验的文件或目录路径') + parser.add_argument('--fix', action='store_true', help='自动修复问题') + args = parser.parse_args() + + path = args.path + auto_fix = args.fix + + print(f"🔍 开始JS脚本校验: {path}") + + all_errors = [] + all_corrections = [] + + if os.path.isfile(path): + # 单个文件 + if path.endswith('.txt'): + # 文件列表 + print(f"📋 检测到文件列表: {path}") + errors, corrections = validate_js_files_from_list(path, auto_fix) + else: + # 单个JS或JSON文件 + print(f"📄 检测到单个文件: {path}") + errors, corrections = validate_js_file(path, auto_fix) + all_errors.extend(errors) + all_corrections.extend(corrections) + + elif os.path.isdir(path): + # 目录 + print(f"📁 检测到目录: {path}") + errors, corrections = validate_js_directory(path, auto_fix) + all_errors.extend(errors) + all_corrections.extend(corrections) + + else: + print(f"❌ 无效的路径: {path}") + exit(1) + + # 输出结果 + if all_errors: + print("\n❌ 发现以下错误:") + for error in all_errors: + print(f"- {error}") + exit(1) + elif all_corrections: + print("\n✅ JS脚本校验完成,并自动修复了以下问题:") + for correction in all_corrections: + print(f"- {correction}") + else: + print("\n✅ JS脚本校验完成,没有发现问题") + +if __name__ == "__main__": + main() \ No newline at end of file