artifactize_core/remote/
push.rs1use std::{collections::BTreeSet, io::Write, path::Path};
2
3use serde::Serialize;
4
5use super::session::{has_scope, record};
6use crate::{auth::remote, store::history};
7
8const PAGE_ENTRIES: usize = 100;
10
11#[derive(Debug, Default, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Push {
14 pub dry_run: bool,
15 pub pushed: usize,
17 pub existing: usize,
18 pub skipped: usize,
20}
21
22pub async fn push(
25 state: Option<&Path>,
26 repo: Option<&Path>,
27 dry_run: bool,
28) -> Result<Push, String> {
29 let remote = remote::remote(state, repo)?.ok_or(
30 "No remote review store is configured; run `artifactize remote login URL` or set ARTIFACTIZE_REMOTE.",
31 )?;
32 let principal = remote.whoami().await.map_err(|failure| failure.message)?;
33 if !has_scope(&principal, &crate::auth::remote::Scope::Publish) {
34 return Err(format!(
35 "Remote token {} lacks the publish scope.",
36 principal.principal
37 ));
38 }
39 let state = crate::store::state_dir(state)?;
40 let mut report = Push {
41 dry_run,
42 ..Push::default()
43 };
44 let mut after = None;
45 loop {
46 let page = history::local(&state, after.take(), PAGE_ENTRIES).await?;
47 let Some(last) = page.last() else {
48 break;
49 };
50 after = Some(last.key.clone().expect("cached key"));
51 let mut records = Vec::new();
52 for execution in &page {
53 match record(execution, &principal, remote.share) {
54 Ok(record) => records.push(record),
55 Err(reason) => {
56 report.skipped += 1;
57 let _ = writeln!(
58 std::io::stderr().lock(),
59 "Skipped {} ({}): {reason}.",
60 execution.key.as_deref().unwrap_or_default(),
61 execution.provenance.eval_id
62 );
63 }
64 }
65 }
66 let existing: BTreeSet<_> = if has_scope(&principal, &crate::auth::remote::Scope::Read) {
68 let keys: Vec<_> = records.iter().map(|record| record.key.clone()).collect();
69 remote
70 .lookup(&keys)
71 .await
72 .map_err(|failure| failure.message)?
73 .into_iter()
74 .filter_map(Result::ok)
75 .map(|entry| (entry.key.to_string(), entry.execution_id.to_string()))
76 .collect()
77 } else {
78 BTreeSet::new()
79 };
80 for record in records {
81 let created = !existing
83 .contains(&(record.key.to_string(), record.execution_id.to_string()))
84 && (dry_run
85 || remote
86 .publish(&record)
87 .await
88 .map_err(|failure| failure.message)?);
89 if created {
90 report.pushed += 1;
91 } else {
92 report.existing += 1;
93 }
94 }
95 }
96 Ok(report)
97}