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