simit 0.17.7

Semver-aware git commit helper for Rust projects
Documentation
use anyhow::Result;

use crate::cargo;
use crate::cli::InitReleaseCommand;
use crate::cli::{Platform, Runtime};
use crate::commands::scaffold::{ArtifactCheck, CheckPrintMode, print_next_steps};
use crate::commands::upgrade;
use crate::config::{
    AptOverrides, AurOverrides, ChocolateyOverrides, CoprOverrides, HomebrewOverrides,
    ProjectConfig, ScoopOverrides,
};
use crate::registry::{self, FeatureStatus};
use crate::render::release_workflow::{self, ReleaseWorkflowInputs};
use crate::user_config::{ResolvedRunner, UserConfig};

const WORKFLOW_PATH: &str = ".forgejo/workflows/release.yml";

pub fn run(command: InitReleaseCommand) -> Result<()> {
    let mode = CheckPrintMode::parse("init release", command.check, command.print, command.diff)?;

    let metadata = cargo::metadata_for_current_dir()?;
    let workspace_root = metadata.workspace_root.as_std_path();
    let package = cargo::representative_package(&metadata, command.package.as_deref())?;
    let cfg = ProjectConfig::load(workspace_root)?;

    let codeberg = cfg.resolve_codeberg_release()?;
    let aur = cfg
        .aur
        .as_ref()
        .map(|_| cfg.resolve_aur(AurOverrides::default(), &package))
        .transpose()?;
    let copr = cfg
        .copr
        .as_ref()
        .map(|_| cfg.resolve_copr(CoprOverrides::default(), &package))
        .transpose()?;
    let apt = cfg
        .apt
        .as_ref()
        .map(|_| cfg.resolve_apt(AptOverrides::default(), &package))
        .transpose()?;
    let homebrew = cfg
        .homebrew
        .as_ref()
        .map(|_| cfg.resolve_homebrew(HomebrewOverrides::default(), &package))
        .transpose()?;
    let scoop = cfg
        .scoop
        .as_ref()
        .map(|_| cfg.resolve_scoop(ScoopOverrides::default(), &package))
        .transpose()?;
    let chocolatey = cfg
        .chocolatey
        .as_ref()
        .map(|_| cfg.resolve_chocolatey(ChocolateyOverrides::default(), &package))
        .transpose()?;

    let (runner, preinstalled_nix) = resolve_release_runner(&cfg)?;

    let inputs = ReleaseWorkflowInputs {
        runner: &runner,
        preinstalled_nix,
        publish_enforcement: cfg.release.publish.enforcement,
        artifacts: &cfg.release.artifacts,
        smoke_command: cfg.release.smoke.command.as_deref(),
        codeberg: codeberg.as_ref(),
        attic: cfg.release.attic.as_ref(),
        aur: aur.as_ref(),
        copr: copr.as_ref(),
        apt: apt.as_ref(),
        homebrew: homebrew.as_ref(),
        scoop: scoop.as_ref(),
        chocolatey: chocolatey.as_ref(),
        windows_signing: cfg.release.windows_signing.as_ref(),
        flatpak: cfg.flatpak.as_ref(),
        winget: cfg.winget.as_ref(),
        announce: cfg.release.announce.as_ref(),
    };
    let content = release_workflow::render(&inputs);
    let path = workspace_root.join(WORKFLOW_PATH);

    match mode {
        CheckPrintMode::Print => {
            print!("{content}");
            Ok(())
        }
        CheckPrintMode::Check { diff } => ArtifactCheck {
            label: "release workflow",
            path: &path,
            expected: &content,
            remediation: "run `simit init release`",
        }
        .verify(diff)
        .and_then(|_| upgrade::update_readme_badges_if_present(workspace_root, true, diff)),
        CheckPrintMode::Write => {
            let parent = path.parent().expect("workflow path has a parent");
            std::fs::create_dir_all(parent)?;
            std::fs::write(&path, &content)?;
            upgrade::update_readme_badges_if_present(workspace_root, false, false)?;
            print_next_steps(
                workspace_root,
                "Generated release workflow",
                &[
                    format!("git add {WORKFLOW_PATH}"),
                    "configure the secrets listed at the top of the workflow".to_owned(),
                    "push a signed tag like 0.1.0 to trigger it".to_owned(),
                ],
            );
            registry::touch_current_project_or_warn([("ci", FeatureStatus::Managed)]);
            Ok(())
        }
    }
}

fn resolve_release_runner(cfg: &ProjectConfig) -> Result<(String, bool)> {
    if let Some(runner) = cfg
        .release
        .artifacts
        .runner
        .clone()
        .or_else(|| cfg.ci.runner.clone())
    {
        let preinstalled_nix = match UserConfig::load() {
            Ok(user_config) => user_config.explicit_label_is_trusted_forgejo_nix_runner(&runner)?,
            Err(_) => false,
        };
        return Ok((runner, preinstalled_nix));
    }

    let user_config = UserConfig::load()?;
    let runners =
        user_config.resolve_ci_runners(Platform::Forgejo, Runtime::Nix, None, None, false)?;
    Ok((runs_on(&runners.release), true))
}

fn runs_on(runner: &ResolvedRunner) -> String {
    if runner.labels.len() == 1 {
        return runner.labels[0].clone();
    }

    let labels = runner
        .labels
        .iter()
        .map(|label| format!("\"{}\"", label.replace('"', "\\\"")))
        .collect::<Vec<_>>()
        .join(", ");
    format!("[{labels}]")
}