Update backport workflow to use commit SHA input (#14043)

This commit is contained in:
comfyanonymous 2026-05-21 18:22:47 -07:00 committed by GitHub
parent 8fecef0686
commit 8edff549e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -3,8 +3,8 @@ name: Backport Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
branch: commit:
description: 'Source branch containing the backported commits (PR source branch into master)' description: 'Full 40-char SHA of the tip commit of the backport source branch (the PR head commit that passed tests). The branch is resolved from this SHA and must be unique.'
required: true required: true
type: string type: string
@ -39,21 +39,71 @@ jobs:
git config user.name "fen-release[bot]" git config user.name "fen-release[bot]"
git config user.email "fen-release[bot]@users.noreply.github.com" git config user.email "fen-release[bot]@users.noreply.github.com"
- name: Validate source branch exists - name: Resolve source branch from commit SHA
id: resolve
env: env:
SOURCE_BRANCH: ${{ inputs.branch }} SOURCE_COMMIT: ${{ inputs.commit }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: | run: |
set -euo pipefail set -euo pipefail
if [[ "${SOURCE_BRANCH}" == "${DEFAULT_BRANCH}" ]]; then
# Require a full 40-char lowercase-hex SHA. Short SHAs are ambiguous
# and we will be comparing this value against API responses (PR head
# SHA, ref tips) that always return the full form.
if [[ ! "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Input commit '${SOURCE_COMMIT}' is not a full 40-char lowercase hex SHA."
exit 1
fi
# Fetch all remote branches so we can search for which one(s) point
# at this SHA. `actions/checkout` with fetch-depth: 0 fetches full
# history of the checked-out ref but does not necessarily populate
# every refs/remotes/origin/*, so do it explicitly.
git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*'
# Verify the commit actually exists in this repo's object DB.
if ! git cat-file -e "${SOURCE_COMMIT}^{commit}" 2>/dev/null; then
echo "::error::Commit ${SOURCE_COMMIT} was not found in the repository."
exit 1
fi
# Find every remote branch whose tip == SOURCE_COMMIT. Exactly one
# branch must point at it. If zero, the commit isn't anyone's tip
# (likely stale, force-pushed past, or never the PR head). If more
# than one, the (branch -> SHA) mapping is ambiguous and we refuse
# to guess — the operator must give us a unique branch to release.
mapfile -t matching_branches < <(
git for-each-ref \
--format='%(refname:strip=3)' \
--points-at="${SOURCE_COMMIT}" \
refs/remotes/origin/ \
| grep -vx 'HEAD' || true
)
if [[ "${#matching_branches[@]}" -eq 0 ]]; then
echo "::error::No branch on origin has ${SOURCE_COMMIT} as its tip."
echo "::error::Either the branch was updated after you copied this SHA, or this commit was never the head of a branch."
exit 1
fi
if [[ "${#matching_branches[@]}" -gt 1 ]]; then
echo "::error::More than one branch on origin has ${SOURCE_COMMIT} as its tip; cannot pick one:"
for b in "${matching_branches[@]}"; do
echo "::error:: - ${b}"
done
echo "::error::Refusing to proceed with an ambiguous source branch."
exit 1
fi
source_branch="${matching_branches[0]}"
if [[ "${source_branch}" == "${DEFAULT_BRANCH}" ]]; then
echo "::error::Source branch must not be the default branch ('${DEFAULT_BRANCH}')." echo "::error::Source branch must not be the default branch ('${DEFAULT_BRANCH}')."
exit 1 exit 1
fi fi
git fetch origin "refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/${SOURCE_BRANCH}"
if ! git show-ref --verify --quiet "refs/remotes/origin/${SOURCE_BRANCH}"; then echo "Resolved commit ${SOURCE_COMMIT} to branch '${source_branch}'."
echo "::error::Source branch '${SOURCE_BRANCH}' not found on origin." echo "source_branch=${source_branch}" >> "$GITHUB_OUTPUT"
exit 1
fi
- name: Determine latest stable release - name: Determine latest stable release
id: latest id: latest
@ -107,13 +157,18 @@ jobs:
- name: Validate source branch is cut directly from the latest stable release - name: Validate source branch is cut directly from the latest stable release
env: env:
SOURCE_BRANCH: ${{ inputs.branch }} SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
SOURCE_COMMIT: ${{ inputs.commit }}
LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }}
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
run: | run: |
set -euo pipefail set -euo pipefail
source_sha="$(git rev-parse "refs/remotes/origin/${SOURCE_BRANCH}")" # Use the user-provided SHA directly rather than re-resolving the branch
# tip — the resolve step already proved the branch tip equals SOURCE_COMMIT,
# and pinning to the SHA here makes the rest of the job TOCTOU-safe against
# someone pushing to the branch mid-run.
source_sha="${SOURCE_COMMIT}"
# Walking first-parent from the source tip must reach LATEST_TAG_SHA. # Walking first-parent from the source tip must reach LATEST_TAG_SHA.
# We capture rev-list into a variable and grep against a here-string # We capture rev-list into a variable and grep against a here-string
@ -156,10 +211,11 @@ jobs:
added_count="$(printf '%s\n' "${all_added}" | grep -c . || true)" added_count="$(printf '%s\n' "${all_added}" | grep -c . || true)"
echo "Source branch is cut directly from ${LATEST_TAG} with ${added_count} commit(s) on top." echo "Source branch is cut directly from ${LATEST_TAG} with ${added_count} commit(s) on top."
- name: Validate PR exists, is named correctly, and checks pass - name: Validate PR exists, is open, named correctly, has latest commit, and checks pass
env: env:
GH_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }}
SOURCE_BRANCH: ${{ inputs.branch }} SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
SOURCE_COMMIT: ${{ inputs.commit }}
NEW_VERSION: ${{ steps.latest.outputs.new_version }} NEW_VERSION: ${{ steps.latest.outputs.new_version }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
run: | run: |
@ -167,20 +223,22 @@ jobs:
expected_title="ComfyUI backport release ${NEW_VERSION}" expected_title="ComfyUI backport release ${NEW_VERSION}"
# Find open PRs from this branch into master # Find open PRs from this branch into master. The --state open filter
# is load-bearing: a closed/merged PR with passing checks must not be
# accepted as authorization for a new release.
pr_json="$( pr_json="$(
gh pr list \ gh pr list \
--repo "${REPO}" \ --repo "${REPO}" \
--state open \ --state open \
--head "${SOURCE_BRANCH}" \ --head "${SOURCE_BRANCH}" \
--base master \ --base master \
--json number,title,headRefOid \ --json number,title,headRefOid,state \
--limit 10 --limit 10
)" )"
pr_count="$(echo "${pr_json}" | jq 'length')" pr_count="$(echo "${pr_json}" | jq 'length')"
if [[ "${pr_count}" -eq 0 ]]; then if [[ "${pr_count}" -eq 0 ]]; then
echo "::error::No open PR found from '${SOURCE_BRANCH}' into 'master'." echo "::error::No open PR found from '${SOURCE_BRANCH}' into 'master'. The PR must exist and be open."
exit 1 exit 1
fi fi
@ -199,7 +257,19 @@ jobs:
exit 1 exit 1
fi fi
echo "Found PR #${pr_number} titled '${expected_title}' (head ${pr_head_sha})." # The PR's current head commit must equal the SHA the operator gave us.
# This is what closes the door on releasing stale code: if anyone has
# pushed to the branch since the operator validated tests passed, the
# PR head will have advanced past SOURCE_COMMIT and we abort. (The
# resolve step already proved the branch tip == SOURCE_COMMIT; this
# ties that same SHA to the PR that authorizes the release.)
if [[ "${pr_head_sha}" != "${SOURCE_COMMIT}" ]]; then
echo "::error::PR #${pr_number} head commit is ${pr_head_sha}, but the operator-provided commit is ${SOURCE_COMMIT}."
echo "::error::The PR has new commits since this release was authorized. Re-run with the new head SHA after verifying its checks."
exit 1
fi
echo "Found open PR #${pr_number} titled '${expected_title}' at head ${pr_head_sha} (matches operator-provided commit)."
# Verify all check runs on the head commit have completed successfully. # Verify all check runs on the head commit have completed successfully.
# A check is considered passing if conclusion is success, neutral, or skipped. # A check is considered passing if conclusion is success, neutral, or skipped.
@ -241,7 +311,6 @@ jobs:
env: env:
GH_TOKEN: ${{ steps.app-token.outputs.token }} GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
SOURCE_BRANCH: ${{ inputs.branch }}
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }} LATEST_TAG_SHA: ${{ steps.latest.outputs.latest_sha }}
@ -277,7 +346,8 @@ jobs:
- name: Fast-forward merge source branch into release branch - name: Fast-forward merge source branch into release branch
env: env:
SOURCE_BRANCH: ${{ inputs.branch }} SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
SOURCE_COMMIT: ${{ inputs.commit }}
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
run: | run: |
set -euo pipefail set -euo pipefail
@ -288,12 +358,16 @@ jobs:
# that the source branch is rooted on the latest stable tag, and the # that the source branch is rooted on the latest stable tag, and the
# release branch tip equals that same tag, this fast-forward should # release branch tip equals that same tag, this fast-forward should
# always succeed for a well-formed backport branch. # always succeed for a well-formed backport branch.
if ! git merge --ff-only "refs/remotes/origin/${SOURCE_BRANCH}"; then #
echo "::error::Cannot fast-forward '${RELEASE_BRANCH}' to '${SOURCE_BRANCH}'. A merge commit would be required. Aborting." # We merge the operator-provided SHA, not the branch ref, so a push to
# the branch in the window between resolve and now cannot smuggle new
# commits into the release.
if ! git merge --ff-only "${SOURCE_COMMIT}"; then
echo "::error::Cannot fast-forward '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}'). A merge commit would be required. Aborting."
exit 1 exit 1
fi fi
echo "Fast-forwarded '${RELEASE_BRANCH}' to tip of '${SOURCE_BRANCH}'." echo "Fast-forwarded '${RELEASE_BRANCH}' to ${SOURCE_COMMIT} (tip of '${SOURCE_BRANCH}')."
- name: Bump version files - name: Bump version files
env: env:
@ -390,14 +464,20 @@ jobs:
NEW_VERSION: ${{ steps.latest.outputs.new_version }} NEW_VERSION: ${{ steps.latest.outputs.new_version }}
RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }} RELEASE_BRANCH: ${{ steps.latest.outputs.release_branch }}
LATEST_TAG: ${{ steps.latest.outputs.latest_tag }} LATEST_TAG: ${{ steps.latest.outputs.latest_tag }}
SOURCE_BRANCH: ${{ inputs.branch }} SOURCE_BRANCH: ${{ steps.resolve.outputs.source_branch }}
SOURCE_COMMIT: ${{ inputs.commit }}
run: | run: |
# SOURCE_BRANCH is empty if the resolve step never produced an output
# (e.g. the workflow failed in or before that step). Show a placeholder
# in that case so the summary table still renders cleanly.
source_branch_display="${SOURCE_BRANCH:-(unresolved)}"
{ {
echo "## Backport release" echo "## Backport release"
echo "" echo ""
echo "| Field | Value |" echo "| Field | Value |"
echo "|---|---|" echo "|---|---|"
echo "| Source branch | \`${SOURCE_BRANCH}\` |" echo "| Source commit | \`${SOURCE_COMMIT}\` |"
echo "| Source branch | \`${source_branch_display}\` |"
echo "| Previous stable | \`${LATEST_TAG}\` |" echo "| Previous stable | \`${LATEST_TAG}\` |"
echo "| New version | \`${NEW_VERSION}\` |" echo "| New version | \`${NEW_VERSION}\` |"
echo "| Release branch | \`${RELEASE_BRANCH}\` |" echo "| Release branch | \`${RELEASE_BRANCH}\` |"