fission-command-release 0.8.0

Release content, store, auth, beta, and review workflows for the fission command
Documentation
use super::*;

pub(super) fn release_steps(
    requirements: &[ReleaseRequirement],
    has_artifact: bool,
    status: &str,
) -> Vec<ReleaseStep> {
    vec![
        ReleaseStep {
            id: "inspect".to_string(),
            title: "Inspect project and release configuration".to_string(),
            status: step_status_for(
                requirements,
                &["release_config.", "release.version.", "release.build."],
            ),
            jobs: vec![ReleaseJob {
                id: "inspect.fission_toml".to_string(),
                title: "Read fission.toml, package config, and active release".to_string(),
                status: step_status_for(
                    requirements,
                    &["release_config.", "release.version.", "release.build."],
                ),
            }],
        },
        ReleaseStep {
            id: "preflight".to_string(),
            title: "Preflight target, package, provider, and secrets".to_string(),
            status: step_status_for(requirements, PREFLIGHT_PREFIXES),
            jobs: vec![ReleaseJob {
                id: "preflight.requirements".to_string(),
                title: "Evaluate provider-required and Fission-recommended requirements"
                    .to_string(),
                status: step_status_for(requirements, PREFLIGHT_PREFIXES),
            }],
        },
        ReleaseStep {
            id: "configure".to_string(),
            title: "Configure app identity, signing, track, locales, and provider account"
                .to_string(),
            status: step_status_for(
                requirements,
                &[
                    "release_config.",
                    "signing.",
                    "release.play_store.track",
                    "release.app_store.track",
                    "release.microsoft_store.track",
                    "distribution.",
                ],
            ),
            jobs: vec![ReleaseJob {
                id: "configure.release_inputs".to_string(),
                title: "Resolve release inputs and interactive fixes".to_string(),
                status: step_status_for(
                    requirements,
                    &[
                        "release_config.",
                        "signing.",
                        "release.play_store.track",
                        "release.app_store.track",
                        "release.microsoft_store.track",
                        "distribution.",
                    ],
                ),
            }],
        },
        ReleaseStep {
            id: "release_content".to_string(),
            title: "Validate release notes, screenshots, metadata, and review content".to_string(),
            status: step_status_for(requirements, &["release_content."]),
            jobs: vec![ReleaseJob {
                id: "release_content.validate".to_string(),
                title: "Validate release-content references and provider assets".to_string(),
                status: step_status_for(requirements, &["release_content."]),
            }],
        },
        ReleaseStep {
            id: "package".to_string(),
            title: "Build and package artifact".to_string(),
            status: if has_artifact {
                StepStatus::Completed
            } else if status == "blocked" {
                StepStatus::Pending
            } else {
                StepStatus::Ready
            },
            jobs: vec![ReleaseJob {
                id: "package.artifact".to_string(),
                title: "Create artifact manifest with target, format, version, build, and hashes"
                    .to_string(),
                status: if has_artifact {
                    StepStatus::Completed
                } else {
                    StepStatus::Pending
                },
            }],
        },
        ReleaseStep {
            id: "dry_run".to_string(),
            title: "Preview artifact, provider destination, metadata, assets, and mutations"
                .to_string(),
            status: if status == "blocked" {
                StepStatus::Pending
            } else {
                StepStatus::Ready
            },
            jobs: vec![ReleaseJob {
                id: "dry_run.plan".to_string(),
                title: "Show exact artifact, version/build, track, locales, and provider action"
                    .to_string(),
                status: if status == "blocked" {
                    StepStatus::Pending
                } else {
                    StepStatus::Ready
                },
            }],
        },
        ReleaseStep {
            id: "publish".to_string(),
            title: "Dry-run, publish, and write receipt".to_string(),
            status: match status {
                "blocked" => StepStatus::Blocked,
                "warning" => StepStatus::Warning,
                _ => StepStatus::Ready,
            },
            jobs: vec![ReleaseJob {
                id: "publish.provider".to_string(),
                title: "Upload/update provider state and write a distribution receipt".to_string(),
                status: match status {
                    "blocked" => StepStatus::Blocked,
                    _ => StepStatus::Ready,
                },
            }],
        },
        ReleaseStep {
            id: "observe".to_string(),
            title: "Observe provider processing state and follow-up".to_string(),
            status: if status == "blocked" {
                StepStatus::Pending
            } else {
                StepStatus::Ready
            },
            jobs: vec![ReleaseJob {
                id: "observe.provider_status".to_string(),
                title: "Fetch provider status after upload/update when supported".to_string(),
                status: if status == "blocked" {
                    StepStatus::Pending
                } else {
                    StepStatus::Ready
                },
            }],
        },
    ]
}

