release_tool/publisher/
mod.rs1mod 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}