fix github release
This commit is contained in:
+89
-20
@@ -113,9 +113,14 @@ def build_github_steps(
|
||||
tag: str,
|
||||
assets: list[Path],
|
||||
notes_file: Path | None,
|
||||
create_tag: bool = True,
|
||||
title: str | None = None,
|
||||
) -> list[CommandStep]:
|
||||
"""The mutating commands, as pure data so they can be asserted in tests."""
|
||||
"""The mutating commands, as pure data so they can be asserted in tests.
|
||||
|
||||
``create_tag=False`` is the resume path: the tag already exists (a previous
|
||||
run pushed it before failing), so only the remaining steps are emitted.
|
||||
"""
|
||||
release_argv = [
|
||||
"gh",
|
||||
"release",
|
||||
@@ -130,15 +135,34 @@ def build_github_steps(
|
||||
release_argv += ["--notes-file", str(notes_file)]
|
||||
release_argv += [str(asset) for asset in assets]
|
||||
|
||||
return [
|
||||
CommandStep("create tag", ["git", "tag", tag]),
|
||||
CommandStep("push branch", ["git", "push", "origin", branch]),
|
||||
CommandStep("push tag", ["git", "push", "origin", tag]),
|
||||
CommandStep("create GitHub Release with assets", release_argv),
|
||||
]
|
||||
steps: list[CommandStep] = []
|
||||
if create_tag:
|
||||
steps.append(CommandStep("create tag", ["git", "tag", tag]))
|
||||
steps.append(CommandStep("push branch", ["git", "push", "origin", branch]))
|
||||
steps.append(CommandStep("push tag", ["git", "push", "origin", tag]))
|
||||
steps.append(CommandStep("create GitHub Release with assets", release_argv))
|
||||
return steps
|
||||
|
||||
|
||||
def _check_gh() -> list[str]:
|
||||
def non_github_remote_problem(repo: Path) -> str | None:
|
||||
"""Why ``gh`` cannot target this repo, or ``None`` if the remote is fine.
|
||||
|
||||
``gh`` resolves its repository from the working directory. Running it from
|
||||
this checkout instead of the release repo is a real failure mode: the SDK
|
||||
repo's origin is an internal host, and ``gh`` reports the confusing "none of
|
||||
the git remotes ... point to a known GitHub host".
|
||||
"""
|
||||
remote = _git(repo, "remote", "get-url", "origin", check=False)
|
||||
if "github.com" in remote:
|
||||
return None
|
||||
return (
|
||||
f"the release repo's origin is {remote!r}, which is not a GitHub host. "
|
||||
"`gh` resolves the repository from the working directory, so point origin "
|
||||
"at github.com before publishing a release"
|
||||
)
|
||||
|
||||
|
||||
def _check_gh(repo: Path) -> list[str]:
|
||||
"""Problems that would stop the release step, reported before any mutation."""
|
||||
problems: list[str] = []
|
||||
if shutil.which("gh") is None:
|
||||
@@ -147,6 +171,11 @@ def _check_gh() -> list[str]:
|
||||
"and authenticate (`gh auth login`), or use --skip-release-upload"
|
||||
)
|
||||
return problems
|
||||
|
||||
remote_problem = non_github_remote_problem(repo)
|
||||
if remote_problem:
|
||||
problems.append(remote_problem)
|
||||
|
||||
completed = subprocess.run(
|
||||
["gh", "auth", "status"], capture_output=True, text=True, check=False
|
||||
)
|
||||
@@ -161,12 +190,25 @@ def _existing_release_tags(repo: Path, version: str) -> list[str]:
|
||||
return sorted(wanted & tags)
|
||||
|
||||
|
||||
def _release_exists(tag: str) -> bool:
|
||||
def _release_exists(repo: Path, tag: str) -> bool | None:
|
||||
"""Is there already a GitHub Release for ``tag``?
|
||||
|
||||
Returns ``None`` when that cannot be determined (``gh`` unavailable), so the
|
||||
caller can distinguish "not published yet" from "unknown" instead of treating
|
||||
a missing tool as proof that nothing was published.
|
||||
|
||||
Must run with ``cwd`` set to the release repo: `gh` resolves the target
|
||||
repository from the working directory, and this checkout's own remote is an
|
||||
internal host.
|
||||
"""
|
||||
if shutil.which("gh") is None:
|
||||
return None
|
||||
completed = subprocess.run(
|
||||
["gh", "release", "view", tag, "--json", "tagName"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
cwd=str(repo),
|
||||
)
|
||||
return completed.returncode == 0
|
||||
|
||||
@@ -244,23 +286,37 @@ def publish(options: Options) -> int:
|
||||
|
||||
tag = f"v{version}"
|
||||
existing = _existing_release_tags(repo, version)
|
||||
# A tag that exists without a GitHub Release means a previous run got as far
|
||||
# as pushing the tag and then failed (or the upload was left manual). That is
|
||||
# exactly the resumable state, so continue instead of refusing.
|
||||
create_tag = not existing
|
||||
if existing:
|
||||
print(
|
||||
f"[FAIL] tag(s) {existing} already exist in {repo}; "
|
||||
f"{version} looks already released"
|
||||
f"[warn] tag(s) {existing} already exist in {repo}; skipping tag creation "
|
||||
"and continuing to the release upload"
|
||||
)
|
||||
release_state = _release_exists(repo, tag)
|
||||
if release_state is True:
|
||||
print(f"[FAIL] GitHub Release {tag} already exists; nothing to publish")
|
||||
return 1
|
||||
if shutil.which("gh") and _release_exists(tag):
|
||||
print(f"[FAIL] GitHub Release {tag} already exists")
|
||||
return 1
|
||||
print(f"[ok ] no existing tag or release for {tag}")
|
||||
if release_state is None:
|
||||
print(
|
||||
"[warn] cannot tell whether a GitHub Release for "
|
||||
f"{tag} already exists (`gh` not installed); --apply will stop at the gh check"
|
||||
)
|
||||
if create_tag:
|
||||
print(f"[ok ] tag {tag} does not exist in {repo}")
|
||||
elif create_tag:
|
||||
print(f"[ok ] no existing tag or GitHub Release for {tag}")
|
||||
else:
|
||||
print(f"[ok ] no GitHub Release for {tag} yet")
|
||||
|
||||
# A dry run is a preview of what would happen, so missing tooling is a warning
|
||||
# there and a hard stop only when we are about to mutate something.
|
||||
if options.skip_release_upload:
|
||||
print("[warn] --skip-release-upload: the GitHub Release will be left to you")
|
||||
else:
|
||||
gh_problems = _check_gh()
|
||||
gh_problems = _check_gh(repo)
|
||||
if gh_problems:
|
||||
for problem in gh_problems:
|
||||
print(f"[{'FAIL' if options.apply else 'warn'}] {problem}")
|
||||
@@ -280,13 +336,21 @@ def publish(options: Options) -> int:
|
||||
assets = [asset_dir / wheel.name for wheel in wheels]
|
||||
notes_file = options.notes_file
|
||||
steps = build_github_steps(
|
||||
version=version, branch=branch, tag=tag, assets=assets, notes_file=notes_file
|
||||
version=version,
|
||||
branch=branch,
|
||||
tag=tag,
|
||||
assets=assets,
|
||||
notes_file=notes_file,
|
||||
create_tag=create_tag,
|
||||
)
|
||||
|
||||
print()
|
||||
print(f"[plan] sync {len(manifest_entry().get('sync', []))} entry(ies) into {repo}")
|
||||
print(f"[plan] stage {len(wheels)} wheel(s) into {asset_dir}")
|
||||
print(f"[plan] commit {repo} and tag {tag}")
|
||||
if create_tag:
|
||||
print(f"[plan] commit {repo} and create tag {tag}")
|
||||
else:
|
||||
print(f"[plan] commit {repo} (tag {tag} already exists)")
|
||||
for step in steps:
|
||||
manual = options.skip_release_upload and step.description.startswith(
|
||||
"create GitHub Release"
|
||||
@@ -316,7 +380,9 @@ def publish(options: Options) -> int:
|
||||
else:
|
||||
print("[ok ] nothing new to commit in the release repo")
|
||||
|
||||
_git(repo, "tag", tag)
|
||||
if create_tag:
|
||||
_git(repo, "tag", tag)
|
||||
# Pushing an already-up-to-date ref is a no-op, so resuming is safe.
|
||||
_git(repo, "push", "origin", branch)
|
||||
_git(repo, "push", "origin", tag)
|
||||
print(f"[ok ] pushed {branch} and {tag}")
|
||||
@@ -336,7 +402,9 @@ def publish(options: Options) -> int:
|
||||
print("=== release github: repo synced and tagged; release upload left manual ===")
|
||||
return 0
|
||||
|
||||
completed = subprocess.run(release_argv, check=False)
|
||||
# `gh` resolves its target repository from the working directory, so it must
|
||||
# run inside the release repo - this checkout's own origin is an internal host.
|
||||
completed = subprocess.run(release_argv, check=False, cwd=str(repo))
|
||||
if completed.returncode != 0:
|
||||
print(f"[FAIL] gh release create exited {completed.returncode}")
|
||||
return 1
|
||||
@@ -348,6 +416,7 @@ def publish(options: Options) -> int:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
cwd=str(repo),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
print("[FAIL] could not read back the release to verify assets")
|
||||
|
||||
Reference in New Issue
Block a user