1use crate::output::VersionVariables;
9use std::env;
10
11fn escape_value(v: &str) -> String {
13 v.replace('|', "||")
14 .replace('\'', "|'")
15 .replace('[', "|[")
16 .replace(']', "|]")
17 .replace('\r', "|r")
18 .replace('\n', "|n")
19}
20
21pub trait BuildAgent {
23 fn name(&self) -> &'static str;
25
26 fn set_build_number(&self, vars: &VersionVariables) -> String {
28 vars.full_sem_ver.clone()
29 }
30
31 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String>;
33
34 fn write_integration(&self, vars: &VersionVariables, update_build_number: bool) -> Vec<String> {
36 base_integration(self, vars, update_build_number)
37 }
38}
39
40fn base_integration(
42 agent: &(impl BuildAgent + ?Sized),
43 vars: &VersionVariables,
44 update_build_number: bool,
45) -> Vec<String> {
46 let mut out = Vec::new();
47 if update_build_number {
48 out.push(format!("Set Build Number for '{}'.", agent.name()));
49 let bn = agent.set_build_number(vars);
52 if !bn.is_empty() {
53 out.push(bn);
54 }
55 }
56 out.push(format!("Set Output Variables for '{}'.", agent.name()));
57 for (key, value) in vars.to_map() {
58 out.extend(agent.set_output_variable(&key, &value));
59 }
60 out
61}
62
63struct TeamCity;
67impl BuildAgent for TeamCity {
68 fn name(&self) -> &'static str {
69 "TeamCity"
70 }
71 fn set_build_number(&self, vars: &VersionVariables) -> String {
72 format!(
73 "##teamcity[buildNumber '{}']",
74 escape_value(&vars.full_sem_ver)
75 )
76 }
77 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
78 let e = escape_value(value);
79 vec![
80 format!("##teamcity[setParameter name='GitVersion.{name}' value='{e}']"),
81 format!("##teamcity[setParameter name='system.GitVersion.{name}' value='{e}']"),
82 ]
83 }
84}
85
86struct MyGet;
88impl BuildAgent for MyGet {
89 fn name(&self) -> &'static str {
90 "MyGet"
91 }
92 fn set_build_number(&self, vars: &VersionVariables) -> String {
93 format!(
94 "##myget[buildNumber '{}']",
95 escape_value(&vars.full_sem_ver)
96 )
97 }
98 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
99 vec![format!(
100 "##myget[setParameter name='GitVersion.{name}' value='{}']",
101 escape_value(value)
102 )]
103 }
104}
105
106struct AzurePipelines;
108impl BuildAgent for AzurePipelines {
109 fn name(&self) -> &'static str {
110 "AzurePipelines"
111 }
112 fn set_build_number(&self, vars: &VersionVariables) -> String {
113 match env::var("BUILD_BUILDNUMBER") {
115 Ok(bn) if !bn.trim().is_empty() => {
116 let replaced = replace_azure_vars(&bn, vars);
117 if replaced != bn {
118 format!("##vso[build.updatebuildnumber]{replaced}")
119 } else {
120 let v = vars
121 .full_sem_ver
122 .strip_suffix("+0")
123 .unwrap_or(&vars.full_sem_ver);
124 format!("##vso[build.updatebuildnumber]{v}")
125 }
126 }
127 _ => vars.full_sem_ver.clone(),
128 }
129 }
130 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
131 vec![
132 format!("##vso[task.setvariable variable=GitVersion.{name}]{value}"),
133 format!("##vso[task.setvariable variable=GitVersion.{name};isOutput=true]{value}"),
134 ]
135 }
136}
137
138fn replace_azure_vars(build_number: &str, vars: &VersionVariables) -> String {
139 let mut out = build_number.to_string();
140 for (key, value) in vars.to_map() {
141 out = out.replace(&format!("$(GITVERSION_{key})"), &value);
142 out = out.replace(&format!("$(GITVERSION.{key})"), &value);
143 }
144 out
145}
146
147struct ContinuaCi;
149impl BuildAgent for ContinuaCi {
150 fn name(&self) -> &'static str {
151 "ContinuaCi"
152 }
153 fn set_build_number(&self, vars: &VersionVariables) -> String {
154 format!("@@continua[setBuildVersion value='{}']", vars.full_sem_ver)
155 }
156 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
157 vec![format!(
158 "@@continua[setVariable name='GitVersion_{name}' value='{value}' skipIfNotDefined='true']"
159 )]
160 }
161}
162
163struct EnvRun;
165impl BuildAgent for EnvRun {
166 fn name(&self) -> &'static str {
167 "EnvRun"
168 }
169 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
170 vec![format!(
171 "@@envrun[set name='GitVersion_{name}' value='{value}']"
172 )]
173 }
174}
175
176fn key_value_line(name: &str, value: &str) -> Vec<String> {
178 vec![format!("GitVersion_{name}={value}")]
179}
180
181struct TravisCi;
182impl BuildAgent for TravisCi {
183 fn name(&self) -> &'static str {
184 "TravisCi"
185 }
186 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
187 key_value_line(name, value)
188 }
189}
190
191struct Drone;
192impl BuildAgent for Drone {
193 fn name(&self) -> &'static str {
194 "Drone"
195 }
196 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
197 key_value_line(name, value)
198 }
199}
200
201fn write_properties_file(vars: &VersionVariables) {
203 let lines: Vec<String> = vars
204 .to_map()
205 .iter()
206 .map(|(k, v)| format!("GitVersion_{k}={v}"))
207 .collect();
208 let _ = std::fs::write("gitversion.properties", lines.join("\n") + "\n");
209}
210
211struct GitLabCi;
212impl BuildAgent for GitLabCi {
213 fn name(&self) -> &'static str {
214 "GitLabCi"
215 }
216 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
217 key_value_line(name, value)
218 }
219 fn write_integration(&self, vars: &VersionVariables, ubn: bool) -> Vec<String> {
220 let mut out = base_integration(self, vars, ubn);
221 out.push("Outputting variables to 'gitversion.properties' ... ".into());
222 write_properties_file(vars);
223 out
224 }
225}
226
227struct Jenkins;
228impl BuildAgent for Jenkins {
229 fn name(&self) -> &'static str {
230 "Jenkins"
231 }
232 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
233 key_value_line(name, value)
234 }
235 fn write_integration(&self, vars: &VersionVariables, ubn: bool) -> Vec<String> {
236 let mut out = base_integration(self, vars, ubn);
237 write_properties_file(vars);
238 out.push("Outputting variables to 'gitversion.properties' ... ".into());
239 out
240 }
241}
242
243struct CodeBuild;
244impl BuildAgent for CodeBuild {
245 fn name(&self) -> &'static str {
246 "CodeBuild"
247 }
248 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
249 key_value_line(name, value)
250 }
251 fn write_integration(&self, vars: &VersionVariables, ubn: bool) -> Vec<String> {
252 let mut out = base_integration(self, vars, ubn);
253 write_properties_file(vars);
254 out.push("Outputting variables to 'gitversion.properties' ... ".into());
255 out
256 }
257}
258
259struct BitBucketPipelines;
261impl BuildAgent for BitBucketPipelines {
262 fn name(&self) -> &'static str {
263 "BitBucketPipelines"
264 }
265 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
266 vec![format!("GITVERSION_{}={value}", name.to_uppercase())]
267 }
268 fn write_integration(&self, vars: &VersionVariables, ubn: bool) -> Vec<String> {
269 let mut out = base_integration(self, vars, ubn);
270 let pf = "gitversion.properties";
271 let ps1 = "gitversion.ps1";
272 let exports: Vec<String> = vars
273 .to_map()
274 .iter()
275 .map(|(k, v)| format!("export GITVERSION_{}={v}", k.to_uppercase()))
276 .collect();
277 let _ = std::fs::write(pf, exports.join("\n") + "\n");
278 out.push(format!("Outputting variables to '{pf}' for Bash,"));
280 out.push(format!("and to '{ps1}' for Powershell ... "));
281 out.push(
282 "To import the file into your build environment, add the following line to your build step:"
283 .into(),
284 );
285 out.push("Bash:".into());
286 out.push(format!(" - source {pf}"));
287 out.push("Powershell:".into());
288 out.push(format!(" - . .\\{ps1}"));
289 out.push(String::new());
290 out.push("To reuse the file across build steps, add the file as a build artifact:".into());
291 out.push("Bash:".into());
292 out.push(" artifacts:".into());
293 out.push(format!(" - {pf}"));
294 out.push("Powershell:".into());
295 out.push(" artifacts:".into());
296 out.push(format!(" - {ps1}"));
297 out
298 }
299}
300
301struct GitHubActions;
303impl BuildAgent for GitHubActions {
304 fn name(&self) -> &'static str {
305 "GitHubActions"
306 }
307 fn set_build_number(&self, _vars: &VersionVariables) -> String {
308 String::new()
309 }
310 fn set_output_variable(&self, _name: &str, _value: &str) -> Vec<String> {
311 Vec::new()
312 }
313 fn write_integration(&self, vars: &VersionVariables, ubn: bool) -> Vec<String> {
314 let mut out = base_integration(self, vars, ubn);
315 match env::var("GITHUB_ENV") {
316 Ok(path) => {
317 out.push(format!("Writing version variables to $GITHUB_ENV file for '{}'.", self.name()));
318 let lines: Vec<String> = vars
319 .to_map()
320 .iter()
321 .filter(|(_, v)| !v.is_empty())
322 .map(|(k, v)| format!("GitVersion_{k}={v}"))
323 .collect();
324 use std::io::Write;
325 if let Ok(mut f) =
326 std::fs::OpenOptions::new().create(true).append(true).open(&path)
327 {
328 let _ = writeln!(f, "{}", lines.join("\n"));
329 }
330 }
331 Err(_) => out.push(
332 "Unable to write GitVersion variables to $GITHUB_ENV because the environment variable is not set."
333 .into(),
334 ),
335 }
336 out
337 }
338}
339
340struct BuildKite;
342impl BuildAgent for BuildKite {
343 fn name(&self) -> &'static str {
344 "BuildKite"
345 }
346 fn set_build_number(&self, _vars: &VersionVariables) -> String {
347 String::new()
348 }
349 fn set_output_variable(&self, _name: &str, _value: &str) -> Vec<String> {
350 Vec::new()
351 }
352}
353
354struct SpaceAutomation;
355impl BuildAgent for SpaceAutomation {
356 fn name(&self) -> &'static str {
357 "SpaceAutomation"
358 }
359 fn set_build_number(&self, _vars: &VersionVariables) -> String {
360 String::new()
361 }
362 fn set_output_variable(&self, _name: &str, _value: &str) -> Vec<String> {
363 Vec::new()
364 }
365}
366
367struct AppVeyor;
369impl BuildAgent for AppVeyor {
370 fn name(&self) -> &'static str {
371 "AppVeyor"
372 }
373 fn set_build_number(&self, vars: &VersionVariables) -> String {
374 format!("Set AppVeyor build number to '{}'.", vars.full_sem_ver)
375 }
376 fn set_output_variable(&self, name: &str, value: &str) -> Vec<String> {
377 vec![format!(
378 "Adding Environment Variable. name='GitVersion_{name}' value='{value}']"
379 )]
380 }
381}
382
383impl AppVeyor {
384 #[cfg(test)]
389 fn build_number_body(vars: &VersionVariables, build_number: &str) -> String {
390 format!(
391 r#"{{"version":"{}.build.{}"}}"#,
392 vars.full_sem_ver, build_number
393 )
394 }
395
396 #[cfg(test)]
397 fn output_variable_body(name: &str, value: &str) -> String {
398 format!(r#"{{"name":"GitVersion_{name}","value":"{value}"}}"#)
399 }
400}
401
402pub fn by_name(name: &str) -> Option<Box<dyn BuildAgent>> {
404 let agent: Box<dyn BuildAgent> = match name {
405 "TeamCity" => Box::new(TeamCity),
406 "MyGet" => Box::new(MyGet),
407 "AzurePipelines" => Box::new(AzurePipelines),
408 "ContinuaCi" => Box::new(ContinuaCi),
409 "EnvRun" => Box::new(EnvRun),
410 "TravisCI" | "TravisCi" => Box::new(TravisCi),
411 "Drone" => Box::new(Drone),
412 "GitLabCi" => Box::new(GitLabCi),
413 "Jenkins" => Box::new(Jenkins),
414 "CodeBuild" => Box::new(CodeBuild),
415 "BitBucketPipelines" => Box::new(BitBucketPipelines),
416 "GitHubActions" => Box::new(GitHubActions),
417 "BuildKite" => Box::new(BuildKite),
418 "SpaceAutomation" => Box::new(SpaceAutomation),
419 "AppVeyor" => Box::new(AppVeyor),
420 _ => return None,
421 };
422 Some(agent)
423}
424
425pub fn detect() -> Option<Box<dyn BuildAgent>> {
427 let has = |k: &str| env::var(k).map(|v| !v.is_empty()).unwrap_or(false);
428
429 if has("TEAMCITY_VERSION") {
430 Some(Box::new(TeamCity))
431 } else if has("TF_BUILD") {
432 Some(Box::new(AzurePipelines))
433 } else if has("GITHUB_ACTIONS") {
434 Some(Box::new(GitHubActions))
435 } else if has("GITLAB_CI") {
436 Some(Box::new(GitLabCi))
437 } else if has("JENKINS_URL") {
438 Some(Box::new(Jenkins))
439 } else if has("CODEBUILD_WEBHOOK_HEAD_REF") {
440 Some(Box::new(CodeBuild))
441 } else if has("TRAVIS") {
442 Some(Box::new(TravisCi))
443 } else if has("DRONE") {
444 Some(Box::new(Drone))
445 } else if has("APPVEYOR") {
446 Some(Box::new(AppVeyor))
447 } else if has("ENVRUN_DATABASE") {
448 Some(Box::new(EnvRun))
449 } else if has("ContinuaCI.Version") {
450 Some(Box::new(ContinuaCi))
451 } else if has("BITBUCKET_WORKSPACE") {
452 Some(Box::new(BitBucketPipelines))
453 } else if has("BUILDKITE") {
454 Some(Box::new(BuildKite))
455 } else if has("JB_SPACE_PROJECT_KEY") {
456 Some(Box::new(SpaceAutomation))
457 } else if env::var("BuildRunner")
458 .map(|v| v.eq_ignore_ascii_case("MyGet"))
459 .unwrap_or(false)
460 {
461 Some(Box::new(MyGet))
462 } else {
463 None
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 fn sample() -> VersionVariables {
472 VersionVariables {
473 full_sem_ver: "1.0.1-1".into(),
474 ..Default::default()
475 }
476 }
477
478 #[test]
479 fn appveyor_http_body_matches_dotnet() {
480 let vars = VersionVariables {
482 full_sem_ver: "1.2.3-beta.1".into(),
483 ..Default::default()
484 };
485 assert_eq!(
486 AppVeyor::build_number_body(&vars, "42"),
487 r#"{"version":"1.2.3-beta.1.build.42"}"#
488 );
489 assert_eq!(
490 AppVeyor::output_variable_body("Major", "1"),
491 r#"{"name":"GitVersion_Major","value":"1"}"#
492 );
493 }
494
495 #[test]
496 fn teamcity_format() {
497 let a = TeamCity;
498 assert_eq!(
499 a.set_build_number(&sample()),
500 "##teamcity[buildNumber '1.0.1-1']"
501 );
502 assert_eq!(
503 a.set_output_variable("FullSemVer", "1.0.1-1"),
504 vec![
505 "##teamcity[setParameter name='GitVersion.FullSemVer' value='1.0.1-1']",
506 "##teamcity[setParameter name='system.GitVersion.FullSemVer' value='1.0.1-1']",
507 ]
508 );
509 }
510
511 #[test]
512 fn teamcity_escapes_special_chars() {
513 let a = TeamCity;
514 assert_eq!(
515 a.set_output_variable("X", "a'b[c]"),
516 vec![
517 "##teamcity[setParameter name='GitVersion.X' value='a|'b|[c|]']",
518 "##teamcity[setParameter name='system.GitVersion.X' value='a|'b|[c|]']",
519 ]
520 );
521 }
522
523 #[test]
524 fn azure_format() {
525 let a = AzurePipelines;
526 assert_eq!(
527 a.set_output_variable("Major", "1"),
528 vec![
529 "##vso[task.setvariable variable=GitVersion.Major]1",
530 "##vso[task.setvariable variable=GitVersion.Major;isOutput=true]1",
531 ]
532 );
533 }
534
535 #[test]
536 fn key_value_agents() {
537 assert_eq!(
538 GitLabCi.set_output_variable("Sha", "abc"),
539 vec!["GitVersion_Sha=abc"]
540 );
541 assert_eq!(
542 TravisCi.set_output_variable("Sha", "abc"),
543 vec!["GitVersion_Sha=abc"]
544 );
545 assert_eq!(
546 BitBucketPipelines.set_output_variable("FullSemVer", "1.0.1-1"),
547 vec!["GITVERSION_FULLSEMVER=1.0.1-1"]
548 );
549 }
550
551 #[test]
552 fn integration_skips_build_number_when_disabled() {
553 let out = TeamCity.write_integration(&sample(), false);
554 assert!(out.iter().all(|l| !l.contains("buildNumber")));
555 assert!(out
556 .iter()
557 .any(|l| l.starts_with("Set Output Variables for 'TeamCity'.")));
558 }
559
560 #[test]
561 fn integration_includes_build_number_when_enabled() {
562 let out = TeamCity.write_integration(&sample(), true);
563 assert!(out.iter().any(|l| l == "##teamcity[buildNumber '1.0.1-1']"));
564 }
565}