xbp 10.28.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use semver::Version;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub(crate) struct ReleaseDocEntry {
    pub(crate) tag: String,
    pub(crate) version: Version,
    pub(crate) date: String,
}

pub(crate) fn sync_release_docs(
    project_root: &Path,
    owner: &str,
    repo: &str,
) -> Result<Vec<PathBuf>, String> {
    let entries: Vec<ReleaseDocEntry> = release_doc_entries(project_root)?;
    let updated_paths = vec![
        sync_changelog(project_root, owner, repo, &entries)?,
        sync_security_policy(project_root, &entries)?,
    ];
    Ok(updated_paths)
}

fn sync_changelog(
    project_root: &Path,
    owner: &str,
    repo: &str,
    entries: &[ReleaseDocEntry],
) -> Result<PathBuf, String> {
    let changelog: String = render_changelog(owner, repo, entries);
    let path: PathBuf = project_root.join("CHANGELOG.md");
    fs::write(&path, changelog)
        .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
    Ok(path)
}

fn sync_security_policy(
    project_root: &Path,
    entries: &[ReleaseDocEntry],
) -> Result<PathBuf, String> {
    let policy: String = render_security_policy(entries);
    let path: PathBuf = project_root.join("SECURITY.md");
    fs::write(&path, policy).map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
    Ok(path)
}

fn release_doc_entries(project_root: &Path) -> Result<Vec<ReleaseDocEntry>, String> {
    let output: String = super::run_git_command(
        project_root,
        &[
            "for-each-ref",
            "--sort=-creatordate",
            "--format=%(refname:strip=2)|%(creatordate:short)",
            "refs/tags",
        ],
    )?;

    let mut entries: Vec<ReleaseDocEntry> = Vec::new();
    for line in output.lines() {
        let trimmed: &str = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let Some((tag_raw, date_raw)) = trimmed.split_once('|') else {
            continue;
        };
        let tag: String = tag_raw.trim().to_string();
        let date: String = date_raw.trim().to_string();
        let Ok(version) = super::parse_version(&tag) else {
            continue;
        };
        entries.push(ReleaseDocEntry { tag, version, date });
    }
    Ok(entries)
}

pub(crate) fn release_channel(version: &Version) -> &'static str {
    let label: String = version.pre.to_string().to_ascii_lowercase();
    if label.is_empty() {
        "stable"
    } else if label.contains("nightly") {
        "nightly"
    } else {
        "experimental"
    }
}

pub(crate) fn render_changelog(owner: &str, repo: &str, entries: &[ReleaseDocEntry]) -> String {
    let mut out: String = String::new();
    out.push_str("# Changelog\n\n");
    out.push_str("## Unreleased\n\n");
    out.push_str("### Notes\n\n");
    out.push_str("- _No unreleased changes yet._\n\n");

    for (idx, entry) in entries.iter().enumerate() {
        let version_label: String = entry.version.to_string();
        let line = if idx + 1 < entries.len() {
            let prev_tag: &str = &entries[idx + 1].tag;
            format!(
                "## [{}](https://github.com/{}/{}/compare/{}...{}) ({})\n\n",
                version_label, owner, repo, prev_tag, entry.tag, entry.date
            )
        } else {
            format!(
                "## [{}](https://github.com/{}/{}/releases/tag/{}) ({})\n\n",
                version_label, owner, repo, entry.tag, entry.date
            )
        };
        out.push_str(&line);
        out.push_str("- Release channel: ");
        out.push_str(release_channel(&entry.version));
        out.push('\n');
        out.push_str("- Tag: `");
        out.push_str(&entry.tag);
        out.push_str("`\n\n");
    }

    out
}

pub(crate) fn render_security_policy(entries: &[ReleaseDocEntry]) -> String {
    let mut out: String = String::new();
    out.push_str("# Security Policy\n\n");
    out.push_str("## Supported Versions\n\n");
    out.push_str("| Version | Channel | Supported |\n");
    out.push_str("| ------- | ------- | --------- |\n");
    for entry in entries {
        out.push_str("| ");
        out.push_str(&entry.version.to_string());
        out.push_str(" | ");
        out.push_str(release_channel(&entry.version));
        out.push_str(" | :white_check_mark: |\n");
    }
    if entries.is_empty() {
        out.push_str("| _none yet_ | n/a | :x: |\n");
    }

    out.push_str("\n## Reporting a Vulnerability\n\n");
    out.push_str(
        "Report vulnerabilities privately to the maintainers. Include reproduction steps, impact, and affected versions. You will receive an acknowledgement and triage update as soon as possible.\n",
    );
    out
}