Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ tt tags

# Show statistics
tt stats

# Create a GitHub Actions workflow
tt workflow init
```

## Configuration
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -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<PathBuf>,

#[arg(
long,
help = "GitHub Action reference to use in the generated workflow"
)]
pub action: Option<String>,
}

#[derive(Args, Debug, Clone)]
pub struct StatsArgs {
#[arg(value_hint = ValueHint::AnyPath, help = "Path to scan (defaults to current directory)")]
Expand Down
1 change: 1 addition & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config> {
if let Some(config_path) = config_path {
Expand Down
109 changes: 109 additions & 0 deletions cli/src/commands/workflow.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Comment thread
atrtde marked this conversation as resolved.

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(())
Comment thread
atrtde marked this conversation as resolved.
}

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
Comment thread
atrtde marked this conversation as resolved.

- 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(())
}
3 changes: 2 additions & 1 deletion cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -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),
}
}
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading