✨ feat(renovate): migrate from shell script to python implementation and enhance automation
- replace bash script with python script for better version extraction and handling
- add major version label checking and auto-merge workflow
- implement pr author verification and automatic reviewer assignment
- enhance version extraction with multiple pattern support (dots, hyphens, mixed separators)
- add pr body columns configuration and update package rules
🔧 chore(workflows): restructure github actions workflow
- change trigger from push to pull_request events
- add python setup and dependency installation
- implement multi-stage workflow with label checking and auto-merge
- add commit author verification
- improve error handling and retry logic for pr merging
This commit is contained in:
parent
a8061d9521
commit
deae7397a8
|
|
@ -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()
|
||||
|
|
@ -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 <image>:<version>
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue