Skip to content

Commit a0ad286

Browse files
refi64sjoerdsimons
authored andcommitted
Isolate GitLab-specific command handling
All the generic command handling is now part of an `actions` module, leaving only GitLab-specific functionality in `handler`.
1 parent e7c8ee2 commit a0ad286

File tree

3 files changed

+463
-415
lines changed

3 files changed

+463
-415
lines changed

src/actions.rs

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
use std::{collections::HashMap, io::SeekFrom};
2+
3+
use camino::{Utf8Path, Utf8PathBuf};
4+
use clap::{ArgAction, Parser};
5+
use color_eyre::eyre::{Context, Report, Result};
6+
use open_build_service_api as obs;
7+
use serde::{Deserialize, Serialize};
8+
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
9+
use tracing::{debug, instrument};
10+
11+
use crate::{
12+
artifacts::{ArtifactDirectory, ArtifactReader, ArtifactWriter, MissingArtifactToNone},
13+
binaries::download_binaries,
14+
build_meta::{
15+
BuildHistoryRetrieval, BuildMeta, BuildMetaOptions, CommitBuildInfo, DisabledRepos,
16+
RepoArch,
17+
},
18+
monitor::{MonitoredPackage, ObsMonitor, PackageCompletion, PackageMonitoringOptions},
19+
outputln,
20+
prune::prune_branch,
21+
retry_request,
22+
upload::ObsDscUploader,
23+
};
24+
25+
pub const DEFAULT_BUILD_INFO: &str = "build-info.yml";
26+
pub const DEFAULT_BUILD_LOG: &str = "build.log";
27+
28+
// Our flags can all take explicit values, because it makes it easier to
29+
// conditionally set things in the pipelines.
30+
pub trait FlagSupportingExplicitValue {
31+
fn flag_supporting_explicit_value(self) -> Self;
32+
}
33+
34+
impl FlagSupportingExplicitValue for clap::Arg {
35+
fn flag_supporting_explicit_value(self) -> Self {
36+
self.num_args(0..=1)
37+
.require_equals(true)
38+
.required(false)
39+
.default_value("false")
40+
.default_missing_value("true")
41+
.action(ArgAction::Set)
42+
}
43+
}
44+
45+
#[derive(Parser, Debug)]
46+
pub struct DputAction {
47+
pub project: String,
48+
pub dsc: String,
49+
#[clap(long, default_value = "")]
50+
pub branch_to: String,
51+
#[clap(long, default_value_t = DEFAULT_BUILD_INFO.to_owned().into())]
52+
pub build_info_out: Utf8PathBuf,
53+
#[clap(long, flag_supporting_explicit_value())]
54+
pub rebuild_if_unchanged: bool,
55+
}
56+
57+
#[derive(Parser, Debug)]
58+
pub struct MonitorAction {
59+
#[clap(long)]
60+
pub project: String,
61+
#[clap(long)]
62+
pub package: String,
63+
#[clap(long)]
64+
pub rev: String,
65+
#[clap(long)]
66+
pub srcmd5: String,
67+
#[clap(long)]
68+
pub repository: String,
69+
#[clap(long)]
70+
pub arch: String,
71+
#[clap(long)]
72+
pub prev_endtime_for_commit: Option<u64>,
73+
#[clap(long)]
74+
pub build_log_out: String,
75+
}
76+
77+
#[derive(Parser, Debug)]
78+
pub struct DownloadBinariesAction {
79+
#[clap(long)]
80+
pub project: String,
81+
#[clap(long)]
82+
pub package: String,
83+
#[clap(long)]
84+
pub repository: String,
85+
#[clap(long)]
86+
pub arch: String,
87+
#[clap(long)]
88+
pub build_results_dir: Utf8PathBuf,
89+
}
90+
91+
#[derive(Parser, Debug)]
92+
pub struct PruneAction {
93+
#[clap(long, default_value_t = DEFAULT_BUILD_INFO.to_owned())]
94+
pub build_info: String,
95+
#[clap(long, flag_supporting_explicit_value())]
96+
pub ignore_missing_build_info: bool,
97+
}
98+
99+
#[derive(Clone, Debug, Serialize, Deserialize)]
100+
pub struct ObsBuildInfo {
101+
pub project: String,
102+
pub package: String,
103+
pub rev: Option<String>,
104+
pub srcmd5: Option<String>,
105+
pub is_branched: bool,
106+
pub enabled_repos: HashMap<RepoArch, CommitBuildInfo>,
107+
}
108+
109+
impl ObsBuildInfo {
110+
#[instrument(skip(artifacts))]
111+
async fn save(self, artifacts: &mut impl ArtifactDirectory, path: &Utf8Path) -> Result<()> {
112+
artifacts
113+
.save_with(path, async |file: &mut ArtifactWriter| {
114+
let data =
115+
serde_yaml::to_string(&self).wrap_err("Failed to serialize build info")?;
116+
file.write_all(data.as_bytes())
117+
.await
118+
.wrap_err("Failed to write build info file")?;
119+
Ok::<_, Report>(())
120+
})
121+
.await
122+
}
123+
}
124+
125+
#[derive(Debug, thiserror::Error)]
126+
#[error("Failed build")]
127+
pub struct FailedBuild;
128+
129+
pub const LOG_TAIL_2MB: u64 = 2 * 1024 * 1024;
130+
131+
pub struct Actions {
132+
pub client: obs::Client,
133+
}
134+
135+
impl Actions {
136+
#[instrument(skip_all, fields(args))]
137+
pub async fn dput(
138+
&mut self,
139+
args: DputAction,
140+
artifacts: &mut impl ArtifactDirectory,
141+
) -> Result<()> {
142+
let branch_to = if !args.branch_to.is_empty() {
143+
Some(args.branch_to)
144+
} else {
145+
None
146+
};
147+
let is_branched = branch_to.is_some();
148+
149+
// The upload prep and actual upload are split in two so that we can
150+
// already tell what the project & package name are, so build-info.yaml
151+
// can be written and pruning can take place regardless of the actual
152+
// *upload* success.
153+
let uploader = ObsDscUploader::prepare(
154+
self.client.clone(),
155+
args.project.clone(),
156+
branch_to,
157+
args.dsc.as_str().into(),
158+
artifacts,
159+
)
160+
.await?;
161+
162+
let build_info = ObsBuildInfo {
163+
project: uploader.project().to_owned(),
164+
package: uploader.package().to_owned(),
165+
rev: None,
166+
srcmd5: None,
167+
is_branched,
168+
enabled_repos: HashMap::new(),
169+
};
170+
debug!("Saving initial build info: {:?}", build_info);
171+
build_info
172+
.clone()
173+
.save(artifacts, &args.build_info_out)
174+
.await?;
175+
176+
let initial_build_meta = BuildMeta::get_if_package_exists(
177+
self.client.clone(),
178+
build_info.project.clone(),
179+
build_info.package.clone(),
180+
&BuildMetaOptions {
181+
history_retrieval: BuildHistoryRetrieval::Full,
182+
// Getting disabled repos has to happen *after* the upload,
183+
// since the new version can change the supported architectures.
184+
disabled_repos: DisabledRepos::Keep,
185+
},
186+
)
187+
.await?;
188+
debug!(?initial_build_meta);
189+
190+
let result = uploader.upload_package(artifacts).await?;
191+
192+
// If we couldn't get the metadata before because the package didn't
193+
// exist yet, get it now but without history, so we leave the previous
194+
// endtime empty (if there was no previous package, there were no
195+
// previous builds).
196+
let mut build_meta = if let Some(mut build_meta) = initial_build_meta {
197+
build_meta
198+
.remove_disabled_repos(&Default::default())
199+
.await?;
200+
build_meta
201+
} else {
202+
BuildMeta::get(
203+
self.client.clone(),
204+
build_info.project.clone(),
205+
build_info.package.clone(),
206+
&BuildMetaOptions {
207+
history_retrieval: BuildHistoryRetrieval::None,
208+
disabled_repos: DisabledRepos::Skip {
209+
wait_options: Default::default(),
210+
},
211+
},
212+
)
213+
.await?
214+
};
215+
216+
if result.unchanged {
217+
outputln!("Package unchanged at revision {}.", result.rev);
218+
219+
if args.rebuild_if_unchanged {
220+
retry_request!(
221+
self.client
222+
.project(build_info.project.clone())
223+
.package(build_info.package.clone())
224+
.rebuild()
225+
.await
226+
.wrap_err("Failed to trigger rebuild")
227+
)?;
228+
} else {
229+
// Clear out the history used to track endtime values. This is
230+
// normally important to make sure the monitor doesn't
231+
// accidentally pick up an old build result...but if we didn't
232+
// rebuild anything, picking up the old result is *exactly* the
233+
// behavior we want.
234+
build_meta.clear_stored_history();
235+
}
236+
} else {
237+
outputln!("Package uploaded with revision {}.", result.rev);
238+
}
239+
240+
let enabled_repos = build_meta.get_commit_build_info(&result.build_srcmd5);
241+
let build_info = ObsBuildInfo {
242+
rev: Some(result.rev),
243+
srcmd5: Some(result.build_srcmd5),
244+
enabled_repos,
245+
..build_info
246+
};
247+
debug!("Saving complete build info: {:?}", build_info);
248+
build_info.save(artifacts, &args.build_info_out).await?;
249+
250+
Ok(())
251+
}
252+
253+
#[instrument(skip_all, fields(args))]
254+
pub async fn monitor<F: Future<Output = Result<()>> + Send>(
255+
&mut self,
256+
args: MonitorAction,
257+
monitoring_options: PackageMonitoringOptions,
258+
log_tail_cb: impl FnOnce(ArtifactReader) -> F,
259+
log_tail_bytes: u64,
260+
artifacts: &mut impl ArtifactDirectory,
261+
) -> Result<()> {
262+
let monitor = ObsMonitor::new(
263+
self.client.clone(),
264+
MonitoredPackage {
265+
project: args.project.clone(),
266+
package: args.package.clone(),
267+
repository: args.repository.clone(),
268+
arch: args.arch.clone(),
269+
rev: args.rev.clone(),
270+
srcmd5: args.srcmd5.clone(),
271+
prev_endtime_for_commit: args.prev_endtime_for_commit,
272+
},
273+
);
274+
275+
let completion = monitor.monitor_package(monitoring_options).await?;
276+
debug!("Completed with: {:?}", completion);
277+
278+
let mut log_file = monitor
279+
.download_build_log(&args.build_log_out, artifacts)
280+
.await?;
281+
282+
match completion {
283+
PackageCompletion::Succeeded => {
284+
outputln!("Build succeeded!");
285+
}
286+
PackageCompletion::Superceded => {
287+
outputln!("Build was superceded by a newer revision.");
288+
}
289+
PackageCompletion::Disabled => {
290+
outputln!("Package is disabled for this architecture.");
291+
}
292+
PackageCompletion::Failed(reason) => {
293+
log_file
294+
.file
295+
.seek(SeekFrom::End(
296+
-(std::cmp::min(log_tail_bytes, log_file.len) as i64),
297+
))
298+
.await
299+
.wrap_err("Failed to find length of log file")?;
300+
301+
log_tail_cb(log_file.file).await?;
302+
303+
outputln!("{}", "=".repeat(64));
304+
outputln!(
305+
"Build failed with reason '{}'.",
306+
reason.to_string().to_lowercase()
307+
);
308+
outputln!("The last 2MB of the build log is printed above.");
309+
outputln!(
310+
"(Full logs are available in the build artifact '{}'.)",
311+
args.build_log_out
312+
);
313+
return Err(FailedBuild.into());
314+
}
315+
}
316+
317+
Ok(())
318+
}
319+
320+
#[instrument(skip_all, fields(args))]
321+
pub async fn download_binaries(
322+
&mut self,
323+
args: DownloadBinariesAction,
324+
actions: &mut impl ArtifactDirectory,
325+
) -> Result<()> {
326+
let binaries = download_binaries(
327+
self.client.clone(),
328+
&args.project,
329+
&args.package,
330+
&args.repository,
331+
&args.arch,
332+
actions,
333+
&args.build_results_dir,
334+
)
335+
.await?;
336+
337+
outputln!("Downloaded {} artifact(s).", binaries.paths.len());
338+
Ok(())
339+
}
340+
341+
#[instrument(skip_all, fields(args))]
342+
pub async fn prune(
343+
&mut self,
344+
args: PruneAction,
345+
artifacts: &impl ArtifactDirectory,
346+
) -> Result<()> {
347+
let build_info_data = if args.ignore_missing_build_info {
348+
if let Some(build_info_data) = artifacts
349+
.read_string(&args.build_info)
350+
.await
351+
.missing_artifact_to_none()?
352+
{
353+
build_info_data
354+
} else {
355+
outputln!(
356+
"Skipping prune: build info file '{}' not found.",
357+
args.build_info
358+
);
359+
return Ok(());
360+
}
361+
} else {
362+
artifacts.read_string(&args.build_info).await?
363+
};
364+
365+
let build_info: ObsBuildInfo = serde_yaml::from_str(&build_info_data)
366+
.wrap_err("Failed to parse provided build info file")?;
367+
368+
if build_info.is_branched {
369+
outputln!(
370+
"Pruning branched package {}/{}...",
371+
build_info.project,
372+
build_info.package
373+
);
374+
prune_branch(
375+
&self.client,
376+
&build_info.project,
377+
&build_info.package,
378+
build_info.rev.as_deref(),
379+
)
380+
.await?;
381+
} else {
382+
outputln!("Skipping prune: package was not branched.");
383+
}
384+
385+
Ok(())
386+
}
387+
}

0 commit comments

Comments
 (0)