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 {
90 artifact_id,
91 version,
92 extension,
93 ..
94 } => format!("{artifact_id}-{version}.{extension}"),
95 ArtifactIdentity::OciImage { .. } => {
96 bail!("file publisher received an OCI image artifact")
97 }
98 };
99 if artifact.path.file_name().and_then(|name| name.to_str())
100 != Some(expected_file_name.as_str())
101 {
102 bail!(
103 "prepared artifact file name does not match its identity: expected `{expected_file_name}`, found `{}`",
104 artifact.path.display()
105 );
106 }
107 let bytes = fs::read(&artifact.path).with_context(|| {
108 format!(
109 "failed to read prepared artifact {}",
110 artifact.path.display()
111 )
112 })?;
113 let actual = hex::encode(Sha256::digest(bytes));
114 if actual != artifact.sha256 {
115 bail!(
116 "prepared artifact changed after Prepare: {} (expected {}, found {actual})",
117 artifact.path.display(),
118 artifact.sha256
119 );
120 }
121 }
122 Ok(())
123}