Skip to main content

release_tool/publisher/
mod.rs

1mod github_maven;
2mod github_release;
3
4pub use github_maven::{GithubMavenPublisher, MavenRemote};
5pub use github_release::GithubReleasePublisher;
6
7use crate::domain::{ArtifactIdentity, ArtifactManifest, TargetPlan};
8use anyhow::{Context, Result, bail};
9use sha2::{Digest, Sha256};
10use std::collections::HashSet;
11use std::fs;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum PublicationState {
15    Absent,
16    Complete,
17    Partial {
18        present: Vec<String>,
19        missing: Vec<String>,
20    },
21    Invalid {
22        reason: String,
23    },
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct PublicationReceipt {
28    pub tool_version: String,
29    pub repository: String,
30    pub tag: String,
31    pub commit: String,
32    pub target: String,
33    pub publisher: String,
34    pub artifacts: Vec<ArtifactIdentity>,
35    pub skipped: bool,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct VerificationReport {
40    pub target: String,
41    pub verified: bool,
42    pub artifacts: Vec<String>,
43}
44
45pub trait Publisher {
46    fn inspect(&self, plan: &TargetPlan) -> Result<PublicationState>;
47    fn publish(&self, manifest: &ArtifactManifest) -> Result<PublicationReceipt>;
48    fn verify(&self, manifest: &ArtifactManifest) -> Result<VerificationReport>;
49    fn verify_existing(&self, plan: &TargetPlan) -> Result<VerificationReport>;
50}
51
52fn receipt(manifest: &ArtifactManifest, publisher: &str, skipped: bool) -> PublicationReceipt {
53    PublicationReceipt {
54        tool_version: env!("CARGO_PKG_VERSION").to_owned(),
55        repository: manifest.release.repository.clone(),
56        tag: manifest.release.tag.to_string(),
57        commit: manifest.release.commit.clone(),
58        target: manifest.target.clone(),
59        publisher: publisher.to_owned(),
60        artifacts: manifest
61            .artifacts
62            .iter()
63            .map(|artifact| artifact.identity.clone())
64            .collect(),
65        skipped,
66    }
67}
68
69fn validate_manifest(manifest: &ArtifactManifest) -> Result<()> {
70    if manifest.artifacts.is_empty() {
71        bail!("artifact manifest must not be empty");
72    }
73    let mut identities = HashSet::new();
74    for artifact in &manifest.artifacts {
75        if !identities.insert(&artifact.identity) {
76            bail!(
77                "artifact manifest contains a duplicate identity: {:?}",
78                artifact.identity
79            );
80        }
81        let expected_file_name = match &artifact.identity {
82            ArtifactIdentity::GithubReleaseAsset { name } => name.clone(),
83            ArtifactIdentity::MavenPackage {
84                artifact_id,
85                version,
86                extension,
87                ..
88            } => format!("{artifact_id}-{version}.{extension}"),
89        };
90        if artifact.path.file_name().and_then(|name| name.to_str())
91            != Some(expected_file_name.as_str())
92        {
93            bail!(
94                "prepared artifact file name does not match its identity: expected `{expected_file_name}`, found `{}`",
95                artifact.path.display()
96            );
97        }
98        let bytes = fs::read(&artifact.path).with_context(|| {
99            format!(
100                "failed to read prepared artifact {}",
101                artifact.path.display()
102            )
103        })?;
104        let actual = hex::encode(Sha256::digest(bytes));
105        if actual != artifact.sha256 {
106            bail!(
107                "prepared artifact changed after Prepare: {} (expected {}, found {actual})",
108                artifact.path.display(),
109                artifact.sha256
110            );
111        }
112    }
113    Ok(())
114}