sr_core/publishers/
docker.rs1use super::{PublishCtx, PublishState, Publisher};
14use crate::error::ReleaseError;
15use crate::hooks::run_shell;
16
17pub struct DockerPublisher {
18 pub image: String,
19 pub platforms: Vec<String>,
20 pub dockerfile: Option<String>,
21}
22
23impl Publisher for DockerPublisher {
24 fn name(&self) -> &'static str {
25 "docker"
26 }
27
28 fn check(&self, ctx: &PublishCtx<'_>) -> Result<PublishState, ReleaseError> {
29 let (registry, repository) = split_image_ref(&self.image);
30 let manifest_url = format!(
31 "https://{registry}/v2/{repository}/manifests/{}",
32 ctx.version
33 );
34
35 let resp = ureq::head(&manifest_url)
37 .header(
38 "Accept",
39 "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json",
40 )
41 .header("User-Agent", "sr (+https://github.com/urmzd/sr)")
42 .call();
43
44 match resp {
45 Ok(r) if r.status() == 200 => Ok(PublishState::Completed),
46 Ok(r) if r.status() == 404 => Ok(PublishState::Needed),
47 Err(ureq::Error::StatusCode(404)) => Ok(PublishState::Needed),
48 Err(ureq::Error::StatusCode(401)) | Ok(_) => Ok(PublishState::Unknown(format!(
49 "docker registry check inconclusive for {}:{}",
50 self.image, ctx.version
51 ))),
52 Err(e) => Ok(PublishState::Unknown(format!("docker check failed: {e}"))),
53 }
54 }
55
56 fn run(&self, ctx: &PublishCtx<'_>) -> Result<(), ReleaseError> {
57 let mut cmd = String::from("docker buildx build --push");
58 if !self.platforms.is_empty() {
59 cmd.push_str(" --platform ");
60 cmd.push_str(&shell_word(&self.platforms.join(",")));
61 }
62 if let Some(dockerfile) = &self.dockerfile {
63 cmd.push_str(" -f ");
64 cmd.push_str(&shell_word(dockerfile));
65 }
66 cmd.push_str(" -t ");
67 cmd.push_str(&shell_word(&format!("{}:{}", self.image, ctx.version)));
68 if !ctx.version.contains('-') {
70 cmd.push_str(" -t ");
71 cmd.push_str(&shell_word(&format!("{}:latest", self.image)));
72 }
73 cmd.push_str(" .");
74
75 if ctx.dry_run {
76 eprintln!("[dry-run] docker ({}): {cmd}", ctx.package.path);
77 return Ok(());
78 }
79
80 eprintln!("docker ({}): {cmd}", ctx.package.path);
81 let wrapped = format!("cd {} && {cmd}", shell_word(&ctx.package.path));
82 run_shell(&wrapped, None, ctx.env)
83 }
84}
85
86fn split_image_ref(image: &str) -> (String, String) {
89 if let Some((head, tail)) = image.split_once('/') {
91 let has_port = head.contains(':');
92 let has_dot = head.contains('.');
93 if has_port || has_dot || head == "localhost" {
94 return (head.to_string(), tail.to_string());
95 }
96 }
97 let repo = if image.contains('/') {
99 image.to_string()
100 } else {
101 format!("library/{image}")
102 };
103 ("registry-1.docker.io".to_string(), repo)
104}
105
106fn shell_word(s: &str) -> String {
107 let mut out = String::from("'");
108 for ch in s.chars() {
109 if ch == '\'' {
110 out.push_str("'\\''");
111 } else {
112 out.push(ch);
113 }
114 }
115 out.push('\'');
116 out
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn split_ghcr_image() {
125 let (r, repo) = split_image_ref("ghcr.io/owner/repo");
126 assert_eq!(r, "ghcr.io");
127 assert_eq!(repo, "owner/repo");
128 }
129
130 #[test]
131 fn split_docker_hub_multi_segment() {
132 let (r, repo) = split_image_ref("urmzd/sr");
133 assert_eq!(r, "registry-1.docker.io");
134 assert_eq!(repo, "urmzd/sr");
135 }
136
137 #[test]
138 fn split_docker_hub_single_segment() {
139 let (r, repo) = split_image_ref("nginx");
140 assert_eq!(r, "registry-1.docker.io");
141 assert_eq!(repo, "library/nginx");
142 }
143
144 #[test]
145 fn split_localhost_port() {
146 let (r, repo) = split_image_ref("localhost:5000/img");
147 assert_eq!(r, "localhost:5000");
148 assert_eq!(repo, "img");
149 }
150}