tasktree 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tasktree/cli.py CHANGED
@@ -1,8 +1,5 @@
1
- """Command-line interface for Task Tree."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
- import sys
6
3
  from pathlib import Path
7
4
  from typing import Any, List, Optional
8
5
 
@@ -124,23 +121,24 @@ def _init_recipe():
124
121
 
125
122
  # Example task definitions:
126
123
 
127
- # build:
128
- # desc: Compile the application
129
- # outputs: [target/release/bin]
130
- # cmd: cargo build --release
131
-
132
- # test:
133
- # desc: Run tests
134
- # deps: [build]
135
- # cmd: cargo test
136
-
137
- # deploy:
138
- # desc: Deploy to environment
139
- # deps: [build]
140
- # args: [environment, region=eu-west-1]
141
- # cmd: |
142
- # echo "Deploying to {{environment}} in {{region}}"
143
- # ./deploy.sh {{environment}} {{region}}
124
+ tasks:
125
+ # build:
126
+ # desc: Compile the application
127
+ # outputs: [target/release/bin]
128
+ # cmd: cargo build --release
129
+
130
+ # test:
131
+ # desc: Run tests
132
+ # deps: [build]
133
+ # cmd: cargo test
134
+
135
+ # deploy:
136
+ # desc: Deploy to environment
137
+ # deps: [build]
138
+ # args: [environment, region=eu-west-1]
139
+ # cmd: |
140
+ # echo "Deploying to {{environment}} in {{region}}"
141
+ # ./deploy.sh {{environment}} {{region}}
144
142
 
145
143
  # Uncomment and modify the examples above to define your tasks
146
144
  """
@@ -168,22 +166,20 @@ def main(
168
166
  is_eager=True,
169
167
  help="Show version and exit",
170
168
  ),
171
- list_tasks: Optional[bool] = typer.Option(
172
- None, "--list", "-l", help="List all available tasks"
173
- ),
174
- show: Optional[str] = typer.Option(None, "--show", help="Show task definition"),
175
- tree: Optional[str] = typer.Option(None, "--tree", help="Show dependency tree"),
169
+ list_opt: Optional[bool] = typer.Option(None, "--list", "-l", help="List all available tasks"),
170
+ show: Optional[str] = typer.Option(None, "--show", "-s", help="Show task definition"),
171
+ tree: Optional[str] = typer.Option(None, "--tree", "-t", help="Show dependency tree"),
176
172
  init: Optional[bool] = typer.Option(
177
- None, "--init", help="Create a blank tasktree.yaml"
173
+ None, "--init", "-i", help="Create a blank tasktree.yaml"
178
174
  ),
179
175
  clean: Optional[bool] = typer.Option(
180
- None, "--clean", help="Remove state file (reset task cache)"
176
+ None, "--clean", "-c", help="Remove state file (reset task cache)"
181
177
  ),
182
178
  clean_state: Optional[bool] = typer.Option(
183
- None, "--clean-state", help="Remove state file (reset task cache)"
179
+ None, "--clean-state", "-C", help="Remove state file (reset task cache)"
184
180
  ),
185
181
  reset: Optional[bool] = typer.Option(
186
- None, "--reset", help="Remove state file (reset task cache)"
182
+ None, "--reset", "-r", help="Remove state file (reset task cache)"
187
183
  ),
