From c1ab3dae7f6357c3d79ca806aa02203fb8300b34 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Mon, 27 Jul 2026 08:56:36 +0000 Subject: [PATCH] Make release script more robust and send user agent The data access policy of crates.io requires a `user-agent` header that identifies the application. Otherwise it may block the request, which we saw in previous runs. See https://crates.io/data-access for details. This commit adds the required user-agent header and also fixes a related bug that misclassified the error response as 'release does not exist yet'. We now only create a new release if we're sure that the release does not exist yet. Errors are raised and fail the CI job so that we can investigate them. --- .../trigger-release/trigger-release.py | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/trigger-release/trigger-release.py b/.github/workflows/trigger-release/trigger-release.py index 5eab9c64..164000a5 100644 --- a/.github/workflows/trigger-release/trigger-release.py +++ b/.github/workflows/trigger-release/trigger-release.py @@ -1,3 +1,4 @@ +import sys import toml import requests import subprocess @@ -6,16 +7,23 @@ crate_version = cargo_toml["workspace"]["package"]["version"] print("Detected crate version " + crate_version) +# crates.io enforces a data-access policy that requires a descriptive +# User-Agent with contact info; requests without one get rejected, which +# previously made this script misclassify already-published versions as +# unreleased. See https://crates.io/data-access. +headers = { + "User-Agent": "bootloader-release-ci (https://github.com/rust-osdev/bootloader)" +} api_url = "https://crates.io/api/v1/crates/bootloader/" + crate_version -released_version = requests.get(api_url).json() +response = requests.get(api_url, headers=headers) -if "version" in released_version: - version = released_version["version"] +if response.status_code == 200 and "version" in response.json(): + version = response.json()["version"] assert (version["crate"] == "bootloader") assert (version["num"] == crate_version) print("Version " + crate_version + " already exists on crates.io") -else: +elif response.status_code == 404: print("Could not find version " + crate_version + " on crates.io; creating a new release") @@ -33,6 +41,26 @@ "-F", "draft=false", "-F", "prerelease=false", "-F", "generate_release_notes=false", ] print(" Running `" + ' '.join(command) + '`') - subprocess.run(command, check=True) + result = subprocess.run(command, capture_output=True, text=True) + sys.stdout.write(result.stdout) + sys.stderr.write(result.stderr) + if result.returncode != 0: + # A release for this tag may already exist (e.g. the version was + # published on crates.io but the crates.io check briefly failed, or + # this workflow is being re-run). Treat that as a no-op instead of a + # hard failure; fail loudly on any other error. + if "already_exists" in result.stdout or "already_exists" in result.stderr: + print(f" Release {tag_name} already exists; nothing to do") + else: + raise SystemExit(result.returncode) + else: + print(" Done") - print(" Done") +else: + # Any other response (rate limiting, data-access policy rejection, 5xx, + # etc.) is ambiguous: we can't tell whether the version is published. + # Fail loudly rather than guessing and attempting to re-create a release. + raise SystemExit( + f"Unexpected response from crates.io (HTTP {response.status_code}): " + f"{response.text}" + )