mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
adds yaml linting to CI and applies rules to existing yaml files. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6682-ci-add-yamllint-2aa6d73d365081b4b67ae9d9cc86760f) by [Unito](https://www.unito.io)
415 lines
16 KiB
YAML
415 lines
16 KiB
YAML
name: PR Backport
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [closed, labeled]
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
pr_number:
|
|
description: 'PR number to backport'
|
|
required: true
|
|
type: string
|
|
force_rerun:
|
|
description: 'Force rerun even if backports exist'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
jobs:
|
|
backport:
|
|
if: >
|
|
(github.event_name == 'pull_request_target' &&
|
|
github.event.pull_request.merged == true &&
|
|
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
|
|
github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
steps:
|
|
- name: Validate inputs for manual triggers
|
|
if: github.event_name == 'workflow_dispatch'
|
|
run: |
|
|
# Validate PR number format
|
|
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
|
|
echo "::error::Invalid PR number format. Must be a positive integer."
|
|
exit 1
|
|
fi
|
|
|
|
# Validate PR exists and is merged
|
|
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
|
|
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
|
|
exit 1
|
|
fi
|
|
|
|
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
|
|
if [ "$MERGED" != "true" ]; then
|
|
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
|
|
exit 1
|
|
fi
|
|
|
|
# Validate PR has needs-backport label
|
|
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
|
|
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
|
|
exit 1
|
|
fi
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Configure git
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Collect backport targets
|
|
id: targets
|
|
run: |
|
|
TARGETS=()
|
|
declare -A SEEN=()
|
|
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
|
else
|
|
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
|
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
|
fi
|
|
|
|
add_target() {
|
|
local label="$1"
|
|
local target="$2"
|
|
|
|
if [ -z "$target" ]; then
|
|
return
|
|
fi
|
|
|
|
target=$(echo "$target" | xargs)
|
|
|
|
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
|
|
return
|
|
fi
|
|
|
|
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
|
|
TARGETS+=("$target")
|
|
SEEN["$target"]=1
|
|
else
|
|
echo "::warning::Label '${label}' references missing branch '${target}'"
|
|
fi
|
|
}
|
|
|
|
while IFS= read -r label; do
|
|
[ -z "$label" ] && continue
|
|
|
|
if [[ "$label" =~ ^branch:(.+)$ ]]; then
|
|
add_target "$label" "${BASH_REMATCH[1]}"
|
|
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
|
add_target "$label" "${BASH_REMATCH[1]}"
|
|
elif [[ "$label" =~ ^core\/([0-9]+)\.([0-9]+)$ ]]; then
|
|
SAFE_MAJOR="${BASH_REMATCH[1]}"
|
|
SAFE_MINOR="${BASH_REMATCH[2]}"
|
|
add_target "$label" "core/${SAFE_MAJOR}.${SAFE_MINOR}"
|
|
elif [[ "$label" =~ ^cloud\/([0-9]+)\.([0-9]+)$ ]]; then
|
|
SAFE_MAJOR="${BASH_REMATCH[1]}"
|
|
SAFE_MINOR="${BASH_REMATCH[2]}"
|
|
add_target "$label" "cloud/${SAFE_MAJOR}.${SAFE_MINOR}"
|
|
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
|
add_target "$label" "core/${label}"
|
|
fi
|
|
done <<< "$LABELS"
|
|
|
|
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
|
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
|
|
exit 1
|
|
fi
|
|
|
|
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
|
echo "Found backport targets: ${TARGETS[*]}"
|
|
|
|
- name: Filter already backported targets
|
|
id: filter-targets
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
FORCE_RERUN_INPUT: >-
|
|
${{ github.event_name == 'workflow_dispatch' && inputs.force_rerun
|
|
|| 'false' }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_NUMBER: >-
|
|
${{ github.event_name == 'workflow_dispatch' && inputs.pr_number
|
|
|| github.event.pull_request.number }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
REQUESTED_TARGETS="${{ steps.targets.outputs.targets }}"
|
|
if [ -z "$REQUESTED_TARGETS" ]; then
|
|
echo "skip=true" >> $GITHUB_OUTPUT
|
|
echo "pending-targets=" >> $GITHUB_OUTPUT
|
|
exit 0
|
|
fi
|
|
|
|
FORCE_RERUN=false
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$FORCE_RERUN_INPUT" = "true" ]; then
|
|
FORCE_RERUN=true
|
|
fi
|
|
|
|
mapfile -t EXISTING_BRANCHES < <(
|
|
git ls-remote --heads origin "backport-${PR_NUMBER}-to-*" || true
|
|
)
|
|
|
|
PENDING=()
|
|
SKIPPED=()
|
|
REUSED=()
|
|
|
|
for target in $REQUESTED_TARGETS; do
|
|
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
|
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
|
|
|
if [ "$FORCE_RERUN" = true ]; then
|
|
PENDING+=("$target")
|
|
continue
|
|
fi
|
|
|
|
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
|
|
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
|
|
OPEN_PR=$(
|
|
gh pr list \
|
|
--state open \
|
|
--head "${BACKPORT_BRANCH}" \
|
|
--json number \
|
|
--jq 'if length > 0 then .[0].number else "" end'
|
|
)
|
|
if [ -n "$OPEN_PR" ]; then
|
|
SKIPPED+=("${target} (PR #${OPEN_PR})")
|
|
continue
|
|
fi
|
|
|
|
REUSED+=("$BACKPORT_BRANCH")
|
|
fi
|
|
|
|
PENDING+=("$target")
|
|
done
|
|
|
|
SKIPPED_JOINED="${SKIPPED[*]:-}"
|
|
PENDING_JOINED="${PENDING[*]:-}"
|
|
|
|
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
|
|
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
|
|
echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT
|
|
|
|
if [ -z "$PENDING_JOINED" ]; then
|
|
echo "skip=true" >> $GITHUB_OUTPUT
|
|
if [ -n "$SKIPPED_JOINED" ]; then
|
|
echo "::warning::Backport branches exist: ${SKIPPED_JOINED}"
|
|
fi
|
|
else
|
|
echo "skip=false" >> $GITHUB_OUTPUT
|
|
if [ -n "$SKIPPED_JOINED" ]; then
|
|
echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}"
|
|
fi
|
|
if [ "${#REUSED[@]}" -gt 0 ]; then
|
|
echo "::notice::Reusing backport branches: ${REUSED[*]}"
|
|
fi
|
|
fi
|
|
|
|
- name: Backport commits
|
|
if: steps.filter-targets.outputs.skip != 'true'
|
|
id: backport
|
|
env:
|
|
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
|
run: |
|
|
FAILED=""
|
|
SUCCESS=""
|
|
|
|
CREATED_BRANCHES_FILE="$(
|
|
mktemp "$RUNNER_TEMP/backport-branches-XXXXXX"
|
|
)"
|
|
echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV"
|
|
|
|
# Get PR data for manual triggers
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
|
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
|
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
|
else
|
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
|
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
|
fi
|
|
|
|
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
|
TARGET_BRANCH="${target}"
|
|
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
|
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
|
REMOTE_BACKPORT_EXISTS=false
|
|
|
|
if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
|
REMOTE_BACKPORT_EXISTS=true
|
|
echo "::notice::Updating existing branch ${BACKPORT_BRANCH}"
|
|
fi
|
|
|
|
echo "::group::Backporting to ${TARGET_BRANCH}"
|
|
|
|
# Fetch target branch (fail if doesn't exist)
|
|
if ! git fetch origin "${TARGET_BRANCH}"; then
|
|
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
|
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
|
|
echo "::endgroup::"
|
|
continue
|
|
fi
|
|
|
|
# Check if commit already exists on target branch
|
|
if git branch -r --contains "${MERGE_COMMIT}" | grep -q "origin/${TARGET_BRANCH}"; then
|
|
echo "::notice::Commit ${MERGE_COMMIT} already exists on ${TARGET_BRANCH}, skipping backport"
|
|
FAILED="${FAILED}${TARGET_BRANCH}:already-exists "
|
|
echo "::endgroup::"
|
|
continue
|
|
fi
|
|
|
|
# Create backport branch
|
|
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
|
|
|
|
# Try cherry-pick
|
|
if git cherry-pick "${MERGE_COMMIT}"; then
|
|
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
|
|
git push --force-with-lease origin "${BACKPORT_BRANCH}"
|
|
else
|
|
git push origin "${BACKPORT_BRANCH}"
|
|
fi
|
|
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
|
|
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
|
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
|
# Return to main (keep the branch, we need it for PR)
|
|
git checkout main
|
|
else
|
|
# Get conflict info
|
|
CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
|
|
git cherry-pick --abort
|
|
|
|
echo "::error::Cherry-pick failed due to conflicts"
|
|
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
|
|
|
# Clean up the failed branch
|
|
git checkout main
|
|
git branch -D "${BACKPORT_BRANCH}"
|
|
fi
|
|
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
|
|
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
|
|
|
|
if [ -s "$CREATED_BRANCHES_FILE" ]; then
|
|
CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE")
|
|
echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "created-branches=" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
if [ -n "${FAILED}" ]; then
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create PR for each successful backport
|
|
if: steps.filter-targets.outputs.skip != 'true' && steps.backport.outputs.success
|
|
env:
|
|
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
|
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
|
run: |
|
|
# Get PR data for manual triggers
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
|
|
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
|
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
|
else
|
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
|
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
|
fi
|
|
|
|
for backport in ${{ steps.backport.outputs.success }}; do
|
|
IFS=':' read -r target branch <<< "${backport}"
|
|
|
|
if PR_URL=$(gh pr create \
|
|
--base "${target}" \
|
|
--head "${branch}" \
|
|
--title "[backport ${target}] ${PR_TITLE}" \
|
|
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
|
--label "backport" 2>&1); then
|
|
|
|
# Extract PR number from URL
|
|
PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$')
|
|
|
|
if [ -n "${PR_NUM}" ]; then
|
|
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
|
fi
|
|
else
|
|
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
|
|
# Still try to comment on the original PR about the failure
|
|
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
|
|
fi
|
|
done
|
|
|
|
- name: Comment on failures
|
|
if: steps.filter-targets.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
|
|
PR_NUMBER="${{ inputs.pr_number }}"
|
|
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
|
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
|
else
|
|
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
|
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
|
fi
|
|
|
|
for failure in ${{ steps.backport.outputs.failed }}; do
|
|
IFS=':' read -r target reason conflicts <<< "${failure}"
|
|
|
|
if [ "${reason}" = "branch-missing" ]; then
|
|
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
|
|
|
elif [ "${reason}" = "already-exists" ]; then
|
|
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
|
|
|
|
elif [ "${reason}" = "conflicts" ]; then
|
|
# Convert comma-separated conflicts back to newlines for display
|
|
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
|
|
|
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
|
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
|
fi
|
|
done
|
|
|
|
- name: Cleanup stranded backport branches
|
|
if: steps.filter-targets.outputs.skip != 'true' && failure()
|
|
run: |
|
|
FILE="${CREATED_BRANCHES_FILE:-}"
|
|
|
|
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
|
|
echo "No backport branches recorded for cleanup"
|
|
exit 0
|
|
fi
|
|
|
|
while IFS= read -r branch; do
|
|
[ -z "$branch" ] && continue
|
|
printf 'Deleting branch %s\n' "${branch}"
|
|
if ! git push origin --delete "$branch"; then
|
|
echo "::warning::Failed to delete ${branch}"
|
|
fi
|
|
done < "$FILE"
|
|
|
|
|
|
- name: Remove needs-backport label
|
|
if: steps.filter-targets.outputs.skip != 'true' && success()
|
|
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|