mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-22 01:00:07 +08:00
Compare commits
8 Commits
v0.22.0
...
feat/cut-r
| Author | SHA1 | Date | |
|---|---|---|---|
| 91fe94bb78 | |||
| b7a09856e0 | |||
| 8626a290c4 | |||
| 47417ce7ec | |||
| a0a65f51bc | |||
| 3566e6b6a6 | |||
| a8e11936f3 | |||
| 08a9245e7f |
456
.github/workflows/cut-release.yml
vendored
Normal file
456
.github/workflows/cut-release.yml
vendored
Normal file
@ -0,0 +1,456 @@
|
||||
name: Cut Release
|
||||
|
||||
# Promote a prepared "candidate" backport branch into a real release.
|
||||
#
|
||||
# Operator workflow:
|
||||
# 1. Locally cherry-pick the desired commits onto the previous stable tag
|
||||
# (e.g. v0.20.2). Do NOT include a version-bump commit.
|
||||
# 2. Push the candidate branch to origin and open a PR against `master`.
|
||||
# 3. Wait for CI to go green; get an APPROVED review from a user listed
|
||||
# in the `STABLE_RELEASE_APPROVERS` repo variable (comma-separated
|
||||
# GitHub logins).
|
||||
# 4. Run this workflow with `source_branch` = the candidate branch.
|
||||
#
|
||||
# This workflow then verifies all gates, computes the next version
|
||||
# (previous stable tag + patch+1), creates the version-bump commit on top
|
||||
# of the candidate, and pushes `release/v<version>` + `v<version>`
|
||||
# atomically using RELEASE_BOT_TOKEN.
|
||||
#
|
||||
# After this runs, kick off `release-stable-all.yml` manually with the new
|
||||
# tag to build portable artifacts.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_branch:
|
||||
description: 'Candidate backport branch on origin (cherry-picks only — NO version bump)'
|
||||
required: true
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Validate only — do not push branch or tag'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
cut-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# GITHUB_TOKEN is used only for read-only API calls (PR / review /
|
||||
# check lookups). The actual ref creation uses RELEASE_BOT_TOKEN to
|
||||
# bypass branch-protection rules on `release/*` and `v*`.
|
||||
contents: read
|
||||
pull-requests: read
|
||||
checks: read
|
||||
env:
|
||||
# All workflow inputs are pulled in via env vars instead of being
|
||||
# interpolated directly into bash, to avoid command injection.
|
||||
INPUT_SOURCE_BRANCH: ${{ inputs.source_branch }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
REPO_FULL: ${{ github.repository }}
|
||||
APPROVERS: ${{ vars.STABLE_RELEASE_APPROVERS }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Validate inputs and config
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF_RE='^[A-Za-z0-9._/-]+$'
|
||||
if ! [[ "$INPUT_SOURCE_BRANCH" =~ $REF_RE ]]; then
|
||||
echo "::error::Invalid source_branch '$INPUT_SOURCE_BRANCH' — only [A-Za-z0-9._/-] allowed."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${APPROVERS:-}" ]; then
|
||||
echo "::error::Repository variable STABLE_RELEASE_APPROVERS is not set. Configure it as a comma-separated list of GitHub logins permitted to approve stable backport releases."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check out master with full history + tags
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
# Defense in depth: don't leave a usable git credential on disk
|
||||
# for any code that runs against the checked-out branch.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Determine latest stable tag and next version
|
||||
id: ver
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force origin
|
||||
# Latest stable tag = highest semver vX.Y.Z (no pre-release suffix).
|
||||
LATEST_TAG=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
| head -n1)
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "::error::Could not determine latest stable vX.Y.Z tag."
|
||||
exit 1
|
||||
fi
|
||||
# Peel annotated tag → commit SHA.
|
||||
LATEST_SHA=$(git rev-list -n1 "$LATEST_TAG")
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
IFS=. read -r MAJ MIN PATCH <<<"$BASE_VERSION"
|
||||
NEXT_PATCH=$((PATCH + 1))
|
||||
NEXT_VERSION="${MAJ}.${MIN}.${NEXT_PATCH}"
|
||||
NEXT_TAG="v${NEXT_VERSION}"
|
||||
NEXT_BRANCH="release/${NEXT_TAG}"
|
||||
{
|
||||
echo "latest_tag=$LATEST_TAG"
|
||||
echo "latest_sha=$LATEST_SHA"
|
||||
echo "latest_version=$BASE_VERSION"
|
||||
echo "next_version=$NEXT_VERSION"
|
||||
echo "next_tag=$NEXT_TAG"
|
||||
echo "next_branch=$NEXT_BRANCH"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Latest stable: $LATEST_TAG ($LATEST_SHA)"
|
||||
echo "Next release : $NEXT_TAG"
|
||||
|
||||
- name: Pre-flight remote checks
|
||||
env:
|
||||
NEXT_BRANCH: ${{ steps.ver.outputs.next_branch }}
|
||||
NEXT_TAG: ${{ steps.ver.outputs.next_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="https://github.com/${REPO_FULL}.git"
|
||||
if ! git ls-remote --heads --exit-code "$REPO_URL" "$INPUT_SOURCE_BRANCH" >/dev/null; then
|
||||
echo "::error::Source branch '$INPUT_SOURCE_BRANCH' does not exist on origin."
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --heads --exit-code "$REPO_URL" "$NEXT_BRANCH" >/dev/null 2>&1; then
|
||||
echo "::error::Release branch '$NEXT_BRANCH' already exists on origin. Refusing to overwrite."
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --tags --exit-code "$REPO_URL" "$NEXT_TAG" >/dev/null 2>&1; then
|
||||
echo "::error::Tag '$NEXT_TAG' already exists on origin. Refusing to overwrite."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify candidate branch is rooted at latest stable tag
|
||||
id: root
|
||||
env:
|
||||
LATEST_SHA: ${{ steps.ver.outputs.latest_sha }}
|
||||
LATEST_TAG: ${{ steps.ver.outputs.latest_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch origin "+${INPUT_SOURCE_BRANCH}:refs/remotes/origin/${INPUT_SOURCE_BRANCH}"
|
||||
SOURCE_SHA=$(git rev-parse "refs/remotes/origin/${INPUT_SOURCE_BRANCH}")
|
||||
|
||||
# 1. Latest stable tag must be an ancestor of the candidate.
|
||||
if ! git merge-base --is-ancestor "$LATEST_SHA" "$SOURCE_SHA"; then
|
||||
echo "::error::Latest stable tag $LATEST_TAG is not an ancestor of $INPUT_SOURCE_BRANCH. The candidate branch must be branched from $LATEST_TAG."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. merge-base(source, master) MUST equal the latest tag commit.
|
||||
# If the operator merged or rebased master into the backport, the
|
||||
# branch will share extra commits with master past the last release
|
||||
# — that's not a clean backport.
|
||||
MB=$(git merge-base "$SOURCE_SHA" "origin/master")
|
||||
if [ "$MB" != "$LATEST_SHA" ]; then
|
||||
echo "::error::Branch $INPUT_SOURCE_BRANCH is not rooted at $LATEST_TAG. merge-base with master is $MB, expected $LATEST_SHA. The candidate must contain only cherry-picked commits on top of $LATEST_TAG."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. No merge commits in the cherry-pick range — backports must be
|
||||
# linear so the diff being released is auditable in the PR.
|
||||
if [ -n "$(git log --merges --format=%H "${LATEST_SHA}..${SOURCE_SHA}")" ]; then
|
||||
echo "::error::Candidate branch contains merge commits between $LATEST_TAG and HEAD. Backport branches must be linear cherry-picks only."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Branch is rooted at $LATEST_TAG ✅"
|
||||
|
||||
- name: Verify version files are unchanged on candidate
|
||||
env:
|
||||
LATEST_VERSION: ${{ steps.ver.outputs.latest_version }}
|
||||
SOURCE_SHA: ${{ steps.root.outputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Read version files at the candidate HEAD WITHOUT switching the
|
||||
# worktree, so we don't run any candidate-supplied code.
|
||||
PYPROJECT_RAW=$(git show "${SOURCE_SHA}:pyproject.toml")
|
||||
PYPROJECT_VERSION=$(printf '%s' "$PYPROJECT_RAW" \
|
||||
| python3 -c "import sys,tomllib; print(tomllib.loads(sys.stdin.read())['project']['version'])")
|
||||
if [ "$PYPROJECT_VERSION" != "$LATEST_VERSION" ]; then
|
||||
echo "::error::pyproject.toml version on candidate is '$PYPROJECT_VERSION' but should still be '$LATEST_VERSION' (the previous stable tag). Do not include a version-bump commit on the candidate — this workflow adds it."
|
||||
exit 1
|
||||
fi
|
||||
# comfyui_version.py contains Python — never `exec()` it. A
|
||||
# malicious candidate could replace it with arbitrary code that
|
||||
# would then run in CI with RELEASE_BOT_TOKEN in scope. Statically
|
||||
# parse the AST to extract __version__ instead.
|
||||
# Note: piping into `python3 -c '...'` (NOT `python3 - <<EOF`,
|
||||
# which would redirect stdin to the heredoc itself).
|
||||
MODULE_VERSION=$(git show "${SOURCE_SHA}:comfyui_version.py" | python3 -c '
|
||||
import ast, sys
|
||||
tree = ast.parse(sys.stdin.read())
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "__version__":
|
||||
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
||||
print(node.value.value)
|
||||
sys.exit(0)
|
||||
sys.exit("Could not statically read __version__ from comfyui_version.py")
|
||||
')
|
||||
if [ "$MODULE_VERSION" != "$LATEST_VERSION" ]; then
|
||||
echo "::error::comfyui_version.py __version__ on candidate is '$MODULE_VERSION' but should still be '$LATEST_VERSION'. Do not include a version-bump commit on the candidate — this workflow adds it."
|
||||
exit 1
|
||||
fi
|
||||
echo "Version files unchanged on candidate ✅"
|
||||
|
||||
- name: Verify PR exists, is approved by a permitted user, and CI is green
|
||||
id: pr
|
||||
env:
|
||||
SOURCE_SHA: ${{ steps.root.outputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
OWNER="${REPO_FULL%/*}"
|
||||
|
||||
# Locate the open PR with this branch as its head (same-repo only).
|
||||
PR_LIST=$(gh api "repos/${REPO_FULL}/pulls" \
|
||||
-X GET -f state=open -f "head=${OWNER}:${INPUT_SOURCE_BRANCH}" \
|
||||
--jq '[.[] | {number,head_sha:.head.sha,base:.base.ref}]')
|
||||
PR_COUNT=$(echo "$PR_LIST" | jq 'length')
|
||||
if [ "$PR_COUNT" -ne 1 ]; then
|
||||
echo "::error::Expected exactly 1 open PR with head '${INPUT_SOURCE_BRANCH}', found $PR_COUNT."
|
||||
echo "$PR_LIST" | jq .
|
||||
exit 1
|
||||
fi
|
||||
PR_NUMBER=$(echo "$PR_LIST" | jq -r '.[0].number')
|
||||
PR_HEAD_SHA=$(echo "$PR_LIST" | jq -r '.[0].head_sha')
|
||||
PR_BASE=$(echo "$PR_LIST" | jq -r '.[0].base')
|
||||
|
||||
if [ "$PR_BASE" != "master" ]; then
|
||||
echo "::error::PR #${PR_NUMBER} targets '$PR_BASE', expected 'master'."
|
||||
exit 1
|
||||
fi
|
||||
if [ "$PR_HEAD_SHA" != "$SOURCE_SHA" ]; then
|
||||
echo "::error::PR #${PR_NUMBER} HEAD ($PR_HEAD_SHA) does not match candidate branch HEAD ($SOURCE_SHA). The branch may have been updated after the PR was approved — re-trigger after pushing the latest commits."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- Approval check ----
|
||||
# An approving review must be:
|
||||
# * from a user in the STABLE_RELEASE_APPROVERS allow-list,
|
||||
# * state == APPROVED,
|
||||
# * on the current HEAD (commit_id == PR_HEAD_SHA) — newer
|
||||
# commits invalidate older approvals.
|
||||
REVIEWS=$(gh api "repos/${REPO_FULL}/pulls/${PR_NUMBER}/reviews" --paginate)
|
||||
# Normalize allow-list and find an APPROVED review on the current
|
||||
# head SHA, all inside jq so we don't trip over login chars like
|
||||
# `[bot]` that bash globbing/word-splitting would mishandle.
|
||||
#
|
||||
# GitHub returns every review submission separately, so a reviewer
|
||||
# who approved and then later submitted CHANGES_REQUESTED or had
|
||||
# their approval dismissed will still have the old APPROVED entry
|
||||
# in the list. To honor the *latest* state, filter to stateful
|
||||
# reviews only (APPROVED / CHANGES_REQUESTED / DISMISSED — plain
|
||||
# COMMENTED replies don't override an approval, matching GitHub's
|
||||
# own behavior), group by reviewer, take each reviewer's last
|
||||
# stateful submission, then accept if it's APPROVED.
|
||||
APPROVED_BY=$(echo "$REVIEWS" | jq -r --arg list "$APPROVERS" --arg sha "$PR_HEAD_SHA" '
|
||||
($list
|
||||
| split(",")
|
||||
| map(gsub("\\s"; "") | ascii_downcase)
|
||||
| map(select(length > 0))
|
||||
) as $allow
|
||||
| [ .[]
|
||||
| select(.commit_id == $sha)
|
||||
| select(.user.login | ascii_downcase | IN($allow[]))
|
||||
| select(.state == "APPROVED"
|
||||
or .state == "CHANGES_REQUESTED"
|
||||
or .state == "DISMISSED")
|
||||
]
|
||||
| group_by(.user.login | ascii_downcase)
|
||||
| map(sort_by(.submitted_at, .id) | last)
|
||||
| map(select(.state == "APPROVED"))
|
||||
| .[0].user.login // ""
|
||||
')
|
||||
if [ -z "$APPROVED_BY" ]; then
|
||||
echo "::error::PR #${PR_NUMBER} has no APPROVED review on commit ${PR_HEAD_SHA:0:12} from a permitted approver. Allowed approvers: ${APPROVERS}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- CI check ----
|
||||
# All check-runs on PR_HEAD_SHA must be completed with conclusion
|
||||
# success/neutral/skipped. We do NOT use mergeable_state because
|
||||
# branch protection may require "up-to-date with master", which
|
||||
# backport branches by definition are not (and shouldn't be).
|
||||
CHECKS=$(gh api "repos/${REPO_FULL}/commits/${PR_HEAD_SHA}/check-runs" --paginate \
|
||||
--jq '.check_runs')
|
||||
PENDING=$(echo "$CHECKS" | jq -r '.[] | select(.status!="completed") | .name')
|
||||
FAILED=$(echo "$CHECKS" | jq -r '.[] | select(.status=="completed" and (.conclusion!="success" and .conclusion!="neutral" and .conclusion!="skipped")) | "\(.name) (\(.conclusion))"')
|
||||
TOTAL=$(echo "$CHECKS" | jq 'length')
|
||||
if [ "$TOTAL" -eq 0 ]; then
|
||||
echo "::error::No check-runs found on commit ${PR_HEAD_SHA:0:12}. CI must run before promoting a release."
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$PENDING" ]; then
|
||||
echo "::error::PR #${PR_NUMBER} has pending check-runs on ${PR_HEAD_SHA:0:12}:"
|
||||
echo "$PENDING" | sed 's/^/ - /'
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$FAILED" ]; then
|
||||
echo "::error::PR #${PR_NUMBER} has failing check-runs on ${PR_HEAD_SHA:0:12}:"
|
||||
echo "$FAILED" | sed 's/^/ - /'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Also reject if any classic commit-status is failing/pending.
|
||||
STATUSES=$(gh api "repos/${REPO_FULL}/commits/${PR_HEAD_SHA}/status" \
|
||||
--jq '{state: .state, contexts: [.statuses[] | {context, state}]}')
|
||||
STATE=$(echo "$STATUSES" | jq -r '.state')
|
||||
if [ "$STATE" != "success" ] && [ "$STATE" != "pending" ]; then
|
||||
# "pending" with zero contexts means "no statuses" (only check-runs) — allow.
|
||||
CTX_COUNT=$(echo "$STATUSES" | jq '.contexts | length')
|
||||
if [ "$STATE" = "pending" ] && [ "$CTX_COUNT" -eq 0 ]; then
|
||||
:
|
||||
else
|
||||
echo "::error::PR #${PR_NUMBER} commit-status rollup is '$STATE' on ${PR_HEAD_SHA:0:12}:"
|
||||
echo "$STATUSES" | jq -r '.contexts[] | " - \(.context): \(.state)"'
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$STATE" = "pending" ]; then
|
||||
CTX_COUNT=$(echo "$STATUSES" | jq '.contexts | length')
|
||||
if [ "$CTX_COUNT" -gt 0 ]; then
|
||||
echo "::error::PR #${PR_NUMBER} has pending commit-statuses on ${PR_HEAD_SHA:0:12}:"
|
||||
echo "$STATUSES" | jq -r '.contexts[] | select(.state!="success") | " - \(.context): \(.state)"'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "pr_number=$PR_NUMBER"
|
||||
echo "approved_by=$APPROVED_BY"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "PR #${PR_NUMBER} approved by @${APPROVED_BY}, CI green ($TOTAL checks) ✅"
|
||||
|
||||
- name: Apply version bump on top of candidate
|
||||
id: bump
|
||||
env:
|
||||
NEXT_VERSION: ${{ steps.ver.outputs.next_version }}
|
||||
SOURCE_SHA: ${{ steps.root.outputs.source_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Materialize the candidate as a local branch so we can build a
|
||||
# commit on top of it. We do NOT execute anything from the
|
||||
# candidate's content — only edit two files via stdlib parsers.
|
||||
git checkout -B _release_local "$SOURCE_SHA"
|
||||
|
||||
python3 - "$NEXT_VERSION" <<'PY'
|
||||
import pathlib, re, sys
|
||||
version = sys.argv[1]
|
||||
# pyproject.toml — replace only the top-level `version = "..."`
|
||||
# line under [project]. Use a strict, anchored regex.
|
||||
p = pathlib.Path("pyproject.toml")
|
||||
text = p.read_text(encoding="utf-8")
|
||||
new = re.sub(r'(?m)^(version\s*=\s*")([^"]+)(")',
|
||||
lambda m: m.group(1) + version + m.group(3),
|
||||
text, count=1)
|
||||
if new == text:
|
||||
sys.exit("Failed to update version in pyproject.toml")
|
||||
p.write_text(new, encoding="utf-8")
|
||||
|
||||
# comfyui_version.py — replace `__version__ = "..."`.
|
||||
p = pathlib.Path("comfyui_version.py")
|
||||
text = p.read_text(encoding="utf-8")
|
||||
new = re.sub(r'__version__\s*=\s*"[^"]+"',
|
||||
f'__version__ = "{version}"',
|
||||
text, count=1)
|
||||
if new == text:
|
||||
sys.exit("Failed to update __version__ in comfyui_version.py")
|
||||
p.write_text(new, encoding="utf-8")
|
||||
PY
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add pyproject.toml comfyui_version.py
|
||||
git commit -m "Bump version to ${NEXT_VERSION}"
|
||||
BUMP_SHA=$(git rev-parse HEAD)
|
||||
echo "bump_sha=$BUMP_SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "Created bump commit ${BUMP_SHA:0:12}"
|
||||
|
||||
- name: Plan summary
|
||||
env:
|
||||
LATEST_TAG: ${{ steps.ver.outputs.latest_tag }}
|
||||
NEXT_TAG: ${{ steps.ver.outputs.next_tag }}
|
||||
NEXT_BRANCH: ${{ steps.ver.outputs.next_branch }}
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
||||
APPROVED_BY: ${{ steps.pr.outputs.approved_by }}
|
||||
SOURCE_SHA: ${{ steps.root.outputs.source_sha }}
|
||||
BUMP_SHA: ${{ steps.bump.outputs.bump_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "## Cut Release plan"
|
||||
echo ""
|
||||
echo "| Field | Value |"
|
||||
echo "| --- | --- |"
|
||||
echo "| Source branch | \`$INPUT_SOURCE_BRANCH\` @ \`${SOURCE_SHA:0:12}\` |"
|
||||
echo "| Source PR | [#${PR_NUMBER}](https://github.com/${REPO_FULL}/pull/${PR_NUMBER}) |"
|
||||
echo "| Approved by | @${APPROVED_BY} |"
|
||||
echo "| Previous stable tag | \`$LATEST_TAG\` |"
|
||||
echo "| Next tag | \`$NEXT_TAG\` |"
|
||||
echo "| Next branch | \`$NEXT_BRANCH\` |"
|
||||
echo "| Version-bump commit | \`${BUMP_SHA:0:12}\` |"
|
||||
echo "| Dry run | \`$INPUT_DRY_RUN\` |"
|
||||
echo ""
|
||||
echo "### Commits to be released (\`${LATEST_TAG}..${BUMP_SHA}\`)"
|
||||
echo ""
|
||||
echo '```'
|
||||
git log --oneline --no-decorate "${LATEST_TAG}..${BUMP_SHA}"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Push release branch and tag (atomic)
|
||||
if: inputs.dry_run != true && inputs.dry_run != 'true'
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }}
|
||||
NEXT_BRANCH: ${{ steps.ver.outputs.next_branch }}
|
||||
NEXT_TAG: ${{ steps.ver.outputs.next_tag }}
|
||||
NEXT_VERSION: ${{ steps.ver.outputs.next_version }}
|
||||
BUMP_SHA: ${{ steps.bump.outputs.bump_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||
echo "::error::secrets.RELEASE_BOT_TOKEN is not set."
|
||||
exit 1
|
||||
fi
|
||||
# Create the annotated tag locally first so we can push branch +
|
||||
# tag in a single atomic operation: either both refs are created
|
||||
# on origin, or neither — no half-promoted state.
|
||||
git tag -a "$NEXT_TAG" "$BUMP_SHA" -m "ComfyUI v${NEXT_VERSION}"
|
||||
AUTH_URL="https://x-access-token:${RELEASE_TOKEN}@github.com/${REPO_FULL}.git"
|
||||
git push --atomic "$AUTH_URL" \
|
||||
"${BUMP_SHA}:refs/heads/${NEXT_BRANCH}" \
|
||||
"refs/tags/${NEXT_TAG}"
|
||||
|
||||
- name: Final summary
|
||||
if: always()
|
||||
env:
|
||||
NEXT_BRANCH: ${{ steps.ver.outputs.next_branch }}
|
||||
NEXT_TAG: ${{ steps.ver.outputs.next_tag }}
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo ""
|
||||
echo "### Result"
|
||||
if [ "$JOB_STATUS" != "success" ]; then
|
||||
echo "❌ Workflow did not complete successfully (job status: \`$JOB_STATUS\`). See the run logs for details. No branch or tag should be assumed to have been created."
|
||||
elif [ "$INPUT_DRY_RUN" = "true" ]; then
|
||||
echo "🔍 **Dry run** — no branch or tag was created."
|
||||
else
|
||||
echo "✅ Created \`$NEXT_BRANCH\` and tagged \`$NEXT_TAG\` from \`$INPUT_SOURCE_BRANCH\` (with version bump)."
|
||||
echo ""
|
||||
echo "- Branch: <https://github.com/${REPO_FULL}/tree/${NEXT_BRANCH}>"
|
||||
# /releases/tag/ would 404 — this workflow creates an annotated
|
||||
# git tag, not a GitHub Release object.
|
||||
echo "- Tag: <https://github.com/${REPO_FULL}/tree/${NEXT_TAG}>"
|
||||
echo ""
|
||||
echo "Next: run [release-stable-all.yml](https://github.com/${REPO_FULL}/actions/workflows/release-stable-all.yml) with \`git_tag=${NEXT_TAG}\`."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Reference in New Issue
Block a user