mod github_maven;
mod github_release;
mod oci_registry;
pub use github_maven::{GithubMavenPublisher, MavenRemote};
pub use github_release::GithubReleasePublisher;
pub use oci_registry::OciRegistryPublisher;
use crate::domain::{ArtifactIdentity, ArtifactManifest, TargetPlan};
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::fs;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PublicationState {
Absent,
Complete,
Partial {
present: Vec<String>,
missing: Vec<String>,
},
Invalid {
reason: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicationReceipt {
pub tool_version: String,
pub repository: String,
pub tag: String,
pub commit: String,
pub target: String,
pub publisher: String,
pub artifacts: Vec<ArtifactIdentity>,
pub skipped: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VerificationReport {
pub target: String,
pub verified: bool,
pub artifacts: Vec<String>,
}
pub trait Publisher {
fn inspect(&self, plan: &TargetPlan) -> Result<PublicationState>;
fn publish(&self, manifest: &ArtifactManifest) -> Result<PublicationReceipt>;
fn verify(&self, manifest: &ArtifactManifest) -> Result<VerificationReport>;
fn verify_existing(&self, plan: &TargetPlan) -> Result<VerificationReport>;
}
pub(crate) fn receipt(
manifest: &ArtifactManifest,
publisher: &str,
skipped: bool,
) -> PublicationReceipt {
PublicationReceipt {
tool_version: env!("CARGO_PKG_VERSION").to_owned(),
repository: manifest.release.repository.clone(),
tag: manifest.release.tag.to_string(),
commit: manifest.release.commit.clone(),
target: manifest.target.clone(),
publisher: publisher.to_owned(),
artifacts: manifest
.artifacts
.iter()
.map(|artifact| artifact.identity.clone())
.collect(),
skipped,
}
}
fn validate_manifest(manifest: &ArtifactManifest) -> Result<()> {
if manifest.artifacts.is_empty() {
bail!("artifact manifest must not be empty");
}
let mut identities = HashSet::new();
for artifact in &manifest.artifacts {
if !identities.insert(&artifact.identity) {
bail!(
"artifact manifest contains a duplicate identity: {:?}",
artifact.identity
);
}
let expected_file_name = match &artifact.identity {
ArtifactIdentity::GithubReleaseAsset { name } => name.clone(),
ArtifactIdentity::MavenPackage {
artifact_id,
version,
extension,
..
} => format!("{artifact_id}-{version}.{extension}"),
ArtifactIdentity::OciImage { .. } => {
bail!("file publisher received an OCI image artifact")
}
};
if artifact.path.file_name().and_then(|name| name.to_str())
!= Some(expected_file_name.as_str())
{
bail!(
"prepared artifact file name does not match its identity: expected `{expected_file_name}`, found `{}`",
artifact.path.display()
);
}
let bytes = fs::read(&artifact.path).with_context(|| {
format!(
"failed to read prepared artifact {}",
artifact.path.display()
)
})?;
let actual = hex::encode(Sha256::digest(bytes));
if actual != artifact.sha256 {
bail!(
"prepared artifact changed after Prepare: {} (expected {}, found {actual})",
artifact.path.display(),
artifact.sha256
);
}
}
Ok(())
}