loopflow 0.11.3

Run steps and flows with coding agents
Documentation
use std::path::Path;

use sha2::{Digest, Sha256};

#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BuildProvenance {
    Development,
    Release,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MigrationAuthority {
    Published,
    ValidationOnly,
}

impl BuildProvenance {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Development => "development",
            Self::Release => "release",
        }
    }

    pub fn is_release(self) -> bool {
        self == Self::Release
    }
}

impl std::fmt::Display for BuildProvenance {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

pub fn provenance() -> BuildProvenance {
    parse_provenance(env!("LOOPFLOW_BUILD_PROVENANCE"))
        .expect("build script embeds a validated Loopflow provenance")
}

pub fn source_root() -> Option<&'static Path> {
    let root = env!("LOOPFLOW_BUILD_SOURCE_ROOT");
    (root != "release").then(|| Path::new(root))
}

pub fn source_identity() -> String {
    source_root()
        .map(source_identity_for)
        .unwrap_or_else(|| "release".to_string())
}

pub fn source_revision() -> &'static str {
    env!("LOOPFLOW_BUILD_SOURCE_REVISION")
}

pub fn migration_authority() -> MigrationAuthority {
    match env!("LOOPFLOW_MIGRATION_AUTHORITY") {
        "published" => MigrationAuthority::Published,
        "validation_only" => MigrationAuthority::ValidationOnly,
        _ => unreachable!("build script validates migration authority"),
    }
}

fn parse_provenance(value: &str) -> Option<BuildProvenance> {
    match value {
        "development" => Some(BuildProvenance::Development),
        "release" => Some(BuildProvenance::Release),
        _ => None,
    }
}

fn source_identity_for(root: &Path) -> String {
    let name = root
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("loopflow")
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric() || character == '-' {
                character
            } else {
                '-'
            }
        })
        .collect::<String>();
    let digest = Sha256::digest(root.as_os_str().as_encoded_bytes());
    format!("{name}-{}", hex::encode(&digest[..6]))
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::{
        migration_authority, parse_provenance, provenance, source_identity_for, source_revision,
        source_root, BuildProvenance, MigrationAuthority,
    };

    #[test]
    fn source_identity_is_stable_and_distinguishes_worktrees() {
        assert_eq!(
            source_identity_for(Path::new("/src/loopflow")),
            source_identity_for(Path::new("/src/loopflow"))
        );
        assert_ne!(
            source_identity_for(Path::new("/src/loopflow")),
            source_identity_for(Path::new("/src/loopflow-feature"))
        );
    }

    #[test]
    fn embedded_build_metadata_is_valid_in_checkouts_and_packaged_sources() {
        assert!(matches!(
            provenance(),
            BuildProvenance::Development | BuildProvenance::Release
        ));
        match provenance() {
            BuildProvenance::Development => {
                assert!(source_root().is_some_and(Path::is_absolute));
            }
            BuildProvenance::Release => {
                assert_eq!(source_root(), None);
                assert_eq!(super::source_identity(), "release");
            }
        }
        assert_eq!(
            parse_provenance("development"),
            Some(BuildProvenance::Development)
        );
        assert_eq!(parse_provenance("release"), Some(BuildProvenance::Release));
        assert_eq!(parse_provenance("other"), None);
        assert!(matches!(
            migration_authority(),
            MigrationAuthority::Published | MigrationAuthority::ValidationOnly
        ));
        assert!(!source_revision().is_empty());
    }
}