use anyhow::{bail, Result};
use std::fs;
use std::path::Path;
const VALIDATE_TEMPLATE: &str = r#"name: Validate kazam site
on:
push:
paths: ['**.yaml']
pull_request:
paths: ['**.yaml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kazam
run: cargo install --git https://github.com/tdiderich/kazam
- name: Validate
run: kazam validate .
"#;
const FRESHNESS_TEMPLATE: &str = r#"name: Freshness check
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am UTC
workflow_dispatch:
jobs:
freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kazam
run: cargo install --git https://github.com/tdiderich/kazam
- name: Check freshness
id: freshness
run: |
kazam freshness . > freshness.json
echo "report<<EOF" >> $GITHUB_OUTPUT
kazam freshness . --pretty >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create issue if stale pages found
if: steps.freshness.outputs.report != ''
uses: actions/github-script@v7
with:
script: |
const report = `${{ steps.freshness.outputs.report }}`;
if (report.includes('overdue') || report.includes('due_soon')) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `📚 Weekly freshness report - ${new Date().toISOString().split('T')[0]}`,
body: '## Freshness Report\n\n```\n' + report + '\n```\n\nGenerated by `kazam freshness`.',
labels: ['documentation', 'freshness']
});
}
"#;
const BUILD_TEMPLATE: &str = r#"name: Build kazam site
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kazam
run: cargo install --git https://github.com/tdiderich/kazam
- name: Build
run: kazam build . --out _site --release --json > build.log
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site/
"#;
struct Template {
name: &'static str,
description: &'static str,
content: &'static str,
}
const TEMPLATES: &[Template] = &[
Template {
name: "validate",
description: "Run kazam validate on every push/PR that touches YAML",
content: VALIDATE_TEMPLATE,
},
Template {
name: "freshness",
description: "Weekly freshness report posted as a GitHub issue",
content: FRESHNESS_TEMPLATE,
},
Template {
name: "build",
description: "Build site and upload artifact on push to main",
content: BUILD_TEMPLATE,
},
];
fn find_template(name: &str) -> Option<&'static Template> {
TEMPLATES.iter().find(|t| t.name == name)
}
pub fn list() -> Result<()> {
println!("Available action templates:");
for t in TEMPLATES {
println!(" {:<12}— {}", t.name, t.description);
}
Ok(())
}
pub fn init(name: &str, project_dir: &Path) -> Result<()> {
let Some(template) = find_template(name) else {
let names: Vec<&str> = TEMPLATES.iter().map(|t| t.name).collect();
bail!(
"Unknown template '{}'. Available: {}",
name,
names.join(", ")
);
};
let workflows_dir = project_dir.join(".github").join("workflows");
fs::create_dir_all(&workflows_dir)?;
let file_path = workflows_dir.join(format!("kazam-{}.yml", name));
if file_path.exists() {
println!("File already exists: {}", file_path.display());
println!("Remove the file first if you want to regenerate it.");
return Ok(());
}
fs::write(&file_path, template.content)?;
println!("Created: {}", file_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tmp_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join("kazam_actions_tests").join(label);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn list_returns_all_three_templates() {
assert_eq!(TEMPLATES.len(), 3);
let names: Vec<&str> = TEMPLATES.iter().map(|t| t.name).collect();
assert!(names.contains(&"validate"));
assert!(names.contains(&"freshness"));
assert!(names.contains(&"build"));
}
#[test]
fn validate_template_contains_expected_strings() {
let t = find_template("validate").expect("validate template missing");
assert!(t.content.contains("kazam validate ."));
assert!(t.content.contains("actions/checkout@v4"));
assert!(t.content.contains("cargo install --git"));
}
#[test]
fn freshness_template_contains_expected_strings() {
let t = find_template("freshness").expect("freshness template missing");
assert!(t.content.contains("kazam freshness ."));
assert!(t.content.contains("schedule"));
assert!(t.content.contains("workflow_dispatch"));
}
#[test]
fn build_template_contains_expected_strings() {
let t = find_template("build").expect("build template missing");
assert!(t.content.contains("kazam build ."));
assert!(t.content.contains("upload-pages-artifact"));
assert!(t.content.contains("branches: [main]"));
}
#[test]
fn init_creates_file() {
let dir = tmp_dir("init_creates_file");
init("validate", &dir).unwrap();
let expected = dir.join(".github/workflows/kazam-validate.yml");
assert!(expected.exists());
let content = fs::read_to_string(&expected).unwrap();
assert!(content.contains("kazam validate ."));
}
#[test]
fn init_twice_does_not_overwrite() {
let dir = tmp_dir("init_twice_does_not_overwrite");
init("validate", &dir).unwrap();
let file_path = dir.join(".github/workflows/kazam-validate.yml");
fs::write(&file_path, "SENTINEL").unwrap();
init("validate", &dir).unwrap();
let content = fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "SENTINEL");
}
#[test]
fn init_unknown_template_returns_error() {
let dir = tmp_dir("init_unknown_template_returns_error");
let result = init("nonexistent", &dir);
assert!(result.is_err());
}
}