188
184
  force: Optional[bool] = typer.Option(
189
185
  None, "--force", "-f", help="Force re-run all tasks (ignore freshness)"
@@ -210,38 +206,32 @@ def main(
210
206
  tt --list # List all tasks
211
207
  tt --tree test # Show dependency tree for 'test'
212
208
  """
213
- # Handle list option
214
- if list_tasks:
209
+
210
+ if list_opt:
215
211
  _list_tasks()
216
212
  raise typer.Exit()
217
213
 
218
- # Handle show option
219
214
  if show:
220
215
  _show_task(show)
221
216
  raise typer.Exit()
222
217
 
223
- # Handle tree option
224
218
  if tree:
225
219
  _show_tree(tree)
226
220
  raise typer.Exit()
227
221
 
228
- # Handle init option
229
222
  if init:
230
223
  _init_recipe()
231
224
  raise typer.Exit()
232
225
 
233
- # Handle clean options (all three aliases)
234
226
  if clean or clean_state or reset:
235
227
  _clean_state()
236
228
  raise typer.Exit()
237
229
 
238
- # Handle task execution
239
230
  if task_args:
240
- # When --only is specified, force execution (--only implies --force)
231
+ # --only implies --force
241
232
  force_execution = force or only or False
242
233
  _execute_dynamic_task(task_args, force=force_execution, only=only or False, env=env)
243
234
  else:
244
- # No arguments - show available tasks
245
235
  recipe = _get_recipe()
246
236
  if recipe is None:
247
237
  console.print("[red]No recipe file found (tasktree.yaml or tt.yaml)[/red]")
@@ -274,7 +264,7 @@ def _clean_state() -> None:
274
264
  console.print(f"[yellow]No state file found at {state_path}[/yellow]")
275
265
 
276
266
 
277
- def _get_recipe() -> Recipe | None:
267
+ def _get_recipe() -> Optional[Recipe]:
278
268
  """Get parsed recipe or None if not found."""
279
269
  recipe_path = find_recipe_file()
280
270
  if recipe_path is None:
@@ -287,15 +277,7 @@ def _get_recipe() -> Recipe | None:
287
277
  raise typer.Exit(1)
288
278
 
289
279
 
290
- def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: str | None = None) -> None:
291
- """Execute a task specified by name with arguments.
292
-
293
- Args:
294
- args: Command line arguments (task name and task arguments)
295
- force: If True, ignore freshness and re-run all tasks
296
- only: If True, run only the specified task without dependencies
297
- env: If provided, override environment for all tasks
298
- """
280
+ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: Optional[str] = None) -> None:
299
281
  if not args:
300
282
  return
301
283
 
@@ -350,31 +332,17 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
350
332
 
351
333
 
352
334
  def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, Any]:
353
- """Parse command line arguments for a task.
354
-
355
- Args:
356
- arg_specs: Argument specifications from task definition
357
- arg_values: Command line argument values
358
-
359
- Returns:
360
- Dictionary of argument names to values
361
-
362
- Raises:
363
- typer.Exit: If arguments are invalid
364
- """
365
335
  if not arg_specs:
366
336
  if arg_values:
367
337
  console.print(f"[red]Task does not accept arguments[/red]")
368
338
  raise typer.Exit(1)
369
339
  return {}
370
340
 
371
- # Parse argument specifications
372
341
  parsed_specs = []
373
342
  for spec in arg_specs:
374
343
  name, arg_type, default = parse_arg_spec(spec)
375
344
  parsed_specs.append((name, arg_type, default))
376
345
 
377
- # Build argument dictionary
378
346
  args_dict = {}
379
347
  positional_index = 0
380
348
 
@@ -424,14 +392,6 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
424
392
 
425
393
 
426
394
  def _build_rich_tree(dep_tree: dict) -> Tree:
427
- """Build a Rich Tree from dependency tree structure.
428
-
429
- Args:
430
- dep_tree: Dependency tree structure
431
-
432
- Returns:
433
- Rich Tree for display
434
- """
435
395
  task_name = dep_tree["name"]
436
396
  tree = Tree(task_name)
437
397
 
tasktree/hasher.py CHANGED
@@ -1,77 +1,27 @@
1
- """Hashing logic for tasks and arguments."""
2
-
3
1
  import hashlib
4
2
  import json
5
- from typing import Any
3
+ from typing import Any, Optional
6
4
 
7
5
 
8
6
  def hash_task(cmd: str, outputs: list[str], working_dir: str, args: list[str], env: str = "") -> str:
9
- """Compute task definition hash.
10
-
11
- The hash includes:
12
- - cmd: The command to execute
13
- - outputs: Declared output files
14
- - working_dir: Execution directory
15
- - args: Parameter definitions (names and types)
16
- - env: Environment name (effective environment after resolution)
17
-
18
- The hash excludes:
19
- - deps: Only affects scheduling order
20
- - inputs: Tracked separately via timestamps
21
- - desc: Documentation only
22
-
23
- Args:
24
- cmd: Command to execute
25
- outputs: List of output glob patterns
26
- working_dir: Working directory for execution
27
- args: List of argument definitions
28
- env: Environment name (empty string for platform default)
29
-
30
- Returns:
31
- 8-character hex hash string
32
- """
33
- # Create a stable representation
34
7
  data = {
35
8
  "cmd": cmd,
36
- "outputs": sorted(outputs), # Sort for stability
9
+ "outputs": sorted(outputs),
37
10
  "working_dir": working_dir,
38
- "args": sorted(args), # Sort for stability
39
- "env": env, # Include effective environment
11
+ "args": sorted(args),
12
+ "env": env,
40
13
  }
41
14
 
42
- # Serialize to JSON with sorted keys for deterministic hashing
43
15
  serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
44
-
45
- # Compute hash and truncate to 8 characters
46
16
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
47
17
 
48
18
 
49
19
  def hash_args(args_dict: dict[str, Any]) -> str:
50
- """Compute hash of task arguments.
51
-
52
- Args:
53
- args_dict: Dictionary of argument names to values
54
-
55
- Returns:
56
- 8-character hex hash string
57
- """
58
- # Serialize arguments to JSON with sorted keys for deterministic hashing
59
20
  serialized = json.dumps(args_dict, sort_keys=True, separators=(",", ":"))
60
-
61
- # Compute hash and truncate to 8 characters
62
21
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
63
22
 
64
23
 
65
- def make_cache_key(task_hash: str, args_hash: str | None = None) -> str:
66
- """Create cache key for task execution.
67
-
68
- Args:
69
- task_hash: Task definition hash
70
- args_hash: Optional arguments hash
71
-
72
- Returns:
73
- Cache key string (task_hash or task_hash__args_hash)
74
- """
24
+ def make_cache_key(task_hash: str, args_hash: Optional[str] = None) -> str:
75
25
  if args_hash:
76
26
  return f"{task_hash}__{args_hash}"
77
27
  return task_hash
tasktree/parser.py CHANGED
@@ -270,7 +270,7 @@ def _parse_file(
270
270
  local_import_namespaces: set[str] = set()
271
271
 
272
272
  # Process nested imports FIRST
273
- imports = data.get("import", [])
273
+ imports = data.get("imports", [])
274
274
  if imports:
275
275
  for import_spec in imports:
276
276
  child_file = import_spec["file"]
@@ -297,15 +297,49 @@ def _parse_file(
297
297
 
298
298
  tasks.update(nested_tasks)
299
299
 
300
- # Determine where tasks are defined
301
- # Tasks can be either at root level OR inside a "tasks:" key
302
- tasks_data = data.get("tasks", data) if "tasks" in data else data
300
+ # Validate top-level keys (only imports, environments, and tasks are allowed)
301
+ VALID_TOP_LEVEL_KEYS = {"imports", "environments", "tasks"}
302
+
303
+ # Check if tasks key is missing when there appear to be task definitions at root
304
+ # Do this BEFORE checking for unknown keys, to provide better error message
305
+ if "tasks" not in data and data:
306
+ # Check if there are potential task definitions at root level
307
+ potential_tasks = [
308
+ k for k, v in data.items()
309
+ if isinstance(v, dict) and k not in VALID_TOP_LEVEL_KEYS
310
+ ]
311
+
312
+ if potential_tasks:
313
+ raise ValueError(
314
+ f"Invalid recipe format in {file_path}\n\n"
315
+ f"Task definitions must be under a top-level 'tasks:' key.\n\n"
316
+ f"Found these keys at root level: {', '.join(potential_tasks)}\n\n"
317
+ f"Did you mean:\n\n"
318
+ f"tasks:\n"
319
+ + '\n'.join(f" {k}:" for k in potential_tasks) +
320
+ "\n cmd: ...\n\n"
321
+ f"Valid top-level keys are: {', '.join(sorted(VALID_TOP_LEVEL_KEYS))}"
322
+ )
323
+
324
+ # Now check for other invalid top-level keys (non-dict values)
325
+ invalid_keys = set(data.keys()) - VALID_TOP_LEVEL_KEYS
326
+ if invalid_keys:
327
+ raise ValueError(
328
+ f"Invalid recipe format in {file_path}\n\n"
329
+ f"Unknown top-level keys: {', '.join(sorted(invalid_keys))}\n\n"
330
+ f"Valid top-level keys are:\n"
331
+ f" - imports (for importing task files)\n"
332
+ f" - environments (for shell environment configuration)\n"
333
+ f" - tasks (for task definitions)"
334
+ )
335
+
336
+ # Extract tasks from "tasks" key
337
+ tasks_data = data.get("tasks", {})
338
+ if tasks_data is None:
339
+ tasks_data = {}
303
340
 
304
341
  # Process local tasks
305
342
  for task_name, task_data in tasks_data.items():
306
- # Skip special sections (only relevant if tasks are at root level)
307
- if task_name in ("import", "environments", "tasks"):
308
- continue
309
343
 
310
344
  if not isinstance(task_data, dict):
311
345
  raise ValueError(f"Task '{task_name}' must be a dictionary")
tasktree/types.py CHANGED
@@ -4,7 +4,7 @@ import re
4
4
  from datetime import datetime
5
5
  from ipaddress import IPv4Address, IPv6Address, ip_address
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Optional
8
8
 
9
9
  import click
10
10
 
@@ -19,7 +19,7 @@ class HostnameType(click.ParamType):
19
19
  r"^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.?$"
20
20
  )
21
21
 
22
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
22
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
23
23
  if isinstance(value, str):
24
24
  if self.HOSTNAME_PATTERN.match(value):
25
25
  return value
@@ -36,7 +36,7 @@ class EmailType(click.ParamType):
36
36
  r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
37
37
  )
38
38
 
39
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
39
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
40
40
  if isinstance(value, str):
41
41
  if self.EMAIL_PATTERN.match(value):
42
42
  return value
@@ -48,7 +48,7 @@ class IPType(click.ParamType):
48
48
 
49
49
  name = "ip"
50
50
 
51
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
51
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
52
52
  try:
53
53
  ip_address(value)
54
54
  return str(value)
@@ -61,7 +61,7 @@ class IPv4Type(click.ParamType):
61
61
 
62
62
  name = "ipv4"
63
63
 
64
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
64
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
65
65
  try:
66
66
  IPv4Address(value)
67
67
  return str(value)
@@ -74,7 +74,7 @@ class IPv6Type(click.ParamType):
74
74
 
75
75
  name = "ipv6"
76
76
 
77
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
77
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
78
78
  try:
79
79
  IPv6Address(value)
80
80
  return str(value)
@@ -87,7 +87,7 @@ class DateTimeType(click.ParamType):
87
87
 
88
88
  name = "datetime"
89
89
 
90
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
90
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
91
91
  if isinstance(value, str):
92
92
  try:
93
93
  datetime.fromisoformat(value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -18,6 +18,142 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  A task automation tool that combines simple command execution with dependency tracking and incremental execution.
20
20
 
21
+ ## Motivation
22
+ In any project of even moderate size, various scripts inevitably come into being along the way. These scripts often must be run in a particular order, or at a particular time. For historical reasons, this almost certainly a problem if your project is developed in a Linux environment; in Windows, an IDE like Visual Studio may be taking care of a significant proportion of your build, packaging and deployment tasks. Then again, it may not...
23
+
24
+ The various incantations that have to be issued to build, package, test and deploy a project can build up and then all of a sudden there's only a few people that remember which to invoke and when and then people start making helpful readme guides on what to do with the scripts and then those become out of date and start telling lies about things and so on.
25
+
26
+ Then there's the scripts themselves. In Linux, they're probably a big pile of Bash and Python, or something (Ruby, Perl, you name it). You can bet the house on people solving the problem of passing parameters to their scripts in a whole bunch of different and inconsistent ways.
27
+
28
+ ```bash
29
+ #!/usr/bin/env bash
30
+ # It's an environment variable defined.... somewhere?
31
+ echo "FOO is: $FOO"
32
+ ```
33
+ ```bash
34
+ #!/usr/bin/env bash
35
+ # Using simple positional arguments... guess what means what when you're invoking it!
36
+ echo "First: $1, Second: $2"
37
+ ```
38
+ ```bash
39
+ #!/usr/bin/env bash
40
+ # Oooooh fancy "make me look like a proper app" named option parsing... don't try and do --foo=bar though!
41
+ FOO=""
42
+ while [[ $# -gt 0 ]]; do
43
+ case "$1" in
44
+ --foo) FOO=$2; shift ;;
45
+ --) break ;;
46
+ *) echo "Unknown: $1";;
47
+ esac
48
+ shift
49
+ done
50
+ ```
51
+ ```bash
52
+ #!/usr/bin/env bash
53
+ # This thing...
54
+ ARGS=$(getopt -o f:b --long foo:,bar: -n 'myscript' -- "$@")
55
+ eval set -- "$ARGS"
56
+ while true; do
57
+ case "$1" in
58
+ -b|--bar) echo "Bar: $2"; shift 2 ;;
59
+ -f|--foo) echo "Foo: $2"; shift 2 ;;
60
+ --) shift; break ;;
61
+ *) break ;;
62
+ esac
63
+ done
64
+ ```
65
+
66
+ What about help info? Who has time to wire that in?
67
+
68
+ ### The point
69
+ Is this just whining and moaning? Should we just man up and revel in our own ability to memorize all the right incantations like some kind of scripting shaman?
70
+
71
+ ... No. That's **a dumb idea**.
72
+
73
+ Task Tree allows you to pile all the knowledge of **what** to run, **when** to run it, **where** to run it and **how** to run it into a single, readable place. Then you can delete all the scripts that no-one knows how to use and all the readme docs that lie to the few people that actually waste their time reading them.
74
+
75
+ The tasks you need to perform to deliver your project become summarised in an executable file that looks like:
76
+ ```yaml
77
+ tasks:
78
+ build:
79
+ desc: Compile stuff
80
+ outputs: [target/release/bin]
81
+ cmd: cargo build --release
82
+
83
+ package:
84
+ desc: build installers
85
+ deps: [build]
86
+ outputs: [awesome.deb]
87
+ cmd: |
88
+ for bin in target/release/*; do
89
+ if [[ -x "$bin" && ! -d "$bin" ]]; then
90
+ install -Dm755 "$bin" "debian/awesome/usr/bin/$(basename "$bin")"
91
+ fi
92
+ done
93
+
94
+ dpkg-buildpackage -us -uc
95
+
96
+ test:
97
+ desc: Run tests
98
+ deps: [package]
99
+ inputs: [tests/**/*.py]
100
+ cmd: PYTHONPATH=src python3 -m pytest tests/ -v
101
+ ```
102
+
103
+ If you want to run the tests then:
104
+ ```bash
105
+ tt test
106
+ ```
107
+ Boom! Done. `build` will always run, because there's no sensible way to know what Cargo did. However, if Cargo decided that nothing needed to be done and didn't touch the binaries, then `package` will realize that and not do anything. Then `test` will just run with the new tests that you just wrote. If you then immediately run `test` again, then `test` will figure out that none of the dependencies did anything and that none of the test files have changed and then just _do nothing_ - as it should.
108
+
109
+ This is a toy example, but you can image how it plays out on a more complex project.
110
+
111
+ ## Migrating from v1.x to v2.0
112
+
113
+ Version 2.0 requires all task definitions to be under a top-level `tasks:` key.
114
+
115
+ ### Quick Migration
116
+
117
+ Wrap your existing tasks in a `tasks:` block:
118
+
119
+ ```yaml
120
+ # Before (v1.x)
121
+ build:
122
+ cmd: cargo build
123
+
124
+ # After (v2.0)
125
+ tasks:
126
+ build:
127
+ cmd: cargo build
128
+ ```
129
+
130
+ ### Why This Change?
131
+
132
+ 1. **Clearer structure**: Explicit separation of tasks from configuration
133
+ 2. **No naming conflicts**: You can now create tasks named "imports" or "environments"
134
+ 3. **Better error messages**: More helpful validation errors
135
+ 4. **Consistency**: All recipe files use the same format
136
+
137
+ ### Error Messages
138
+
139
+ If you forget to update, you'll see a clear error:
140
+
141
+ ```
142
+ Invalid recipe format in tasktree.yaml
143
+
144
+ Task definitions must be under a top-level "tasks:" key.
145
+
146
+ Found these keys at root level: build, test
147
+
148
+ Did you mean:
149
+
150
+ tasks:
151
+ build:
152
+ cmd: ...
153
+ test:
154
+ cmd: ...
155
+ ```
156
+
21
157
  ## Installation
22
158
 
23
159
  ### From PyPI (Recommended)
@@ -26,6 +162,16 @@ A task automation tool that combines simple command execution with dependency tr
26
162
  pipx install tasktree
27
163
  ```
28
164
 
165
+ If you have multiple Python interpreter versions installed, and the _default_ interpreter is a version <3.11, then you can use `pipx`'s `--python` option to specify an interpreter with a version >=3.11:
166
+
167
+ ```bash
168
+ # If the target version is on the PATH
169
+ pipx install --python python3.12 tasktree
170
+
171
+ # With a path to an interpreter
172
+ pipx install --python /path/to/python3.12 tasktree
173
+ ```
174
+
29
175
  ### From Source
30
176
 
31
177
  For the latest unreleased version from GitHub:
@@ -47,23 +193,26 @@ pipx install .
47
193
  Create a `tasktree.yaml` (or `tt.yaml`) in your project:
48
194
 
49
195
  ```yaml
50
- build:
51
- desc: Compile the application
52
- outputs: [target/release/bin]
53
- cmd: cargo build --release
196
+ tasks:
197
+ build:
198
+ desc: Compile the application
199
+ outputs: [target/release/bin]
200
+ cmd: cargo build --release
54
201
 
55
- test:
56
- desc: Run tests
57
- deps: [build]
58
- cmd: cargo test
202
+ test:
203
+ desc: Run tests
204
+ deps: [build]
205
+ cmd: cargo test
59
206
  ```
60
207
 
61
208
  Run tasks:
62
209
 
63
210
  ```bash
64
- tt build # Build the application
65
- tt test # Run tests (builds first if needed)
211
+ tt # Print the help
212
+ tt --help # ...also print the help
66
213
  tt --list # Show all available tasks
214
+ tt build # Build the application (assuming this is in your tasktree.yaml)
215
+ tt test # Run tests (builds first if needed)
67
216
  ```
68
217
 
69
218
  ## Core Concepts
@@ -84,15 +233,16 @@ Task Tree only runs tasks when necessary. A task executes if:
84
233
  Tasks automatically inherit inputs from dependencies, eliminating redundant declarations:
85
234
 
86
235
  ```yaml
87
- build:
88
- outputs: [dist/app]
89
- cmd: go build -o dist/app
90
-
91
- package:
92
- deps: [build]
93
- outputs: [dist/app.tar.gz]
94
- cmd: tar czf dist/app.tar.gz dist/app
95
- # Automatically tracks dist/app as an input
236
+ tasks:
237
+ build:
238
+ outputs: [dist/app]
239
+ cmd: go build -o dist/app
240
+
241
+ package:
242
+ deps: [build]
243
+ outputs: [dist/app.tar.gz]
244
+ cmd: tar czf dist/app.tar.gz dist/app
245
+ # Automatically tracks dist/app as an input
96
246
  ```
97
247
 
98
248
  ### Single State File
@@ -104,15 +254,16 @@ All state lives in `.tasktree-state` at your project root. Stale entries are aut
104
254
  ### Basic Structure
105
255
 
106
256
  ```yaml
107
- task-name:
108
- desc: Human-readable description (optional)
109
- deps: [other-task] # Task dependencies
110
- inputs: [src/**/*.go] # Explicit input files (glob patterns)
111
- outputs: [dist/binary] # Output files (glob patterns)
112
- working_dir: subproject/ # Execution directory (default: project root)
113
- env: bash-strict # Execution environment (optional)
114
- args: [param1, param2:path=default] # Task parameters
115
- cmd: go build -o dist/binary # Command to execute
257
+ tasks:
258
+ task-name:
259
+ desc: Human-readable description (optional)
260
+ deps: [other-task] # Task dependencies
261
+ inputs: [src/**/*.go] # Explicit input files (glob patterns)
262
+ outputs: [dist/binary] # Output files (glob patterns)
263
+ working_dir: subproject/ # Execution directory (default: project root)
264
+ env: bash-strict # Execution environment (optional)
265
+ args: [param1, param2:path=default] # Task parameters
266
+ cmd: go build -o dist/binary # Command to execute
116
267
  ```
117
268
 
118
269
  ### Commands
@@ -120,18 +271,20 @@ task-name:
120
271
  **Single-line commands** are executed directly via the configured shell:
121
272
 
122
273
  ```yaml
123
- build:
124
- cmd: cargo build --release
274
+ tasks:
275
+ build:
276
+ cmd: cargo build --release
125
277
  ```
126
278
 
127
279
  **Multi-line commands** are written to temporary script files for proper execution:
128
280
 
129
281
  ```yaml
130
- deploy:
131
- cmd: |
132
- mkdir -p dist
133
- cp build/* dist/
134
- rsync -av dist/ server:/opt/app/
282
+ tasks:
283
+ deploy:
284
+ cmd: |
285
+ mkdir -p dist
286
+ cp build/* dist/
287
+ rsync -av dist/ server:/opt/app/
135
288
  ```
136
289
 
137
290
  Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) and support shebangs on Unix/macOS.
@@ -139,12 +292,13 @@ Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) a
139
292
  Or use folded blocks for long single-line commands:
140
293
 
141
294
  ```yaml
142
- compile:
143
- cmd: >
144
- gcc -o bin/app
145
- src/*.c
146
- -I include
147
- -L lib -lm
295
+ tasks:
296
+ compile:
297
+ cmd: >
298
+ gcc -o bin/app
299
+ src/*.c
300
+ -I include
301
+ -L lib -lm
148
302
  ```
149
303
 
150
304
  ### Execution Environments
@@ -204,12 +358,13 @@ tasks:
204
358
  Tasks can accept arguments with optional defaults:
205
359
 
206
360
  ```yaml
207
- deploy:
208
- args: [environment, region=eu-west-1]
209
- deps: [build]
210
- cmd: |
211
- aws s3 cp dist/app.zip s3://{{environment}}-{{region}}/
212
- aws lambda update-function-code --function-name app-{{environment}}
361
+ tasks:
362
+ deploy:
363
+ args: [environment, region=eu-west-1]
364
+ deps: [build]
365
+ cmd: |
366
+ aws s3 cp dist/app.zip s3://{{environment}}-{{region}}/
367
+ aws lambda update-function-code --function-name app-{{environment}}
213
368
  ```
214
369
 
215
370
  Invoke with: `tt deploy production` or `tt deploy staging us-east-1` or `tt deploy staging region=us-east-1`.
@@ -236,18 +391,19 @@ Split task definitions across multiple files for better organisation:
236
391
 
237
392
  ```yaml
238
393
  # tasktree.yaml
239
- import:
394
+ imports:
240
395
  - file: build/tasks.yml
241
396
  as: build
242
397
  - file: deploy/tasks.yml
243
398
  as: deploy
244
399
 
245
- test:
246
- deps: [build.compile, build.test-compile]
247
- cmd: ./run-tests.sh
400
+ tasks:
401
+ test:
402
+ deps: [build.compile, build.test-compile]
403
+ cmd: ./run-tests.sh
248
404
 
249
- ci:
250
- deps: [build.all, test, deploy.staging]
405
+ ci:
406
+ deps: [build.all, test, deploy.staging]
251
407
  ```
252
408
 
253
409
  Imported tasks are namespaced and can be referenced as dependencies. Each imported file is self-contained—it cannot depend on tasks in the importing file.
@@ -365,41 +521,42 @@ imports:
365
521
  - file: common/docker.yml
366
522
  as: docker
367
523
 
368
- compile:
369
- desc: Build application binaries
370
- outputs: [target/release/app]
371
- cmd: cargo build --release
372
-
373
- test-unit:
374
- desc: Run unit tests
375
- deps: [compile]
376
- cmd: cargo test
377
-
378
- package:
379
- desc: Create distribution archive
380
- deps: [compile]
381
- outputs: [dist/app-{{version}}.tar.gz]
382
- args: [version]
383
- cmd: |
384
- mkdir -p dist
385
- tar czf dist/app-{{version}}.tar.gz \
386
- target/release/app \
387
- config/ \
388
- migrations/
389
-
390
- deploy:
391
- desc: Deploy to environment
392
- deps: [package, docker.build-runtime]
393
- args: [environment, version]
394
- cmd: |
395
- scp dist/app-{{version}}.tar.gz {{environment}}:/opt/
396
- ssh {{environment}} /opt/deploy.sh {{version}}
397
-
398
- integration-test:
399
- desc: Run integration tests against deployed environment
400
- deps: [deploy]
401
- args: [environment, version]
402
- cmd: pytest tests/integration/ --env={{environment}}
524
+ tasks:
525
+ compile:
526
+ desc: Build application binaries
527
+ outputs: [target/release/app]
528
+ cmd: cargo build --release
529
+
530
+ test-unit:
531
+ desc: Run unit tests
532
+ deps: [compile]
533
+ cmd: cargo test
534
+
535
+ package:
536
+ desc: Create distribution archive
537
+ deps: [compile]
538
+ outputs: [dist/app-{{version}}.tar.gz]
539
+ args: [version]
540
+ cmd: |
541
+ mkdir -p dist
542
+ tar czf dist/app-{{version}}.tar.gz \
543
+ target/release/app \
544
+ config/ \
545
+ migrations/
546
+
547
+ deploy:
548
+ desc: Deploy to environment
549
+ deps: [package, docker.build-runtime]
550
+ args: [environment, version]
551
+ cmd: |
552
+ scp dist/app-{{version}}.tar.gz {{environment}}:/opt/
553
+ ssh {{environment}} /opt/deploy.sh {{version}}
554
+
555
+ integration-test:
556
+ desc: Run integration tests against deployed environment
557
+ deps: [deploy]
558
+ args: [environment, version]
559
+ cmd: pytest tests/integration/ --env={{environment}}
403
560
  ```
404
561
 
405
562
  Run the full pipeline:
@@ -0,0 +1,13 @@
1
+ tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
+ tasktree/cli.py,sha256=jeLFmyFjTeF6JXDPLI46H5Xao9br68tPWL7D6LbC7u0,13272
3
+ tasktree/executor.py,sha256=_E37tShHuiOj0Mvx2GbS9y3GIozC3hpzAVhAjbvYJqg,18638
4
+ tasktree/graph.py,sha256=9ngfg93y7EkOIN_lUQa0u-JhnwiMN1UdQQvIFw8RYCE,4181
5
+ tasktree/hasher.py,sha256=puJey9wF_p37k_xqjhYr_6ICsbAfrTBWHec6MqKV4BU,814
6
+ tasktree/parser.py,sha256=5WXoqN2cUtO9icIPDSvK5889XinBcAQMBCyNoJ2ZO6w,14152
7
+ tasktree/state.py,sha256=rxKtS3SbsPtAuraHbN807RGWfoYYkQ3pe8CxUstwo2k,3535
8
+ tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
9
+ tasktree/types.py,sha256=w--sKjRTc8mGYkU5eAduqV86SolDqOYspAPuVKIuSQQ,3797
10
+ tasktree-0.0.5.dist-info/METADATA,sha256=ncZRu7zCKZRd5YGG5fAWAL1TtH1mXbtnTqAne3RHdeA,17456
11
+ tasktree-0.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ tasktree-0.0.5.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
+ tasktree-0.0.5.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=hfxKF4N8nKPvy3WwyQgENHNH8YbUD3Zd7DLHGuHdeHU,14400
3
- tasktree/executor.py,sha256=_E37tShHuiOj0Mvx2GbS9y3GIozC3hpzAVhAjbvYJqg,18638
4
- tasktree/graph.py,sha256=9ngfg93y7EkOIN_lUQa0u-JhnwiMN1UdQQvIFw8RYCE,4181
5
- tasktree/hasher.py,sha256=xOeth3vufP-QrjnVgTDh02clkZb867aJPD6HSjhMNsg,2336
6
- tasktree/parser.py,sha256=apLfN3_YVa7lQIy0rcHFwo931Pg-8mKG1044AQE6fLQ,12690
7
- tasktree/state.py,sha256=rxKtS3SbsPtAuraHbN807RGWfoYYkQ3pe8CxUstwo2k,3535
8
- tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
9
- tasktree/types.py,sha256=wrBzO-Z2ebCTRjWyOWNvuCjqAq-74Zyb9E4FQ4beF38,3751
10
- tasktree-0.0.3.dist-info/METADATA,sha256=qlpV-egfSY502xJKLmmam1CTUVpVEiU4oS10LntL7gE,11961
11
- tasktree-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tasktree-0.0.3.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
- tasktree-0.0.3.dist-info/RECORD,,