name: Update app version in Renovate Branches on: push: branches: [ 'renovate/*' ] workflow_dispatch: inputs: manual-trigger: description: 'Manually trigger Renovate' default: '' # 并发控制:确保同一时间只有一个 PR 合并操作在进行,避免竞争条件 concurrency: group: merge-renovate-pr cancel-in-progress: false jobs: update-app-version: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: Check if triggered by self (prevent circular trigger) id: check-circular run: | # 检查最后一个 commit 的 author,如果是自己则跳过 last_author=$(git log -1 --format='%an') last_message=$(git log -1 --format='%s') echo "Last commit author: $last_author" echo "Last commit message: $last_message" # 如果是 github-action 自己 commit 的,说明是工作流触发的,跳过避免循环 if [[ "$last_author" == "github-action update-app-version" ]] || \ [[ "$last_author" == "github-action[bot]" ]] || \ [[ "$last_message" == *"[auto-version-bump]"* ]]; then echo "⚠️ Skipping: triggered by own commit (preventing circular trigger)" echo "skip=true" >> $GITHUB_OUTPUT else echo "✅ Proceeding: triggered by external commit" echo "skip=false" >> $GITHUB_OUTPUT fi - name: Configure git if: steps.check-circular.outputs.skip != 'true' run: | git config --local user.email "githubaction@githubaction.com" git config --local user.name "github-action update-app-version" - name: Get list of updated files by the last commit if: steps.check-circular.outputs.skip != 'true' id: updated-files run: | echo "files=$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} | tr '\n' ' ')" >> $GITHUB_OUTPUT - name: Run renovate-app-version.sh on updated files if: steps.check-circular.outputs.skip != 'true' id: rename run: | set -e chmod +x .github/workflows/renovate-app-version.sh files="${{ steps.updated-files.outputs.files }}" declare -a changed_apps=() echo "Updated files: $files" for file in $files; do if [[ $file == *"docker-compose.yml"* ]]; then echo "Processing file: $file" app_name=$(echo $file | cut -d'/' -f 2) old_version=$(echo $file | cut -d'/' -f 3) echo "App name: $app_name, old version: $old_version" # 获取所有服务名 services=$(yq '.services | keys | .[]' "$file") service="" image_line="" for s in $services; do # 通过awk获取服务下的image行(包含注释) image_line=$(awk "/services:/{flag=0} /^\s*$s:/{flag=1} flag && /^\s*image:/{print; exit}" "$file") echo "Service $s image line: $image_line" if [[ "$image_line" != *"[ignore]"* ]]; then service="$s" break else echo "Skipping service $s due to [ignore]" fi done if [[ -z "$service" ]]; then echo "No valid service found in $file, skipping..." continue fi # 提取image纯字符串,去除注释和多余空格 image=$(echo "$image_line" | sed -E 's/^\s*image:\s*([^ #]+).*/\1/') echo "Selected service: $service" echo "Extracted image: $image" if [[ "$image" == *":"* ]]; then new_version=$(cut -d ":" -f2- <<< "$image") trimmed_version=${new_version/#"v"/} echo "Parsed new version: $trimmed_version" else trimmed_version="" echo "No version tag found in image." fi changed_apps+=("${app_name}:${old_version}:${trimmed_version}") echo "Calling renovate-app-version.sh with: $app_name, $old_version, $trimmed_version" .github/workflows/renovate-app-version.sh "$app_name" "$old_version" "$trimmed_version" fi done echo "All changed apps: ${changed_apps[*]}" echo "apps=$(IFS=, ; echo "${changed_apps[*]}")" >> $GITHUB_OUTPUT - name: Commit & Push Changes if: steps.check-circular.outputs.skip != 'true' run: | set -e IFS=',' read -r -a apps <<< "${{ steps.rename.outputs.apps }}" for item in "${apps[@]}"; do app_name=$(cut -d':' -f1 <<< "$item") old_version=$(cut -d':' -f2 <<< "$item") new_version=$(cut -d':' -f3 <<< "$item") if [[ -n "$app_name" && -n "$new_version" ]]; then git add "apps/$app_name/*" git commit -m "📈将应用 $app_name 的版本从 $old_version 升级到 $new_version" --no-verify || echo "无内容可提交" fi done git push || echo "无内容可推送" - name: Rebase to latest main and merge PR if: steps.check-circular.outputs.skip != 'true' && github.ref_name != 'main' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e branch_name=$(git rev-parse --abbrev-ref HEAD) echo "Current branch: $branch_name" # 获取 PR 编号 pr_number=$(gh pr list --state open --head "$branch_name" --json number -q '.[0].number') if [ -z "$pr_number" ]; then echo "No PR found for branch $branch_name" exit 0 fi echo "Found PR #$pr_number, preparing to merge..." # Fetch 最新的 main 分支 echo "Fetching latest main branch..." git fetch origin main # Rebase 当前分支到最新的 main # 由于 renovate PR 修改的是不同的应用文件夹,不会有真正的冲突 echo "Rebasing $branch_name onto origin/main..." git rebase origin/main # Force push 更新后的分支 echo "Force pushing rebased branch..." git push --force-with-lease # 等待 GitHub 重新计算 mergeable 状态 echo "Waiting for GitHub to recalculate mergeable status..." sleep 15 # 检查 mergeable 状态,最多等待 2 分钟 max_attempts=12 attempt=0 while [ $attempt -lt $max_attempts ]; do mergeable=$(gh pr view "$pr_number" --json mergeable -q '.mergeable' 2>/dev/null || echo "UNKNOWN") echo "Mergeable status: $mergeable (attempt $((attempt+1))/$max_attempts)" if [ "$mergeable" = "MERGEABLE" ]; then echo "PR is mergeable!" break fi # 如果是 CONFLICTING,说明有真正的冲突(不应该发生) if [ "$mergeable" = "CONFLICTING" ]; then echo "Warning: PR has conflicts, but proceeding with admin merge..." break fi sleep 10 attempt=$((attempt+1)) done # 合并 PR echo "Merging PR #$pr_number..." gh pr merge "$pr_number" --merge --delete-branch --admin echo "Successfully merged PR #$pr_number!" - name: Trigger sync to CNB mirror if: steps.check-circular.outputs.skip != 'true' && github.ref_name != 'main' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Triggering sync-to-cnb workflow..." gh workflow run sync-to-cnb.yml --ref main echo "Sync workflow triggered successfully!"