pub(super) fn step_status_label(status: StepStatus) -> &'static str {
    match status {
        StepStatus::Pending => "pending",
        StepStatus::Blocked => "blocked",
        StepStatus::Warning => "warning",
        StepStatus::Ready => "ready",
        StepStatus::Completed => "completed",
    }
}

pub(super) fn requirement_level_label(level: RequirementLevel) -> &'static str {
    match level {
        RequirementLevel::ProviderRequired => "provider-required",
        RequirementLevel::FissionRecommended => "fission-recommended",
        RequirementLevel::Optional => "optional",
    }
}

pub(super) fn requirement_status_label(status: RequirementStatus) -> &'static str {
    match status {
        RequirementStatus::Passed => "passed",
        RequirementStatus::Missing => "missing",
        RequirementStatus::Failed => "failed",
        RequirementStatus::Warning => "warning",
        RequirementStatus::Skipped => "skipped",
    }
}

const PREFLIGHT_PREFIXES: &[&str] = &[
    "release.package.",
    "release.distribution.",
    "release.play_store.",
    "release.app_store.",
    "release.microsoft_store.",
    "release.github_releases.",
    "release.github_pages.",
    "release.cloudflare_pages.",
    "release.netlify.",
    "release.s3.",
    "release.google_drive.",
    "release.onedrive.",
    "release.dropbox.",
    "release.docker_registry.",
];

fn step_status_for(requirements: &[ReleaseRequirement], prefixes: &[&str]) -> StepStatus {
    let matching = requirements
        .iter()
        .filter(|req| prefixes.iter().any(|prefix| req.id.starts_with(prefix)))
        .collect::<Vec<_>>();
    if matching.is_empty() {
        return StepStatus::Ready;
    }
    if matching.iter().any(|req| {
        req.level == RequirementLevel::ProviderRequired
            && matches!(
                req.status,
                RequirementStatus::Missing | RequirementStatus::Failed
            )
    }) {
        StepStatus::Blocked
    } else if matching.iter().any(|req| {
        !matches!(
            req.status,
            RequirementStatus::Passed | RequirementStatus::Skipped
        )
    }) {
        StepStatus::Warning
    } else {
        StepStatus::Ready
    }
}

pub(super) fn print_release_plan(report: &ReleasePlanReport, json: bool) -> Result<()> {
    if json {
        println!("{}", serde_json::to_string_pretty(report)?);
        return Ok(());
    }
    println!("release plan: {}", report.status);
    println!("provider: {}", report.provider);
    if let Some(target) = &report.target {
        println!("target: {target}");
    }
    if let Some(format) = &report.format {
        println!("format: {format}");
    }
    if let Some(artifact) = &report.artifact {
        println!("artifact: {artifact}");
    }
    println!();
    println!("capabilities:");
    for capability in &report.capabilities {
        println!(
            "[{:?}] {} - {}",
            capability.status, capability.id, capability.summary
        );
        if let Some(details) = &capability.details {
            println!("  {details}");
        }
    }
    println!();
    for req in &report.requirements {
        let marker = match req.level {
            RequirementLevel::ProviderRequired => "required",
            RequirementLevel::FissionRecommended => "recommended",
            RequirementLevel::Optional => "optional",
        };
        println!("[{:?}] {marker}: {}", req.status, req.id);
        if let Some(details) = &req.details {
            println!("  {details}");
        }
        for remediation in &req.remediation {
            println!("  fix: {remediation}");
        }
    }
    Ok(())
}