release-tool 0.5.0

Configuration-driven release lifecycle for computed-parameter repositories
Documentation
use crate::tag::CalendarTag;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReleaseIntent {
    Existing,
    New,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReleaseCandidate {
    pub repository: String,
    pub commit: String,
    pub tag: CalendarTag,
    pub intent: ReleaseIntent,
    pub tag_already_sealed: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SealedRelease {
    pub release: ReleaseCandidate,
    pub tag_object: String,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ArtifactIdentity {
    GithubReleaseAsset {
        name: String,
    },
    MavenPackage {
        group_id: String,
        artifact_id: String,
        version: String,
        extension: String,
        classifier: Option<String>,
    },
    OciImage {
        repository: String,
        tag: String,
        platform: String,
    },
}

impl ArtifactIdentity {
    pub(crate) fn maven_file_name(&self) -> anyhow::Result<String> {
        let Self::MavenPackage {
            artifact_id,
            version,
            extension,
            classifier,
            ..
        } = self
        else {
            anyhow::bail!("expected a Maven artifact identity");
        };
        let suffix = match classifier {
            Some(classifier) => {
                validate_maven_attachment(classifier, extension)?;
                format!("-{classifier}")
            }
            None => String::new(),
        };
        Ok(format!("{artifact_id}-{version}{suffix}.{extension}"))
    }
}

pub(crate) fn validate_maven_attachment(classifier: &str, extension: &str) -> anyhow::Result<()> {
    if !classifier
        .as_bytes()
        .first()
        .is_some_and(u8::is_ascii_alphanumeric)
        || !classifier
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
    {
        anyhow::bail!(
            "Maven attachment classifier must be a nonempty safe identifier: `{classifier}`"
        );
    }
    if !matches!(extension, "jar" | "zip") {
        anyhow::bail!("Maven attachment extension must be jar or zip: `{extension}`");
    }
    Ok(())
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetPlan {
    pub name: String,
    pub publisher: String,
    pub release: ReleaseCandidate,
    pub artifacts: Vec<ArtifactIdentity>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedArtifact {
    pub identity: ArtifactIdentity,
    pub path: std::path::PathBuf,
    pub sha256: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ArtifactManifest {
    pub target: String,
    pub publisher: String,
    pub release: ReleaseCandidate,
    pub artifacts: Vec<PreparedArtifact>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReleasePlan {
    pub tool_version: String,
    pub release: ReleaseCandidate,
    pub targets: Vec<TargetPlan>,
}