#!/usr/bin/env rust-script
use std::env;
use std::fs;
use std::io::Write;
use std::process::{Command, exit};
#[path = "rust-paths.rs"]
mod rust_paths;
fn get_arg(name: &str) -> Option<String> {
let args: Vec<String> = env::args().collect();
let flag = format!("--{}", name);
if let Some(idx) = args.iter().position(|a| a == &flag) {
return args.get(idx + 1).cloned();
}
None
}
fn set_output(key: &str, value: &str) {
if let Ok(output_file) = env::var("GITHUB_OUTPUT") {
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(&output_file) {
let _ = writeln!(file, "{}={}", key, value);
}
}
println!("Output: {}={}", key, value);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FailureKind {
AlreadyExists,
AuthFailed,
RateLimited,
Unknown,
}
impl FailureKind {
fn output_value(self) -> &'static str {
match self {
FailureKind::AlreadyExists => "already_exists",
FailureKind::AuthFailed => "auth_failed",
FailureKind::RateLimited => "rate_limited",
FailureKind::Unknown => "failed",
}
}
fn is_deferred(self) -> bool {
matches!(self, FailureKind::RateLimited)
}
}
fn classify_failure(combined: &str) -> FailureKind {
if combined.contains("already uploaded") || combined.contains("already exists") {
FailureKind::AlreadyExists
} else if combined.contains("429 Too Many Requests")
|| combined.contains("Too Many Requests")
|| combined.contains("too many versions")
|| combined.contains("too many requests")
{
FailureKind::RateLimited
} else if combined.contains("non-empty token")
|| combined.contains("please provide a")
|| combined.contains("unauthorized")
|| combined.contains("authentication")
{
FailureKind::AuthFailed
} else {
FailureKind::Unknown
}
}
fn main() {
let rust_root = match rust_paths::get_rust_root(None, true) {
Ok(root) => root,
Err(e) => {
eprintln!("Error: {}", e);
exit(1);
}
};
let cargo_toml = rust_paths::get_cargo_toml_path(&rust_root);
let package_manifest = match rust_paths::get_package_manifest_path(&cargo_toml) {
Ok(path) => path,
Err(e) => {
eprintln!("Error: {}", e);
exit(1);
}
};
let token = get_arg("token")
.or_else(|| env::var("CARGO_REGISTRY_TOKEN").ok().filter(|s| !s.is_empty()))
.or_else(|| env::var("CARGO_TOKEN").ok().filter(|s| !s.is_empty()));
let package_info = match rust_paths::read_package_info(&package_manifest) {
Ok(info) => info,
Err(e) => {
eprintln!("Error: {}", e);
exit(1);
}
};
let name = package_info.name;
let version = package_info.version;
println!("Package: {}@{}", name, version);
if name == "example-sum-package-name" {
println!("Skipping publish: package name is the template default 'example-sum-package-name'");
println!("Rename the package in Cargo.toml before publishing to crates.io");
set_output("publish_result", "skipped");
return;
}
println!();
println!("=== Attempting to publish to crates.io ===");
if token.is_none() {
println!("::warning::Neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN is set, attempting publish without explicit token");
println!();
println!("To fix this, ensure one of the following secrets is configured:");
println!(" - CARGO_REGISTRY_TOKEN (Cargo's native env var, preferred)");
println!(" - CARGO_TOKEN (alternative for backwards compatibility)");
println!();
println!("For organization secrets, you may need to map the secret name in your workflow:");
println!(" env:");
println!(" CARGO_REGISTRY_TOKEN: ${{{{ secrets.CARGO_TOKEN }}}}");
println!();
} else {
println!("Using provided authentication token");
}
let mut cmd = Command::new("cargo");
cmd.arg("publish")
.arg("--allow-dirty")
.arg("--manifest-path")
.arg(&package_manifest);
if let Some(t) = &token {
cmd.arg("--token").arg(t);
}
let output = cmd.output().expect("Failed to execute cargo publish");
if output.status.success() {
println!("Successfully published {}@{} to crates.io", name, version);
set_output("publish_result", "success");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}\n{}", stdout, stderr);
let kind = classify_failure(&combined);
match kind {
FailureKind::AlreadyExists => {
eprintln!();
eprintln!("=== VERSION ALREADY PUBLISHED ===");
eprintln!();
eprintln!("Version {} already exists on crates.io.", version);
eprintln!("The release pipeline must always publish a version greater than what is already published.");
eprintln!("This indicates a bug in version bumping: the pipeline should have computed a new, unpublished version.");
eprintln!();
}
FailureKind::RateLimited => {
eprintln!();
eprintln!("=== CRATES.IO RATE LIMIT (HTTP 429) ===");
eprintln!();
eprintln!("crates.io rejected the publish because too many versions of this");
eprintln!("crate have been published in the last 24 hours.");
eprintln!();
eprintln!("Original cargo publish error:");
eprintln!("{}", combined.trim());
eprintln!();
eprintln!("This is a TRANSIENT, automatically-recoverable throttle, not a pipeline bug.");
eprintln!("No action is required other than waiting for the 24-hour window to roll over.");
eprintln!("scripts/check-release-needed.rs will re-attempt the same version on the next");
eprintln!("push to 'main' once the throttle window has cleared.");
eprintln!();
eprintln!("See: https://doc.rust-lang.org/cargo/reference/publishing.html#publishing-a-new-version-of-an-existing-crate");
eprintln!();
}
FailureKind::AuthFailed => {
eprintln!();
eprintln!("=== AUTHENTICATION FAILURE ===");
eprintln!();
eprintln!("Failed to publish due to missing or invalid authentication token.");
eprintln!();
eprintln!("SOLUTION: Configure one of these secrets in your repository or organization:");
eprintln!(" 1. CARGO_REGISTRY_TOKEN - Cargo's native environment variable (preferred)");
eprintln!(" 2. CARGO_TOKEN - Alternative name for backwards compatibility");
eprintln!();
eprintln!("If using organization secrets with a different name, map it in your workflow:");
eprintln!(" - name: Publish to Crates.io");
eprintln!(" env:");
eprintln!(" CARGO_REGISTRY_TOKEN: ${{{{ secrets.YOUR_SECRET_NAME }}}}");
eprintln!();
eprintln!("See: https://doc.rust-lang.org/cargo/reference/publishing.html");
eprintln!();
}
FailureKind::Unknown => {
eprintln!("Failed to publish for unknown reason");
eprintln!("{}", combined);
}
}
set_output("publish_result", kind.output_value());
if kind.is_deferred() {
return;
}
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_already_exists_response() {
let body = "error: crate version 0.1.0 is already uploaded";
assert_eq!(classify_failure(body), FailureKind::AlreadyExists);
assert_eq!(FailureKind::AlreadyExists.output_value(), "already_exists");
}
#[test]
fn classifies_rate_limit_response() {
let body = "\
error: failed to publish my-crate v0.42.0 to registry at https://crates.io
Caused by:
the remote server responded with an error (status 429 Too Many Requests): \
You have published too many versions of this crate in the last 24 hours
";
assert_eq!(classify_failure(body), FailureKind::RateLimited);
assert_eq!(FailureKind::RateLimited.output_value(), "rate_limited");
}
#[test]
fn classifies_rate_limit_from_too_many_versions_marker() {
let body = "the remote server responded: You have published too many versions";
assert_eq!(classify_failure(body), FailureKind::RateLimited);
}
#[test]
fn classifies_auth_failure_response() {
let body = "error: failed to publish: please provide a non-empty token";
assert_eq!(classify_failure(body), FailureKind::AuthFailed);
assert_eq!(FailureKind::AuthFailed.output_value(), "auth_failed");
}
#[test]
fn classifies_unknown_response() {
let body = "error: some brand new failure mode nobody has seen before";
assert_eq!(classify_failure(body), FailureKind::Unknown);
assert_eq!(FailureKind::Unknown.output_value(), "failed");
}
#[test]
fn rate_limit_takes_precedence_over_auth_markers() {
let body = "status 429 Too Many Requests: authentication retry later";
assert_eq!(classify_failure(body), FailureKind::RateLimited);
}
#[test]
fn only_rate_limit_is_deferred() {
assert!(FailureKind::RateLimited.is_deferred());
assert!(!FailureKind::AlreadyExists.is_deferred());
assert!(!FailureKind::AuthFailed.is_deferred());
assert!(!FailureKind::Unknown.is_deferred());
}
}