blob: e25f2f4df6b68a38a5d3c27f5a1072e585c4b0ae [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import re
import subprocess
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from plan import PROJECT_DIR, Package, plan
from trusted_publishing import temporary_trusted_publishing_token
def parse_retry_after(output: str) -> int:
match = re.search(r"Please try again after ([^\n]+)", output)
if match:
value = match.group(1).strip().rstrip(".")
for parser in (
parsedate_to_datetime,
lambda text: datetime.fromisoformat(text.replace("Z", "+00:00")),
):
try:
retry_at = parser(value)
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=timezone.utc)
return max(
60,
int((retry_at - datetime.now(timezone.utc)).total_seconds()) + 8,
)
except ValueError:
continue
return 610
def should_retry(output: str) -> bool:
lowered = output.lower()
return (
"too many requests" in lowered
or "rate limit" in lowered
or "you have published too many crates" in lowered
)
def already_published(output: str, package: Package) -> bool:
lowered = output.lower()
identity = f"{package.name}@{package.version}".lower()
return identity in lowered and (
"already exists" in lowered or "already uploaded" in lowered
)
def publish_package(package: Package) -> str:
command = [
"cargo",
"publish",
"--package",
package.name,
"--no-verify",
]
while True:
print(f"Publishing {package.name} {package.version}", flush=True)
with temporary_trusted_publishing_token() as token:
env = os.environ.copy()
env["CARGO_REGISTRY_TOKEN"] = token
process = subprocess.run(
command,
cwd=PROJECT_DIR,
check=False,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
output = process.stdout or ""
print(output, end="", flush=True)
if process.returncode == 0:
return "published"
if already_published(output, package):
print(
f"Skipping {package.name} {package.version}: already published",
flush=True,
)
return "already published"
if should_retry(output):
delay = parse_retry_after(output)
print(
f"crates.io rate limited {package.name}; sleeping {delay}s",
flush=True,
)
time.sleep(delay)
continue
raise subprocess.CalledProcessError(process.returncode, command, output=output)
def main() -> int:
results = [(package, publish_package(package)) for package in plan()]
print("Publish plan completed:", flush=True)
for package, result in results:
print(f"- {package.name} {package.version}: {result}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())