diff --git a/.github/workflows/renovate-app-version.py b/.github/workflows/renovate-app-version.py new file mode 100644 index 000000000..2ed4480f2 --- /dev/null +++ b/.github/workflows/renovate-app-version.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +import os +import re +import shutil +import sys +import yaml + +def extract_version_from_string(input_string): + """ + 从字符串中提取版本号,支持镜像名和文件夹名 + + Args: + input_string (str): 可以是Docker镜像名或文件夹名 + + Returns: + dict: 包含提取结果的字典 {success: bool, version: str} + """ + if ':' in input_string: + parts = input_string.split(':') + candidate = parts[-1] + else: + candidate = os.path.basename(input_string) + + # 保存原始候选字符串用于失败时返回 + original_candidate = candidate + + # 标准的 major.minor.patch 格式(点号分隔) + pattern1 = r'(\d+\.\d+\.\d+)' + match1 = re.search(pattern1, candidate) + if match1: + return {"success": True, "version": match1.group(1)} + + # 连字符分隔的版本号 major-minor-patch + pattern2 = r'(\d+\-\d+\-\d+)' + match2 = re.search(pattern2, candidate) + if match2: + # 将连字符转换为点号 + version_with_dots = match2.group(1).replace('-', '.') + return {"success": True, "version": version_with_dots} + + # 混合分隔符(如 major.minor-patch) + pattern3 = r'(\d+[\.\-]\d+[\.\-]\d+)' + match3 = re.search(pattern3, candidate) + if match3: + # 将所有分隔符统一为点号 + version_mixed = match3.group(1) + version_normalized = re.sub(r'[\.\-]', '.', version_mixed) + return {"success": True, "version": version_normalized} + + # 两个部分的版本号 (major.minor 或 major-minor) + pattern4 = r'(\d+[\.\-]\d+)(?![\.\-]\d)' # 确保后面没有第三个数字部分 + match4 = re.search(pattern4, candidate) + if match4: + version_two_part = match4.group(1) + version_normalized = re.sub(r'[\.\-]', '.', version_two_part) + return {"success": True, "version": version_normalized} + + # 从复杂标签中提取包含数字和分隔符的版本号部分 + pattern5 = r'(\d+(?:[\.\-]\d+)+)' + matches = re.findall(pattern5, candidate) + if matches: + # 选择最长的匹配项,并统一分隔符为点号 + best_match = max(matches, key=len) + normalized_version = re.sub(r'[\.\-]', '.', best_match) + return {"success": True, "version": normalized_version} + + return {"success": False, "version": original_candidate} + +def replace_version_in_dirname(old_ver_dir, new_version): + """ + 将旧版本文件夹名中的版本号替换为新版本号 + + Args: + old_ver_dir (str): 旧版本文件夹名 + new_version (str): 新版本号 + + Returns: + str: 新版本文件夹名 + """ + version_info = extract_version_from_string(old_ver_dir) + + if version_info["success"]: + old_version = version_info["original"] + new_ver_dir = old_ver_dir.replace(old_version, new_version) + return new_ver_dir + else: + return new_version + +def main(): + app_name = sys.argv[1] + old_ver_dir = sys.argv[2] + + docker_compose_file = f"apps/{app_name}/{old_ver_dir}/docker-compose.yml" + + if not os.path.exists(docker_compose_file): + print(f"错误: 文件 {docker_compose_file} 不存在") + sys.exit(1) + + with open(docker_compose_file, 'r') as f: + compose_data = yaml.safe_load(f) + + services = compose_data.get('services', {}) + + first_service = list(services.keys())[0] + print(f"第一个服务是: {first_service}") + + image = services[first_service].get('image', '') + print(f"该服务的镜像: {image}") + + new_version = extract_version_from_string(image) + print(f"版本号: {new_version}") + + old_version = extract_version_from_string(old_ver_dir) + new_ver_dir = replace_version_in_dirname(old_ver_dir, new_version) + if old_version != new_version: + old_path = f"apps/{app_name}/{old_ver_dir}" + new_path = f"apps/{app_name}/{new_ver_dir}" + + if not os.path.exists(new_path): + print(f"将 {old_path} 重命名为 {new_path}") + shutil.move(old_path, new_path) + + version_file = f"apps/{app_name}/{old_version}.version" + with open(version_file, 'w') as f: + f.write(new_version) + else: + print(f"错误: {new_path} 文件夹已存在") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/renovate-app-version.sh b/.github/workflows/renovate-app-version.sh deleted file mode 100644 index 4c396102a..000000000 --- a/.github/workflows/renovate-app-version.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# This script copies the version from docker-compose.yml to config.json. - -app_name=$1 -old_version=$2 - -# find all docker-compose files under apps/$app_name (there should be only one) -docker_compose_files=$(find apps/$app_name/$old_version -name docker-compose.yml) - -for docker_compose_file in $docker_compose_files -do - # Assuming that the app version will be from the first docker image - first_service=$(yq '.services | keys | .[0]' $docker_compose_file) - echo "第一个服务是: $first_service" - - image=$(yq .services.$first_service.image $docker_compose_file) - echo "该服务的镜像: $image" - - # Only apply changes if the format is : - if [[ "$image" == *":"* ]]; then - version=$(cut -d ":" -f2- <<< "$image") - echo "版本号: $version" - - # Trim the "v" prefix - trimmed_version=${version/#"v"} - echo "Trimmed version: $trimmed_version" - if [ "$old_version" != "$trimmed_version" ]; then - echo "将 apps/$app_name/$old_version 重命名为 apps/$app_name/$trimmed_version" - if [ ! -d "apps/$app_name/$trimmed_version" ]; then - mv apps/$app_name/$old_version apps/$app_name/$trimmed_version - echo "$trimmed_version" > apps/$app_name/${old_version}.version - else - echo "apps/$app_name/$trimmed_version 文件夹已存在" - exit 1 - fi - fi - fi -done \ No newline at end of file diff --git a/.github/workflows/renovate-app-version.yml b/.github/workflows/renovate-app-version.yml index f807cdfcb..cf075a6f7 100644 --- a/.github/workflows/renovate-app-version.yml +++ b/.github/workflows/renovate-app-version.yml @@ -1,26 +1,61 @@ -name: Update app version in Renovate Branches +name: App Version CI/CD on: - push: - branches: [ 'renovate/*' ] + pull_request: + types: + - opened + - synchronize + - reopened + +env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} jobs: + get-latest-commit-author: + name: Get Latest Commit Author + runs-on: ubuntu-latest + outputs: + latest_commit_author: ${{ steps.latest-commit-author.outputs.latest_commit_author }} + steps: + - name: Get latest commit author + id: latest-commit-author + run: | + resp=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository}}/commits/${{ github.event.pull_request.head.ref }}) + author=$(echo "$resp" | jq -r '.author.login // empty') + echo "the Author of Latest Commit on Head Branch: $author" + echo "latest_commit_author=$author" >> $GITHUB_OUTPUT + update-app-version: + name: Update App Version + needs: get-latest-commit-author + if: | + github.actor == 'renovate[bot]' && + github.triggering_actor == 'renovate[bot]' && + needs.get-latest-commit-author.outputs.latest_commit_author != github.repository_owner && + startsWith(github.head_ref, 'renovate/') runs-on: ubuntu-latest permissions: contents: write statuses: write steps: - name: Checkout - uses: actions/checkout@v4.3.0 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 + - uses: actions/setup-python@main + with: + python-version: 3.x + pip-install: PyYAML + - name: Configure repo run: | git config --local user.email "githubaction@githubaction.com" git config --local user.name "github-action" - gh auth login --with-token <<< "${{ github.token }}" - name: Get list of updated files by the last commit in this branch separated by space id: updated-files @@ -35,8 +70,8 @@ jobs: if [[ $file == *"docker-compose.yml"* ]]; then app_name=$(echo $file | cut -d'/' -f 2) old_version=$(echo $file | cut -d'/' -f 3) - chmod +x .github/workflows/renovate-app-version.sh - .github/workflows/renovate-app-version.sh $app_name $old_version + chmod +x .github/workflows/renovate-app-version.py + python3 .github/workflows/renovate-app-version.py $app_name $old_version fi done @@ -61,4 +96,77 @@ jobs: -f 'context=${{ github.workflow}}' fi fi - done \ No newline at end of file + done + + check-labels: + name: Check labels + if: | + github.actor == 'renovate[bot]' && + github.triggering_actor == 'renovate[bot]' + runs-on: ubuntu-latest + outputs: + enable: ${{ steps.check-labels.outputs.enable }} + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Check for major label + id: check-labels + run: | + pr_labels='${{ join(github.event.pull_request.labels.*.name, ',') }}' + if [[ $pr_labels =~ "major" ]]; then + echo "❌ PR has major label" + echo "enable=false" >> $GITHUB_OUTPUT + else + echo "✅ PR does not have major label" + echo "enable=true" >> $GITHUB_OUTPUT + fi + + - name: Add reviewer and comment for major PR + if: steps.check-labels.outputs.enable == 'false' + run: | + gh pr edit $PR_NUMBER --add-reviewer ${{ github.actor }} + gh pr edit $PR_NUMBER --add-assignee ${{ github.actor }} + + - name: remove labels + run: | + pr_labels='${{ join(github.event.pull_request.labels.*.name, ',') }}' + IFS=',' read -ra labels_array <<< "$pr_labels" + for label in "${labels_array[@]}"; do + echo "Removing label: $label" + gh pr edit $PR_NUMBER --remove-label="$label" + done + + merge-prs: + name: Auto Merge the PR + runs-on: ubuntu-latest + needs: + - update-app-version + - check-labels + if: | + needs.check-labels.outputs.enable == 'true' && + (needs.update-app-version.result == 'success' || needs.update-app-version.result == 'skipped') + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + ref: ${{ github.base_ref }} + + - name: Merge PR + run: | + max_attempts=5 + for attempt in $(seq 1 $max_attempts); do + if gh pr merge $PR_NUMBER --squash --delete-branch --body ""; then + echo "✅ Merge PR #$PR_NUMBER Success" + exit 0 + else + echo "⚠️ Merge PR #$PR_NUMBER Failed ($attempt / $max_attempts)" + sleep 5 + fi + done diff --git a/renovate.json b/renovate.json index 69487e486..d0388a883 100644 --- a/renovate.json +++ b/renovate.json @@ -3,19 +3,25 @@ "extends": [ "config:recommended" ], - "reviewers": ["pooneyy"], "gitIgnoredAuthors": [ "githubaction@githubaction.com", "85266337+pooneyy@users.noreply.github.com" ], - "automerge": true, - "automergeType": "pr", - "automergeStrategy": "squash", + "prBodyColumns": [ + "Package", + "Update", + "Change", + "Pending" + ], "rebaseWhen": "never", "prConcurrentLimit":0, "packageRules": [ { - "matchPackageNames": [ + "matchUpdateTypes": ["major", "minor", "patch", "pin", "pinDigest", "digest", "lockFileMaintenance", "rollback", "bump", "replacement"], + "addLabels": ["{{ updateType }}"] + }, + { + "matchManagers": [ "docker-compose" ], "automerge": true @@ -151,9 +157,9 @@ "allowedVersions": "<2" }, { - "matchPackageNames": ["xhofe/alist"], - "matchCurrentVersion": "v3.40.0", - "allowedVersions": "v3.40.0" + "matchPackageNames": ["xhofe/alist**"], + "matchCurrentVersion": "/^v3.40.*/", + "enabled": false }, { "matchPackageNames": ["yisier1/npc", "yisier1/nps"],