//! The tag publish step publishes onto the existing tag with BlueGit's //! create-only route POST /repos/{owner}/{repo}/releases/{tag}/assets: it //! must never call the tag-creating POST .../releases and never send replace. #![cfg(unix)] use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::process::Command; fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } fn path_prefix_env(prefix: &std::path::Path) -> std::ffi::OsString { let mut path = std::ffi::OsString::from(prefix); path.push(":"); path.push(std::env::var_os("PATH").unwrap_or_default()); path } fn install_curl_trap(dir: &std::path::Path) -> PathBuf { let curl = dir.join("curl"); fs::write( &curl, "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${CURL_TRAP_LOG:?}\"\nprintf 201\nexit 0\n", ) .unwrap(); let mut permissions = fs::metadata(&curl).unwrap().permissions(); permissions.set_mode(0o755); fs::set_permissions(&curl, permissions).unwrap(); curl } fn executable_lines(script: &str) -> String { script .lines() .map(str::trim) .filter(|line| !line.is_empty() && !line.starts_with('#')) .collect::>() .join("\n") } #[test] fn tag_publish_uses_only_the_existing_tag_route_without_replace() { let dir = std::env::temp_dir().join(format!("paperdoll-release-trap-{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); let log = dir.join("curl.log"); install_curl_trap(&dir); let output = Command::new("bash") .arg(repo_root().join("scripts/ci/release.sh")) .env("PATH", path_prefix_env(&dir)) .env("CURL_TRAP_LOG", &log) .env("CI_API_URL", "https://git.example.invalid/api/v1") .env("CI_JOB_TOKEN", "bgj_test_token_not_secret") .env("CI_COMMIT_SHA", "0123456789abcdef0123456789abcdef01234567") .env("CI_PROJECT_PATH", "golden-age/paperdoll-renderer") .env("CI_COMMIT_TAG", "v0.1.0") .env_remove("PACKAGE_VERSION") .output() .unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); assert_eq!( output.status.code(), Some(0), "stdout:\n{stdout}\nstderr:\n{stderr}" ); let calls = fs::read_to_string(&log).unwrap(); assert_eq!( calls.lines().filter(|l| l.contains("http")).count(), 1, "{calls}" ); assert!( calls.contains( "https://git.example.invalid/api/v1/repos/golden-age/paperdoll-renderer/releases/v0.1.0/assets" ), "{calls}" ); assert!(!calls.contains("replace"), "{calls}"); // BlueGit requires the full tag commit (pipeline #582 failed without it). assert!( calls.contains("\"commit\":\"0123456789abcdef0123456789abcdef01234567\""), "{calls}" ); assert!(!calls.contains("\"tag\""), "{calls}"); for name in [ "paperdoll-renderer-v0.1.0-x86_64.AppImage", "paperdoll-renderer-v0.1.0-amd64.deb", "paperdoll-renderer-v0.1.0-x86_64.pkg.tar.zst", "paperdoll-renderer-v0.1.0-windows-x86_64.zip", "paperdoll-renderer-v0.1.0-SHA256SUMS.txt", ] { assert!(calls.contains(name), "{name} missing: {calls}"); } let script = fs::read_to_string(repo_root().join("scripts/ci/release.sh")).unwrap(); let executable = executable_lines(&script); assert!( !executable.contains("replace"), "executable release script must not send replace:\n{executable}" ); let _ = fs::remove_dir_all(&dir); } #[test] fn tag_publish_rejects_a_non_semver_tag_before_the_api() { let dir = std::env::temp_dir().join(format!("paperdoll-release-trap-bad-{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); let log = dir.join("curl.log"); install_curl_trap(&dir); let output = Command::new("bash") .arg(repo_root().join("scripts/ci/release.sh")) .env("PATH", path_prefix_env(&dir)) .env("CURL_TRAP_LOG", &log) .env("CI_API_URL", "https://git.example.invalid/api/v1") .env("CI_JOB_TOKEN", "bgj_test_token_not_secret") .env("CI_COMMIT_SHA", "0123456789abcdef0123456789abcdef01234567") .env("CI_PROJECT_PATH", "golden-age/paperdoll-renderer") .env("CI_COMMIT_TAG", "not-a-version") .env_remove("PACKAGE_VERSION") .output() .unwrap(); assert_ne!(output.status.code(), Some(0)); assert_ne!(output.status.code(), Some(3)); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("package version must be X.Y.Z"), "{stderr}"); assert!(!log.exists(), "curl was invoked for an invalid tag"); let _ = fs::remove_dir_all(&dir); }