diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ccfa0..394c215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## todo-tree-0.5.0 + +### Added +- Added `workflow init` command to scaffold `.github/workflows/todo-tree.yml`. +- Generated workflow template now pins `alexandretrotel/todo-tree-action@v1.0.3`. + +### Documentation +- Documented GitHub Actions setup with `tt workflow init`. + ## todo-tree-0.4.0 ### Breaking Changes diff --git a/Cargo.lock b/Cargo.lock index 4a484e4..217ff4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,7 +458,7 @@ dependencies = [ [[package]] name = "todo-tree" -version = "0.4.2" +version = "0.5.0" dependencies = [ "anyhow", "clap", @@ -475,7 +475,7 @@ dependencies = [ [[package]] name = "todo-tree-core" -version = "0.4.2" +version = "0.5.0" dependencies = [ "serde", "serde_json", diff --git a/README.md b/README.md index fa8f957..0840f82 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ tt tags # Show statistics tt stats + +# Create a GitHub Actions workflow +tt workflow init ``` ## Configuration @@ -217,6 +220,24 @@ These defaults align with most coding conventions and help you find **intentiona ## Terminal Support +## GitHub Actions + +Generate a workflow file at `.github/workflows/todo-tree.yml`: + +```bash +tt workflow init +``` + +This creates a pull request workflow that checks out the repository and runs `alexandretrotel/todo-tree-action@v1.0.3` by default. + +Use `--force` to overwrite an existing workflow, `--path` to write the template elsewhere, or `--action` to override the generated action ref: + +```bash +tt workflow init --force +tt workflow init --path .github/workflows/custom-todo-tree.yml +tt workflow init --action alexandretrotel/todo-tree-action@main +``` + ### Clickable Links The tool generates clickable hyperlinks (OSC 8) in supported terminals: diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f797972..d61dfb2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "todo-tree" -version = "0.4.2" +version = "0.5.0" edition.workspace = true authors.workspace = true license-file.workspace = true @@ -25,7 +25,7 @@ name = "tt" path = "src/bin/tt.rs" [dependencies] -todo-tree-core = { path = "../core", version = "0.4.0" } +todo-tree-core = { path = "../core", version = "0.5.0" } clap = { version = "4.5.60", features = ["derive", "env"] } regex = "1.12.3" ignore = "0.4.25" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1725d5c..d21432d 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -44,6 +44,8 @@ pub enum Commands { Tags(TagsArgs), #[command(about = "Create a default configuration file")] Init(InitArgs), + #[command(about = "Manage GitHub Actions workflow templates")] + Workflow(WorkflowArgs), #[command(about = "Show summary stats for TODO matches")] Stats(StatsArgs), } @@ -177,6 +179,37 @@ pub struct InitArgs { pub force: bool, } +#[derive(Args, Debug, Clone)] +pub struct WorkflowArgs { + #[command(subcommand)] + pub command: WorkflowCommands, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum WorkflowCommands { + #[command(about = "Create a GitHub Actions workflow for todo-tree-action")] + Init(WorkflowInitArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct WorkflowInitArgs { + #[arg(short, long, help = "Overwrite the workflow file if it exists")] + pub force: bool, + + #[arg( + long, + value_hint = ValueHint::FilePath, + help = "Path to the workflow file" + )] + pub path: Option, + + #[arg( + long, + help = "GitHub Action reference to use in the generated workflow" + )] + pub action: Option, +} + #[derive(Args, Debug, Clone)] pub struct StatsArgs { #[arg(value_hint = ValueHint::AnyPath, help = "Path to scan (defaults to current directory)")] diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index dc00ef6..92d78eb 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -8,6 +8,7 @@ pub mod list; pub mod scan; pub mod stats; pub mod tags; +pub mod workflow; pub(crate) fn load_config(path: &Path, config_path: Option<&Path>) -> Result { if let Some(config_path) = config_path { diff --git a/cli/src/commands/workflow.rs b/cli/src/commands/workflow.rs new file mode 100644 index 0000000..26b0684 --- /dev/null +++ b/cli/src/commands/workflow.rs @@ -0,0 +1,109 @@ +use crate::cli::{WorkflowArgs, WorkflowCommands, WorkflowInitArgs}; +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; + +const DEFAULT_WORKFLOW_PATH: &str = ".github/workflows/todo-tree.yml"; +const ACTION_VERSION: &str = "v1.0.3"; + +pub fn run(args: WorkflowArgs) -> Result<()> { + match args.command { + WorkflowCommands::Init(args) => init(args), + } +} + +fn init(args: WorkflowInitArgs) -> Result<()> { + let action = args.action.unwrap_or_else(default_action_ref); + let path = args + .path + .unwrap_or_else(|| PathBuf::from(DEFAULT_WORKFLOW_PATH)); + + validate_action_ref(&action)?; + write_workflow_template(&path, args.force, &action)?; + + println!("Created workflow file: {}", path.display()); + println!("The workflow will run on pull requests using {action}."); + + Ok(()) +} + +fn default_action_ref() -> String { + format!("alexandretrotel/todo-tree-action@{ACTION_VERSION}") +} + +fn validate_action_ref(action: &str) -> Result<()> { + let Some((repo, reference)) = action.split_once('@') else { + anyhow::bail!( + "Invalid action reference {:?}. Expected format: owner/repo@ref", + action + ); + }; + + let mut repo_parts = repo.split('/'); + let owner = repo_parts.next().unwrap_or_default(); + let name = repo_parts.next().unwrap_or_default(); + + if owner.is_empty() + || name.is_empty() + || reference.is_empty() + || repo_parts.next().is_some() + || action.contains('\n') + || action.contains('\r') + || action.contains(' ') + || action.contains('\t') + || reference.contains('@') + { + anyhow::bail!( + "Invalid action reference {:?}. Expected format: owner/repo@ref", + action + ); + } + + Ok(()) +} + +fn workflow_template(action: &str) -> String { + format!( + r#"name: todo-tree + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + todo-tree: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Scan TODOs + uses: {action} + with: + github-token: ${{{{ secrets.GITHUB_TOKEN }}}} + changed-only: true + new-only: true +"# + ) +} + +fn write_workflow_template(path: &Path, force: bool, action: &str) -> Result<()> { + if path.exists() && !force { + anyhow::bail!( + "Workflow file {} already exists. Use --force to overwrite.", + path.display() + ); + } + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory: {}", parent.display()))?; + } + + std::fs::write(path, workflow_template(action)) + .with_context(|| format!("Failed to write workflow file: {}", path.display()))?; + + Ok(()) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 6c32c26..624db02 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,7 +9,7 @@ pub mod utils; use anyhow::Result; use clap::Parser; use cli::{Cli, Commands}; -use commands::{init, list, scan, stats, tags as cli_tags}; +use commands::{init, list, scan, stats, tags as cli_tags, workflow}; pub use todo_tree_core::{Priority, ScanResult, ScanSummary, TodoItem}; pub fn run() -> Result<()> { @@ -24,6 +24,7 @@ pub fn run() -> Result<()> { Commands::List(args) => list::run(args, &cli.global), Commands::Tags(args) => cli_tags::run(args, &cli.global), Commands::Init(args) => init::run(args), + Commands::Workflow(args) => workflow::run(args), Commands::Stats(args) => stats::run(args, &cli.global), } } diff --git a/core/Cargo.toml b/core/Cargo.toml index 01cba1c..1e5b214 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "todo-tree-core" -version = "0.4.2" +version = "0.5.0" edition.workspace = true authors.workspace = true license-file.workspace = true