fixed testpypi upload

This commit is contained in:
ykcheungab
2026-09-12 17:34:46 +08:00
parent 94f38bc389
commit 488c8b0bc1
7 changed files with 1173 additions and 68 deletions
+136 -8
View File
@@ -1,8 +1,9 @@
"""Command line for ``python build/run.py release``.
Commands: ``preflight`` (local gate), ``audit`` (single wheel), ``github`` (sync
the public repo + publish a GitHub Release), ``pypi`` (TestPyPI then PyPI).
The publish commands are dry-run unless ``--apply`` is passed.
Commands: ``preflight`` (local gate), ``audit`` (single wheel), ``auth`` (PyPI
credentials in ~/.pypirc), ``github`` (sync the public repo + publish a GitHub
Release), ``pypi`` (TestPyPI then PyPI). The publish commands are dry-run unless
``--apply`` is passed, and tokens are never accepted as command-line arguments.
"""
from __future__ import annotations
@@ -24,7 +25,7 @@ from common.verify.wheel import ( # noqa: E402
verify_wheel_runtime,
)
from common.verify.wheel_contents import inspect_wheel # noqa: E402
from release import github, pypi # noqa: E402
from release import auth, github, pypi # noqa: E402
from release.preflight import PreflightOptions, run_preflight # noqa: E402
@@ -223,6 +224,106 @@ def _cmd_github(argv: list[str]) -> int:
)
def _cmd_auth(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
prog="build/run.py release auth",
description=(
"Inspect or set up PyPI credentials in ~/.pypirc. "
"Tokens are read from a hidden prompt or stdin, never from argv."
),
)
parser.add_argument(
"--set-token",
choices=sorted(auth.INDEXES),
default=None,
help="store a token for this index (prompts unless --token-stdin)",
)
parser.add_argument(
"--token-stdin",
action="store_true",
help="read the token from stdin instead of prompting (for scripts/CI)",
)
parser.add_argument(
"--repair",
action="store_true",
help=(
"add existing sections to [distutils] index-servers; twine ignores "
"sections that are not listed there"
),
)
parser.add_argument(
"--index",
choices=sorted(auth.INDEXES),
default="pypi",
help="index to report on when not setting a token (default: pypi)",
)
args = parser.parse_args(argv)
if args.repair:
path = auth.pypirc_path()
added = auth.repair_index_servers(path)
if added:
print(f"[ok ] added {added} to [distutils] index-servers in {path}")
else:
print(f"[ok ] nothing to repair in {path}")
for index in sorted(auth.INDEXES):
state = auth.credential_state(index)
flag = "[ok ]" if state.configured else "[-- ]"
detail = state.source if state.configured else state.problem
print(f"{flag} [{index}] {detail}")
return 0 if auth.credential_state(args.index).configured else 1
if args.set_token:
if args.token_stdin:
token = auth.read_token_from_stdin()
else:
token = auth.prompt_for_token(args.set_token)
if not token:
print("[FAIL] no token entered")
return 1
for problem in auth.validate_token(token):
print(f"[FAIL] {problem.removeprefix('ERROR ')}")
return 1
for warning in auth.token_warnings(token):
print(f"[warn] {warning.removeprefix('WARN ')}")
path = auth.write_token(args.set_token, token)
print(f"[ok ] wrote [{args.set_token}] credentials to {path}")
state = auth.credential_state(args.set_token)
if not state.configured:
print(f"[FAIL] the file still does not validate: {state.problem}")
return 1
print(f"[ok ] [{args.set_token}] credentials verified ({state.source})")
return 0
path = auth.pypirc_path()
print(f"config file: {path} ({'exists' if path.is_file() else 'missing'})")
for index in sorted(auth.INDEXES):
state = auth.credential_state(index)
if state.configured:
print(f"[ok ] [{index}] configured ({state.source})")
else:
print(f"[-- ] [{index}] not usable: {state.problem}")
unlisted = auth.unlisted_sections(path)
if unlisted:
print()
print(
f"[warn] {unlisted} have sections that [distutils] index-servers does not "
"list; twine ignores those entirely"
)
print(" fix with: python build/run.py release auth --repair")
print()
print("set a token with:")
for index in sorted(auth.INDEXES):
if not auth.credential_state(index).configured:
print(f" python build/run.py release auth --set-token {index}")
print(" (TestPyPI needs its own account; if you only hold a production token,")
print(" skip it and publish with `release pypi --pypi-only`)")
return 0 if auth.credential_state(args.index).configured else 1
def _cmd_pypi(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
prog="build/run.py release pypi",
@@ -240,7 +341,22 @@ def _cmd_pypi(argv: list[str]) -> int:
help="after TestPyPI, also publish to the real PyPI",
)
parser.add_argument(
"--repository", default="testpypi", help="twine repository for the first phase"
"--pypi-only",
action="store_true",
help=(
"upload straight to PyPI, skipping the TestPyPI rehearsal "
"(for when you only hold a production token)"
),
)
parser.add_argument(
"--prompt-credentials",
action="store_true",
help="if a token is missing, prompt for it and store it in ~/.pypirc",
)
parser.add_argument(
"--token-stdin",
action="store_true",
help="read a missing token from stdin instead of prompting (scripts/CI)",
)
parser.add_argument(
"--skip-install-verify",
@@ -250,14 +366,24 @@ def _cmd_pypi(argv: list[str]) -> int:
parser.add_argument("--skip-check", action="store_true", help="skip `twine check`")
args = parser.parse_args(argv)
if args.pypi_only and args.confirm_pypi:
parser.error("--pypi-only and --confirm-pypi are mutually exclusive")
if args.token_stdin and not args.prompt_credentials:
parser.error("--token-stdin requires --prompt-credentials")
token = auth.read_token_from_stdin() if args.token_stdin else None
return pypi.publish(
pypi.Options(
version=args.version,
apply=args.apply,
confirm_pypi=args.confirm_pypi,
repository=args.repository,
verify_install=not args.skip_install_verify,
skip_check=args.skip_check,
pypi_only=args.pypi_only,
prompt_credentials=args.prompt_credentials,
token=token,
)
)
@@ -266,7 +392,7 @@ def main(argv: list[str] | None = None) -> int:
argv = list(sys.argv[1:] if argv is None else argv)
if not argv or argv[0] in ("-h", "--help"):
print(__doc__)
print("Commands: preflight | audit | github | pypi")
print("Commands: preflight | audit | auth | github | pypi")
return 0
command, rest = argv[0], argv[1:]
@@ -274,13 +400,15 @@ def main(argv: list[str] | None = None) -> int:
return _cmd_preflight(rest)
if command == "audit":
return _cmd_audit(rest)
if command == "auth":
return _cmd_auth(rest)
if command == "github":
return _cmd_github(rest)
if command == "pypi":
return _cmd_pypi(rest)
print(f"Unknown release command: {command!r}", file=sys.stderr)
print("Choose from: preflight, audit, github, pypi", file=sys.stderr)
print("Choose from: preflight, audit, auth, github, pypi", file=sys.stderr)
return 2