Skip to main content

artifactize_core/remote/
push.rs

1use 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
8/// Bound retained local execution payloads per push batch while paging the whole key history.
9const PAGE_ENTRIES: usize = 100;
10
11#[derive(Debug, Default, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Push {
14    pub dry_run: bool,
15    /// Sent and created, or with `--dry-run` not yet in the store.
16    pub pushed: usize,
17    pub existing: usize,
18    /// Kept local: Human sign-offs without the human scope, or records over their limit.
19    pub skipped: usize,
20}
21
22/// Send the latest local GREEN/RED record of each key, never a mirror; records the store
23/// already holds are no-ops. Unlike verify, this explicit command fails on any remote failure.
24pub 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        // With the read scope, a record that is already the store's latest is not sent again.
67        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            // The store keeps each execution once; a resent record answers `created: false`.
82            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}