Repository: go-task/task Branch: main Commit: 19d8fae5f95a Files: 715 Total size: 1.0 MB Directory structure: gitextract_c5f3ar5r/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── pull_request_template.md │ ├── renovate.json │ └── workflows/ │ ├── issue-awaiting-response.yml │ ├── issue-closed.yml │ ├── issue-experiment.yml │ ├── issue-needs-triage.yml │ ├── lint.yml │ ├── pr-build.yml │ ├── release-nightly.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser-nightly.yml ├── .goreleaser-pr.yml ├── .goreleaser.yml ├── .mockery.yaml ├── .prettierrc.yml ├── .taskrc.yml ├── .vscode/ │ ├── extensions.json │ └── settings-sample.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yml ├── args/ │ ├── args.go │ └── args_test.go ├── bin/ │ └── .keep ├── call.go ├── cmd/ │ ├── release/ │ │ └── main.go │ ├── sleepit/ │ │ └── sleepit.go │ └── task/ │ └── task.go ├── compiler.go ├── completion/ │ ├── bash/ │ │ └── task.bash │ ├── fish/ │ │ └── task.fish │ ├── ps/ │ │ └── task.ps1 │ └── zsh/ │ └── _task ├── completion.go ├── concurrency.go ├── errors/ │ ├── error_taskfile_decode.go │ ├── errors.go │ ├── errors_task.go │ ├── errors_taskfile.go │ └── errors_taskrc.go ├── executor.go ├── executor_test.go ├── experiments/ │ ├── errors.go │ ├── experiment.go │ ├── experiment_test.go │ └── experiments.go ├── formatter_test.go ├── go.mod ├── go.sum ├── hash.go ├── help.go ├── init.go ├── init_test.go ├── install-task.sh ├── internal/ │ ├── deepcopy/ │ │ └── deepcopy.go │ ├── editors/ │ │ └── output.go │ ├── env/ │ │ └── env.go │ ├── execext/ │ │ ├── coreutils.go │ │ ├── devnull.go │ │ └── exec.go │ ├── filepathext/ │ │ └── filepathext.go │ ├── fingerprint/ │ │ ├── checker.go │ │ ├── checker_mock.go │ │ ├── glob.go │ │ ├── sources.go │ │ ├── sources_checksum.go │ │ ├── sources_checksum_test.go │ │ ├── sources_none.go │ │ ├── sources_timestamp.go │ │ ├── status.go │ │ ├── task.go │ │ └── task_test.go │ ├── flags/ │ │ └── flags.go │ ├── fsext/ │ │ ├── fs.go │ │ ├── fs_test.go │ │ └── testdata/ │ │ ├── bar.txt │ │ └── foo.txt │ ├── fsnotifyext/ │ │ └── fsnotify_dedup.go │ ├── goext/ │ │ └── meta.go │ ├── hash/ │ │ └── hash.go │ ├── input/ │ │ └── input.go │ ├── logger/ │ │ └── logger.go │ ├── output/ │ │ ├── group.go │ │ ├── interleaved.go │ │ ├── output.go │ │ ├── output_test.go │ │ └── prefixed.go │ ├── slicesext/ │ │ ├── slicesext.go │ │ └── slicesext_test.go │ ├── sort/ │ │ ├── sorter.go │ │ └── sorter_test.go │ ├── summary/ │ │ ├── summary.go │ │ └── summary_test.go │ ├── sysinfo/ │ │ ├── uid.go │ │ └── uid_win.go │ ├── templater/ │ │ ├── funcs.go │ │ └── templater.go │ ├── term/ │ │ └── term.go │ └── version/ │ ├── version.go │ └── version.txt ├── precondition.go ├── requires.go ├── setup.go ├── signals.go ├── signals_test.go ├── status.go ├── task.go ├── task_test.go ├── taskfile/ │ ├── ast/ │ │ ├── cmd.go │ │ ├── defer.go │ │ ├── dep.go │ │ ├── for.go │ │ ├── glob.go │ │ ├── graph.go │ │ ├── include.go │ │ ├── location.go │ │ ├── matrix.go │ │ ├── output.go │ │ ├── platforms.go │ │ ├── platforms_test.go │ │ ├── precondition.go │ │ ├── precondition_test.go │ │ ├── prompt.go │ │ ├── requires.go │ │ ├── task.go │ │ ├── taskfile.go │ │ ├── taskfile_test.go │ │ ├── tasks.go │ │ ├── var.go │ │ └── vars.go │ ├── dotenv.go │ ├── node.go │ ├── node_base.go │ ├── node_cache.go │ ├── node_file.go │ ├── node_git.go │ ├── node_git_test.go │ ├── node_http.go │ ├── node_http_test.go │ ├── node_stdin.go │ ├── node_test.go │ ├── reader.go │ ├── snippet.go │ ├── snippet_test.go │ ├── taskfile.go │ ├── templates/ │ │ └── default.yml │ └── themes/ │ └── task.xml ├── taskrc/ │ ├── ast/ │ │ └── taskrc.go │ ├── node.go │ ├── reader.go │ ├── taskrc.go │ └── taskrc_test.go ├── testdata/ │ ├── alias/ │ │ ├── Taskfile.yml │ │ ├── Taskfile2.yml │ │ └── testdata/ │ │ ├── TestAlias-alias.golden │ │ ├── TestAlias-alias_summary.golden │ │ ├── TestAlias-duplicate_alias-err-run.golden │ │ └── TestAlias-duplicate_alias.golden │ ├── checksum/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ ├── generated-wildcard.txt │ │ ├── ignore_me.txt │ │ └── source.txt │ ├── cmds_vars/ │ │ ├── Taskfile.yml │ │ └── source.txt │ ├── concurrency/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestConcurrency.golden │ ├── cyclic/ │ │ └── Taskfile.yml │ ├── deferred/ │ │ └── Taskfile.yml │ ├── deps/ │ │ ├── Taskfile.yml │ │ ├── d1.txt │ │ ├── d11.txt │ │ ├── d12.txt │ │ ├── d13.txt │ │ ├── d2.txt │ │ ├── d21.txt │ │ ├── d22.txt │ │ ├── d23.txt │ │ ├── d3.txt │ │ ├── d31.txt │ │ ├── d32.txt │ │ ├── d33.txt │ │ └── testdata/ │ │ └── TestDeps.golden │ ├── desc/ │ │ └── Taskfile.yml │ ├── dir/ │ │ ├── Taskfile.yml │ │ ├── dynamic_var/ │ │ │ ├── .gitignore │ │ │ ├── Taskfile.yml │ │ │ └── subdirectory/ │ │ │ └── Taskfile.yml │ │ ├── dynamic_var_on_created_dir/ │ │ │ └── Taskfile.yml │ │ ├── explicit_doesnt_exist/ │ │ │ └── Taskfile.yml │ │ └── explicit_exists/ │ │ ├── Taskfile.yml │ │ └── exists/ │ │ └── .keep │ ├── dotenv/ │ │ ├── .gitignore │ │ ├── default/ │ │ │ └── Taskfile.yml │ │ ├── env_var_in_path/ │ │ │ └── Taskfile.yml │ │ ├── error_included_envs/ │ │ │ └── Taskfile.yml │ │ ├── include1/ │ │ │ └── Taskfile.yml │ │ ├── local_env_in_path/ │ │ │ └── Taskfile.yml │ │ ├── local_var_in_path/ │ │ │ └── Taskfile.yml │ │ ├── missing_env/ │ │ │ └── Taskfile.yml │ │ └── parse_error/ │ │ ├── .env-with-error │ │ └── Taskfile.yml │ ├── dotenv_task/ │ │ └── default/ │ │ ├── .gitignore │ │ └── Taskfile.yml │ ├── dry/ │ │ └── Taskfile.yml │ ├── dry_checksum/ │ │ ├── Taskfile.yml │ │ └── source.txt │ ├── empty_task/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestEmptyTask.golden │ ├── empty_taskfile/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestEmptyTaskfile-err-setup.golden │ │ └── TestEmptyTaskfile.golden │ ├── env/ │ │ ├── Taskfile.yml │ │ ├── dynamic.txt │ │ ├── global.txt │ │ ├── local.txt │ │ ├── multiple_type.txt │ │ ├── not-overridden.txt │ │ ├── overridden.txt │ │ └── testdata/ │ │ ├── TestEnv-env_precedence_disabled.golden │ │ └── TestEnv-env_precedence_enabled.golden │ ├── error_code/ │ │ └── Taskfile.yml │ ├── evaluate_symlinks_in_paths/ │ │ ├── Taskfile.yaml │ │ ├── shared/ │ │ │ ├── b │ │ │ └── inner_shared/ │ │ │ └── c │ │ └── src/ │ │ └── a │ ├── exit_code/ │ │ └── Taskfile.yml │ ├── exit_immediately/ │ │ └── Taskfile.yml │ ├── expand/ │ │ └── Taskfile.yml │ ├── failfast/ │ │ ├── default/ │ │ │ ├── Taskfile.yaml │ │ │ └── testdata/ │ │ │ ├── TestFailfast-Default-default-err-run.golden │ │ │ ├── TestFailfast-Default-default.golden │ │ │ ├── TestFailfast-Option-default-err-run.golden │ │ │ └── TestFailfast-Option-default.golden │ │ └── task/ │ │ ├── Taskfile.yaml │ │ └── testdata/ │ │ ├── TestFailfast-Task-task-err-run.golden │ │ └── TestFailfast-Task-task.golden │ ├── file_names/ │ │ ├── .gitignore │ │ ├── Taskfile.dist.yaml/ │ │ │ └── Taskfile.dist.yaml │ │ ├── Taskfile.dist.yml/ │ │ │ └── Taskfile.dist.yml │ │ ├── Taskfile.yaml/ │ │ │ └── Taskfile.yaml │ │ └── Taskfile.yml/ │ │ └── Taskfile.yml │ ├── for/ │ │ ├── cmds/ │ │ │ ├── Taskfile.yml │ │ │ ├── bar.txt │ │ │ ├── foo.txt │ │ │ └── testdata/ │ │ │ ├── TestForCmds-loop-different-tasks.golden │ │ │ ├── TestForCmds-loop-explicit.golden │ │ │ ├── TestForCmds-loop-generates-glob.golden │ │ │ ├── TestForCmds-loop-generates.golden │ │ │ ├── TestForCmds-loop-matrix-ref-error-err-run.golden │ │ │ ├── TestForCmds-loop-matrix-ref-error.golden │ │ │ ├── TestForCmds-loop-matrix-ref.golden │ │ │ ├── TestForCmds-loop-matrix.golden │ │ │ ├── TestForCmds-loop-sources-glob.golden │ │ │ ├── TestForCmds-loop-sources.golden │ │ │ ├── TestForCmds-loop-task-as.golden │ │ │ ├── TestForCmds-loop-task.golden │ │ │ ├── TestForCmds-loop-vars-sh.golden │ │ │ └── TestForCmds-loop-vars.golden │ │ └── deps/ │ │ ├── Taskfile.yml │ │ ├── bar.txt │ │ ├── foo.txt │ │ └── testdata/ │ │ ├── TestForDeps-loop-different-tasks.golden │ │ ├── TestForDeps-loop-explicit.golden │ │ ├── TestForDeps-loop-generates-glob.golden │ │ ├── TestForDeps-loop-generates.golden │ │ ├── TestForDeps-loop-matrix-ref-error-err-run.golden │ │ ├── TestForDeps-loop-matrix-ref-error.golden │ │ ├── TestForDeps-loop-matrix-ref.golden │ │ ├── TestForDeps-loop-matrix.golden │ │ ├── TestForDeps-loop-sources-glob.golden │ │ ├── TestForDeps-loop-sources.golden │ │ ├── TestForDeps-loop-task-as.golden │ │ ├── TestForDeps-loop-task.golden │ │ ├── TestForDeps-loop-vars-sh.golden │ │ └── TestForDeps-loop-vars.golden │ ├── force/ │ │ └── Taskfile.yml │ ├── fuzzy/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestFuzzyModel-fuzzy-err-run.golden │ │ ├── TestFuzzyModel-fuzzy.golden │ │ ├── TestFuzzyModel-intern-err-run.golden │ │ ├── TestFuzzyModel-intern.golden │ │ └── TestFuzzyModel-not-fuzzy.golden │ ├── generates/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── sub/ │ │ └── .keep │ ├── if/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestIf-cmd-if-false.golden │ │ ├── TestIf-cmd-if-true.golden │ │ ├── TestIf-if-in-for-loop.golden │ │ ├── TestIf-task-call-if-false.golden │ │ ├── TestIf-task-call-if-true.golden │ │ ├── TestIf-task-if-dynamic-false.golden │ │ ├── TestIf-task-if-dynamic-true.golden │ │ ├── TestIf-task-if-false.golden │ │ ├── TestIf-task-if-true.golden │ │ ├── TestIf-task-level-template-false.golden │ │ ├── TestIf-task-level-template.golden │ │ ├── TestIf-template-and.golden │ │ ├── TestIf-template-bool-false.golden │ │ ├── TestIf-template-bool-true.golden │ │ ├── TestIf-template-cli-var.golden │ │ ├── TestIf-template-direct-false.golden │ │ ├── TestIf-template-direct-true.golden │ │ ├── TestIf-template-eq-false.golden │ │ ├── TestIf-template-eq-true.golden │ │ ├── TestIf-template-ne.golden │ │ └── TestIf-template-or.golden │ ├── ignore_errors/ │ │ └── Taskfile.yml │ ├── ignore_nil_elements/ │ │ ├── cmds/ │ │ │ └── Taskfile.yml │ │ ├── deps/ │ │ │ └── Taskfile.yml │ │ ├── includes/ │ │ │ ├── Taskfile.yml │ │ │ └── inc.yml │ │ └── preconditions/ │ │ └── Taskfile.yml │ ├── ignore_signals/ │ │ └── Taskfile.yml │ ├── include_with_vars/ │ │ ├── Taskfile.yml │ │ └── include/ │ │ ├── Taskfile.include1.yml │ │ ├── Taskfile.include2.yml │ │ └── Taskfile.include3.yml │ ├── include_with_vars_inside_include/ │ │ ├── Taskfile.yml │ │ └── include/ │ │ └── Taskfile.include.yml │ ├── include_with_vars_multi_level/ │ │ ├── Taskfile.yml │ │ ├── bar/ │ │ │ └── Taskfile.yml │ │ ├── foo/ │ │ │ └── Taskfile.yml │ │ └── lib/ │ │ └── Taskfile.yml │ ├── included_taskfile_var_merging/ │ │ ├── Taskfile.yaml │ │ ├── bar/ │ │ │ └── Taskfile.yaml │ │ └── foo/ │ │ └── Taskfile.yaml │ ├── includes/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ ├── Taskfile2.yml │ │ ├── Taskfile_darwin.yml │ │ ├── Taskfile_linux.yml │ │ ├── Taskfile_windows.yml │ │ ├── included/ │ │ │ └── Taskfile.yml │ │ ├── module1/ │ │ │ └── Taskfile.yml │ │ └── module2/ │ │ └── Taskfile.yml │ ├── includes_call_root_task/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── Taskfile2.yml │ ├── includes_checksum/ │ │ ├── correct/ │ │ │ ├── Taskfile.yml │ │ │ └── testdata/ │ │ │ └── TestIncludeChecksum-correct.golden │ │ ├── correct_remote/ │ │ │ └── Taskfile.yml │ │ ├── included.yml │ │ └── incorrect/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestIncludeChecksum-incorrect-err-setup.golden │ │ └── TestIncludeChecksum-incorrect.golden │ ├── includes_cycle/ │ │ ├── Taskfile.yml │ │ └── one/ │ │ ├── Taskfile.yml │ │ └── two/ │ │ └── Taskfile.yml │ ├── includes_deps/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── Taskfile2.yml │ ├── includes_empty/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── Taskfile2.yml │ ├── includes_flatten/ │ │ ├── .gitignore │ │ ├── Taskfile.multiple.yml │ │ ├── Taskfile.with_default.yml │ │ ├── Taskfile.yml │ │ ├── included/ │ │ │ └── Taskfile.yml │ │ └── nested/ │ │ └── Taskfile.yml │ ├── includes_http/ │ │ ├── child-taskfile2.yml │ │ ├── child-taskfile3.yml │ │ ├── root-taskfile-remotefile-empty-dir-1st.yml │ │ └── root-taskfile-remotefile-empty-dir-2nd.yml │ ├── includes_incorrect/ │ │ ├── Taskfile.yml │ │ └── incomplete.yml │ ├── includes_internal/ │ │ ├── Taskfile.yml │ │ └── Taskfile2.yml │ ├── includes_interpolation/ │ │ ├── include/ │ │ │ └── Taskfile.yml │ │ ├── include_with_dir/ │ │ │ └── Taskfile.yml │ │ ├── include_with_env_variable/ │ │ │ └── Taskfile.yml │ │ └── included/ │ │ └── Taskfile.yml │ ├── includes_multi_level/ │ │ ├── Taskfile.yml │ │ ├── called_one.txt │ │ ├── called_three.txt │ │ ├── called_two.txt │ │ └── one/ │ │ ├── Taskfile.yml │ │ └── two/ │ │ ├── Taskfile.yml │ │ └── three/ │ │ └── Taskfile.yml │ ├── includes_optional/ │ │ ├── .gitignore │ │ └── Taskfile.yml │ ├── includes_optional_explicit_false/ │ │ └── Taskfile.yml │ ├── includes_optional_implicit_false/ │ │ └── Taskfile.yml │ ├── includes_rel_path/ │ │ ├── Taskfile.yml │ │ ├── common/ │ │ │ └── Taskfile.yml │ │ └── included/ │ │ └── Taskfile.yml │ ├── includes_remote/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── first/ │ │ ├── Taskfile.yml │ │ └── second/ │ │ └── Taskfile.yml │ ├── includes_shadowed_default/ │ │ ├── Taskfile.yml │ │ ├── Taskfile2.yml │ │ └── file.txt │ ├── includes_silent/ │ │ ├── Taskfile-inc.yml │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestIncludeSilent-include-taskfile-silent.golden │ ├── includes_unshadowed_default/ │ │ ├── Taskfile.yml │ │ ├── Taskfile2.yml │ │ └── file.txt │ ├── includes_with_excludes/ │ │ ├── Taskfile.yml │ │ └── included/ │ │ └── Taskfile.yml │ ├── includes_yaml/ │ │ ├── .gitignore │ │ ├── Custom.ext │ │ └── included/ │ │ ├── Taskfile.yaml │ │ └── custom.yaml │ ├── init/ │ │ └── .gitignore │ ├── interactive_vars/ │ │ ├── .taskrc.yml │ │ └── Taskfile.yml │ ├── internal_task/ │ │ └── Taskfile.yml │ ├── json_list_format/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestJsonListFormat.golden │ ├── label_error/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestLabel-label_in_error-err-run.golden │ │ └── TestLabel-label_in_error.golden │ ├── label_list/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestNoLabelInList.golden │ ├── label_status/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestLabel-status-err-status.golden │ │ └── TestLabel-status.golden │ ├── label_summary/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestLabel-label_in_summary.golden │ │ └── TestLabel-summary.golden │ ├── label_uptodate/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestLabel-up_to_date.golden │ ├── label_var/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestLabel-var.golden │ ├── list_desc_interpolation/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestListDescInterpolation.golden │ ├── list_mixed_desc/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestListAllShowsNoDesc.golden │ │ └── TestListCanListDescOnly.golden │ ├── output_group/ │ │ └── Taskfile.yml │ ├── output_group_error_only/ │ │ └── Taskfile.yml │ ├── params/ │ │ ├── Taskfile.yml │ │ ├── dep1.txt │ │ ├── dep2.txt │ │ ├── exclamation.txt │ │ ├── german.txt │ │ ├── hello.txt │ │ ├── portuguese.txt │ │ ├── portuguese2.txt │ │ ├── spanish-dep.txt │ │ ├── spanish.txt │ │ ├── testdata/ │ │ │ └── TestParams.golden │ │ └── world.txt │ ├── platforms/ │ │ └── Taskfile.yml │ ├── precondition/ │ │ ├── Taskfile.yml │ │ ├── foo.txt │ │ └── testdata/ │ │ ├── TestPrecondition-a_precondition_has_been_met.golden │ │ ├── TestPrecondition-a_precondition_was_not_met-err-run.golden │ │ ├── TestPrecondition-a_precondition_was_not_met.golden │ │ ├── TestPrecondition-precondition_in_cmd_fails_the_task-err-run.golden │ │ ├── TestPrecondition-precondition_in_cmd_fails_the_task.golden │ │ ├── TestPrecondition-precondition_in_dependency_fails_the_task-err-run.golden │ │ └── TestPrecondition-precondition_in_dependency_fails_the_task.golden │ ├── prefix_uptodate/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestPrefix-up_to_dat_with_no_output_style.golden │ │ └── TestPrefix-up_to_date.golden │ ├── prompt/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestPromptAssumeYes---yes_flag_should_skip_prompt.golden │ │ ├── TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError-err-run.golden │ │ ├── TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError.golden │ │ ├── TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task-err-run.golden │ │ ├── TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task.golden │ │ ├── TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task-err-run.golden │ │ ├── TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task.golden │ │ ├── TestPromptInSummary-test_long_approval-test_long_approval.golden │ │ ├── TestPromptInSummary-test_short_approval-test_short_approval.golden │ │ ├── TestPromptInSummary-test_stops_task-test_stops_task-err-run.golden │ │ ├── TestPromptInSummary-test_stops_task-test_stops_task.golden │ │ ├── TestPromptInSummary-test_uppercase_approval-test_uppercase_approval.golden │ │ └── TestPromptWithIndirectTask.golden │ ├── requires/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestRequires-fails_validation-err-run.golden │ │ ├── TestRequires-fails_validation.golden │ │ ├── TestRequires-passes_validation.golden │ │ ├── TestRequires-require_before_compile-err-run.golden │ │ ├── TestRequires-require_before_compile.golden │ │ ├── TestRequires-required_var_missing-err-run.golden │ │ ├── TestRequires-required_var_missing.golden │ │ ├── TestRequires-required_var_missing_+_fails_validation#01.golden │ │ ├── TestRequires-required_var_missing_+_fails_validation-err-run.golden │ │ ├── TestRequires-required_var_missing_+_fails_validation.golden │ │ ├── TestRequires-required_var_ok.golden │ │ └── TestRequires-var_defined_in_task.golden │ ├── run/ │ │ ├── .gitignore │ │ └── Taskfile.yml │ ├── run_once_shared_deps/ │ │ ├── Taskfile.yml │ │ ├── library/ │ │ │ └── Taskfile.yml │ │ ├── service-a/ │ │ │ ├── Taskfile.yml │ │ │ └── src/ │ │ │ └── imasource.go │ │ └── service-b/ │ │ ├── Taskfile.yml │ │ └── src/ │ │ └── imasource.go │ ├── run_when_changed/ │ │ ├── Taskfile.yml │ │ ├── library/ │ │ │ └── Taskfile.yml │ │ ├── service-a/ │ │ │ └── Taskfile.yml │ │ └── service-b/ │ │ └── Taskfile.yml │ ├── shopts/ │ │ ├── command_level/ │ │ │ └── Taskfile.yml │ │ ├── global_level/ │ │ │ └── Taskfile.yml │ │ └── task_level/ │ │ └── Taskfile.yml │ ├── short_task_notation/ │ │ └── Taskfile.yml │ ├── silent/ │ │ └── Taskfile.yml │ ├── single_cmd_dep/ │ │ ├── .gitignore │ │ └── Taskfile.yml │ ├── special_vars/ │ │ ├── Taskfile.yml │ │ ├── included/ │ │ │ └── Taskfile.yml │ │ ├── subdir/ │ │ │ ├── .gitkeep │ │ │ └── testdata/ │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-print-root-taskfile.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-print-task.golden │ │ │ ├── TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden │ │ │ └── TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden │ │ └── testdata/ │ │ ├── TestSpecialVars-testdata-special_vars-included-print-root-dir.golden │ │ ├── TestSpecialVars-testdata-special_vars-included-print-task.golden │ │ ├── TestSpecialVars-testdata-special_vars-included-print-taskfile-dir.golden │ │ ├── TestSpecialVars-testdata-special_vars-included-print-taskfile.golden │ │ ├── TestSpecialVars-testdata-special_vars-print-root-dir.golden │ │ ├── TestSpecialVars-testdata-special_vars-print-root-taskfile.golden │ │ ├── TestSpecialVars-testdata-special_vars-print-task-dir.golden │ │ ├── TestSpecialVars-testdata-special_vars-print-task.golden │ │ ├── TestSpecialVars-testdata-special_vars-print-taskfile-dir.golden │ │ └── TestSpecialVars-testdata-special_vars-print-taskfile.golden │ ├── split_args/ │ │ └── Taskfile.yml │ ├── status/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestStatus-run_gen-bar_1_silent.golden │ │ ├── TestStatus-run_gen-bar_2_silent.golden │ │ ├── TestStatus-run_gen-bar_3_silent.golden │ │ ├── TestStatus-run_gen-bar_4_silent.golden │ │ ├── TestStatus-run_gen-bar_5.golden │ │ ├── TestStatus-run_gen-bar_6.golden │ │ ├── TestStatus-run_gen-baz_2.golden │ │ ├── TestStatus-run_gen-baz_3.golden │ │ ├── TestStatus-run_gen-baz_4_verbose.golden │ │ ├── TestStatus-run_gen-baz_silent.golden │ │ ├── TestStatus-run_gen-foo_1_silent.golden │ │ ├── TestStatus-run_gen-foo_2.golden │ │ └── TestStatus-run_gen-foo_3.golden │ ├── status_vars/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ └── source.txt │ ├── summary/ │ │ ├── Taskfile.yml │ │ └── task-with-summary.txt │ ├── summary-vars-requires/ │ │ ├── Taskfile-with-env.yml │ │ ├── Taskfile-with-globals.yml │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestSummaryWithVarsAndRequires-shell-vars.golden │ │ └── TestSummaryWithVarsAndRequires-vars-and-requires.golden │ ├── taskfile_walk/ │ │ ├── Taskfile.yml │ │ └── foo/ │ │ └── bar/ │ │ └── .gitkeep │ ├── user_working_dir/ │ │ └── Taskfile.yml │ ├── user_working_dir_with_includes/ │ │ ├── Taskfile.yml │ │ ├── included/ │ │ │ └── Taskfile.yml │ │ └── somedir/ │ │ └── .keep │ ├── var_inheritance/ │ │ └── v3/ │ │ ├── entrypoint-global-dotenv/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-global-dotenv.golden │ │ ├── entrypoint-global-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-global-vars.golden │ │ ├── entrypoint-task-call-dotenv/ │ │ │ ├── Taskfile.yml │ │ │ ├── called-task.env │ │ │ ├── global.env │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-task-call-dotenv.golden │ │ ├── entrypoint-task-call-task-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-task-call-task-vars.golden │ │ ├── entrypoint-task-call-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-task-call-vars.golden │ │ ├── entrypoint-task-dotenv/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-task-dotenv.golden │ │ ├── entrypoint-task-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-entrypoint-task-vars.golden │ │ ├── included-global-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-global-vars.golden │ │ ├── included-task/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ └── task.env │ │ ├── included-task-call-dotenv/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-task-call-dotenv.golden │ │ ├── included-task-call-task-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-task-call-task-vars.golden │ │ ├── included-task-call-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-task-call-vars.golden │ │ ├── included-task-dotenv/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-task-dotenv.golden │ │ ├── included-task-vars/ │ │ │ ├── Taskfile.yml │ │ │ ├── global.env │ │ │ ├── included.yml │ │ │ ├── task.env │ │ │ └── testdata/ │ │ │ └── TestVarInheritance-included-task-vars.golden │ │ └── shell/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ └── TestVarInheritance-shell.golden │ ├── var_references/ │ │ ├── Taskfile.yml │ │ └── testdata/ │ │ ├── TestReference-reference_in_command.golden │ │ ├── TestReference-reference_in_dependency.golden │ │ ├── TestReference-reference_using_templating_resolver.golden │ │ └── TestReference-reference_using_templating_resolver_and_dynamic_var.golden │ ├── vars/ │ │ ├── .gitignore │ │ ├── Taskfile.yml │ │ ├── any/ │ │ │ ├── Taskfile.yml │ │ │ ├── example.json │ │ │ └── example.yaml │ │ └── testdata/ │ │ ├── TestVars-cli-var-priority-default.golden │ │ ├── TestVars-cli-var-priority-override.golden │ │ └── TestVars.golden │ ├── version/ │ │ ├── v1/ │ │ │ └── Taskfile.yml │ │ ├── v2/ │ │ │ └── Taskfile.yml │ │ └── v3/ │ │ └── Taskfile.yml │ ├── watch/ │ │ ├── .gitignore │ │ └── Taskfile.yaml │ └── wildcards/ │ └── Taskfile.yml ├── variables.go ├── watch.go ├── watch_test.go └── website/ ├── .gitignore ├── .prettierignore ├── .vitepress/ │ ├── components/ │ │ ├── AuthorCard.vue │ │ ├── BlogPost.vue │ │ ├── HomePage.vue │ │ ├── VPTeamMembersItem.vue │ │ └── Version.vue │ ├── components.d.ts │ ├── config.ts │ ├── meta.ts │ ├── plugins/ │ │ └── github-links.ts │ ├── sponsors.ts │ ├── team.ts │ └── theme/ │ ├── custom.css │ └── index.ts ├── Taskfile.yml ├── netlify.toml ├── package.json ├── prettier.config.js ├── src/ │ ├── blog/ │ │ ├── any-variables.md │ │ ├── if-and-variable-prompt.md │ │ ├── index.md │ │ ├── task-in-2023.md │ │ └── windows-core-utils.md │ ├── docs/ │ │ ├── changelog.md │ │ ├── community.md │ │ ├── contributing.md │ │ ├── deprecations/ │ │ │ ├── completion-scripts.md │ │ │ ├── index.md │ │ │ ├── template-functions.md │ │ │ ├── template.md │ │ │ └── version-2-schema.md │ │ ├── experiments/ │ │ │ ├── env-precedence.md │ │ │ ├── gentle-force.md │ │ │ ├── index.md │ │ │ ├── remote-taskfiles.md │ │ │ └── template.md │ │ ├── faq.md │ │ ├── getting-started.md │ │ ├── guide.md │ │ ├── installation.md │ │ ├── integrations.md │ │ ├── reference/ │ │ │ ├── cli.md │ │ │ ├── config.md │ │ │ ├── environment.md │ │ │ ├── package.md │ │ │ ├── schema.md │ │ │ └── templating.md │ │ ├── releasing.md │ │ ├── styleguide.md │ │ └── taskfile-versions.md │ ├── donate.md │ ├── index.md │ ├── public/ │ │ ├── CNAME │ │ ├── _redirects │ │ ├── install.sh │ │ ├── robots.txt │ │ ├── schema-taskrc.json │ │ └── schema.json │ └── team.md └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = tab [*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,vue,css,svg,sh,bash,fish}] indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ * text=auto *.mdx -linguist-detectable ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: '🐞 Bug Report' description: Report a bug in Task. labels: ['state: needs-triage'] body: - type: markdown attributes: value: | Thanks for your bug report! Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same bug was not already reported by someone else. - type: textarea id: description attributes: label: Description description: Describe the bug you're seeing. placeholder: | - What did you do? - What did you expect to happen? - What happened instead? validations: required: true - type: input id: version attributes: label: Version description: What version(s) of Task is the issue occurring on? validations: required: true - type: input id: os attributes: label: Operating system description: What operating system(s) is the issue occurring on? validations: required: true - type: dropdown id: experiments attributes: label: Experiments Enabled description: Do you have any experiments enabled? You can check by running `task --experiments`. multiple: true options: - Env Precedence - Gentle Force - Map Variables (1) - Map Variables (2) - Remote Taskfiles validations: required: false - type: textarea id: logs attributes: label: Example Taskfile description: | If you have a Taskfile that reproduces the issue, please paste it here. This will be automatically formatted into code, so no need for backticks. render: YAML placeholder: | version: '3' tasks: default: cmds: - 'echo "This Taskfile is buggy :("' ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: '🔌 Task for Visual Studio Code' url: https://github.com/go-task/vscode-task about: 'Issues related to the Visual Studio Code extension should be opened here.' - name: '💬 Help forum on Discord' url: https://discord.com/channels/974121106208354339/1025054680289660989 about: 'The #help channel on our Discord is the best way to get help from the community.' - name: '❓ Questions, Ideas and General Discussions' url: https://github.com/go-task/task/discussions about: 'Ask questions and discuss general ideas with the community.' ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: '✨ Feature Request' description: Suggest a new feature or enhancement for Task. labels: ['state: needs-triage'] body: - type: markdown attributes: value: | Thanks for your feature request! Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same change was not already requested by someone else. If your request is more of an idea than a feature request, consider opening a [discussion](https://github.com/go-task/task/discussions) instead. - type: textarea id: description attributes: label: Description description: Describe the feature/enhancement you want to see in Task. placeholder: | - Give a general overview of the feature/enhancement. - Explain problem is the change trying to solve. - Give examples of how you would use the feature. validations: required: true ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", "group:allNonMajor", "schedule:weekly", ":semanticCommitTypeAll(chore)" ], "mode": "full", "addLabels":["area: dependencies"], "customManagers": [ { "customType": "regex", "fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"], "matchStrings": [ "uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?v[\\d.]+)" ], "datasourceTemplate": "github-releases", "depNameTemplate": "golangci/golangci-lint" } ], "packageRules": [ { "matchManagers": ["github-actions"], "addLabels": ["area: github actions"] }, { "matchCategories": ["js", "node"], "addLabels": ["lang: javascript"] }, { "matchCategories": ["golang"], "addLabels": ["lang: go"] } ] } ================================================ FILE: .github/workflows/issue-awaiting-response.yml ================================================ name: issue awaiting response on: issue_comment: types: [created] jobs: issue-awaiting-response: runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }) const comments = await github.paginate( github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, } ) const labels = await github.paginate( github.rest.issues.listLabelsOnIssue, { issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, } ) if (labels.find(label => label.name === 'state: awaiting response')) { if (comments[comments.length-1].user?.login === issue.data.user?.login) { github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'state: awaiting response' }) } } ================================================ FILE: .github/workflows/issue-closed.yml ================================================ name: issue closed on: issues: types: [closed] jobs: issue-closed: runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | const labels = await github.paginate( github.rest.issues.listLabelsOnIssue, { issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, } ) if (labels.find(label => label.name === 'state: needs triage')) { github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, name: 'state: needs triage' }) } ================================================ FILE: .github/workflows/issue-experiment.yml ================================================ name: issue experiment on: issues: types: [labeled] jobs: issue-experiment-proposed: if: github.event.label.name == format('status{0} proposed', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) issue-experiment-draft: if: github.event.label.name == format('status{0} draft', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) issue-experiment-candidate: if: github.event.label.name == format('status{0} candidate', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been marked as a candidate! :fire: This means that the implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) issue-experiment-stable: if: github.event.label.name == format('status{0} stable', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been marked as stable! :metal: This means that the implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) issue-experiment-released: if: github.event.label.name == format('status{0} released', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been released! :rocket: This means that it is no longer an experiment and is available in the latest major version of Task. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) github.rest.issues.update({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, state: 'closed' }) issue-experiment-abandoned: if: github.event.label.name == format('status{0} abandoned', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been abandoned. :disappointed: This means that this feature will not be added to Task and any experimental functionality will be removed. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) github.rest.issues.update({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, state: 'closed' }) issue-experiment-superseded: if: github.event.label.name == format('status{0} superseded', ':') runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'This experiment has been superseded. :seedling: This means that another experiment has replaced this one. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.' }) github.rest.issues.update({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, state: 'closed' }) ================================================ FILE: .github/workflows/issue-needs-triage.yml ================================================ name: issue needs triage on: issues: types: [opened] jobs: issue-needs-triage: runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{secrets.GH_PAT}} script: | const labels = await github.paginate( github.rest.issues.listLabelsOnIssue, { issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, } ) if (labels.length === 0) { github.rest.issues.addLabels({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, labels: ['state: needs triage'] }) } ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: pull_request: push: tags: - v* branches: - main jobs: lint: name: Lint strategy: matrix: go-version: [1.25.x, 1.26.x] runs-on: ubuntu-latest steps: - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{matrix.go-version}} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: v2.11.1 lint-jsonschema: runs-on: ubuntu-latest steps: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.14 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: install check-jsonschema run: python -m pip install 'check-jsonschema==0.27.3' - name: check-jsonschema (metaschema) run: check-jsonschema --check-metaschema website/src/public/schema.json ================================================ FILE: .github/workflows/pr-build.yml ================================================ name: PR Build on: pull_request_target: types: [labeled, synchronize] permissions: contents: read pull-requests: write jobs: build: if: contains(github.event.pull_request.labels.*.name, 'needs-build') runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: '1.26.x' cache: true - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 with: version: '~> v2' args: release --snapshot --clean --config .goreleaser-pr.yml - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: task_linux_amd64 path: dist/task_linux_amd64.tar.gz - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: task_linux_arm64 path: dist/task_linux_arm64.tar.gz - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: task_darwin_amd64 path: dist/task_darwin_amd64.tar.gz - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: task_darwin_arm64 path: dist/task_darwin_arm64.tar.gz - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: task_windows_amd64 path: dist/task_windows_amd64.zip - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: checksums path: dist/task_checksums.txt - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: find-comment with: token: ${{ secrets.GH_PAT || github.token }} issue-number: ${{ github.event.pull_request.number }} body-includes: '📦 Build artifacts ready!' - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: token: ${{ secrets.GH_PAT || github.token }} comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body: | ## 📦 Build artifacts ready! Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). Available platforms: Linux, macOS, Windows (amd64, arm64) edit-mode: replace ================================================ FILE: .github/workflows/release-nightly.yml ================================================ name: Release nightly on: workflow_dispatch: schedule: - cron: 0 0 * * * jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - name: Run GoReleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 with: distribution: goreleaser-pro version: latest args: release --clean --nightly -f .goreleaser-nightly.yml env: GITHUB_TOKEN: ${{secrets.GH_PAT}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}} ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: tags: - 'v*' permissions: id-token: write # Required for OIDC contents: read jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.26.x - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' registry-url: 'https://registry.npmjs.org' - name: Update npm run: npm install -g npm@latest - name: Install Task uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1 - name: Install pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 with: package_json_file: 'website/package.json' run_install: 'true' - name: Run GoReleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7 with: distribution: goreleaser-pro version: latest args: release --clean --draft env: GITHUB_TOKEN: ${{secrets.GH_PAT}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}} - name: Deploy Website shell: bash run: | task website:deploy:prod env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: pull_request: push: tags: - v* branches: - main jobs: test: name: Test strategy: matrix: go-version: [1.25.x, 1.26.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{matrix.platform}} steps: - name: Set up Go ${{matrix.go-version}} uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{matrix.go-version}} id: go - name: Check out code into the Go module directory uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download Go modules run: go mod download env: GOPROXY: https://proxy.golang.org - name: Build run: go build -o ./bin/task -v ./cmd/task - name: Test run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::' ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Graphvis files *.gv # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ ./task .task dist/ .DS_Store # editors .idea/ .vscode/settings.json .fleet/ # exuberant ctags tags /bin/* !/bin/.keep /testdata/vars/v1 /tmp node_modules website/.netlify/ ================================================ FILE: .golangci.yml ================================================ version: "2" formatters: enable: - gofmt - gofumpt - goimports - gci settings: gofmt: simplify: true rewrite-rules: - pattern: interface{} replacement: any gofumpt: module-path: github.com/go-task/task/v3 goimports: local-prefixes: - github.com/go-task gci: sections: - standard - default - prefix(github.com/go-task) - localmodule exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ linters: enable: - depguard - mirror - misspell - noctx - paralleltest - thelper - tparallel - usetesting settings: depguard: rules: main: files: - $all - '!$test' - '!**/errors/*.go' deny: - pkg: errors desc: Use github.com/go-task/task/v3/errors instead exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .goreleaser-nightly.yml ================================================ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 pro: true release: name_template: 'v{{.Version}}' nightly: publish_release: true keep_single_release: true version_template: "{{incminor .Version}}-nightly" includes: - from_file: path: ./.goreleaser.yml ================================================ FILE: .goreleaser-pr.yml ================================================ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 builds: - binary: task main: ./cmd/task goos: [windows, darwin, linux] goarch: [amd64, arm64] env: - CGO_ENABLED=0 mod_timestamp: '{{ .CommitTimestamp }}' flags: - -trimpath ldflags: - "-s -w" archives: - name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}' files: - README.md - LICENSE - completion/**/* format_overrides: - goos: windows formats: [zip] snapshot: version_template: 'pr-{{ .ShortCommit }}' checksum: name_template: 'task_checksums.txt' ================================================ FILE: .goreleaser.yml ================================================ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 builds: - binary: task main: ./cmd/task goos: - windows - darwin - linux - freebsd goarch: - '386' - amd64 - arm - arm64 - riscv64 goarm: - '6' ignore: - goos: darwin goarch: '386' - goos: darwin goarch: riscv64 - goos: windows goarch: arm - goos: windows goarch: riscv64 env: - CGO_ENABLED=0 mod_timestamp: '{{ .CommitTimestamp }}' flags: - -trimpath ldflags: - "-s -w" - "{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}" gomod: proxy: true archives: - name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}' files: - README.md - LICENSE - completion/**/* format_overrides: - goos: windows formats: [zip] git: ignore_tags: - "{{if not .IsNightly}}nightly{{end}}" snapshot: version_template: '{{.Version}}' checksum: name_template: 'task_checksums.txt' nfpms: - vendor: Task homepage: https://taskfile.dev maintainer: The Task authors description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. section: golang license: MIT conflicts: - taskwarrior formats: - deb - rpm - apk file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}' contents: - src: completion/bash/task.bash dst: /etc/bash_completion.d/task - src: completion/fish/task.fish dst: /usr/share/fish/completions/task.fish - src: completion/zsh/_task dst: /usr/local/share/zsh/site-functions/_task brews: - name: go-task description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. license: MIT homepage: https://taskfile.dev directory: Formula repository: owner: go-task name: homebrew-tap test: system "#{bin}/task", "--help" install: |- bin.install "task" bash_completion.install "completion/bash/task.bash" => "task" zsh_completion.install "completion/zsh/_task" => "_task" fish_completion.install "completion/fish/task.fish" commit_author: name: task-bot email: 106601941+task-bot@users.noreply.github.com winget: - name: Task publisher: Task short_description: The modern task runner. description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. license: MIT homepage: https://taskfile.dev/ publisher_url: https://taskfile.dev/ publisher_support_url: https://github.com/go-task/task/issues package_identifier: Task.Task commit_author: name: task-bot email: 106601941+task-bot@users.noreply.github.com commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}' release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}} tags: - build - build-tool - devops - go - make - makefile - runner - task - task-runner - taskfile - tool repository: owner: go-task name: winget-pkgs branch: 'task-{{.Version}}' pull_request: enabled: true draft: false check_boxes: true base: owner: microsoft name: winget-pkgs branch: master body: | /cc @andreynering @pd93 @vmaerten npms: - name: "@go-task/cli" repository: "git+https://github.com/go-task/task.git" bugs: https://github.com/go-task/task/issues description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. homepage: https://taskfile.dev license: MIT author: "The Task authors" access: public keywords: - "task" - "taskfile" - "build-tool" - "task-runner" cloudsmiths: - organization: "task" repository: "{{if not .IsNightly}}task{{end}}" formats: - deb - rpm - apk distributions: deb: - "any-distro/any-version" rpm: - "any-distro/any-version" alpine: - "alpine/any-version" component: main republish: true ================================================ FILE: .mockery.yaml ================================================ all: False template: testify filename: '{{base (trimSuffix ".go" .InterfaceFile)}}_mock.go' packages: github.com/go-task/task/v3/internal/fingerprint: interfaces: SourcesCheckable: StatusCheckable: ================================================ FILE: .prettierrc.yml ================================================ trailingComma: none singleQuote: true overrides: - files: "*.md" options: printWidth: 80 proseWrap: always ================================================ FILE: .taskrc.yml ================================================ experiments: GENTLE_FORCE: 0 REMOTE_TASKFILES: 0 ENV_PRECEDENCE: 0 ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "editorconfig.editorconfig", "golang.go", "task.vscode-task" ] } ================================================ FILE: .vscode/settings-sample.json ================================================ { "yaml.schemas": { "./website/src/public/schema.json": [ "Taskfile.yml", "Taskfile.yaml", "taskfile.yml", "taskfile.yaml" ] }, "gopls": { "formatting.local": "github.com/go-task" }, "go.formatTool": "gofumpt" } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v3.49.1 - 2026-03-08 * Reverted #2632 for now, which caused some regressions. That change will be reworked (#2720, #2722, #2723). ## v3.49.0 - 2026-03-07 - Fixed included Taskfiles with `watch: true` not triggering watch mode when called from the root Taskfile (#2686, #1763 by @trulede). - Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths (#2656 by @Trim21). - Fixed remote Git Taskfiles timing out when resolving includes after accepting the trust prompt (#2669, #2668 by @vmaerten). - Fixed unclear error message when Taskfile search stops at a directory ownership boundary (#2682, #1683 by @trulede). - Fixed global variables from imported Taskfiles not resolving `ref:` values correctly (#2632 by @trulede). - Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed environment variable, making CI and container configuration easier (#2607, #1066 by @vmaerten). ## v3.48.0 - 2026-01-26 - Fixed `if:` conditions when using to check dynamic variables. Also, skip variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten). - Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by @trulede). - Included Taskfiles with `silent: true` now properly propagate silence to their tasks, while still allowing individual tasks to override with `silent: false` (#2640, #1319 by @trulede). - Added TLS certificate options for Remote Taskfiles: use `--cacert` for self-signed certificates and `--cert`/`--cert-key` for mTLS authentication (#2537, #2242 by @vmaerten). ## v3.47.0 - 2026-01-24 - Fixed remote git Taskfiles: cloning now works without explicit ref, and directory includes are properly resolved (#2602 by @vmaerten). - For `output: prefixed`, print `prefix:` if set instead of task name (#1566, #2633 by @trulede). - Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by @trulede). - Task aliases can now contain wildcards and will match accordingly (e.g., `s-*` as alias for `start-*`) (#1900, #2234 by @vmaerten). - Added conditional execution with the `if` field: skip tasks, commands, or task calls based on shell exit codes or template expressions like `{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten). - Task can now interactively prompt for missing required variables when running in a TTY, with support for enum selection menus. Enable with `--interactive` flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten). ## v3.46.4 - 2025-12-24 - Fixed regressions in completion script for Fish (#2591, #2604, #2592 by @WinkelCode). ## v3.46.3 - 2025-12-19 - Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten). ## v3.46.2 - 2025-12-18 - Fixed a regression on previous release that affected variables passed via command line (#2588, #2589 by @vmaerten). ## v3.46.1 - 2025-12-18 ### ✨ Features - A small behavior change was made to dependencies. Task will now wait for all dependencies to finish running before continuing, even if any of them fail. To opt for the previous behavior, set `failfast: true` either on your `.taskrc.yml` or per task, or use the `--failfast` flag, which will also work for `--parallel` (#1246, #2525 by @andreynering). - The `--summary` flag now displays `vars:` (both global and task-level), `env:`, and `requires:` sections. Dynamic variables show their shell command (e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by @vmaerten). - Improved performance of fuzzy task name matching by implementing lazy initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten). - Added LLM-optimized documentation via VitePress plugin, generating `llms.txt` and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten). - Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to skip confirmation prompts for specified hosts when using Remote Taskfiles (#2491, #2473 by @maciejlech). - When running in GitHub Actions, Task now automatically emits error annotations on failure, improving visibility in workflow summaries (#2568 by @vmaerten). - The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES` variable (#2577, #2479 by @semihbkgr). - Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing flags and dynamic experimental feature detection (#2532 by @vmaerten). - Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536, #1944 by @vmaerten). - Shell completion now works when Task is installed or aliased under a different binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten). - Some small fixes and improvements were made to `task --init` and to the default Taskfile it generates (#2433 by @andreynering). - Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to customize the cache directory for Remote Taskfiles (#2572 by @vmaerten). - Zsh completion now supports zstyle verbose option to show or hide task descriptions (#2571 by @vmaerten). - Task now automatically enables colored output in CI environments (GitHub Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by @vmaerten). - Added color taskrc option to explicitly enable or disable colored output globally (#2569 by @vmaerten). - Improved Git Remote Taskfiles by switching to go-getter: SSH authentication now works out of the box and `applyOf` is properly supported (#2512 by @vmaerten). ### 🐛 Fixes - Fix RPM upload to Cloudsmith by including the version in the filename to ensure unique filenames (#2507 by @vmaerten). - Fix `run: when_changed` to work properly for Taskfiles included multiple times (#2508, #2511 by @trulede). - Fixed Zsh and Fish completions to stop suggesting task names after `--` separator, allowing proper CLI_ARGS completion (#1843, #1844 by @boiledfroginthewell). - Watch mode (`--watch`) now always runs the task, regardless of `run: once` or `run: when_changed` settings (#2566, #1388 by @trulede). - Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in root-level vars section (#2403, #2397 by @trulede, @vmaerten). - Fixed a bug where `ignore_error` was ignored when using `task:` to call another task (#2552, #363 by @trulede). - Fixed Zsh completion not suggesting global tasks when using `-g`/`--global` flag (#1574, #2574 by @vmaerten). - Fixed Fish completion failing to parse task descriptions containing colons (e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten). - Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when using `for` loops in Taskfiles (#2576 by @vmaerten). ## v3.45.5 - 2025-11-11 - Fixed bug that made a generic message, instead of an useful one, appear when a Taskfile could not be found (#2431 by @andreynering). - Fixed a bug that caused an error when including a Remote Git Taskfile (#2438 by @twelvelabs). - Fixed issue where `.taskrc.yml` was not returned if reading it failed, and corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten). - Improved performance of `--list` and `--list-all` by introducing a faster compilation method that skips source globbing and checksum updates (#1322, #2053 by @vmaerten). - Fixed a concurrency bug with `output: group`. This ensures that begin/end parts won't be mixed up from different tasks (#1208, #2349, #2350 by @trulede). - Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede). - Improve error message when a Taskfile is not found (#2441, #2494 by @vmaerten). - Fixed generic error message `exit status 1` when a dependency task failed (#2286 by @GrahamDennis). - Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork maintained by the official YAML org (#2171, #2434 by @andreynering). - On Windows, the built-in version of the `rm` core utils contains a fix related to the `-f` flag (#2426, [u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464), [mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by @andreynering). ## v3.45.4 - 2025-09-17 - Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423 by @vmaerten). - Fixed a bug where `.taskrc.yml` files in parent folders were not read correctly (#2424 by @vmaerten). - Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by @vmaerten). ## v3.45.3 - 2025-09-15 - Task now includes built-in core utilities to greatly improve compatibility on Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any other common core utility will now work by default on Windows, without extra setup. This is something we wanted to address for many many years, and it's finally being shipped! [Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils). (#197, #2360 by @andreynering). - :sparkles: Built and deployed a [brand new website](https://taskfile.dev) using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by @vmaerten, @andreynering, @pd93). - Began releasing [nightly builds](https://github.com/go-task/task/releases/tag/nightly). This will allow people to test our changes before they are fully released and without having to install Go to build them (#2358 by @vmaerten). - Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or `$HOME/.taskrc.yml`. Check out our new [configuration guide](https://taskfile.dev/docs/reference/config) for more details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93). - Added experiments to the taskrc schema to clarify the expected keys and values (#2235 by @vmaerten). - Added support for new properties in `.taskrc.yml`: insecure, verbose, concurrency, remote offline, remote timeout, and remote expiry. :warning: Note: setting offline via environment variable is no longer supported. (#2389 by @vmaerten) - Added a `--nested` flag when outputting tasks using `--list --json`. This will output tasks in a nested structure when tasks are namespaced (#2415 by @pd93). - Enhanced support for tasks with wildcards: they are now logged correctly, and wildcard parameters are fully considered during fingerprinting (#1808, #1795 by @vmaerten). - Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417 by @trulede). #### Package API - Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten). #### Other news We recently released our [official GitHub Action](https://github.com/go-task/setup-task). This is based on the fantastic work by the Arduino team who created and maintained the community version. Now that this is officially adopted, fixes/updates should be more timely. We have already merged a couple of longstanding PRs in our [first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by @pd93, @shrink, @trim21 and all the previous contributors to [arduino/setup-task](https://github.com/arduino/setup-task/)). ## v3.45.0-v3.45.2 - 2025-09-15 Failed due to an issue with our release process. ## v3.44.1 - 2025-07-23 - Internal tasks will no longer be shown as suggestions since they cannot be called (#2309, #2323 by @maxmzkrcensys) - Fixed install script for some ARM platforms (#1516, #2291 by @trulede). - Fixed a regression where fingerprinting was not working correctly if the path to you Taskfile contained a space (#2321, #2322 by @pd93). - Reverted a breaking change to `randInt` (#2312, #2316 by @pd93). - Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests (#2265 by @pd93). ## v3.44.0 - 2025-06-08 - Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by @pd93). - Added new `CLI_ARGS_LIST` array variable which contains the arguments passed to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a string). (#2138, #2139, #2140 by @pd93). - Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93). - Added `task` field the `--list --json` output (#2256 by @aleksandersh). - Added the ability to [pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning) by specifying a checksum. This works with both local and remote Taskfiles (#2222, #2223 by @pd93). - When using the [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317), any credentials used in the URL will now be redacted in Task's output (#2100, #2220 by @pd93). - Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200 by @vmaerten). - Fixed a bug where taskfiles in directories containing spaces created directories in the wrong location (#2208, #2216 by @pd93). - Added support for dual JSON schema files, allowing changes without affecting the current schema. The current schemas will only be updated during releases. (#2211 by @vmaerten). - Improved fingerprint documentation by specifying that the method can be set at the root level to apply to all tasks (#2233 by @vmaerten). - Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by @wazazaby, #2271 by @andreynering). ## v3.43.3 - 2025-04-27 Reverted the changes made in #2113 and #2186 that affected the `USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and #2208. ## v3.43.2 - 2025-04-21 - Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by @vmaerten). ## v3.43.1 - 2025-04-21 - Significant improvements were made to the watcher. We migrated from [watcher](https://github.com/radovskyb/watcher) to [fsnotify](https://github.com/fsnotify/fsnotify). The former library used polling, which means Task had a high CPU usage when watching too many files. `fsnotify` uses proper the APIs from each operating system to watch files, which means a much better performance. The default interval changed from 5 seconds to 100 milliseconds, because now it configures the wait time for duplicated events, instead of the polling time (#2048 by @andreynering, #1508, #985, #1179). - The [Map Variables experiment](https://github.com/go-task/task/issues/1585) was made generally available so you can now [define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables) (#1585, #1547, #2081 by @pd93). - Wildcards can now [match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072, #2121 by @pd93). - Added the ability to [loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files). This works the same way as looping over sources (#2151 by @sedyh). - Added the ability to resolve variables when defining an include variable (#2108, #2113 by @pd93). - A few changes have been made to the [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317) (#1402, #2176 by @pd93): - Cached files are now prioritized over remote ones. - Added an `--expiry` flag which sets the TTL for a remote file cache. By default the value will be 0 (caching disabled). If Task is running in offline mode or fails to make a connection, it will fallback on the cache. - `.taskrc` files can now be used from subdirectories and will be searched for recursively up the file tree in the same way that Taskfiles are (#2159, #2166 by @pd93). - The default taskfile (output when using the `--init` flag) is now an embedded file in the binary instead of being stored in the code (#2112 by @pd93). - Improved the way we report the Task version when using the `--version` flag or `{{.TASK_VERSION}}` variable. This should now be more consistent and easier for package maintainers to use (#2131 by @pd93). - Fixed a bug where globstar (`**`) matching in `sources` only resolved the first result (#2073, #2075 by @pd93). - Fixed a bug where sorting tasks by "none" would use the default sorting instead of leaving tasks in the order they were defined (#2124, #2125 by @trulede). - Fixed Fish completion on newer Fish versions (#2130 by @atusy). - Fixed a bug where undefined/null variables resolved to an empty string instead of `nil` (#1911, #2144 by @pd93). - The `USER_WORKING_DIR` special now will now properly account for the `--dir` (`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering). - Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy). - Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten). #### Package API - The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now uses the functional options pattern (#2085, #2147, #2148 by @pd93). - The functional options for the [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) and [`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet) types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by @pd93). - [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) no longer accepts a [`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node). Instead nodes are passed directly into the [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read) method (#2169 by @pd93). - [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read) also now accepts a [`context.Context`](https://pkg.go.dev/context#Context) (#2176 by @pd93). ## v3.42.1 - 2025-03-10 - Fixed a bug where some special variables caused a type error when used global variables (#2106, #2107 by @pd93). ## v3.42.0 - 2025-03-08 - Made `--init` less verbose by default and respect `--silent` and `--verbose` flags (#2009, #2011 by @HeCorr). - `--init` now accepts a file name or directory as an argument (#2008, #2018 by @HeCorr). - Fix a bug where an HTTP node's location was being mutated incorrectly (#2007 by @jeongukjae). - Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by @vmaerten). - Use only the relevant checker (timestamp or checksum) to improve performance (#2029, #2031 by @vmaerten). - Print warnings when attempting to enable an inactive experiment or an active experiment with an invalid value (#1979, #2049 by @pd93). - Refactored the experiments package and added tests (#2049 by @pd93). - Show allowed values when a variable with an enum is missing (#2027, #2052 by @vmaerten). - Refactored how snippets in error work and added tests (#2068 by @pd93). - Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by @pd93). - Fixed a bug in the Taskfile schema where `defer` statements in the shorthand `cmds` syntax were not considered valid (#2068 by @pd93). - Refactored how task sorting functions work (#1798 by @pd93). - Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable experiments (similar to `.env`) (#1982 by @vmaerten). - Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086 by @pd93). - Allow `matrix` to use references to other variables (#2065, #2069 by @pd93). - Fixed a bug where, when a dynamic variable is provided, even if it is not used, all other variables become unavailable in the templating system within the include (#2092 by @vmaerten). #### Package API Unlike our CLI tool, [Task's package API is not currently stable](https://taskfile.dev/reference/package). In an effort to ease the pain of breaking changes for our users, we will be providing changelogs for our package API going forwards. The hope is that these changes will provide a better long-term experience for our users and allow to stabilize the API in the future. #121 now tracks this piece of work. - Bumped the minimum required Go version to 1.23 (#2059 by @pd93). - [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile) (#2011, ff8c913 by @HeCorr and @pd93) - No longer accepts an `io.Writer` (output is now the caller's responsibility). - The path argument can now be a filename OR a directory. - The function now returns the full path of the generated file. - [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo) now accepts a string instead of the arguments required to generate a snippet (#2068 by @pd93). - The caller is now expected to create the snippet themselves (see below). - [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet) and related code moved from the `errors` package to the `taskfile` package (#2068 by @pd93). - Renamed `TaskMissingRequiredVars` to [`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError) (#2052 by @vmaerten). - Renamed `TaskNotAllowedVars` to [`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError) (#2052 by @vmaerten). - The [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) is now constructed using the functional options pattern (#2082 by @pd93). - Removed our internal `logger.Logger` from the entire `taskfile` package (#2082 by @pd93). - Users are now expected to pass a custom debug/prompt functions into [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) if they want this functionality by using the new [`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc) and [`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc) functional options. - Remove `Range` functions in the `taskfile/ast` package in favour of new iterator functions (#1798 by @pd93). - `ast.Call` was moved from the `taskfile/ast` package to the main `task` package (#2084 by @pd93). - `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the `task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93). - The `Compiler` and its `GetVariables` and `FastGetVariables` methods were moved from the `internal/compiler` package to the main `task` package (#2084 by @pd93). ## v3.41.0 - 2025-01-18 - Fixed an issue where dynamic variables were not properly logged in verbose mode (#1920, #1921 by @mgbowman). - Support `silent` for defer statements (#1877, #1879 by @danilobuerger). - Added an option to exclude some tasks from being included (#1859 by @vmaerten). - Fixed an issue where a required variable was incorrectly handled in a template function (#1950, #1962 by @vmaerten). - Expose a new `TASK_DIR` special variable, which will contain the absolute path of task directory. (#1959, #1961 by @vmaerten). - Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by @pd93, @GrahamDennis and @trim21). - Refactored internal ordered map implementation to use [github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap) (#1797 by @pd93). - Fixed a bug where variables defined at the task level were being ignored in the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko) - The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds` (#1872 by @niklasr22). - Updated [installation docs](https://taskfile.dev/installation) and added pip installation method (#935, #1989 by @pd93). - Fixed a bug where dynamic variables could not access environment variables (#630, #1869 by @rohm1 and @pd93). - Disable version check for use as an external library (#1938 by @leaanthony). ## v3.40.1 - 2024-12-06 - Fixed a security issue in `git-urls` by switching to the maintained fork `chainguard-dev/git-urls` (#1917 by @AlekSi). - Added missing `platforms` property to `cmds` that use `for` (#1915 by @dkarter). - Added misspell linter to check for misspelled English words (#1883 by @christiandins). ## v3.40.0 - 2024-11-05 - Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in for loops (#1822, #1823 by @stawii). - Added a new `TASK_OFFLINE` environment variable to configure the `--offline` flag and expose it as a special variable in the templating system (#1470, #1716 by @vmaerten and @pd93). - Fixed a bug where multiple remote includes caused all prompts to display without waiting for user input (#1832, #1833 by @vmaerten and @pd93). - When using the "[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)". experiment, you can now include Taskfiles from Git repositories (#1652 by @vmaerten). - Improved the error message when a dotenv file cannot be parsed (#1842 by @pbitty). - Fix issue with directory when using the remote experiment (#1757 by @pbitty). - Fixed an issue where a special variable was used in combination with a dotenv file (#1232, #1810 by @vmaerten). - Refactor the way Task reads Taskfiles to improve readability (#1771 by @pbitty). - Added a new option to ensure variable is within the list of values (#1827 by @vmaerten). - Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina). - Added new template function: `numCPU`, which returns the number of logical CPUs usable (#1890, #1887 by @Amoghrd). - Fixed a bug where non-nil, empty dynamic variables are returned as an empty interface (#1903, #1904 by @pd93). ## v3.39.2 - 2024-09-19 - Fix dynamic variables not working properly for a defer: statement (#1803, #1818 by @vmaerten). ## v3.39.1 - 2024-09-18 - Added Renovate configuration to automatically create PRs to keep dependencies up to date (#1783 by @vmaerten). - Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten). - Fixed a bug where ZSH and PowerShell completions did not work when using the recommended method. (#1813, #1809 by @vmaerten and @shirayu) - Fix variables not working properly for a `defer:` statement (#1803, #1814 by @vmaerten and @andreynering). ## v3.39.0 - 2024-09-07 - Added [Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence) (#1038, #1633 by @vmaerten). - Added a CI lint job to ensure that the docs are updated correctly (#1719 by @vmaerten). - Updated minimum required Go version to 1.22 (#1758 by @pd93). - Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering). - Expose a new `ALIAS` special variable, which will contain the alias used to call the current task. Falls back to the task name. (#1764 by @DanStory). - Fixed `TASK_REMOTE_DIR` environment variable not working when the path was absolute. (#1715 by @vmaerten). - Added an option to declare an included Taskfile as flattened (#1704 by @vmaerten). - Added a new [`--completion` flag](https://taskfile.dev/installation/#setup-completions) to output completion scripts for various shells (#293, #1157 by @pd93). - This is now the preferred way to install completions. - The completion scripts in the `completion` directory [are now deprecated](https://taskfile.dev/deprecations/completion-scripts/). - Added the ability to [loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix) (#1766, #1767, #1784 by @pd93). - Fixed a bug in fish completion where aliases were not displayed (#1781, #1782 by @vmaerten). - Fixed panic when having a flattened included Taskfile that contains a `default` task (#1777, #1778 by @vmaerten). - Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten). ## v3.38.0 - 2024-06-30 - Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering). - Some YAML parsing errors will now show in a more user friendly way (#1619 by @pd93). - Prefixed outputs will now be colorized by default (#1572 by @AlexanderArvidsson) - [References](https://taskfile.dev/usage/#referencing-other-variables) are now generally available (no experiments required) (#1654 by @pd93). - Templating functions can now be used in references (#1645, #1654 by @pd93). - Added a new [templating reference page](https://taskfile.dev/reference/templating/) to the documentation (#1614, #1653 by @pd93). - If using the [Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1), references are available by [prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references) (#1654 by @pd93). - If using the [Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2), the `yaml` and `json` keys are no longer available (#1654 by @pd93). - Added a new `TASK_REMOTE_DIR` environment variable to configure where cached remote Taskfiles are stored (#1661 by @vmaerten). - Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639 by @vmaerten). - Improved the readability of cached remote Taskfile filenames (#1636 by @vmaerten). - Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by @mengzhuo). - Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith). - Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93). - Fixed JSON Schema to disallow invalid properties (#1657 by @pd93). - Fixed version checks not working as intended (#872, #1663 by @vmaerten). - Fixed a bug where included tasks were run multiple times even if `run: once` was set (#852, #1655 by @pd93). - Fixed some bugs related to column formatting in the terminal (#1350, #1637, #1656 by @vmaerten). ## v3.37.2 - 2024-05-12 - Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93). - Fixed a bug where includes Taskfile variable were not being merged correctly (#1643, #1649 by @pd93). ## v3.37.1 - 2024-05-09 - Fix bug where non-string values (numbers, bools) added to `env:` weren't been correctly exported (#1640, #1641 by @vmaerten and @andreynering). ## v3.37.0 - 2024-05-08 - Released the [Any Variables experiment](https://taskfile.dev/blog/any-variables), but [_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925) (#1415, #1547 by @pd93). - Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563, #1607 by @pd93). - Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by @pd93). - Fix error when a file or directory in the project contained a special char like `&`, `(` or `)` (#1551, #1584 by @andreynering). - Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt) - Added support for `~` on ZSH completions (#1613 by @jwater7). - Added the ability to pass variables by reference using Go template syntax when the [Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is enabled (#1612 by @pd93). - Added support for environment variables in the templating engine in `includes` (#1610 by @vmaerten). ## v3.36.0 - 2024-04-08 - Added support for [looping over dependencies](https://taskfile.dev/usage/#looping-over-dependencies) (#1299, #1541 by @pd93). - When using the "[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)" experiment, you are now able to use [remote Taskfiles as your entrypoint](https://taskfile.dev/experiments/remote-taskfiles/#root-remote-taskfiles). - `includes` in remote Taskfiles will now also resolve correctly (#1347 by @pd93). - When using the "[Any Variables](https://taskfile.dev/experiments/any-variables/)" experiments, templating is now supported in collection-type variables (#1477, #1511, #1526 by @pd93). - Fixed a bug where variables being passed to an included Taskfile were not available when defining global variables (#1503, #1533 by @pd93). - Improved support to customized colors by allowing 8-bit colors and multiple ANSI attributes (#1576 by @pd93). ## v3.35.1 - 2024-03-04 - Fixed a bug where the `TASKFILE_DIR` variable was sometimes incorrect (#1522, #1523 by @pd93). - Added a new `TASKFILE` special variable that holds the root Taskfile path (#1523 by @pd93). - Fixed various issues related to running a Taskfile from a subdirectory (#1529, #1530 by @pd93). ## v3.35.0 - 2024-02-28 - Added support for [wildcards in task names](https://taskfile.dev/usage/#wildcard-arguments) (#836, #1489 by @pd93). - Added the ability to [run Taskfiles via stdin](https://taskfile.dev/usage/#reading-a-taskfile-from-stdin) (#655, #1483 by @pd93). - Bumped minimum Go version to 1.21 (#1500 by @pd93). - Fixed bug related to the `--list` flag (#1509, #1512 by @pd93, #1514, #1520 by @pd93). - Add mention on the documentation to the fact that the variable declaration order is respected (#1510 by @kirkrodrigues). - Improved style guide docs (#1495 by @iwittkau). - Removed duplicated entry for `requires` on the API docs (#1491 by @teatimeguest). ## v3.34.1 - 2024-01-27 - Fixed prompt regression on [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles/) (#1486, #1487 by @pd93). ## v3.34.0 - 2024-01-25 - Removed support for `version: 2` schemas. See the [deprecation notice on our website](https://taskfile.dev/deprecations/version-2-schema) (#1197, #1447 by @pd93). - Fixed a couple of issues in the JSON Schema + added a CI step to ensure it's correct (#1471, #1474, #1476 by @sirosen). - Added [Any Variables experiment proposal 2](https://taskfile.dev/experiments/any-variables/?proposal=2) (#1415, #1444 by @pd93). - Updated the experiments and deprecations documentation format (#1445 by @pd93). - Added new template function: `spew`, which can be used to print variables for debugging purposes (#1452 by @pd93). - Added new template function: `merge`, which can be used to merge any number of map variables (#1438, #1464 by @pd93). - Small change on the API when using as a library: `call.Direct` became `call.Indirect` (#1459 by @pd93). - Refactored the public `read` and `taskfile` packages and introduced `taskfile/ast` (#1450 by @pd93). - `ast.IncludedTaskfiles` renamed to `ast.Includes` and `orderedmap` package renamed to `omap` plus some internal refactor work (#1456 by @pd93). - Fix zsh completion script to allow lowercase `taskfile` file names (#1482 by @xontab). - Improvements on how we check the Taskfile version (#1465 by @pd93). - Added a new `ROOT_TASKFILE` special variable (#1468, #1469 by @pd93). - Fix experiment flags in `.env` when the `--dir` or `--taskfile` flags were used (#1478 by @pd93). ## v3.33.1 - 2023-12-21 - Added support for looping over map variables with the [Any Variables experiment](https://taskfile.dev/experiments/any-variables) enabled (#1435, #1437 by @pd93). - Fixed a bug where dynamic variables were causing errors during fast compilation (#1435, #1437 by @pd93) ## v3.33.0 - 2023-12-20 - Added [Any Variables experiment](https://taskfile.dev/experiments/any-variables) (#1415, #1421 by @pd93). - Updated Docusaurus to v3 (#1432 by @pd93). - Added `aliases` to `--json` flag output (#1430, #1431 by @pd93). - Added new `CLI_FORCE` special variable containing whether the `--force` or `--force-all` flags were set (#1412, #1434 by @pd93). ## v3.32.0 - 2023-11-29 - Added ability to exclude some files from `sources:` by using `exclude:` (#225, #1324 by @pd93 and @andreynering). - The [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) now prefers remote files over cached ones by default (#1317, #1345 by @pd93). - Added `--timeout` flag to the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) (#1317, #1345 by @pd93). - Fix bug where dynamic `vars:` and `env:` were being executed when they should actually be skipped by `platforms:` (#1273, #1377 by @andreynering). - Fix `schema.json` to make `silent` valid in `cmds` that use `for` (#1385, #1386 by @iainvm). - Add new `--no-status` flag to skip expensive status checks when running `task --list --json` (#1348, #1368 by @amancevice). ## v3.31.0 - 2023-10-07 - Enabled the `--yes` flag for the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) (#1317, #1344 by @pd93). - Add ability to set `watch: true` in a task to automatically run it in watch mode (#231, #1361 by @andreynering). - Fixed a bug on the watch mode where paths that contained `.git` (like `.github`), for example, were also being ignored (#1356 by @butuzov). - Fixed a nil pointer error when running a Taskfile with no contents (#1341, #1342 by @pd93). - Added a new [exit code](https://taskfile.dev/api/#exit-codes) (107) for when a Taskfile does not contain a schema version (#1342 by @pd93). - Increased limit of maximum task calls from 100 to 1000 for now, as some people have been reaching this limit organically now that we have loops. This check exists to detect recursive calls, but will be removed in favor of a better algorithm soon (#1321, #1332). - Fixed templating on descriptions on `task --list` (#1343 by @blackjid). - Fixed a bug where precondition errors were incorrectly being printed when task execution was aborted (#1337, #1338 by @sylv-io). ## v3.30.1 - 2023-09-14 - Fixed a regression where some special variables weren't being set correctly (#1331, #1334 by @pd93). ## v3.30.0 - 2023-09-13 - Prep work for Remote Taskfiles (#1316 by @pd93). - Added the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) as a draft (#1152, #1317 by @pd93). - Improve performance of content checksumming on `sources:` by replacing md5 with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking change because checksums will be invalidated when upgrading to this release (#1325 by @ReillyBrogan). ## v3.29.1 - 2023-08-26 - Update to Go 1.21 (bump minimum version to 1.20) (#1302 by @pd93) - Fix a missing a line break on log when using `--watch` mode (#1285, #1297 by @FilipSolich). - Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering). - Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by @andarto, #1309 by @andreynering). - Fix bug on `--status` flag. Running this flag should not have side-effects: it should not update the checksum on `.task`, only report its status (#1305, #1307 by @visciang, #1313 by @andreynering). ## v3.28.0 - 2023-07-24 - Added the ability to [loop over commands and tasks](https://taskfile.dev/usage/#looping-over-values) using `for` (#82, #1220 by @pd93). - Fixed variable propagation in multi-level includes (#778, #996, #1256 by @hudclark). - Fixed a bug where the `--exit-code` code flag was not returning the correct exit code when calling commands indirectly (#1266, #1270 by @pd93). - Fixed a `nil` panic when a dependency was commented out or left empty (#1263 by @neomantra). ## v3.27.1 - 2023-06-30 - Fix panic when a `.env` directory (not file) is present on current directory (#1244, #1245 by @pd93). ## v3.27.0 - 2023-06-29 - Allow Taskfiles starting with lowercase characters (#947, #1221 by @pd93). - e.g. `taskfile.yml`, `taskfile.yaml`, `taskfile.dist.yml` & `taskfile.dist.yaml` - Bug fixes were made to the [npm installation method](https://taskfile.dev/installation/#npm). (#1190, by @sounisi5011). - Added the [gentle force experiment](https://taskfile.dev/experiments/gentle-force) as a draft (#1200, #1216 by @pd93). - Added an `--experiments` flag to allow you to see which experiments are enabled (#1242 by @pd93). - Added ability to specify which variables are required in a task (#1203, #1204 by @benc-uk). ## v3.26.0 - 2023-06-10 - Only rewrite checksum files in `.task` if the checksum has changed (#1185, #1194 by @deviantintegral). - Added [experiments documentation](https://taskfile.dev/experiments) to the website (#1198 by @pd93). - Deprecated `version: 2` schema. This will be removed in the next major release (#1197, #1198, #1199 by @pd93). - Added a new `prompt:` prop to set a warning prompt to be shown before running a potential dangerous task (#100, #1163 by @MaxCheetham, [Documentation](https://taskfile.dev/usage/#warning-prompts)). - Added support for single command task syntax. With this change, it's now possible to declare just `cmd:` in a task, avoiding the more complex `cmds: []` when you have only a single command for that task (#1130, #1131 by @timdp). ## v3.25.0 - 2023-05-22 - Support `silent:` when calling another tasks (#680, #1142 by @danquah). - Improve PowerShell completion script (#1168 by @trim21). - Add more languages to the website menu and show translation progress percentage (#1173 by @misitebao). - Starting on this release, official binaries for FreeBSD will be available to download (#1068 by @andreynering). - Fix some errors being unintendedly suppressed (#1134 by @clintmod). - Fix a nil pointer error when `version` is omitted from a Taskfile (#1148, #1149 by @pd93). - Fix duplicate error message when a task does not exists (#1141, #1144 by @pd93). ## v3.24.0 - 2023-04-15 - Fix Fish shell completion for tasks with aliases (#1113 by @patricksjackson). - The default branch was renamed from `master` to `main` (#1049, #1048 by @pd93). - Fix bug where "up-to-date" logs were not being omitted for silent tasks (#546, #1107 by @danquah). - Add `.hg` (Mercurial) to the list of ignored directories when using `--watch` (#1098 by @misery). - More improvements to the release tool (#1096 by @pd93). - Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93) - Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by @pd93). - Task now has [custom exit codes](https://taskfile.dev/api/#exit-codes) depending on the error (#1114 by @pd93). ## v3.23.0 - 2023-03-26 Task now has an [official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task) contributed by @pd93! :tada: The extension is maintained in a [new repository](https://github.com/go-task/vscode-task) under the `go-task` organization. We're looking to gather feedback from the community so please give it a go and let us know what you think via a [discussion](https://github.com/go-task/vscode-task/discussions), [issue](https://github.com/go-task/vscode-task/issues) or on our [Discord](https://discord.gg/6TY36E39UK)! > **NOTE:** The extension _requires_ v3.23.0 to be installed in order to work. - The website was integrated with [Crowdin](https://crowdin.com/project/taskfile) to allow the community to contribute with translations! [Chinese](https://taskfile.dev/zh-Hans/) is the first language available (#1057, #1058 by @misitebao). - Added task location data to the `--json` flag output (#1056 by @pd93) - Change the name of the file generated by `task --init` from `Taskfile.yaml` to `Taskfile.yml` (#1062 by @misitebao). - Added new `splitArgs` template function (`{{splitArgs "foo bar 'foo bar baz'"}}`) to ensure string is split as arguments (#1040, #1059 by @dhanusaputra). - Fix the value of `{{.CHECKSUM}}` variable in status (#1076, #1080 by @pd93). - Fixed deep copy implementation (#1072 by @pd93) - Created a tool to assist with releases (#1086 by @pd93). ## v3.22.0 - 2023-03-10 - Add a brand new `--global` (`-g`) flag that will run a Taskfile from your `$HOME` directory. This is useful to have automation that you can run from anywhere in your system! ([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), #1029 by @andreynering). - Add ability to set `error_only: true` on the `group` output mode. This will instruct Task to only print a command output if it returned with a non-zero exit code (#664, #1022 by @jaedle). - Fixed bug where `.task/checksum` file was sometimes not being created when task also declares a `status:` (#840, #1035 by @harelwa, #1037 by @pd93). - Refactored and decoupled fingerprinting from the main Task executor (#1039 by @pd93). - Fixed deadlock issue when using `run: once` (#715, #1025 by @theunrepentantgeek). ## v3.21.0 - 2023-02-22 - Added new `TASK_VERSION` special variable (#990, #1014 by @ja1code). - Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007 by @pd93). - Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93) - Added environment variable `FORCE_COLOR` support to force color output. Useful for environments without TTY (#1003 by @automation-stack) ## v3.20.0 - 2023-01-14 - Improve behavior and performance of status checking when using the `timestamp` mode (#976, #977 by @aminya). - Performance optimizations were made for large Taskfiles (#982 by @pd93). - Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html) and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins (#908, #929 by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)). - Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to choose in which platforms that given task or command will be run on. Possible values are operating system (GOOS), architecture (GOARCH) or a combination of the two. Example: `platforms: [linux]`, `platforms: [amd64]` or `platforms: [linux/amd64]`. Other platforms will be skipped (#978, #980 by @leaanthony). ## v3.19.1 - 2022-12-31 - Small bug fix: closing `Taskfile.yml` once we're done reading it (#963, #964 by @HeCorr). - Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file (#961, #971 by @pd93). - Fixed a bug where watch intervals set in the Taskfile were not being respected (#969, #970 by @pd93) - Add `--json` flag (alias `-j`) with the intent to improve support for code editors and add room to other possible integrations. This is basic for now, but we plan to add more info in the near future (#936 by @davidalpert, #764). ## v3.19.0 - 2022-12-05 - Installation via npm now supports [pnpm](https://pnpm.io/) as well ([go-task/go-npm#2](https://github.com/go-task/go-npm/issues/2), [go-task/go-npm#3](https://github.com/go-task/go-npm/pull/3)). - It's now possible to run Taskfiles from subdirectories! A new `USER_WORKING_DIR` special variable was added to add even more flexibility for monorepos (#289, #920). - Add task-level `dotenv` support (#389, #904). - It's now possible to use global level variables on `includes` (#942, #943). - The website got a brand new [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/) by [@DeronW](https://github.com/DeronW). Thanks! ## v3.18.0 - 2022-11-12 - Show aliases on `task --list --silent` (`task --ls`). This means that aliases will be completed by the completion scripts (#919). - Tasks in the root Taskfile will now be displayed first in `--list`/`--list-all` output (#806, #890). - It's now possible to call a `default` task in an included Taskfile by using just the namespace. For example: `docs:default` is now automatically aliased to `docs` (#661, #815). ## v3.17.0 - 2022-10-14 - Add a "Did you mean ...?" suggestion when a task does not exits another one with a similar name is found (#867, #880). - Now YAML parse errors will print which Taskfile failed to parse (#885, #887). - Add ability to set `aliases` for tasks and namespaces (#268, #340, #879). - Improvements to Fish shell completion (#897). - Added ability to set a different watch interval by setting `interval: '500ms'` or using the `--interval=500ms` flag (#813, #865). - Add colored output to `--list`, `--list-all` and `--summary` flags (#845, #874). - Fix unexpected behavior where `label:` was being shown instead of the task name on `--list` (#603, #877). ## v3.16.0 - 2022-09-29 - Add `npm` as new installation method: `npm i -g @go-task/cli` (#870, #871, [npm package](https://www.npmjs.com/package/@go-task/cli)). - Add support to marking tasks and includes as internal, which will hide them from `--list` and `--list-all` (#818). ## v3.15.2 - 2022-09-08 - Fix error when using variable in `env:` introduced in the previous release (#858, #866). - Fix handling of `CLI_ARGS` (`--`) in Bash completion (#863). - On zsh completion, add ability to replace `--list-all` with `--list` as already possible on the Bash completion (#861). ## v3.15.0 - 2022-09-03 - Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly requested feature (#215, #857, [Documentation](https://taskfile.dev/api/#special-variables)). - Follow symlinks on `sources` (#826, #831). - Improvements and fixes to Bash completion (#835, #844). ## v3.14.1 - 2022-08-03 - Always resolve relative include paths relative to the including Taskfile (#822, #823). - Fix ZSH and PowerShell completions to consider all tasks instead of just the public ones (those with descriptions) (#803). ## v3.14.0 - 2022-07-08 - Add ability to override the `.task` directory location with the `TASK_TEMP_DIR` environment variable. - Allow to override Task colors using environment variables: `TASK_COLOR_RESET`, `TASK_COLOR_BLUE`, `TASK_COLOR_GREEN`, `TASK_COLOR_CYAN`, `TASK_COLOR_YELLOW`, `TASK_COLOR_MAGENTA` and `TASK_COLOR_RED` (#568, #792). - Fixed bug when using the `output: group` mode where STDOUT and STDERR were being print in separated blocks instead of in the right order (#779). - Starting on this release, ARM architecture binaries are been released to Snap as well (#795). - i386 binaries won't be available anymore on Snap because Ubuntu removed the support for this architecture. - Upgrade mvdan.cc/sh, which fixes a bug with associative arrays (#785, [mvdan/sh#884](https://github.com/mvdan/sh/issues/884), [mvdan/sh#893](https://github.com/mvdan/sh/pull/893)). ## v3.13.0 - 2022-06-13 - Added `-n` as an alias to `--dry` (#776, #777). - Fix behavior of interrupt (SIGINT, SIGTERM) signals. Task will now give time for the processes running to do cleanup work (#458, #479, #728, #769). - Add new `--exit-code` (`-x`) flag that will pass-through the exit form the command being ran (#755). ## v3.12.1 - 2022-05-10 - Fixed bug where, on Windows, variables were ending with `\r` because we were only removing the final `\n` but not `\r\n` (#717). ## v3.12.0 - 2022-03-31 - The `--list` and `--list-all` flags can now be combined with the `--silent` flag to print the task names only, without their description (#691). - Added support for multi-level inclusion of Taskfiles. This means that included Taskfiles can also include other Taskfiles. Before this was limited to one level (#390, #623, #656). - Add ability to specify vars when including a Taskfile. [Check out the documentation](https://taskfile.dev/#/usage?id=vars-of-included-taskfiles) for more information (#677). ## v3.11.0 - 2022-02-19 - Task now supports printing begin and end messages when using the `group` output mode, useful for grouping tasks in CI systems. [Check out the documentation](http://taskfile.dev/#/usage?id=output-syntax) for more information (#647, #651). - Add `Taskfile.dist.yml` and `Taskfile.dist.yaml` to the supported file name list. [Check out the documentation](https://taskfile.dev/#/usage?id=supported-file-names) for more information (#498, #666). ## v3.10.0 - 2022-01-04 - A new `--list-all` (alias `-a`) flag is now available. It's similar to the exiting `--list` (`-l`) but prints all tasks, even those without a description (#383, #401). - It's now possible to schedule cleanup commands to run once a task finishes with the `defer:` keyword ([Documentation](https://taskfile.dev/#/usage?id=doing-task-cleanup-with-defer), #475, #626). - Remove long deprecated and undocumented `$` variable prefix and `^` command prefix (#642, #644, #645). - Add support for `.yaml` extension (as an alternative to `.yml`). This was requested multiple times throughout the years. Enjoy! (#183, #184, #369, #584, #621). - Fixed error when computing a variable when the task directory do not exist yet (#481, #579). ## v3.9.2 - 2021-12-02 - Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for a important regression on Windows (#619, [mvdan/sh#768](https://github.com/mvdan/sh/issues/768), [mvdan/sh#769](https://github.com/mvdan/sh/pull/769)). ## v3.9.1 - 2021-11-28 - Add logging in verbose mode for when a task starts and finishes (#533, #588). - Fix an issue with preconditions and context errors (#597, #598). - Quote each `{{.CLI_ARGS}}` argument to prevent one with spaces to become many (#613). - Fix nil pointer when `cmd:` was left empty (#612, #614). - Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains two relevant fixes: - Fix quote of empty strings in `shellQuote` (#609, [mvdan/sh#763](https://github.com/mvdan/sh/issues/763)). - Fix issue of wrong environment variable being picked when there's another very similar one (#586, [mvdan/sh#745](https://github.com/mvdan/sh/pull/745)). - Install shell completions automatically when installing via Homebrew (#264, #592, [go-task/homebrew-tap#2](https://github.com/go-task/homebrew-tap/pull/2)). ## v3.9.0 - 2021-10-02 - A new `shellQuote` function was added to the template system (`{{shellQuote "a string"}}`) to ensure a string is safe for use in shell ([mvdan/sh#727](https://github.com/mvdan/sh/pull/727), [mvdan/sh#737](https://github.com/mvdan/sh/pull/737), [Documentation](https://pkg.go.dev/mvdan.cc/sh/v3@v3.4.0/syntax#Quote)) - In this version [mvdan.cc/sh](https://github.com/mvdan/sh) was upgraded with some small fixes and features - The `read -p` flag is now supported (#314, [mvdan/sh#551](https://github.com/mvdan/sh/issues/551), [mvdan/sh#772](https://github.com/mvdan/sh/pull/722)) - The `pwd -P` and `pwd -L` flags are now supported (#553, [mvdan/sh#724](https://github.com/mvdan/sh/issues/724), [mvdan/sh#728](https://github.com/mvdan/sh/pull/728)) - The `$GID` environment variable is now correctly being set (#561, [mvdan/sh#723](https://github.com/mvdan/sh/pull/723)) ## v3.8.0 - 2021-09-26 - Add `interactive: true` setting to improve support for interactive CLI apps (#217, #563). - Fix some `nil` errors (#534, #573). - Add ability to declare an included Taskfile as optional (#519, #552). - Add support for including Taskfiles in the home directory by using `~` (#539, #557). ## v3.7.3 - 2021-09-04 - Add official support to Apple M1 (#564, #567). - Our [official Homebrew tap](https://github.com/go-task/homebrew-tap) will support more platforms, including Apple M1 ## v3.7.0 - 2021-07-31 - Add `run:` setting to control if tasks should run multiple times or not. Available options are `always` (the default), `when_changed` (if a variable modified the task) and `once` (run only once no matter what). This is a long time requested feature. Enjoy! (#53, #359). ## v3.6.0 - 2021-07-10 - Allow using both `sources:` and `status:` in the same task (#411, #427, #477). - Small optimization and bug fix: don't compute variables if not needed for `dotenv:` (#517). ## v3.5.0 - 2021-07-04 - Add support for interpolation in `dotenv:` (#433, #434, #453). ## v3.4.3 - 2021-05-30 - Add support for the `NO_COLOR` environment variable. (#459, [fatih/color#137](https://github.com/fatih/color/pull/137)). - Fix bug where sources were not considering the right directory in `--watch` mode (#484, #485). ## v3.4.2 - 2021-04-23 - On watch, report which file failed to read (#472). - Do not try to catch SIGKILL signal, which are not actually possible (#476). - Improve version reporting when building Task from source using Go Modules (#462, #473). ## v3.4.1 - 2021-04-17 - Improve error reporting when parsing YAML: in some situations where you would just see an generic error, you'll now see the actual error with more detail: the YAML line the failed to parse, for example (#467). - A JSON Schema was published [here](https://json.schemastore.org/taskfile.json) and is automatically being used by some editors like Visual Studio Code (#135). - Print task name before the command in the log output (#398). ## v3.3.0 - 2021-03-20 - Add support for delegating CLI arguments to commands with `--` and a special `CLI_ARGS` variable (#327). - Add a `--concurrency` (alias `-C`) flag, to limit the number of tasks that run concurrently. This is useful for heavy workloads. (#345). ## v3.2.2 - 2021-01-12 - Improve performance of `--list` and `--summary` by skipping running shell variables for these flags (#332). - Fixed a bug where an environment in a Taskfile was not always overridable by the system environment (#425). - Fixed environment from .env files not being available as variables (#379). - The install script is now working for ARM platforms (#428). ## v3.2.1 - 2021-01-09 - Fixed some bugs and regressions regarding dynamic variables and directories (#426). - The [slim-sprig](https://github.com/go-task/slim-sprig) package was updated with the upstream [sprig](https://github.com/Masterminds/sprig). ## v3.2.0 - 2021-01-07 - Fix the `.task` directory being created in the task directory instead of the Taskfile directory (#247). - Fix a bug where dynamic variables (those declared with `sh:`) were not running in the task directory when the task has a custom dir or it was in an included Taskfile (#384). - The watch feature (via the `--watch` flag) got a few different bug fixes and should be more stable now (#423, #365). ## v3.1.0 - 2021-01-03 - Fix a bug when the checksum up-to-date resolution is used by a task with a custom `label:` attribute (#412). - Starting from this release, we're releasing official ARMv6 and ARM64 binaries for Linux (#375, #418). - Task now respects the order of declaration of included Taskfiles when evaluating variables declaring by them (#393). - `set -e` is now automatically set on every command. This was done to fix an issue where multiline string commands wouldn't really fail unless the sentence was in the last line (#403). ## v3.0.1 - 2020-12-26 - Allow use as a library by moving the required packages out of the `internal` directory (#358). - Do not error if a specified dotenv file does not exist (#378, #385). - Fix panic when you have empty tasks in your Taskfile (#338, #362). ## v3.0.0 - 2020-08-16 - On `v3`, all CLI variables will be considered global variables (#336, #341) - Add support to `.env` like files (#324, #356). - Add `label:` to task so you can override the task name in the logs (#321, #337). - Refactor how variables work on version 3 (#311). - Disallow `expansions` on v3 since it has no effect. - `Taskvars.yml` is not automatically included anymore. - `Taskfile_{{OS}}.yml` is not automatically included anymore. - Allow interpolation on `includes`, so you can manually include a Taskfile based on operation system, for example. - Expose `.TASK` variable in templates with the task name (#252). - Implement short task syntax (#194, #240). - Added option to make included Taskfile run commands on its own directory (#260, #144) - Taskfiles in version 1 are not supported anymore (#237). - Added global `method:` option. With this option, you can set a default method to all tasks in a Taskfile (#246). - Changed default method from `timestamp` to `checksum` (#246). - New magic variables are now available when using `status:`: `.TIMESTAMP` which contains the greatest modification date from the files listed in `sources:`, and `.CHECKSUM`, which contains a checksum of all files listed in `status:`. This is useful for manual checking when using external, or even remote, artifacts when using `status:` (#216). - We're now using [slim-sprig](https://github.com/go-task/slim-sprig) instead of [sprig](https://github.com/Masterminds/sprig), which allowed a file size reduction of about 22% (#219). - We now use some colors on Task output to better distinguish message types - commands are green, errors are red, etc (#207). ## v2.8.1 - 2020-05-20 - Fix error code for the `--help` flag (#300, #330). - Print version to stdout instead of stderr (#299, #329). - Suppress `context` errors when using the `--watch` flag (#313, #317). - Support templating on description (#276, #283). ## v2.8.0 - 2019-12-07 - Add `--parallel` flag (alias `-p`) to run tasks given by the command line in parallel (#266). - Fixed bug where calling the `task` CLI only informing global vars would not execute the `default` task. - Add ability to silent all tasks by adding `silent: true` a the root of the Taskfile. ## v2.7.1 - 2019-11-10 - Fix error being raised when `exit 0` was called (#251). ## v2.7.0 - 2019-09-22 - Fixed panic bug when assigning a global variable (#229, #243). - A task with `method: checksum` will now re-run if generated files are deleted (#228, #238). ## v2.6.0 - 2019-07-21 - Fixed some bugs regarding minor version checks on `version:`. - Add `preconditions:` to task (#205). - Create directory informed on `dir:` if it doesn't exist (#209, #211). - We now have a `--taskfile` flag (alias `-t`), which can be used to run another Taskfile (other than the default `Taskfile.yml`) (#221). - It's now possible to install Task using Homebrew on Linux ([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)). ## v2.5.2 - 2019-05-11 - Reverted YAML upgrade due issues with CRLF on Windows (#201, [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)). - Allow setting global variables through the CLI (#192). ## 2.5.1 - 2019-04-27 - Fixed some issues with interactive command line tools, where sometimes the output were not being shown, and similar issues (#114, #190, #200). - Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3. ## v2.5.0 - 2019-03-16 - We moved from the taskfile.org domain to the new fancy taskfile.dev domain. While stuff is being redirected, we strongly recommend to everyone that use [this install script](https://taskfile.dev/#/installation?id=install-script) to use the new taskfile.dev domain on scripts from now on. - Fixed to the ZSH completion (#182). - Add [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task) (#180). ## v2.4.0 - 2019-02-21 - Allow calling a task of the root Taskfile from an included Taskfile by prefixing it with `:` (#161, #172). - Add flag to override the `output` option (#173). - Fix bug where Task was persisting the new checksum on the disk when the Dry Mode is enabled (#166). - Fix file timestamp issue when the file name has spaces (#176). - Mitigating path expanding issues on Windows (#170). ## v2.3.0 - 2019-01-02 - On Windows, Task can now be installed using [Scoop](https://scoop.sh/) (#152). - Fixed issue with file/directory globing (#153). - Added ability to globally set environment variables (#138, #159). ## v2.2.1 - 2018-12-09 - This repository now uses Go Modules (#143). We'll still keep the `vendor` directory in sync for some time, though; - Fixing a bug when the Taskfile has no tasks but includes another Taskfile (#150); - Fix a bug when calling another task or a dependency in an included Taskfile (#151). ## v2.2.0 - 2018-10-25 - Added support for [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles) (#98) - This should be considered experimental. For now, only including local files is supported, but support for including remote Taskfiles is being discussed. If you have any feedback, please comment on #98. - Task now have a dedicated documentation site: https://taskfile.org - Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To check the source code, just take a look at the [docs](https://github.com/go-task/task/tree/main/docs) directory of this repository. Contributions to the documentation is really appreciated. ## v2.1.1 - 2018-09-17 - Fix suggestion to use `task --init` not being shown anymore (when a `Taskfile.yml` is not found) - Fix error when using checksum method and no file exists for a source glob (#131) - Fix signal handling when the `--watch` flag is given (#132) ## v2.1.0 - 2018-08-19 - Add a `ignore_error` option to task and command (#123) - Add a dry run mode (`--dry` flag) (#126) ## v2.0.3 - 2018-06-24 - Expand environment variables on "dir", "sources" and "generates" (#116) - Fix YAML merging syntax (#112) - Add ZSH completion (#111) - Implement new `output` option. Please check out the [documentation](https://github.com/go-task/task#output-syntax) ## v2.0.2 - 2018-05-01 - Fix merging of YAML anchors (#112) ## v2.0.1 - 2018-03-11 - Fixes panic on `task --list` ## v2.0.0 - 2018-03-08 Version 2.0.0 is here, with a new Taskfile format. Please, make sure to read the [Taskfile versions](https://github.com/go-task/task/blob/main/TASKFILE_VERSIONS.md) document, since it describes in depth what changed for this version. - New Taskfile version 2 (#77) - Possibility to have global variables in the `Taskfile.yml` instead of `Taskvars.yml` (#66) - Small improvements and fixes ## v1.4.4 - 2017-11-19 - Handle SIGINT and SIGTERM (#75); - List: print message with there's no task with description; - Expand home dir ("~" symbol) on paths (#74); - Add Snap as an installation method; - Move examples to its own repo; - Watch: also walk on tasks called on on "cmds", and not only on "deps"; - Print logs to stderr instead of stdout (#68); - Remove deprecated `set` keyword; - Add checksum based status check, alternative to timestamp based. ## v1.4.3 - 2017-09-07 - Allow assigning variables to tasks at run time via CLI (#33) - Added support for multiline variables from sh (#64) - Fixes env: remove square braces and evaluate shell (#62) - Watch: change watch library and few fixes and improvements - When use watching, cancel and restart long running process on file change (#59 and #60) ## v1.4.2 - 2017-07-30 - Flag to set directory of execution - Always echo command if is verbose mode - Add silent mode to disable echoing of commands - Fixes and improvements of variables (#56) ## v1.4.1 - 2017-07-15 - Allow use of YAML for dynamic variables instead of $ prefix - `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello` - Add `--list` (or `-l`) flag to print existing tasks - OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`, etc) - Consider task up-to-date on equal timestamps (#49) - Allow absolute path in generates section (#48) - Bugfix: allow templating when calling deps (#42) - Fix panic for invalid task in cyclic dep detection - Better error output for dynamic variables in Taskvars.yml (#41) - Allow template evaluation in parameters ## v1.4.0 - 2017-07-06 - Cache dynamic variables - Add verbose mode (`-v` flag) - Support to task parameters (overriding vars) (#31) (#32) - Print command, also when "set:" is specified (#35) - Improve task command help text (#35) ## v1.3.1 - 2017-06-14 - Fix glob not working on commands (#28) - Add ExeExt template function - Add `--init` flag to create a new Taskfile - Add status option to prevent task from running (#27) - Allow interpolation on `generates` and `sources` attributes (#26) ## v1.3.0 - 2017-04-24 - Migrate from os/exec.Cmd to a native Go sh/bash interpreter - This is a potentially breaking change if you use Windows. - Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for your commands, even on Windows. - Add "ToSlash" and "FromSlash" to template functions - Use functions defined on github.com/Masterminds/sprig - Do not redirect stdin while running variables commands - Using `context` and `errgroup` packages (this will make other tasks to be cancelled, if one returned an error) ## v1.2.0 - 2017-04-02 - More tests and Travis integration - Watch a task (experimental) - Possibility to call another task - Fix "=" not being recognized in variables/environment variables - Tasks can now have a description, and help will print them (#10) - Task dependencies now run concurrently - Support for a default task (#16) ## v1.1.0 - 2017-03-08 - Support for YAML, TOML and JSON (#1) - Support running command in another directory (#4) - `--force` or `-f` flag to force execution of task even when it's up-to-date - Detection of cyclic dependencies (#5) - Support for variables (#6, #9, #14) - Operation System specific commands and variables (#13) ## v1.0.0 - 2017-02-28 - Add LICENSE file ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Andrey Nering Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Task: The Modern Task Runner

A fast, cross-platform build tool inspired by Make, designed for modern workflows.

InstallationGetting StartedDocsTwitterBlueskyMastodonDiscord

Gold Sponsors

Community Sponsors

================================================ FILE: Taskfile.yml ================================================ version: '3' includes: website: aliases: [w, docs, d] taskfile: ./website dir: ./website vars: BIN: "{{.ROOT_DIR}}/bin" GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}' env: CGO_ENABLED: '0' tasks: default: cmds: - task: lint - task: test run: desc: Runs Task cmds: - go run ./cmd/task {{.CLI_ARGS}} install: desc: Installs Task aliases: [i] sources: - './**/*.go' - go.mod cmds: - go install -v ./cmd/task generate: aliases: [gen, g] desc: Runs all generate tasks cmds: - task: generate:mocks - task: generate:fixtures generate:mocks: desc: Runs Mockery to create mocks aliases: [gen:mocks, g:mocks] deps: [install:mockery] sources: - "internal/fingerprint/checker.go" generates: - "internal/mocks/*.go" cmds: - find . -type f -name *_mock.go -delete - "{{.BIN}}/mockery" generate:fixtures: desc: Runs tests and generates golden fixture files aliases: [gen:fixtures, g:fixtures] env: GOLDIE_UPDATE: 'true' GOLDIE_TEMPLATE: 'true' cmds: - find ./testdata -name '*.golden' -delete - go test ./... install:mockery: desc: Installs mockgen; a tool to generate mock files vars: MOCKERY_VERSION: v3.2.2 env: GOBIN: "{{.BIN}}" status: - go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}} cmds: - GOBIN="{{.BIN}}" go install github.com/vektra/mockery/v3@{{.MOCKERY_VERSION}} mod: desc: Downloads and tidy Go modules cmds: - go mod download - go mod tidy clean: desc: Cleans temp files and folders aliases: [clear] cmds: - rm -rf dist/ - rm -rf tmp/ lint: desc: Runs golangci-lint aliases: [l] sources: - './**/*.go' - .golangci.yml - go.mod cmds: - golangci-lint run lint:fix: desc: Runs golangci-lint and fixes any issues sources: - './**/*.go' - .golangci.yml - go.mod cmds: - golangci-lint run --fix format: desc: Runs golangci-lint and formats any Go files aliases: [fmt, f] sources: - './**/*.go' - .golangci.yml cmds: - golangci-lint fmt sleepit:build: desc: Builds the sleepit test helper sources: - ./cmd/sleepit/**/*.go generates: - "{{.BIN}}/sleepit" cmds: - go build -o {{.BIN}}/sleepit{{exeExt}} ./cmd/sleepit sleepit:run: desc: Builds the sleepit test helper deps: [sleepit:build] cmds: - "{{.BIN}}/sleepit {{.CLI_ARGS}}" silent: true test: desc: Runs test suite aliases: [t] deps: [gotestsum:install] sources: - "**/*.go" - "testdata/**/*" cmds: - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... test:watch: desc: Runs test suite with watch tests included deps: [sleepit:build, gotestsum:install] cmds: - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch' test:all: desc: Runs test suite with signals and watch tests included deps: [sleepit:build, gotestsum:install] cmds: - gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./... goreleaser:test: desc: Tests release process without publishing cmds: - goreleaser --snapshot --clean gotestsum:install: desc: Installs gotestsum status: - command -v gotestsum cmds: - go install gotest.tools/gotestsum@latest goreleaser:install: desc: Installs goreleaser cmds: - go install github.com/goreleaser/goreleaser/v2@latest gorelease:install: desc: "Installs gorelease: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease" status: - command -v gorelease cmds: - go install golang.org/x/exp/cmd/gorelease@latest api:check: desc: Checks what changes have been made to the public API deps: [gorelease:install] vars: LATEST: sh: git describe --tags --abbrev=0 cmds: - gorelease -base={{.LATEST}} release:*: desc: Prepare the project for a new release summary: | This task will do the following: - Update the version and date in the CHANGELOG.md file - Update the version in the package.json and package-lock.json files - Copy the latest docs to the "current" version on the website - Commit the changes - Create a new tag - Push the commit/tag to the repository - Create a GitHub release To use the task, run "task release:" where "" is is one of: - "major" - Bumps the major number - "minor" - Bumps the minor number - "patch" - Bumps the patch number - A semver compatible version number (e.g. "1.2.3") vars: VERSION: sh: "go run ./cmd/release --version {{index .MATCH 0}}" COMPLETE_MESSAGE: | Creating release with GoReleaser: https://github.com/go-task/task/actions/workflows/release.yml Please wait for the CI to finish and then do the following: - Copy the changelog for v{{.VERSION}} to the GitHub release - Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml preconditions: - sh: test $(git rev-parse --abbrev-ref HEAD) = "main" msg: "You must be on the main branch to release" - sh: "[[ -z $(git diff --shortstat main) ]]" msg: "You must have a clean working tree to release" prompt: "Are you sure you want to release version {{.VERSION}}?" cmds: - cmd: echo "Releasing v{{.VERSION}}" silent: true - "go run ./cmd/release {{.VERSION}}" - "git add --all" - "git commit -m v{{.VERSION}}" - "git push" - "git tag -a v{{.VERSION}} -m v{{.VERSION}}" - "git push origin tag v{{.VERSION}}" - cmd: printf "%s" '{{.COMPLETE_MESSAGE}}' silent: true ================================================ FILE: args/args.go ================================================ package args import ( "strings" "github.com/spf13/pflag" "mvdan.cc/sh/v3/syntax" "github.com/go-task/task/v3" "github.com/go-task/task/v3/taskfile/ast" ) // Get fetches the remaining arguments after CLI parsing and splits them into // two groups: the arguments before the double dash (--) and the arguments after // the double dash. func Get() ([]string, []string, error) { args := pflag.Args() doubleDashPos := pflag.CommandLine.ArgsLenAtDash() if doubleDashPos == -1 { return args, nil, nil } return args[:doubleDashPos], args[doubleDashPos:], nil } // Parse parses command line argument: tasks and global variables func Parse(args ...string) ([]*task.Call, *ast.Vars) { calls := []*task.Call{} globals := ast.NewVars() for _, arg := range args { if !strings.Contains(arg, "=") { calls = append(calls, &task.Call{Task: arg}) continue } name, value := splitVar(arg) globals.Set(name, ast.Var{Value: value}) } return calls, globals } func ToQuotedString(args []string) (string, error) { var quotedCliArgs []string for _, arg := range args { quotedCliArg, err := syntax.Quote(arg, syntax.LangBash) if err != nil { return "", err } quotedCliArgs = append(quotedCliArgs, quotedCliArg) } return strings.Join(quotedCliArgs, " "), nil } func splitVar(s string) (string, string) { pair := strings.SplitN(s, "=", 2) return pair[0], pair[1] } ================================================ FILE: args/args_test.go ================================================ package args_test import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/go-task/task/v3" "github.com/go-task/task/v3/args" "github.com/go-task/task/v3/taskfile/ast" ) func TestArgs(t *testing.T) { t.Parallel() tests := []struct { Args []string ExpectedCalls []*task.Call ExpectedGlobals *ast.Vars }{ { Args: []string{"task-a", "task-b", "task-c"}, ExpectedCalls: []*task.Call{ {Task: "task-a"}, {Task: "task-b"}, {Task: "task-c"}, }, }, { Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, ExpectedCalls: []*task.Call{ {Task: "task-a"}, {Task: "task-b"}, {Task: "task-c"}, }, ExpectedGlobals: ast.NewVars( &ast.VarElement{ Key: "FOO", Value: ast.Var{ Value: "bar", }, }, &ast.VarElement{ Key: "BAR", Value: ast.Var{ Value: "baz", }, }, &ast.VarElement{ Key: "BAZ", Value: ast.Var{ Value: "foo", }, }, ), }, { Args: []string{"task-a", "CONTENT=with some spaces"}, ExpectedCalls: []*task.Call{ {Task: "task-a"}, }, ExpectedGlobals: ast.NewVars( &ast.VarElement{ Key: "CONTENT", Value: ast.Var{ Value: "with some spaces", }, }, ), }, { Args: []string{"FOO=bar", "task-a", "task-b"}, ExpectedCalls: []*task.Call{ {Task: "task-a"}, {Task: "task-b"}, }, ExpectedGlobals: ast.NewVars( &ast.VarElement{ Key: "FOO", Value: ast.Var{ Value: "bar", }, }, ), }, { Args: nil, ExpectedCalls: []*task.Call{}, }, { Args: []string{}, ExpectedCalls: []*task.Call{}, }, { Args: []string{"FOO=bar", "BAR=baz"}, ExpectedCalls: []*task.Call{}, ExpectedGlobals: ast.NewVars( &ast.VarElement{ Key: "FOO", Value: ast.Var{ Value: "bar", }, }, &ast.VarElement{ Key: "BAR", Value: ast.Var{ Value: "baz", }, }, ), }, } for i, test := range tests { t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) { t.Parallel() calls, globals := args.Parse(test.Args...) assert.Equal(t, test.ExpectedCalls, calls) if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 { assert.Equal(t, test.ExpectedGlobals, globals) assert.Equal(t, test.ExpectedGlobals, globals) } }) } } ================================================ FILE: bin/.keep ================================================ ================================================ FILE: call.go ================================================ package task import "github.com/go-task/task/v3/taskfile/ast" // Call is the parameters to a task call type Call struct { Task string Vars *ast.Vars Silent bool Indirect bool // True if the task was called by another task } ================================================ FILE: cmd/release/main.go ================================================ package main import ( "fmt" "os" "regexp" "strings" "time" "github.com/Masterminds/semver/v3" "github.com/spf13/pflag" "github.com/go-task/task/v3/errors" ) const ( changelogSource = "CHANGELOG.md" changelogTarget = "website/src/docs/changelog.md" versionFile = "internal/version/version.txt" ) var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`) // Flags var ( versionFlag bool ) func init() { pflag.BoolVarP(&versionFlag, "version", "v", false, "resolved version number") pflag.Parse() } func main() { if err := release(); err != nil { fmt.Println(err) os.Exit(1) } } func release() error { if len(pflag.Args()) != 1 { return errors.New("error: expected version number") } version, err := getVersion(versionFile) if err != nil { return err } if err := bumpVersion(version, pflag.Arg(0)); err != nil { return err } if versionFlag { fmt.Println(version) return nil } if err := changelog(version); err != nil { return err } if err := setVersionFile(versionFile, version); err != nil { return err } return nil } func getVersion(filename string) (*semver.Version, error) { b, err := os.ReadFile(filename) if err != nil { return nil, err } return semver.NewVersion(strings.TrimSpace(string(b))) } func bumpVersion(version *semver.Version, verb string) error { switch verb { case "major": *version = version.IncMajor() case "minor": *version = version.IncMinor() case "patch": *version = version.IncPatch() default: *version = *semver.MustParse(verb) } return nil } func changelog(version *semver.Version) error { // Open changelog target file b, err := os.ReadFile(changelogTarget) if err != nil { return err } // Get the current frontmatter currentChangelog := string(b) sections := strings.SplitN(currentChangelog, "---", 3) if len(sections) != 3 { return errors.New("error: invalid frontmatter") } frontmatter := strings.TrimSpace(sections[1]) // Open changelog source file b, err = os.ReadFile(changelogSource) if err != nil { return err } changelog := string(b) date := time.Now().Format("2006-01-02") // Replace "Unreleased" with the new version and date changelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf("## v%s - %s", version, date)) // Write the changelog to the source file if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil { return err } // Wrap the changelog content with v-pre directive for VitePress to prevent // Vue from interpreting template syntax like {{.TASK_VERSION}} changelogWithVPre := strings.Replace(changelog, "# Changelog\n\n", "# Changelog\n\n::: v-pre\n\n", 1) + "\n:::" // Add the frontmatter to the changelog changelogWithFrontmatter := fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelogWithVPre) // Write the changelog to the target file return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644) } func setVersionFile(fileName string, version *semver.Version) error { return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644) } ================================================ FILE: cmd/sleepit/sleepit.go ================================================ // This code is released under the MIT License // Copyright (c) 2020 Marco Molteni and the timeit contributors. package main import ( "flag" "fmt" "os" "os/signal" "time" ) const usage = `sleepit: sleep for the specified duration, optionally handling signals When the line "sleepit: ready" is printed, it means that it is safe to send signals to it Usage: sleepit [] Commands default Use default action: on reception of SIGINT terminate abruptly handle Handle signals: on reception of SIGINT perform cleanup before exiting version Show the sleepit version` // Filled by the linker. var fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty func main() { os.Exit(run(os.Args[1:])) } func run(args []string) int { if len(args) < 1 { fmt.Fprintln(os.Stderr, usage) return 2 } defaultCmd := flag.NewFlagSet("default", flag.ExitOnError) defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration") handleCmd := flag.NewFlagSet("handle", flag.ExitOnError) handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration") handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration") handleTermAfter := handleCmd.Int("term-after", 0, "Terminate immediately after `N` signals.\n"+ "Default is to terminate only when the cleanup phase has completed.") versionCmd := flag.NewFlagSet("version", flag.ExitOnError) switch args[0] { case "default": _ = defaultCmd.Parse(args[1:]) if len(defaultCmd.Args()) > 0 { fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args()) return 2 } return supervisor(*defaultSleep, 0, 0, nil) case "handle": _ = handleCmd.Parse(args[1:]) if *handleTermAfter == 1 { fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n") return 2 } if len(handleCmd.Args()) > 0 { fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args()) return 2 } sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh) case "version": _ = versionCmd.Parse(args[1:]) if len(versionCmd.Args()) > 0 { fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args()) return 2 } fmt.Printf("sleepit version %s\n", fullVersion) return 0 default: fmt.Fprintln(os.Stderr, usage) return 2 } } func supervisor( sleep time.Duration, cleanup time.Duration, termAfter int, sigCh <-chan os.Signal, ) int { fmt.Printf("sleepit: ready\n") fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n", os.Getpid(), sleep, cleanup) cancelWork := make(chan struct{}) workerDone := worker(cancelWork, sleep, "work") cancelCleaner := make(chan struct{}) var cleanerDone <-chan struct{} sigCount := 0 for { select { case sig := <-sigCh: sigCount++ fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount) if sigCount == 1 { // since `cancelWork` is unbuffered, sending will be synchronous: // we are ensured that the worker has terminated before starting cleanup. // This is important in some real-life situations. cancelWork <- struct{}{} cleanerDone = worker(cancelCleaner, cleanup, "cleanup") } if sigCount == termAfter { cancelCleaner <- struct{}{} return 4 } case <-workerDone: return 0 case <-cleanerDone: return 3 } } } // Start a worker goroutine and return immediately a `workerDone` channel. // The goroutine will prepend its prints with the prefix `name`. // The goroutine will simulate some work and will terminate when one of the following // conditions happens: // 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel. // 2. When something happens on channel `canceled`. Note that this simulates real-life, // so cancellation is not instantaneous: if the caller wants a synchronous cancel, // it should send a message; if instead it wants an asynchronous cancel, it should // close the channel. func worker( canceled <-chan struct{}, howlong time.Duration, name string, ) <-chan struct{} { workerDone := make(chan struct{}) deadline := time.Now().Add(howlong) go func() { fmt.Printf("sleepit: %s started\n", name) for { select { case <-canceled: fmt.Printf("sleepit: %s canceled\n", name) return default: if doSomeWork(deadline) { fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE workerDone <- struct{}{} return } } } }() return workerDone } // Do some work and then return, so that the caller can decide whether to continue or not. // Return true when all work is done. func doSomeWork(deadline time.Time) bool { if time.Now().After(deadline) { return true } timeout := 100 * time.Millisecond time.Sleep(timeout) return false } ================================================ FILE: cmd/task/task.go ================================================ package main import ( "context" "fmt" "os" "path/filepath" "strconv" "github.com/spf13/pflag" "github.com/go-task/task/v3" "github.com/go-task/task/v3/args" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/flags" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/taskfile/ast" ) func main() { if err := run(); err != nil { l := &logger.Logger{ Stdout: os.Stdout, Stderr: os.Stderr, Verbose: flags.Verbose, Color: flags.Color, } if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode { emitCIErrorAnnotation(err) l.Errf(logger.Red, "%v\n", err) os.Exit(err.TaskExitCode()) } if err, ok := err.(errors.TaskError); ok { emitCIErrorAnnotation(err) l.Errf(logger.Red, "%v\n", err) os.Exit(err.Code()) } emitCIErrorAnnotation(err) l.Errf(logger.Red, "%v\n", err) os.Exit(errors.CodeUnknown) } os.Exit(errors.CodeOk) } // emitCIErrorAnnotation emits an error annotation for supported CI providers. func emitCIErrorAnnotation(err error) { if isGA, _ := strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")); !isGA { return } if e, ok := err.(*errors.TaskRunError); ok { fmt.Fprintf(os.Stdout, "::error title=Task '%s' failed::%v\n", e.TaskName, e.Err) return } fmt.Fprintf(os.Stdout, "::error title=Task failed::%v\n", err) } func run() error { log := &logger.Logger{ Stdout: os.Stdout, Stderr: os.Stderr, Verbose: flags.Verbose, Color: flags.Color, } if err := flags.Validate(); err != nil { return err } if err := experiments.Validate(); err != nil { log.Warnf("%s\n", err.Error()) } if flags.Version { fmt.Println(version.GetVersionWithBuildInfo()) return nil } if flags.Help { pflag.Usage() return nil } if flags.Experiments { return log.PrintExperiments() } if flags.Init { wd, err := os.Getwd() if err != nil { return err } args, _, err := args.Get() if err != nil { return err } path := wd if len(args) > 0 { name := args[0] if filepathext.IsExtOnly(name) { name = filepathext.SmartJoin(filepath.Dir(name), "Taskfile"+filepath.Ext(name)) } path = filepathext.SmartJoin(wd, name) } finalPath, err := task.InitTaskfile(path) if err != nil { return err } if !flags.Silent { if flags.Verbose { log.Outf(logger.Default, "%s\n", task.DefaultTaskfile) } log.Outf(logger.Green, "Taskfile created: %s\n", filepathext.TryAbsToRel(finalPath)) } return nil } if flags.Completion != "" { script, err := task.Completion(flags.Completion) if err != nil { return err } fmt.Println(script) return nil } e := task.NewExecutor( flags.WithFlags(), task.WithVersionCheck(true), ) if err := e.Setup(); err != nil { return err } if flags.ClearCache { cachePath := filepath.Join(e.TempDir.Remote, "remote") return os.RemoveAll(cachePath) } listOptions := task.NewListOptions( flags.List, flags.ListAll, flags.ListJson, flags.NoStatus, flags.Nested, ) if listOptions.ShouldListTasks() { if flags.Silent { return e.ListTaskNames(flags.ListAll) } foundTasks, err := e.ListTasks(listOptions) if err != nil { return err } if !foundTasks { os.Exit(errors.CodeUnknown) } return nil } // Parse the remaining arguments cliArgsPreDash, cliArgsPostDash, err := args.Get() if err != nil { return err } calls, globals := args.Parse(cliArgsPreDash...) // If there are no calls, run the default task instead if len(calls) == 0 { calls = append(calls, &task.Call{Task: "default"}) } // Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults e.Taskfile.Vars.Merge(globals, nil) // Then ReverseMerge special variables so they're available for templating cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash) if err != nil { return err } specialVars := ast.NewVars() specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted}) specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash}) specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline}) specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes}) e.Taskfile.Vars.ReverseMerge(specialVars, nil) if !flags.Watch { e.InterceptInterruptSignals() } ctx := context.Background() if flags.Status { return e.Status(ctx, calls...) } return e.Run(ctx, calls...) } ================================================ FILE: compiler.go ================================================ package task import ( "bytes" "context" "fmt" "os" "path/filepath" "strings" "sync" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/taskfile/ast" ) type Compiler struct { Dir string Entrypoint string UserWorkingDir string TaskfileEnv *ast.Vars TaskfileVars *ast.Vars Logger *logger.Logger dynamicCache map[string]string muDynamicCache sync.Mutex } func (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) { return c.getVariables(nil, nil, true) } func (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { return c.getVariables(t, call, true) } func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) { return c.getVariables(t, call, false) } func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) { result := env.GetEnviron() specialVars, err := c.getSpecialVars(t, call) if err != nil { return nil, err } for k, v := range specialVars { result.Set(k, ast.Var{Value: v}) } getRangeFunc := func(dir string) func(k string, v ast.Var) error { return func(k string, v ast.Var) error { cache := &templater.Cache{Vars: result} // Replace values newVar := templater.ReplaceVar(v, cache) // If the variable should not be evaluated, but is nil, set it to an empty string // This stops empty interface errors when using the templater to replace values later // Preserve the Sh field so it can be displayed in summary if !evaluateShVars && newVar.Value == nil { result.Set(k, ast.Var{Value: "", Sh: newVar.Sh}) return nil } // If the variable should not be evaluated and it is set, we can set it and return if !evaluateShVars { result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh}) return nil } // Now we can check for errors since we've handled all the cases when we don't want to evaluate if err := cache.Err(); err != nil { return err } // If the variable is already set, we can set it and return if newVar.Value != nil || newVar.Sh == nil { result.Set(k, ast.Var{Value: newVar.Value}) return nil } // If the variable is dynamic, we need to resolve it first static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result)) if err != nil { return err } result.Set(k, ast.Var{Value: static}) return nil } } rangeFunc := getRangeFunc(c.Dir) var taskRangeFunc func(k string, v ast.Var) error if t != nil { // NOTE(@andreynering): We're manually joining these paths here because // this is the raw task, not the compiled one. cache := &templater.Cache{Vars: result} dir := templater.Replace(t.Dir, cache) if err := cache.Err(); err != nil { return nil, err } dir = filepathext.SmartJoin(c.Dir, dir) taskRangeFunc = getRangeFunc(dir) } for k, v := range c.TaskfileEnv.All() { if err := rangeFunc(k, v); err != nil { return nil, err } } for k, v := range c.TaskfileVars.All() { if err := rangeFunc(k, v); err != nil { return nil, err } } if t != nil { for k, v := range t.IncludeVars.All() { if err := rangeFunc(k, v); err != nil { return nil, err } } for k, v := range t.IncludedTaskfileVars.All() { if err := taskRangeFunc(k, v); err != nil { return nil, err } } } if t == nil || call == nil { return result, nil } for k, v := range call.Vars.All() { if err := rangeFunc(k, v); err != nil { return nil, err } } for k, v := range t.Vars.All() { if err := taskRangeFunc(k, v); err != nil { return nil, err } } return result, nil } func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) { c.muDynamicCache.Lock() defer c.muDynamicCache.Unlock() // If the variable is not dynamic or it is empty, return an empty string if v.Sh == nil || *v.Sh == "" { return "", nil } if c.dynamicCache == nil { c.dynamicCache = make(map[string]string, 30) } if result, ok := c.dynamicCache[*v.Sh]; ok { return result, nil } // NOTE(@andreynering): If a var have a specific dir, use this instead if v.Dir != "" { dir = v.Dir } var stdout bytes.Buffer opts := &execext.RunCommandOptions{ Command: *v.Sh, Dir: dir, Stdout: &stdout, Stderr: c.Logger.Stderr, Env: e, } if err := execext.RunCommand(context.Background(), opts); err != nil { return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err) } // Trim a single trailing newline from the result to make most command // output easier to use in shell commands. result := strings.TrimSuffix(stdout.String(), "\r\n") result = strings.TrimSuffix(result, "\n") c.dynamicCache[*v.Sh] = result c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result) return result, nil } // ResetCache clear the dynamic variables cache func (c *Compiler) ResetCache() { c.muDynamicCache.Lock() defer c.muDynamicCache.Unlock() c.dynamicCache = nil } func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) { allVars := map[string]string{ "TASK_EXE": filepath.ToSlash(os.Args[0]), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_DIR": c.Dir, "USER_WORKING_DIR": c.UserWorkingDir, "TASK_VERSION": version.GetVersion(), } if t != nil { allVars["TASK"] = t.Task allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir) allVars["TASKFILE"] = t.Location.Taskfile allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile) } else { allVars["TASK"] = "" allVars["TASK_DIR"] = "" allVars["TASKFILE"] = "" allVars["TASKFILE_DIR"] = "" } if call != nil { allVars["ALIAS"] = call.Task } else { allVars["ALIAS"] = "" } return allVars, nil } ================================================ FILE: completion/bash/task.bash ================================================ # vim: set tabstop=2 shiftwidth=2 expandtab: _GO_TASK_COMPLETION_LIST_OPTION='--list-all' TASK_CMD="${TASK_EXE:-task}" function _task() { local cur prev words cword _init_completion -n : || return # Check for `--` within command-line and quit or strip suffix. local i for i in "${!words[@]}"; do if [ "${words[$i]}" == "--" ]; then # Do not complete words following `--` passed to CLI_ARGS. [ $cword -gt $i ] && return # Remove the words following `--` to not put --list in CLI_ARGS. words=( "${words[@]:0:$i}" ) break fi done # Handle special arguments of options. case "$prev" in -d|--dir|--remote-cache-dir) _filedir -d return $? ;; --cacert|--cert|--cert-key) _filedir return $? ;; -t|--taskfile) _filedir yaml || return $? _filedir yml return $? ;; -o|--output) COMPREPLY=( $( compgen -W "interleaved group prefixed" -- $cur ) ) return 0 ;; esac # Handle normal options. case "$cur" in -*) COMPREPLY=( $( compgen -W "$(_parse_help $1)" -- $cur ) ) return 0 ;; esac # Prepare task name completions. local tasks=( $( "${words[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) ) COMPREPLY=( $( compgen -W "${tasks[*]}" -- "$cur" ) ) # Post-process because task names might contain colons. __ltrim_colon_completions "$cur" } complete -F _task "$TASK_CMD" ================================================ FILE: completion/fish/task.fish ================================================ set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end) # Cache variables for experiments (global) set -g __task_experiments_cache "" set -g __task_experiments_cache_time 0 # Helper function to get experiments with 1-second cache function __task_get_experiments set -l now (date +%s) set -l ttl 1 # Cache for 1 second only # Return cached value if still valid if test (math "$now - $__task_experiments_cache_time") -lt $ttl printf '%s\n' $__task_experiments_cache return end # Refresh cache set -g __task_experiments_cache (task --experiments 2>/dev/null) set -g __task_experiments_cache_time $now printf '%s\n' $__task_experiments_cache end # Helper function to check if an experiment is enabled function __task_is_experiment_enabled set -l experiment $argv[1] __task_get_experiments | string match -qr "^\* $experiment:.*on" end function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME # Check if the global task is requested set -l global_task false commandline --current-process | read --tokenize --list --local cmd_args for arg in $cmd_args if test "_$arg" = "_--" break # ignore arguments to be passed to the task end if test "_$arg" = "_--global" -o "_$arg" = "_-g" set global_task true break end end # Read the list of tasks (and potential errors) if $global_task $GO_TASK_PROGNAME --global --list-all else $GO_TASK_PROGNAME --list-all end 2>&1 | read -lz rawOutput # Return on non-zero exit code (for cases when there is no Taskfile found or etc.) if test $status -ne 0 return end # Grab names and descriptions (if any) of the tasks set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0) if test $output echo $output end end complete -c $GO_TASK_PROGNAME \ -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \ -xa "(__task_get_tasks)" \ -n "not __fish_seen_subcommand_from --" # Standard flags complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks' complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)' complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks' complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell" complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution' complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names' complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing' complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command' complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments' complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails' complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date' complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory' complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help' complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile' complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads' complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes' complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON' complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions' complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON' complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON' complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables' complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed" complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output' complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output' complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks' complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel' complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing' complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none" complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date' complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary' complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run' complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output' complete -c $GO_TASK_PROGNAME -l version -d 'show version' complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes' complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts' # Experimental flags (dynamically checked at completion time via -n condition) # GentleForce experiment complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies' # RemoteTaskfiles experiment - Options complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles' complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads' complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration' complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)" complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r # RemoteTaskfiles experiment - Operations complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile' complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache' ================================================ FILE: completion/ps/task.ps1 ================================================ using namespace System.Management.Automation Register-ArgumentCompleter -CommandName task -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) if ($commandName.StartsWith('-')) { $completions = @( # Standard flags (alphabetical order) [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'), [CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'), [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'), [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'), [CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'), [CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'), [CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'), [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'), [CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'), [CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'), [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'), [CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'), [CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'), [CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'), [CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'), [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'), [CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'), [CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'), [CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'), [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'), [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'), [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'), [CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'), [CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'), [CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'), [CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'), [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'), [CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'), [CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'), [CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'), [CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'), [CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'), [CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'), [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'), [CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'), [CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'), [CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'), [CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'), [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'), [CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'), [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'), [CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'), [CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'), [CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'), [CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'), [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'), [CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'), [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'), [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'), [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'), [CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'), [CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'), [CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'), [CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes') ) # Experimental flags (dynamically added based on enabled experiments) $experiments = & task --experiments 2>$null | Out-String if ($experiments -match '\* GENTLE_FORCE:.*on') { $completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies') } if ($experiments -match '\* REMOTE_TASKFILES:.*on') { # Options $completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles') $completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout') $completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry') $completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory') $completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate') $completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate') $completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key') # Operations $completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile') $completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache') } return $completions.Where{ $_.CompletionText.StartsWith($commandName) } } return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " } } ================================================ FILE: completion/zsh/_task ================================================ #compdef task typeset -A opt_args TASK_CMD="${TASK_EXE:-task}" compdef _task "$TASK_CMD" _GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}" # Check if an experiment is enabled function __task_is_experiment_enabled() { local experiment=$1 task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on" } # Listing commands from Taskfile.yml function __task_list() { local -a scripts cmd local -i enabled=0 local taskfile item task desc cmd=($TASK_CMD) taskfile=${(Qv)opt_args[(i)-t|--taskfile]} taskfile=${taskfile//\~/$HOME} for arg in "${words[@]:0:$CURRENT}"; do if [[ "$arg" = "--" ]]; then # Use default completion for words after `--` as they are CLI_ARGS. _default return 0 fi done if [[ -n "$taskfile" && -f "$taskfile" ]]; then cmd+=(--taskfile "$taskfile") fi # Check if global flag is set if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then cmd+=(--global) fi if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then enabled=1 fi (( enabled )) || return 0 scripts=() # Read zstyle verbose option (default = true via -T) local show_desc zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false for item in "${(@)${(f)output}[2,-1]#\* }"; do task="${item%%:[[:space:]]*}" if [[ "$show_desc" == "true" ]]; then local desc="${item##[^[:space:]]##[[:space:]]##}" scripts+=( "${task//:/\\:}:$desc" ) else scripts+=( "$task" ) fi done if [[ "$show_desc" == "true" ]]; then _describe 'Task to run' scripts else compadd -Q -a scripts fi } _task() { local -a standard_args operation_args standard_args=( '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' '(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]' '(-f --force)'{-f,--force}'[run even if task is up-to-date]' '(-c --color)'{-c,--color}'[colored output]' '(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)' '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' '(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]' '(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]' '(--dry)--dry[dry-run mode, compile and print tasks only]' '(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]' '(--experiments)--experiments[list available experiments]' '(-g --global)'{-g,--global}'[run global Taskfile from home directory]' '(--insecure)--insecure[allow insecure Taskfile downloads]' '(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: ' '(-j --json)'{-j,--json}'[format task list as JSON]' '(--nested)--nested[nest namespaces when listing as JSON]' '(--no-status)--no-status[ignore status when listing as JSON]' '(--interactive)--interactive[prompt for missing required variables]' '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' '(--output-group-end)--output-group-end[message template after grouped output]:template text: ' '(--output-group-error-only)--output-group-error-only[hide output from successful tasks]' '(-s --silent)'{-s,--silent}'[disable echoing]' '(--sort)--sort[set task sorting order]:order:(default alphanumeric none)' '(--status)--status[exit non-zero if supplied tasks not up-to-date]' '(--summary)--summary[show summary\: field from tasks instead of running them]' '(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' '(-v --verbose)'{-v,--verbose}'[verbose mode]' '(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' '(-y --yes)'{-y,--yes}'[assume yes to all prompts]' ) # Experimental flags (dynamically added based on enabled experiments) # Options (modify behavior) if __task_is_experiment_enabled "GENTLE_FORCE"; then standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]') fi if __task_is_experiment_enabled "REMOTE_TASKFILES"; then standard_args+=( '(--offline --download)--offline[use only local or cached Taskfiles]' '(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: ' '(--expiry)--expiry[cache expiry duration]:duration: ' '(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs' '(--cacert)--cacert[custom CA certificate for TLS]:file:_files' '(--cert)--cert[client certificate for mTLS]:file:_files' '(--cert-key)--cert-key[client certificate private key]:file:_files' ) fi operation_args=( # Task names completion (can be specified multiple times) '(operation)*: :__task_list' # Operational args completion (mutually exclusive) + '(operation)' '(*)'{-l,--list}'[list describable tasks]' '(*)'{-a,--list-all}'[list all tasks]' '(*)'{-i,--init}'[create new Taskfile.yml]' '(- *)'{-h,--help}'[show help]' '(- *)--version[show version and exit]' ) # Experimental operations (dynamically added based on enabled experiments) if __task_is_experiment_enabled "REMOTE_TASKFILES"; then standard_args+=( '(--offline --clear-cache)--download[download remote Taskfile]' ) operation_args+=( '(* --download)--clear-cache[clear remote Taskfile cache]' ) fi _arguments -S $standard_args $operation_args } # don't run the completion function when being source-ed or eval-ed if [ "$funcstack[1]" = "_task" ]; then _task "$@" fi ================================================ FILE: completion.go ================================================ package task import ( _ "embed" "fmt" ) //go:embed completion/bash/task.bash var completionBash string //go:embed completion/fish/task.fish var completionFish string //go:embed completion/ps/task.ps1 var completionPowershell string //go:embed completion/zsh/_task var completionZsh string func Completion(completion string) (string, error) { // Get the file extension for the selected shell switch completion { case "bash": return completionBash, nil case "fish": return completionFish, nil case "powershell": return completionPowershell, nil case "zsh": return completionZsh, nil default: return "", fmt.Errorf("unknown shell: %s", completion) } } ================================================ FILE: concurrency.go ================================================ package task func (e *Executor) acquireConcurrencyLimit() func() { if e.concurrencySemaphore == nil { return emptyFunc } e.concurrencySemaphore <- struct{}{} return func() { <-e.concurrencySemaphore } } func (e *Executor) releaseConcurrencyLimit() func() { if e.concurrencySemaphore == nil { return emptyFunc } <-e.concurrencySemaphore return func() { e.concurrencySemaphore <- struct{}{} } } func emptyFunc() {} ================================================ FILE: errors/error_taskfile_decode.go ================================================ package errors import ( "bytes" "cmp" "errors" "fmt" "strings" "github.com/fatih/color" "go.yaml.in/yaml/v3" ) type ( TaskfileDecodeError struct { Message string Location string Line int Column int Tag string Snippet string Err error } ) func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError { // If the error is already a DecodeError, return it taskfileInvalidErr := &TaskfileDecodeError{} if errors.As(err, &taskfileInvalidErr) { return taskfileInvalidErr } return &TaskfileDecodeError{ Line: node.Line, Column: node.Column, Tag: node.ShortTag(), Err: err, } } func (err *TaskfileDecodeError) Error() string { buf := &bytes.Buffer{} // Print the error message if err.Message != "" { fmt.Fprintln(buf, color.RedString("err: %s", err.Message)) } else { // Extract the errors from the TypeError te := &yaml.TypeError{} if errors.As(err.Err, &te) { if len(te.Errors) > 1 { fmt.Fprintln(buf, color.RedString("errs:")) for _, message := range te.Errors { fmt.Fprintln(buf, color.RedString("- %s", message)) } } else { fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0])) } } else { // Otherwise print the error message normally fmt.Fprintln(buf, color.RedString("err: %s", err.Err)) } } fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column)) fmt.Fprint(buf, err.Snippet) return buf.String() } func (err *TaskfileDecodeError) Debug() string { const indentWidth = 2 buf := &bytes.Buffer{} fmt.Fprintln(buf, "TaskfileDecodeError:") // Recursively loop through the error chain and print any details var debug func(error, int) debug = func(err error, indent int) { indentStr := strings.Repeat(" ", indent*indentWidth) // Nothing left to unwrap if err == nil { fmt.Fprintf(buf, "%sEnd of chain\n", indentStr) return } // Taskfile decode error decodeErr := &TaskfileDecodeError{} if errors.As(err, &decodeErr) { fmt.Fprintf(buf, "%s%s (%s:%d:%d)\n", indentStr, cmp.Or(decodeErr.Message, ""), decodeErr.Location, decodeErr.Line, decodeErr.Column, ) debug(errors.Unwrap(err), indent+1) return } fmt.Fprintf(buf, "%s%s\n", indentStr, err) debug(errors.Unwrap(err), indent+1) } debug(err, 0) return buf.String() } func (err *TaskfileDecodeError) Unwrap() error { return err.Err } func (err *TaskfileDecodeError) Code() int { return CodeTaskfileDecode } func (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError { err.Message = fmt.Sprintf(format, a...) return err } func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError { err.Message = fmt.Sprintf("cannot unmarshal %s into %s", err.Tag, t) return err } func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError { err.Location = location err.Snippet = snippet return err } ================================================ FILE: errors/errors.go ================================================ package errors import "errors" // General exit codes const ( CodeOk int = iota // Used when the program exits without errors CodeUnknown // Used when no other exit code is appropriate ) // TaskRC related exit codes const ( CodeTaskRCNotFoundError int = iota + 50 ) // Taskfile related exit codes const ( CodeTaskfileNotFound int = iota + 100 CodeTaskfileAlreadyExists CodeTaskfileDecode CodeTaskfileFetchFailed CodeTaskfileNotTrusted CodeTaskfileNotSecure CodeTaskfileCacheNotFound CodeTaskfileVersionCheckError CodeTaskfileNetworkTimeout CodeTaskfileInvalid CodeTaskfileCycle CodeTaskfileDoesNotMatchChecksum ) // Task related exit codes const ( CodeTaskNotFound int = iota + 200 CodeTaskRunError CodeTaskInternal CodeTaskNameConflict CodeTaskCalledTooManyTimes CodeTaskCancelled CodeTaskMissingRequiredVars CodeTaskNotAllowedVars ) // TaskError extends the standard error interface with a Code method. This code will // be used as the exit code of the program which allows the user to distinguish // between different types of errors. type TaskError interface { error Code() int } // New returns an error that formats as the given text. Each call to New returns // a distinct error value even if the text is identical. This wraps the standard // errors.New function so that we don't need to alias that package. func New(text string) error { return errors.New(text) } // Is wraps the standard errors.Is function so that we don't need to alias that package. func Is(err, target error) bool { return errors.Is(err, target) } // As wraps the standard errors.As function so that we don't need to alias that package. func As(err error, target any) bool { return errors.As(err, target) } // Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package. func Unwrap(err error) error { return errors.Unwrap(err) } ================================================ FILE: errors/errors_task.go ================================================ package errors import ( "errors" "fmt" "strings" "mvdan.cc/sh/v3/interp" ) // TaskNotFoundError is returned when the specified task is not found in the // Taskfile. type TaskNotFoundError struct { TaskName string DidYouMean string } func (err *TaskNotFoundError) Error() string { if err.DidYouMean != "" { return fmt.Sprintf( `task: Task %q does not exist. Did you mean %q?`, err.TaskName, err.DidYouMean, ) } return fmt.Sprintf(`task: Task %q does not exist`, err.TaskName) } func (err *TaskNotFoundError) Code() int { return CodeTaskNotFound } // TaskRunError is returned when a command in a task returns a non-zero exit // code. type TaskRunError struct { TaskName string Err error } func (err *TaskRunError) Error() string { return fmt.Sprintf(`task: Failed to run task %q: %v`, err.TaskName, err.Err) } func (err *TaskRunError) Code() int { return CodeTaskRunError } func (err *TaskRunError) TaskExitCode() int { var exit interp.ExitStatus if errors.As(err.Err, &exit) { return int(exit) } return err.Code() } func (err *TaskRunError) Unwrap() error { return err.Err } // TaskInternalError when the user attempts to invoke a task that is internal. type TaskInternalError struct { TaskName string } func (err *TaskInternalError) Error() string { return fmt.Sprintf(`task: Task "%s" is internal`, err.TaskName) } func (err *TaskInternalError) Code() int { return CodeTaskInternal } // TaskNameConflictError is returned when multiple tasks with a matching name or // alias are found. type TaskNameConflictError struct { Call string TaskNames []string } func (err *TaskNameConflictError) Error() string { return fmt.Sprintf(`task: Found multiple tasks (%s) that match %q`, strings.Join(err.TaskNames, ", "), err.Call) } func (err *TaskNameConflictError) Code() int { return CodeTaskNameConflict } type TaskNameFlattenConflictError struct { TaskName string Include string } func (err *TaskNameFlattenConflictError) Error() string { return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include) } func (err *TaskNameFlattenConflictError) Code() int { return CodeTaskNameConflict } // TaskCalledTooManyTimesError is returned when the maximum task call limit is // exceeded. This is to prevent infinite loops and cyclic dependencies. type TaskCalledTooManyTimesError struct { TaskName string MaximumTaskCall int } func (err *TaskCalledTooManyTimesError) Error() string { return fmt.Sprintf( `task: Maximum task call exceeded (%d) for task %q: probably an cyclic dep or infinite loop`, err.MaximumTaskCall, err.TaskName, ) } func (err *TaskCalledTooManyTimesError) Code() int { return CodeTaskCalledTooManyTimes } // TaskCancelledByUserError is returned when the user does not accept an optional prompt to continue. type TaskCancelledByUserError struct { TaskName string } func (err *TaskCancelledByUserError) Error() string { return fmt.Sprintf(`task: Task %q cancelled by user`, err.TaskName) } func (err *TaskCancelledByUserError) Code() int { return CodeTaskCancelled } // TaskCancelledNoTerminalError is returned when trying to run a task with a prompt in a non-terminal environment. type TaskCancelledNoTerminalError struct { TaskName string } func (err *TaskCancelledNoTerminalError) Error() string { return fmt.Sprintf( `task: Task %q cancelled because it has a prompt and the environment is not a terminal. Use --yes (-y) to run anyway.`, err.TaskName, ) } func (err *TaskCancelledNoTerminalError) Code() int { return CodeTaskCancelled } // TaskMissingRequiredVarsError is returned when a task is missing required variables. type MissingVar struct { Name string AllowedValues []string } type TaskMissingRequiredVarsError struct { TaskName string MissingVars []MissingVar } func (v MissingVar) String() string { if len(v.AllowedValues) == 0 { return v.Name } return fmt.Sprintf("%s (allowed values: %v)", v.Name, v.AllowedValues) } func (err *TaskMissingRequiredVarsError) Error() string { vars := make([]string, 0, len(err.MissingVars)) for _, v := range err.MissingVars { vars = append(vars, v.String()) } return fmt.Sprintf( `task: Task %q cancelled because it is missing required variables: %s`, err.TaskName, strings.Join(vars, ", ")) } func (err *TaskMissingRequiredVarsError) Code() int { return CodeTaskMissingRequiredVars } type NotAllowedVar struct { Value string Enum []string Name string } type TaskNotAllowedVarsError struct { TaskName string NotAllowedVars []NotAllowedVar } func (err *TaskNotAllowedVarsError) Error() string { var builder strings.Builder builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) //nolint:staticcheck for _, s := range err.NotAllowedVars { builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum)) //nolint:staticcheck } return builder.String() } func (err *TaskNotAllowedVarsError) Code() int { return CodeTaskNotAllowedVars } ================================================ FILE: errors/errors_taskfile.go ================================================ package errors import ( "fmt" "net/http" "time" "github.com/Masterminds/semver/v3" ) // TaskfileNotFoundError is returned when no appropriate Taskfile is found when // searching the filesystem. type TaskfileNotFoundError struct { URI string Walk bool AskInit bool OwnerChange bool } func (err TaskfileNotFoundError) Error() string { var walkText string if err.OwnerChange { walkText = " (or any of the parent directories until ownership changed)." } else if err.Walk { walkText = " (or any of the parent directories)." } if err.AskInit { walkText += " Run `task --init` to create a new Taskfile." } return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText) } func (err TaskfileNotFoundError) Code() int { return CodeTaskfileNotFound } // TaskfileAlreadyExistsError is returned on creating a Taskfile if one already // exists. type TaskfileAlreadyExistsError struct{} func (err TaskfileAlreadyExistsError) Error() string { return "task: A Taskfile already exists" } func (err TaskfileAlreadyExistsError) Code() int { return CodeTaskfileAlreadyExists } // TaskfileInvalidError is returned when the Taskfile contains syntax errors or // cannot be parsed for some reason. type TaskfileInvalidError struct { URI string Err error } func (err TaskfileInvalidError) Error() string { return fmt.Sprintf("task: Failed to parse %s:\n%v", err.URI, err.Err) } func (err TaskfileInvalidError) Code() int { return CodeTaskfileInvalid } // TaskfileFetchFailedError is returned when no appropriate Taskfile is found when // searching the filesystem. type TaskfileFetchFailedError struct { URI string HTTPStatusCode int } func (err TaskfileFetchFailedError) Error() string { var statusText string if err.HTTPStatusCode != 0 { statusText = fmt.Sprintf(" with status code %d (%s)", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode)) } return fmt.Sprintf(`task: Download of %q failed%s`, err.URI, statusText) } func (err TaskfileFetchFailedError) Code() int { return CodeTaskfileFetchFailed } // TaskfileNotTrustedError is returned when the user does not accept the trust // prompt when downloading a remote Taskfile. type TaskfileNotTrustedError struct { URI string } func (err *TaskfileNotTrustedError) Error() string { return fmt.Sprintf( `task: Taskfile %q not trusted by user`, err.URI, ) } func (err *TaskfileNotTrustedError) Code() int { return CodeTaskfileNotTrusted } // TaskfileNotSecureError is returned when the user attempts to download a // remote Taskfile over an insecure connection. type TaskfileNotSecureError struct { URI string } func (err *TaskfileNotSecureError) Error() string { return fmt.Sprintf( `task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`, err.URI, ) } func (err *TaskfileNotSecureError) Code() int { return CodeTaskfileNotSecure } // TaskfileCacheNotFoundError is returned when the user attempts to use an offline // (cached) Taskfile but the files does not exist in the cache. type TaskfileCacheNotFoundError struct { URI string } func (err *TaskfileCacheNotFoundError) Error() string { return fmt.Sprintf( `task: Taskfile %q was not found in the cache. Remove the --offline flag to use a remote copy or download it using the --download flag`, err.URI, ) } func (err *TaskfileCacheNotFoundError) Code() int { return CodeTaskfileCacheNotFound } // TaskfileVersionCheckError is returned when the user attempts to run a // Taskfile that does not contain a Taskfile schema version key or if they try // to use a feature that is not supported by the schema version. type TaskfileVersionCheckError struct { URI string SchemaVersion *semver.Version Message string } func (err *TaskfileVersionCheckError) Error() string { if err.SchemaVersion == nil { return fmt.Sprintf( `task: Missing schema version in Taskfile %q`, err.URI, ) } return fmt.Sprintf( "task: Invalid schema version in Taskfile %q:\nSchema version (%s) %s", err.URI, err.SchemaVersion.String(), err.Message, ) } func (err *TaskfileVersionCheckError) Code() int { return CodeTaskfileVersionCheckError } // TaskfileNetworkTimeoutError is returned when the user attempts to use a remote // Taskfile but a network connection could not be established within the timeout. type TaskfileNetworkTimeoutError struct { URI string Timeout time.Duration } func (err *TaskfileNetworkTimeoutError) Error() string { return fmt.Sprintf( `task: Network connection timed out after %s while attempting to download Taskfile %q`, err.Timeout, err.URI, ) } func (err *TaskfileNetworkTimeoutError) Code() int { return CodeTaskfileNetworkTimeout } // TaskfileCycleError is returned when we detect that a Taskfile includes a // set of Taskfiles that include each other in a cycle. type TaskfileCycleError struct { Source string Destination string } func (err TaskfileCycleError) Error() string { return fmt.Sprintf("task: include cycle detected between %s <--> %s", err.Source, err.Destination, ) } func (err TaskfileCycleError) Code() int { return CodeTaskfileCycle } // TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not // match the one pinned in the parent Taskfile. type TaskfileDoesNotMatchChecksum struct { URI string ExpectedChecksum string ActualChecksum string } func (err *TaskfileDoesNotMatchChecksum) Error() string { return fmt.Sprintf( "task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q", err.URI, err.ActualChecksum, err.ExpectedChecksum, ) } func (err *TaskfileDoesNotMatchChecksum) Code() int { return CodeTaskfileDoesNotMatchChecksum } ================================================ FILE: errors/errors_taskrc.go ================================================ package errors import "fmt" type TaskRCNotFoundError struct { URI string Walk bool } func (err TaskRCNotFoundError) Error() string { var walkText string if err.Walk { walkText = " (or any of the parent directories)" } return fmt.Sprintf(`task: No Task config file found at %q%s`, err.URI, walkText) } func (err TaskRCNotFoundError) Code() int { return CodeTaskRCNotFoundError } ================================================ FILE: executor.go ================================================ package task import ( "context" "io" "os" "sync" "time" "github.com/puzpuzpuz/xsync/v4" "github.com/sajari/fuzzy" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile/ast" ) type ( // An ExecutorOption is any type that can apply a configuration to an // [Executor]. ExecutorOption interface { ApplyToExecutor(*Executor) } // An Executor is used for processing Taskfile(s) and executing the task(s) // within them. Executor struct { // Flags Dir string Entrypoint string TempDir TempDir Force bool ForceAll bool Insecure bool Download bool Offline bool TrustedHosts []string Timeout time.Duration CacheExpiryDuration time.Duration RemoteCacheDir string CACert string Cert string CertKey string Watch bool Verbose bool Silent bool DisableFuzzy bool AssumeYes bool AssumeTerm bool // Used for testing Interactive bool Dry bool Summary bool Parallel bool Color bool Concurrency int Interval time.Duration Failfast bool // I/O Stdin io.Reader Stdout io.Writer Stderr io.Writer // Internal Taskfile *ast.Taskfile Logger *logger.Logger Compiler *Compiler Output output.Output OutputStyle ast.Output TaskSorter sort.Sorter UserWorkingDir string EnableVersionCheck bool fuzzyModel *fuzzy.Model fuzzyModelOnce sync.Once promptedVars *ast.Vars // vars collected via interactive prompts concurrencySemaphore chan struct{} taskCallCount map[string]*int32 mkdirMutexMap map[string]*sync.Mutex executionHashes map[string]context.Context executionHashesMutex sync.Mutex watchedDirs *xsync.Map[string, bool] } TempDir struct { Remote string Fingerprint string } ) // NewExecutor creates a new [Executor] and applies the given functional options // to it. func NewExecutor(opts ...ExecutorOption) *Executor { e := &Executor{ Timeout: time.Second * 10, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, Logger: nil, Compiler: nil, Output: nil, OutputStyle: ast.Output{}, TaskSorter: sort.AlphaNumericWithRootTasksFirst, UserWorkingDir: "", fuzzyModel: nil, concurrencySemaphore: nil, taskCallCount: map[string]*int32{}, mkdirMutexMap: map[string]*sync.Mutex{}, executionHashes: map[string]context.Context{}, executionHashesMutex: sync.Mutex{}, } e.Options(opts...) return e } // Options loops through the given [ExecutorOption] functions and applies them // to the [Executor]. func (e *Executor) Options(opts ...ExecutorOption) { for _, opt := range opts { opt.ApplyToExecutor(e) } } // WithDir sets the working directory of the [Executor]. By default, the // directory is set to the user's current working directory. func WithDir(dir string) ExecutorOption { return &dirOption{dir} } type dirOption struct { dir string } func (o *dirOption) ApplyToExecutor(e *Executor) { e.Dir = o.dir } // WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By // default, Task will search for one of the default Taskfiles in the given // directory. func WithEntrypoint(entrypoint string) ExecutorOption { return &entrypointOption{entrypoint} } type entrypointOption struct { entrypoint string } func (o *entrypointOption) ApplyToExecutor(e *Executor) { e.Entrypoint = o.entrypoint } // WithTempDir sets the temporary directory that will be used by [Executor] for // storing temporary files like checksums and cached remote files. By default, // the temporary directory is set to the user's temporary directory. func WithTempDir(tempDir TempDir) ExecutorOption { return &tempDirOption{tempDir} } type tempDirOption struct { tempDir TempDir } func (o *tempDirOption) ApplyToExecutor(e *Executor) { e.TempDir = o.tempDir } // WithForce ensures that the [Executor] always runs a task, even when // fingerprinting or prompts would normally stop it. func WithForce(force bool) ExecutorOption { return &forceOption{force} } type forceOption struct { force bool } func (o *forceOption) ApplyToExecutor(e *Executor) { e.Force = o.force } // WithForceAll ensures that the [Executor] always runs all tasks (including // subtasks), even when fingerprinting or prompts would normally stop them. func WithForceAll(forceAll bool) ExecutorOption { return &forceAllOption{forceAll} } type forceAllOption struct { forceAll bool } func (o *forceAllOption) ApplyToExecutor(e *Executor) { e.ForceAll = o.forceAll } // WithInsecure allows the [Executor] to make insecure connections when reading // remote taskfiles. By default, insecure connections are rejected. func WithInsecure(insecure bool) ExecutorOption { return &insecureOption{insecure} } type insecureOption struct { insecure bool } func (o *insecureOption) ApplyToExecutor(e *Executor) { e.Insecure = o.insecure } // WithDownload forces the [Executor] to download a fresh copy of the taskfile // from the remote source. func WithDownload(download bool) ExecutorOption { return &downloadOption{download} } type downloadOption struct { download bool } func (o *downloadOption) ApplyToExecutor(e *Executor) { e.Download = o.download } // WithOffline stops the [Executor] from being able to make network connections. // It will still be able to read local files and cached copies of remote files. func WithOffline(offline bool) ExecutorOption { return &offlineOption{offline} } type offlineOption struct { offline bool } func (o *offlineOption) ApplyToExecutor(e *Executor) { e.Offline = o.offline } // WithTrustedHosts configures the [Executor] with a list of trusted hosts for remote // Taskfiles. Hosts in this list will not prompt for user confirmation. func WithTrustedHosts(trustedHosts []string) ExecutorOption { return &trustedHostsOption{trustedHosts} } type trustedHostsOption struct { trustedHosts []string } func (o *trustedHostsOption) ApplyToExecutor(e *Executor) { e.TrustedHosts = o.trustedHosts } // WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By // default, the timeout is set to 10 seconds. func WithTimeout(timeout time.Duration) ExecutorOption { return &timeoutOption{timeout} } type timeoutOption struct { timeout time.Duration } func (o *timeoutOption) ApplyToExecutor(e *Executor) { e.Timeout = o.timeout } // WithCacheExpiryDuration sets the duration after which the cache is considered // expired. By default, the cache is 0 (disabled). func WithCacheExpiryDuration(duration time.Duration) ExecutorOption { return &cacheExpiryDurationOption{duration: duration} } type cacheExpiryDurationOption struct { duration time.Duration } func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) { r.CacheExpiryDuration = o.duration } // WithRemoteCacheDir sets the directory where remote taskfiles are cached. func WithRemoteCacheDir(dir string) ExecutorOption { return &remoteCacheDirOption{dir: dir} } type remoteCacheDirOption struct { dir string } func (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) { e.RemoteCacheDir = o.dir } // WithCACert sets the path to a custom CA certificate for TLS connections. func WithCACert(caCert string) ExecutorOption { return &caCertOption{caCert: caCert} } type caCertOption struct { caCert string } func (o *caCertOption) ApplyToExecutor(e *Executor) { e.CACert = o.caCert } // WithCert sets the path to a client certificate for TLS connections. func WithCert(cert string) ExecutorOption { return &certOption{cert: cert} } type certOption struct { cert string } func (o *certOption) ApplyToExecutor(e *Executor) { e.Cert = o.cert } // WithCertKey sets the path to a client certificate key for TLS connections. func WithCertKey(certKey string) ExecutorOption { return &certKeyOption{certKey: certKey} } type certKeyOption struct { certKey string } func (o *certKeyOption) ApplyToExecutor(e *Executor) { e.CertKey = o.certKey } // WithWatch tells the [Executor] to keep running in the background and watch // for changes to the fingerprint of the tasks that are run. When changes are // detected, a new task run is triggered. func WithWatch(watch bool) ExecutorOption { return &watchOption{watch} } type watchOption struct { watch bool } func (o *watchOption) ApplyToExecutor(e *Executor) { e.Watch = o.watch } // WithVerbose tells the [Executor] to output more information about the tasks // that are run. func WithVerbose(verbose bool) ExecutorOption { return &verboseOption{verbose} } type verboseOption struct { verbose bool } func (o *verboseOption) ApplyToExecutor(e *Executor) { e.Verbose = o.verbose } // WithSilent tells the [Executor] to suppress all output except for the output // of the tasks that are run. func WithSilent(silent bool) ExecutorOption { return &silentOption{silent} } type silentOption struct { silent bool } func (o *silentOption) ApplyToExecutor(e *Executor) { e.Silent = o.silent } // WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names. func WithDisableFuzzy(disableFuzzy bool) ExecutorOption { return &disableFuzzyOption{disableFuzzy} } type disableFuzzyOption struct { disableFuzzy bool } func (o *disableFuzzyOption) ApplyToExecutor(e *Executor) { e.DisableFuzzy = o.disableFuzzy } // WithAssumeYes tells the [Executor] to assume "yes" for all prompts. func WithAssumeYes(assumeYes bool) ExecutorOption { return &assumeYesOption{assumeYes} } type assumeYesOption struct { assumeYes bool } func (o *assumeYesOption) ApplyToExecutor(e *Executor) { e.AssumeYes = o.assumeYes } // WithAssumeTerm is used for testing purposes to simulate a terminal. func WithAssumeTerm(assumeTerm bool) ExecutorOption { return &assumeTermOption{assumeTerm} } type assumeTermOption struct { assumeTerm bool } func (o *assumeTermOption) ApplyToExecutor(e *Executor) { e.AssumeTerm = o.assumeTerm } // WithInteractive tells the [Executor] to prompt for missing required variables. func WithInteractive(interactive bool) ExecutorOption { return &interactiveOption{interactive} } type interactiveOption struct { interactive bool } func (o *interactiveOption) ApplyToExecutor(e *Executor) { e.Interactive = o.interactive } // WithDry tells the [Executor] to output the commands that would be run without // actually running them. func WithDry(dry bool) ExecutorOption { return &dryOption{dry} } type dryOption struct { dry bool } func (o *dryOption) ApplyToExecutor(e *Executor) { e.Dry = o.dry } // WithSummary tells the [Executor] to output a summary of the given tasks // instead of running them. func WithSummary(summary bool) ExecutorOption { return &summaryOption{summary} } type summaryOption struct { summary bool } func (o *summaryOption) ApplyToExecutor(e *Executor) { e.Summary = o.summary } // WithParallel tells the [Executor] to run tasks given in the same call in // parallel. func WithParallel(parallel bool) ExecutorOption { return ¶llelOption{parallel} } type parallelOption struct { parallel bool } func (o *parallelOption) ApplyToExecutor(e *Executor) { e.Parallel = o.parallel } // WithColor tells the [Executor] whether or not to output using colorized // strings. func WithColor(color bool) ExecutorOption { return &colorOption{color} } type colorOption struct { color bool } func (o *colorOption) ApplyToExecutor(e *Executor) { e.Color = o.color } // WithConcurrency sets the maximum number of tasks that the [Executor] can run // in parallel. func WithConcurrency(concurrency int) ExecutorOption { return &concurrencyOption{concurrency} } type concurrencyOption struct { concurrency int } func (o *concurrencyOption) ApplyToExecutor(e *Executor) { e.Concurrency = o.concurrency } // WithInterval sets the interval at which the [Executor] will wait for // duplicated events before running a task. func WithInterval(interval time.Duration) ExecutorOption { return &intervalOption{interval} } type intervalOption struct { interval time.Duration } func (o *intervalOption) ApplyToExecutor(e *Executor) { e.Interval = o.interval } // WithOutputStyle sets the output style of the [Executor]. By default, the // output style is set to the style defined in the Taskfile. func WithOutputStyle(outputStyle ast.Output) ExecutorOption { return &outputStyleOption{outputStyle} } type outputStyleOption struct { outputStyle ast.Output } func (o *outputStyleOption) ApplyToExecutor(e *Executor) { e.OutputStyle = o.outputStyle } // WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By // default, the sorter is set to sort tasks alphabetically, but with tasks with // no namespace (in the root Taskfile) first. func WithTaskSorter(sorter sort.Sorter) ExecutorOption { return &taskSorterOption{sorter} } type taskSorterOption struct { sorter sort.Sorter } func (o *taskSorterOption) ApplyToExecutor(e *Executor) { e.TaskSorter = o.sorter } // WithStdin sets the [Executor]'s standard input [io.Reader]. func WithStdin(stdin io.Reader) ExecutorOption { return &stdinOption{stdin} } type stdinOption struct { stdin io.Reader } func (o *stdinOption) ApplyToExecutor(e *Executor) { e.Stdin = o.stdin } // WithStdout sets the [Executor]'s standard output [io.Writer]. func WithStdout(stdout io.Writer) ExecutorOption { return &stdoutOption{stdout} } type stdoutOption struct { stdout io.Writer } func (o *stdoutOption) ApplyToExecutor(e *Executor) { e.Stdout = o.stdout } // WithStderr sets the [Executor]'s standard error [io.Writer]. func WithStderr(stderr io.Writer) ExecutorOption { return &stderrOption{stderr} } type stderrOption struct { stderr io.Writer } func (o *stderrOption) ApplyToExecutor(e *Executor) { e.Stderr = o.stderr } // WithIO sets the [Executor]'s standard input, output, and error to the same // [io.ReadWriter]. func WithIO(rw io.ReadWriter) ExecutorOption { return &ioOption{rw} } type ioOption struct { rw io.ReadWriter } func (o *ioOption) ApplyToExecutor(e *Executor) { e.Stdin = o.rw e.Stdout = o.rw e.Stderr = o.rw } // WithVersionCheck tells the [Executor] whether or not to check the version of func WithVersionCheck(enableVersionCheck bool) ExecutorOption { return &versionCheckOption{enableVersionCheck} } type versionCheckOption struct { enableVersionCheck bool } func (o *versionCheckOption) ApplyToExecutor(e *Executor) { e.EnableVersionCheck = o.enableVersionCheck } // WithFailfast tells the [Executor] whether or not to check the version of func WithFailfast(failfast bool) ExecutorOption { return &failfastOption{failfast} } type failfastOption struct { failfast bool } func (o *failfastOption) ApplyToExecutor(e *Executor) { e.Failfast = o.failfast } ================================================ FILE: executor_test.go ================================================ package task_test import ( "bytes" "cmp" "fmt" "os" "path/filepath" "testing" "github.com/sebdah/goldie/v2" "github.com/stretchr/testify/require" "github.com/go-task/task/v3" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile/ast" ) type ( // A ExecutorTestOption is a function that configures an [ExecutorTest]. ExecutorTestOption interface { applyToExecutorTest(*ExecutorTest) } // A ExecutorTest is a test wrapper around a [task.Executor] to make it easy // to write tests for tasks. See [NewExecutorTest] for information on // creating and running ExecutorTests. These tests use fixture files to // assert whether the result of a task is correct. If Task's behavior has // been changed, the fixture files can be updated by running `task // gen:fixtures`. ExecutorTest struct { TaskTest task string vars map[string]any input string executorOpts []task.ExecutorOption wantSetupError bool wantRunError bool wantStatusError bool } ) // NewExecutorTest sets up a new [task.Executor] with the given options and runs // a task with the given [ExecutorTestOption]s. The output of the task is // written to a set of fixture files depending on the configuration of the test. func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) { t.Helper() tt := &ExecutorTest{ task: "default", vars: map[string]any{}, TaskTest: TaskTest{ experiments: map[*experiments.Experiment]int{}, fixtureTemplateData: map[string]any{}, }, } // Apply the functional options for _, opt := range opts { opt.applyToExecutorTest(tt) } // Enable any experiments that have been set for x, v := range tt.experiments { prev := *x *x = experiments.Experiment{ Name: prev.Name, AllowedValues: []int{v}, Value: v, } t.Cleanup(func() { *x = prev }) } tt.run(t) } // Functional options // WithInput tells the test to create a reader with the given input. This can be // used to simulate user input when a task requires it. func WithInput(input string) ExecutorTestOption { return &inputTestOption{input} } type inputTestOption struct { input string } func (opt *inputTestOption) applyToExecutorTest(t *ExecutorTest) { t.input = opt.input } // WithRunError tells the test to expect an error during the run phase of the // task execution. A fixture will be created with the output of any errors. func WithRunError() ExecutorTestOption { return &runErrorTestOption{} } type runErrorTestOption struct{} func (opt *runErrorTestOption) applyToExecutorTest(t *ExecutorTest) { t.wantRunError = true } // WithStatusError tells the test to make an additional call to // [task.Executor.Status] after the task has been run. A fixture will be created // with the output of any errors. func WithStatusError() ExecutorTestOption { return &statusErrorTestOption{} } type statusErrorTestOption struct{} func (opt *statusErrorTestOption) applyToExecutorTest(t *ExecutorTest) { t.wantStatusError = true } // Helpers // writeFixtureErrRun is a wrapper for writing the output of an error during the // run phase of the task to a fixture file. func (tt *ExecutorTest) writeFixtureErrRun( t *testing.T, g *goldie.Goldie, err error, ) { t.Helper() tt.writeFixture(t, g, "err-run", []byte(err.Error())) } // writeFixtureStatus is a wrapper for writing the output of an error when // making an additional call to [task.Executor.Status] to a fixture file. func (tt *ExecutorTest) writeFixtureStatus( t *testing.T, g *goldie.Goldie, status string, ) { t.Helper() tt.writeFixture(t, g, "err-status", []byte(status)) } // run is the main function for running the test. It sets up the task executor, // runs the task, and writes the output to a fixture file. func (tt *ExecutorTest) run(t *testing.T) { t.Helper() f := func(t *testing.T) { t.Helper() var buffer SyncBuffer opts := append( tt.executorOpts, task.WithStdout(&buffer), task.WithStderr(&buffer), ) // If the test has input, create a reader for it and add it to the // executor options if tt.input != "" { var reader bytes.Buffer reader.WriteString(tt.input) opts = append(opts, task.WithStdin(&reader)) } // Set up the task executor e := task.NewExecutor(opts...) // Create a golden fixture file for the output g := goldie.New(t, goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")), ) // Call setup and check for errors if err := e.Setup(); tt.wantSetupError { require.Error(t, err) tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureBuffer(t, g, buffer.buf) return } else { require.NoError(t, err) } // Create the task call vars := ast.NewVars() for key, value := range tt.vars { vars.Set(key, ast.Var{Value: value}) } call := &task.Call{ Task: tt.task, Vars: vars, } // Run the task and check for errors ctx := t.Context() if err := e.Run(ctx, call); tt.wantRunError { require.Error(t, err) tt.writeFixtureErrRun(t, g, err) tt.writeFixtureBuffer(t, g, buffer.buf) return } else { require.NoError(t, err) } // If the status flag is set, run the status check if tt.wantStatusError { if err := e.Status(ctx, call); err != nil { tt.writeFixtureStatus(t, g, err.Error()) } } tt.writeFixtureBuffer(t, g, buffer.buf) } // Run the test (with a name if it has one) if tt.name != "" { t.Run(tt.name, f) } else { f(t) } } func TestEmptyTask(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/empty_task"), ), ) } func TestEmptyTaskfile(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/empty_taskfile"), ), WithSetupError(), WithFixtureTemplating(), ) } func TestEnv(t *testing.T) { t.Setenv("QUX", "from_os") NewExecutorTest(t, WithName("env precedence disabled"), WithExecutorOptions( task.WithDir("testdata/env"), task.WithSilent(true), ), ) NewExecutorTest(t, WithName("env precedence enabled"), WithExecutorOptions( task.WithDir("testdata/env"), task.WithSilent(true), ), WithExperiment(&experiments.EnvPrecedence, 1), ) } func TestVars(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/vars"), task.WithSilent(true), ), ) NewExecutorTest(t, WithName("cli-var-priority-default"), WithExecutorOptions( task.WithDir("testdata/vars"), task.WithSilent(true), ), WithTask("cli-var-priority"), ) NewExecutorTest(t, WithName("cli-var-priority-override"), WithExecutorOptions( task.WithDir("testdata/vars"), task.WithSilent(true), ), WithTask("cli-var-priority"), WithVar("CLI_VAR", "from_cli"), ) } func TestRequires(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("required var missing"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("missing-var"), WithRunError(), ) NewExecutorTest(t, WithName("required var ok"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("missing-var"), WithVar("FOO", "bar"), ) NewExecutorTest(t, WithName("fails validation"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("validation-var"), WithVar("ENV", "dev"), WithVar("FOO", "bar"), WithRunError(), ) NewExecutorTest(t, WithName("passes validation"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("validation-var"), WithVar("FOO", "one"), WithVar("ENV", "dev"), ) NewExecutorTest(t, WithName("required var missing + fails validation"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("validation-var"), WithRunError(), ) NewExecutorTest(t, WithName("required var missing + fails validation"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("validation-var-dynamic"), WithVar("FOO", "one"), WithVar("ENV", "dev"), ) NewExecutorTest(t, WithName("require before compile"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("require-before-compile"), WithRunError(), ) NewExecutorTest(t, WithName("var defined in task"), WithExecutorOptions( task.WithDir("testdata/requires"), ), WithTask("var-defined-in-task"), ) } // TODO: mock fs func TestSpecialVars(t *testing.T) { t.Parallel() const dir = "testdata/special_vars" const subdir = "testdata/special_vars/subdir" tests := []string{ // Root "print-task", "print-root-dir", "print-root-taskfile", "print-taskfile", "print-taskfile-dir", "print-task-dir", // Included "included:print-task", "included:print-root-dir", "included:print-taskfile", "included:print-taskfile-dir", } for _, dir := range []string{dir, subdir} { for _, test := range tests { NewExecutorTest(t, WithName(fmt.Sprintf("%s-%s", dir, test)), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), task.WithVersionCheck(true), ), WithTask(test), WithFixtureTemplating(), ) } } } func TestConcurrency(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/concurrency"), task.WithConcurrency(1), ), WithPostProcessFn(PPSortedLines), ) } func TestParams(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/params"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), ) } func TestDeps(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/deps"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), ) } // TODO: mock fs func TestStatus(t *testing.T) { t.Parallel() const dir = "testdata/status" files := []string{ "foo.txt", "bar.txt", "baz.txt", } for _, f := range files { path := filepathext.SmartJoin(dir, f) _ = os.Remove(path) if _, err := os.Stat(path); err == nil { t.Errorf("File should not exist: %v", err) } } // gen-foo creates foo.txt, and will always fail it's status check. NewExecutorTest(t, WithName("run gen-foo 1 silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-foo"), ) // gen-foo creates bar.txt, and will pass its status-check the 3. time it // is run. It creates bar.txt, but also lists it as its source. So, the checksum // for the file won't match before after the second run as we the file // only exists after the first run. NewExecutorTest(t, WithName("run gen-bar 1 silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), ) // gen-silent-baz is marked as being silent, and should only produce output // if e.Verbose is set to true. NewExecutorTest(t, WithName("run gen-baz silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-silent-baz"), ) for _, f := range files { if _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil { t.Errorf("File should exist: %v", err) } } // Run gen-bar a second time to produce a checksum file that matches bar.txt NewExecutorTest(t, WithName("run gen-bar 2 silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), ) // Run gen-bar a third time, to make sure we've triggered the status check. NewExecutorTest(t, WithName("run gen-bar 3 silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), ) // Now, let's remove source file, and run the task again to to prepare // for the next test. err := os.Remove(filepathext.SmartJoin(dir, "bar.txt")) require.NoError(t, err) NewExecutorTest(t, WithName("run gen-bar 4 silent"), WithExecutorOptions( task.WithDir(dir), task.WithSilent(true), ), WithTask("gen-bar"), ) // all: not up-to-date NewExecutorTest(t, WithName("run gen-foo 2"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-foo"), ) // status: not up-to-date NewExecutorTest(t, WithName("run gen-foo 3"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-foo"), ) // sources: not up-to-date NewExecutorTest(t, WithName("run gen-bar 5"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-bar"), ) // all: up-to-date NewExecutorTest(t, WithName("run gen-bar 6"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-bar"), ) // sources: not up-to-date, no output produced. NewExecutorTest(t, WithName("run gen-baz 2"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-silent-baz"), ) // up-to-date, no output produced NewExecutorTest(t, WithName("run gen-baz 3"), WithExecutorOptions( task.WithDir(dir), ), WithTask("gen-silent-baz"), ) // up-to-date, output produced due to Verbose mode. NewExecutorTest(t, WithName("run gen-baz 4 verbose"), WithExecutorOptions( task.WithDir(dir), task.WithVerbose(true), ), WithTask("gen-silent-baz"), WithFixtureTemplating(), ) } func TestPrecondition(t *testing.T) { t.Parallel() const dir = "testdata/precondition" NewExecutorTest(t, WithName("a precondition has been met"), WithExecutorOptions( task.WithDir(dir), ), WithTask("foo"), ) NewExecutorTest(t, WithName("a precondition was not met"), WithExecutorOptions( task.WithDir(dir), ), WithTask("impossible"), WithRunError(), ) NewExecutorTest(t, WithName("precondition in dependency fails the task"), WithExecutorOptions( task.WithDir(dir), ), WithTask("depends_on_impossible"), WithRunError(), ) NewExecutorTest(t, WithName("precondition in cmd fails the task"), WithExecutorOptions( task.WithDir(dir), ), WithTask("executes_failing_task_as_cmd"), WithRunError(), ) } func TestAlias(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("alias"), WithExecutorOptions( task.WithDir("testdata/alias"), ), WithTask("f"), ) NewExecutorTest(t, WithName("duplicate alias"), WithExecutorOptions( task.WithDir("testdata/alias"), ), WithTask("x"), WithRunError(), ) NewExecutorTest(t, WithName("alias summary"), WithExecutorOptions( task.WithDir("testdata/alias"), task.WithSummary(true), ), WithTask("f"), ) } func TestSummaryWithVarsAndRequires(t *testing.T) { t.Parallel() // Test basic case from prompt.md - vars and requires NewExecutorTest(t, WithName("vars-and-requires"), WithExecutorOptions( task.WithDir("testdata/summary-vars-requires"), task.WithSummary(true), ), WithTask("mytask"), ) // Test with shell variables NewExecutorTest(t, WithName("shell-vars"), WithExecutorOptions( task.WithDir("testdata/summary-vars-requires"), task.WithSummary(true), ), WithTask("with-sh-var"), ) } func TestLabel(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("up to date"), WithExecutorOptions( task.WithDir("testdata/label_uptodate"), ), WithTask("foo"), ) NewExecutorTest(t, WithName("summary"), WithExecutorOptions( task.WithDir("testdata/label_summary"), task.WithSummary(true), ), WithTask("foo"), ) NewExecutorTest(t, WithName("status"), WithExecutorOptions( task.WithDir("testdata/label_status"), ), WithTask("foo"), WithStatusError(), ) NewExecutorTest(t, WithName("var"), WithExecutorOptions( task.WithDir("testdata/label_var"), ), WithTask("foo"), ) NewExecutorTest(t, WithName("label in summary"), WithExecutorOptions( task.WithDir("testdata/label_summary"), ), WithTask("foo"), ) NewExecutorTest(t, WithName("label in error"), WithExecutorOptions( task.WithDir("testdata/label_error"), ), WithTask("foo"), WithRunError(), ) } func TestPrefix(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("up to date"), WithExecutorOptions( task.WithDir("testdata/prefix_uptodate"), task.WithOutputStyle(ast.Output{Name: "prefixed"}), ), WithTask("foo"), ) NewExecutorTest(t, WithName("up to dat with no output style"), WithExecutorOptions( task.WithDir("testdata/prefix_uptodate"), ), WithTask("foo"), ) } func TestPromptInSummary(t *testing.T) { t.Parallel() tests := []struct { name string input string wantError bool }{ {"test short approval", "y\n", false}, {"test long approval", "yes\n", false}, {"test uppercase approval", "Y\n", false}, {"test stops task", "n\n", true}, {"test junk value stops task", "foobar\n", true}, {"test Enter stops task", "\n", true}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() opts := []ExecutorTestOption{ WithName(test.name), WithExecutorOptions( task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("foo"), WithInput(test.input), } if test.wantError { opts = append(opts, WithRunError()) } NewExecutorTest(t, opts...) }) } } func TestPromptWithIndirectTask(t *testing.T) { t.Parallel() NewExecutorTest(t, WithExecutorOptions( task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("bar"), WithInput("y\n"), ) } func TestPromptAssumeYes(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("--yes flag should skip prompt"), WithExecutorOptions( task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), task.WithAssumeYes(true), ), WithTask("foo"), WithInput("\n"), ) NewExecutorTest(t, WithName("task should raise errors.TaskCancelledError"), WithExecutorOptions( task.WithDir("testdata/prompt"), task.WithAssumeTerm(true), ), WithTask("foo"), WithInput("\n"), WithRunError(), ) } func TestForCmds(t *testing.T) { t.Parallel() tests := []struct { name string wantErr bool }{ {name: "loop-explicit"}, {name: "loop-matrix"}, {name: "loop-matrix-ref"}, { name: "loop-matrix-ref-error", wantErr: true, }, {name: "loop-sources"}, {name: "loop-sources-glob"}, {name: "loop-generates"}, {name: "loop-generates-glob"}, {name: "loop-vars"}, {name: "loop-vars-sh"}, {name: "loop-task"}, {name: "loop-task-as"}, {name: "loop-different-tasks"}, } for _, test := range tests { opts := []ExecutorTestOption{ WithName(test.name), WithExecutorOptions( task.WithDir("testdata/for/cmds"), task.WithSilent(true), task.WithForce(true), ), WithTask(test.name), WithFixtureTemplating(), } if test.wantErr { opts = append(opts, WithRunError()) } NewExecutorTest(t, opts...) } } func TestForDeps(t *testing.T) { t.Parallel() tests := []struct { name string wantErr bool }{ {name: "loop-explicit"}, {name: "loop-matrix"}, {name: "loop-matrix-ref"}, { name: "loop-matrix-ref-error", wantErr: true, }, {name: "loop-sources"}, {name: "loop-sources-glob"}, {name: "loop-generates"}, {name: "loop-generates-glob"}, {name: "loop-vars"}, {name: "loop-vars-sh"}, {name: "loop-task"}, {name: "loop-task-as"}, {name: "loop-different-tasks"}, } for _, test := range tests { opts := []ExecutorTestOption{ WithName(test.name), WithExecutorOptions( task.WithDir("testdata/for/deps"), task.WithSilent(true), task.WithForce(true), // Force output of each dep to be grouped together to prevent interleaving task.WithOutputStyle(ast.Output{Name: "group"}), ), WithTask(test.name), WithFixtureTemplating(), WithPostProcessFn(PPSortedLines), } if test.wantErr { opts = append(opts, WithRunError()) } NewExecutorTest(t, opts...) } } func TestReference(t *testing.T) { t.Parallel() tests := []struct { name string call string }{ { name: "reference in command", call: "ref-cmd", }, { name: "reference in dependency", call: "ref-dep", }, { name: "reference using templating resolver", call: "ref-resolver", }, { name: "reference using templating resolver and dynamic var", call: "ref-resolver-sh", }, } for _, test := range tests { NewExecutorTest(t, WithName(test.name), WithExecutorOptions( task.WithDir("testdata/var_references"), task.WithSilent(true), task.WithForce(true), ), WithTask(cmp.Or(test.call, "default")), ) } } func TestVarInheritance(t *testing.T) { enableExperimentForTest(t, &experiments.EnvPrecedence, 1) tests := []struct { name string call string }{ {name: "shell"}, {name: "entrypoint-global-dotenv"}, {name: "entrypoint-global-vars"}, // We can't send env vars to a called task, so the env var is not overridden {name: "entrypoint-task-call-vars"}, // Dotenv doesn't set variables {name: "entrypoint-task-call-dotenv"}, {name: "entrypoint-task-call-task-vars"}, // Dotenv doesn't set variables {name: "entrypoint-task-dotenv"}, {name: "entrypoint-task-vars"}, // { // // Dotenv not currently allowed in included taskfiles // name: "included-global-dotenv", // want: "included-global-dotenv\nincluded-global-dotenv\n", // }, { name: "included-global-vars", call: "included", }, { // We can't send env vars to a called task, so the env var is not overridden name: "included-task-call-vars", call: "included", }, { // Dotenv doesn't set variables // Dotenv not currently allowed in included taskfiles (but doesn't error in a task) name: "included-task-call-dotenv", call: "included", }, { name: "included-task-call-task-vars", call: "included", }, { // Dotenv doesn't set variables // Somehow dotenv is working here! name: "included-task-dotenv", call: "included", }, { name: "included-task-vars", call: "included", }, } t.Setenv("VAR", "shell") t.Setenv("ENV", "shell") for _, test := range tests { NewExecutorTest(t, WithName(test.name), WithExecutorOptions( task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)), task.WithSilent(true), task.WithForce(true), ), WithTask(cmp.Or(test.call, "default")), ) } } func TestFuzzyModel(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("fuzzy"), WithExecutorOptions( task.WithDir("testdata/fuzzy"), ), WithTask("instal"), WithRunError(), ) NewExecutorTest(t, WithName("not-fuzzy"), WithExecutorOptions( task.WithDir("testdata/fuzzy"), ), WithTask("install"), ) NewExecutorTest(t, WithName("intern"), WithExecutorOptions( task.WithDir("testdata/fuzzy"), ), WithTask("intern"), WithRunError(), ) } func TestIncludeChecksum(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("correct"), WithExecutorOptions( task.WithDir("testdata/includes_checksum/correct"), ), ) NewExecutorTest(t, WithName("incorrect"), WithExecutorOptions( task.WithDir("testdata/includes_checksum/incorrect"), ), WithSetupError(), WithFixtureTemplating(), ) } func TestIncludeSilent(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("include-taskfile-silent"), WithExecutorOptions( task.WithDir("testdata/includes_silent"), ), WithTask("default"), ) } func TestFailfast(t *testing.T) { t.Parallel() t.Run("Default", func(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("default"), WithExecutorOptions( task.WithDir("testdata/failfast/default"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), WithRunError(), ) }) t.Run("Option", func(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("default"), WithExecutorOptions( task.WithDir("testdata/failfast/default"), task.WithSilent(true), task.WithFailfast(true), ), WithPostProcessFn(PPSortedLines), WithRunError(), ) }) t.Run("Task", func(t *testing.T) { t.Parallel() NewExecutorTest(t, WithName("task"), WithExecutorOptions( task.WithDir("testdata/failfast/task"), task.WithSilent(true), ), WithPostProcessFn(PPSortedLines), WithRunError(), ) }) } func TestIf(t *testing.T) { t.Parallel() tests := []struct { name string task string vars map[string]any verbose bool }{ // Basic command-level if {name: "cmd-if-true", task: "cmd-if-true"}, {name: "cmd-if-false", task: "cmd-if-false"}, // Task-level if {name: "task-if-true", task: "task-if-true"}, {name: "task-if-false", task: "task-if-false", verbose: true}, // Task call with if {name: "task-call-if-true", task: "task-call-if-true"}, {name: "task-call-if-false", task: "task-call-if-false", verbose: true}, // Go template conditions {name: "template-eq-true", task: "template-eq-true"}, {name: "template-eq-false", task: "template-eq-false", verbose: true}, {name: "template-ne", task: "template-ne"}, {name: "template-bool-true", task: "template-bool-true"}, {name: "template-bool-false", task: "template-bool-false"}, {name: "template-direct-true", task: "template-direct-true"}, {name: "template-direct-false", task: "template-direct-false"}, {name: "template-and", task: "template-and"}, {name: "template-or", task: "template-or"}, // CLI variable override {name: "template-cli-var", task: "template-cli-var", vars: map[string]any{"MY_VAR": "yes"}}, // Task-level if with template {name: "task-level-template", task: "task-level-template"}, {name: "task-level-template-false", task: "task-level-template-false", verbose: true}, // For loop with if {name: "if-in-for-loop", task: "if-in-for-loop", verbose: true}, // Task-level if with dynamic variable {name: "task-if-dynamic-true", task: "task-if-dynamic-true"}, {name: "task-if-dynamic-false", task: "task-if-dynamic-false", verbose: true}, } for _, test := range tests { opts := []ExecutorTestOption{ WithName(test.name), WithExecutorOptions( task.WithDir("testdata/if"), task.WithSilent(true), task.WithVerbose(test.verbose), ), WithTask(test.task), } if test.vars != nil { for k, v := range test.vars { opts = append(opts, WithVar(k, v)) } } NewExecutorTest(t, opts...) } } ================================================ FILE: experiments/errors.go ================================================ package experiments import ( "fmt" "strconv" "strings" "github.com/go-task/task/v3/internal/slicesext" ) type InvalidValueError struct { Name string AllowedValues []int Value int } func (err InvalidValueError) Error() string { return fmt.Sprintf( "task: Experiment %q has an invalid value %q (allowed values: %s)", err.Name, err.Value, strings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), ", "), ) } type InactiveError struct { Name string } func (err InactiveError) Error() string { return fmt.Sprintf( "task: Experiment %q is inactive and cannot be enabled", err.Name, ) } ================================================ FILE: experiments/experiment.go ================================================ package experiments import ( "fmt" "slices" "strconv" "github.com/go-task/task/v3/taskrc/ast" ) type Experiment struct { Name string // The name of the experiment. AllowedValues []int // The values that can enable this experiment. Value int // The version of the experiment that is enabled. } // New creates a new experiment with the given name and sets the values that can // enable it. func New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment { var value int if config != nil { value = config.Experiments[xName] } if value == 0 { value, _ = strconv.Atoi(getEnv(xName)) } x := Experiment{ Name: xName, AllowedValues: allowedValues, Value: value, } xList = append(xList, x) return x } func (x Experiment) Enabled() bool { return slices.Contains(x.AllowedValues, x.Value) } func (x Experiment) Active() bool { return len(x.AllowedValues) > 0 } func (x Experiment) Valid() error { if !x.Active() && x.Value != 0 { return &InactiveError{ Name: x.Name, } } if !x.Enabled() && x.Value != 0 { return &InvalidValueError{ Name: x.Name, AllowedValues: x.AllowedValues, Value: x.Value, } } return nil } func (x Experiment) String() string { if x.Enabled() { return fmt.Sprintf("on (%d)", x.Value) } return "off" } ================================================ FILE: experiments/experiment_test.go ================================================ package experiments_test import ( "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/taskrc/ast" ) func TestNew(t *testing.T) { const ( exampleExperiment = "EXAMPLE" exampleExperimentEnv = "TASK_X_EXAMPLE" ) tests := []struct { name string config *ast.TaskRC allowedValues []int env int wantEnabled bool wantActive bool wantValid error wantValue int }{ { name: `[] allowed, env=""`, wantEnabled: false, wantActive: false, }, { name: `[] allowed, env="1"`, env: 1, wantEnabled: false, wantActive: false, wantValid: &experiments.InactiveError{ Name: exampleExperiment, }, wantValue: 1, }, { name: `[1] allowed, env=""`, allowedValues: []int{1}, wantEnabled: false, wantActive: true, }, { name: `[1] allowed, env="1"`, allowedValues: []int{1}, env: 1, wantEnabled: true, wantActive: true, wantValue: 1, }, { name: `[1] allowed, env="2"`, allowedValues: []int{1}, env: 2, wantEnabled: false, wantActive: true, wantValid: &experiments.InvalidValueError{ Name: exampleExperiment, AllowedValues: []int{1}, Value: 2, }, wantValue: 2, }, { name: `[1, 2] allowed, env="1"`, allowedValues: []int{1, 2}, env: 1, wantEnabled: true, wantActive: true, wantValue: 1, }, { name: `[1, 2] allowed, env="1"`, allowedValues: []int{1, 2}, env: 2, wantEnabled: true, wantActive: true, wantValue: 2, }, { name: `[1] allowed, config="1"`, config: &ast.TaskRC{ Experiments: map[string]int{ exampleExperiment: 1, }, }, allowedValues: []int{1}, wantEnabled: true, wantActive: true, wantValue: 1, }, { name: `[1] allowed, config="2"`, config: &ast.TaskRC{ Experiments: map[string]int{ exampleExperiment: 2, }, }, allowedValues: []int{1}, wantEnabled: false, wantActive: true, wantValid: &experiments.InvalidValueError{ Name: exampleExperiment, AllowedValues: []int{1}, Value: 2, }, wantValue: 2, }, { name: `[1, 2] allowed, env="1", config="2"`, config: &ast.TaskRC{ Experiments: map[string]int{ exampleExperiment: 2, }, }, allowedValues: []int{1, 2}, env: 1, wantEnabled: true, wantActive: true, wantValue: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.env)) x := experiments.New(exampleExperiment, tt.config, tt.allowedValues...) assert.Equal(t, exampleExperiment, x.Name) assert.Equal(t, tt.wantEnabled, x.Enabled()) assert.Equal(t, tt.wantActive, x.Active()) assert.Equal(t, tt.wantValid, x.Valid()) assert.Equal(t, tt.wantValue, x.Value) }) } } ================================================ FILE: experiments/experiments.go ================================================ package experiments import ( "fmt" "os" "path/filepath" "strings" "github.com/joho/godotenv" "github.com/go-task/task/v3/taskrc" "github.com/go-task/task/v3/taskrc/ast" ) const envPrefix = "TASK_X_" // Active experiments. var ( GentleForce Experiment RemoteTaskfiles Experiment EnvPrecedence Experiment ) // Inactive experiments. These are experiments that cannot be enabled, but are // preserved for error handling. var ( AnyVariables Experiment MapVariables Experiment ) // An internal list of all the initialized experiments used for iterating. var xList []Experiment func Parse(dir string) { config, _ := taskrc.GetConfig(dir) ParseWithConfig(dir, config) } func ParseWithConfig(dir string, config *ast.TaskRC) { // Read any .env files readDotEnv(dir) // Initialize the experiments GentleForce = New("GENTLE_FORCE", config, 1) RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1) EnvPrecedence = New("ENV_PRECEDENCE", config, 1) AnyVariables = New("ANY_VARIABLES", config) MapVariables = New("MAP_VARIABLES", config) } // Validate checks if any experiments have been enabled while being inactive. // If one is found, the function returns an error. func Validate() error { for _, x := range List() { if err := x.Valid(); err != nil { return err } } return nil } func List() []Experiment { return xList } func getEnv(xName string) string { envName := fmt.Sprintf("%s%s", envPrefix, xName) return os.Getenv(envName) } func getFilePath(filename, dir string) string { if dir != "" { return filepath.Join(dir, filename) } return filename } func readDotEnv(dir string) { env, err := godotenv.Read(getFilePath(".env", dir)) if err != nil { return } // If the env var is an experiment, set it. for key, value := range env { if strings.HasPrefix(key, envPrefix) { os.Setenv(key, value) } } } ================================================ FILE: formatter_test.go ================================================ package task_test import ( "bytes" "path/filepath" "testing" "github.com/sebdah/goldie/v2" "github.com/stretchr/testify/require" "github.com/go-task/task/v3" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/taskfile/ast" ) type ( // A FormatterTestOption is a function that configures an [FormatterTest]. FormatterTestOption interface { applyToFormatterTest(*FormatterTest) } // A FormatterTest is a test wrapper around a [task.Executor] to make it // easy to write tests for the task formatter. See [NewFormatterTest] for // information on creating and running FormatterTests. These tests use // fixture files to assert whether the result of the output is correct. If // Task's behavior has been changed, the fixture files can be updated by // running `task gen:fixtures`. FormatterTest struct { TaskTest task string vars map[string]any executorOpts []task.ExecutorOption listOptions task.ListOptions wantSetupError bool wantListError bool } ) // NewFormatterTest sets up a new [task.Executor] with the given options and // runs a task with the given [FormatterTestOption]s. The output of the task is // written to a set of fixture files depending on the configuration of the test. func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) { t.Helper() tt := &FormatterTest{ task: "default", vars: map[string]any{}, TaskTest: TaskTest{ experiments: map[*experiments.Experiment]int{}, fixtureTemplateData: map[string]any{}, }, } // Apply the functional options for _, opt := range opts { opt.applyToFormatterTest(tt) } // Enable any experiments that have been set for x, v := range tt.experiments { prev := *x *x = experiments.Experiment{ Name: prev.Name, AllowedValues: []int{v}, Value: v, } t.Cleanup(func() { *x = prev }) } tt.run(t) } // Functional options // WithListOptions sets the list options for the formatter. func WithListOptions(opts task.ListOptions) FormatterTestOption { return &listOptionsTestOption{opts} } type listOptionsTestOption struct { listOptions task.ListOptions } func (opt *listOptionsTestOption) applyToFormatterTest(t *FormatterTest) { t.listOptions = opt.listOptions } // WithListError tells the test to expect an error when running the formatter. // A fixture will be created with the output of any errors. func WithListError() FormatterTestOption { return &listErrorTestOption{} } type listErrorTestOption struct{} func (opt *listErrorTestOption) applyToFormatterTest(t *FormatterTest) { t.wantListError = true } // Helpers // writeFixtureErrList is a wrapper for writing the output of an error when // running the formatter to a fixture file. func (tt *FormatterTest) writeFixtureErrList( t *testing.T, g *goldie.Goldie, err error, ) { t.Helper() tt.writeFixture(t, g, "err-list", []byte(err.Error())) } // run is the main function for running the test. It sets up the task executor, // runs the task, and writes the output to a fixture file. func (tt *FormatterTest) run(t *testing.T) { t.Helper() f := func(t *testing.T) { t.Helper() var buf bytes.Buffer opts := append( tt.executorOpts, task.WithStdout(&buf), task.WithStderr(&buf), ) // Set up the task executor e := task.NewExecutor(opts...) // Create a golden fixture file for the output g := goldie.New(t, goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")), ) // Call setup and check for errors if err := e.Setup(); tt.wantSetupError { require.Error(t, err) tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureBuffer(t, g, buf) return } else { require.NoError(t, err) } // Create the task call vars := ast.NewVars() for key, value := range tt.vars { vars.Set(key, ast.Var{Value: value}) } // Run the formatter and check for errors if _, err := e.ListTasks(tt.listOptions); tt.wantListError { require.Error(t, err) tt.writeFixtureErrList(t, g, err) tt.writeFixtureBuffer(t, g, buf) return } else { require.NoError(t, err) } tt.writeFixtureBuffer(t, g, buf) } // Run the test (with a name if it has one) if tt.name != "" { t.Run(tt.name, f) } else { f(t) } } func TestNoLabelInList(t *testing.T) { t.Parallel() NewFormatterTest(t, WithExecutorOptions( task.WithDir("testdata/label_list"), ), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), ) } // task -al case 1: listAll list all tasks func TestListAllShowsNoDesc(t *testing.T) { t.Parallel() NewFormatterTest(t, WithExecutorOptions( task.WithDir("testdata/list_mixed_desc"), ), WithListOptions(task.ListOptions{ ListAllTasks: true, }), ) } // task -al case 2: !listAll list some tasks (only those with desc) func TestListCanListDescOnly(t *testing.T) { t.Parallel() NewFormatterTest(t, WithExecutorOptions( task.WithDir("testdata/list_mixed_desc"), ), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), ) } func TestListDescInterpolation(t *testing.T) { t.Parallel() NewFormatterTest(t, WithExecutorOptions( task.WithDir("testdata/list_desc_interpolation"), ), WithListOptions(task.ListOptions{ ListOnlyTasksWithDescriptions: true, }), ) } func TestJsonListFormat(t *testing.T) { t.Parallel() NewFormatterTest(t, WithExecutorOptions( task.WithDir("testdata/json_list_format"), ), WithListOptions(task.ListOptions{ FormatTaskListAsJSON: true, }), WithFixtureTemplating(), ) } ================================================ FILE: go.mod ================================================ module github.com/go-task/task/v3 go 1.25 require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.0 github.com/Ladicle/tabwriter v1.0.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/chainguard-dev/git-urls v1.0.2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dominikbraun/graph v0.23.0 github.com/elliotchance/orderedmap/v3 v3.1.0 github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/template v0.2.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-getter v1.8.4 github.com/joho/godotenv v1.5.1 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/puzpuzpuz/xsync/v4 v4.4.0 github.com/sajari/fuzzy v1.0.0 github.com/sebdah/goldie/v2 v2.8.0 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/zeebo/xxh3 v1.1.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/sync v0.19.0 golang.org/x/term v0.40.0 mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b ) require ( cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.58.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect github.com/aws/smithy-go v1.24.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.256.0 // indirect google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.1 h1:B8e9zzK7x9JJ+XvHGF4xnYu9Xa0E0y0MyggY6dbaCfQ= charm.land/bubbletea/v2 v2.0.1/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ= github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A= github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA= github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE= mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo= mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b h1:PUPnLxbDzRO9kg/03l7TZk7+ywTv7FxmOhDHOtOdOtk= mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b/go.mod h1:mencVHx2sy9XZG5wJbCA9nRUOE3zvMtoRXOmXMxH7sc= ================================================ FILE: hash.go ================================================ package task import ( "cmp" "fmt" "github.com/go-task/task/v3/internal/hash" "github.com/go-task/task/v3/taskfile/ast" ) func (e *Executor) GetHash(t *ast.Task) (string, error) { r := cmp.Or(t.Run, e.Taskfile.Run) var h hash.HashFunc switch r { case "always": h = hash.Empty case "once": h = hash.Name case "when_changed": h = hash.Hash default: return "", fmt.Errorf(`task: invalid run "%s"`, r) } return h(t) } ================================================ FILE: help.go ================================================ package task import ( "context" "encoding/json" "fmt" "io" "os" "strings" "github.com/Ladicle/tabwriter" "golang.org/x/sync/errgroup" "github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile/ast" ) // ListOptions collects list-related options type ListOptions struct { ListOnlyTasksWithDescriptions bool ListAllTasks bool FormatTaskListAsJSON bool NoStatus bool Nested bool } // NewListOptions creates a new ListOptions instance func NewListOptions(list, listAll, listAsJson, noStatus, nested bool) ListOptions { return ListOptions{ ListOnlyTasksWithDescriptions: list, ListAllTasks: listAll, FormatTaskListAsJSON: listAsJson, NoStatus: noStatus, Nested: nested, } } // ShouldListTasks returns true if one of the options to list tasks has been set to true func (o ListOptions) ShouldListTasks() bool { return o.ListOnlyTasksWithDescriptions || o.ListAllTasks } // Filters returns the slice of FilterFunc which filters a list // of ast.Task according to the given ListOptions func (o ListOptions) Filters() []FilterFunc { filters := []FilterFunc{FilterOutInternal} if o.ListOnlyTasksWithDescriptions { filters = append(filters, FilterOutNoDesc) } return filters } // ListTasks prints a list of tasks. // Tasks that match the given filters will be excluded from the list. // The function returns a boolean indicating whether tasks were found // and an error if one was encountered while preparing the output. func (e *Executor) ListTasks(o ListOptions) (bool, error) { tasks, err := e.GetTaskList(o.Filters()...) if err != nil { return false, err } if o.FormatTaskListAsJSON { output, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested) if err != nil { return false, err } encoder := json.NewEncoder(e.Stdout) encoder.SetIndent("", " ") if err := encoder.Encode(output); err != nil { return false, err } return len(tasks) > 0, nil } if len(tasks) == 0 { if o.ListOnlyTasksWithDescriptions { e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks\n") } else if o.ListAllTasks { e.Logger.Outf(logger.Yellow, "task: No tasks available\n") } return false, nil } e.Logger.Outf(logger.Default, "task: Available tasks for this project:\n") // Format in tab-separated columns with a tab stop of 8. w := tabwriter.NewWriter(e.Stdout, 0, 8, 6, ' ', 0) for _, task := range tasks { e.Logger.FOutf(w, logger.Yellow, "* ") e.Logger.FOutf(w, logger.Green, task.Task) desc := strings.ReplaceAll(task.Desc, "\n", " ") e.Logger.FOutf(w, logger.Default, ": \t%s", desc) if len(task.Aliases) > 0 { e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", ")) } _, _ = fmt.Fprint(w, "\n") } if err := w.Flush(); err != nil { return false, err } return true, nil } // ListTaskNames prints only the task names in a Taskfile. // Only tasks with a non-empty description are printed if allTasks is false. // Otherwise, all task names are printed. func (e *Executor) ListTaskNames(allTasks bool) error { // use stdout if no output defined var w io.Writer = os.Stdout if e.Stdout != nil { w = e.Stdout } // Sort the tasks if e.TaskSorter == nil { e.TaskSorter = sort.AlphaNumericWithRootTasksFirst } // Create a list of task names taskNames := make([]string, 0, e.Taskfile.Tasks.Len()) for task := range e.Taskfile.Tasks.Values(e.TaskSorter) { if (allTasks || task.Desc != "") && !task.Internal { taskNames = append(taskNames, strings.TrimRight(task.Task, ":")) for _, alias := range task.Aliases { taskNames = append(taskNames, strings.TrimRight(alias, ":")) } } } for _, t := range taskNames { fmt.Fprintln(w, t) } return nil } func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) { var g errgroup.Group editorTasks := make([]editors.Task, len(tasks)) // Look over each task in parallel and turn it into an editor task for i := range tasks { g.Go(func() error { editorTask := editors.NewTask(tasks[i]) if noStatus { editorTasks[i] = editorTask return nil } // Get the fingerprinting method to use method := e.Taskfile.Method if tasks[i].Method != "" { method = tasks[i].Method } upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i], fingerprint.WithMethod(method), fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithDry(e.Dry), fingerprint.WithLogger(e.Logger), ) if err != nil { return err } editorTask.UpToDate = &upToDate editorTasks[i] = editorTask return nil }) } if err := g.Wait(); err != nil { return nil, err } // Create the root namespace var tasksLen int if !nested { tasksLen = len(editorTasks) } rootNamespace := &editors.Namespace{ Tasks: make([]editors.Task, tasksLen), Location: e.Taskfile.Location, } // Recursively add namespaces to the root namespace or if nesting is // disabled add them all to the root namespace for i, task := range editorTasks { taskNamespacePath := strings.Split(task.Task, ast.NamespaceSeparator) if nested { rootNamespace.AddNamespace(taskNamespacePath, task) } else { rootNamespace.Tasks[i] = task } } return rootNamespace, g.Wait() } ================================================ FILE: init.go ================================================ package task import ( _ "embed" "os" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile" ) const defaultFilename = "Taskfile.yml" //go:embed taskfile/templates/default.yml var DefaultTaskfile string // InitTaskfile creates a new Taskfile at path. // // path can be either a file path or a directory path. // If path is a directory, path/Taskfile.yml will be created. // // The final file path is always returned and may be different from the input path. func InitTaskfile(path string) (string, error) { info, err := os.Stat(path) if err == nil && !info.IsDir() { return path, errors.TaskfileAlreadyExistsError{} } if info != nil && info.IsDir() { // path was a directory, check if there is a Taskfile already if hasDefaultTaskfile(path) { return path, errors.TaskfileAlreadyExistsError{} } path = filepathext.SmartJoin(path, defaultFilename) } if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil { return path, err } return path, nil } func hasDefaultTaskfile(dir string) bool { for _, name := range taskfile.DefaultTaskfiles { if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil { return true } } return false } ================================================ FILE: init_test.go ================================================ package task_test import ( "os" "testing" "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/filepathext" ) func TestInitDir(t *testing.T) { t.Parallel() const dir = "testdata/init" file := filepathext.SmartJoin(dir, "Taskfile.yml") _ = os.Remove(file) if _, err := os.Stat(file); err == nil { t.Errorf("Taskfile.yml should not exist") } if _, err := task.InitTaskfile(dir); err != nil { t.Error(err) } if _, err := os.Stat(file); err != nil { t.Errorf("Taskfile.yml should exist") } _ = os.Remove(file) } func TestInitFile(t *testing.T) { t.Parallel() const dir = "testdata/init" file := filepathext.SmartJoin(dir, "Tasks.yml") _ = os.Remove(file) if _, err := os.Stat(file); err == nil { t.Errorf("Tasks.yml should not exist") } if _, err := task.InitTaskfile(file); err != nil { t.Error(err) } if _, err := os.Stat(file); err != nil { t.Errorf("Tasks.yml should exist") } _ = os.Remove(file) } ================================================ FILE: install-task.sh ================================================ #!/bin/sh set -e # Code generated by godownloader on 2021-01-12T13:40:40Z. DO NOT EDIT. # usage() { this=$1 cat </dev/null } echoerr() { echo "$@" 1>&2 } log_prefix() { echo "$0" } _logp=6 log_set_priority() { _logp="$1" } log_priority() { if test -z "$1"; then echo "$_logp" return fi [ "$1" -le "$_logp" ] } log_tag() { case $1 in 0) echo "emerg" ;; 1) echo "alert" ;; 2) echo "crit" ;; 3) echo "err" ;; 4) echo "warning" ;; 5) echo "notice" ;; 6) echo "info" ;; 7) echo "debug" ;; *) echo "$1" ;; esac } log_debug() { log_priority 7 || return 0 echoerr "$(log_prefix)" "$(log_tag 7)" "$@" } log_info() { log_priority 6 || return 0 echoerr "$(log_prefix)" "$(log_tag 6)" "$@" } log_err() { log_priority 3 || return 0 echoerr "$(log_prefix)" "$(log_tag 3)" "$@" } log_crit() { log_priority 2 || return 0 echoerr "$(log_prefix)" "$(log_tag 2)" "$@" } uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$os" in cygwin_nt*) os="windows" ;; mingw*) os="windows" ;; msys_nt*) os="windows" ;; esac echo "$os" } uname_arch() { arch=$(uname -m) case $arch in x86_64) arch="amd64" ;; x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; aarch64) arch="arm64" ;; armv5*) arch="arm" ;; armv6*) arch="arm" ;; armv7*) arch="arm" ;; esac echo ${arch} } uname_os_check() { os=$(uname_os) case "$os" in darwin) return 0 ;; dragonfly) return 0 ;; freebsd) return 0 ;; linux) return 0 ;; android) return 0 ;; nacl) return 0 ;; netbsd) return 0 ;; openbsd) return 0 ;; plan9) return 0 ;; solaris) return 0 ;; windows) return 0 ;; esac log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" return 1 } uname_arch_check() { arch=$(uname_arch) case "$arch" in 386) return 0 ;; amd64) return 0 ;; arm64) return 0 ;; arm) return 0 ;; ppc64) return 0 ;; ppc64le) return 0 ;; mips) return 0 ;; mipsle) return 0 ;; mips64) return 0 ;; mips64le) return 0 ;; s390x) return 0 ;; amd64p32) return 0 ;; esac log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" return 1 } untar() { tarball=$1 case "${tarball}" in *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; *.tar) tar --no-same-owner -xf "${tarball}" ;; *.zip) unzip "${tarball}" ;; *) log_err "untar unknown archive format for ${tarball}" return 1 ;; esac } http_download_curl() { local_file=$1 source_url=$2 header=$3 if [ -z "$header" ]; then code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") else code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") fi if [ "$code" != "200" ]; then log_debug "http_download_curl received HTTP status $code" return 1 fi return 0 } http_download_wget() { local_file=$1 source_url=$2 header=$3 if [ -z "$header" ]; then wget -q -O "$local_file" "$source_url" else wget -q --header "$header" -O "$local_file" "$source_url" fi } http_download() { log_debug "http_download $2" if is_command curl; then http_download_curl "$@" return elif is_command wget; then http_download_wget "$@" return fi log_crit "http_download unable to find wget or curl" return 1 } http_copy() { tmp=$(mktemp) http_download "${tmp}" "$1" "$2" || return 1 body=$(cat "$tmp") rm -f "${tmp}" echo "$body" } github_release() { owner_repo=$1 version=$2 test -z "$version" && version="latest" giturl="https://github.com/${owner_repo}/releases/${version}" json=$(http_copy "$giturl" "Accept:application/json") test -z "$json" && return 1 version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') test -z "$version" && return 1 echo "$version" } hash_sha256() { TARGET=${1:-/dev/stdin} if is_command gsha256sum; then hash=$(gsha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command sha256sum; then hash=$(sha256sum "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command shasum; then hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 echo "$hash" | cut -d ' ' -f 1 elif is_command openssl; then hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 echo "$hash" | cut -d ' ' -f a else log_crit "hash_sha256 unable to find command to compute sha-256 hash" return 1 fi } hash_sha256_verify() { TARGET=$1 checksums=$2 if [ -z "$checksums" ]; then log_err "hash_sha256_verify checksum file not specified in arg2" return 1 fi BASENAME=${TARGET##*/} want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) if [ -z "$want" ]; then log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" return 1 fi got=$(hash_sha256 "$TARGET") if [ "$want" != "$got" ]; then log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" return 1 fi } cat /dev/null < 0 { aliases = task.Aliases } return Task{ Name: task.Name(), Task: task.Task, Desc: task.Desc, Summary: task.Summary, Aliases: aliases, Location: &Location{ Line: task.Location.Line, Column: task.Location.Column, Taskfile: task.Location.Taskfile, }, } } func (parent *Namespace) AddNamespace(namespacePath []string, task Task) { if len(namespacePath) == 0 { return } // If there are no child namespaces, then we have found a task and we can // simply add it to the current namespace if len(namespacePath) == 1 { parent.Tasks = append(parent.Tasks, task) return } // Get the key of the current namespace in the path namespaceKey := namespacePath[0] // Add the namespace to the parent namespaces map using the namespace key if parent.Namespaces == nil { parent.Namespaces = make(map[string]*Namespace, 0) } // Search for the current namespace in the parent namespaces map // If it doesn't exist, create it namespace, ok := parent.Namespaces[namespaceKey] if !ok { namespace = &Namespace{} parent.Namespaces[namespaceKey] = namespace } // Remove the current namespace key from the namespace path. childNamespacePath := namespacePath[1:] // If there are no child namespaces in the task name, then we have found the // namespace of the task and we can add it to the current namespace. // Otherwise, we need to go deeper namespace.AddNamespace(childNamespacePath, task) } ================================================ FILE: internal/env/env.go ================================================ package env import ( "fmt" "os" "strconv" "strings" "time" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/taskfile/ast" ) const taskVarPrefix = "TASK_" // GetEnviron the all return all environment variables encapsulated on a // ast.Vars func GetEnviron() *ast.Vars { m := ast.NewVars() for _, e := range os.Environ() { keyVal := strings.SplitN(e, "=", 2) key, val := keyVal[0], keyVal[1] m.Set(key, ast.Var{Value: val}) } return m } func Get(t *ast.Task) []string { if t.Env == nil { return nil } return GetFromVars(t.Env) } func GetFromVars(env *ast.Vars) []string { environ := os.Environ() for k, v := range env.ToCacheMap() { if !isTypeAllowed(v) { continue } if !experiments.EnvPrecedence.Enabled() { if _, alreadySet := os.LookupEnv(k); alreadySet { continue } } environ = append(environ, fmt.Sprintf("%s=%v", k, v)) } return environ } func isTypeAllowed(v any) bool { switch v.(type) { case string, bool, int, float32, float64: return true default: return false } } func GetTaskEnv(key string) string { return os.Getenv(taskVarPrefix + key) } // GetTaskEnvBool returns the boolean value of a TASK_ prefixed env var. // Returns the value and true if set and valid, or false and false if not set or invalid. func GetTaskEnvBool(key string) (bool, bool) { v := GetTaskEnv(key) if v == "" { return false, false } b, err := strconv.ParseBool(v) return b, err == nil } // GetTaskEnvInt returns the integer value of a TASK_ prefixed env var. // Returns the value and true if set and valid, or 0 and false if not set or invalid. func GetTaskEnvInt(key string) (int, bool) { v := GetTaskEnv(key) if v == "" { return 0, false } i, err := strconv.Atoi(v) return i, err == nil } // GetTaskEnvDuration returns the duration value of a TASK_ prefixed env var. // Returns the value and true if set and valid, or 0 and false if not set or invalid. func GetTaskEnvDuration(key string) (time.Duration, bool) { v := GetTaskEnv(key) if v == "" { return 0, false } d, err := time.ParseDuration(v) return d, err == nil } // GetTaskEnvString returns the string value of a TASK_ prefixed env var. // Returns the value and true if set (non-empty), or empty string and false if not set. func GetTaskEnvString(key string) (string, bool) { v := GetTaskEnv(key) return v, v != "" } // GetTaskEnvStringSlice returns a comma-separated list from a TASK_ prefixed env var. // Returns the slice and true if set (non-empty), or nil and false if not set. func GetTaskEnvStringSlice(key string) ([]string, bool) { v := GetTaskEnv(key) if v == "" { return nil, false } parts := strings.Split(v, ",") result := make([]string, 0, len(parts)) for _, p := range parts { if trimmed := strings.TrimSpace(p); trimmed != "" { result = append(result, trimmed) } } if len(result) == 0 { return nil, false } return result, true } ================================================ FILE: internal/execext/coreutils.go ================================================ package execext import ( "runtime" "strconv" "github.com/go-task/task/v3/internal/env" ) var useGoCoreUtils bool func init() { // If TASK_CORE_UTILS is set to either true or false, respect that. // By default, enable on Windows only. if v, err := strconv.ParseBool(env.GetTaskEnv("CORE_UTILS")); err == nil { useGoCoreUtils = v } else { useGoCoreUtils = runtime.GOOS == "windows" } } ================================================ FILE: internal/execext/devnull.go ================================================ package execext import ( "io" ) var _ io.ReadWriteCloser = devNull{} type devNull struct{} func (devNull) Read(p []byte) (int, error) { return 0, io.EOF } func (devNull) Write(p []byte) (int, error) { return len(p), nil } func (devNull) Close() error { return nil } ================================================ FILE: internal/execext/exec.go ================================================ package execext import ( "context" "fmt" "io" "os" "path/filepath" "strings" "mvdan.cc/sh/moreinterp/coreutils" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" "github.com/go-task/task/v3/errors" ) // ErrNilOptions is returned when a nil options is given var ErrNilOptions = errors.New("execext: nil options given") // RunCommandOptions is the options for the [RunCommand] func. type RunCommandOptions struct { Command string Dir string Env []string PosixOpts []string BashOpts []string Stdin io.Reader Stdout io.Writer Stderr io.Writer } // RunCommand runs a shell command func RunCommand(ctx context.Context, opts *RunCommandOptions) error { if opts == nil { return ErrNilOptions } // Set "-e" or "errexit" by default opts.PosixOpts = append(opts.PosixOpts, "e") // Format POSIX options into a slice that mvdan/sh understands var params []string for _, opt := range opts.PosixOpts { if len(opt) == 1 { params = append(params, fmt.Sprintf("-%s", opt)) } else { params = append(params, "-o") params = append(params, opt) } } environ := opts.Env if len(environ) == 0 { environ = os.Environ() } r, err := interp.New( interp.Params(params...), interp.Env(expand.ListEnviron(environ...)), interp.ExecHandlers(execHandlers()...), interp.OpenHandler(openHandler), interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), dirOption(opts.Dir), ) if err != nil { return err } parser := syntax.NewParser() // Run any shopt commands if len(opts.BashOpts) > 0 { shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " ")) shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "") if err != nil { return err } if err := r.Run(ctx, shoptCmd); err != nil { return err } } // Run the user-defined command p, err := parser.Parse(strings.NewReader(opts.Command), "") if err != nil { return err } return r.Run(ctx, p) } func escape(s string) string { s = filepath.ToSlash(s) s = strings.ReplaceAll(s, " ", `\ `) s = strings.ReplaceAll(s, "&", `\&`) s = strings.ReplaceAll(s, "(", `\(`) s = strings.ReplaceAll(s, ")", `\)`) return s } // ExpandLiteral is a wrapper around [expand.Literal]. It will escape the input // string, expand any shell symbols (such as '~') and resolve any environment // variables. func ExpandLiteral(s string) (string, error) { if s == "" { return "", nil } p := syntax.NewParser() word, err := p.Document(strings.NewReader(s)) if err != nil { return "", err } cfg := &expand.Config{ Env: expand.FuncEnviron(os.Getenv), ReadDir2: os.ReadDir, GlobStar: true, } return expand.Literal(cfg, word) } // ExpandFields is a wrapper around [expand.Fields]. It will escape the input // string, expand any shell symbols (such as '~') and resolve any environment // variables. It also expands brace expressions ({a.b}) and globs (*/**) and // returns the results as a list of strings. func ExpandFields(s string) ([]string, error) { s = escape(s) p := syntax.NewParser() var words []*syntax.Word for w := range p.WordsSeq(strings.NewReader(s)) { words = append(words, w) } cfg := &expand.Config{ Env: expand.FuncEnviron(os.Getenv), ReadDir2: os.ReadDir, GlobStar: true, NullGlob: true, } return expand.Fields(cfg, words...) } func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) { if useGoCoreUtils { handlers = append(handlers, coreutils.ExecHandler) } return handlers } func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { if path == "/dev/null" { return devNull{}, nil } return interp.DefaultOpenHandler()(ctx, path, flag, perm) } func dirOption(path string) interp.RunnerOption { return func(r *interp.Runner) error { err := interp.Dir(path)(r) if err == nil { return nil } // If the specified directory doesn't exist, it will be created later. // Therefore, even if `interp.Dir` method returns an error, the // directory path should be set only when the directory cannot be found. if absPath, _ := filepath.Abs(path); absPath != "" { if _, err := os.Stat(absPath); os.IsNotExist(err) { r.Dir = absPath return nil } } return err } } ================================================ FILE: internal/filepathext/filepathext.go ================================================ package filepathext import ( "os" "path/filepath" "strings" ) // SmartJoin joins two paths, but only if the second is not already an // absolute path. func SmartJoin(a, b string) string { if IsAbs(b) { return b } return filepath.Join(a, b) } func IsAbs(path string) bool { // NOTE(@andreynering): If the path contains any if the special // variables that we know are absolute, return true. if isSpecialDir(path) { return true } return filepath.IsAbs(path) } var knownAbsDirs = []string{ ".ROOT_DIR", ".TASKFILE_DIR", ".USER_WORKING_DIR", } func isSpecialDir(dir string) bool { for _, d := range knownAbsDirs { if strings.Contains(dir, d) { return true } } return false } // TryAbsToRel tries to convert an absolute path to relative based on the // process working directory. If it can't, it returns the absolute path. func TryAbsToRel(abs string) string { wd, err := os.Getwd() if err != nil { return abs } rel, err := filepath.Rel(wd, abs) if err != nil { return abs } return rel } // IsExtOnly checks whether path points to a file with no name but with // an extension, i.e. ".yaml" func IsExtOnly(path string) bool { return filepath.Base(path) == filepath.Ext(path) } ================================================ FILE: internal/fingerprint/checker.go ================================================ package fingerprint import ( "context" "github.com/go-task/task/v3/taskfile/ast" ) // StatusCheckable defines any type that can check if the status of a task is up-to-date. type StatusCheckable interface { IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) } // SourcesCheckable defines any type that can check if the sources of a task are up-to-date. type SourcesCheckable interface { IsUpToDate(t *ast.Task) (bool, error) Value(t *ast.Task) (any, error) OnError(t *ast.Task) error Kind() string } ================================================ FILE: internal/fingerprint/checker_mock.go ================================================ // Code generated by mockery; DO NOT EDIT. // github.com/vektra/mockery // template: testify package fingerprint import ( "context" "github.com/go-task/task/v3/taskfile/ast" mock "github.com/stretchr/testify/mock" ) // NewMockStatusCheckable creates a new instance of MockStatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockStatusCheckable(t interface { mock.TestingT Cleanup(func()) }) *MockStatusCheckable { mock := &MockStatusCheckable{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MockStatusCheckable is an autogenerated mock type for the StatusCheckable type type MockStatusCheckable struct { mock.Mock } type MockStatusCheckable_Expecter struct { mock *mock.Mock } func (_m *MockStatusCheckable) EXPECT() *MockStatusCheckable_Expecter { return &MockStatusCheckable_Expecter{mock: &_m.Mock} } // IsUpToDate provides a mock function for the type MockStatusCheckable func (_mock *MockStatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) { ret := _mock.Called(ctx, t) if len(ret) == 0 { panic("no return value specified for IsUpToDate") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok { return returnFunc(ctx, t) } if returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok { r0 = returnFunc(ctx, t) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok { r1 = returnFunc(ctx, t) } else { r1 = ret.Error(1) } return r0, r1 } // MockStatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate' type MockStatusCheckable_IsUpToDate_Call struct { *mock.Call } // IsUpToDate is a helper method to define mock.On call // - ctx // - t func (_e *MockStatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *MockStatusCheckable_IsUpToDate_Call { return &MockStatusCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", ctx, t)} } func (_c *MockStatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *MockStatusCheckable_IsUpToDate_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*ast.Task)) }) return _c } func (_c *MockStatusCheckable_IsUpToDate_Call) Return(b bool, err error) *MockStatusCheckable_IsUpToDate_Call { _c.Call.Return(b, err) return _c } func (_c *MockStatusCheckable_IsUpToDate_Call) RunAndReturn(run func(ctx context.Context, t *ast.Task) (bool, error)) *MockStatusCheckable_IsUpToDate_Call { _c.Call.Return(run) return _c } // NewMockSourcesCheckable creates a new instance of MockSourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockSourcesCheckable(t interface { mock.TestingT Cleanup(func()) }) *MockSourcesCheckable { mock := &MockSourcesCheckable{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } // MockSourcesCheckable is an autogenerated mock type for the SourcesCheckable type type MockSourcesCheckable struct { mock.Mock } type MockSourcesCheckable_Expecter struct { mock *mock.Mock } func (_m *MockSourcesCheckable) EXPECT() *MockSourcesCheckable_Expecter { return &MockSourcesCheckable_Expecter{mock: &_m.Mock} } // IsUpToDate provides a mock function for the type MockSourcesCheckable func (_mock *MockSourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) { ret := _mock.Called(t) if len(ret) == 0 { panic("no return value specified for IsUpToDate") } var r0 bool var r1 error if returnFunc, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok { return returnFunc(t) } if returnFunc, ok := ret.Get(0).(func(*ast.Task) bool); ok { r0 = returnFunc(t) } else { r0 = ret.Get(0).(bool) } if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok { r1 = returnFunc(t) } else { r1 = ret.Error(1) } return r0, r1 } // MockSourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate' type MockSourcesCheckable_IsUpToDate_Call struct { *mock.Call } // IsUpToDate is a helper method to define mock.On call // - t func (_e *MockSourcesCheckable_Expecter) IsUpToDate(t interface{}) *MockSourcesCheckable_IsUpToDate_Call { return &MockSourcesCheckable_IsUpToDate_Call{Call: _e.mock.On("IsUpToDate", t)} } func (_c *MockSourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_IsUpToDate_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(*ast.Task)) }) return _c } func (_c *MockSourcesCheckable_IsUpToDate_Call) Return(b bool, err error) *MockSourcesCheckable_IsUpToDate_Call { _c.Call.Return(b, err) return _c } func (_c *MockSourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(t *ast.Task) (bool, error)) *MockSourcesCheckable_IsUpToDate_Call { _c.Call.Return(run) return _c } // Kind provides a mock function for the type MockSourcesCheckable func (_mock *MockSourcesCheckable) Kind() string { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Kind") } var r0 string if returnFunc, ok := ret.Get(0).(func() string); ok { r0 = returnFunc() } else { r0 = ret.Get(0).(string) } return r0 } // MockSourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind' type MockSourcesCheckable_Kind_Call struct { *mock.Call } // Kind is a helper method to define mock.On call func (_e *MockSourcesCheckable_Expecter) Kind() *MockSourcesCheckable_Kind_Call { return &MockSourcesCheckable_Kind_Call{Call: _e.mock.On("Kind")} } func (_c *MockSourcesCheckable_Kind_Call) Run(run func()) *MockSourcesCheckable_Kind_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockSourcesCheckable_Kind_Call) Return(s string) *MockSourcesCheckable_Kind_Call { _c.Call.Return(s) return _c } func (_c *MockSourcesCheckable_Kind_Call) RunAndReturn(run func() string) *MockSourcesCheckable_Kind_Call { _c.Call.Return(run) return _c } // OnError provides a mock function for the type MockSourcesCheckable func (_mock *MockSourcesCheckable) OnError(t *ast.Task) error { ret := _mock.Called(t) if len(ret) == 0 { panic("no return value specified for OnError") } var r0 error if returnFunc, ok := ret.Get(0).(func(*ast.Task) error); ok { r0 = returnFunc(t) } else { r0 = ret.Error(0) } return r0 } // MockSourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError' type MockSourcesCheckable_OnError_Call struct { *mock.Call } // OnError is a helper method to define mock.On call // - t func (_e *MockSourcesCheckable_Expecter) OnError(t interface{}) *MockSourcesCheckable_OnError_Call { return &MockSourcesCheckable_OnError_Call{Call: _e.mock.On("OnError", t)} } func (_c *MockSourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_OnError_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(*ast.Task)) }) return _c } func (_c *MockSourcesCheckable_OnError_Call) Return(err error) *MockSourcesCheckable_OnError_Call { _c.Call.Return(err) return _c } func (_c *MockSourcesCheckable_OnError_Call) RunAndReturn(run func(t *ast.Task) error) *MockSourcesCheckable_OnError_Call { _c.Call.Return(run) return _c } // Value provides a mock function for the type MockSourcesCheckable func (_mock *MockSourcesCheckable) Value(t *ast.Task) (any, error) { ret := _mock.Called(t) if len(ret) == 0 { panic("no return value specified for Value") } var r0 any var r1 error if returnFunc, ok := ret.Get(0).(func(*ast.Task) (any, error)); ok { return returnFunc(t) } if returnFunc, ok := ret.Get(0).(func(*ast.Task) any); ok { r0 = returnFunc(t) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(any) } } if returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok { r1 = returnFunc(t) } else { r1 = ret.Error(1) } return r0, r1 } // MockSourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value' type MockSourcesCheckable_Value_Call struct { *mock.Call } // Value is a helper method to define mock.On call // - t func (_e *MockSourcesCheckable_Expecter) Value(t interface{}) *MockSourcesCheckable_Value_Call { return &MockSourcesCheckable_Value_Call{Call: _e.mock.On("Value", t)} } func (_c *MockSourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_Value_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(*ast.Task)) }) return _c } func (_c *MockSourcesCheckable_Value_Call) Return(v any, err error) *MockSourcesCheckable_Value_Call { _c.Call.Return(v, err) return _c } func (_c *MockSourcesCheckable_Value_Call) RunAndReturn(run func(t *ast.Task) (any, error)) *MockSourcesCheckable_Value_Call { _c.Call.Return(run) return _c } ================================================ FILE: internal/fingerprint/glob.go ================================================ package fingerprint import ( "os" "sort" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile/ast" ) func Globs(dir string, globs []*ast.Glob) ([]string, error) { resultMap := make(map[string]bool) for _, g := range globs { matches, err := glob(dir, g.Glob) if err != nil { continue } for _, match := range matches { resultMap[match] = !g.Negate } } return collectKeys(resultMap), nil } func glob(dir string, g string) ([]string, error) { g = filepathext.SmartJoin(dir, g) fs, err := execext.ExpandFields(g) if err != nil { return nil, err } results := make(map[string]bool, len(fs)) for _, f := range fs { info, err := os.Stat(f) if err != nil { return nil, err } if info.IsDir() { continue } results[f] = true } return collectKeys(results), nil } func collectKeys(m map[string]bool) []string { keys := make([]string, 0, len(m)) for k, v := range m { if v { keys = append(keys, k) } } sort.Strings(keys) return keys } ================================================ FILE: internal/fingerprint/sources.go ================================================ package fingerprint import "fmt" func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) { switch method { case "timestamp": return NewTimestampChecker(tempDir, dry), nil case "checksum": return NewChecksumChecker(tempDir, dry), nil case "none": return NoneChecker{}, nil default: return nil, fmt.Errorf(`task: invalid method "%s"`, method) } } ================================================ FILE: internal/fingerprint/sources_checksum.go ================================================ package fingerprint import ( "fmt" "io" "os" "path/filepath" "regexp" "strings" "github.com/zeebo/xxh3" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile/ast" ) // ChecksumChecker validates if a task is up to date by calculating its source // files checksum type ChecksumChecker struct { tempDir string dry bool } func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker { return &ChecksumChecker{ tempDir: tempDir, dry: dry, } } func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) { if len(t.Sources) == 0 { return false, nil } checksumFile := checker.checksumFilePath(t) data, _ := os.ReadFile(checksumFile) oldHash := strings.TrimSpace(string(data)) newHash, err := checker.checksum(t) if err != nil { return false, nil } if !checker.dry && oldHash != newHash { _ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755) if err = os.WriteFile(checksumFile, []byte(newHash+"\n"), 0o644); err != nil { return false, err } } if len(t.Generates) > 0 { // For each specified 'generates' field, check whether the files actually exist for _, g := range t.Generates { if g.Negate { continue } generates, err := glob(t.Dir, g.Glob) if os.IsNotExist(err) { return false, nil } if err != nil { return false, err } if len(generates) == 0 { return false, nil } } } return oldHash == newHash, nil } func (checker *ChecksumChecker) Value(t *ast.Task) (any, error) { return checker.checksum(t) } func (checker *ChecksumChecker) OnError(t *ast.Task) error { if len(t.Sources) == 0 { return nil } return os.Remove(checker.checksumFilePath(t)) } func (*ChecksumChecker) Kind() string { return "checksum" } func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { sources, err := Globs(t.Dir, t.Sources) if err != nil { return "", err } h := xxh3.New() buf := make([]byte, 128*1024) for _, f := range sources { // also sum the filename, so checksum changes for renaming a file if _, err := io.CopyBuffer(h, strings.NewReader(filepath.Base(f)), buf); err != nil { return "", err } f, err := os.Open(f) if err != nil { return "", err } if _, err = io.CopyBuffer(h, f, buf); err != nil { return "", err } f.Close() } hash := h.Sum128() return fmt.Sprintf("%x%x", hash.Hi, hash.Lo), nil } func (checker *ChecksumChecker) checksumFilePath(t *ast.Task) string { return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name())) } var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]") // replaces invalid characters on filenames with "-" func normalizeFilename(f string) string { return checksumFilenameRegexp.ReplaceAllString(f, "-") } ================================================ FILE: internal/fingerprint/sources_checksum_test.go ================================================ package fingerprint import ( "testing" "github.com/stretchr/testify/assert" ) func TestNormalizeFilename(t *testing.T) { t.Parallel() tests := []struct { In, Out string }{ {"foobarbaz", "foobarbaz"}, {"foo/bar/baz", "foo-bar-baz"}, {"foo@bar/baz", "foo-bar-baz"}, {"foo1bar2baz3", "foo1bar2baz3"}, } for _, test := range tests { assert.Equal(t, test.Out, normalizeFilename(test.In)) } } ================================================ FILE: internal/fingerprint/sources_none.go ================================================ package fingerprint import "github.com/go-task/task/v3/taskfile/ast" // NoneChecker is a no-op Checker. // It will always report that the task is not up-to-date. type NoneChecker struct{} func (NoneChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } func (NoneChecker) Value(t *ast.Task) (any, error) { return "", nil } func (NoneChecker) OnError(t *ast.Task) error { return nil } func (NoneChecker) Kind() string { return "none" } ================================================ FILE: internal/fingerprint/sources_timestamp.go ================================================ package fingerprint import ( "os" "path/filepath" "time" "github.com/go-task/task/v3/taskfile/ast" ) // TimestampChecker checks if any source change compared with the generated files, // using file modifications timestamps. type TimestampChecker struct { tempDir string dry bool } func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker { return &TimestampChecker{ tempDir: tempDir, dry: dry, } } // IsUpToDate implements the Checker interface func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { if len(t.Sources) == 0 { return false, nil } sources, err := Globs(t.Dir, t.Sources) if err != nil { return false, nil } generates, err := Globs(t.Dir, t.Generates) if err != nil { return false, nil } timestampFile := checker.timestampFilePath(t) // If the file exists, add the file path to the generates. // If the generate file is old, the task will be executed. _, err = os.Stat(timestampFile) if err == nil { generates = append(generates, timestampFile) } else { // Create the timestamp file for the next execution when the file does not exist. if !checker.dry { if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil { return false, err } f, err := os.Create(timestampFile) if err != nil { return false, err } f.Close() } } taskTime := time.Now() // Compare the time of the generates and sources. If the generates are old, the task will be executed. // Get the max time of the generates. generateMaxTime, err := getMaxTime(generates...) if err != nil || generateMaxTime.IsZero() { return false, nil } // Check if any of the source files is newer than the max time of the generates. shouldUpdate, err := anyFileNewerThan(sources, generateMaxTime) if err != nil { return false, nil } // Modify the metadata of the file to the the current time. if !checker.dry { if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil { return false, err } } return !shouldUpdate, nil } func (checker *TimestampChecker) Kind() string { return "timestamp" } // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *ast.Task) (any, error) { sources, err := Globs(t.Dir, t.Sources) if err != nil { return time.Now(), err } sourcesMaxTime, err := getMaxTime(sources...) if err != nil { return time.Now(), err } if sourcesMaxTime.IsZero() { return time.Unix(0, 0), nil } return sourcesMaxTime, nil } func getMaxTime(files ...string) (time.Time, error) { var t time.Time for _, f := range files { info, err := os.Stat(f) if err != nil { return time.Time{}, err } t = maxTime(t, info.ModTime()) } return t, nil } func maxTime(a, b time.Time) time.Time { if a.After(b) { return a } return b } // If the modification time of any of the files is newer than the the given time, returns true. // This function is lazy, as it stops when it finds a file newer than the given time. func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) { for _, f := range files { info, err := os.Stat(f) if err != nil { return false, err } if info.ModTime().After(givenTime) { return true, nil } } return false, nil } // OnError implements the Checker interface func (*TimestampChecker) OnError(t *ast.Task) error { return nil } func (checker *TimestampChecker) timestampFilePath(t *ast.Task) string { return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task)) } ================================================ FILE: internal/fingerprint/status.go ================================================ package fingerprint import ( "context" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/taskfile/ast" ) type StatusChecker struct { logger *logger.Logger } func NewStatusChecker(logger *logger.Logger) StatusCheckable { return &StatusChecker{ logger: logger, } } func (checker *StatusChecker) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) { for _, s := range t.Status { err := execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: s, Dir: t.Dir, Env: env.Get(t), }) if err != nil { checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s\n", s, err) return false, nil } checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero\n", s) } return true, nil } ================================================ FILE: internal/fingerprint/task.go ================================================ package fingerprint import ( "context" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/taskfile/ast" ) type ( CheckerOption func(*CheckerConfig) CheckerConfig struct { method string dry bool tempDir string logger *logger.Logger statusChecker StatusCheckable sourcesChecker SourcesCheckable } ) func WithMethod(method string) CheckerOption { return func(config *CheckerConfig) { config.method = method } } func WithDry(dry bool) CheckerOption { return func(config *CheckerConfig) { config.dry = dry } } func WithTempDir(tempDir string) CheckerOption { return func(config *CheckerConfig) { config.tempDir = tempDir } } func WithLogger(logger *logger.Logger) CheckerOption { return func(config *CheckerConfig) { config.logger = logger } } func WithStatusChecker(checker StatusCheckable) CheckerOption { return func(config *CheckerConfig) { config.statusChecker = checker } } func WithSourcesChecker(checker SourcesCheckable) CheckerOption { return func(config *CheckerConfig) { config.sourcesChecker = checker } } func IsTaskUpToDate( ctx context.Context, t *ast.Task, opts ...CheckerOption, ) (bool, error) { var statusUpToDate bool var sourcesUpToDate bool var err error // Default config config := &CheckerConfig{ method: "none", tempDir: "", dry: false, logger: nil, statusChecker: nil, sourcesChecker: nil, } // Apply functional options for _, opt := range opts { opt(config) } // If no status checker was given, set up the default one if config.statusChecker == nil { config.statusChecker = NewStatusChecker(config.logger) } // If no sources checker was given, set up the default one if config.sourcesChecker == nil { config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry) if err != nil { return false, err } } statusIsSet := len(t.Status) != 0 sourcesIsSet := len(t.Sources) != 0 // If status is set, check if it is up-to-date if statusIsSet { statusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t) if err != nil { return false, err } } // If sources is set, check if they are up-to-date if sourcesIsSet { sourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t) if err != nil { return false, err } } // If both status and sources are set, the task is up-to-date if both are up-to-date if statusIsSet && sourcesIsSet { return statusUpToDate && sourcesUpToDate, nil } // If only status is set, the task is up-to-date if the status is up-to-date if statusIsSet { return statusUpToDate, nil } // If only sources is set, the task is up-to-date if the sources are up-to-date if sourcesIsSet { return sourcesUpToDate, nil } // If no status or sources are set, the task should always run // i.e. it is never considered "up-to-date" return false, nil } ================================================ FILE: internal/fingerprint/task_test.go ================================================ package fingerprint import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/go-task/task/v3/taskfile/ast" ) // TruthTable // // | Status up-to-date | Sources up-to-date | Task is up-to-date | // | ----------------- | ------------------ | ------------------ | // | not set | not set | false | // | not set | true | true | // | not set | false | false | // | true | not set | true | // | true | true | true | // | true | false | false | // | false | not set | false | // | false | true | false | // | false | false | false | func TestIsTaskUpToDate(t *testing.T) { t.Parallel() tests := []struct { name string task *ast.Task setupMockStatusChecker func(m *MockStatusCheckable) setupMockSourcesChecker func(m *MockSourcesCheckable) expected bool }{ { name: "expect FALSE when no status or sources are defined", task: &ast.Task{ Status: nil, Sources: nil, }, setupMockStatusChecker: nil, setupMockSourcesChecker: nil, expected: false, }, { name: "expect TRUE when no status is defined and sources are up-to-date", task: &ast.Task{ Status: nil, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: nil, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) }, expected: true, }, { name: "expect FALSE when no status is defined and sources are NOT up-to-date", task: &ast.Task{ Status: nil, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: nil, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) }, expected: false, }, { name: "expect TRUE when status is up-to-date and sources are not defined", task: &ast.Task{ Status: []string{"status"}, Sources: nil, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) }, setupMockSourcesChecker: nil, expected: true, }, { name: "expect TRUE when status and sources are up-to-date", task: &ast.Task{ Status: []string{"status"}, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) }, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) }, expected: true, }, { name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date", task: &ast.Task{ Status: []string{"status"}, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil) }, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) }, expected: false, }, { name: "expect FALSE when status is NOT up-to-date and sources are not defined", task: &ast.Task{ Status: []string{"status"}, Sources: nil, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) }, setupMockSourcesChecker: nil, expected: false, }, { name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date", task: &ast.Task{ Status: []string{"status"}, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) }, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(true, nil) }, expected: false, }, { name: "expect FALSE when status and sources are NOT up-to-date", task: &ast.Task{ Status: []string{"status"}, Sources: []*ast.Glob{{Glob: "sources"}}, }, setupMockStatusChecker: func(m *MockStatusCheckable) { m.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil) }, setupMockSourcesChecker: func(m *MockSourcesCheckable) { m.EXPECT().IsUpToDate(mock.Anything).Return(false, nil) }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mockStatusChecker := NewMockStatusCheckable(t) if tt.setupMockStatusChecker != nil { tt.setupMockStatusChecker(mockStatusChecker) } mockSourcesChecker := NewMockSourcesCheckable(t) if tt.setupMockSourcesChecker != nil { tt.setupMockSourcesChecker(mockSourcesChecker) } result, err := IsTaskUpToDate( t.Context(), tt.task, WithStatusChecker(mockStatusChecker), WithSourcesChecker(mockSourcesChecker), ) require.NoError(t, err) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: internal/flags/flags.go ================================================ package flags import ( "cmp" "log" "os" "path/filepath" "strconv" "time" "github.com/fatih/color" "github.com/spf13/pflag" "github.com/go-task/task/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskrc" taskrcast "github.com/go-task/task/v3/taskrc/ast" ) const usage = `Usage: task [flags...] [task...] Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified. Example: 'task hello' with the following 'Taskfile.yml' file will generate an 'output.txt' file with the content "hello". ''' version: '3' tasks: hello: cmds: - echo "I am going to write a file named 'output.txt' now." - echo "hello" > output.txt generates: - output.txt ''' Options: ` var ( Version bool Help bool Init bool Completion string List bool ListAll bool ListJson bool TaskSort string Status bool NoStatus bool Nested bool Insecure bool Force bool ForceAll bool Watch bool Verbose bool Silent bool DisableFuzzy bool AssumeYes bool Dry bool Summary bool ExitCode bool Parallel bool Concurrency int Dir string Entrypoint string Output ast.Output Color bool Interval time.Duration Failfast bool Global bool Experiments bool Download bool Offline bool TrustedHosts []string ClearCache bool Timeout time.Duration CacheExpiryDuration time.Duration RemoteCacheDir string CACert string Cert string CertKey string Interactive bool ) func init() { // Config files can enable experiments which alter the availability and/or // behavior of some flags, so we need to parse the experiments before the // flags. However, we need the --taskfile and --dir flags before we can // parse the experiments as they can alter the location of the config files. // Because of this circular dependency, we parse the flags twice. First, we // get the --taskfile and --dir flags, then we parse the experiments, then // we parse the flags again to get the full set. We use a flagset here so // that we can parse a subset of flags without exiting on error. var dir, entrypoint string fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError) fs.StringVarP(&dir, "dir", "d", "", "") fs.StringVarP(&entrypoint, "taskfile", "t", "", "") fs.Usage = func() {} _ = fs.Parse(os.Args[1:]) // Parse the experiments dir = cmp.Or(dir, filepath.Dir(entrypoint)) config, _ := taskrc.GetConfig(dir) experiments.ParseWithConfig(dir, config) // Parse the rest of the flags log.SetFlags(0) log.SetOutput(os.Stderr) pflag.Usage = func() { log.Print(usage) pflag.PrintDefaults() } pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.") pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.") pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.") pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.") pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].") pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.") pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON") pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON") pflag.BoolVar(&Insecure, "insecure", getConfig(config, "REMOTE_INSECURE", func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.") pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.") pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, "VERBOSE", func() *bool { return config.Verbose }, false), "Enables verbose mode.") pflag.BoolVarP(&Silent, "silent", "s", getConfig(config, "SILENT", func() *bool { return config.Silent }, false), "Disables echoing.") pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, "DISABLE_FUZZY", func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.") pflag.BoolVarP(&AssumeYes, "yes", "y", getConfig(config, "ASSUME_YES", func() *bool { return nil }, false), "Assume \"yes\" as answer to all prompts.") pflag.BoolVar(&Interactive, "interactive", getConfig(config, "INTERACTIVE", func() *bool { return config.Interactive }, false), "Prompt for missing required variables.") pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.") pflag.BoolVarP(&Dry, "dry", "n", getConfig(config, "DRY", func() *bool { return nil }, false), "Compiles and prints tasks in the order that they would be run, without executing them.") pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.") pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.") pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.") pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`) pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].") pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.") pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") // Gentle force experiment will override the force flag and add a new force-all flag if experiments.GentleForce.Enabled() { pflag.BoolVarP(&Force, "force", "f", false, "Forces execution of the directly called task.") pflag.BoolVar(&ForceAll, "force-all", false, "Forces execution of the called task and all its dependant tasks.") } else { pflag.BoolVarP(&ForceAll, "force", "f", false, "Forces execution even when the task is up-to-date.") } // Remote Taskfiles experiment will adds the "download" and "offline" flags if experiments.RemoteTaskfiles.Enabled() { pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.") pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).") pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.") pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.") pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.") pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.") pflag.StringVar(&CACert, "cacert", getConfig(config, "REMOTE_CACERT", func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.") pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.") pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.") } pflag.Parse() // Auto-detect color based on environment when not explicitly configured // Priority: CLI flag > TASK_COLOR env > taskrc config > NO_COLOR > FORCE_COLOR/CI > default colorExplicitlySet := pflag.Lookup("color").Changed || env.GetTaskEnv("COLOR") != "" || (config != nil && config.Color != nil) if !colorExplicitlySet { if os.Getenv("NO_COLOR") != "" { Color = false color.NoColor = true } else if os.Getenv("FORCE_COLOR") != "" || isCI() { Color = true color.NoColor = false // Force colors even without TTY } // Otherwise, let fatih/color auto-detect TTY } else { // Explicit config: sync with fatih/color color.NoColor = !Color } } // isCI returns true if running in a CI environment func isCI() bool { ci, _ := strconv.ParseBool(os.Getenv("CI")) return ci } func Validate() error { if Download && Offline { return errors.New("task: You can't set both --download and --offline flags") } if Download && ClearCache { return errors.New("task: You can't set both --download and --clear-cache flags") } if Global && Dir != "" { return errors.New("task: You can't set both --global and --dir") } if Output.Name != "group" { if Output.Group.Begin != "" { return errors.New("task: You can't set --output-group-begin without --output=group") } if Output.Group.End != "" { return errors.New("task: You can't set --output-group-end without --output=group") } if Output.Group.ErrorOnly { return errors.New("task: You can't set --output-group-error-only without --output=group") } } if List && ListAll { return errors.New("task: cannot use --list and --list-all at the same time") } if ListJson && !List && !ListAll { return errors.New("task: --json only applies to --list or --list-all") } if NoStatus && !ListJson { return errors.New("task: --no-status only applies to --json with --list or --list-all") } if Nested && !ListJson { return errors.New("task: --nested only applies to --json with --list or --list-all") } // Validate certificate flags if (Cert != "" && CertKey == "") || (Cert == "" && CertKey != "") { return errors.New("task: --cert and --cert-key must be provided together") } return nil } // WithFlags is a special internal functional option that is used to pass flags // from the CLI into any constructor that accepts functional options. func WithFlags() task.ExecutorOption { return &flagsOption{} } type flagsOption struct{} func (o *flagsOption) ApplyToExecutor(e *task.Executor) { // Set the sorter var sorter sort.Sorter switch TaskSort { case "none": sorter = sort.NoSort case "alphanumeric": sorter = sort.AlphaNumeric } // Change the directory to the user's home directory if the global flag is set dir := Dir if Global { home, err := os.UserHomeDir() if err == nil { dir = home } } e.Options( task.WithDir(dir), task.WithEntrypoint(Entrypoint), task.WithForce(Force), task.WithForceAll(ForceAll), task.WithInsecure(Insecure), task.WithDownload(Download), task.WithOffline(Offline), task.WithTrustedHosts(TrustedHosts), task.WithTimeout(Timeout), task.WithCacheExpiryDuration(CacheExpiryDuration), task.WithRemoteCacheDir(RemoteCacheDir), task.WithCACert(CACert), task.WithCert(Cert), task.WithCertKey(CertKey), task.WithWatch(Watch), task.WithVerbose(Verbose), task.WithSilent(Silent), task.WithDisableFuzzy(DisableFuzzy), task.WithAssumeYes(AssumeYes), task.WithInteractive(Interactive), task.WithDry(Dry || Status), task.WithSummary(Summary), task.WithParallel(Parallel), task.WithColor(Color), task.WithConcurrency(Concurrency), task.WithInterval(Interval), task.WithOutputStyle(Output), task.WithTaskSorter(sorter), task.WithVersionCheck(true), task.WithFailfast(Failfast), ) } // getConfig extracts a config value with priority: env var > taskrc config > fallback func getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T { if envKey != "" { if val, ok := getEnvAs[T](envKey); ok { return val } } if config != nil { if field := fieldFunc(); field != nil { return *field } } return fallback } // getEnvAs parses a TASK_ prefixed env var as type T func getEnvAs[T any](envKey string) (T, bool) { var zero T switch any(zero).(type) { case bool: if val, ok := env.GetTaskEnvBool(envKey); ok { return any(val).(T), true } case int: if val, ok := env.GetTaskEnvInt(envKey); ok { return any(val).(T), true } case time.Duration: if val, ok := env.GetTaskEnvDuration(envKey); ok { return any(val).(T), true } case string: if val, ok := env.GetTaskEnvString(envKey); ok { return any(val).(T), true } case []string: if val, ok := env.GetTaskEnvStringSlice(envKey); ok { return any(val).(T), true } } return zero, false } ================================================ FILE: internal/fsext/fs.go ================================================ package fsext import ( "os" "path/filepath" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/sysinfo" ) // DefaultDir will return the default directory given an entrypoint or // directory. If the directory is set, it will ensure it is an absolute path and // return it. If the entrypoint is set, but the directory is not, it will leave // the directory blank. If both are empty, it will default the directory to the // current working directory. func DefaultDir(entrypoint, dir string) string { // If the directory is set, ensure it is an absolute path if dir != "" { var err error dir, err = filepath.Abs(dir) if err != nil { return "" } return dir } // If the entrypoint and dir are empty, we default the directory to the current working directory if entrypoint == "" { wd, err := os.Getwd() if err != nil { return "" } return wd } // If the entrypoint is set, but the directory is not, we leave the directory blank return "" } // ResolveDir returns an absolute path to the directory that the task should be // run in. If the entrypoint and dir are BOTH set, then the Taskfile will not // sit inside the directory specified by dir and we should ensure that the dir // is absolute. Otherwise, the dir will always be the parent directory of the // resolved entrypoint, so we should return that parent directory. func ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) { if entrypoint != "" && dir != "" { return filepath.Abs(dir) } return filepath.Dir(resolvedEntrypoint), nil } // Search looks for files with the given possible filenames using the given // entrypoint and directory. If the entrypoint is set, it checks if the // entrypoint matches a file or if it matches a directory containing one of the // possible filenames. Otherwise, it walks up the file tree starting at the // given directory and performs a search in each directory for the possible // filenames until it finds a match or reaches the root directory. If the // entrypoint and directory are both empty, it defaults the directory to the // current working directory and performs a recursive search starting there. If // a match is found, the absolute path to the file is returned with its // directory. If no match is found, an error is returned. func Search(entrypoint, dir string, possibleFilenames []string) (string, error) { var err error if entrypoint != "" { entrypoint, err = SearchPath(entrypoint, possibleFilenames) if err != nil { return "", err } return entrypoint, nil } if dir == "" { dir, err = os.Getwd() if err != nil { return "", err } } entrypoint, err = SearchPathRecursively(dir, possibleFilenames) if err != nil { return "", err } return entrypoint, nil } // SearchAll looks for files with the given possible filenames using the given // entrypoint and directory. If the entrypoint is set, it checks if the // entrypoint matches a file or if it matches a directory containing one of the // possible filenames and add it to a list of matches. It then walks up the file // tree starting at the given directory and performs a search in each directory // for the possible filenames until it finds a match or reaches the root // directory. If the entrypoint and directory are both empty, it defaults the // directory to the current working directory and performs a recursive search // starting there. If matches are found, the absolute path to each file is added // to the list and returned. func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) { var err error var entrypoints []string if entrypoint != "" { entrypoint, err = SearchPath(entrypoint, possibleFilenames) if err != nil { return nil, err } entrypoints = append(entrypoints, entrypoint) } if dir == "" { dir, err = os.Getwd() if err != nil { return nil, err } } paths, err := SearchNPathRecursively(dir, possibleFilenames, -1) // The call to SearchNPathRecursively is ambiguous and may return // os.ErrPermission if its search ends, however it may have still // returned valid paths. Caller may choose to ignore that error. return append(entrypoints, paths...), err } // SearchPath will check if a file at the given path exists or not. If it does, // it will return the path to it. If it does not, it will search for any files // at the given path with any of the given possible names. If any of these match // a file, the first matching path will be returned. If no files are found, an // error will be returned. func SearchPath(path string, possibleFilenames []string) (string, error) { // Get file info about the path fi, err := os.Stat(path) if err != nil { return "", err } // If the path exists and is a regular file, device, symlink, or named pipe, // return the absolute path to it if fi.Mode().IsRegular() || fi.Mode()&os.ModeDevice != 0 || fi.Mode()&os.ModeSymlink != 0 || fi.Mode()&os.ModeNamedPipe != 0 { return filepath.Abs(path) } // If the path is a directory, check if any of the possible names exist // in that directory for _, filename := range possibleFilenames { alt := filepathext.SmartJoin(path, filename) if _, err := os.Stat(alt); err == nil { return filepath.Abs(alt) } } return "", os.ErrNotExist } // SearchPathRecursively walks up the directory tree starting at the given // path, calling the Search function in each directory until it finds a matching // file or reaches the root directory. On supported operating systems, it will // also check if the user ID of the directory changes and abort if it does. func SearchPathRecursively(path string, possibleFilenames []string) (string, error) { paths, err := SearchNPathRecursively(path, possibleFilenames, 1) if len(paths) > 0 { // Regardless of the error, return the first possible filename. return paths[0], nil } else { if err != nil { return "", err } else { return "", os.ErrNotExist } } } // SearchNPathRecursively walks up the directory tree starting at the given // path, calling the Search function in each directory and adding each matching // file that it finds to a list until it reaches the root directory or the // length of the list exceeds n. On supported operating systems, it will also // check if the user ID of the directory changes and abort if it does. func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) { var paths []string owner, err := sysinfo.Owner(path) if err != nil { return nil, err } for n == -1 || len(paths) < n { fpath, err := SearchPath(path, possibleFilenames) if err == nil { paths = append(paths, fpath) } // Get the parent path/user id parentPath := filepath.Dir(path) parentOwner, err := sysinfo.Owner(parentPath) if err != nil { return nil, err } // If the user id of the directory changes indicate a permission error, otherwise // the calling code will infer an error condition based on the accumulated // contents of paths. if path == parentPath { return paths, nil } else if parentOwner != owner { return paths, os.ErrPermission } owner = parentOwner path = parentPath } return paths, nil } ================================================ FILE: internal/fsext/fs_test.go ================================================ package fsext import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestDefaultDir(t *testing.T) { t.Parallel() wd, err := os.Getwd() require.NoError(t, err) tests := []struct { name string entrypoint string dir string expected string }{ { name: "default to current working directory", entrypoint: "", dir: "", expected: wd, }, { name: "resolves relative dir path", entrypoint: "", dir: "./dir", expected: filepath.Join(wd, "dir"), }, { name: "return entrypoint if set", entrypoint: filepath.Join(wd, "entrypoint"), dir: "", expected: "", }, { name: "if entrypoint and dir are set", entrypoint: filepath.Join(wd, "entrypoint"), dir: filepath.Join(wd, "dir"), expected: filepath.Join(wd, "dir"), }, { name: "if entrypoint and dir are set and dir is relative", entrypoint: filepath.Join(wd, "entrypoint"), dir: "./dir", expected: filepath.Join(wd, "dir"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() require.Equal(t, tt.expected, DefaultDir(tt.entrypoint, tt.dir)) }) } } func TestSearch(t *testing.T) { t.Parallel() wd, err := os.Getwd() require.NoError(t, err) tests := []struct { name string entrypoint string dir string possibleFilenames []string expectedEntrypoint string }{ { name: "find foo.txt using relative entrypoint", entrypoint: "./testdata/foo.txt", possibleFilenames: []string{"foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find foo.txt using absolute entrypoint", entrypoint: filepath.Join(wd, "testdata", "foo.txt"), possibleFilenames: []string{"foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find foo.txt using relative dir", dir: "./testdata", possibleFilenames: []string{"foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find foo.txt using absolute dir", dir: filepath.Join(wd, "testdata"), possibleFilenames: []string{"foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find foo.txt using relative dir and relative entrypoint", entrypoint: "./testdata/foo.txt", dir: "./testdata/some/other/dir", possibleFilenames: []string{"foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find fs.go using no entrypoint or dir", entrypoint: "", dir: "", possibleFilenames: []string{"fs.go"}, expectedEntrypoint: filepath.Join(wd, "fs.go"), }, { name: "find ../../Taskfile.yml using no entrypoint or dir by walking", entrypoint: "", dir: "", possibleFilenames: []string{"Taskfile.yml"}, expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"), }, { name: "find foo.txt first if listed first in possible filenames", entrypoint: "./testdata", possibleFilenames: []string{"foo.txt", "bar.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), }, { name: "find bar.txt first if listed first in possible filenames", entrypoint: "./testdata", possibleFilenames: []string{"bar.txt", "foo.txt"}, expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() entrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames) require.NoError(t, err) require.Equal(t, tt.expectedEntrypoint, entrypoint) require.NoError(t, err) }) } } func TestResolveDir(t *testing.T) { t.Parallel() wd, err := os.Getwd() require.NoError(t, err) tests := []struct { name string entrypoint string resolvedEntrypoint string dir string expectedDir string }{ { name: "find foo.txt using relative entrypoint", entrypoint: "./testdata/foo.txt", resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedDir: filepath.Join(wd, "testdata"), }, { name: "find foo.txt using absolute entrypoint", entrypoint: filepath.Join(wd, "testdata", "foo.txt"), resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedDir: filepath.Join(wd, "testdata"), }, { name: "find foo.txt using relative dir", resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), dir: "./testdata", expectedDir: filepath.Join(wd, "testdata"), }, { name: "find foo.txt using absolute dir", resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), dir: filepath.Join(wd, "testdata"), expectedDir: filepath.Join(wd, "testdata"), }, { name: "find foo.txt using relative dir and relative entrypoint", entrypoint: "./testdata/foo.txt", resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), dir: "./testdata/some/other/dir", expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"), }, { name: "find fs.go using no entrypoint or dir", entrypoint: "", resolvedEntrypoint: filepath.Join(wd, "fs.go"), dir: "", expectedDir: wd, }, { name: "find ../../Taskfile.yml using no entrypoint or dir by walking", entrypoint: "", resolvedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"), dir: "", expectedDir: filepath.Join(wd, "..", ".."), }, { name: "find foo.txt first if listed first in possible filenames", entrypoint: "./testdata", resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedDir: filepath.Join(wd, "testdata"), }, { name: "find bar.txt first if listed first in possible filenames", entrypoint: "./testdata", resolvedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"), expectedDir: filepath.Join(wd, "testdata"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() dir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir) require.NoError(t, err) require.Equal(t, tt.expectedDir, dir) require.NoError(t, err) }) } } ================================================ FILE: internal/fsext/testdata/bar.txt ================================================ ================================================ FILE: internal/fsext/testdata/foo.txt ================================================ ================================================ FILE: internal/fsnotifyext/fsnotify_dedup.go ================================================ package fsnotifyext import ( "math" "time" "github.com/fsnotify/fsnotify" ) type Deduper struct { w *fsnotify.Watcher waitTime time.Duration } func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper { return &Deduper{ w: w, waitTime: waitTime, } } // GetChan returns a chan of deduplicated [fsnotify.Event]. // // [fsnotify.Chmod] operations will be skipped. func (d *Deduper) GetChan() <-chan fsnotify.Event { channel := make(chan fsnotify.Event) go func() { timers := make(map[string]*time.Timer) for { event, ok := <-d.w.Events switch { case !ok: return case event.Has(fsnotify.Chmod): continue } timer, ok := timers[event.String()] if !ok { timer = time.AfterFunc(math.MaxInt64, func() { channel <- event }) timer.Stop() timers[event.String()] = timer } timer.Reset(d.waitTime) } }() return channel } ================================================ FILE: internal/goext/meta.go ================================================ package goext // NOTE(@andreynering): The lists in this file were copied from: // // https://github.com/golang/go/blob/master/src/go/build/syslist.go func IsKnownOS(str string) bool { _, known := knownOS[str] return known } func IsKnownArch(str string) bool { _, known := knownArch[str] return known } var knownOS = map[string]struct{}{ "aix": {}, "android": {}, "darwin": {}, "dragonfly": {}, "freebsd": {}, "hurd": {}, "illumos": {}, "ios": {}, "js": {}, "linux": {}, "nacl": {}, "netbsd": {}, "openbsd": {}, "plan9": {}, "solaris": {}, "windows": {}, "zos": {}, "__test__": {}, } var knownArch = map[string]struct{}{ "386": {}, "amd64": {}, "amd64p32": {}, "arm": {}, "armbe": {}, "arm64": {}, "arm64be": {}, "loong64": {}, "mips": {}, "mipsle": {}, "mips64": {}, "mips64le": {}, "mips64p32": {}, "mips64p32le": {}, "ppc": {}, "ppc64": {}, "ppc64le": {}, "riscv": {}, "riscv64": {}, "s390": {}, "s390x": {}, "sparc": {}, "sparc64": {}, "wasm": {}, } ================================================ FILE: internal/hash/hash.go ================================================ package hash import ( "fmt" "github.com/mitchellh/hashstructure/v2" "github.com/go-task/task/v3/taskfile/ast" ) type HashFunc func(*ast.Task) (string, error) func Empty(*ast.Task) (string, error) { return "", nil } func Name(t *ast.Task) (string, error) { return fmt.Sprintf("%s:%s", t.Location.Taskfile, t.LocalName()), nil } func Hash(t *ast.Task) (string, error) { h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil) return fmt.Sprintf("%s:%s:%d", t.Location.Taskfile, t.LocalName(), h), err } ================================================ FILE: internal/input/input.go ================================================ package input import ( "fmt" "io" "strings" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/go-task/task/v3/errors" ) var ErrCancelled = errors.New("prompt cancelled") var ( promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) // green bold dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray ) // Prompter handles interactive variable prompting type Prompter struct { Stdin io.Reader Stdout io.Writer Stderr io.Writer } // Text prompts the user for a text value func (p *Prompter) Text(varName string) (string, error) { m := newTextModel(varName) prog := tea.NewProgram(m, tea.WithInput(p.Stdin), tea.WithOutput(p.Stderr), ) result, err := prog.Run() if err != nil { return "", err } model := result.(textModel) if model.cancelled { return "", ErrCancelled } return model.value, nil } // Select prompts the user to select from a list of options func (p *Prompter) Select(varName string, options []string) (string, error) { if len(options) == 0 { return "", errors.New("no options provided") } m := newSelectModel(varName, options) prog := tea.NewProgram(m, tea.WithInput(p.Stdin), tea.WithOutput(p.Stderr), ) result, err := prog.Run() if err != nil { return "", err } model := result.(selectModel) if model.cancelled { return "", ErrCancelled } return model.options[model.cursor], nil } // Prompt prompts for a variable value, using Select if enum is provided, Text otherwise func (p *Prompter) Prompt(varName string, enum []string) (string, error) { if len(enum) > 0 { return p.Select(varName, enum) } return p.Text(varName) } // textModel is the Bubble Tea model for text input type textModel struct { varName string textInput textinput.Model value string cancelled bool done bool } func newTextModel(varName string) textModel { ti := textinput.New() ti.Placeholder = "" ti.CharLimit = 256 ti.SetWidth(40) ti.Focus() return textModel{ varName: varName, textInput: ti, } } func (m textModel) Init() tea.Cmd { return tea.Batch(m.textInput.Focus(), textinput.Blink) } func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.Keystroke() { case "ctrl+c", "escape": m.cancelled = true m.done = true return m, tea.Quit case "enter": m.value = m.textInput.Value() m.done = true return m, tea.Quit } } var cmd tea.Cmd m.textInput, cmd = m.textInput.Update(msg) return m, cmd } func (m textModel) View() tea.View { if m.done { return tea.NewView("") } prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName)) return tea.NewView(prompt + m.textInput.View() + "\n") } // selectModel is the Bubble Tea model for selection type selectModel struct { varName string options []string cursor int cancelled bool done bool } func newSelectModel(varName string, options []string) selectModel { return selectModel{ varName: varName, options: options, cursor: 0, } } func (m selectModel) Init() tea.Cmd { return nil } func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.Keystroke() { case "ctrl+c", "escape": m.cancelled = true m.done = true return m, tea.Quit case "up", "shift+tab", "k": if m.cursor > 0 { m.cursor-- } case "down", "tab", "j": if m.cursor < len(m.options)-1 { m.cursor++ } case "enter": m.done = true return m, tea.Quit } } return m, nil } func (m selectModel) View() tea.View { if m.done { return tea.NewView("") } var b strings.Builder b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName))) b.WriteString("\n") for i, opt := range m.options { if i == m.cursor { b.WriteString(cursorStyle.Render("❯ ")) b.WriteString(selectedStyle.Render(opt)) } else { b.WriteString(" " + opt) } b.WriteString("\n") } b.WriteString(dimStyle.Render(" (↑/↓ to move, enter to select, esc to cancel)")) return tea.NewView(b.String()) } ================================================ FILE: internal/logger/logger.go ================================================ package logger import ( "bufio" "io" "slices" "strconv" "strings" "github.com/Ladicle/tabwriter" "github.com/fatih/color" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/term" ) var ( ErrPromptCancelled = errors.New("prompt cancelled") ErrNoTerminal = errors.New("no terminal") ) var ( attrsReset = envColor("COLOR_RESET", color.Reset) attrsFgBlue = envColor("COLOR_BLUE", color.FgBlue) attrsFgGreen = envColor("COLOR_GREEN", color.FgGreen) attrsFgCyan = envColor("COLOR_CYAN", color.FgCyan) attrsFgYellow = envColor("COLOR_YELLOW", color.FgYellow) attrsFgMagenta = envColor("COLOR_MAGENTA", color.FgMagenta) attrsFgRed = envColor("COLOR_RED", color.FgRed) attrsFgHiBlue = envColor("COLOR_BRIGHT_BLUE", color.FgHiBlue) attrsFgHiGreen = envColor("COLOR_BRIGHT_GREEN", color.FgHiGreen) attrsFgHiCyan = envColor("COLOR_BRIGHT_CYAN", color.FgHiCyan) attrsFgHiYellow = envColor("COLOR_BRIGHT_YELLOW", color.FgHiYellow) attrsFgHiMagenta = envColor("COLOR_BRIGHT_MAGENTA", color.FgHiMagenta) attrsFgHiRed = envColor("COLOR_BRIGHT_RED", color.FgHiRed) ) type ( Color func() PrintFunc PrintFunc func(io.Writer, string, ...any) ) func None() PrintFunc { c := color.New() c.DisableColor() return c.FprintfFunc() } func Default() PrintFunc { return color.New(attrsReset...).FprintfFunc() } func Blue() PrintFunc { return color.New(attrsFgBlue...).FprintfFunc() } func Green() PrintFunc { return color.New(attrsFgGreen...).FprintfFunc() } func Cyan() PrintFunc { return color.New(attrsFgCyan...).FprintfFunc() } func Yellow() PrintFunc { return color.New(attrsFgYellow...).FprintfFunc() } func Magenta() PrintFunc { return color.New(attrsFgMagenta...).FprintfFunc() } func Red() PrintFunc { return color.New(attrsFgRed...).FprintfFunc() } func BrightBlue() PrintFunc { return color.New(attrsFgHiBlue...).FprintfFunc() } func BrightGreen() PrintFunc { return color.New(attrsFgHiGreen...).FprintfFunc() } func BrightCyan() PrintFunc { return color.New(attrsFgHiCyan...).FprintfFunc() } func BrightYellow() PrintFunc { return color.New(attrsFgHiYellow...).FprintfFunc() } func BrightMagenta() PrintFunc { return color.New(attrsFgHiMagenta...).FprintfFunc() } func BrightRed() PrintFunc { return color.New(attrsFgHiRed...).FprintfFunc() } func envColor(name string, defaultColor color.Attribute) []color.Attribute { // Fetch the environment variable override := env.GetTaskEnv(name) // First, try splitting the string by commas (RGB shortcut syntax) and if it // matches, then prepend the 256-color foreground escape sequence. // Otherwise, split by semicolons (ANSI color codes) and use them as is. attributeStrs := strings.Split(override, ",") if len(attributeStrs) == 3 { attributeStrs = slices.Concat([]string{"38", "2"}, attributeStrs) } else { attributeStrs = strings.Split(override, ";") } // Loop over the attributes and convert them to integers attributes := make([]color.Attribute, len(attributeStrs)) for i, attributeStr := range attributeStrs { attribute, err := strconv.Atoi(attributeStr) if err != nil { return []color.Attribute{defaultColor} } attributes[i] = color.Attribute(attribute) } return attributes } // Logger is just a wrapper that prints stuff to STDOUT or STDERR, // with optional color. type Logger struct { Stdin io.Reader Stdout io.Writer Stderr io.Writer Verbose bool Color bool AssumeYes bool AssumeTerm bool // Used for testing } // Outf prints stuff to STDOUT. func (l *Logger) Outf(color Color, s string, args ...any) { l.FOutf(l.Stdout, color, s, args...) } // FOutf prints stuff to the given writer. func (l *Logger) FOutf(w io.Writer, color Color, s string, args ...any) { if len(args) == 0 { s, args = "%s", []any{s} } if !l.Color { color = None } print := color() print(w, s, args...) } // VerboseOutf prints stuff to STDOUT if verbose mode is enabled. func (l *Logger) VerboseOutf(color Color, s string, args ...any) { if l.Verbose { l.Outf(color, s, args...) } } // Errf prints stuff to STDERR. func (l *Logger) Errf(color Color, s string, args ...any) { if len(args) == 0 { s, args = "%s", []any{s} } if !l.Color { color = None } print := color() print(l.Stderr, s, args...) } // VerboseErrf prints stuff to STDERR if verbose mode is enabled. func (l *Logger) VerboseErrf(color Color, s string, args ...any) { if l.Verbose { l.Errf(color, s, args...) } } func (l *Logger) Warnf(message string, args ...any) { l.Errf(Yellow, message, args...) } func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continueValues ...string) error { if l.AssumeYes { l.Outf(color, "%s [assuming yes]\n", prompt) return nil } if !l.AssumeTerm && !term.IsTerminal() { return ErrNoTerminal } if len(continueValues) == 0 { return errors.New("no continue values provided") } l.Outf(color, "%s [%s/%s]: ", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue)) reader := bufio.NewReader(l.Stdin) input, err := reader.ReadString('\n') if err != nil { return err } input = strings.TrimSpace(strings.ToLower(input)) if !slices.Contains(continueValues, input) { return ErrPromptCancelled } return nil } func (l *Logger) PrintExperiments() error { w := tabwriter.NewWriter(l.Stdout, 0, 8, 0, ' ', 0) for _, x := range experiments.List() { if !x.Active() { continue } l.FOutf(w, Yellow, "* ") l.FOutf(w, Green, x.Name) l.FOutf(w, Default, ": \t%s\n", x.String()) } return w.Flush() } ================================================ FILE: internal/output/group.go ================================================ package output import ( "bytes" "io" "github.com/go-task/task/v3/internal/templater" ) type Group struct { Begin, End string ErrorOnly bool } func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc) { gw := &groupWriter{writer: stdOut} if g.Begin != "" { gw.begin = templater.Replace(g.Begin, cache) + "\n" } if g.End != "" { gw.end = templater.Replace(g.End, cache) + "\n" } return gw, gw, func(err error) error { if g.ErrorOnly && err == nil { return nil } return gw.close() } } type groupWriter struct { writer io.Writer buff bytes.Buffer begin, end string } func (gw *groupWriter) Write(p []byte) (int, error) { return gw.buff.Write(p) } func (gw *groupWriter) close() error { switch { case gw.buff.Len() == 0: return nil case gw.begin == "" && gw.end == "": _, err := io.Copy(gw.writer, &gw.buff) return err default: _, err := io.Copy(gw.writer, gw.combinedBuff()) return err } } func (gw *groupWriter) combinedBuff() io.Reader { var b bytes.Buffer _, _ = b.WriteString(gw.begin) _, _ = io.Copy(&b, &gw.buff) _, _ = b.WriteString(gw.end) return &b } ================================================ FILE: internal/output/interleaved.go ================================================ package output import ( "io" "github.com/go-task/task/v3/internal/templater" ) type Interleaved struct{} func (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { return stdOut, stdErr, func(error) error { return nil } } ================================================ FILE: internal/output/output.go ================================================ package output import ( "fmt" "io" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) type Output interface { WrapWriter(stdOut, stdErr io.Writer, prefix string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc) } type CloseFunc func(err error) error // Build the Output for the requested ast.Output. func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) { switch o.Name { case "interleaved", "": if err := checkOutputGroupUnset(o); err != nil { return nil, err } return Interleaved{}, nil case "group": return Group{ Begin: o.Group.Begin, End: o.Group.End, ErrorOnly: o.Group.ErrorOnly, }, nil case "prefixed": if err := checkOutputGroupUnset(o); err != nil { return nil, err } return NewPrefixed(logger), nil default: return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) } } func checkOutputGroupUnset(o *ast.Output) error { if o.Group.IsSet() { return fmt.Errorf("task: output style %q does not support the group begin/end parameter", o.Name) } return nil } ================================================ FILE: internal/output/output_test.go ================================================ package output_test import ( "bytes" "errors" "fmt" "io" "testing" "github.com/fatih/color" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) func TestInterleaved(t *testing.T) { t.Parallel() var b bytes.Buffer var o output.Output = output.Interleaved{} w, _, _ := o.WrapWriter(&b, io.Discard, "", nil) fmt.Fprintln(w, "foo\nbar") assert.Equal(t, "foo\nbar\n", b.String()) fmt.Fprintln(w, "baz") assert.Equal(t, "foo\nbar\nbaz\n", b.String()) } func TestGroup(t *testing.T) { t.Parallel() var b bytes.Buffer var o output.Output = output.Group{} stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil) fmt.Fprintln(stdOut, "out\nout") assert.Equal(t, "", b.String()) fmt.Fprintln(stdErr, "err\nerr") assert.Equal(t, "", b.String()) fmt.Fprintln(stdOut, "out") assert.Equal(t, "", b.String()) fmt.Fprintln(stdErr, "err") assert.Equal(t, "", b.String()) require.NoError(t, cleanup(nil)) assert.Equal(t, "out\nout\nerr\nerr\nout\nerr\n", b.String()) } func TestGroupWithBeginEnd(t *testing.T) { t.Parallel() tmpl := templater.Cache{ Vars: ast.NewVars( &ast.VarElement{ Key: "VAR1", Value: ast.Var{Value: "example-value"}, }, ), } var o output.Output = output.Group{ Begin: "::group::{{ .VAR1 }}", End: "::endgroup::", } t.Run("simple", func(t *testing.T) { t.Parallel() var b bytes.Buffer w, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl) fmt.Fprintln(w, "foo\nbar") assert.Equal(t, "", b.String()) fmt.Fprintln(w, "baz") assert.Equal(t, "", b.String()) require.NoError(t, cleanup(nil)) assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String()) }) t.Run("no output", func(t *testing.T) { t.Parallel() var b bytes.Buffer _, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl) require.NoError(t, cleanup(nil)) assert.Equal(t, "", b.String()) }) } func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) { t.Parallel() var b bytes.Buffer var o output.Output = output.Group{ ErrorOnly: true, } stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil) _, _ = fmt.Fprintln(stdOut, "std-out") _, _ = fmt.Fprintln(stdErr, "std-err") require.NoError(t, cleanup(nil)) assert.Empty(t, b.String()) } func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { t.Parallel() var b bytes.Buffer var o output.Output = output.Group{ ErrorOnly: true, } stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil) _, _ = fmt.Fprintln(stdOut, "std-out") _, _ = fmt.Fprintln(stdErr, "std-err") require.NoError(t, cleanup(errors.New("any-error"))) assert.Equal(t, "std-out\nstd-err\n", b.String()) } func TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel var b bytes.Buffer l := &logger.Logger{ Color: false, } var o output.Output = output.NewPrefixed(l) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) t.Run("simple use cases", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel b.Reset() fmt.Fprintln(w, "foo\nbar") assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String()) fmt.Fprintln(w, "baz") assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String()) require.NoError(t, cleanup(nil)) }) t.Run("multiple writes for a single line", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel b.Reset() for _, char := range []string{"T", "e", "s", "t", "!"} { fmt.Fprint(w, char) assert.Equal(t, "", b.String()) } require.NoError(t, cleanup(nil)) assert.Equal(t, "[prefix] Test!\n", b.String()) }) } func TestPrefixedWithColor(t *testing.T) { t.Parallel() color.NoColor = false var b bytes.Buffer l := &logger.Logger{ Color: true, } var o output.Output = output.NewPrefixed(l) writers := make([]io.Writer, 16) for i := range writers { writers[i], _, _ = o.WrapWriter(&b, io.Discard, fmt.Sprintf("prefix-%d", i), nil) } t.Run("colors should loop", func(t *testing.T) { t.Parallel() for i, w := range writers { b.Reset() color := output.PrefixColorSequence[i%len(output.PrefixColorSequence)] var prefix bytes.Buffer l.FOutf(&prefix, color, fmt.Sprintf("prefix-%d", i)) fmt.Fprintln(w, "foo\nbar") assert.Equal( t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()), b.String(), ) } }) } ================================================ FILE: internal/output/prefixed.go ================================================ package output import ( "bytes" "fmt" "io" "strings" "sync" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" ) type Prefixed struct { logger *logger.Logger seen map[string]uint counter *uint mutex sync.Mutex } func NewPrefixed(logger *logger.Logger) *Prefixed { var counter uint return &Prefixed{ seen: make(map[string]uint), counter: &counter, logger: logger, } } func (p *Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: p} return pw, pw, func(error) error { return pw.close() } } type prefixWriter struct { writer io.Writer prefixed *Prefixed prefix string buff bytes.Buffer } func (pw *prefixWriter) Write(p []byte) (int, error) { n, err := pw.buff.Write(p) if err != nil { return n, err } return n, pw.writeOutputLines(false) } func (pw *prefixWriter) close() error { return pw.writeOutputLines(true) } func (pw *prefixWriter) writeOutputLines(force bool) error { for { switch line, err := pw.buff.ReadString('\n'); err { case nil: if err = pw.writeLine(line); err != nil { return err } case io.EOF: // if this line was not a complete line, re-add to the buffer if !force && !strings.HasSuffix(line, "\n") { _, err = pw.buff.WriteString(line) return err } return pw.writeLine(line) default: return err } } } var PrefixColorSequence = []logger.Color{ logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red, logger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed, } func (pw *prefixWriter) writeLine(line string) error { if line == "" { return nil } if !strings.HasSuffix(line, "\n") { line += "\n" } defer pw.prefixed.mutex.Unlock() pw.prefixed.mutex.Lock() idx, ok := pw.prefixed.seen[pw.prefix] if !ok { idx = *pw.prefixed.counter pw.prefixed.seen[pw.prefix] = idx *pw.prefixed.counter++ } if _, err := fmt.Fprint(pw.writer, "["); err != nil { return nil } color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))] pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix) if _, err := fmt.Fprint(pw.writer, "] "); err != nil { return nil } _, err := fmt.Fprint(pw.writer, line) return err } ================================================ FILE: internal/slicesext/slicesext.go ================================================ package slicesext import ( "cmp" "slices" ) func UniqueJoin[T cmp.Ordered](ss ...[]T) []T { var length int for _, s := range ss { length += len(s) } r := make([]T, length) var i int for _, s := range ss { i += copy(r[i:], s) } slices.Sort(r) return slices.Compact(r) } func Convert[T, U any](s []T, f func(T) U) []U { // Create a new slice with the same length as the input slice result := make([]U, len(s)) // Convert each element using the provided function for i, v := range s { result[i] = f(v) } return result } ================================================ FILE: internal/slicesext/slicesext_test.go ================================================ package slicesext import ( "math" "strconv" "testing" ) func TestConvertIntToString(t *testing.T) { t.Parallel() input := []int{1, 2, 3, 4, 5} expected := []string{"1", "2", "3", "4", "5"} result := Convert(input, strconv.Itoa) if len(result) != len(expected) { t.Errorf("Expected length %d, got %d", len(expected), len(result)) } for i := range expected { if result[i] != expected[i] { t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i]) } } } func TestConvertStringToInt(t *testing.T) { t.Parallel() input := []string{"1", "2", "3", "4", "5"} expected := []int{1, 2, 3, 4, 5} result := Convert(input, func(s string) int { n, _ := strconv.Atoi(s) return n }) if len(result) != len(expected) { t.Errorf("Expected length %d, got %d", len(expected), len(result)) } for i := range expected { if result[i] != expected[i] { t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i]) } } } func TestConvertFloatToInt(t *testing.T) { t.Parallel() input := []float64{1.1, 2.2, 3.7, 4.5, 5.9} expected := []int{1, 2, 4, 5, 6} result := Convert(input, func(f float64) int { return int(math.Round(f)) }) if len(result) != len(expected) { t.Errorf("Expected length %d, got %d", len(expected), len(result)) } for i := range expected { if result[i] != expected[i] { t.Errorf("At index %d: expected %v, got %v", i, expected[i], result[i]) } } } func TestConvertEmptySlice(t *testing.T) { t.Parallel() input := []int{} result := Convert(input, strconv.Itoa) if len(result) != 0 { t.Errorf("Expected empty slice, got length %d", len(result)) } } func TestConvertNilSlice(t *testing.T) { t.Parallel() var input []int result := Convert(input, strconv.Itoa) if result == nil { t.Error("Expected non-nil empty slice, got nil") } if len(result) != 0 { t.Errorf("Expected empty slice, got length %d", len(result)) } } ================================================ FILE: internal/sort/sorter.go ================================================ package sort import ( "slices" "sort" "strings" ) // A Sorter is any function that sorts a set of tasks. type Sorter func(items []string, namespaces []string) []string // NoSort leaves the tasks in the order they are defined. func NoSort(items []string, namespaces []string) []string { return items } // AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order // by task name. func AlphaNumeric(items []string, namespaces []string) []string { slices.Sort(items) return items } // AlphaNumericWithRootTasksFirst sorts the JSON output so that tasks are in // alpha numeric order by task name. It will also ensure that tasks that are not // namespaced will be listed before tasks that are. We detect this by searching // for a ':' in the task name. func AlphaNumericWithRootTasksFirst(items []string, namespaces []string) []string { if len(namespaces) > 0 { return AlphaNumeric(items, namespaces) } sort.Slice(items, func(i, j int) bool { iContainsColon := strings.Contains(items[i], ":") jContainsColon := strings.Contains(items[j], ":") if iContainsColon == jContainsColon { return items[i] < items[j] } if !iContainsColon && jContainsColon { return true } return false }) return items } ================================================ FILE: internal/sort/sorter_test.go ================================================ package sort import ( "testing" "github.com/stretchr/testify/assert" ) func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) { t.Parallel() item1 := "a-item1" item2 := "m-item2" item3 := "ns1:item3" item4 := "ns2:item4" item5 := "z-item5" item6 := "ns3:item6" tests := []struct { name string items []string want []string }{ { name: "no namespace items sorted alphabetically first", items: []string{item3, item2, item1}, want: []string{item1, item2, item3}, }, { name: "namespace items sorted alphabetically after non-namespaced items", items: []string{item3, item4, item5}, want: []string{item5, item3, item4}, }, { name: "all items sorted alphabetically with root items first", items: []string{item6, item5, item4, item3, item2, item1}, want: []string{item1, item2, item5, item3, item4, item6}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() AlphaNumericWithRootTasksFirst(tt.items, nil) assert.Equal(t, tt.want, tt.items) }) } } func TestAlphaNumeric_Sort(t *testing.T) { t.Parallel() item1 := "a-item1" item2 := "m-item2" item3 := "ns1:item3" item4 := "ns2:item4" item5 := "z-item5" item6 := "ns3:item6" tests := []struct { name string items []string want []string }{ { name: "all items sorted alphabetically", items: []string{item3, item2, item5, item1, item4, item6}, want: []string{item1, item2, item3, item4, item6, item5}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() AlphaNumeric(tt.items, nil) assert.Equal(t, tt.want, tt.items) }) } } func TestNoSort_Sort(t *testing.T) { t.Parallel() item1 := "a-item1" item2 := "m-item2" item3 := "ns1:item3" item4 := "ns2:item4" item5 := "z-item5" item6 := "ns3:item6" tests := []struct { name string items []string want []string }{ { name: "all items in order of definition", items: []string{item3, item2, item5, item1, item4, item6}, want: []string{item3, item2, item5, item1, item4, item6}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() NoSort(tt.items, nil) assert.Equal(t, tt.want, tt.items) }) } } ================================================ FILE: internal/summary/summary.go ================================================ package summary import ( "fmt" "os" "strings" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/taskfile/ast" ) func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []string) { for i, call := range c { PrintSpaceBetweenSummaries(l, i) if task, ok := t.Tasks.Get(call); ok { PrintTask(l, task) } } } func PrintSpaceBetweenSummaries(l *logger.Logger, i int) { spaceRequired := i > 0 if !spaceRequired { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "\n") } func PrintTask(l *logger.Logger, t *ast.Task) { printTaskName(l, t) printTaskDescribingText(t, l) printTaskVars(l, t) printTaskEnv(l, t) printTaskRequires(l, t) printTaskDependencies(l, t) printTaskAliases(l, t) printTaskCommands(l, t) } func printTaskDescribingText(t *ast.Task, l *logger.Logger) { if hasSummary(t) { printTaskSummary(l, t) } else if hasDescription(t) { printTaskDescription(l, t) } else { printNoDescriptionOrSummary(l) } } func hasSummary(t *ast.Task) bool { return t.Summary != "" } func printTaskSummary(l *logger.Logger, t *ast.Task) { lines := strings.Split(t.Summary, "\n") for i, line := range lines { notLastLine := i+1 < len(lines) if notLastLine || line != "" { l.Outf(logger.Default, "%s\n", line) } } } func printTaskName(l *logger.Logger, t *ast.Task) { l.Outf(logger.Default, "task: ") l.Outf(logger.Green, "%s\n", t.Name()) l.Outf(logger.Default, "\n") } func printTaskAliases(l *logger.Logger, t *ast.Task) { if len(t.Aliases) == 0 { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "aliases:\n") for _, alias := range t.Aliases { l.Outf(logger.Default, " - ") l.Outf(logger.Cyan, "%s\n", alias) } } func hasDescription(t *ast.Task) bool { return t.Desc != "" } func printTaskDescription(l *logger.Logger, t *ast.Task) { l.Outf(logger.Default, "%s\n", t.Desc) } func printNoDescriptionOrSummary(l *logger.Logger) { l.Outf(logger.Default, "(task does not have description or summary)\n") } func printTaskDependencies(l *logger.Logger, t *ast.Task) { if len(t.Deps) == 0 { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "dependencies:\n") for _, d := range t.Deps { l.Outf(logger.Default, " - %s\n", d.Task) } } func printTaskCommands(l *logger.Logger, t *ast.Task) { if len(t.Cmds) == 0 { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "commands:\n") for _, c := range t.Cmds { isCommand := c.Cmd != "" l.Outf(logger.Default, " - ") if isCommand { l.Outf(logger.Yellow, "%s\n", c.Cmd) } else { l.Outf(logger.Green, "Task: %s\n", c.Task) } } } func printTaskVars(l *logger.Logger, t *ast.Task) { if t.Vars == nil || t.Vars.Len() == 0 { return } osEnvVars := getEnvVarNames() taskfileEnvVars := make(map[string]bool) if t.Env != nil { for key := range t.Env.All() { taskfileEnvVars[key] = true } } hasNonEnvVars := false for key := range t.Vars.All() { if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] { hasNonEnvVars = true break } } if !hasNonEnvVars { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "vars:\n") for key, value := range t.Vars.All() { // Only display variables that are not from OS environment or Taskfile env if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] { formattedValue := formatVarValue(value) l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue) } } } func printTaskEnv(l *logger.Logger, t *ast.Task) { if t.Env == nil || t.Env.Len() == 0 { return } envVars := getEnvVarNames() hasNonEnvVars := false for key := range t.Env.All() { if !isEnvVar(key, envVars) { hasNonEnvVars = true break } } if !hasNonEnvVars { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "env:\n") for key, value := range t.Env.All() { // Only display variables that are not from OS environment if !isEnvVar(key, envVars) { formattedValue := formatVarValue(value) l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue) } } } // formatVarValue formats a variable value based on its type. // Handles static values, shell commands (sh:), references (ref:), and maps. func formatVarValue(v ast.Var) string { // Shell command - check this first before Value // because dynamic vars may have both Sh and an empty Value if v.Sh != nil { return fmt.Sprintf("sh: %s", *v.Sh) } // Reference if v.Ref != "" { return fmt.Sprintf("ref: %s", v.Ref) } // Static value if v.Value != nil { // Check if it's a map or complex type if m, ok := v.Value.(map[string]any); ok { return formatMap(m, 4) } // Simple string value return fmt.Sprintf(`"%v"`, v.Value) } return `""` } // formatMap formats a map value with proper indentation for YAML. func formatMap(m map[string]any, indent int) string { if len(m) == 0 { return "{}" } var result strings.Builder result.WriteString("\n") spaces := strings.Repeat(" ", indent) for k, v := range m { result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v)) //nolint:staticcheck } return result.String() } func printTaskRequires(l *logger.Logger, t *ast.Task) { if t.Requires == nil || len(t.Requires.Vars) == 0 { return } l.Outf(logger.Default, "\n") l.Outf(logger.Default, "requires:\n") l.Outf(logger.Default, " vars:\n") for _, v := range t.Requires.Vars { // If the variable has enum constraints, format accordingly if len(v.Enum) > 0 { l.Outf(logger.Yellow, " - %s:\n", v.Name) l.Outf(logger.Yellow, " enum:\n") for _, enumValue := range v.Enum { l.Outf(logger.Yellow, " - %s\n", enumValue) } } else { // Simple required variable l.Outf(logger.Yellow, " - %s\n", v.Name) } } } func getEnvVarNames() map[string]bool { envMap := make(map[string]bool) for _, e := range os.Environ() { parts := strings.SplitN(e, "=", 2) if len(parts) > 0 { envMap[parts[0]] = true } } return envMap } // isEnvVar checks if a variable is from OS environment or auto-generated by Task. func isEnvVar(key string, envVars map[string]bool) bool { // Filter out auto-generated Task variables if strings.HasPrefix(key, "TASK_") || strings.HasPrefix(key, "CLI_") || strings.HasPrefix(key, "ROOT_") || key == "TASK" || key == "TASKFILE" || key == "TASKFILE_DIR" || key == "USER_WORKING_DIR" || key == "ALIAS" || key == "MATCH" { return true } return envVars[key] } ================================================ FILE: internal/summary/summary_test.go ================================================ package summary_test import ( "bytes" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/taskfile/ast" ) func TestPrintsDependenciesIfPresent(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Deps: []*ast.Dep{ {Task: "dep1"}, {Task: "dep2"}, {Task: "dep3"}, }, } summary.PrintTask(&l, task) assert.Contains(t, buffer.String(), "\ndependencies:\n - dep1\n - dep2\n - dep3\n") } func createDummyLogger() (*bytes.Buffer, logger.Logger) { buffer := &bytes.Buffer{} l := logger.Logger{ Stderr: buffer, Stdout: buffer, Verbose: false, } return buffer, l } func TestDoesNotPrintDependenciesIfMissing(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Deps: []*ast.Dep{}, } summary.PrintTask(&l, task) assert.NotContains(t, buffer.String(), "dependencies:") } func TestPrintTaskName(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Task: "my-task-name", } summary.PrintTask(&l, task) assert.Contains(t, buffer.String(), "task: my-task-name\n") } func TestPrintTaskCommandsIfPresent(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Cmds: []*ast.Cmd{ {Cmd: "command-1"}, {Cmd: "command-2"}, {Task: "task-1"}, }, } summary.PrintTask(&l, task) assert.Contains(t, buffer.String(), "\ncommands:\n") assert.Contains(t, buffer.String(), "\n - command-1\n") assert.Contains(t, buffer.String(), "\n - command-2\n") assert.Contains(t, buffer.String(), "\n - Task: task-1\n") } func TestDoesNotPrintCommandIfMissing(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Cmds: []*ast.Cmd{}, } summary.PrintTask(&l, task) assert.NotContains(t, buffer.String(), "commands") } func TestLayout(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() task := &ast.Task{ Task: "sample-task", Summary: "line1\nline2\nline3\n", Deps: []*ast.Dep{ {Task: "dependency"}, }, Cmds: []*ast.Cmd{ {Cmd: "command"}, }, } summary.PrintTask(&l, task) assert.Equal(t, expectedOutput(), buffer.String()) } func expectedOutput() string { expected := `task: sample-task line1 line2 line3 dependencies: - dependency commands: - command ` return expected } func TestPrintDescriptionAsFallback(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() taskWithoutSummary := &ast.Task{ Desc: "description", } taskWithSummary := &ast.Task{ Desc: "description", Summary: "summary", } taskWithoutSummaryOrDescription := &ast.Task{} summary.PrintTask(&l, taskWithoutSummary) assert.Contains(t, buffer.String(), "description") buffer.Reset() summary.PrintTask(&l, taskWithSummary) assert.NotContains(t, buffer.String(), "description") buffer.Reset() summary.PrintTask(&l, taskWithoutSummaryOrDescription) assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n") } func TestPrintAllWithSpaces(t *testing.T) { t.Parallel() buffer, l := createDummyLogger() t1 := &ast.Task{Task: "t1"} t2 := &ast.Task{Task: "t2"} t3 := &ast.Task{Task: "t3"} tasks := ast.NewTasks() tasks.Set("t1", t1) tasks.Set("t2", t2) tasks.Set("t3", t3) summary.PrintTasks(&l, &ast.Taskfile{Tasks: tasks}, []string{"t1", "t2", "t3"}, ) assert.True(t, strings.HasPrefix(buffer.String(), "task: t1")) assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2") assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t3") } ================================================ FILE: internal/sysinfo/uid.go ================================================ //go:build !windows package sysinfo import ( "os" "syscall" ) func Owner(path string) (int, error) { info, err := os.Stat(path) if err != nil { return 0, err } var uid int if stat, ok := info.Sys().(*syscall.Stat_t); ok { uid = int(stat.Uid) } else { uid = os.Getuid() } return uid, nil } ================================================ FILE: internal/sysinfo/uid_win.go ================================================ //go:build windows package sysinfo // NOTE: This always returns -1 since there is currently no easy way to get // file owner information on Windows. func Owner(path string) (int, error) { return -1, nil } ================================================ FILE: internal/templater/funcs.go ================================================ package templater import ( "maps" "math/rand/v2" "path/filepath" "runtime" "strings" "github.com/davecgh/go-spew/spew" "github.com/google/uuid" "go.yaml.in/yaml/v3" "mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/syntax" sprig "github.com/go-task/slim-sprig/v3" "github.com/go-task/template" ) var templateFuncs template.FuncMap func init() { taskFuncs := template.FuncMap{ "OS": os, "ARCH": arch, "numCPU": runtime.NumCPU, "catLines": catLines, "splitLines": splitLines, "fromSlash": filepath.FromSlash, "toSlash": filepath.ToSlash, "exeExt": exeExt, "shellQuote": shellQuote, "splitArgs": splitArgs, "IsSH": IsSH, // Deprecated "joinPath": filepath.Join, "relPath": filepath.Rel, "merge": merge, "spew": spew.Sdump, "fromYaml": fromYaml, "mustFromYaml": mustFromYaml, "toYaml": toYaml, "mustToYaml": mustToYaml, "uuid": uuid.New, "randIntN": rand.IntN, } // aliases taskFuncs["q"] = taskFuncs["shellQuote"] // Deprecated aliases for renamed functions. taskFuncs["FromSlash"] = taskFuncs["fromSlash"] taskFuncs["ToSlash"] = taskFuncs["toSlash"] taskFuncs["ExeExt"] = taskFuncs["exeExt"] templateFuncs = template.FuncMap(sprig.TxtFuncMap()) maps.Copy(templateFuncs, taskFuncs) } func os() string { return runtime.GOOS } func arch() string { return runtime.GOARCH } func catLines(s string) string { s = strings.ReplaceAll(s, "\r\n", " ") return strings.ReplaceAll(s, "\n", " ") } func splitLines(s string) []string { s = strings.ReplaceAll(s, "\r\n", "\n") return strings.Split(s, "\n") } func exeExt() string { if runtime.GOOS == "windows" { return ".exe" } return "" } func shellQuote(str string) (string, error) { return syntax.Quote(str, syntax.LangBash) } func splitArgs(s string) ([]string, error) { return shell.Fields(s, nil) } // Deprecated: now always returns true func IsSH() bool { return true } func merge(base map[string]any, v ...map[string]any) map[string]any { cap := len(v) for _, m := range v { cap += len(m) } result := make(map[string]any, cap) maps.Copy(result, base) for _, m := range v { maps.Copy(result, m) } return result } func fromYaml(v string) any { output, _ := mustFromYaml(v) return output } func mustFromYaml(v string) (any, error) { var output any err := yaml.Unmarshal([]byte(v), &output) return output, err } func toYaml(v any) string { output, _ := yaml.Marshal(v) return string(output) } func mustToYaml(v any) (string, error) { output, err := yaml.Marshal(v) if err != nil { return "", err } return string(output), nil } ================================================ FILE: internal/templater/templater.go ================================================ package templater import ( "bytes" "fmt" "maps" "strings" "github.com/go-task/template" "github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/taskfile/ast" ) // Cache is a help struct that allow us to call "replaceX" funcs multiple // times, without having to check for error each time. The first error that // happen will be assigned to r.err, and consecutive calls to funcs will just // return the zero value. type Cache struct { Vars *ast.Vars cacheMap map[string]any err error } func (r *Cache) ResetCache() { r.cacheMap = r.Vars.ToCacheMap() } func (r *Cache) Err() error { return r.err } func ResolveRef(ref string, cache *Cache) any { // If there is already an error, do nothing if cache.err != nil { return nil } // Initialize the cache map if it's not already initialized if cache.cacheMap == nil { cache.cacheMap = cache.Vars.ToCacheMap() } if ref == "." { return cache.cacheMap } t, err := template.New("resolver").Funcs(templateFuncs).Parse(fmt.Sprintf("{{%s}}", ref)) if err != nil { cache.err = err return nil } val, err := t.Resolve(cache.cacheMap) if err != nil { cache.err = err return nil } return val } func Replace[T any](v T, cache *Cache) T { return ReplaceWithExtra(v, cache, nil) } func ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T { // If there is already an error, do nothing if cache.err != nil { return v } // Initialize the cache map if it's not already initialized if cache.cacheMap == nil { cache.cacheMap = cache.Vars.ToCacheMap() } // Create a copy of the cache map to avoid editing the original // If there is extra data, merge it with the cache map data := maps.Clone(cache.cacheMap) if extra != nil { maps.Copy(data, extra) } // Traverse the value and parse any template variables copy, err := deepcopy.TraverseStringsFunc(v, func(v string) (string, error) { tpl, err := template.New("").Funcs(templateFuncs).Parse(v) if err != nil { return v, err } var b bytes.Buffer if err := tpl.Execute(&b, data); err != nil { return v, err } return strings.ReplaceAll(b.String(), "", ""), nil }) if err != nil { cache.err = err return v } return copy } func ReplaceGlobs(globs []*ast.Glob, cache *Cache) []*ast.Glob { if cache.err != nil || len(globs) == 0 { return nil } new := make([]*ast.Glob, len(globs)) for i, g := range globs { new[i] = &ast.Glob{ Glob: Replace(g.Glob, cache), Negate: g.Negate, } } return new } func ReplaceVar(v ast.Var, cache *Cache) ast.Var { return ReplaceVarWithExtra(v, cache, nil) } func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var { if v.Ref != "" { return ast.Var{Value: ResolveRef(v.Ref, cache)} } return ast.Var{ Value: ReplaceWithExtra(v.Value, cache, extra), Sh: ReplaceWithExtra(v.Sh, cache, extra), Live: v.Live, Ref: v.Ref, Dir: v.Dir, } } func ReplaceVars(vars *ast.Vars, cache *Cache) *ast.Vars { return ReplaceVarsWithExtra(vars, cache, nil) } func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *ast.Vars { if cache.err != nil || vars.Len() == 0 { return nil } newVars := ast.NewVars() for k, v := range vars.All() { newVars.Set(k, ReplaceVarWithExtra(v, cache, extra)) } return newVars } ================================================ FILE: internal/term/term.go ================================================ package term import ( "os" "golang.org/x/term" ) func IsTerminal() bool { return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) } ================================================ FILE: internal/version/version.go ================================================ package version import ( _ "embed" "runtime/debug" "strings" ) var ( //go:embed version.txt version string commit string dirty bool ) func init() { version = strings.TrimSpace(version) // Attempt to get build info from the Go runtime. We only use this if not // built from a tagged version. if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version == "(devel)" { commit = getCommit(info) dirty = getDirty(info) } } func getDirty(info *debug.BuildInfo) bool { for _, setting := range info.Settings { if setting.Key == "vcs.modified" { return setting.Value == "true" } } return false } func getCommit(info *debug.BuildInfo) string { for _, setting := range info.Settings { if setting.Key == "vcs.revision" { return setting.Value[:7] } } return "" } // GetVersion returns the version of Task. By default, this is retrieved from // the embedded version.txt file which is kept up-to-date by our release script. // However, it can also be overridden at build time using: // -ldflags="-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'". func GetVersion() string { return version } // GetVersionWithBuildInfo is the same as [GetVersion], but it also includes // the commit hash and dirty status if available. This will only work when built // within inside of a Git checkout. func GetVersionWithBuildInfo() string { var buildMetadata []string if commit != "" { buildMetadata = append(buildMetadata, commit) } if dirty { buildMetadata = append(buildMetadata, "dirty") } if len(buildMetadata) > 0 { return version + "+" + strings.Join(buildMetadata, ".") } return version } ================================================ FILE: internal/version/version.txt ================================================ 3.49.1 ================================================ FILE: precondition.go ================================================ package task import ( "context" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/taskfile/ast" ) // ErrPreconditionFailed is returned when a precondition fails var ErrPreconditionFailed = errors.New("task: precondition not met") func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *ast.Task) (bool, error) { for _, p := range t.Preconditions { err := execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: p.Sh, Dir: t.Dir, Env: env.Get(t), }) if err != nil { if !errors.Is(err, context.Canceled) { e.Logger.Errf(logger.Magenta, "task: %s\n", p.Msg) } return false, ErrPreconditionFailed } } return true, nil } ================================================ FILE: requires.go ================================================ package task import ( "slices" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/input" "github.com/go-task/task/v3/internal/term" "github.com/go-task/task/v3/taskfile/ast" ) func (e *Executor) canPrompt() bool { return e.Interactive && (e.AssumeTerm || term.IsTerminal()) } func (e *Executor) newPrompter() *input.Prompter { return &input.Prompter{ Stdin: e.Stdin, Stdout: e.Stdout, Stderr: e.Stderr, } } // promptDepsVars traverses the dependency tree, collects all missing required // variables, and prompts for them upfront. This is used for deps which execute // in parallel, so all prompts must happen before execution to avoid interleaving. // Prompted values are stored in e.promptedVars for injection into task calls. func (e *Executor) promptDepsVars(calls []*Call) error { if !e.canPrompt() { return nil } // Collect all missing vars from the dependency tree visited := make(map[string]bool) varsMap := make(map[string]*ast.VarsWithValidation) var collect func(call *Call) error collect = func(call *Call) error { compiledTask, err := e.FastCompiledTask(call) if err != nil { return err } for _, v := range getMissingRequiredVars(compiledTask) { if _, exists := varsMap[v.Name]; !exists { varsMap[v.Name] = v } } // Check visited AFTER collecting vars to handle duplicate task calls with different vars if visited[call.Task] { return nil } visited[call.Task] = true for _, dep := range compiledTask.Deps { depCall := &Call{ Task: dep.Task, Vars: dep.Vars, Silent: dep.Silent, } if err := collect(depCall); err != nil { return err } } return nil } for _, call := range calls { if err := collect(call); err != nil { return err } } if len(varsMap) == 0 { return nil } prompter := e.newPrompter() e.promptedVars = ast.NewVars() for _, v := range varsMap { value, err := prompter.Prompt(v.Name, v.Enum) if err != nil { if errors.Is(err, input.ErrCancelled) { return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"} } return err } e.promptedVars.Set(v.Name, ast.Var{Value: value}) } return nil } // promptTaskVars prompts for any missing required vars from a single task. // Used for sequential task calls (cmds) where we can prompt just-in-time. // Returns true if any vars were prompted (caller should recompile the task). func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) { if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 { return false, nil } // Find missing vars, excluding already prompted ones var missing []*ast.VarsWithValidation for _, v := range getMissingRequiredVars(t) { if e.promptedVars != nil { if _, ok := e.promptedVars.Get(v.Name); ok { continue } } missing = append(missing, v) } if len(missing) == 0 { return false, nil } prompter := e.newPrompter() for _, v := range missing { value, err := prompter.Prompt(v.Name, v.Enum) if err != nil { if errors.Is(err, input.ErrCancelled) { return false, &errors.TaskCancelledByUserError{TaskName: t.Name()} } return false, err } // Add to call.Vars for recompilation if call.Vars == nil { call.Vars = ast.NewVars() } call.Vars.Set(v.Name, ast.Var{Value: value}) // Cache for reuse by other tasks if e.promptedVars == nil { e.promptedVars = ast.NewVars() } e.promptedVars.Set(v.Name, ast.Var{Value: value}) } return true, nil } // getMissingRequiredVars returns required vars that are not set in the task's vars. func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation { if t.Requires == nil { return nil } var missing []*ast.VarsWithValidation for _, v := range t.Requires.Vars { if _, ok := t.Vars.Get(v.Name); !ok { missing = append(missing, v) } } return missing } func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error { missing := getMissingRequiredVars(t) if len(missing) == 0 { return nil } missingVars := make([]errors.MissingVar, len(missing)) for i, v := range missing { missingVars[i] = errors.MissingVar{ Name: v.Name, AllowedValues: v.Enum, } } return &errors.TaskMissingRequiredVarsError{ TaskName: t.Name(), MissingVars: missingVars, } } func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error { if t.Requires == nil || len(t.Requires.Vars) == 0 { return nil } var notAllowedValuesVars []errors.NotAllowedVar for _, requiredVar := range t.Requires.Vars { varValue, _ := t.Vars.Get(requiredVar.Name) value, isString := varValue.Value.(string) if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) { notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{ Value: value, Enum: requiredVar.Enum, Name: requiredVar.Name, }) } } if len(notAllowedValuesVars) > 0 { return &errors.TaskNotAllowedVarsError{ TaskName: t.Name(), NotAllowedVars: notAllowedValuesVars, } } return nil } ================================================ FILE: setup.go ================================================ package task import ( "context" "fmt" "os" "path/filepath" "slices" "strings" "sync" "github.com/Masterminds/semver/v3" "github.com/sajari/fuzzy" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile/ast" ) func (e *Executor) Setup() error { e.setupLogger() node, err := e.getRootNode() if err != nil { return err } if err := e.setupTempDir(); err != nil { return err } if err := e.readTaskfile(node); err != nil { return err } e.setupStdFiles() if err := e.setupOutput(); err != nil { return err } if err := e.setupCompiler(); err != nil { return err } if err := e.readDotEnvFiles(); err != nil { return err } if err := e.doVersionChecks(); err != nil { return err } e.setupDefaults() e.setupConcurrencyState() return nil } func (e *Executor) getRootNode() (taskfile.Node, error) { node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout, taskfile.WithCACert(e.CACert), taskfile.WithCert(e.Cert), taskfile.WithCertKey(e.CertKey), ) var taskNotFoundError errors.TaskfileNotFoundError if errors.As(err, &taskNotFoundError) { taskNotFoundError.AskInit = true return nil, taskNotFoundError } if err != nil { return nil, err } e.Dir = node.Dir() e.Entrypoint = node.Location() return node, err } func (e *Executor) readTaskfile(node taskfile.Node) error { ctx, cf := context.WithTimeout(context.Background(), e.Timeout) defer cf() debugFunc := func(s string) { e.Logger.VerboseOutf(logger.Magenta, s) } promptFunc := func(s string) error { return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes") } reader := taskfile.NewReader( taskfile.WithInsecure(e.Insecure), taskfile.WithDownload(e.Download), taskfile.WithOffline(e.Offline), taskfile.WithTrustedHosts(e.TrustedHosts), taskfile.WithTempDir(e.TempDir.Remote), taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration), taskfile.WithReaderCACert(e.CACert), taskfile.WithReaderCert(e.Cert), taskfile.WithReaderCertKey(e.CertKey), taskfile.WithDebugFunc(debugFunc), taskfile.WithPromptFunc(promptFunc), ) graph, err := reader.Read(ctx, node) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout} } return err } if e.Taskfile, err = graph.Merge(); err != nil { return err } return nil } func (e *Executor) setupFuzzyModel() { if e.Taskfile == nil { return } model := fuzzy.NewModel() model.SetThreshold(1) // because we want to build grammar based on every task name var words []string for name, task := range e.Taskfile.Tasks.All(nil) { if task.Internal { continue } words = append(words, name) words = slices.Concat(words, task.Aliases) } model.Train(words) e.fuzzyModel = model } func (e *Executor) setupTempDir() error { if e.TempDir != (TempDir{}) { return nil } tempDir := env.GetTaskEnv("TEMP_DIR") if tempDir == "" { e.TempDir = TempDir{ Remote: filepathext.SmartJoin(e.Dir, ".task"), Fingerprint: filepathext.SmartJoin(e.Dir, ".task"), } } else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") { tempDir, err := execext.ExpandLiteral(tempDir) if err != nil { return err } projectDir, _ := filepath.Abs(e.Dir) projectName := filepath.Base(projectDir) e.TempDir = TempDir{ Remote: tempDir, Fingerprint: filepathext.SmartJoin(tempDir, projectName), } } else { e.TempDir = TempDir{ Remote: filepathext.SmartJoin(e.Dir, tempDir), Fingerprint: filepathext.SmartJoin(e.Dir, tempDir), } } // RemoteCacheDir from taskrc/env can override the remote cache directory if e.RemoteCacheDir != "" { if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") { remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir) if err != nil { return err } e.TempDir.Remote = remoteCacheDir } else { e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir) } } return nil } func (e *Executor) setupStdFiles() { if e.Stdin == nil { e.Stdin = os.Stdin } if e.Stdout == nil { e.Stdout = os.Stdout } if e.Stderr == nil { e.Stderr = os.Stderr } } func (e *Executor) setupLogger() { e.Logger = &logger.Logger{ Stdin: e.Stdin, Stdout: e.Stdout, Stderr: e.Stderr, Verbose: e.Verbose, Color: e.Color, AssumeYes: e.AssumeYes, AssumeTerm: e.AssumeTerm, } } func (e *Executor) setupOutput() error { if !e.OutputStyle.IsSet() { e.OutputStyle = e.Taskfile.Output } var err error e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger) return err } func (e *Executor) setupCompiler() error { if e.UserWorkingDir == "" { var err error e.UserWorkingDir, err = os.Getwd() if err != nil { return err } } e.Compiler = &Compiler{ Dir: e.Dir, Entrypoint: e.Entrypoint, UserWorkingDir: e.UserWorkingDir, TaskfileEnv: e.Taskfile.Env, TaskfileVars: e.Taskfile.Vars, Logger: e.Logger, } return nil } func (e *Executor) readDotEnvFiles() error { if e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 { return nil } if e.Taskfile.Version.LessThan(ast.V3) { return nil } vars, err := e.Compiler.GetTaskfileVariables() if err != nil { return err } env, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir) if err != nil { return err } for k, v := range env.All() { if _, ok := e.Taskfile.Env.Get(k); !ok { e.Taskfile.Env.Set(k, v) } } return err } func (e *Executor) setupDefaults() { if e.Taskfile.Method == "" { e.Taskfile.Method = "checksum" } if e.Taskfile.Run == "" { e.Taskfile.Run = "always" } } func (e *Executor) setupConcurrencyState() { e.executionHashes = make(map[string]context.Context) e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len()) e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len()) for k := range e.Taskfile.Tasks.Keys(nil) { e.taskCallCount[k] = new(int32) e.mkdirMutexMap[k] = &sync.Mutex{} } if e.Concurrency > 0 { e.concurrencySemaphore = make(chan struct{}, e.Concurrency) } } func (e *Executor) doVersionChecks() error { if !e.EnableVersionCheck { return nil } // Copy the version to avoid modifying the original schemaVersion := &semver.Version{} *schemaVersion = *e.Taskfile.Version // Error if the Taskfile uses a schema version below v3 if schemaVersion.LessThan(ast.V3) { return &errors.TaskfileVersionCheckError{ URI: e.Taskfile.Location, SchemaVersion: schemaVersion, Message: `no longer supported. Please use v3 or above`, } } // Get the current version of Task // If we can't parse the version (e.g. when its "devel"), then ignore the current version checks currentVersion, err := semver.NewVersion(version.GetVersion()) if err != nil { return nil } // Error if the Taskfile uses a schema version above the current version of Task if schemaVersion.GreaterThan(currentVersion) { return &errors.TaskfileVersionCheckError{ URI: e.Taskfile.Location, SchemaVersion: schemaVersion, Message: fmt.Sprintf(`is greater than the current version of Task (%s)`, currentVersion.String()), } } return nil } ================================================ FILE: signals.go ================================================ package task import ( "os" "os/signal" "syscall" "github.com/go-task/task/v3/internal/logger" ) const maxInterruptSignals = 3 // NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals // so the Task process is not killed immediately and processes running have // time to do cleanup work. func (e *Executor) InterceptInterruptSignals() { ch := make(chan os.Signal, maxInterruptSignals) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { for i := range maxInterruptSignals { sig := <-ch if i+1 >= maxInterruptSignals { e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig) os.Exit(1) } e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig) } }() } ================================================ FILE: signals_test.go ================================================ //go:build signals // +build signals // This file contains tests for signal handling on Unix. // Based on code from https://github.com/marco-m/timeit // Due to how signals work, for robustness we always spawn a separate process; // we never send signals to the test process. package task_test import ( "bytes" "errors" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "time" ) var SLEEPIT, _ = filepath.Abs("./bin/sleepit") func TestSignalSentToProcessGroup(t *testing.T) { task, err := getTaskPath() if err != nil { t.Fatal(err) } testCases := map[string]struct { args []string sendSigs int want []string notWant []string }{ // regression: // - child is terminated, immediately, by "context canceled" (another bug???) "child does not handle sigint: receives sigint and terminates immediately": { args: []string{task, "--", SLEEPIT, "default", "-sleep=10s"}, sendSigs: 1, want: []string{ "sleepit: ready\n", "sleepit: work started\n", "task: Signal received: \"interrupt\"\n", // 130 = 128 + SIGINT "task: Failed to run task \"default\": exit status 130\n", }, notWant: []string{ "task: Failed to run task \"default\": context canceled\n", }, }, // 2 regressions: // - child receives 2 signals instead of 1 // - child is terminated, immediately, by "context canceled" (another bug???) // TODO we need -cleanup=2s only to show reliably the bug; once the fix is committed, // we can use -cleanup=50ms to speed the test up "child intercepts sigint: receives sigint and does cleanup": { args: []string{task, "--", SLEEPIT, "handle", "-sleep=10s", "-cleanup=2s"}, sendSigs: 1, want: []string{ "sleepit: ready\n", "sleepit: work started\n", "task: Signal received: \"interrupt\"\n", "sleepit: got signal=interrupt count=1\n", "sleepit: work canceled\n", "sleepit: cleanup started\n", "sleepit: cleanup done\n", "task: Failed to run task \"default\": exit status 3\n", }, notWant: []string{ "sleepit: got signal=interrupt count=2\n", "task: Failed to run task \"default\": context canceled\n", }, }, // regression: child receives 2 signal instead of 1 and thus terminates abruptly "child simulates terraform: receives 1 sigint and does cleanup": { args: []string{task, "--", SLEEPIT, "handle", "-term-after=2", "-sleep=10s", "-cleanup=50ms"}, sendSigs: 1, want: []string{ "sleepit: ready\n", "sleepit: work started\n", "task: Signal received: \"interrupt\"\n", "sleepit: got signal=interrupt count=1\n", "sleepit: work canceled\n", "sleepit: cleanup started\n", "sleepit: cleanup done\n", "task: Failed to run task \"default\": exit status 3\n", }, notWant: []string{ "sleepit: got signal=interrupt count=2\n", "sleepit: cleanup canceled\n", "task: Failed to run task \"default\": exit status 4\n", }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { var out bytes.Buffer sut := exec.Command(tc.args[0], tc.args[1:]...) sut.Stdout = &out sut.Stderr = &out sut.Dir = "testdata/ignore_signals" // Create a new process group by setting the process group ID of the child // to the child PID. // By default, the child would inherit the process group of the parent, but // we want to avoid this, to protect the parent (the test process) from the // signal that this test will send. More info in the comments below for // syscall.Kill(). sut.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} if err := sut.Start(); err != nil { t.Fatalf("starting the SUT process: %v", err) } // After the child is started, we want to avoid a race condition where we send // it a signal before it had time to setup its own signal handlers. Sleeping // is way too flaky, instead we parse the child output until we get a line // that we know is printed after the signal handlers are installed... ready := false timeout := time.Duration(time.Second) start := time.Now() for time.Since(start) < timeout { if strings.Contains(out.String(), "sleepit: ready\n") { ready = true break } time.Sleep(10 * time.Millisecond) } if !ready { t.Fatalf("sleepit not ready after %v\n"+ "additional information:\n"+ " output:\n%s", timeout, out.String()) } // When we have a running program in a shell and type CTRL-C, the tty driver // will send a SIGINT signal to all the processes in the foreground process // group (see https://en.wikipedia.org/wiki/Process_group). // // Here we want to emulate this behavior: send SIGINT to the process group of // the test executable. Although Go for some reasons doesn't wrap the // killpg(2) system call, what works is using syscall.Kill(-PID, SIGINT), // where the negative PID means the corresponding process group. Note that // this negative PID works only as long as the caller of the kill(2) system // call has a different PID, which is the case for this test. for range tc.sendSigs - 1 { if err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil { t.Fatalf("sending INT signal to the process group: %v", err) } time.Sleep(1 * time.Millisecond) } err := sut.Wait() var wantErr *exec.ExitError const wantExitStatus = 201 if errors.As(err, &wantErr) { if wantErr.ExitCode() != wantExitStatus { t.Errorf( "waiting for child process: got exit status %v; want %d\n"+ "additional information:\n"+ " process state: %q", wantErr.ExitCode(), wantExitStatus, wantErr.String()) } } else { t.Errorf("waiting for child process: got unexpected error type %v (%T); want (%T)", err, err, wantErr) } gotLines := strings.SplitAfter(out.String(), "\n") notFound := listDifference(tc.want, gotLines) if len(notFound) > 0 { t.Errorf("\nwanted but not found:\n%v", notFound) } found := listIntersection(tc.notWant, gotLines) if len(found) > 0 { t.Errorf("\nunwanted but found:\n%v", found) } if len(notFound) > 0 || len(found) > 0 { t.Errorf("\noutput:\n%v", gotLines) } }) } } func getTaskPath() (string, error) { if info, err := os.Stat("./bin/task"); err == nil { return info.Name(), nil } if path, err := exec.LookPath("task"); err == nil { return path, nil } return "", errors.New("task: \"task\" binary was not found!") } // Return the difference of the two lists: the elements that are present in the first // list, but not in the second one. The notion of presence is not with `=` but with // string.Contains(l2, l1). // FIXME this does not enforce ordering. We might want to support both. func listDifference(lines1, lines2 []string) []string { difference := []string{} for _, l1 := range lines1 { found := false for _, l2 := range lines2 { if strings.Contains(l2, l1) { found = true break } } if !found { difference = append(difference, l1) } } return difference } // Return the intersection of the two lists: the elements that are present in both lists. // The notion of presence is not with '=' but with string.Contains(l2, l1) // FIXME this does not enforce ordering. We might want to support both. func listIntersection(lines1, lines2 []string) []string { intersection := []string{} for _, l1 := range lines1 { for _, l2 := range lines2 { if strings.Contains(l2, l1) { intersection = append(intersection, l1) break } } } return intersection } ================================================ FILE: status.go ================================================ package task import ( "context" "fmt" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/taskfile/ast" ) // Status returns an error if any the of given tasks is not up-to-date func (e *Executor) Status(ctx context.Context, calls ...*Call) error { for _, call := range calls { // Compile the task t, err := e.CompiledTask(call) if err != nil { return err } // Get the fingerprinting method to use method := e.Taskfile.Method if t.Method != "" { method = t.Method } // Check if the task is up-to-date isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t, fingerprint.WithMethod(method), fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithDry(e.Dry), fingerprint.WithLogger(e.Logger), ) if err != nil { return err } if !isUpToDate { return fmt.Errorf(`task: Task "%s" is not up-to-date`, t.Name()) } } return nil } func (e *Executor) statusOnError(t *ast.Task) error { method := t.Method if method == "" { method = e.Taskfile.Method } checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry) if err != nil { return err } return checker.OnError(t) } ================================================ FILE: task.go ================================================ package task import ( "context" "fmt" "os" "runtime" "slices" "strings" "sync/atomic" "golang.org/x/sync/errgroup" "mvdan.cc/sh/v3/interp" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/slicesext" "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) const ( // MaximumTaskCall is the max number of times a task can be called. // This exists to prevent infinite loops on cyclic dependencies MaximumTaskCall = 1000 ) // MatchingTask represents a task that matches a given call. It includes the // task itself and a list of wildcards that were matched. type MatchingTask struct { Task *ast.Task Wildcards []string } // Run runs Task func (e *Executor) Run(ctx context.Context, calls ...*Call) error { // check if given tasks exist for _, call := range calls { task, err := e.GetTask(call) if err != nil { if _, ok := err.(*errors.TaskNotFoundError); ok { if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil { return err } } return err } if task.Internal { if _, ok := err.(*errors.TaskNotFoundError); ok { if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil { return err } } return &errors.TaskInternalError{TaskName: call.Task} } } if e.Summary { for i, c := range calls { compiledTask, err := e.FastCompiledTask(c) if err != nil { return nil } summary.PrintSpaceBetweenSummaries(e.Logger, i) summary.PrintTask(e.Logger, compiledTask) } return nil } // Prompt for all required vars from deps upfront (parallel execution) if err := e.promptDepsVars(calls); err != nil { return err } regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...) if err != nil { return err } g := &errgroup.Group{} if e.Failfast { g, ctx = errgroup.WithContext(ctx) } for _, c := range regularCalls { if e.Parallel { g.Go(func() error { return e.RunTask(ctx, c) }) } else { if err := e.RunTask(ctx, c); err != nil { return err } } } if err := g.Wait(); err != nil { return err } if len(watchCalls) > 0 { return e.watchTasks(watchCalls...) } return nil } func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Call, watchCalls []*Call, err error) { for _, c := range calls { t, err := e.GetTask(c) if err != nil { return nil, nil, err } if e.Watch || t.Watch { watchCalls = append(watchCalls, c) } else { regularCalls = append(regularCalls, c) } } return regularCalls, watchCalls, err } // RunTask runs a task by its name func (e *Executor) RunTask(ctx context.Context, call *Call) error { // Inject prompted vars into call if available if e.promptedVars != nil { if call.Vars == nil { call.Vars = ast.NewVars() } for name, v := range e.promptedVars.All() { // Only inject if not already set in call if _, ok := call.Vars.Get(name); !ok { call.Vars.Set(name, v) } } } t, err := e.FastCompiledTask(call) if err != nil { return err } if !shouldRunOnCurrentPlatform(t.Platforms) { e.Logger.VerboseOutf(logger.Yellow, `task: %q not for current platform - ignored\n`, call.Task) return nil } // Check required vars early (before template compilation) if we can't prompt. // This gives a clear "missing required variables" error instead of a template error. if !e.canPrompt() { if err := e.areTaskRequiredVarsSet(t); err != nil { return err } } t, err = e.CompiledTask(call) if err != nil { return err } // Check if condition after CompiledTask so dynamic variables are resolved if strings.TrimSpace(t.If) != "" { if err := execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: t.If, Dir: t.Dir, Env: env.Get(t), }); err != nil { e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task) return nil } } // Prompt for missing required vars after if check (avoid prompting if task won't run) prompted, err := e.promptTaskVars(t, call) if err != nil { return err } if prompted { // Recompile with the new vars t, err = e.FastCompiledTask(call) if err != nil { return err } } if err := e.areTaskRequiredVarsSet(t); err != nil { return err } if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil { return err } if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { return &errors.TaskCalledTooManyTimesError{ TaskName: t.Task, MaximumTaskCall: MaximumTaskCall, } } release := e.acquireConcurrencyLimit() defer release() if err = e.startExecution(ctx, t, func(ctx context.Context) error { e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) if err := e.runDeps(ctx, t); err != nil { return err } skipFingerprinting := e.ForceAll || (!call.Indirect && e.Force) if !skipFingerprinting { if err := ctx.Err(); err != nil { return err } preCondMet, err := e.areTaskPreconditionsMet(ctx, t) if err != nil { return err } // Get the fingerprinting method to use method := e.Taskfile.Method if t.Method != "" { method = t.Method } upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, fingerprint.WithMethod(method), fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithDry(e.Dry), fingerprint.WithLogger(e.Logger), ) if err != nil { return err } if upToDate && preCondMet { if e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) { name := t.Name() if e.OutputStyle.Name == "prefixed" { name = t.Prefix } e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", name) } return nil } } for _, p := range t.Prompt { if p != "" && !e.Dry { if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) { return &errors.TaskCancelledNoTerminalError{TaskName: call.Task} } else if errors.Is(err, logger.ErrPromptCancelled) { return &errors.TaskCancelledByUserError{TaskName: call.Task} } else if err != nil { return err } } } if err := e.mkdir(t); err != nil { e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err) } var deferredExitCode uint8 for i := range t.Cmds { if t.Cmds[i].Defer { defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode) continue } if err := e.runCommand(ctx, t, call, i); err != nil { if err2 := e.statusOnError(t); err2 != nil { e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) } var exitCode interp.ExitStatus if errors.As(err, &exitCode) { if t.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) continue } deferredExitCode = uint8(exitCode) } return err } } e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task) return nil }); err != nil { return &errors.TaskRunError{TaskName: t.Name(), Err: err} } return nil } func (e *Executor) mkdir(t *ast.Task) error { if t.Dir == "" { return nil } mutex := e.mkdirMutexMap[t.Task] mutex.Lock() defer mutex.Unlock() if _, err := os.Stat(t.Dir); os.IsNotExist(err) { if err := os.MkdirAll(t.Dir, 0o755); err != nil { return err } } return nil } func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error { g := &errgroup.Group{} if e.Failfast || t.Failfast { g, ctx = errgroup.WithContext(ctx) } reacquire := e.releaseConcurrencyLimit() defer reacquire() for _, d := range t.Deps { g.Go(func() error { err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) if err != nil { return err } return nil }) } return g.Wait() } func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() cmd := t.Cmds[i] cache := &templater.Cache{Vars: vars} extra := map[string]any{} if deferredExitCode != nil && *deferredExitCode > 0 { extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode) } cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra) cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra) cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra) if err := e.runCommand(ctx, t, call, i); err != nil { e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error()) } } func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error { cmd := t.Cmds[i] // Check if condition for any command type if strings.TrimSpace(cmd.If) != "" { if err := execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: cmd.If, Dir: t.Dir, Env: env.Get(t), }); err != nil { e.Logger.VerboseOutf(logger.Yellow, "task: [%s] if condition not met - skipped\n", t.Name()) return nil } } switch { case cmd.Task != "": reacquire := e.releaseConcurrencyLimit() defer reacquire() err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) var exitCode interp.ExitStatus if errors.As(err, &exitCode) && cmd.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err) return nil } return err case cmd.Cmd != "": if !shouldRunOnCurrentPlatform(cmd.Platforms) { e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd) return nil } if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) { e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd) } if e.Dry { return nil } outputWrapper := e.Output if t.Interactive { outputWrapper = output.Interleaved{} } vars, err := e.Compiler.FastGetVariables(t, call) outputTemplater := &templater.Cache{Vars: vars} if err != nil { return fmt.Errorf("task: failed to get variables: %w", err) } stdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater) err = execext.RunCommand(ctx, &execext.RunCommandOptions{ Command: cmd.Cmd, Dir: t.Dir, Env: env.Get(t), PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set), BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt), Stdin: e.Stdin, Stdout: stdOut, Stderr: stdErr, }) if closeErr := closer(err); closeErr != nil { e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) } var exitCode interp.ExitStatus if errors.As(err, &exitCode) && cmd.IgnoreError { e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err) return nil } return err default: return nil } } func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func(ctx context.Context) error) error { h, err := e.GetHash(t) if err != nil { return err } if h == "" || t.Watch { return execute(ctx) } e.executionHashesMutex.Lock() if otherExecutionCtx, ok := e.executionHashes[h]; ok { e.executionHashesMutex.Unlock() e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s\n", h) // Release our execution slot to avoid blocking other tasks while we wait reacquire := e.releaseConcurrencyLimit() defer reacquire() <-otherExecutionCtx.Done() return nil } ctx, cancel := context.WithCancel(ctx) defer cancel() e.executionHashes[h] = ctx e.executionHashesMutex.Unlock() return execute(ctx) } // FindMatchingTasks returns a list of tasks that match the given call. A task // matches a call if its name is equal to the call's task name, or one of aliases, or if it matches // a wildcard pattern. The function returns a list of MatchingTask structs, each // containing a task and a list of wildcards that were matched. // If multiple tasks match due to aliases, a TaskNameConflictError is returned. func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) { if call == nil { return nil, nil } var matchingTasks []*MatchingTask // If there is a direct match, return it if task, ok := e.Taskfile.Tasks.Get(call.Task); ok { matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil}) return matchingTasks, nil } var aliasedTasks []string for task := range e.Taskfile.Tasks.Values(nil) { if slices.Contains(task.Aliases, call.Task) { aliasedTasks = append(aliasedTasks, task.Task) matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil}) } } if len(aliasedTasks) == 1 { return matchingTasks, nil } // If we found multiple tasks if len(aliasedTasks) > 1 { return nil, &errors.TaskNameConflictError{ Call: call.Task, TaskNames: aliasedTasks, } } // Attempt a wildcard match for _, value := range e.Taskfile.Tasks.All(nil) { if match, wildcards := value.WildcardMatch(call.Task); match { matchingTasks = append(matchingTasks, &MatchingTask{ Task: value, Wildcards: wildcards, }) } } return matchingTasks, nil } // GetTask will return the task with the name matching the given call from the taskfile. // If no task is found, it will search for tasks with a matching alias. // If multiple tasks contain the same alias or no matches are found an error is returned. func (e *Executor) GetTask(call *Call) (*ast.Task, error) { // Search for a matching task matchingTasks, err := e.FindMatchingTasks(call) if err != nil { return nil, err } if len(matchingTasks) > 0 { if call.Vars == nil { call.Vars = ast.NewVars() } call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards}) return matchingTasks[0].Task, nil } // If we found no tasks didYouMean := "" if !e.DisableFuzzy { e.fuzzyModelOnce.Do(e.setupFuzzyModel) if e.fuzzyModel != nil { didYouMean = e.fuzzyModel.SpellCheck(call.Task) } } return nil, &errors.TaskNotFoundError{ TaskName: call.Task, DidYouMean: didYouMean, } } type FilterFunc func(task *ast.Task) bool func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) { tasks := make([]*ast.Task, 0, e.Taskfile.Tasks.Len()) // Create an error group to wait for each task to be compiled var g errgroup.Group // Sort the tasks if e.TaskSorter == nil { e.TaskSorter = sort.AlphaNumericWithRootTasksFirst } // Filter tasks based on the given filter functions for task := range e.Taskfile.Tasks.Values(e.TaskSorter) { var shouldFilter bool for _, filter := range filters { if filter(task) { shouldFilter = true } } if !shouldFilter { tasks = append(tasks, task) } } // Compile the list of tasks for i := range tasks { g.Go(func() error { compiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task}) if err != nil { return err } tasks[i] = compiledTask return nil }) } // Wait for all the go routines to finish if err := g.Wait(); err != nil { return nil, err } return tasks, nil } // FilterOutNoDesc removes all tasks that do not contain a description. func FilterOutNoDesc(task *ast.Task) bool { return task.Desc == "" } // FilterOutInternal removes all tasks that are marked as internal. func FilterOutInternal(task *ast.Task) bool { return task.Internal } func shouldRunOnCurrentPlatform(platforms []*ast.Platform) bool { if len(platforms) == 0 { return true } for _, p := range platforms { if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) { return true } } return false } ================================================ FILE: task_test.go ================================================ package task_test import ( "bytes" "fmt" "io" "io/fs" "maps" rand "math/rand/v2" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "runtime" "slices" "sort" "strings" "sync" "testing" "time" "github.com/Masterminds/semver/v3" "github.com/sebdah/goldie/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/go-task/task/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile/ast" ) func init() { _ = os.Setenv("NO_COLOR", "1") } type ( TestOption interface { ExecutorTestOption FormatterTestOption } TaskTest struct { name string experiments map[*experiments.Experiment]int postProcessFns []PostProcessFn fixtureTemplateData map[string]any fixtureTemplatingEnabled bool } ) // goldenFileName makes the file path for fixture files safe for all well-known // operating systems. Windows in particular has a lot of restrictions the // characters that can be used in file paths. func goldenFileName(t *testing.T) string { t.Helper() name := t.Name() for _, c := range []string{` `, `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`} { name = strings.ReplaceAll(name, c, "-") } return name } // writeFixture writes a fixture file for the test. The fixture file is created // using the [goldie.Goldie] package. The fixture file is created with the // output of the task, after any post-process functions have been applied. func (tt *TaskTest) writeFixture( t *testing.T, g *goldie.Goldie, goldenFileSuffix string, b []byte, ) { t.Helper() // Apply any post-process functions for _, fn := range tt.postProcessFns { b = fn(t, b) } // Write the fixture file goldenFileName := goldenFileName(t) if goldenFileSuffix != "" { goldenFileName += "-" + goldenFileSuffix } // Create a set of data to be made available to every test fixture wd, err := os.Getwd() require.NoError(t, err) if tt.fixtureTemplatingEnabled { fixtureTemplateData := map[string]any{ "TEST_NAME": t.Name(), "TEST_DIR": wd, } // If the test has additional template data, copy it into the map if tt.fixtureTemplateData != nil { maps.Copy(fixtureTemplateData, tt.fixtureTemplateData) } g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b) } else { g.Assert(t, goldenFileName, b) } } // writeFixtureBuffer is a wrapper for writing the main output of the task to a // fixture file. func (tt *TaskTest) writeFixtureBuffer( t *testing.T, g *goldie.Goldie, buff bytes.Buffer, ) { t.Helper() tt.writeFixture(t, g, "", buff.Bytes()) } // writeFixtureErrSetup is a wrapper for writing the output of an error during // the setup phase of the task to a fixture file. func (tt *TaskTest) writeFixtureErrSetup( t *testing.T, g *goldie.Goldie, err error, ) { t.Helper() tt.writeFixture(t, g, "err-setup", []byte(err.Error())) } // Functional options // WithName gives the test fixture output a name. This should be used when // running multiple tests in a single test function. func WithName(name string) TestOption { return &nameTestOption{name: name} } type nameTestOption struct { name string } func (opt *nameTestOption) applyToExecutorTest(t *ExecutorTest) { t.name = opt.name } func (opt *nameTestOption) applyToFormatterTest(t *FormatterTest) { t.name = opt.name } // WithTask sets the name of the task to run. This should be used when the task // to run is not the default task. func WithTask(task string) TestOption { return &taskTestOption{task: task} } type taskTestOption struct { task string } func (opt *taskTestOption) applyToExecutorTest(t *ExecutorTest) { t.task = opt.task } func (opt *taskTestOption) applyToFormatterTest(t *FormatterTest) { t.task = opt.task } // WithVar sets a variable to be passed to the task. This can be called multiple // times to set more than one variable. func WithVar(key string, value any) TestOption { return &varTestOption{key: key, value: value} } type varTestOption struct { key string value any } func (opt *varTestOption) applyToExecutorTest(t *ExecutorTest) { t.vars[opt.key] = opt.value } func (opt *varTestOption) applyToFormatterTest(t *FormatterTest) { t.vars[opt.key] = opt.value } // WithExecutorOptions sets the [task.ExecutorOption]s to be used when creating // a [task.Executor]. func WithExecutorOptions(executorOpts ...task.ExecutorOption) TestOption { return &executorOptionsTestOption{executorOpts: executorOpts} } type executorOptionsTestOption struct { executorOpts []task.ExecutorOption } func (opt *executorOptionsTestOption) applyToExecutorTest(t *ExecutorTest) { t.executorOpts = slices.Concat(t.executorOpts, opt.executorOpts) } func (opt *executorOptionsTestOption) applyToFormatterTest(t *FormatterTest) { t.executorOpts = slices.Concat(t.executorOpts, opt.executorOpts) } // WithExperiment sets an experiment to be enabled for the test. This can be // called multiple times to enable more than one experiment. func WithExperiment(experiment *experiments.Experiment, value int) TestOption { return &experimentTestOption{experiment: experiment, value: value} } type experimentTestOption struct { experiment *experiments.Experiment value int } func (opt *experimentTestOption) applyToExecutorTest(t *ExecutorTest) { t.experiments[opt.experiment] = opt.value } func (opt *experimentTestOption) applyToFormatterTest(t *FormatterTest) { t.experiments[opt.experiment] = opt.value } // WithPostProcessFn adds a [PostProcessFn] function to the test. Post-process // functions are run on the output of the task before a fixture is created. This // can be used to remove absolute paths, sort lines, etc. This can be called // multiple times to add more than one post-process function. func WithPostProcessFn(fn PostProcessFn) TestOption { return &postProcessFnTestOption{fn: fn} } type postProcessFnTestOption struct { fn PostProcessFn } func (opt *postProcessFnTestOption) applyToExecutorTest(t *ExecutorTest) { t.postProcessFns = append(t.postProcessFns, opt.fn) } func (opt *postProcessFnTestOption) applyToFormatterTest(t *FormatterTest) { t.postProcessFns = append(t.postProcessFns, opt.fn) } // WithSetupError sets the test to expect an error during the setup phase of the // task execution. A fixture will be created with the output of any errors. func WithSetupError() TestOption { return &setupErrorTestOption{} } type setupErrorTestOption struct{} func (opt *setupErrorTestOption) applyToExecutorTest(t *ExecutorTest) { t.wantSetupError = true } func (opt *setupErrorTestOption) applyToFormatterTest(t *FormatterTest) { t.wantSetupError = true } // WithFixtureTemplating enables templating for the golden fixture files with // the default set of data. This is useful if the golden file is dynamic in some // way (e.g. contains user-specific directories). To add more data, see // WithFixtureTemplateData. func WithFixtureTemplating() TestOption { return &fixtureTemplatingTestOption{} } type fixtureTemplatingTestOption struct{} func (opt *fixtureTemplatingTestOption) applyToExecutorTest(t *ExecutorTest) { t.fixtureTemplatingEnabled = true } func (opt *fixtureTemplatingTestOption) applyToFormatterTest(t *FormatterTest) { t.fixtureTemplatingEnabled = true } // WithFixtureTemplateData adds data to the golden fixture file templates. Keys // given here will override any existing values. This option will also enable // global templating, so you do not need to call WithFixtureTemplating as well. func WithFixtureTemplateData(key string, value any) TestOption { return &fixtureTemplateDataTestOption{key, value} } type fixtureTemplateDataTestOption struct { k string v any } func (opt *fixtureTemplateDataTestOption) applyToExecutorTest(t *ExecutorTest) { t.fixtureTemplatingEnabled = true t.fixtureTemplateData[opt.k] = opt.v } func (opt *fixtureTemplateDataTestOption) applyToFormatterTest(t *FormatterTest) { t.fixtureTemplatingEnabled = true t.fixtureTemplateData[opt.k] = opt.v } // Post-processing // A PostProcessFn is a function that can be applied to the output of a test // fixture before the file is written. type PostProcessFn func(*testing.T, []byte) []byte // PPSortedLines sorts the lines of the output of the task. This is useful when // the order of the output is not important, but the output is expected to be // the same each time the task is run (e.g. when running tasks in parallel). func PPSortedLines(t *testing.T, b []byte) []byte { t.Helper() lines := strings.Split(strings.TrimSpace(string(b)), "\n") sort.Strings(lines) return []byte(strings.Join(lines, "\n") + "\n") } // SyncBuffer is a threadsafe buffer for testing. // Some times replace stdout/stderr with a buffer to capture output. // stdout and stderr are threadsafe, but a regular bytes.Buffer is not. // Using this instead helps prevents race conditions with output. type SyncBuffer struct { buf bytes.Buffer mu sync.Mutex } func (sb *SyncBuffer) Write(p []byte) (n int, err error) { sb.mu.Lock() defer sb.mu.Unlock() return sb.buf.Write(p) } // fileContentTest provides a basic reusable test-case for running a Taskfile // and inspect generated files. type fileContentTest struct { Dir string Entrypoint string Target string TrimSpace bool Files map[string]string } func (fct fileContentTest) name(file string) string { return fmt.Sprintf("target=%q,file=%q", fct.Target, file) } func (fct fileContentTest) Run(t *testing.T) { t.Helper() for f := range fct.Files { _ = os.Remove(filepathext.SmartJoin(fct.Dir, f)) } e := task.NewExecutor( task.WithDir(fct.Dir), task.WithTempDir(task.TempDir{ Remote: filepathext.SmartJoin(fct.Dir, ".task"), Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"), }), task.WithEntrypoint(fct.Entrypoint), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) require.NoError(t, e.Setup(), "e.Setup()") require.NoError(t, e.Run(t.Context(), &task.Call{Task: fct.Target}), "e.Run(target)") for name, expectContent := range fct.Files { t.Run(fct.name(name), func(t *testing.T) { path := filepathext.SmartJoin(e.Dir, name) b, err := os.ReadFile(path) require.NoError(t, err, "Error reading file") s := string(b) if fct.TrimSpace { s = strings.TrimSpace(s) } assert.Equal(t, expectContent, s, "unexpected file content in %s", path) }) } } func TestGenerates(t *testing.T) { t.Parallel() const dir = "testdata/generates" const ( srcTask = "sub/src.txt" relTask = "rel.txt" absTask = "abs.txt" fileWithSpaces = "my text file.txt" ) srcFile := filepathext.SmartJoin(dir, srcTask) for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} { path := filepathext.SmartJoin(dir, task) _ = os.Remove(path) if _, err := os.Stat(path); err == nil { t.Errorf("File should not exist: %v", err) } } buff := bytes.NewBuffer(nil) e := task.NewExecutor( task.WithDir(dir), task.WithStdout(buff), task.WithStderr(buff), ) require.NoError(t, e.Setup()) for _, theTask := range []string{relTask, absTask, fileWithSpaces} { destFile := filepathext.SmartJoin(dir, theTask) upToDate := fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) + fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) // Run task for the first time. require.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask})) if _, err := os.Stat(srcFile); err != nil { t.Errorf("File should exist: %v", err) } if _, err := os.Stat(destFile); err != nil { t.Errorf("File should exist: %v", err) } // Ensure task was not incorrectly found to be up-to-date on first run. if buff.String() == upToDate { t.Errorf("Wrong output message: %s", buff.String()) } buff.Reset() // Re-run task to ensure it's now found to be up-to-date. require.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask})) if buff.String() != upToDate { t.Errorf("Wrong output message: %s", buff.String()) } buff.Reset() } } func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in parallel const dir = "testdata/checksum" tests := []struct { files []string task string }{ {[]string{"generated.txt", ".task/checksum/build"}, "build"}, {[]string{"generated-wildcard.txt", ".task/checksum/build-wildcard"}, "build-wildcard"}, {[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"}, } for _, test := range tests { // nolint:paralleltest // cannot run in parallel t.Run(test.task, func(t *testing.T) { for _, f := range test.files { _ = os.Remove(filepathext.SmartJoin(dir, f)) _, err := os.Stat(filepathext.SmartJoin(dir, f)) require.Error(t, err) } var buff bytes.Buffer tempDir := task.TempDir{ Remote: filepathext.SmartJoin(dir, ".task"), Fingerprint: filepathext.SmartJoin(dir, ".task"), } e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithTempDir(tempDir), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task})) for _, f := range test.files { _, err := os.Stat(filepathext.SmartJoin(dir, f)) require.NoError(t, err) } // Capture the modification time, so we can ensure the checksum file // is not regenerated when the hash hasn't changed. s, err := os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) require.NoError(t, err) time := s.ModTime() buff.Reset() require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task})) assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task)) require.NoError(t, err) assert.Equal(t, time, s.ModTime()) }) } } func TestStatusVariables(t *testing.T) { t.Parallel() const dir = "testdata/status_vars" _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithTempDir(task.TempDir{ Remote: filepathext.SmartJoin(dir, ".task"), Fingerprint: filepathext.SmartJoin(dir, ".task"), }), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(false), task.WithVerbose(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-checksum"})) assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") buff.Reset() require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-ts"})) inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) require.NoError(t, err) ts := fmt.Sprintf("%d", inf.ModTime().Unix()) tf := inf.ModTime().String() assert.Contains(t, buff.String(), ts) assert.Contains(t, buff.String(), tf) } func TestCmdsVariables(t *testing.T) { t.Parallel() const dir = "testdata/cmds_vars" _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithTempDir(task.TempDir{ Remote: filepathext.SmartJoin(dir, ".task"), Fingerprint: filepathext.SmartJoin(dir, ".task"), }), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(false), task.WithVerbose(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-checksum"})) assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a") buff.Reset() require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-ts"})) inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) require.NoError(t, err) ts := fmt.Sprintf("%d", inf.ModTime().Unix()) tf := inf.ModTime().String() assert.Contains(t, buff.String(), ts) assert.Contains(t, buff.String(), tf) } func TestCyclicDep(t *testing.T) { t.Parallel() const dir = "testdata/cyclic" e := task.NewExecutor( task.WithDir(dir), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "task-1"}) var taskCalledTooManyTimesError *errors.TaskCalledTooManyTimesError assert.ErrorAs(t, err, &taskCalledTooManyTimesError) } func TestTaskVersion(t *testing.T) { t.Parallel() tests := []struct { Dir string Version *semver.Version wantErr bool }{ {"testdata/version/v1", semver.MustParse("1"), true}, {"testdata/version/v2", semver.MustParse("2"), true}, {"testdata/version/v3", semver.MustParse("3"), false}, } for _, test := range tests { t.Run(test.Dir, func(t *testing.T) { t.Parallel() e := task.NewExecutor( task.WithDir(test.Dir), task.WithStdout(io.Discard), task.WithStderr(io.Discard), task.WithVersionCheck(true), ) err := e.Setup() if test.wantErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, test.Version, e.Taskfile.Version) assert.Equal(t, 2, e.Taskfile.Tasks.Len()) }) } } func TestTaskIgnoreErrors(t *testing.T) { t.Parallel() const dir = "testdata/ignore_errors" e := task.NewExecutor( task.WithDir(dir), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-should-pass"})) require.Error(t, e.Run(t.Context(), &task.Call{Task: "task-should-fail"})) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-pass"})) require.Error(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-fail"})) } func TestExpand(t *testing.T) { t.Parallel() const dir = "testdata/expand" home, err := os.UserHomeDir() if err != nil { t.Errorf("Couldn't get $HOME: %v", err) } var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "pwd"})) assert.Equal(t, home, strings.TrimSpace(buff.String())) } func TestDry(t *testing.T) { t.Parallel() const dir = "testdata/dry" file := filepathext.SmartJoin(dir, "file.txt") _ = os.Remove(file) var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithDry(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) assert.Equal(t, "task: [build] touch file.txt", strings.TrimSpace(buff.String())) if _, err := os.Stat(file); err == nil { t.Errorf("File should not exist %s", file) } } // TestDryChecksum tests if the checksum file is not being written to disk // if the dry mode is enabled. func TestDryChecksum(t *testing.T) { t.Parallel() const dir = "testdata/dry_checksum" checksumFile := filepathext.SmartJoin(dir, ".task/checksum/default") _ = os.Remove(checksumFile) e := task.NewExecutor( task.WithDir(dir), task.WithTempDir(task.TempDir{ Remote: filepathext.SmartJoin(dir, ".task"), Fingerprint: filepathext.SmartJoin(dir, ".task"), }), task.WithStdout(io.Discard), task.WithStderr(io.Discard), task.WithDry(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) _, err := os.Stat(checksumFile) require.Error(t, err, "checksum file should not exist") e.Dry = false require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) _, err = os.Stat(checksumFile) require.NoError(t, err, "checksum file should exist") } func TestIncludes(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes", Target: "default", TrimSpace: true, Files: map[string]string{ "main.txt": "main", "included_directory.txt": "included_directory", "included_directory_without_dir.txt": "included_directory_without_dir", "included_taskfile_without_dir.txt": "included_taskfile_without_dir", "./module2/included_directory_with_dir.txt": "included_directory_with_dir", "./module2/included_taskfile_with_dir.txt": "included_taskfile_with_dir", "os_include.txt": "os", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesMultiLevel(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_multi_level", Target: "default", TrimSpace: true, Files: map[string]string{ "called_one.txt": "one", "called_two.txt": "two", "called_three.txt": "three", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesRemote(t *testing.T) { enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) dir := "testdata/includes_remote" os.RemoveAll(filepath.Join(dir, ".task", "remote")) srv := httptest.NewServer(http.FileServer(http.Dir(dir))) defer srv.Close() tcs := []struct { firstRemote string secondRemote string }{ { firstRemote: srv.URL + "/first/Taskfile.yml", secondRemote: srv.URL + "/first/second/Taskfile.yml", }, { firstRemote: srv.URL + "/first/Taskfile.yml", secondRemote: "./second/Taskfile.yml", }, { firstRemote: srv.URL + "/first/", secondRemote: srv.URL + "/first/second/", }, } taskCalls := []*task.Call{ {Task: "first:write-file"}, {Task: "first:second:write-file"}, } for i, tc := range tcs { t.Run(fmt.Sprint(i), func(t *testing.T) { t.Setenv("FIRST_REMOTE_URL", tc.firstRemote) t.Setenv("SECOND_REMOTE_URL", tc.secondRemote) var buff SyncBuffer // Extract host from server URL for trust testing parsedURL, err := url.Parse(srv.URL) require.NoError(t, err) trustedHost := parsedURL.Host executors := []struct { name string executor *task.Executor }{ { name: "online, always download", executor: task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithTimeout(time.Minute), task.WithInsecure(true), task.WithStdout(&buff), task.WithStderr(&buff), task.WithVerbose(true), // Without caching task.WithAssumeYes(true), task.WithDownload(true), ), }, { name: "offline, use cache", executor: task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithTimeout(time.Minute), task.WithInsecure(true), task.WithStdout(&buff), task.WithStderr(&buff), task.WithVerbose(true), // With caching task.WithAssumeYes(false), task.WithDownload(false), task.WithOffline(true), ), }, { name: "with trusted hosts, no prompts", executor: task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithTimeout(time.Minute), task.WithInsecure(true), task.WithStdout(&buff), task.WithStderr(&buff), task.WithVerbose(true), // With trusted hosts task.WithTrustedHosts([]string{trustedHost}), task.WithDownload(true), ), }, } for _, e := range executors { t.Run(e.name, func(t *testing.T) { require.NoError(t, e.executor.Setup()) for k, taskCall := range taskCalls { t.Run(taskCall.Task, func(t *testing.T) { expectedContent := fmt.Sprint(rand.Int64()) t.Setenv("CONTENT", expectedContent) outputFile := fmt.Sprintf("%d.%d.txt", i, k) t.Setenv("OUTPUT_FILE", outputFile) path := filepath.Join(dir, outputFile) require.NoError(t, os.RemoveAll(path)) require.NoError(t, e.executor.Run(t.Context(), taskCall)) actualContent, err := os.ReadFile(path) require.NoError(t, err) assert.Equal(t, expectedContent, strings.TrimSpace(string(actualContent))) }) } }) } t.Log("\noutput:\n", buff.buf.String()) }) } } func TestIncludeCycle(t *testing.T) { t.Parallel() const dir = "testdata/includes_cycle" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) err := e.Setup() require.Error(t, err) assert.Contains(t, err.Error(), "task: include cycle detected between") } func TestIncludesIncorrect(t *testing.T) { t.Parallel() const dir = "testdata/includes_incorrect" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) err := e.Setup() require.Error(t, err) assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error()) } func TestIncludesEmptyMain(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_empty", Target: "included:default", TrimSpace: true, Files: map[string]string{ "file.txt": "default", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesHttp(t *testing.T) { enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) dir, err := filepath.Abs("testdata/includes_http") require.NoError(t, err) srv := httptest.NewServer(http.FileServer(http.Dir(dir))) defer srv.Close() t.Cleanup(func() { // This test fills the .task/remote directory with cache entries because the include URL // is different on every test due to the dynamic nature of the TCP port in srv.URL if err := os.RemoveAll(filepath.Join(dir, ".task")); err != nil { t.Logf("error cleaning up: %s", err) } }) taskfiles, err := fs.Glob(os.DirFS(dir), "root-taskfile-*.yml") require.NoError(t, err) remotes := []struct { name string root string }{ { name: "local", root: ".", }, { name: "http-remote", root: srv.URL, }, } for _, taskfile := range taskfiles { t.Run(taskfile, func(t *testing.T) { for _, remote := range remotes { t.Run(remote.name, func(t *testing.T) { t.Setenv("INCLUDE_ROOT", remote.root) entrypoint := filepath.Join(dir, taskfile) var buff SyncBuffer e := task.NewExecutor( task.WithEntrypoint(entrypoint), task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithInsecure(true), task.WithDownload(true), task.WithAssumeYes(true), task.WithStdout(&buff), task.WithStderr(&buff), task.WithVerbose(true), task.WithTimeout(time.Minute), ) require.NoError(t, e.Setup()) defer func() { t.Log("output:", buff.buf.String()) }() tcs := []struct { name, dir string }{ { name: "second-with-dir-1:third-with-dir-1:default", dir: filepath.Join(dir, "dir-1"), }, { name: "second-with-dir-1:third-with-dir-2:default", dir: filepath.Join(dir, "dir-2"), }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { t.Parallel() task, err := e.CompiledTask(&task.Call{Task: tc.name}) require.NoError(t, err) assert.Equal(t, tc.dir, task.Dir) }) } }) } }) } } func TestIncludesDependencies(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_deps", Target: "default", TrimSpace: true, Files: map[string]string{ "default.txt": "default", "called_dep.txt": "called_dep", "called_task.txt": "called_task", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesCallingRoot(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_call_root_task", Target: "included:call-root", TrimSpace: true, Files: map[string]string{ "root_task.txt": "root task", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesOptional(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_optional", Target: "default", TrimSpace: true, Files: map[string]string{ "called_dep.txt": "called_dep", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesOptionalImplicitFalse(t *testing.T) { t.Parallel() const dir = "testdata/includes_optional_implicit_false" wd, _ := os.Getwd() message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\"" expected := fmt.Sprintf(message, wd, dir) e := task.NewExecutor( task.WithDir(dir), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) err := e.Setup() require.Error(t, err) assert.Equal(t, expected, err.Error()) } func TestIncludesOptionalExplicitFalse(t *testing.T) { t.Parallel() const dir = "testdata/includes_optional_explicit_false" wd, _ := os.Getwd() message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\"" expected := fmt.Sprintf(message, wd, dir) e := task.NewExecutor( task.WithDir(dir), task.WithStdout(io.Discard), task.WithStderr(io.Discard), ) err := e.Setup() require.Error(t, err) assert.Equal(t, expected, err.Error()) } func TestIncludesFromCustomTaskfile(t *testing.T) { t.Parallel() tt := fileContentTest{ Entrypoint: "testdata/includes_yaml/Custom.ext", Dir: "testdata/includes_yaml", Target: "default", TrimSpace: true, Files: map[string]string{ "main.txt": "main", "included_with_yaml_extension.txt": "included_with_yaml_extension", "included_with_custom_file.txt": "included_with_custom_file", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesRelativePath(t *testing.T) { t.Parallel() const dir = "testdata/includes_rel_path" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "common:pwd"})) assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") buff.Reset() require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:common:pwd"})) assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") } func TestIncludesInternal(t *testing.T) { t.Parallel() const dir = "testdata/internal_task" tests := []struct { name string task string expectedErr bool expectedOutput string }{ {"included internal task via task", "task-1", false, "Hello, World!\n"}, {"included internal task via dep", "task-2", false, "Hello, World!\n"}, {"included internal direct", "included:task-3", true, "task: No tasks with description available. Try --list-all to list all tasks\n"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) if test.expectedErr { require.Error(t, err) } else { require.NoError(t, err) } assert.Equal(t, test.expectedOutput, buff.String()) }) } } func TestIncludesFlatten(t *testing.T) { t.Parallel() const dir = "testdata/includes_flatten" tests := []struct { name string taskfile string task string expectedErr bool expectedOutput string }{ {name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"}, {name: "included flatten with default", taskfile: "Taskfile.yml", task: "default", expectedOutput: "default from included flatten\n"}, {name: "included flatten can call entrypoint tasks", taskfile: "Taskfile.yml", task: "from_entrypoint", expectedOutput: "from entrypoint\n"}, {name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"}, {name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"}, {name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithEntrypoint(dir+"/"+test.taskfile), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) err := e.Setup() if test.expectedErr { assert.EqualError(t, err, test.expectedOutput) } else { require.NoError(t, err) _ = e.Run(t.Context(), &task.Call{Task: test.task}) assert.Equal(t, test.expectedOutput, buff.String()) } }) } } func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot run in parallel const dir = "testdata/includes_interpolation" tests := []struct { name string task string expectedErr bool expectedOutput string }{ {"include", "include", false, "include\n"}, {"include_with_env_variable", "include-with-env-variable", false, "include_with_env_variable\n"}, {"include_with_dir", "include-with-dir", false, "included\n"}, } t.Setenv("MODULE", "included") for _, test := range tests { // nolint:paralleltest // cannot run in parallel t.Run(test.name, func(t *testing.T) { var buff bytes.Buffer e := task.NewExecutor( task.WithDir(filepath.Join(dir, test.name)), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) if test.expectedErr { require.Error(t, err) } else { require.NoError(t, err) } assert.Equal(t, test.expectedOutput, buff.String()) }) } } func TestIncludesWithExclude(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/includes_with_excludes"), task.WithSilent(true), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "included:bar"}) require.NoError(t, err) assert.Equal(t, "bar\n", buff.String()) buff.Reset() err = e.Run(t.Context(), &task.Call{Task: "included:foo"}) require.Error(t, err) buff.Reset() err = e.Run(t.Context(), &task.Call{Task: "bar"}) require.Error(t, err) buff.Reset() err = e.Run(t.Context(), &task.Call{Task: "foo"}) require.NoError(t, err) assert.Equal(t, "foo\n", buff.String()) } func TestIncludedTaskfileVarMerging(t *testing.T) { t.Parallel() const dir = "testdata/included_taskfile_var_merging" tests := []struct { name string task string expectedOutput string }{ {"foo", "foo:pwd", "included_taskfile_var_merging/foo\n"}, {"bar", "bar:pwd", "included_taskfile_var_merging/bar\n"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) require.NoError(t, err) assert.Contains(t, buff.String(), test.expectedOutput) }) } } func TestInternalTask(t *testing.T) { t.Parallel() const dir = "testdata/internal_task" tests := []struct { name string task string expectedErr bool expectedOutput string }{ {"internal task via task", "task-1", false, "Hello, World!\n"}, {"internal task via dep", "task-2", false, "Hello, World!\n"}, {"internal direct", "task-3", true, ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) if test.expectedErr { require.Error(t, err) } else { require.NoError(t, err) } assert.Equal(t, test.expectedOutput, buff.String()) }) } } func TestIncludesShadowedDefault(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_shadowed_default", Target: "included", TrimSpace: true, Files: map[string]string{ "file.txt": "shadowed", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestIncludesUnshadowedDefault(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/includes_unshadowed_default", Target: "included", TrimSpace: true, Files: map[string]string{ "file.txt": "included", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestSupportedFileNames(t *testing.T) { t.Parallel() fileNames := []string{ "Taskfile.yml", "Taskfile.yaml", "Taskfile.dist.yml", "Taskfile.dist.yaml", } for _, fileName := range fileNames { t.Run(fileName, func(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: fmt.Sprintf("testdata/file_names/%s", fileName), Target: "default", TrimSpace: true, Files: map[string]string{ "output.txt": "hello", }, } tt.Run(t) }) } } func TestSummary(t *testing.T) { t.Parallel() const dir = "testdata/summary" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSummary(true), task.WithSilent(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-with-summary"}, &task.Call{Task: "other-task-with-summary"})) data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt")) require.NoError(t, err) expectedOutput := string(data) if runtime.GOOS == "windows" { expectedOutput = strings.ReplaceAll(expectedOutput, "\r\n", "\n") } assert.Equal(t, expectedOutput, buff.String()) } func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) { t.Parallel() const expected = "dir" const dir = "testdata/" + expected var out bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&out), task.WithStderr(&out), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"})) // got should be the "dir" part of "testdata/dir" got := strings.TrimSuffix(filepath.Base(out.String()), "\n") assert.Equal(t, expected, got, "Mismatch in the working directory") } func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) { t.Parallel() const expected = "exists" const dir = "testdata/dir/explicit_exists" var out bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&out), task.WithStderr(&out), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"})) got := strings.TrimSuffix(filepath.Base(out.String()), "\n") assert.Equal(t, expected, got, "Mismatch in the working directory") } func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) { t.Parallel() const expected = "createme" const dir = "testdata/dir/explicit_doesnt_exist/" const toBeCreated = dir + expected const target = "whereami" var out bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&out), task.WithStderr(&out), ) // Ensure that the directory to be created doesn't actually exist. _ = os.RemoveAll(toBeCreated) if _, err := os.Stat(toBeCreated); err == nil { t.Errorf("Directory should not exist: %v", err) } require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: target})) got := strings.TrimSuffix(filepath.Base(out.String()), "\n") assert.Equal(t, expected, got, "Mismatch in the working directory") // Clean-up after ourselves only if no error. _ = os.RemoveAll(toBeCreated) } func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) { t.Parallel() const expected = "created" const dir = "testdata/dir/dynamic_var_on_created_dir/" const toBeCreated = dir + expected const target = "default" var out bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&out), task.WithStderr(&out), ) // Ensure that the directory to be created doesn't actually exist. _ = os.RemoveAll(toBeCreated) if _, err := os.Stat(toBeCreated); err == nil { t.Errorf("Directory should not exist: %v", err) } require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: target})) got := strings.TrimSuffix(filepath.Base(out.String()), "\n") assert.Equal(t, expected, got, "Mismatch in the working directory") // Clean-up after ourselves only if no error. _ = os.RemoveAll(toBeCreated) } func TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dir/dynamic_var", Target: "default", TrimSpace: false, Files: map[string]string{ "subdirectory/from_root_taskfile.txt": "subdirectory\n", "subdirectory/from_included_taskfile.txt": "subdirectory\n", "subdirectory/from_included_taskfile_task.txt": "subdirectory\n", "subdirectory/from_interpolated_dir.txt": "subdirectory\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestDisplaysErrorOnVersion1Schema(t *testing.T) { t.Parallel() e := task.NewExecutor( task.WithDir("testdata/version/v1"), task.WithStdout(io.Discard), task.WithStderr(io.Discard), task.WithVersionCheck(true), ) err := e.Setup() require.Error(t, err) assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v1\/Taskfile\.yml\":\nSchema version \(1\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) } func TestDisplaysErrorOnVersion2Schema(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/version/v2"), task.WithStdout(io.Discard), task.WithStderr(&buff), task.WithVersionCheck(true), ) err := e.Setup() require.Error(t, err) assert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \".*testdata\/version\/v2\/Taskfile\.yml\":\nSchema version \(2\.0\.0\) no longer supported\. Please use v3 or above`), err.Error()) } func TestShortTaskNotation(t *testing.T) { t.Parallel() const dir = "testdata/short_task_notation" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String()) } func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv/default", Target: "default", TrimSpace: false, Files: map[string]string{ "include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/dotenv/error_included_envs"), task.WithSummary(true), task.WithStdout(&buff), task.WithStderr(&buff), ) err := e.Setup() require.Error(t, err) assert.Contains(t, err.Error(), "move the dotenv") } func TestDotenvShouldAllowMissingEnv(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv/missing_env", Target: "default", TrimSpace: false, Files: map[string]string{ "include.txt": "INCLUDE1='' INCLUDE2=''\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestDotenvHasLocalEnvInPath(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv/local_env_in_path", Target: "default", TrimSpace: false, Files: map[string]string{ "var.txt": "VAR='var_in_dot_env_1'\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestDotenvHasLocalVarInPath(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv/local_var_in_path", Target: "default", TrimSpace: false, Files: map[string]string{ "var.txt": "VAR='var_in_dot_env_3'\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestDotenvHasEnvVarInPath(t *testing.T) { // nolint:paralleltest // cannot run in parallel t.Setenv("ENV_VAR", "testing") tt := fileContentTest{ Dir: "testdata/dotenv/env_var_in_path", Target: "default", TrimSpace: false, Files: map[string]string{ "var.txt": "VAR='var_in_dot_env_2'\n", }, } tt.Run(t) } func TestTaskDotenvParseErrorMessage(t *testing.T) { t.Parallel() e := task.NewExecutor( task.WithDir("testdata/dotenv/parse_error"), ) path, _ := filepath.Abs(filepath.Join(e.Dir, ".env-with-error")) expected := fmt.Sprintf("error reading env file %s:", path) err := e.Setup() require.ErrorContains(t, err, expected) } func TestTaskDotenv(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv_task/default", Target: "dotenv", TrimSpace: true, Files: map[string]string{ "dotenv.txt": "foo", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestTaskDotenvFail(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv_task/default", Target: "no-dotenv", TrimSpace: true, Files: map[string]string{ "no-dotenv.txt": "global", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestTaskDotenvOverriddenByEnv(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv_task/default", Target: "dotenv-overridden-by-env", TrimSpace: true, Files: map[string]string{ "dotenv-overridden-by-env.txt": "overridden", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestTaskDotenvWithVarName(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/dotenv_task/default", Target: "dotenv-with-var-name", TrimSpace: true, Files: map[string]string{ "dotenv-with-var-name.txt": "foo", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestExitImmediately(t *testing.T) { t.Parallel() const dir = "testdata/exit_immediately" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) require.Error(t, e.Run(t.Context(), &task.Call{Task: "default"})) assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`) } func TestRunOnlyRunsJobsHashOnce(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/run", Target: "generate-hash", Files: map[string]string{ "hash.txt": "starting 1\n1\n2\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestRunOnlyRunsJobsHashOnceWithWildcard(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/run", Target: "deploy", Files: map[string]string{ "wildcard.txt": "Deploy infra\nDeploy js\nDeploy go\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestRunOnceSharedDeps(t *testing.T) { t.Parallel() const dir = "testdata/run_once_shared_deps" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithForceAll(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"})) rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`) matches := rx.FindAllStringSubmatch(buff.String(), -1) assert.Len(t, matches, 1) assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`) assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`) } func TestRunWhenChanged(t *testing.T) { t.Parallel() const dir = "testdata/run_when_changed" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithForceAll(true), task.WithSilent(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "start"})) expectedOutputOrder := strings.TrimSpace(` login server=fubar user=fubar login server=foo user=foo login server=bar user=bar `) assert.Contains(t, buff.String(), expectedOutputOrder) } func TestDeferredCmds(t *testing.T) { t.Parallel() const dir = "testdata/deferred" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) expectedOutputOrder := strings.TrimSpace(` task: [task-2] echo 'cmd ran' cmd ran task: [task-2] exit 1 task: [task-2] echo 'failing' && exit 2 failing echo ran task-1 ran successfully task: [task-1] echo 'task-1 ran successfully' task-1 ran successfully `) require.Error(t, e.Run(t.Context(), &task.Call{Task: "task-2"})) assert.Contains(t, buff.String(), expectedOutputOrder) buff.Reset() require.NoError(t, e.Run(t.Context(), &task.Call{Task: "parent"})) assert.Contains(t, buff.String(), "child task deferred value-from-parent") } func TestExitCodeZero(t *testing.T) { t.Parallel() const dir = "testdata/exit_code" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "exit-zero"})) assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String())) } func TestExitCodeOne(t *testing.T) { t.Parallel() const dir = "testdata/exit_code" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.Error(t, e.Run(t.Context(), &task.Call{Task: "exit-one"})) assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String())) } func TestIgnoreNilElements(t *testing.T) { t.Parallel() tests := []struct { name string dir string }{ {"nil cmd", "testdata/ignore_nil_elements/cmds"}, {"nil dep", "testdata/ignore_nil_elements/deps"}, {"nil include", "testdata/ignore_nil_elements/includes"}, {"nil precondition", "testdata/ignore_nil_elements/preconditions"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(test.dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) assert.Equal(t, "string-slice-1\n", buff.String()) }) } } func TestOutputGroup(t *testing.T) { t.Parallel() const dir = "testdata/output_group" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) expectedOutputOrder := strings.TrimSpace(` task: [hello] echo 'Hello!' ::group::hello Hello! ::endgroup:: task: [bye] echo 'Bye!' ::group::bye Bye! ::endgroup:: `) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "bye"})) t.Log(buff.String()) assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) } func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) { t.Parallel() const dir = "testdata/output_group_error_only" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "passing"})) t.Log(buff.String()) assert.Empty(t, buff.String()) } func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) { t.Parallel() const dir = "testdata/output_group_error_only" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.Error(t, e.Run(t.Context(), &task.Call{Task: "failing"})) t.Log(buff.String()) assert.Contains(t, "failing-output", strings.TrimSpace(buff.String())) assert.NotContains(t, "passing", strings.TrimSpace(buff.String())) } func TestIncludedVars(t *testing.T) { t.Parallel() const dir = "testdata/include_with_vars" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) expectedOutputOrder := strings.TrimSpace(` task: [included1:task1] echo "VAR_1 is included1-var1" VAR_1 is included1-var1 task: [included1:task1] echo "VAR_2 is included-default-var2" VAR_2 is included-default-var2 task: [included2:task1] echo "VAR_1 is included2-var1" VAR_1 is included2-var1 task: [included2:task1] echo "VAR_2 is included-default-var2" VAR_2 is included-default-var2 task: [included3:task1] echo "VAR_1 is included-default-var1" VAR_1 is included-default-var1 task: [included3:task1] echo "VAR_2 is included-default-var2" VAR_2 is included-default-var2 `) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task1"})) t.Log(buff.String()) assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) } func TestIncludeWithVarsInInclude(t *testing.T) { t.Parallel() const dir = "testdata/include_with_vars_inside_include" var buff bytes.Buffer e := task.Executor{ Dir: dir, Stdout: &buff, Stderr: &buff, } require.NoError(t, e.Setup()) } func TestIncludedVarsMultiLevel(t *testing.T) { t.Parallel() const dir = "testdata/include_with_vars_multi_level" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) expectedOutputOrder := strings.TrimSpace(` task: [lib:greet] echo 'Hello world' Hello world task: [foo:lib:greet] echo 'Hello foo' Hello foo task: [bar:lib:greet] echo 'Hello bar' Hello bar `) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) t.Log(buff.String()) assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String())) } func TestErrorCode(t *testing.T) { t.Parallel() const dir = "testdata/error_code" tests := []struct { name string task string expected int }{ { name: "direct task", task: "direct", expected: 42, }, { name: "indirect task", task: "indirect", expected: 42, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) require.Error(t, err) taskRunErr, ok := err.(*errors.TaskRunError) assert.True(t, ok, "cannot cast returned error to *task.TaskRunError") assert.Equal(t, test.expected, taskRunErr.TaskExitCode(), "unexpected exit code from task") }) } } func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel const dir = "testdata/evaluate_symlinks_in_paths" var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(false), ) tests := []struct { name string task string expected string }{ { name: "default (1)", task: "default", expected: "task: [default] echo \"some job\"\nsome job", }, { name: "test-sym (1)", task: "test-sym", expected: "task: [test-sym] echo \"shared file source changed\" > src/shared/b", }, { name: "default (2)", task: "default", expected: "task: [default] echo \"some job\"\nsome job", }, { name: "default (3)", task: "default", expected: `task: Task "default" is up to date`, }, { name: "reset", task: "reset", expected: "task: [reset] echo \"shared file source\" > src/shared/b\ntask: [reset] echo \"file source\" > src/a", }, } for _, test := range tests { // nolint:paralleltest // cannot run in parallel t.Run(test.name, func(t *testing.T) { require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: test.task}) require.NoError(t, err) assert.Equal(t, test.expected, strings.TrimSpace(buff.String())) buff.Reset() }) } err := os.RemoveAll(dir + "/.task") require.NoError(t, err) } func TestTaskfileWalk(t *testing.T) { t.Parallel() tests := []struct { name string dir string expected string }{ { name: "walk from root directory", dir: "testdata/taskfile_walk", expected: "foo\n", }, { name: "walk from sub directory", dir: "testdata/taskfile_walk/foo", expected: "foo\n", }, { name: "walk from sub sub directory", dir: "testdata/taskfile_walk/foo/bar", expected: "foo\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir(test.dir), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) assert.Equal(t, test.expected, buff.String()) }) } } func TestUserWorkingDirectory(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/user_working_dir"), task.WithStdout(&buff), task.WithStderr(&buff), ) wd, err := os.Getwd() require.NoError(t, err) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"})) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) } func TestUserWorkingDirectoryWithIncluded(t *testing.T) { t.Parallel() wd, err := os.Getwd() require.NoError(t, err) wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir") var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/user_working_dir_with_includes"), task.WithStdout(&buff), task.WithStderr(&buff), ) e.UserWorkingDir = wd require.NoError(t, err) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:echo"})) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) } func TestPlatforms(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/platforms"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-" + runtime.GOOS})) assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String()) } func TestPOSIXShellOptsGlobalLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/global_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "pipefail"}) require.NoError(t, err) assert.Equal(t, "pipefail\ton\n", buff.String()) } func TestPOSIXShellOptsTaskLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/task_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "pipefail"}) require.NoError(t, err) assert.Equal(t, "pipefail\ton\n", buff.String()) } func TestPOSIXShellOptsCommandLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/command_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "pipefail"}) require.NoError(t, err) assert.Equal(t, "pipefail\ton\n", buff.String()) } func TestBashShellOptsGlobalLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/global_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "globstar"}) require.NoError(t, err) assert.Equal(t, "globstar\ton\n", buff.String()) } func TestBashShellOptsTaskLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/task_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "globstar"}) require.NoError(t, err) assert.Equal(t, "globstar\ton\n", buff.String()) } func TestBashShellOptsCommandLevel(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/shopts/command_level"), task.WithStdout(&buff), task.WithStderr(&buff), ) require.NoError(t, e.Setup()) err := e.Run(t.Context(), &task.Call{Task: "globstar"}) require.NoError(t, err) assert.Equal(t, "globstar\ton\n", buff.String()) } func TestSplitArgs(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/split_args"), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), ) require.NoError(t, e.Setup()) vars := ast.NewVars() vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"}) err := e.Run(t.Context(), &task.Call{Task: "default", Vars: vars}) require.NoError(t, err) assert.Equal(t, "3\n", buff.String()) } func TestSingleCmdDep(t *testing.T) { t.Parallel() tt := fileContentTest{ Dir: "testdata/single_cmd_dep", Target: "foo", Files: map[string]string{ "foo.txt": "foo\n", "bar.txt": "bar\n", }, } t.Run("", func(t *testing.T) { t.Parallel() tt.Run(t) }) } func TestSilence(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/silent"), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(false), ) require.NoError(t, e.Setup()) // First verify that the silent flag is in place. fetchedTask, err := e.GetTask(&task.Call{Task: "task-test-silent-calls-chatty-silenced"}) require.NoError(t, err, "Unable to look up task task-test-silent-calls-chatty-silenced") require.True(t, fetchedTask.Cmds[0].Silent, "The task task-test-silent-calls-chatty-silenced should have a silent call to chatty") // Then test the two basic cases where the task is silent or not. // A silenced task. err = e.Run(t.Context(), &task.Call{Task: "silent"}) require.NoError(t, err) require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent") buff.Reset() // A chatty (not silent) task. err = e.Run(t.Context(), &task.Call{Task: "chatty"}) require.NoError(t, err) require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent") buff.Reset() // Then test invoking the two task from other tasks. // A silenced task that calls a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-silent-calls-chatty-non-silenced"}) require.NoError(t, err) require.NotEmpty(t, buff.String(), "While running task-test-silent-calls-chatty-non-silenced: Expected to see output. The task is silenced, but the called task is not. Silence does not propagate to called tasks.") buff.Reset() // A silent task that does a silent call to a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-silent-calls-chatty-silenced"}) require.NoError(t, err) require.Empty(t, buff.String(), "While running task-test-silent-calls-chatty-silenced: Expected not to see output. The task calls chatty task, but the call is silenced.") buff.Reset() // A chatty task that does a call to a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-chatty-non-silenced"}) require.NoError(t, err) require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-non-silenced: Expected to see output. Both caller and callee are chatty and not silenced.") buff.Reset() // A chatty task that does a silenced call to a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-chatty-silenced"}) require.NoError(t, err) require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-silenced: Expected to see output. Call to a chatty task is silenced, but the parent task is not.") buff.Reset() // A chatty task with no cmd's of its own that does a silenced call to a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-no-cmds-calls-chatty-silenced"}) require.NoError(t, err) require.Empty(t, buff.String(), "While running task-test-no-cmds-calls-chatty-silenced: Expected not to see output. While the task itself is not silenced, it does not have any cmds and only does an invocation of a silenced task.") buff.Reset() // A chatty task that does a silenced invocation of a task. err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-silenced-cmd"}) require.NoError(t, err) require.Empty(t, buff.String(), "While running task-test-chatty-calls-silenced-cmd: Expected not to see output. While the task itself is not silenced, its call to the chatty task is silent.") buff.Reset() // Then test calls via dependencies. // A silent task that depends on a chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-non-silenced"}) require.NoError(t, err) require.NotEmpty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-non-silenced: Expected to see output. The task is silent and depends on a chatty task. Dependencies does not inherit silence.") buff.Reset() // A silent task that depends on a silenced chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-silenced"}) require.NoError(t, err) require.Empty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-silenced: Expected not to see output. The task is silent and has a silenced dependency on a chatty task.") buff.Reset() // A chatty task that, depends on a silenced chatty task. err = e.Run(t.Context(), &task.Call{Task: "task-test-is-chatty-depends-on-chatty-silenced"}) require.NoError(t, err) require.Empty(t, buff.String(), "While running task-test-is-chatty-depends-on-chatty-silenced: Expected not to see output. The task is chatty but does not have commands and has a silenced dependency on a chatty task.") buff.Reset() } func TestForce(t *testing.T) { t.Parallel() tests := []struct { name string env map[string]string force bool forceAll bool }{ { name: "force", force: true, }, { name: "force-all", forceAll: true, }, { name: "force with gentle force experiment", force: true, env: map[string]string{ "TASK_X_GENTLE_FORCE": "1", }, }, { name: "force-all with gentle force experiment", forceAll: true, env: map[string]string{ "TASK_X_GENTLE_FORCE": "1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/force"), task.WithStdout(&buff), task.WithStderr(&buff), task.WithForce(tt.force), task.WithForceAll(tt.forceAll), ) require.NoError(t, e.Setup()) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-with-dep"})) }) } } func TestWildcard(t *testing.T) { t.Parallel() tests := []struct { name string call string expectedOutput string wantErr bool }{ { name: "basic wildcard", call: "wildcard-foo", expectedOutput: "Hello foo\n", }, { name: "double wildcard", call: "foo-wildcard-bar", expectedOutput: "Hello foo bar\n", }, { name: "store wildcard", call: "start-foo", expectedOutput: "Starting foo\n", }, { name: "alias", call: "s-foo", expectedOutput: "Starting foo\n", }, { name: "matches exactly", call: "matches-exactly-*", expectedOutput: "I don't consume matches: []\n", }, { name: "no matches", call: "no-match", wantErr: true, }, { name: "multiple matches", call: "wildcard-foo-bar", expectedOutput: "Hello foo-bar\n", }, } for _, test := range tests { t.Run(test.call, func(t *testing.T) { t.Parallel() var buff bytes.Buffer e := task.NewExecutor( task.WithDir("testdata/wildcards"), task.WithStdout(&buff), task.WithStderr(&buff), task.WithSilent(true), task.WithForce(true), ) require.NoError(t, e.Setup()) if test.wantErr { require.Error(t, e.Run(t.Context(), &task.Call{Task: test.call})) return } require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.call})) assert.Equal(t, test.expectedOutput, buff.String()) }) } } // enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests, // with the experiment being restored to its previous state when tests complete. // // Typically experiments are controlled via TASK_X_ env vars, but we cannot use those in tests // because the experiment settings are parsed during experiments.init(), before any tests run. func enableExperimentForTest(t *testing.T, e *experiments.Experiment, val int) { t.Helper() prev := *e *e = experiments.Experiment{ Name: prev.Name, AllowedValues: []int{val}, Value: val, } t.Cleanup(func() { *e = prev }) } ================================================ FILE: taskfile/ast/cmd.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) // Cmd is a task command type Cmd struct { Cmd string Task string For *For If string Silent bool Set []string Shopt []string Vars *Vars IgnoreError bool Defer bool Platforms []*Platform } func (c *Cmd) DeepCopy() *Cmd { if c == nil { return nil } return &Cmd{ Cmd: c.Cmd, Task: c.Task, For: c.For.DeepCopy(), If: c.If, Silent: c.Silent, Set: deepcopy.Slice(c.Set), Shopt: deepcopy.Slice(c.Shopt), Vars: c.Vars.DeepCopy(), IgnoreError: c.IgnoreError, Defer: c.Defer, Platforms: deepcopy.Slice(c.Platforms), } } func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { return errors.NewTaskfileDecodeError(err, node) } c.Cmd = cmd return nil case yaml.MappingNode: var cmdStruct struct { Cmd string Task string For *For If string Silent bool Set []string Shopt []string Vars *Vars IgnoreError bool `yaml:"ignore_error"` Defer *Defer Platforms []*Platform } if err := node.Decode(&cmdStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) } if cmdStruct.Defer != nil { // A deferred command if cmdStruct.Defer.Cmd != "" { c.Defer = true c.Cmd = cmdStruct.Defer.Cmd c.Silent = cmdStruct.Silent return nil } // A deferred task call if cmdStruct.Defer.Task != "" { c.Defer = true c.Task = cmdStruct.Defer.Task c.Vars = cmdStruct.Defer.Vars c.Silent = cmdStruct.Defer.Silent return nil } return nil } // A task call if cmdStruct.Task != "" { c.Task = cmdStruct.Task c.Vars = cmdStruct.Vars c.For = cmdStruct.For c.If = cmdStruct.If c.Silent = cmdStruct.Silent c.IgnoreError = cmdStruct.IgnoreError return nil } // A command with additional options if cmdStruct.Cmd != "" { c.Cmd = cmdStruct.Cmd c.For = cmdStruct.For c.If = cmdStruct.If c.Silent = cmdStruct.Silent c.Set = cmdStruct.Set c.Shopt = cmdStruct.Shopt c.IgnoreError = cmdStruct.IgnoreError c.Platforms = cmdStruct.Platforms return nil } return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command") } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command") } ================================================ FILE: taskfile/ast/defer.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) type Defer struct { Cmd string Task string Vars *Vars Silent bool } func (d *Defer) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { return errors.NewTaskfileDecodeError(err, node) } d.Cmd = cmd return nil case yaml.MappingNode: var deferStruct struct { Defer string Task string Vars *Vars Silent bool } if err := node.Decode(&deferStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) } d.Cmd = deferStruct.Defer d.Task = deferStruct.Task d.Vars = deferStruct.Vars d.Silent = deferStruct.Silent return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("defer") } ================================================ FILE: taskfile/ast/dep.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) // Dep is a task dependency type Dep struct { Task string For *For Vars *Vars Silent bool } func (d *Dep) DeepCopy() *Dep { if d == nil { return nil } return &Dep{ Task: d.Task, For: d.For.DeepCopy(), Vars: d.Vars.DeepCopy(), Silent: d.Silent, } } func (d *Dep) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var task string if err := node.Decode(&task); err != nil { return errors.NewTaskfileDecodeError(err, node) } d.Task = task return nil case yaml.MappingNode: var taskCall struct { Task string For *For Vars *Vars Silent bool } if err := node.Decode(&taskCall); err != nil { return errors.NewTaskfileDecodeError(err, node) } d.Task = taskCall.Task d.For = taskCall.For d.Vars = taskCall.Vars d.Silent = taskCall.Silent return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("dependency") } ================================================ FILE: taskfile/ast/for.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) type For struct { From string List []any Matrix *Matrix Var string Split string As string } func (f *For) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var from string if err := node.Decode(&from); err != nil { return errors.NewTaskfileDecodeError(err, node) } f.From = from return nil case yaml.SequenceNode: var list []any if err := node.Decode(&list); err != nil { return errors.NewTaskfileDecodeError(err, node) } f.List = list return nil case yaml.MappingNode: var forStruct struct { Matrix *Matrix Var string Split string As string } if err := node.Decode(&forStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) } if forStruct.Var == "" && forStruct.Matrix.Len() == 0 { return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for") } if forStruct.Var != "" && forStruct.Matrix.Len() != 0 { return errors.NewTaskfileDecodeError(nil, node).WithMessage("cannot use both var and matrix in for") } f.Matrix = forStruct.Matrix f.Var = forStruct.Var f.Split = forStruct.Split f.As = forStruct.As return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("for") } func (f *For) DeepCopy() *For { if f == nil { return nil } return &For{ From: f.From, List: deepcopy.Slice(f.List), Matrix: f.Matrix.DeepCopy(), Var: f.Var, Split: f.Split, As: f.As, } } ================================================ FILE: taskfile/ast/glob.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) type Glob struct { Glob string Negate bool } func (g *Glob) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: g.Glob = node.Value return nil case yaml.MappingNode: var glob struct { Exclude string } if err := node.Decode(&glob); err != nil { return errors.NewTaskfileDecodeError(err, node) } g.Glob = glob.Exclude g.Negate = true return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("glob") } ================================================ FILE: taskfile/ast/graph.go ================================================ package ast import ( "fmt" "os" "sync" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" "golang.org/x/sync/errgroup" ) type TaskfileGraph struct { sync.Mutex graph.Graph[string, *TaskfileVertex] } // A TaskfileVertex is a vertex on the Taskfile DAG. type TaskfileVertex struct { URI string Taskfile *Taskfile } func taskfileHash(vertex *TaskfileVertex) string { return vertex.URI } func NewTaskfileGraph() *TaskfileGraph { return &TaskfileGraph{ sync.Mutex{}, graph.New(taskfileHash, graph.Directed(), graph.PreventCycles(), graph.Rooted(), ), } } func (tfg *TaskfileGraph) Visualize(filename string) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() return draw.DOT(tfg.Graph, f) } func (tfg *TaskfileGraph) Merge() (*Taskfile, error) { hashes, err := graph.TopologicalSort(tfg.Graph) if err != nil { return nil, err } predecessorMap, err := tfg.PredecessorMap() if err != nil { return nil, err } // Loop over each vertex in reverse topological order except for the root vertex. // This gives us a loop over every included Taskfile in an order which is safe to merge. for i := len(hashes) - 1; i > 0; i-- { hash := hashes[i] // Get the included vertex includedVertex, err := tfg.Vertex(hash) if err != nil { return nil, err } // Create an error group to wait for all the included Taskfiles to be merged with all its parents var g errgroup.Group // Loop over edge that leads to a vertex that includes the current vertex for _, edge := range predecessorMap[hash] { // Start a goroutine to process each included Taskfile g.Go(func() error { // Get the base vertex vertex, err := tfg.Vertex(edge.Source) if err != nil { return err } // Get the merge options includes, ok := edge.Properties.Data.([]*Include) if !ok { return fmt.Errorf("task: Failed to get merge options") } // Merge the included Taskfiles into the parent Taskfile for _, include := range includes { if err := vertex.Taskfile.Merge( includedVertex.Taskfile, include, ); err != nil { return err } } return nil }) if err := g.Wait(); err != nil { return nil, err } } // Wait for all the go routines to finish if err := g.Wait(); err != nil { return nil, err } } // Get the root vertex rootVertex, err := tfg.Vertex(hashes[0]) if err != nil { return nil, err } return rootVertex.Taskfile, nil } ================================================ FILE: taskfile/ast/include.go ================================================ package ast import ( "iter" "sync" "github.com/elliotchance/orderedmap/v3" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) type ( // Include represents information about included taskfiles Include struct { Namespace string Taskfile string Dir string Optional bool Internal bool Aliases []string Excludes []string AdvancedImport bool Vars *Vars Flatten bool Checksum string } // Includes is an ordered map of namespaces to includes. Includes struct { om *orderedmap.OrderedMap[string, *Include] mutex sync.RWMutex } // An IncludeElement is a key-value pair that is used for initializing an // Includes structure. IncludeElement orderedmap.Element[string, *Include] ) // NewIncludes creates a new instance of Includes and initializes it with the // provided set of elements, if any. The elements are added in the order they // are passed. func NewIncludes(els ...*IncludeElement) *Includes { includes := &Includes{ om: orderedmap.NewOrderedMap[string, *Include](), } for _, el := range els { includes.Set(el.Key, el.Value) } return includes } // Len returns the number of includes in the Includes map. func (includes *Includes) Len() int { if includes == nil || includes.om == nil { return 0 } defer includes.mutex.RUnlock() includes.mutex.RLock() return includes.om.Len() } // Get returns the value the the include with the provided key and a boolean // that indicates if the value was found or not. If the value is not found, the // returned include is a zero value and the bool is false. func (includes *Includes) Get(key string) (*Include, bool) { if includes == nil || includes.om == nil { return &Include{}, false } defer includes.mutex.RUnlock() includes.mutex.RLock() return includes.om.Get(key) } // Set sets the value of the include with the provided key to the provided // value. If the include already exists, its value is updated. If the include // does not exist, it is created. func (includes *Includes) Set(key string, value *Include) bool { if includes == nil { includes = NewIncludes() } if includes.om == nil { includes.om = orderedmap.NewOrderedMap[string, *Include]() } defer includes.mutex.Unlock() includes.mutex.Lock() return includes.om.Set(key, value) } // All returns an iterator that loops over all task key-value pairs. // Range calls the provided function for each include in the map. The function // receives the include's key and value as arguments. If the function returns // an error, the iteration stops and the error is returned. func (includes *Includes) All() iter.Seq2[string, *Include] { if includes == nil || includes.om == nil { return func(yield func(string, *Include) bool) {} } return includes.om.AllFromFront() } // Keys returns an iterator that loops over all task keys. func (includes *Includes) Keys() iter.Seq[string] { if includes == nil || includes.om == nil { return func(yield func(string) bool) {} } return includes.om.Keys() } // Values returns an iterator that loops over all task values. func (includes *Includes) Values() iter.Seq[*Include] { if includes == nil || includes.om == nil { return func(yield func(*Include) bool) {} } return includes.om.Values() } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { if includes == nil || includes.om == nil { *includes = *NewIncludes() } switch node.Kind { case yaml.MappingNode: // NOTE: orderedmap does not have an unmarshaler, so we have to decode // the map manually. We increment over 2 values at a time and assign // them as a key-value pair. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] // Decode the value node into an Include struct var v Include if err := valueNode.Decode(&v); err != nil { return errors.NewTaskfileDecodeError(err, node) } // Set the include namespace v.Namespace = keyNode.Value // Add the include to the ordered map includes.Set(keyNode.Value, &v) } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes") } func (include *Include) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var str string if err := node.Decode(&str); err != nil { return errors.NewTaskfileDecodeError(err, node) } include.Taskfile = str return nil case yaml.MappingNode: var includedTaskfile struct { Taskfile string Dir string Optional bool Internal bool Flatten bool Aliases []string Excludes []string Vars *Vars Checksum string } if err := node.Decode(&includedTaskfile); err != nil { return errors.NewTaskfileDecodeError(err, node) } include.Taskfile = includedTaskfile.Taskfile include.Dir = includedTaskfile.Dir include.Optional = includedTaskfile.Optional include.Internal = includedTaskfile.Internal include.Aliases = includedTaskfile.Aliases include.Excludes = includedTaskfile.Excludes include.AdvancedImport = true include.Vars = includedTaskfile.Vars include.Flatten = includedTaskfile.Flatten include.Checksum = includedTaskfile.Checksum return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include") } // DeepCopy creates a new instance of IncludedTaskfile and copies // data by value from the source struct. func (include *Include) DeepCopy() *Include { if include == nil { return nil } return &Include{ Namespace: include.Namespace, Taskfile: include.Taskfile, Dir: include.Dir, Optional: include.Optional, Internal: include.Internal, Excludes: deepcopy.Slice(include.Excludes), AdvancedImport: include.AdvancedImport, Vars: include.Vars.DeepCopy(), Flatten: include.Flatten, Aliases: deepcopy.Slice(include.Aliases), Checksum: include.Checksum, } } ================================================ FILE: taskfile/ast/location.go ================================================ package ast type Location struct { Line int Column int Taskfile string } func (l *Location) DeepCopy() *Location { if l == nil { return nil } return &Location{ Line: l.Line, Column: l.Column, Taskfile: l.Taskfile, } } ================================================ FILE: taskfile/ast/matrix.go ================================================ package ast import ( "iter" "github.com/elliotchance/orderedmap/v3" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) type ( // Matrix is an ordered map of variable names to arrays of values. Matrix struct { om *orderedmap.OrderedMap[string, *MatrixRow] } // A MatrixElement is a key-value pair that is used for initializing a // Matrix structure. MatrixElement orderedmap.Element[string, *MatrixRow] // A MatrixRow list of values for a matrix key or a reference to another // variable. MatrixRow struct { Ref string Value []any } ) func NewMatrix(els ...*MatrixElement) *Matrix { matrix := &Matrix{ om: orderedmap.NewOrderedMap[string, *MatrixRow](), } for _, el := range els { matrix.Set(el.Key, el.Value) } return matrix } func (matrix *Matrix) Len() int { if matrix == nil || matrix.om == nil { return 0 } return matrix.om.Len() } func (matrix *Matrix) Get(key string) (*MatrixRow, bool) { if matrix == nil || matrix.om == nil { return nil, false } return matrix.om.Get(key) } func (matrix *Matrix) Set(key string, value *MatrixRow) bool { if matrix == nil { matrix = NewMatrix() } if matrix.om == nil { matrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]() } return matrix.om.Set(key, value) } // All returns an iterator that loops over all task key-value pairs. func (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] { if matrix == nil || matrix.om == nil { return func(yield func(string, *MatrixRow) bool) {} } return matrix.om.AllFromFront() } // Keys returns an iterator that loops over all task keys. func (matrix *Matrix) Keys() iter.Seq[string] { if matrix == nil || matrix.om == nil { return func(yield func(string) bool) {} } return matrix.om.Keys() } // Values returns an iterator that loops over all task values. func (matrix *Matrix) Values() iter.Seq[*MatrixRow] { if matrix == nil || matrix.om == nil { return func(yield func(*MatrixRow) bool) {} } return matrix.om.Values() } func (matrix *Matrix) DeepCopy() *Matrix { if matrix == nil { return nil } return &Matrix{ om: deepcopy.OrderedMap(matrix.om), } } func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: // NOTE: orderedmap does not have an unmarshaler, so we have to decode // the map manually. We increment over 2 values at a time and assign // them as a key-value pair. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] switch valueNode.Kind { case yaml.SequenceNode: // Decode the value node into a Matrix struct var v []any if err := valueNode.Decode(&v); err != nil { return errors.NewTaskfileDecodeError(err, node) } // Add the row to the ordered map matrix.Set(keyNode.Value, &MatrixRow{ Value: v, }) case yaml.MappingNode: // Decode the value node into a Matrix struct var refStruct struct { Ref string } if err := valueNode.Decode(&refStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) } // Add the reference to the ordered map matrix.Set(keyNode.Value, &MatrixRow{ Ref: refStruct.Ref, }) default: return errors.NewTaskfileDecodeError(nil, node).WithMessage("matrix values must be an array or a reference") } } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("matrix") } ================================================ FILE: taskfile/ast/output.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) // Output of the Task output type Output struct { // Name of the Output. Name string `yaml:"-"` // Group specific style Group OutputGroup } // IsSet returns true if and only if a custom output style is set. func (s *Output) IsSet() bool { return s.Name != "" } func (s *Output) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var name string if err := node.Decode(&name); err != nil { return errors.NewTaskfileDecodeError(err, node) } s.Name = name return nil case yaml.MappingNode: var tmp struct { Group *OutputGroup } if err := node.Decode(&tmp); err != nil { return errors.NewTaskfileDecodeError(err, node) } if tmp.Group == nil { return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`) } *s = Output{ Name: "group", Group: *tmp.Group, } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output") } // OutputGroup is the style options specific to the Group style. type OutputGroup struct { Begin, End string ErrorOnly bool `yaml:"error_only"` } // IsSet returns true if and only if a custom output style is set. func (g *OutputGroup) IsSet() bool { if g == nil { return false } return g.Begin != "" || g.End != "" } ================================================ FILE: taskfile/ast/platforms.go ================================================ package ast import ( "fmt" "strings" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/goext" ) // Platform represents GOOS and GOARCH values type Platform struct { OS string Arch string } func (p *Platform) DeepCopy() *Platform { if p == nil { return nil } return &Platform{ OS: p.OS, Arch: p.Arch, } } type ErrInvalidPlatform struct { Platform string } func (err *ErrInvalidPlatform) Error() string { return fmt.Sprintf(`invalid platform "%s"`, err.Platform) } // UnmarshalYAML implements yaml.Unmarshaler interface. func (p *Platform) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var platform string if err := node.Decode(&platform); err != nil { return errors.NewTaskfileDecodeError(err, node) } if err := p.parsePlatform(platform); err != nil { return errors.NewTaskfileDecodeError(err, node) } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("platform") } // parsePlatform takes a string representing an OS/Arch combination (or either on their own) // and parses it into the Platform struct. It returns an error if the input string is invalid. // Valid combinations for input: OS, Arch, OS/Arch func (p *Platform) parsePlatform(input string) error { splitValues := strings.Split(input, "/") if len(splitValues) > 2 { return &ErrInvalidPlatform{Platform: input} } if err := p.parseOsOrArch(splitValues[0]); err != nil { return &ErrInvalidPlatform{Platform: input} } if len(splitValues) == 2 { if err := p.parseArch(splitValues[1]); err != nil { return &ErrInvalidPlatform{Platform: input} } } return nil } // parseOsOrArch will check if the given input is a valid OS or Arch value. // If so, it will store it. If not, an error is returned func (p *Platform) parseOsOrArch(osOrArch string) error { if osOrArch == "" { return fmt.Errorf("task: Blank OS/Arch value provided") } if goext.IsKnownOS(osOrArch) { p.OS = osOrArch return nil } if goext.IsKnownArch(osOrArch) { p.Arch = osOrArch return nil } return fmt.Errorf("task: Invalid OS/Arch value provided (%s)", osOrArch) } func (p *Platform) parseArch(arch string) error { if arch == "" { return fmt.Errorf("task: Blank Arch value provided") } if p.Arch != "" { return fmt.Errorf("task: Multiple Arch values provided") } if goext.IsKnownArch(arch) { p.Arch = arch return nil } return fmt.Errorf("task: Invalid Arch value provided (%s)", arch) } ================================================ FILE: taskfile/ast/platforms_test.go ================================================ package ast import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPlatformParsing(t *testing.T) { t.Parallel() tests := []struct { Input string ExpectedOS string ExpectedArch string Error string }{ {Input: "windows", ExpectedOS: "windows", ExpectedArch: ""}, {Input: "linux", ExpectedOS: "linux", ExpectedArch: ""}, {Input: "darwin", ExpectedOS: "darwin", ExpectedArch: ""}, {Input: "386", ExpectedOS: "", ExpectedArch: "386"}, {Input: "amd64", ExpectedOS: "", ExpectedArch: "amd64"}, {Input: "arm64", ExpectedOS: "", ExpectedArch: "arm64"}, {Input: "windows/386", ExpectedOS: "windows", ExpectedArch: "386"}, {Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"}, {Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"}, {Input: "invalid", Error: `invalid platform "invalid"`}, {Input: "invalid/invalid", Error: `invalid platform "invalid/invalid"`}, {Input: "windows/invalid", Error: `invalid platform "windows/invalid"`}, {Input: "invalid/amd64", Error: `invalid platform "invalid/amd64"`}, } for _, test := range tests { t.Run(test.Input, func(t *testing.T) { t.Parallel() var p Platform err := p.parsePlatform(test.Input) if test.Error != "" { require.Error(t, err) assert.Equal(t, test.Error, err.Error()) } else { require.NoError(t, err) assert.Equal(t, test.ExpectedOS, p.OS) assert.Equal(t, test.ExpectedArch, p.Arch) } }) } } ================================================ FILE: taskfile/ast/precondition.go ================================================ package ast import ( "fmt" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) // Precondition represents a precondition necessary for a task to run type Precondition struct { Sh string Msg string } func (p *Precondition) DeepCopy() *Precondition { if p == nil { return nil } return &Precondition{ Sh: p.Sh, Msg: p.Msg, } } // UnmarshalYAML implements yaml.Unmarshaler interface. func (p *Precondition) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { return errors.NewTaskfileDecodeError(err, node) } p.Sh = cmd p.Msg = fmt.Sprintf("`%s` failed", cmd) return nil case yaml.MappingNode: var sh struct { Sh string Msg string } if err := node.Decode(&sh); err != nil { return errors.NewTaskfileDecodeError(err, node) } p.Sh = sh.Sh p.Msg = sh.Msg if p.Msg == "" { p.Msg = fmt.Sprintf("%s failed", sh.Sh) } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("precondition") } ================================================ FILE: taskfile/ast/precondition_test.go ================================================ package ast_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/taskfile/ast" ) func TestPreconditionParse(t *testing.T) { t.Parallel() tests := []struct { content string v any expected any }{ { "test -f foo.txt", &ast.Precondition{}, &ast.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed"}, }, { "sh: '[ 1 = 0 ]'", &ast.Precondition{}, &ast.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed"}, }, { ` sh: "[ 1 = 2 ]" msg: "1 is not 2" `, &ast.Precondition{}, &ast.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"}, }, { ` sh: "[ 1 = 2 ]" msg: "1 is not 2" `, &ast.Precondition{}, &ast.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"}, }, } for _, test := range tests { err := yaml.Unmarshal([]byte(test.content), test.v) require.NoError(t, err) assert.Equal(t, test.expected, test.v) } } ================================================ FILE: taskfile/ast/prompt.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) type Prompt []string func (p *Prompt) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var str string if err := node.Decode(&str); err != nil { return errors.NewTaskfileDecodeError(err, node) } *p = []string{str} return nil case yaml.SequenceNode: var list []string if err := node.Decode(&list); err != nil { return errors.NewTaskfileDecodeError(err, node) } *p = list return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("prompt") } ================================================ FILE: taskfile/ast/requires.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) // Requires represents a set of required variables necessary for a task to run type Requires struct { Vars []*VarsWithValidation } func (r *Requires) DeepCopy() *Requires { if r == nil { return nil } return &Requires{ Vars: deepcopy.Slice(r.Vars), } } type VarsWithValidation struct { Name string Enum []string } func (v *VarsWithValidation) DeepCopy() *VarsWithValidation { if v == nil { return nil } return &VarsWithValidation{ Name: v.Name, Enum: v.Enum, } } // UnmarshalYAML implements yaml.Unmarshaler interface. func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.ScalarNode: var cmd string if err := node.Decode(&cmd); err != nil { return errors.NewTaskfileDecodeError(err, node) } v.Name = cmd v.Enum = nil return nil case yaml.MappingNode: var vv struct { Name string Enum []string } if err := node.Decode(&vv); err != nil { return errors.NewTaskfileDecodeError(err, node) } v.Name = vv.Name v.Enum = vv.Enum return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires") } ================================================ FILE: taskfile/ast/task.go ================================================ package ast import ( "fmt" "regexp" "strings" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) // Task represents a task type Task struct { Task string `hash:"ignore"` Cmds []*Cmd Deps []*Dep Label string Desc string Prompt Prompt Summary string Requires *Requires Aliases []string Sources []*Glob Generates []*Glob Status []string Preconditions []*Precondition Dir string Set []string Shopt []string Vars *Vars Env *Vars Dotenv []string Silent *bool Interactive bool Internal bool Method string Prefix string `hash:"ignore"` IgnoreError bool Run string Platforms []*Platform If string Watch bool Location *Location Failfast bool // Populated during merging Namespace string `hash:"ignore"` IncludeVars *Vars IncludedTaskfileVars *Vars FullName string `hash:"ignore"` } func (t *Task) Name() string { if t.Label != "" { return t.Label } if t.FullName != "" { return t.FullName } return t.Task } func (t *Task) LocalName() string { name := t.FullName name = strings.TrimPrefix(name, t.Namespace) name = strings.TrimPrefix(name, ":") return name } // IsSilent returns true if the task has silent mode explicitly enabled. // Returns false if Silent is nil (not set) or explicitly set to false. func (t *Task) IsSilent() bool { return t.Silent != nil && *t.Silent } // WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values. func (t *Task) WildcardMatch(name string) (bool, []string) { names := append([]string{t.Task}, t.Aliases...) for _, taskName := range names { regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)")) regex := regexp.MustCompile(regexStr) wildcards := regex.FindStringSubmatch(name) if len(wildcards) == 0 { continue } // Remove the first match, which is the full string wildcards = wildcards[1:] wildcardCount := strings.Count(taskName, "*") if len(wildcards) != wildcardCount { continue } return true, wildcards } return false, nil } func (t *Task) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { // Shortcut syntax for a task with a single command case yaml.ScalarNode: var cmd Cmd if err := node.Decode(&cmd); err != nil { return errors.NewTaskfileDecodeError(err, node) } t.Cmds = append(t.Cmds, &cmd) return nil // Shortcut syntax for a simple task with a list of commands case yaml.SequenceNode: var cmds []*Cmd if err := node.Decode(&cmds); err != nil { return errors.NewTaskfileDecodeError(err, node) } t.Cmds = cmds return nil // Full task object case yaml.MappingNode: var task struct { Cmds []*Cmd Cmd *Cmd Deps []*Dep Label string Desc string Prompt Prompt Summary string Aliases []string Sources []*Glob Generates []*Glob Status []string Preconditions []*Precondition Dir string Set []string Shopt []string Vars *Vars Env *Vars Dotenv []string Silent *bool `yaml:"silent,omitempty"` Interactive bool Internal bool Method string Prefix string IgnoreError bool `yaml:"ignore_error"` Run string Platforms []*Platform If string Requires *Requires Watch bool Failfast bool } if err := node.Decode(&task); err != nil { return errors.NewTaskfileDecodeError(err, node) } if task.Cmd != nil { if task.Cmds != nil { return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds") } t.Cmds = []*Cmd{task.Cmd} } else { t.Cmds = task.Cmds } t.Deps = task.Deps t.Label = task.Label t.Desc = task.Desc t.Prompt = task.Prompt t.Summary = task.Summary t.Aliases = task.Aliases t.Sources = task.Sources t.Generates = task.Generates t.Status = task.Status t.Preconditions = task.Preconditions t.Dir = task.Dir t.Set = task.Set t.Shopt = task.Shopt t.Vars = task.Vars t.Env = task.Env t.Dotenv = task.Dotenv t.Silent = deepcopy.Scalar(task.Silent) t.Interactive = task.Interactive t.Internal = task.Internal t.Method = task.Method t.Prefix = task.Prefix t.IgnoreError = task.IgnoreError t.Run = task.Run t.Platforms = task.Platforms t.If = task.If t.Requires = task.Requires t.Watch = task.Watch t.Failfast = task.Failfast return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("task") } // DeepCopy creates a new instance of Task and copies // data by value from the source struct. func (t *Task) DeepCopy() *Task { if t == nil { return nil } c := &Task{ Task: t.Task, Cmds: deepcopy.Slice(t.Cmds), Deps: deepcopy.Slice(t.Deps), Label: t.Label, Desc: t.Desc, Prompt: t.Prompt, Summary: t.Summary, Aliases: deepcopy.Slice(t.Aliases), Sources: deepcopy.Slice(t.Sources), Generates: deepcopy.Slice(t.Generates), Status: deepcopy.Slice(t.Status), Preconditions: deepcopy.Slice(t.Preconditions), Dir: t.Dir, Set: deepcopy.Slice(t.Set), Shopt: deepcopy.Slice(t.Shopt), Vars: t.Vars.DeepCopy(), Env: t.Env.DeepCopy(), Dotenv: deepcopy.Slice(t.Dotenv), Silent: deepcopy.Scalar(t.Silent), Interactive: t.Interactive, Internal: t.Internal, Method: t.Method, Prefix: t.Prefix, IgnoreError: t.IgnoreError, Run: t.Run, IncludeVars: t.IncludeVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), Platforms: deepcopy.Slice(t.Platforms), If: t.If, Location: t.Location.DeepCopy(), Requires: t.Requires.DeepCopy(), Namespace: t.Namespace, FullName: t.FullName, Watch: t.Watch, Failfast: t.Failfast, } return c } ================================================ FILE: taskfile/ast/taskfile.go ================================================ package ast import ( "fmt" "time" "github.com/Masterminds/semver/v3" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) // NamespaceSeparator contains the character that separates namespaces const NamespaceSeparator = ":" var V3 = semver.MustParse("3") // ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") // Taskfile is the abstract syntax tree for a Taskfile type Taskfile struct { Location string Version *semver.Version Output Output Method string Includes *Includes Set []string Shopt []string Vars *Vars Env *Vars Tasks *Tasks Silent bool Dotenv []string Run string Interval time.Duration } // Merge merges the second Taskfile into the first func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { if !t1.Version.Equal(t2.Version) { return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) } if len(t2.Dotenv) > 0 { return ErrIncludedTaskfilesCantHaveDotenvs } if t2.Output.IsSet() { t1.Output = t2.Output } if t1.Includes == nil { t1.Includes = NewIncludes() } if t1.Vars == nil { t1.Vars = NewVars() } if t1.Env == nil { t1.Env = NewVars() } if t1.Tasks == nil { t1.Tasks = NewTasks() } if t2.Silent { for _, t := range t2.Tasks.All(nil) { if t.Silent == nil { v := true t.Silent = &v } } } t1.Vars.Merge(t2.Vars, include) t1.Env.Merge(t2.Env, include) return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) } func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: var taskfile struct { Version *semver.Version Output Output Method string Includes *Includes Set []string Shopt []string Vars *Vars Env *Vars Tasks *Tasks Silent bool Dotenv []string Run string Interval time.Duration } if err := node.Decode(&taskfile); err != nil { return errors.NewTaskfileDecodeError(err, node) } tf.Version = taskfile.Version tf.Output = taskfile.Output tf.Method = taskfile.Method tf.Includes = taskfile.Includes tf.Set = taskfile.Set tf.Shopt = taskfile.Shopt tf.Vars = taskfile.Vars tf.Env = taskfile.Env tf.Tasks = taskfile.Tasks tf.Silent = taskfile.Silent tf.Dotenv = taskfile.Dotenv tf.Run = taskfile.Run tf.Interval = taskfile.Interval if tf.Includes == nil { tf.Includes = NewIncludes() } if tf.Vars == nil { tf.Vars = NewVars() } if tf.Env == nil { tf.Env = NewVars() } if tf.Tasks == nil { tf.Tasks = NewTasks() } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile") } ================================================ FILE: taskfile/ast/taskfile_test.go ================================================ package ast_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/taskfile/ast" ) func TestCmdParse(t *testing.T) { t.Parallel() const ( yamlCmd = `echo "a string command"` yamlDep = `"task-name"` yamlTaskCall = ` task: another-task vars: PARAM1: VALUE1 PARAM2: VALUE2 ` yamlDeferredCall = `defer: { task: some_task, vars: { PARAM1: "var" } }` yamlDeferredCmd = `defer: echo 'test'` ) tests := []struct { content string v any expected any }{ { yamlCmd, &ast.Cmd{}, &ast.Cmd{Cmd: `echo "a string command"`}, }, { yamlTaskCall, &ast.Cmd{}, &ast.Cmd{ Task: "another-task", Vars: ast.NewVars( &ast.VarElement{ Key: "PARAM1", Value: ast.Var{ Value: "VALUE1", }, }, &ast.VarElement{ Key: "PARAM2", Value: ast.Var{ Value: "VALUE2", }, }, ), }, }, { yamlDeferredCmd, &ast.Cmd{}, &ast.Cmd{Cmd: "echo 'test'", Defer: true}, }, { yamlDeferredCall, &ast.Cmd{}, &ast.Cmd{ Task: "some_task", Vars: ast.NewVars( &ast.VarElement{ Key: "PARAM1", Value: ast.Var{ Value: "var", }, }, ), Defer: true, }, }, { yamlDep, &ast.Dep{}, &ast.Dep{Task: "task-name"}, }, { yamlTaskCall, &ast.Dep{}, &ast.Dep{ Task: "another-task", Vars: ast.NewVars( &ast.VarElement{ Key: "PARAM1", Value: ast.Var{ Value: "VALUE1", }, }, &ast.VarElement{ Key: "PARAM2", Value: ast.Var{ Value: "VALUE2", }, }, ), }, }, } for _, test := range tests { err := yaml.Unmarshal([]byte(test.content), test.v) require.NoError(t, err) assert.Equal(t, test.expected, test.v) } } ================================================ FILE: taskfile/ast/tasks.go ================================================ package ast import ( "fmt" "iter" "slices" "strings" "sync" "github.com/elliotchance/orderedmap/v3" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/sort" ) type ( // Tasks is an ordered map of task names to Tasks. Tasks struct { om *orderedmap.OrderedMap[string, *Task] mutex sync.RWMutex } // A TaskElement is a key-value pair that is used for initializing a Tasks // structure. TaskElement orderedmap.Element[string, *Task] ) // NewTasks creates a new instance of Tasks and initializes it with the provided // set of elements, if any. The elements are added in the order they are passed. func NewTasks(els ...*TaskElement) *Tasks { tasks := &Tasks{ om: orderedmap.NewOrderedMap[string, *Task](), } for _, el := range els { tasks.Set(el.Key, el.Value) } return tasks } // Len returns the number of variables in the Tasks map. func (tasks *Tasks) Len() int { if tasks == nil || tasks.om == nil { return 0 } defer tasks.mutex.RUnlock() tasks.mutex.RLock() return tasks.om.Len() } // Get returns the value the the task with the provided key and a boolean that // indicates if the value was found or not. If the value is not found, the // returned task is a zero value and the bool is false. func (tasks *Tasks) Get(key string) (*Task, bool) { if tasks == nil || tasks.om == nil { return &Task{}, false } defer tasks.mutex.RUnlock() tasks.mutex.RLock() return tasks.om.Get(key) } // Set sets the value of the task with the provided key to the provided value. // If the task already exists, its value is updated. If the task does not exist, // it is created. func (tasks *Tasks) Set(key string, value *Task) bool { if tasks == nil { tasks = NewTasks() } if tasks.om == nil { tasks.om = orderedmap.NewOrderedMap[string, *Task]() } defer tasks.mutex.Unlock() tasks.mutex.Lock() return tasks.om.Set(key, value) } // All returns an iterator that loops over all task key-value pairs in the order // specified by the sorter. func (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] { if t == nil || t.om == nil { return func(yield func(string, *Task) bool) {} } if sorter == nil { return t.om.AllFromFront() } return func(yield func(string, *Task) bool) { for _, key := range sorter(slices.Collect(t.om.Keys()), nil) { el := t.om.GetElement(key) if !yield(el.Key, el.Value) { return } } } } // Keys returns an iterator that loops over all task keys in the order specified // by the sorter. func (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] { return func(yield func(string) bool) { for k := range t.All(sorter) { if !yield(k) { return } } } } // Values returns an iterator that loops over all task values in the order // specified by the sorter. func (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] { return func(yield func(*Task) bool) { for _, v := range t.All(sorter) { if !yield(v) { return } } } } func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error { defer t2.mutex.RUnlock() t2.mutex.RLock() for name, v := range t2.All(nil) { // We do a deep copy of the task struct here to ensure that no data can // be changed elsewhere once the taskfile is merged. task := v.DeepCopy() // Set the task to internal if EITHER the included task or the included // taskfile are marked as internal task.Internal = task.Internal || (include != nil && include.Internal) taskName := name // if the task is in the exclude list, don't add it to the merged taskfile if slices.Contains(include.Excludes, name) { continue } if !include.Flatten { // Add namespaces to task dependencies for _, dep := range task.Deps { if dep != nil && dep.Task != "" { dep.Task = taskNameWithNamespace(dep.Task, include.Namespace) } } // Add namespaces to task commands for _, cmd := range task.Cmds { if cmd != nil && cmd.Task != "" { cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace) } } // Add namespaces to task aliases for i, alias := range task.Aliases { task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace) } // Add namespace aliases if include != nil { for _, namespaceAlias := range include.Aliases { task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias)) for _, alias := range v.Aliases { task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias)) } } } taskName = taskNameWithNamespace(name, include.Namespace) task.Namespace = include.Namespace task.Task = taskName } if include.AdvancedImport { task.Dir = filepathext.SmartJoin(include.Dir, task.Dir) if task.IncludeVars == nil { task.IncludeVars = NewVars() } task.IncludeVars.Merge(include.Vars, nil) task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy() } if _, ok := t1.Get(taskName); ok { return &errors.TaskNameFlattenConflictError{ TaskName: taskName, Include: include.Namespace, } } // Add the task to the merged taskfile t1.Set(taskName, task) } // If the included Taskfile has a default task, is not flattened and the // parent namespace has no task with a matching name, we can add an alias so // that the user can run the included Taskfile's default task without // specifying its full name. If the parent namespace has aliases, we add // another alias for each of them. _, t2DefaultExists := t2.Get("default") _, t1NamespaceExists := t1.Get(include.Namespace) if t2DefaultExists && !t1NamespaceExists && !include.Flatten { defaultTaskName := fmt.Sprintf("%s:default", include.Namespace) t1DefaultTask, ok := t1.Get(defaultTaskName) if ok { t1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace) t1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases) } } return nil } func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { if t == nil || t.om == nil { *t = *NewTasks() } switch node.Kind { case yaml.MappingNode: // NOTE: orderedmap does not have an unmarshaler, so we have to decode // the map manually. We increment over 2 values at a time and assign // them as a key-value pair. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] // Decode the value node into a Task struct var v Task if err := valueNode.Decode(&v); err != nil { return errors.NewTaskfileDecodeError(err, node) } // Set the task name and location v.Task = keyNode.Value v.Location = &Location{ Line: keyNode.Line, Column: keyNode.Column, } // Add the task to the ordered map t.Set(keyNode.Value, &v) } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks") } func taskNameWithNamespace(taskName string, namespace string) string { if after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok { return after } return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName) } ================================================ FILE: taskfile/ast/var.go ================================================ package ast import ( "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" ) // Var represents either a static or dynamic variable. type Var struct { Value any Live any Sh *string Ref string Dir string } func (v *Var) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: key := "" if len(node.Content) > 0 { key = node.Content[0].Value } switch key { case "sh", "ref", "map": var m struct { Sh *string Ref string Map any } if err := node.Decode(&m); err != nil { return errors.NewTaskfileDecodeError(err, node) } v.Sh = m.Sh v.Ref = m.Ref v.Value = m.Map return nil default: return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key) } default: var value any if err := node.Decode(&value); err != nil { return errors.NewTaskfileDecodeError(err, node) } v.Value = value return nil } } ================================================ FILE: taskfile/ast/vars.go ================================================ package ast import ( "iter" "sync" "github.com/elliotchance/orderedmap/v3" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" ) type ( // Vars is an ordered map of variable names to values. Vars struct { om *orderedmap.OrderedMap[string, Var] mutex sync.RWMutex } // A VarElement is a key-value pair that is used for initializing a Vars // structure. VarElement orderedmap.Element[string, Var] ) // NewVars creates a new instance of Vars and initializes it with the provided // set of elements, if any. The elements are added in the order they are passed. func NewVars(els ...*VarElement) *Vars { vars := &Vars{ om: orderedmap.NewOrderedMap[string, Var](), } for _, el := range els { vars.Set(el.Key, el.Value) } return vars } // Len returns the number of variables in the Vars map. func (vars *Vars) Len() int { if vars == nil || vars.om == nil { return 0 } defer vars.mutex.RUnlock() vars.mutex.RLock() return vars.om.Len() } // Get returns the value the the variable with the provided key and a boolean // that indicates if the value was found or not. If the value is not found, the // returned variable is a zero value and the bool is false. func (vars *Vars) Get(key string) (Var, bool) { if vars == nil || vars.om == nil { return Var{}, false } defer vars.mutex.RUnlock() vars.mutex.RLock() return vars.om.Get(key) } // Set sets the value of the variable with the provided key to the provided // value. If the variable already exists, its value is updated. If the variable // does not exist, it is created. func (vars *Vars) Set(key string, value Var) bool { if vars == nil { vars = NewVars() } if vars.om == nil { vars.om = orderedmap.NewOrderedMap[string, Var]() } defer vars.mutex.Unlock() vars.mutex.Lock() return vars.om.Set(key, value) } // All returns an iterator that loops over all task key-value pairs. func (vars *Vars) All() iter.Seq2[string, Var] { if vars == nil || vars.om == nil { return func(yield func(string, Var) bool) {} } return vars.om.AllFromFront() } // Keys returns an iterator that loops over all task keys. func (vars *Vars) Keys() iter.Seq[string] { if vars == nil || vars.om == nil { return func(yield func(string) bool) {} } return vars.om.Keys() } // Values returns an iterator that loops over all task values. func (vars *Vars) Values() iter.Seq[Var] { if vars == nil || vars.om == nil { return func(yield func(Var) bool) {} } return vars.om.Values() } // ToCacheMap converts Vars to an unordered map containing only the static // variables func (vars *Vars) ToCacheMap() (m map[string]any) { defer vars.mutex.RUnlock() vars.mutex.RLock() m = make(map[string]any, vars.Len()) for k, v := range vars.All() { if v.Sh != nil && *v.Sh != "" { // Dynamic variable is not yet resolved; trigger // to be used in templates. continue } if v.Live != nil { m[k] = v.Live } else { m[k] = v.Value } } return m } // Merge loops over other and merges it values with the variables in vars. If // the include parameter is not nil and its it is an advanced import, the // directory is set to the value of the include parameter. func (vars *Vars) Merge(other *Vars, include *Include) { if vars == nil || vars.om == nil || other == nil { return } defer other.mutex.RUnlock() other.mutex.RLock() for pair := other.om.Front(); pair != nil; pair = pair.Next() { if include != nil && include.AdvancedImport { pair.Value.Dir = include.Dir } vars.om.Set(pair.Key, pair.Value) } } // ReverseMerge merges other variables with the existing variables in vars, but // keeps the other variables first in order. If the include parameter is not // nil and it is an advanced import, the directory is set to the value of the // include parameter. func (vars *Vars) ReverseMerge(other *Vars, include *Include) { if vars == nil || vars.om == nil || other == nil || other.om == nil { return } newOM := orderedmap.NewOrderedMap[string, Var]() other.mutex.RLock() for pair := other.om.Front(); pair != nil; pair = pair.Next() { val := pair.Value if include != nil && include.AdvancedImport { val.Dir = include.Dir } newOM.Set(pair.Key, val) } other.mutex.RUnlock() vars.mutex.Lock() for pair := vars.om.Front(); pair != nil; pair = pair.Next() { newOM.Set(pair.Key, pair.Value) } vars.om = newOM vars.mutex.Unlock() } func (vs *Vars) DeepCopy() *Vars { if vs == nil { return nil } defer vs.mutex.RUnlock() vs.mutex.RLock() return &Vars{ om: deepcopy.OrderedMap(vs.om), } } func (vs *Vars) UnmarshalYAML(node *yaml.Node) error { if vs == nil || vs.om == nil { *vs = *NewVars() } vs.om = orderedmap.NewOrderedMap[string, Var]() switch node.Kind { case yaml.MappingNode: // NOTE: orderedmap does not have an unmarshaler, so we have to decode // the map manually. We increment over 2 values at a time and assign // them as a key-value pair. for i := 0; i < len(node.Content); i += 2 { keyNode := node.Content[i] valueNode := node.Content[i+1] // Decode the value node into a Task struct var v Var if err := valueNode.Decode(&v); err != nil { return errors.NewTaskfileDecodeError(err, node) } // Add the task to the ordered map vs.Set(keyNode.Value, v) } return nil } return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars") } ================================================ FILE: taskfile/dotenv.go ================================================ package taskfile import ( "fmt" "os" "github.com/joho/godotenv" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) func Dotenv(vars *ast.Vars, tf *ast.Taskfile, dir string) (*ast.Vars, error) { env := ast.NewVars() cache := &templater.Cache{Vars: vars} for _, dotEnvPath := range tf.Dotenv { dotEnvPath = templater.Replace(dotEnvPath, cache) if dotEnvPath == "" { continue } dotEnvPath = filepathext.SmartJoin(dir, dotEnvPath) if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { continue } envs, err := godotenv.Read(dotEnvPath) if err != nil { return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, err) } for key, value := range envs { if _, ok := env.Get(key); !ok { env.Set(key, ast.Var{Value: value}) } } } return env, nil } ================================================ FILE: taskfile/node.go ================================================ package taskfile import ( "context" "strings" "time" giturls "github.com/chainguard-dev/git-urls" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/internal/fsext" ) type Node interface { Read() ([]byte, error) Parent() Node Location() string Dir() string Checksum() string Verify(checksum string) bool ResolveEntrypoint(entrypoint string) (string, error) ResolveDir(dir string) (string, error) } type RemoteNode interface { Node ReadContext(ctx context.Context) ([]byte, error) CacheKey() string } func NewRootNode( entrypoint string, dir string, insecure bool, timeout time.Duration, opts ...NodeOption, ) (Node, error) { dir = fsext.DefaultDir(entrypoint, dir) // If the entrypoint is "-", we read from stdin if entrypoint == "-" { return NewStdinNode(dir) } return NewNode(entrypoint, dir, insecure, opts...) } func NewNode( entrypoint string, dir string, insecure bool, opts ...NodeOption, ) (Node, error) { var node Node var err error scheme, err := getScheme(entrypoint) if err != nil { return nil, err } switch scheme { case "git": node, err = NewGitNode(entrypoint, dir, insecure, opts...) case "http", "https": node, err = NewHTTPNode(entrypoint, dir, insecure, opts...) default: node, err = NewFileNode(entrypoint, dir, opts...) } if _, isRemote := node.(RemoteNode); isRemote && !experiments.RemoteTaskfiles.Enabled() { return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles") } return node, err } func isRemoteEntrypoint(entrypoint string) bool { scheme, _ := getScheme(entrypoint) switch scheme { case "git", "http", "https": return true default: return false } } func getScheme(uri string) (string, error) { u, err := giturls.Parse(uri) if u == nil { return "", err } if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") { return "git", nil } if i := strings.Index(uri, "://"); i != -1 { return uri[:i], nil } return "", nil } ================================================ FILE: taskfile/node_base.go ================================================ package taskfile type ( NodeOption func(*baseNode) // baseNode is a generic node that implements the Parent() methods of the // NodeReader interface. It does not implement the Read() method and it // designed to be embedded in other node types so that this boilerplate code // does not need to be repeated. baseNode struct { parent Node dir string checksum string caCert string cert string certKey string } ) func NewBaseNode(dir string, opts ...NodeOption) *baseNode { node := &baseNode{ parent: nil, dir: dir, } // Apply options for _, opt := range opts { opt(node) } return node } func WithParent(parent Node) NodeOption { return func(node *baseNode) { node.parent = parent } } func WithChecksum(checksum string) NodeOption { return func(node *baseNode) { node.checksum = checksum } } func (node *baseNode) Parent() Node { return node.parent } func (node *baseNode) Dir() string { return node.dir } func (node *baseNode) Checksum() string { return node.checksum } func (node *baseNode) Verify(checksum string) bool { return node.checksum == "" || node.checksum == checksum } func WithCACert(caCert string) NodeOption { return func(node *baseNode) { node.caCert = caCert } } func WithCert(cert string) NodeOption { return func(node *baseNode) { node.cert = cert } } func WithCertKey(certKey string) NodeOption { return func(node *baseNode) { node.certKey = certKey } } ================================================ FILE: taskfile/node_cache.go ================================================ package taskfile import ( "crypto/sha256" "fmt" "os" "path/filepath" "time" ) const remoteCacheDir = "remote" type CacheNode struct { *baseNode source RemoteNode } func NewCacheNode(source RemoteNode, dir string) *CacheNode { return &CacheNode{ baseNode: &baseNode{ dir: filepath.Join(dir, remoteCacheDir), }, source: source, } } func (node *CacheNode) Read() ([]byte, error) { return os.ReadFile(node.Location()) } func (node *CacheNode) Write(data []byte) error { if err := node.CreateCacheDir(); err != nil { return err } return os.WriteFile(node.Location(), data, 0o644) } func (node *CacheNode) ReadTimestamp() time.Time { b, err := os.ReadFile(node.timestampPath()) if err != nil { return time.Time{}.UTC() } timestamp, err := time.Parse(time.RFC3339, string(b)) if err != nil { return time.Time{}.UTC() } return timestamp.UTC() } func (node *CacheNode) WriteTimestamp(t time.Time) error { if err := node.CreateCacheDir(); err != nil { return err } return os.WriteFile(node.timestampPath(), []byte(t.Format(time.RFC3339)), 0o644) } func (node *CacheNode) ReadChecksum() string { b, _ := os.ReadFile(node.checksumPath()) return string(b) } func (node *CacheNode) WriteChecksum(checksum string) error { if err := node.CreateCacheDir(); err != nil { return err } return os.WriteFile(node.checksumPath(), []byte(checksum), 0o644) } func (node *CacheNode) CreateCacheDir() error { if err := os.MkdirAll(node.dir, 0o755); err != nil { return err } return nil } func (node *CacheNode) ChecksumPrompt(checksum string) string { cachedChecksum := node.ReadChecksum() switch { // If the checksum doesn't exist, prompt the user to continue case cachedChecksum == "": return taskfileUntrustedPrompt // If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue case cachedChecksum != checksum: return taskfileChangedPrompt default: return "" } } func (node *CacheNode) Location() string { return node.filePath("yaml") } func (node *CacheNode) checksumPath() string { return node.filePath("checksum") } func (node *CacheNode) timestampPath() string { return node.filePath("timestamp") } func (node *CacheNode) filePath(suffix string) string { return filepath.Join(node.dir, fmt.Sprintf("%s.%s", node.source.CacheKey(), suffix)) } func checksum(b []byte) string { h := sha256.New() h.Write(b) return fmt.Sprintf("%x", h.Sum(nil)) } ================================================ FILE: taskfile/node_file.go ================================================ package taskfile import ( "io" "os" "path/filepath" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fsext" ) // A FileNode is a node that reads a taskfile from the local filesystem. type FileNode struct { *baseNode entrypoint string } func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { // Find the entrypoint file resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles) if err != nil { if errors.Is(err, os.ErrNotExist) { if entrypoint == "" { return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true} } else { return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false} } } else if errors.Is(err, os.ErrPermission) { return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true, OwnerChange: true} } return nil, err } // Resolve the directory resolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir) if err != nil { return nil, err } return &FileNode{ baseNode: NewBaseNode(resolvedDir, opts...), entrypoint: resolvedEntrypoint, }, nil } func (node *FileNode) Location() string { return node.entrypoint } func (node *FileNode) Read() ([]byte, error) { f, err := os.Open(node.Location()) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) { // If the file is remote, we don't need to resolve the path if isRemoteEntrypoint(entrypoint) { return entrypoint, nil } path, err := execext.ExpandLiteral(entrypoint) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // This means that files are included relative to one another entrypointDir := filepath.Dir(node.entrypoint) return filepathext.SmartJoin(entrypointDir, path), nil } func (node *FileNode) ResolveDir(dir string) (string, error) { path, err := execext.ExpandLiteral(dir) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // This means that files are included relative to one another entrypointDir := filepath.Dir(node.entrypoint) return filepathext.SmartJoin(entrypointDir, path), nil } ================================================ FILE: taskfile/node_git.go ================================================ package taskfile import ( "context" "fmt" "net/url" "os" "path" "path/filepath" "strings" "sync" giturls "github.com/chainguard-dev/git-urls" "github.com/hashicorp/go-getter" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fsext" ) // An GitNode is a node that reads a Taskfile from a remote location via Git. type GitNode struct { *baseNode url *url.URL rawUrl string ref string path string } type gitRepoCache struct { mu sync.Mutex // Protects the locks map locks map[string]*sync.Mutex // One mutex per repo cache key } func (c *gitRepoCache) getLockForRepo(cacheKey string) *sync.Mutex { c.mu.Lock() defer c.mu.Unlock() if _, exists := c.locks[cacheKey]; !exists { c.locks[cacheKey] = &sync.Mutex{} } return c.locks[cacheKey] } var globalGitRepoCache = &gitRepoCache{ locks: make(map[string]*sync.Mutex), } func CleanGitCache() error { // Clear the in-memory locks map to prevent memory leak globalGitRepoCache.mu.Lock() globalGitRepoCache.locks = make(map[string]*sync.Mutex) globalGitRepoCache.mu.Unlock() cacheDir := filepath.Join(os.TempDir(), "task-git-repos") return os.RemoveAll(cacheDir) } func NewGitNode( entrypoint string, dir string, insecure bool, opts ...NodeOption, ) (*GitNode, error) { base := NewBaseNode(dir, opts...) u, err := giturls.Parse(entrypoint) if err != nil { return nil, err } basePath, path := splitURLOnDoubleSlash(u) ref := u.Query().Get("ref") rawUrl := u.Redacted() u.RawQuery = "" u.Path = basePath if u.Scheme == "http" && !insecure { return nil, &errors.TaskfileNotSecureError{URI: u.Redacted()} } return &GitNode{ baseNode: base, url: u, rawUrl: rawUrl, ref: ref, path: path, }, nil } func (node *GitNode) Location() string { return node.rawUrl } func (node *GitNode) Remote() bool { return true } func (node *GitNode) Read() ([]byte, error) { return node.ReadContext(context.Background()) } func (node *GitNode) buildURL() string { // Get the base URL baseURL := node.url.String() // Always use git:: prefix for git URLs (following Terraform's pattern) // This forces go-getter to use git protocol if node.ref != "" { return fmt.Sprintf("git::%s?ref=%s&depth=1", baseURL, node.ref) } // When no ref is specified, omit it entirely to let git clone the default branch return fmt.Sprintf("git::%s?depth=1", baseURL) } // getOrCloneRepo returns the path to a cached git repository. // If the repository is not cached, it clones it first. // This function is thread-safe: multiple goroutines cloning the same repo+ref // will synchronize, and only one clone operation will occur. // // The cache directory is /tmp/task-git-repos/{cache_key}/ func (node *GitNode) getOrCloneRepo(ctx context.Context) (string, error) { cacheKey := node.repoCacheKey() repoMutex := globalGitRepoCache.getLockForRepo(cacheKey) repoMutex.Lock() defer repoMutex.Unlock() cacheDir := filepath.Join(os.TempDir(), "task-git-repos", cacheKey) // Check cache FIRST - if already cloned, no network needed, timeout irrelevant gitDir := filepath.Join(cacheDir, ".git") if _, err := os.Stat(gitDir); err == nil { return cacheDir, nil } // Only check context if we need to clone (requires network) if err := ctx.Err(); err != nil { return "", fmt.Errorf("context cancelled while waiting for repository lock: %w", err) } getterURL := node.buildURL() client := &getter.Client{ Ctx: ctx, Src: getterURL, Dst: cacheDir, Mode: getter.ClientModeDir, } if err := client.Get(); err != nil { _ = os.RemoveAll(cacheDir) return "", fmt.Errorf("failed to clone repository: %w", err) } return cacheDir, nil } func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) { // Get or clone the repository into cache repoDir, err := node.getOrCloneRepo(ctx) if err != nil { return nil, err } // Build path to Taskfile in the cached repo // If node.path is empty, search in repo root; otherwise search in the specified path // fsext.SearchPath handles both files and directories (searching for DefaultTaskfiles) searchPath := repoDir if node.path != "" { searchPath = filepath.Join(repoDir, node.path) } filePath, err := fsext.SearchPath(searchPath, DefaultTaskfiles) if err != nil { return nil, err } // Read file from cached repo b, err := os.ReadFile(filePath) if err != nil { return nil, err } return b, nil } func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) { // If the file is remote, we don't need to resolve the path if isRemoteEntrypoint(entrypoint) { return entrypoint, nil } dir, _ := path.Split(node.path) resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, path.Join(dir, entrypoint)) if node.ref != "" { return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil } return resolvedEntrypoint, nil } func (node *GitNode) ResolveDir(dir string) (string, error) { path, err := execext.ExpandLiteral(dir) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // This means that files are included relative to one another entrypointDir := filepath.Dir(node.Dir()) return filepathext.SmartJoin(entrypointDir, path), nil } func (node *GitNode) CacheKey() string { checksum := strings.TrimRight(checksum([]byte(node.Location())), "=") lastDir := filepath.Base(filepath.Dir(node.path)) prefix := filepath.Base(node.path) // Means it's not "", nor "." nor "/", so it's a valid directory if len(lastDir) > 1 { prefix = fmt.Sprintf("%s.%s", lastDir, prefix) } return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum) } // repoCacheKey generates a unique cache key for the repository+ref combination. // Unlike CacheKey() which includes the file path, this identifies the repository itself. // Two GitNodes with the same repo+ref but different file paths will share the same cache. // // Returns a path like: github.com/user/repo.git/main func (node *GitNode) repoCacheKey() string { repoPath := strings.Trim(node.url.Path, "/") ref := node.ref if ref == "" { ref = "_default_" // Placeholder for the remote's default branch } return filepath.Join(node.url.Host, repoPath, ref) } func splitURLOnDoubleSlash(u *url.URL) (string, string) { x := strings.Split(u.Path, "//") switch len(x) { case 0: return "", "" case 1: return x[0], "" default: return x[0], x[1] } } ================================================ FILE: taskfile/node_git_test.go ================================================ package taskfile import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGitNode_ssh(t *testing.T) { t.Parallel() node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) assert.Equal(t, "Taskfile.yml", node.path) assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String()) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) } func TestGitNode_sshWithAltRepo(t *testing.T) { t.Parallel() node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) entrypoint, err := node.ResolveEntrypoint("git@github.com:foo/other.git//Taskfile.yml?ref=dev") assert.NoError(t, err) assert.Equal(t, "git@github.com:foo/other.git//Taskfile.yml?ref=dev", entrypoint) } func TestGitNode_sshWithDir(t *testing.T) { t.Parallel() node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) assert.Equal(t, "directory/Taskfile.yml", node.path) assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String()) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) } func TestGitNode_https(t *testing.T) { t.Parallel() node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) assert.Equal(t, "Taskfile.yml", node.path) assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "https://github.com/foo/bar.git", node.url.String()) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint) } func TestGitNode_httpsWithDir(t *testing.T) { t.Parallel() node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false) assert.NoError(t, err) assert.Equal(t, "main", node.ref) assert.Equal(t, "directory/Taskfile.yml", node.path) assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location()) assert.Equal(t, "https://github.com/foo/bar.git", node.url.String()) entrypoint, err := node.ResolveEntrypoint("common.yml") assert.NoError(t, err) assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) } func TestGitNode_CacheKey(t *testing.T) { t.Parallel() tests := []struct { entrypoint string expectedKey string }{ { entrypoint: "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", expectedKey: "git.github.com.directory.Taskfile.yml.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb", }, { entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main", expectedKey: "git.github.com.Taskfile.yml.39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4", }, { entrypoint: "https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", expectedKey: "git.github.com.directory.Taskfile.yml.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31", }, } for _, tt := range tests { node, err := NewGitNode(tt.entrypoint, "", false) require.NoError(t, err) key := node.CacheKey() assert.Equal(t, tt.expectedKey, key) } } func TestGitNode_buildURL(t *testing.T) { t.Parallel() tests := []struct { name string entrypoint string expectedURL string }{ { name: "HTTPS with ref", entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main", expectedURL: "git::https://github.com/foo/bar.git?ref=main&depth=1", }, { name: "SSH with ref", entrypoint: "git@github.com:foo/bar.git//Taskfile.yml?ref=main", expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=main&depth=1", }, { name: "HTTPS with tag ref", entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=v1.0.0", expectedURL: "git::https://github.com/foo/bar.git?ref=v1.0.0&depth=1", }, { name: "HTTPS without ref (uses remote default branch)", entrypoint: "https://github.com/foo/bar.git//Taskfile.yml", expectedURL: "git::https://github.com/foo/bar.git?depth=1", }, { name: "SSH with directory path", entrypoint: "git@github.com:foo/bar.git//directory/Taskfile.yml?ref=dev", expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=dev&depth=1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() node, err := NewGitNode(tt.entrypoint, "", false) require.NoError(t, err) gotURL := node.buildURL() assert.Equal(t, tt.expectedURL, gotURL) }) } } func TestRepoCacheKey_SameRepoSameRef(t *testing.T) { t.Parallel() // Same repo, same ref, different files should have SAME cache key node1, err := NewGitNode("https://github.com/foo/bar.git//file1.yml?ref=main", "", false) require.NoError(t, err) node2, err := NewGitNode("https://github.com/foo/bar.git//dir/file2.yml?ref=main", "", false) require.NoError(t, err) key1 := node1.repoCacheKey() key2 := node2.repoCacheKey() assert.Equal(t, key1, key2, "Same repo+ref should generate same cache key regardless of file path") } func TestRepoCacheKey_SameRepoDifferentRef(t *testing.T) { t.Parallel() // Same repo, different ref should have DIFFERENT cache keys node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=dev", "", false) require.NoError(t, err) key1 := node1.repoCacheKey() key2 := node2.repoCacheKey() assert.NotEqual(t, key1, key2, "Different refs should generate different cache keys") } func TestRepoCacheKey_DifferentRepos(t *testing.T) { t.Parallel() // Different repos should have DIFFERENT cache keys node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) node2, err := NewGitNode("https://github.com/foo/other.git//file.yml?ref=main", "", false) require.NoError(t, err) key1 := node1.repoCacheKey() key2 := node2.repoCacheKey() assert.NotEqual(t, key1, key2, "Different repos should generate different cache keys") } func TestRepoCacheKey_NoRefVsExplicitRef(t *testing.T) { t.Parallel() // No ref (uses default branch) vs explicit ref should have DIFFERENT cache keys node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml", "", false) require.NoError(t, err) node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) key1 := node1.repoCacheKey() key2 := node2.repoCacheKey() assert.NotEqual(t, key1, key2, "No ref and explicit ref should generate different cache keys") } func TestRepoCacheKey_SSHvsHTTPS(t *testing.T) { t.Parallel() // SSH vs HTTPS pointing to same repo should have SAME cache key // They clone the same repo, so we want to share the cache node1, err := NewGitNode("git@github.com:foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) key1 := node1.repoCacheKey() key2 := node2.repoCacheKey() assert.Equal(t, key1, key2, "SSH and HTTPS for same repo should share cache") } func TestRepoCacheKey_Consistency(t *testing.T) { t.Parallel() // Calling repoCacheKey multiple times on same node should return same key node, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false) require.NoError(t, err) key1 := node.repoCacheKey() key2 := node.repoCacheKey() key3 := node.repoCacheKey() assert.Equal(t, key1, key2) assert.Equal(t, key2, key3) } ================================================ FILE: taskfile/node_http.go ================================================ package taskfile import ( "context" "crypto/tls" "crypto/x509" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" ) // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. type HTTPNode struct { *baseNode url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml) client *http.Client // HTTP client with optional TLS configuration } // buildHTTPClient creates an HTTP client with optional TLS configuration. // If no certificate options are provided, it returns http.DefaultClient. func buildHTTPClient(insecure bool, caCert, cert, certKey string) (*http.Client, error) { // Validate that cert and certKey are provided together if (cert != "" && certKey == "") || (cert == "" && certKey != "") { return nil, fmt.Errorf("both --cert and --cert-key must be provided together") } // If no TLS customization is needed, return the default client if !insecure && caCert == "" && cert == "" { return http.DefaultClient, nil } tlsConfig := &tls.Config{ InsecureSkipVerify: insecure, } // Load custom CA certificate if provided if caCert != "" { caCertData, err := os.ReadFile(caCert) if err != nil { return nil, fmt.Errorf("failed to read CA certificate: %w", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCertData) { return nil, fmt.Errorf("failed to parse CA certificate") } tlsConfig.RootCAs = caCertPool } // Load client certificate and key if provided if cert != "" && certKey != "" { clientCert, err := tls.LoadX509KeyPair(cert, certKey) if err != nil { return nil, fmt.Errorf("failed to load client certificate: %w", err) } tlsConfig.Certificates = []tls.Certificate{clientCert} } return &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, }, nil } func NewHTTPNode( entrypoint string, dir string, insecure bool, opts ...NodeOption, ) (*HTTPNode, error) { base := NewBaseNode(dir, opts...) url, err := url.Parse(entrypoint) if err != nil { return nil, err } if url.Scheme == "http" && !insecure { return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()} } client, err := buildHTTPClient(insecure, base.caCert, base.cert, base.certKey) if err != nil { return nil, err } return &HTTPNode{ baseNode: base, url: url, client: client, }, nil } func (node *HTTPNode) Location() string { return node.url.Redacted() } func (node *HTTPNode) Read() ([]byte, error) { return node.ReadContext(context.Background()) } func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) { url, err := RemoteExists(ctx, *node.url, node.client) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) if err != nil { return nil, errors.TaskfileFetchFailedError{URI: node.Location()} } resp, err := node.client.Do(req.WithContext(ctx)) if err != nil { if ctx.Err() != nil { return nil, err } return nil, errors.TaskfileFetchFailedError{URI: node.Location()} } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.TaskfileFetchFailedError{ URI: node.Location(), HTTPStatusCode: resp.StatusCode, } } // Read the entire response body b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return b, nil } func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) { ref, err := url.Parse(entrypoint) if err != nil { return "", err } return node.url.ResolveReference(ref).String(), nil } func (node *HTTPNode) ResolveDir(dir string) (string, error) { path, err := execext.ExpandLiteral(dir) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory // This means that files are included relative to one another parent := node.Dir() if node.Parent() != nil { parent = node.Parent().Dir() } return filepathext.SmartJoin(parent, path), nil } func (node *HTTPNode) CacheKey() string { checksum := strings.TrimRight(checksum([]byte(node.Location())), "=") dir, filename := filepath.Split(node.url.Path) lastDir := filepath.Base(dir) prefix := filename // Means it's not "", nor "." nor "/", so it's a valid directory if len(lastDir) > 1 { prefix = fmt.Sprintf("%s.%s", lastDir, filename) } return fmt.Sprintf("http.%s.%s.%s", node.url.Host, prefix, checksum) } ================================================ FILE: taskfile/node_http_test.go ================================================ package taskfile import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net/http" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHTTPNode_CacheKey(t *testing.T) { t.Parallel() tests := []struct { entrypoint string expectedKey string }{ { entrypoint: "https://github.com", expectedKey: "http.github.com..996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a", }, { entrypoint: "https://github.com/Taskfile.yml", expectedKey: "http.github.com.Taskfile.yml.85b3c3ad71b78dc74e404c7b4390fc13672925cb644a4d26c21b9f97c17b5fc0", }, { entrypoint: "https://github.com/foo", expectedKey: "http.github.com.foo.df3158dafc823e6847d9bcaf79328446c4877405e79b100723fa6fd545ed3e2b", }, { entrypoint: "https://github.com/foo/Taskfile.yml", expectedKey: "http.github.com.foo.Taskfile.yml.aea946ea7eb6f6bb4e159e8b840b6b50975927778b2e666df988c03bbf10c4c4", }, { entrypoint: "https://github.com/foo/bar", expectedKey: "http.github.com.foo.bar.d3514ad1d4daedf9cc2825225070b49ebc8db47fa5177951b2a5b9994597570c", }, { entrypoint: "https://github.com/foo/bar/Taskfile.yml", expectedKey: "http.github.com.bar.Taskfile.yml.b9cf01e01e47c0e96ea536e1a8bd7b3a6f6c1f1881bad438990d2bfd4ccd0ac0", }, } for _, tt := range tests { node, err := NewHTTPNode(tt.entrypoint, "", false) require.NoError(t, err) key := node.CacheKey() assert.Equal(t, tt.expectedKey, key) } } func TestBuildHTTPClient_Default(t *testing.T) { t.Parallel() // When no TLS customization is needed, should return http.DefaultClient client, err := buildHTTPClient(false, "", "", "") require.NoError(t, err) assert.Equal(t, http.DefaultClient, client) } func TestBuildHTTPClient_Insecure(t *testing.T) { t.Parallel() client, err := buildHTTPClient(true, "", "", "") require.NoError(t, err) require.NotNil(t, client) assert.NotEqual(t, http.DefaultClient, client) // Check that InsecureSkipVerify is set transport, ok := client.Transport.(*http.Transport) require.True(t, ok) require.NotNil(t, transport.TLSClientConfig) assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) } func TestBuildHTTPClient_CACert(t *testing.T) { t.Parallel() // Create a temporary CA cert file tempDir := t.TempDir() caCertPath := filepath.Join(tempDir, "ca.crt") // Generate a valid CA certificate caCertPEM := generateTestCACert(t) err := os.WriteFile(caCertPath, caCertPEM, 0o600) require.NoError(t, err) client, err := buildHTTPClient(false, caCertPath, "", "") require.NoError(t, err) require.NotNil(t, client) assert.NotEqual(t, http.DefaultClient, client) // Check that custom RootCAs is set transport, ok := client.Transport.(*http.Transport) require.True(t, ok) require.NotNil(t, transport.TLSClientConfig) assert.NotNil(t, transport.TLSClientConfig.RootCAs) } func TestBuildHTTPClient_CACertNotFound(t *testing.T) { t.Parallel() client, err := buildHTTPClient(false, "/nonexistent/ca.crt", "", "") assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "failed to read CA certificate") } func TestBuildHTTPClient_CACertInvalid(t *testing.T) { t.Parallel() // Create a temporary file with invalid content tempDir := t.TempDir() caCertPath := filepath.Join(tempDir, "invalid.crt") err := os.WriteFile(caCertPath, []byte("not a valid certificate"), 0o600) require.NoError(t, err) client, err := buildHTTPClient(false, caCertPath, "", "") assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "failed to parse CA certificate") } func TestBuildHTTPClient_CertWithoutKey(t *testing.T) { t.Parallel() client, err := buildHTTPClient(false, "", "/path/to/cert.crt", "") assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together") } func TestBuildHTTPClient_KeyWithoutCert(t *testing.T) { t.Parallel() client, err := buildHTTPClient(false, "", "", "/path/to/key.pem") assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together") } func TestBuildHTTPClient_CertAndKey(t *testing.T) { t.Parallel() // Create temporary cert and key files tempDir := t.TempDir() certPath := filepath.Join(tempDir, "client.crt") keyPath := filepath.Join(tempDir, "client.key") // Generate a self-signed certificate and key for testing cert, key := generateTestCertAndKey(t) err := os.WriteFile(certPath, cert, 0o600) require.NoError(t, err) err = os.WriteFile(keyPath, key, 0o600) require.NoError(t, err) client, err := buildHTTPClient(false, "", certPath, keyPath) require.NoError(t, err) require.NotNil(t, client) assert.NotEqual(t, http.DefaultClient, client) // Check that client certificate is set transport, ok := client.Transport.(*http.Transport) require.True(t, ok) require.NotNil(t, transport.TLSClientConfig) assert.Len(t, transport.TLSClientConfig.Certificates, 1) } func TestBuildHTTPClient_CertNotFound(t *testing.T) { t.Parallel() client, err := buildHTTPClient(false, "", "/nonexistent/cert.crt", "/nonexistent/key.pem") assert.Error(t, err) assert.Nil(t, client) assert.Contains(t, err.Error(), "failed to load client certificate") } func TestBuildHTTPClient_InsecureWithCACert(t *testing.T) { t.Parallel() // Create a temporary CA cert file tempDir := t.TempDir() caCertPath := filepath.Join(tempDir, "ca.crt") // Generate a valid CA certificate caCertPEM := generateTestCACert(t) err := os.WriteFile(caCertPath, caCertPEM, 0o600) require.NoError(t, err) // Both insecure and CA cert can be set together client, err := buildHTTPClient(true, caCertPath, "", "") require.NoError(t, err) require.NotNil(t, client) transport, ok := client.Transport.(*http.Transport) require.True(t, ok) require.NotNil(t, transport.TLSClientConfig) assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) assert.NotNil(t, transport.TLSClientConfig.RootCAs) } // generateTestCertAndKey generates a self-signed certificate and key for testing func generateTestCertAndKey(t *testing.T) (certPEM, keyPEM []byte) { t.Helper() // Generate a new ECDSA private key privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) // Create a certificate template template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Task Org"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, } // Create the certificate certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) require.NoError(t, err) // Encode certificate to PEM certPEM = pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certDER, }) // Encode private key to PEM keyDER, err := x509.MarshalECPrivateKey(privateKey) require.NoError(t, err) keyPEM = pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: keyDER, }) return certPEM, keyPEM } // generateTestCACert generates a self-signed CA certificate for testing func generateTestCACert(t *testing.T) []byte { t.Helper() // Generate a new ECDSA private key privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) // Create a CA certificate template template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Test CA"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, IsCA: true, BasicConstraintsValid: true, } // Create the certificate certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) require.NoError(t, err) // Encode certificate to PEM return pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certDER, }) } ================================================ FILE: taskfile/node_stdin.go ================================================ package taskfile import ( "bufio" "fmt" "os" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" ) // A StdinNode is a node that reads a taskfile from the standard input stream. type StdinNode struct { *baseNode } func NewStdinNode(dir string) (*StdinNode, error) { return &StdinNode{ baseNode: NewBaseNode(dir), }, nil } func (node *StdinNode) Location() string { return "__stdin__" } func (node *StdinNode) Remote() bool { return false } func (node *StdinNode) Read() ([]byte, error) { var stdin []byte scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { stdin = fmt.Appendln(stdin, scanner.Text()) } if err := scanner.Err(); err != nil { return nil, err } return stdin, nil } func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) { // If the file is remote, we don't need to resolve the path if isRemoteEntrypoint(entrypoint) { return entrypoint, nil } path, err := execext.ExpandLiteral(entrypoint) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } return filepathext.SmartJoin(node.Dir(), path), nil } func (node *StdinNode) ResolveDir(dir string) (string, error) { path, err := execext.ExpandLiteral(dir) if err != nil { return "", err } if filepathext.IsAbs(path) { return path, nil } return filepathext.SmartJoin(node.Dir(), path), nil } ================================================ FILE: taskfile/node_test.go ================================================ package taskfile import ( "testing" "github.com/stretchr/testify/assert" ) func TestScheme(t *testing.T) { t.Parallel() scheme, err := getScheme("https://github.com/foo/bar.git") assert.NoError(t, err) assert.Equal(t, "git", scheme) scheme, err = getScheme("https://github.com/foo/bar.git?ref=v1//taskfile/common.yml") assert.NoError(t, err) assert.Equal(t, "git", scheme) scheme, err = getScheme("git@github.com:foo/bar.git?ref=main//Taskfile.yml") assert.NoError(t, err) assert.Equal(t, "git", scheme) scheme, err = getScheme("https://github.com/foo/common.yml") assert.NoError(t, err) assert.Equal(t, "https", scheme) } ================================================ FILE: taskfile/reader.go ================================================ package taskfile import ( "context" "fmt" "net/url" "os" "sync" "time" "github.com/dominikbraun/graph" "go.yaml.in/yaml/v3" "golang.org/x/sync/errgroup" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) const ( taskfileUntrustedPrompt = `The task you are attempting to run depends on the remote Taskfile at %q. --- Make sure you trust the source of this Taskfile before continuing --- Continue?` taskfileChangedPrompt = `The Taskfile at %q has changed since you last used it! --- Make sure you trust the source of this Taskfile before continuing --- Continue?` ) type ( // DebugFunc is a function that can be called to log debug messages. DebugFunc func(string) // PromptFunc is a function that can be called to prompt the user for input. PromptFunc func(string) error // A ReaderOption is any type that can apply a configuration to a [Reader]. ReaderOption interface { ApplyToReader(*Reader) } // A Reader will recursively read Taskfiles from a given [Node] and build a // [ast.TaskfileGraph] from them. Reader struct { graph *ast.TaskfileGraph insecure bool download bool offline bool trustedHosts []string tempDir string cacheExpiryDuration time.Duration caCert string cert string certKey string debugFunc DebugFunc promptFunc PromptFunc promptMutex sync.Mutex } ) // NewReader constructs a new Taskfile [Reader] using the given Node and // options. func NewReader(opts ...ReaderOption) *Reader { r := &Reader{ graph: ast.NewTaskfileGraph(), insecure: false, download: false, offline: false, trustedHosts: nil, tempDir: os.TempDir(), cacheExpiryDuration: 0, debugFunc: nil, promptFunc: nil, promptMutex: sync.Mutex{}, } r.Options(opts...) return r } // Options loops through the given [ReaderOption] functions and applies them to // the [Reader]. func (r *Reader) Options(opts ...ReaderOption) { for _, opt := range opts { opt.ApplyToReader(r) } } // WithInsecure allows the [Reader] to make insecure connections when reading // remote taskfiles. By default, insecure connections are rejected. func WithInsecure(insecure bool) ReaderOption { return &insecureOption{insecure: insecure} } type insecureOption struct { insecure bool } func (o *insecureOption) ApplyToReader(r *Reader) { r.insecure = o.insecure } // WithDownload forces the [Reader] to download a fresh copy of the taskfile // from the remote source. func WithDownload(download bool) ReaderOption { return &downloadOption{download: download} } type downloadOption struct { download bool } func (o *downloadOption) ApplyToReader(r *Reader) { r.download = o.download } // WithOffline stops the [Reader] from being able to make network connections. // It will still be able to read local files and cached copies of remote files. func WithOffline(offline bool) ReaderOption { return &offlineOption{offline: offline} } type offlineOption struct { offline bool } func (o *offlineOption) ApplyToReader(r *Reader) { r.offline = o.offline } // WithTrustedHosts configures the [Reader] with a list of trusted hosts for remote // Taskfiles. Hosts in this list will not prompt for user confirmation. func WithTrustedHosts(trustedHosts []string) ReaderOption { return &trustedHostsOption{trustedHosts: trustedHosts} } type trustedHostsOption struct { trustedHosts []string } func (o *trustedHostsOption) ApplyToReader(r *Reader) { r.trustedHosts = o.trustedHosts } // WithTempDir sets the temporary directory that will be used by the [Reader]. // By default, the reader uses [os.TempDir]. func WithTempDir(tempDir string) ReaderOption { return &tempDirOption{tempDir: tempDir} } type tempDirOption struct { tempDir string } func (o *tempDirOption) ApplyToReader(r *Reader) { r.tempDir = o.tempDir } // WithCacheExpiryDuration sets the duration after which the cache is considered // expired. By default, the cache is considered expired after 24 hours. func WithCacheExpiryDuration(duration time.Duration) ReaderOption { return &cacheExpiryDurationOption{duration: duration} } type cacheExpiryDurationOption struct { duration time.Duration } func (o *cacheExpiryDurationOption) ApplyToReader(r *Reader) { r.cacheExpiryDuration = o.duration } // WithDebugFunc sets the debug function to be used by the [Reader]. If set, // this function will be called with debug messages. This can be useful if the // caller wants to log debug messages from the [Reader]. By default, no debug // function is set and the logs are not written. func WithDebugFunc(debugFunc DebugFunc) ReaderOption { return &debugFuncOption{debugFunc: debugFunc} } type debugFuncOption struct { debugFunc DebugFunc } func (o *debugFuncOption) ApplyToReader(r *Reader) { r.debugFunc = o.debugFunc } // WithPromptFunc sets the prompt function to be used by the [Reader]. If set, // this function will be called with prompt messages. The function should // optionally log the message to the user and return nil if the prompt is // accepted and the execution should continue. Otherwise, it should return an // error which describes why the prompt was rejected. This can then be caught // and used later when calling the [Reader.Read] method. By default, no prompt // function is set and all prompts are automatically accepted. func WithPromptFunc(promptFunc PromptFunc) ReaderOption { return &promptFuncOption{promptFunc: promptFunc} } type promptFuncOption struct { promptFunc PromptFunc } func (o *promptFuncOption) ApplyToReader(r *Reader) { r.promptFunc = o.promptFunc } // WithReaderCACert sets the path to a custom CA certificate for TLS connections. func WithReaderCACert(caCert string) ReaderOption { return &readerCACertOption{caCert: caCert} } type readerCACertOption struct { caCert string } func (o *readerCACertOption) ApplyToReader(r *Reader) { r.caCert = o.caCert } // WithReaderCert sets the path to a client certificate for TLS connections. func WithReaderCert(cert string) ReaderOption { return &readerCertOption{cert: cert} } type readerCertOption struct { cert string } func (o *readerCertOption) ApplyToReader(r *Reader) { r.cert = o.cert } // WithReaderCertKey sets the path to a client certificate key for TLS connections. func WithReaderCertKey(certKey string) ReaderOption { return &readerCertKeyOption{certKey: certKey} } type readerCertKeyOption struct { certKey string } func (o *readerCertKeyOption) ApplyToReader(r *Reader) { r.certKey = o.certKey } // Read will read the Taskfile defined by the [Reader]'s [Node] and recurse // through any [ast.Includes] it finds, reading each included Taskfile and // building an [ast.TaskfileGraph] as it goes. If any errors occur, they will be // returned immediately. func (r *Reader) Read(ctx context.Context, node Node) (*ast.TaskfileGraph, error) { // Clean up git cache after reading all taskfiles defer func() { _ = CleanGitCache() }() if err := r.include(ctx, node); err != nil { return nil, err } return r.graph, nil } func (r *Reader) debugf(format string, a ...any) { if r.debugFunc != nil { r.debugFunc(fmt.Sprintf(format, a...)) } } func (r *Reader) promptf(format string, a ...any) error { if r.promptFunc != nil { return r.promptFunc(fmt.Sprintf(format, a...)) } return nil } // isTrusted checks if a URI's host matches any of the trusted hosts patterns. func (r *Reader) isTrusted(uri string) bool { if len(r.trustedHosts) == 0 { return false } // Parse the URI to extract the host parsedURL, err := url.Parse(uri) if err != nil { return false } host := parsedURL.Host // Check against each trusted pattern (exact match including port if provided) for _, pattern := range r.trustedHosts { if host == pattern { return true } } return false } func (r *Reader) include(ctx context.Context, node Node) error { // Create a new vertex for the Taskfile vertex := &ast.TaskfileVertex{ URI: node.Location(), Taskfile: nil, } // Add the included Taskfile to the DAG // If the vertex already exists, we return early since its Taskfile has // already been read and its children explored if err := r.graph.AddVertex(vertex); err == graph.ErrVertexAlreadyExists { return nil } else if err != nil { return err } // Read and parse the Taskfile from the file and add it to the vertex var err error vertex.Taskfile, err = r.readNode(ctx, node) if err != nil { return err } // Create an error group to wait for all included Taskfiles to be read var g errgroup.Group // Loop over each included taskfile for _, include := range vertex.Taskfile.Includes.All() { vars := env.GetEnviron() vars.Merge(vertex.Taskfile.Vars, nil) // Start a goroutine to process each included Taskfile g.Go(func() error { cache := &templater.Cache{Vars: vars} include = &ast.Include{ Namespace: include.Namespace, Taskfile: templater.Replace(include.Taskfile, cache), Dir: templater.Replace(include.Dir, cache), Optional: include.Optional, Internal: include.Internal, Flatten: include.Flatten, Aliases: include.Aliases, AdvancedImport: include.AdvancedImport, Excludes: include.Excludes, Vars: include.Vars, Checksum: include.Checksum, } if err := cache.Err(); err != nil { return err } entrypoint, err := node.ResolveEntrypoint(include.Taskfile) if err != nil { return err } include.Dir, err = node.ResolveDir(include.Dir) if err != nil { return err } includeNode, err := NewNode(entrypoint, include.Dir, r.insecure, WithParent(node), WithChecksum(include.Checksum), WithCACert(r.caCert), WithCert(r.cert), WithCertKey(r.certKey), ) if err != nil { if include.Optional { return nil } return err } // Recurse into the included Taskfile if err := r.include(ctx, includeNode); err != nil { return err } // Create an edge between the Taskfiles r.graph.Lock() defer r.graph.Unlock() edge, err := r.graph.Edge(node.Location(), includeNode.Location()) if err == graph.ErrEdgeNotFound { // If the edge doesn't exist, create it err = r.graph.AddEdge( node.Location(), includeNode.Location(), graph.EdgeData([]*ast.Include{include}), graph.EdgeWeight(1), ) } else { // If the edge already exists edgeData := append(edge.Properties.Data.([]*ast.Include), include) err = r.graph.UpdateEdge( node.Location(), includeNode.Location(), graph.EdgeData(edgeData), graph.EdgeWeight(len(edgeData)), ) } if errors.Is(err, graph.ErrEdgeCreatesCycle) { return errors.TaskfileCycleError{ Source: node.Location(), Destination: includeNode.Location(), } } return err }) } // Wait for all the go routines to finish return g.Wait() } func (r *Reader) readNode(ctx context.Context, node Node) (*ast.Taskfile, error) { b, err := r.readNodeContent(ctx, node) if err != nil { return nil, err } var tf ast.Taskfile if err := yaml.Unmarshal(b, &tf); err != nil { // Decode the taskfile and add the file info the any errors taskfileDecodeErr := &errors.TaskfileDecodeError{} if errors.As(err, &taskfileDecodeErr) { snippet := NewSnippet(b, WithLine(taskfileDecodeErr.Line), WithColumn(taskfileDecodeErr.Column), WithPadding(2), ) return nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String()) } return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} } // Check that the Taskfile is set and has a schema version if tf.Version == nil { return nil, &errors.TaskfileVersionCheckError{URI: node.Location()} } // Set the taskfile/task's locations tf.Location = node.Location() for task := range tf.Tasks.Values(nil) { // If the task is not defined, create a new one if task == nil { task = &ast.Task{} } // Set the location of the taskfile for each task if task.Location.Taskfile == "" { task.Location.Taskfile = tf.Location } } return &tf, nil } func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error) { if node, isRemote := node.(RemoteNode); isRemote { return r.readRemoteNodeContent(ctx, node) } // Read the Taskfile b, err := node.Read() if err != nil { return nil, err } // If the given checksum doesn't match the sum pinned in the Taskfile checksum := checksum(b) if !node.Verify(checksum) { return nil, &errors.TaskfileDoesNotMatchChecksum{ URI: node.Location(), ExpectedChecksum: node.Checksum(), ActualChecksum: checksum, } } return b, nil } func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) { cache := NewCacheNode(node, r.tempDir) now := time.Now().UTC() timestamp := cache.ReadTimestamp() expiry := timestamp.Add(r.cacheExpiryDuration) cacheValid := now.Before(expiry) var cacheFound bool r.debugf("checking cache for %q in %q\n", node.Location(), cache.Location()) cachedBytes, err := cache.Read() switch { // If the cache doesn't exist, we need to download the file case errors.Is(err, os.ErrNotExist): r.debugf("no cache found\n") // If we couldn't find a cached copy, and we are offline, we can't do anything if r.offline { return nil, &errors.TaskfileCacheNotFoundError{ URI: node.Location(), } } // If the cache is expired case !cacheValid: r.debugf("cache expired at %s\n", expiry.Format(time.RFC3339)) cacheFound = true // If we can't fetch a fresh copy, we should use the cache anyway if r.offline { r.debugf("in offline mode, using expired cache\n") return cachedBytes, nil } // Some other error case err != nil: return nil, err // Found valid cache default: r.debugf("cache found\n") // Not being forced to redownload, return cache if !r.download { return cachedBytes, nil } cacheFound = true } // Try to read the remote file r.debugf("downloading remote file: %s\n", node.Location()) downloadedBytes, err := node.ReadContext(ctx) if err != nil { // If the context timed out or was cancelled, but we found a cached version, use that if ctx.Err() != nil && cacheFound { if cacheValid { r.debugf("failed to fetch remote file: %s: using cache\n", ctx.Err().Error()) } else { r.debugf("failed to fetch remote file: %s: using expired cache\n", ctx.Err().Error()) } return cachedBytes, nil } return nil, err } r.debugf("found remote file at %q\n", node.Location()) // If the given checksum doesn't match the sum pinned in the Taskfile checksum := checksum(downloadedBytes) if !node.Verify(checksum) { return nil, &errors.TaskfileDoesNotMatchChecksum{ URI: node.Location(), ExpectedChecksum: node.Checksum(), ActualChecksum: checksum, } } // If there is no manual checksum pin, run the automatic checks if node.Checksum() == "" { // Prompt the user if required (unless host is trusted) prompt := cache.ChecksumPrompt(checksum) if prompt != "" && !r.isTrusted(node.Location()) { if err := func() error { r.promptMutex.Lock() defer r.promptMutex.Unlock() return r.promptf(prompt, node.Location()) }(); err != nil { return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} } } } // Store the checksum if err := cache.WriteChecksum(checksum); err != nil { return nil, err } // Store the timestamp if err := cache.WriteTimestamp(now); err != nil { return nil, err } // Cache the file r.debugf("caching %q to %q\n", node.Location(), cache.Location()) if err = cache.Write(downloadedBytes); err != nil { return nil, err } return downloadedBytes, nil } ================================================ FILE: taskfile/snippet.go ================================================ package taskfile import ( "bytes" "embed" "fmt" "strings" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/quick" "github.com/alecthomas/chroma/v2/styles" "github.com/fatih/color" ) //go:embed themes/*.xml var embedded embed.FS const ( lineIndicator = ">" columnIndicator = "^" ) func init() { r, err := embedded.Open("themes/task.xml") if err != nil { panic(err) } style, err := chroma.NewXMLStyle(r) if err != nil { panic(err) } styles.Register(style) } type ( // A SnippetOption is any type that can apply a configuration to a [Snippet]. SnippetOption interface { ApplyToSnippet(*Snippet) } // A Snippet is a syntax highlighted snippet of a Taskfile with optional // padding and a line and column indicator. Snippet struct { linesRaw []string linesHighlighted []string start int end int line int column int padding int noIndicators bool } ) // NewSnippet creates a new [Snippet] from a byte slice and a line and column // number. The line and column numbers should be 1-indexed. For example, the // first character in the file would be 1:1 (line 1, column 1). The padding // determines the number of lines to include before and after the chosen line. func NewSnippet(b []byte, opts ...SnippetOption) *Snippet { snippet := &Snippet{} snippet.Options(opts...) // Syntax highlight the input and split it into lines buf := &bytes.Buffer{} if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil { buf.Write(b) } linesRaw := strings.Split(string(b), "\n") linesHighlighted := strings.Split(buf.String(), "\n") // Work out the start and end lines of the snippet snippet.start = max(snippet.line-snippet.padding, 1) snippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1) snippet.linesRaw = linesRaw[snippet.start-1 : snippet.end] snippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end] return snippet } // Options loops through the given [SnippetOption] functions and applies them // to the [Snippet]. func (s *Snippet) Options(opts ...SnippetOption) { for _, opt := range opts { opt.ApplyToSnippet(s) } } // WithLine specifies the line number that the [Snippet] should center around // and point to. func WithLine(line int) SnippetOption { return &lineOption{line: line} } type lineOption struct { line int } func (o *lineOption) ApplyToSnippet(s *Snippet) { s.line = o.line } // WithColumn specifies the column number that the [Snippet] should point to. func WithColumn(column int) SnippetOption { return &columnOption{column: column} } type columnOption struct { column int } func (o *columnOption) ApplyToSnippet(s *Snippet) { s.column = o.column } // WithPadding specifies the number of lines to include before and after the // selected line in the [Snippet]. func WithPadding(padding int) SnippetOption { return &paddingOption{padding: padding} } type paddingOption struct { padding int } func (o *paddingOption) ApplyToSnippet(s *Snippet) { s.padding = o.padding } // WithNoIndicators specifies that the [Snippet] should not include line or // column indicators. func WithNoIndicators() SnippetOption { return &noIndicatorsOption{} } type noIndicatorsOption struct{} func (o *noIndicatorsOption) ApplyToSnippet(s *Snippet) { s.noIndicators = true } func (s *Snippet) String() string { buf := &bytes.Buffer{} maxLineNumberDigits := digits(s.end) lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits) lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits) lineIndicatorSpacer := strings.Repeat(" ", len(lineIndicator)) columnSpacer := strings.Repeat(" ", max(s.column-1, 0)) // Loop over each line in the snippet for i, lineHighlighted := range s.linesHighlighted { if i > 0 { fmt.Fprintln(buf) } currentLine := s.start + i lineNumber := fmt.Sprintf(lineNumberFormat, currentLine) // If this is a padding line or indicators are disabled, print it as normal if currentLine != s.line || s.noIndicators { fmt.Fprintf(buf, "%s %s | %s", lineIndicatorSpacer, lineNumber, lineHighlighted) continue } // Otherwise, print the line with indicators fmt.Fprintf(buf, "%s %s | %s", color.RedString(lineIndicator), lineNumber, lineHighlighted) // Only print the column indicator if the column is in bounds if s.column > 0 && s.column <= len(s.linesRaw[i]) { fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) } } // If there are lines, but no line is selected, print the column indicator under all the lines if len(s.linesHighlighted) > 0 && s.line == 0 && s.column > 0 { fmt.Fprintf(buf, "\n%s %s | %s%s", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator)) } return buf.String() } func digits(number int) int { count := 0 for number != 0 { number /= 10 count += 1 } return count } ================================================ FILE: taskfile/snippet_test.go ================================================ package taskfile import ( "strings" "testing" "github.com/stretchr/testify/require" ) const sample = `version: 3 tasks: default: vars: FOO: foo BAR: bar cmds: - echo "{{.FOO}}" - echo "{{.BAR}}" ` func TestNewSnippet(t *testing.T) { t.Parallel() tests := []struct { name string b []byte opts []SnippetOption want *Snippet }{ { name: "first line, first column", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(1), }, want: &Snippet{ linesRaw: []string{ "version: 3", }, linesHighlighted: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, start: 1, end: 1, line: 1, column: 1, padding: 0, }, }, { name: "first line, first column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(1), WithPadding(2), }, want: &Snippet{ linesRaw: []string{ "version: 3", "", "tasks:", }, linesHighlighted: []string{ "\x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", "\x1b[1m\x1b[30m\x1b[0m", "\x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, start: 1, end: 3, line: 1, column: 1, padding: 2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := NewSnippet(tt.b, tt.opts...) require.Equal(t, tt.want, got) }) } } func TestSnippetString(t *testing.T) { t.Parallel() tests := []struct { name string b []byte opts []SnippetOption want string }{ { name: "empty", b: []byte{}, opts: []SnippetOption{ WithLine(1), WithColumn(1), }, want: "", }, { name: "0th line, 0th column (no indicators)", b: []byte(sample), want: "", }, { name: "1st line, 0th column (line indicator only)", b: []byte(sample), opts: []SnippetOption{ WithLine(1), }, want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "0th line, 1st column (column indicator only)", b: []byte(sample), opts: []SnippetOption{ WithColumn(1), }, want: "", }, { name: "0th line, 1st column, padding=2 (column indicator only)", b: []byte(sample), opts: []SnippetOption{ WithColumn(1), WithPadding(2), }, want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "1st line, 1st column", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(1), }, want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "1st line, 10th column", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(10), }, want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "1st line, 1st column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(1), WithPadding(2), }, want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "1st line, 10th column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(1), WithColumn(10), WithPadding(2), }, want: "> 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "5th line, 1st column", b: []byte(sample), opts: []SnippetOption{ WithLine(5), WithColumn(1), }, want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "5th line, 5th column", b: []byte(sample), opts: []SnippetOption{ WithLine(5), WithColumn(5), }, want: "> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "5th line, 5th column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(5), WithColumn(5), WithPadding(2), }, want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "5th line, 5th column, padding=2, no indicators", b: []byte(sample), opts: []SnippetOption{ WithLine(5), WithColumn(5), WithPadding(2), WithNoIndicators(), }, want: " 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "10th line, 1st column", b: []byte(sample), opts: []SnippetOption{ WithLine(10), WithColumn(1), }, want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "10th line, 23rd column", b: []byte(sample), opts: []SnippetOption{ WithLine(10), WithColumn(23), }, want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "10th line, 24th column (out of bounds)", b: []byte(sample), opts: []SnippetOption{ WithLine(10), WithColumn(24), }, want: "> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "10th line, 23rd column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(10), WithColumn(23), WithPadding(2), }, want: " 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^", }, { name: "5th line, 5th column, padding=100", b: []byte(sample), opts: []SnippetOption{ WithLine(5), WithColumn(5), WithPadding(100), }, want: " 1 | \x1b[33mversion\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36m3\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 2 | \x1b[1m\x1b[30m\x1b[0m\n 3 | \x1b[33mtasks\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 4 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mdefault\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n> 5 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mvars\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n | ^\n 6 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mFOO\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mfoo\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 7 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mBAR\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m \x1b[0m\x1b[36mbar\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 8 | \x1b[1m\x1b[30m \x1b[0m\x1b[33mcmds\x1b[0m\x1b[1m\x1b[30m:\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, { name: "11th line (out of bounds), 1st column", b: []byte(sample), opts: []SnippetOption{ WithLine(11), WithColumn(1), }, want: "", }, { name: "11th line (out of bounds), 1st column, padding=2", b: []byte(sample), opts: []SnippetOption{ WithLine(11), WithColumn(1), WithPadding(2), }, want: " 9 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.FOO}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m\n 10 | \x1b[1m\x1b[30m \x1b[0m\x1b[1m\x1b[30m- \x1b[0m\x1b[36mecho \"{{.BAR}}\"\x1b[0m\x1b[1m\x1b[30m\x1b[0m", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() snippet := NewSnippet(tt.b, tt.opts...) got := snippet.String() if strings.Contains(got, "\t") { t.Fatalf("tab character found in snippet - check the sample string") } require.Equal(t, tt.want, got) }) } } ================================================ FILE: taskfile/taskfile.go ================================================ package taskfile import ( "context" "fmt" "net/http" "net/url" "slices" "strings" "github.com/go-task/task/v3/errors" ) var ( // DefaultTaskfiles is the list of Taskfile file names supported by default. DefaultTaskfiles = []string{ "Taskfile.yml", "taskfile.yml", "Taskfile.yaml", "taskfile.yaml", "Taskfile.dist.yml", "taskfile.dist.yml", "Taskfile.dist.yaml", "taskfile.dist.yaml", } allowedContentTypes = []string{ "text/plain", "text/yaml", "text/x-yaml", "application/yaml", "application/x-yaml", "application/octet-stream", } ) // RemoteExists will check if a file at the given URL Exists. If it does, it // will return its URL. If it does not, it will search the search for any files // at the given URL with any of the default Taskfile files names. If any of // these match a file, the first matching path will be returned. If no files are // found, an error will be returned. func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) { // Create a new HEAD request for the given URL to check if the resource exists req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil) if err != nil { return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()} } // Request the given URL resp, err := client.Do(req) if err != nil { if ctx.Err() != nil { return nil, fmt.Errorf("checking remote file: %w", ctx.Err()) } return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()} } defer resp.Body.Close() // If the request was successful and the content type is allowed, return the // URL The content type check is to avoid downloading files that are not // Taskfiles It means we can try other files instead of downloading // something that is definitely not a Taskfile contentType := resp.Header.Get("Content-Type") if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool { return strings.Contains(contentType, s) }) { return &u, nil } // If the request was not successful, append the default Taskfile names to // the URL and return the URL of the first successful request for _, taskfile := range DefaultTaskfiles { // Fixes a bug with JoinPath where a leading slash is not added to the // path if it is empty if u.Path == "" { u.Path = "/" } alt := u.JoinPath(taskfile) req.URL = alt // Try the alternative URL resp, err = client.Do(req) if err != nil { return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()} } defer resp.Body.Close() // If the request was successful, return the URL if resp.StatusCode == http.StatusOK { return alt, nil } } return nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false} } ================================================ FILE: taskfile/templates/default.yml ================================================ # yaml-language-server: $schema=https://taskfile.dev/schema.json version: '3' vars: GREETING: Hello, world! tasks: default: desc: Print a greeting message cmds: - echo "{{.GREETING}}" silent: true ================================================ FILE: taskfile/themes/task.xml ================================================ ================================================ FILE: taskrc/ast/taskrc.go ================================================ package ast import ( "cmp" "maps" "slices" "time" "github.com/Masterminds/semver/v3" ) type TaskRC struct { Version *semver.Version `yaml:"version"` Verbose *bool `yaml:"verbose"` Silent *bool `yaml:"silent"` Color *bool `yaml:"color"` DisableFuzzy *bool `yaml:"disable-fuzzy"` Concurrency *int `yaml:"concurrency"` Interactive *bool `yaml:"interactive"` Remote Remote `yaml:"remote"` Failfast bool `yaml:"failfast"` Experiments map[string]int `yaml:"experiments"` } type Remote struct { Insecure *bool `yaml:"insecure"` Offline *bool `yaml:"offline"` Timeout *time.Duration `yaml:"timeout"` CacheExpiry *time.Duration `yaml:"cache-expiry"` CacheDir *string `yaml:"cache-dir"` TrustedHosts []string `yaml:"trusted-hosts"` CACert *string `yaml:"cacert"` Cert *string `yaml:"cert"` CertKey *string `yaml:"cert-key"` } // Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC. func (t *TaskRC) Merge(other *TaskRC) { if other == nil { return } t.Version = cmp.Or(other.Version, t.Version) if t.Experiments == nil && other.Experiments != nil { t.Experiments = other.Experiments } else if t.Experiments != nil && other.Experiments != nil { maps.Copy(t.Experiments, other.Experiments) } // Merge Remote fields t.Remote.Insecure = cmp.Or(other.Remote.Insecure, t.Remote.Insecure) t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline) t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout) t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry) t.Remote.CacheDir = cmp.Or(other.Remote.CacheDir, t.Remote.CacheDir) if len(other.Remote.TrustedHosts) > 0 { merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts) slices.Sort(merged) t.Remote.TrustedHosts = slices.Compact(merged) } t.Remote.CACert = cmp.Or(other.Remote.CACert, t.Remote.CACert) t.Remote.Cert = cmp.Or(other.Remote.Cert, t.Remote.Cert) t.Remote.CertKey = cmp.Or(other.Remote.CertKey, t.Remote.CertKey) t.Verbose = cmp.Or(other.Verbose, t.Verbose) t.Silent = cmp.Or(other.Silent, t.Silent) t.Color = cmp.Or(other.Color, t.Color) t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy) t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency) t.Interactive = cmp.Or(other.Interactive, t.Interactive) t.Failfast = cmp.Or(other.Failfast, t.Failfast) } ================================================ FILE: taskrc/node.go ================================================ package taskrc import ( "github.com/go-task/task/v3/internal/fsext" ) type Node struct { entrypoint string } func NewNode( entrypoint string, dir string, possibleFileNames []string, ) (*Node, error) { dir = fsext.DefaultDir(entrypoint, dir) resolvedEntrypoint, err := fsext.SearchPath(dir, possibleFileNames) if err != nil { return nil, err } return &Node{ entrypoint: resolvedEntrypoint, }, nil } ================================================ FILE: taskrc/reader.go ================================================ package taskrc import ( "os" "go.yaml.in/yaml/v3" "github.com/go-task/task/v3/taskrc/ast" ) type ( // DebugFunc is a function that can be called to log debug messages. DebugFunc func(string) // A ReaderOption is any type that can apply a configuration to a [Reader]. ReaderOption interface { ApplyToReader(*Reader) } // A Reader will recursively read Taskfiles from a given [Node] and build a // [ast.TaskRC] from them. Reader struct { debugFunc DebugFunc } ) // NewReader constructs a new Taskfile [Reader] using the given Node and // options. func NewReader(opts ...ReaderOption) *Reader { r := &Reader{ debugFunc: nil, } r.Options(opts...) return r } // Options loops through the given [ReaderOption] functions and applies them to // the [Reader]. func (r *Reader) Options(opts ...ReaderOption) { for _, opt := range opts { opt.ApplyToReader(r) } } // WithDebugFunc sets the debug function to be used by the [Reader]. If set, // this function will be called with debug messages. This can be useful if the // caller wants to log debug messages from the [Reader]. By default, no debug // function is set and the logs are not written. func WithDebugFunc(debugFunc DebugFunc) ReaderOption { return &debugFuncOption{debugFunc: debugFunc} } type debugFuncOption struct { debugFunc DebugFunc } func (o *debugFuncOption) ApplyToReader(r *Reader) { r.debugFunc = o.debugFunc } // Read will read the Task config defined by the [Reader]'s [Node]. func (r *Reader) Read(node *Node) (*ast.TaskRC, error) { var config ast.TaskRC if node == nil { return nil, os.ErrInvalid } // Read the file b, err := os.ReadFile(node.entrypoint) if err != nil { return nil, err } // Parse the content if err := yaml.Unmarshal(b, &config); err != nil { return nil, err } return &config, nil } ================================================ FILE: taskrc/taskrc.go ================================================ package taskrc import ( "os" "path/filepath" "slices" "strings" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/taskrc/ast" ) var ( defaultXDGTaskRCs = []string{ "taskrc.yml", "taskrc.yaml", } defaultTaskRCs = []string{ ".taskrc.yml", ".taskrc.yaml", } ) // GetConfig loads and merges local and global Task configuration files func GetConfig(dir string) (*ast.TaskRC, error) { var config *ast.TaskRC reader := NewReader() // Read the XDG config file if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { xdgConfigNode, err := NewNode("", filepath.Join(xdgConfigHome, "task"), defaultXDGTaskRCs) if err == nil && xdgConfigNode != nil { xdgConfig, err := reader.Read(xdgConfigNode) if err != nil { return nil, err } config = xdgConfig } } // If the current path does not contain $HOME // If it does contain $HOME, then we will find this config later anyway home, err := os.UserHomeDir() if err == nil && !strings.Contains(home, dir) { homeNode, err := NewNode("", home, defaultTaskRCs) if err == nil && homeNode != nil { homeConfig, err := reader.Read(homeNode) if err != nil { return nil, err } if config == nil { config = homeConfig } else { config.Merge(homeConfig) } } } // Find all the nodes from the given directory up to the users home directory absDir, err := filepath.Abs(dir) if err != nil { return config, err } entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs) if errors.Is(err, os.ErrPermission) { err = nil } if err != nil { return config, err } // Reverse the entrypoints since we want the child files to override parent ones slices.Reverse(entrypoints) // Loop over the nodes, and merge them into the main config for _, entrypoint := range entrypoints { node, err := NewNode("", entrypoint, defaultTaskRCs) if err != nil { return nil, err } localConfig, err := reader.Read(node) if err != nil { return nil, err } if localConfig == nil { continue } if config == nil { config = localConfig continue } config.Merge(localConfig) } return config, nil } ================================================ FILE: taskrc/taskrc_test.go ================================================ package taskrc import ( "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/go-task/task/v3/taskrc/ast" ) const ( xdgConfigYAML = ` experiments: FOO: 1 BAR: 1 BAZ: 1 ` homeConfigYAML = ` experiments: FOO: 2 BAR: 2 ` localConfigYAML = ` experiments: FOO: 3 ` ) func setupDirs(t *testing.T) (string, string, string) { t.Helper() xdgConfigDir := t.TempDir() xdgTaskConfigDir := filepath.Join(xdgConfigDir, "task") require.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755)) homeDir := t.TempDir() localDir := filepath.Join(homeDir, "local") require.NoError(t, os.Mkdir(localDir, 0o755)) t.Setenv("XDG_CONFIG_HOME", xdgConfigDir) t.Setenv("HOME", homeDir) return xdgTaskConfigDir, homeDir, localDir } func writeFile(t *testing.T, dir, filename, content string) { t.Helper() err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644) assert.NoError(t, err) } func TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel _, _, localDir := setupDirs(t) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.Nil(t, cfg) } func TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel xdgDir, _, localDir := setupDirs(t) writeFile(t, xdgDir, "taskrc.yml", xdgConfigYAML) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.Equal(t, &ast.TaskRC{ Version: nil, Experiments: map[string]int{ "FOO": 1, "BAR": 1, "BAZ": 1, }, }, cfg) } func TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel _, homeDir, localDir := setupDirs(t) writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.Equal(t, &ast.TaskRC{ Version: nil, Experiments: map[string]int{ "FOO": 2, "BAR": 2, }, }, cfg) } func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel _, _, localDir := setupDirs(t) writeFile(t, localDir, ".taskrc.yml", localConfigYAML) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.Equal(t, &ast.TaskRC{ Version: nil, Experiments: map[string]int{ "FOO": 3, }, }, cfg) } func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel xdgConfigDir, homeDir, localDir := setupDirs(t) // Write local config writeFile(t, localDir, ".taskrc.yml", localConfigYAML) // Write home config writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML) // Write XDG config writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfigYAML) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.NotNil(t, cfg) assert.Equal(t, &ast.TaskRC{ Version: nil, Experiments: map[string]int{ "FOO": 3, "BAR": 2, "BAZ": 1, }, }, cfg) } func TestGetConfig_RemoteTrustedHosts(t *testing.T) { //nolint:paralleltest // cannot run in parallel _, _, localDir := setupDirs(t) // Test with single host configYAML := ` remote: trusted-hosts: - github.com ` writeFile(t, localDir, ".taskrc.yml", configYAML) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.NotNil(t, cfg) assert.Equal(t, []string{"github.com"}, cfg.Remote.TrustedHosts) // Test with multiple hosts configYAML = ` remote: trusted-hosts: - github.com - gitlab.com - example.com:8080 ` writeFile(t, localDir, ".taskrc.yml", configYAML) cfg, err = GetConfig(localDir) assert.NoError(t, err) assert.NotNil(t, cfg) assert.Equal(t, []string{"github.com", "gitlab.com", "example.com:8080"}, cfg.Remote.TrustedHosts) } func TestGetConfig_RemoteTrustedHostsMerge(t *testing.T) { //nolint:paralleltest // cannot run in parallel t.Run("file-based merge precedence", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel xdgConfigDir, homeDir, localDir := setupDirs(t) // XDG config has github.com and gitlab.com xdgConfig := ` remote: trusted-hosts: - github.com - gitlab.com timeout: "30s" ` writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfig) // Home config has example.com (should be combined with XDG) homeConfig := ` remote: trusted-hosts: - example.com ` writeFile(t, homeDir, ".taskrc.yml", homeConfig) cfg, err := GetConfig(localDir) assert.NoError(t, err) assert.NotNil(t, cfg) // Home config entries come first, then XDG assert.Equal(t, []string{"example.com", "github.com", "gitlab.com"}, cfg.Remote.TrustedHosts) // Test with local config too localConfig := ` remote: trusted-hosts: - local.dev ` writeFile(t, localDir, ".taskrc.yml", localConfig) cfg, err = GetConfig(localDir) assert.NoError(t, err) assert.NotNil(t, cfg) // Local config entries come first assert.Equal(t, []string{"example.com", "github.com", "gitlab.com", "local.dev"}, cfg.Remote.TrustedHosts) }) t.Run("merge edge cases", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel tests := []struct { name string base *ast.TaskRC other *ast.TaskRC expected []string }{ { name: "merge hosts into empty", base: &ast.TaskRC{}, other: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{"github.com"}, }, }, expected: []string{"github.com"}, }, { name: "merge combines lists", base: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{"base.com"}, }, }, other: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{"other.com"}, }, }, expected: []string{"base.com", "other.com"}, }, { name: "merge empty list does not override", base: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{"base.com"}, }, }, other: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{}, }, }, expected: []string{"base.com"}, }, { name: "merge nil does not override", base: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: []string{"base.com"}, }, }, other: &ast.TaskRC{ Remote: ast.Remote{ TrustedHosts: nil, }, }, expected: []string{"base.com"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel tt.base.Merge(tt.other) assert.Equal(t, tt.expected, tt.base.Remote.TrustedHosts) }) } }) t.Run("all remote fields merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel insecureTrue := true offlineTrue := true timeout := 30 * time.Second cacheExpiry := 1 * time.Hour base := &ast.TaskRC{} other := &ast.TaskRC{ Remote: ast.Remote{ Insecure: &insecureTrue, Offline: &offlineTrue, Timeout: &timeout, CacheExpiry: &cacheExpiry, TrustedHosts: []string{"github.com", "gitlab.com"}, }, } base.Merge(other) assert.Equal(t, &insecureTrue, base.Remote.Insecure) assert.Equal(t, &offlineTrue, base.Remote.Offline) assert.Equal(t, &timeout, base.Remote.Timeout) assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry) assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts) }) } ================================================ FILE: testdata/alias/Taskfile.yml ================================================ version: '3' includes: included: taskfile: Taskfile2.yml aliases: [inc, i] tasks: foo: aliases: [f, x] cmds: - echo "foo" - task: b bar: aliases: [b, x] cmds: - echo "bar" - task: inc:q ================================================ FILE: testdata/alias/Taskfile2.yml ================================================ version: '3' tasks: qux: aliases: [q, x] cmds: - echo "qux" ================================================ FILE: testdata/alias/testdata/TestAlias-alias.golden ================================================ task: [foo] echo "foo" foo task: [bar] echo "bar" bar task: [included:qux] echo "qux" qux ================================================ FILE: testdata/alias/testdata/TestAlias-alias_summary.golden ================================================ task: foo (task does not have description or summary) aliases: - f - x commands: - echo "foo" - Task: b ================================================ FILE: testdata/alias/testdata/TestAlias-duplicate_alias-err-run.golden ================================================ task: Found multiple tasks (foo, bar) that match "x" ================================================ FILE: testdata/alias/testdata/TestAlias-duplicate_alias.golden ================================================ ================================================ FILE: testdata/checksum/.gitignore ================================================ .task/ generated.txt ================================================ FILE: testdata/checksum/Taskfile.yml ================================================ version: '3' tasks: build: cmds: - cp ./source.txt ./generated.txt sources: - ./**/glob-with-inexistent-file.txt - ./*.txt - exclude: ./ignore_me.txt - exclude: ./generated.txt generates: - ./generated.txt method: checksum build-*: cmds: - cp ./source.txt ./generated-{{index .MATCH 0}}.txt sources: - ./source.txt generates: - ./generated-{{index .MATCH 0}}.txt method: checksum build-with-status: cmds: - cp ./source.txt ./generated.txt sources: - ./source.txt status: - test -f ./generated.txt ================================================ FILE: testdata/checksum/generated-wildcard.txt ================================================ Hello, World! ================================================ FILE: testdata/checksum/ignore_me.txt ================================================ plz ignore me ================================================ FILE: testdata/checksum/source.txt ================================================ Hello, World! ================================================ FILE: testdata/cmds_vars/Taskfile.yml ================================================ version: '3' tasks: build-checksum: sources: - ./source.txt cmds: - echo "{{.CHECKSUM}}" build-ts: method: timestamp sources: - ./source.txt cmds: - echo '{{.TIMESTAMP.Unix}}' - echo '{{.TIMESTAMP}}' ================================================ FILE: testdata/cmds_vars/source.txt ================================================ Hello, World! ================================================ FILE: testdata/concurrency/Taskfile.yml ================================================ version: "3" tasks: default: deps: - t1 t1: deps: - t3 - t4 cmds: - task: t2 - echo done 1 t2: deps: - t5 - t6 cmds: - echo done 2 t3: cmds: - echo done 3 t4: cmds: - echo done 4 t5: cmds: - echo done 5 t6: cmds: - echo done 6 ================================================ FILE: testdata/concurrency/testdata/TestConcurrency.golden ================================================ done 1 done 2 done 3 done 4 done 5 done 6 task: [t1] echo done 1 task: [t2] echo done 2 task: [t3] echo done 3 task: [t4] echo done 4 task: [t5] echo done 5 task: [t6] echo done 6 ================================================ FILE: testdata/cyclic/Taskfile.yml ================================================ version: '3' tasks: task-1: deps: - task: task-2 task-2: deps: - task: task-1 ================================================ FILE: testdata/deferred/Taskfile.yml ================================================ version: '3' tasks: task-1: - echo 'task-1 ran {{.PARAM}}' task-2: - defer: { task: 'task-1', vars: { PARAM: 'successfully' } } - defer: { task: 'task-1', vars: { PARAM: 'successfully' }, silent: true } - defer: echo 'echo ran' silent: true - defer: echo 'failing' && exit 2 - echo 'cmd ran' - exit 1 parent: vars: VAR1: "value-from-parent" cmds: - defer: task: child vars: VAR1: 'task deferred {{.VAR1}}' - task: child vars: VAR1: 'task immediate {{.VAR1}}' child: cmds: - cmd: echo "child {{.VAR1}}" ================================================ FILE: testdata/deps/Taskfile.yml ================================================ version: '3' tasks: default: deps: [d1, d2, d3] d1: deps: [d11, d12, d13] cmds: - echo 'd1' d2: deps: [d21, d22, d23] cmds: - echo 'd2' d3: deps: [d31, d32, d33] cmds: - echo 'd3' d11: cmds: - echo 'd11' d12: cmds: - echo 'd12' d13: cmds: - echo 'd13' d21: cmds: - echo 'd21' d22: cmds: - echo 'd22' d23: cmds: - echo 'd23' d31: cmds: - echo 'd31' d32: cmds: - echo 'd32' d33: cmds: - echo 'd33' ================================================ FILE: testdata/deps/d1.txt ================================================ Text ================================================ FILE: testdata/deps/d11.txt ================================================ Text ================================================ FILE: testdata/deps/d12.txt ================================================ Text ================================================ FILE: testdata/deps/d13.txt ================================================ Text ================================================ FILE: testdata/deps/d2.txt ================================================ Text ================================================ FILE: testdata/deps/d21.txt ================================================ Text ================================================ FILE: testdata/deps/d22.txt ================================================ Text ================================================ FILE: testdata/deps/d23.txt ================================================ Text ================================================ FILE: testdata/deps/d3.txt ================================================ Text ================================================ FILE: testdata/deps/d31.txt ================================================ Text ================================================ FILE: testdata/deps/d32.txt ================================================ Text ================================================ FILE: testdata/deps/d33.txt ================================================ Text ================================================ FILE: testdata/deps/testdata/TestDeps.golden ================================================ d1 d11 d12 d13 d2 d21 d22 d23 d3 d31 d32 d33 ================================================ FILE: testdata/desc/Taskfile.yml ================================================ version: 3 tasks: build: aliases: - b desc: | Multi-line escription with alias which is super long long long long long long another line third line long long long long long long long long test: aliases: - t desc: Single line description with alias ================================================ FILE: testdata/dir/Taskfile.yml ================================================ version: '3' tasks: whereami: cmds: - pwd silent: true ================================================ FILE: testdata/dir/dynamic_var/.gitignore ================================================ *.txt ================================================ FILE: testdata/dir/dynamic_var/Taskfile.yml ================================================ version: '3' includes: sub: taskfile: subdirectory dir: subdirectory vars: DIRECTORY: subdirectory tasks: default: - task: from-root-taskfile - task: sub:from-included-taskfile - task: sub:from-included-taskfile-task - task: from-interpolated-dir from-root-taskfile: cmds: - echo '{{.TASK_DIR}}' > from_root_taskfile.txt dir: subdirectory vars: TASK_DIR: sh: basename $(pwd) silent: true from-interpolated-dir: cmds: - echo '{{.INTERPOLATED_DIR}}' > from_interpolated_dir.txt dir: '{{.DIRECTORY}}' vars: INTERPOLATED_DIR: sh: basename $(pwd) ================================================ FILE: testdata/dir/dynamic_var/subdirectory/Taskfile.yml ================================================ version: '3' vars: TASKFILE_DIR: sh: basename $(pwd) tasks: from-included-taskfile: cmds: - echo '{{.TASKFILE_DIR}}' > from_included_taskfile.txt silent: true from-included-taskfile-task: cmds: - echo '{{.TASKFILE_TASK_DIR}}' > from_included_taskfile_task.txt silent: true vars: TASKFILE_TASK_DIR: sh: basename $(pwd) ================================================ FILE: testdata/dir/dynamic_var_on_created_dir/Taskfile.yml ================================================ version: '3' tasks: default: dir: created cmds: - echo {{.TASK_DIR}} vars: TASK_DIR: sh: echo $(pwd) ================================================ FILE: testdata/dir/explicit_doesnt_exist/Taskfile.yml ================================================ version: '3' tasks: whereami: dir: createme cmds: - pwd silent: true ================================================ FILE: testdata/dir/explicit_exists/Taskfile.yml ================================================ version: '3' tasks: whereami: dir: exists cmds: - pwd silent: true ================================================ FILE: testdata/dir/explicit_exists/exists/.keep ================================================ ================================================ FILE: testdata/dotenv/.gitignore ================================================ *.txt ================================================ FILE: testdata/dotenv/default/Taskfile.yml ================================================ version: '3' dotenv: ['../include1/.env', '../include1/envs/.env'] tasks: default: cmds: - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include.txt ================================================ FILE: testdata/dotenv/env_var_in_path/Taskfile.yml ================================================ version: "3" dotenv: [".env.{{.ENV_VAR}}"] tasks: default: cmds: - echo "VAR='$VAR_IN_DOTENV'" > var.txt ================================================ FILE: testdata/dotenv/error_included_envs/Taskfile.yml ================================================ version: '3' includes: include1: '../include1' tasks: default: cmds: - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include-errors2.txt ================================================ FILE: testdata/dotenv/include1/Taskfile.yml ================================================ version: '3' dotenv: ['.env'] ================================================ FILE: testdata/dotenv/local_env_in_path/Taskfile.yml ================================================ version: "3" env: LOCAL_ENV: testing dotenv: [".env.{{.LOCAL_ENV}}"] tasks: default: cmds: - echo "VAR='$VAR_IN_DOTENV'" > var.txt ================================================ FILE: testdata/dotenv/local_var_in_path/Taskfile.yml ================================================ version: "3" vars: PART_1: test PART_2: ing LOCAL_VAR: "{{.PART_1}}{{.PART_2}}" dotenv: [".env.{{.LOCAL_VAR}}"] tasks: default: cmds: - echo "VAR='$VAR_IN_DOTENV'" > var.txt ================================================ FILE: testdata/dotenv/missing_env/Taskfile.yml ================================================ version: '3' dotenv: ['.env'] tasks: default: cmds: - echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include.txt ================================================ FILE: testdata/dotenv/parse_error/.env-with-error ================================================ #intentional parse error SOME_VAR ================================================ FILE: testdata/dotenv/parse_error/Taskfile.yml ================================================ version: '3' dotenv: ['.env-with-error'] tasks: default: cmd: "true" ================================================ FILE: testdata/dotenv_task/default/.gitignore ================================================ *.txt ================================================ FILE: testdata/dotenv_task/default/Taskfile.yml ================================================ version: '3' env: FOO: global tasks: dotenv: dotenv: ['.env'] cmds: - echo "$FOO" > dotenv.txt dotenv-overridden-by-env: dotenv: ['.env'] env: FOO: overridden cmds: - echo "$FOO" > dotenv-overridden-by-env.txt dotenv-with-var-name: vars: DOTENV: .env dotenv: ['{{.DOTENV}}'] cmds: - echo "$FOO" > dotenv-with-var-name.txt no-dotenv: cmds: - echo "$FOO" > no-dotenv.txt ================================================ FILE: testdata/dry/Taskfile.yml ================================================ version: '3' tasks: build: cmds: - touch file.txt ================================================ FILE: testdata/dry_checksum/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - echo "Working..." sources: - source.txt method: checksum ================================================ FILE: testdata/dry_checksum/source.txt ================================================ Something... ================================================ FILE: testdata/empty_task/Taskfile.yml ================================================ version: '3' tasks: default: ================================================ FILE: testdata/empty_task/testdata/TestEmptyTask.golden ================================================ ================================================ FILE: testdata/empty_taskfile/Taskfile.yml ================================================ ================================================ FILE: testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-setup.golden ================================================ task: Missing schema version in Taskfile "{{.TEST_DIR}}/testdata/empty_taskfile/Taskfile.yml" ================================================ FILE: testdata/empty_taskfile/testdata/TestEmptyTaskfile.golden ================================================ ================================================ FILE: testdata/env/Taskfile.yml ================================================ version: '3' vars: BAZ: sh: echo baz env: FOO: foo BAR: bar BAZ: "{{.BAZ}}" QUX: from_taskfile tasks: default: cmds: - task: local - task: global - task: not-overridden - task: multiple_type - task: dynamic local: vars: AMD64: amd64 env: GOOS: linux GOARCH: "{{.AMD64}}" CGO_ENABLED: sh: echo '0' cmds: - echo "GOOS='$GOOS' GOARCH='$GOARCH' CGO_ENABLED='$CGO_ENABLED'" global: env: BAR: overridden cmds: - echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" multiple_type: env: FOO: 1 BAR: true BAZ: 1.1 cmds: - echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" not-overridden: cmds: - echo "QUX='$QUX'" overridden: cmds: - echo "QUX='$QUX'" dynamic: silent: true vars: DYNAMIC_FOO: sh: echo $FOO cmds: - echo "{{ .DYNAMIC_FOO }}" ================================================ FILE: testdata/env/dynamic.txt ================================================ foo ================================================ FILE: testdata/env/global.txt ================================================ FOO='foo' BAR='overridden' BAZ='baz' ================================================ FILE: testdata/env/local.txt ================================================ GOOS='linux' GOARCH='amd64' CGO_ENABLED='0' ================================================ FILE: testdata/env/multiple_type.txt ================================================ FOO='1' BAR='true' BAZ='1.1' ================================================ FILE: testdata/env/not-overridden.txt ================================================ QUX='from_os' ================================================ FILE: testdata/env/overridden.txt ================================================ QUX='from_taskfile' ================================================ FILE: testdata/env/testdata/TestEnv-env_precedence_disabled.golden ================================================ GOOS='linux' GOARCH='amd64' CGO_ENABLED='0' FOO='foo' BAR='overridden' BAZ='baz' QUX='from_os' FOO='1' BAR='true' BAZ='1.1' foo ================================================ FILE: testdata/env/testdata/TestEnv-env_precedence_enabled.golden ================================================ GOOS='linux' GOARCH='amd64' CGO_ENABLED='0' FOO='foo' BAR='overridden' BAZ='baz' QUX='from_taskfile' FOO='1' BAR='true' BAZ='1.1' foo ================================================ FILE: testdata/error_code/Taskfile.yml ================================================ version: '3' tasks: direct: cmds: - exit 42 indirect: cmds: - task: direct ================================================ FILE: testdata/evaluate_symlinks_in_paths/Taskfile.yaml ================================================ version: '3' tasks: default: sources: - src/**/* cmds: - echo "some job" test-sym: cmds: - echo "shared file source changed" > src/shared/b reset: cmds: - echo "shared file source" > src/shared/b - echo "file source" > src/a ================================================ FILE: testdata/evaluate_symlinks_in_paths/shared/b ================================================ shared file source ================================================ FILE: testdata/evaluate_symlinks_in_paths/shared/inner_shared/c ================================================ inner shared file source ================================================ FILE: testdata/evaluate_symlinks_in_paths/src/a ================================================ file source ================================================ FILE: testdata/exit_code/Taskfile.yml ================================================ version: '3' silent: true vars: PREFIX: EXIT_CODE= tasks: exit-zero: vars: FOO: bar DYNAMIC_FOO: sh: echo 'bar' cmds: - defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}} - exit 0 exit-one: vars: FOO: bar DYNAMIC_FOO: sh: echo 'bar' cmds: - defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}} - exit 1 ================================================ FILE: testdata/exit_immediately/Taskfile.yml ================================================ version: '3' tasks: default: | this_should_fail echo "This shouldn't be print" ================================================ FILE: testdata/expand/Taskfile.yml ================================================ version: '3' tasks: pwd: cmds: - pwd dir: '~' silent: true ================================================ FILE: testdata/failfast/default/Taskfile.yaml ================================================ version: '3' tasks: default: deps: - dep1 - dep2 - dep3 - dep4 dep1: sleep 0.1 && echo 'dep1' dep2: sleep 0.2 && echo 'dep2' dep3: sleep 0.3 && echo 'dep3' dep4: exit 1 ================================================ FILE: testdata/failfast/default/testdata/TestFailfast-Default-default-err-run.golden ================================================ task: Failed to run task "default": task: Failed to run task "dep4": exit status 1 ================================================ FILE: testdata/failfast/default/testdata/TestFailfast-Default-default.golden ================================================ dep1 dep2 dep3 ================================================ FILE: testdata/failfast/default/testdata/TestFailfast-Option-default-err-run.golden ================================================ task: Failed to run task "default": task: Failed to run task "dep4": exit status 1 ================================================ FILE: testdata/failfast/default/testdata/TestFailfast-Option-default.golden ================================================ ================================================ FILE: testdata/failfast/task/Taskfile.yaml ================================================ version: '3' tasks: default: deps: - dep1 - dep2 - dep3 - dep4 failfast: true dep1: sleep 0.1 && echo 'dep1' dep2: sleep 0.2 && echo 'dep2' dep3: sleep 0.3 && echo 'dep3' dep4: exit 1 ================================================ FILE: testdata/failfast/task/testdata/TestFailfast-Task-task-err-run.golden ================================================ task: Failed to run task "default": task: Failed to run task "dep4": exit status 1 ================================================ FILE: testdata/failfast/task/testdata/TestFailfast-Task-task.golden ================================================ ================================================ FILE: testdata/file_names/.gitignore ================================================ *.txt ================================================ FILE: testdata/file_names/Taskfile.dist.yaml/Taskfile.dist.yaml ================================================ version: '3' tasks: default: echo "hello" > output.txt ================================================ FILE: testdata/file_names/Taskfile.dist.yml/Taskfile.dist.yml ================================================ version: '3' tasks: default: echo "hello" > output.txt ================================================ FILE: testdata/file_names/Taskfile.yaml/Taskfile.yaml ================================================ version: '3' tasks: default: echo "hello" > output.txt ================================================ FILE: testdata/file_names/Taskfile.yml/Taskfile.yml ================================================ version: '3' tasks: default: echo "hello" > output.txt ================================================ FILE: testdata/for/cmds/Taskfile.yml ================================================ version: "3" vars: OS_VAR: ["windows", "linux", "darwin"] ARCH_VAR: ["amd64", "arm64"] NOT_A_LIST: "not a list" tasks: # Loop over a list of values loop-explicit: cmds: - for: ["a", "b", "c"] cmd: echo "{{.ITEM}}" loop-matrix: cmds: - for: matrix: OS: ["windows", "linux", "darwin"] ARCH: ["amd64", "arm64"] cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" loop-matrix-ref: cmds: - for: matrix: OS: ref: .OS_VAR ARCH: ref: .ARCH_VAR cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" loop-matrix-ref-error: cmds: - for: matrix: OS: ref: .OS_VAR ARCH: ref: .NOT_A_LIST cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" # Loop over the task's sources loop-sources: sources: - foo.txt - bar.txt cmds: - for: sources cmd: cat "{{.ITEM}}" # Loop over the task's sources when globbed loop-sources-glob: sources: - "*.txt" cmds: - for: sources cmd: cat "{{.ITEM}}" # Loop over the task's generates loop-generates: generates: - foo.txt - bar.txt cmds: - for: generates cmd: cat "{{.ITEM}}" # Loop over the task's generates when globbed loop-generates-glob: generates: - "*.txt" cmds: - for: generates cmd: cat "{{.ITEM}}" # Loop over the contents of a variable loop-vars: vars: FOO: foo.txt,bar.txt cmds: - for: var: FOO split: "," cmd: cat "{{.ITEM}}" # Loop over the output of a command (auto splits on " ") loop-vars-sh: vars: FOO: sh: ls *.txt cmds: - for: var: FOO cmd: cat "{{.ITEM}}" # Loop over another task loop-task: vars: FOO: foo.txt bar.txt cmds: - for: var: FOO task: looped-task vars: FILE: "{{.ITEM}}" # Loop over another task with the variable named differently loop-task-as: vars: FOO: foo.txt bar.txt cmds: - for: var: FOO as: FILE task: looped-task vars: FILE: "{{.FILE}}" # Loop over different tasks using the variable loop-different-tasks: vars: FOO: "1 2 3" cmds: - for: var: FOO task: task-{{.ITEM}} looped-task: internal: true cmd: cat "{{.FILE}}" task-1: internal: true cmd: echo "1" task-2: internal: true cmd: echo "2" task-3: internal: true cmd: echo "3" ================================================ FILE: testdata/for/cmds/bar.txt ================================================ bar ================================================ FILE: testdata/for/cmds/foo.txt ================================================ foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-different-tasks.golden ================================================ 1 2 3 ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-explicit.golden ================================================ a b c ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-generates-glob.golden ================================================ bar foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-generates.golden ================================================ bar foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-error-err-run.golden ================================================ task: Failed to parse {{.TEST_DIR}}/testdata/for/cmds/Taskfile.yml: matrix reference ".NOT_A_LIST" must resolve to a list ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-error.golden ================================================ ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref.golden ================================================ windows/amd64 windows/arm64 linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-matrix.golden ================================================ windows/amd64 windows/arm64 linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-sources-glob.golden ================================================ bar foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-sources.golden ================================================ bar foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-task-as.golden ================================================ foo bar ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-task.golden ================================================ foo bar ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-vars-sh.golden ================================================ bar foo ================================================ FILE: testdata/for/cmds/testdata/TestForCmds-loop-vars.golden ================================================ foo bar ================================================ FILE: testdata/for/deps/Taskfile.yml ================================================ version: "3" vars: OS_VAR: ["windows", "linux", "darwin"] ARCH_VAR: ["amd64", "arm64"] NOT_A_LIST: "not a list" tasks: # Loop over a list of values loop-explicit: deps: - for: ["a", "b", "c"] task: echo vars: TEXT: "{{.ITEM}}" loop-matrix: deps: - for: matrix: OS: ["windows", "linux", "darwin"] ARCH: ["amd64", "arm64"] task: echo vars: TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}" loop-matrix-ref: deps: - for: matrix: OS: ref: .OS_VAR ARCH: ref: .ARCH_VAR task: echo vars: TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}" loop-matrix-ref-error: deps: - for: matrix: OS: ref: .OS_VAR ARCH: ref: .NOT_A_LIST task: echo vars: TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}" # Loop over the task's sources loop-sources: sources: - foo.txt - bar.txt deps: - for: sources task: cat vars: FILE: "{{.ITEM}}" # Loop over the task's sources when globbed loop-sources-glob: sources: - "*.txt" deps: - for: sources task: cat vars: FILE: "{{.ITEM}}" # Loop over the task's generates loop-generates: generates: - foo.txt - bar.txt deps: - for: generates task: cat vars: FILE: "{{.ITEM}}" # Loop over the task's generates when globbed loop-generates-glob: generates: - "*.txt" deps: - for: generates task: cat vars: FILE: "{{.ITEM}}" # Loop over the contents of a variable loop-vars: vars: FOO: foo.txt,bar.txt deps: - for: var: FOO split: "," task: cat vars: FILE: "{{.ITEM}}" # Loop over the output of a command (auto splits on " ") loop-vars-sh: vars: FOO: sh: ls *.txt deps: - for: var: FOO task: cat vars: FILE: "{{.ITEM}}" # Loop over another task loop-task: vars: FOO: foo.txt bar.txt deps: - for: var: FOO task: looped-task vars: FILE: "{{.ITEM}}" # Loop over another task with the variable named differently loop-task-as: vars: FOO: foo.txt bar.txt deps: - for: var: FOO as: FILE task: looped-task vars: FILE: "{{.FILE}}" # Loop over different tasks using the variable loop-different-tasks: vars: FOO: "1 2 3" deps: - for: var: FOO task: task-{{.ITEM}} looped-task: internal: true cmd: cat "{{.FILE}}" task-1: internal: true cmd: echo "1" task-2: internal: true cmd: echo "2" task-3: internal: true cmd: echo "3" echo: cmds: - echo "{{.TEXT}}" cat: cmds: - cat "{{.FILE}}" ================================================ FILE: testdata/for/deps/bar.txt ================================================ bar ================================================ FILE: testdata/for/deps/foo.txt ================================================ foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-different-tasks.golden ================================================ 1 2 3 ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-explicit.golden ================================================ a b c ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-generates-glob.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-generates.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-matrix-ref-error-err-run.golden ================================================ matrix reference ".NOT_A_LIST" must resolve to a list task: Failed to parse {{.TEST_DIR}}/testdata/for/deps/Taskfile.yml: ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-matrix-ref-error.golden ================================================ ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-matrix-ref.golden ================================================ darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64 ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-matrix.golden ================================================ darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64 ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-sources-glob.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-sources.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-task-as.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-task.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-vars-sh.golden ================================================ bar foo ================================================ FILE: testdata/for/deps/testdata/TestForDeps-loop-vars.golden ================================================ bar foo ================================================ FILE: testdata/force/Taskfile.yml ================================================ version: "3" tasks: task-with-dep: status: [ test true ] deps: [ indirect ] cmds: - echo "direct" task-with-subtask: status: [ test true ] cmds: - task: indirect - echo "direct" indirect: status: [ test true ] cmds: - echo "indirect" ================================================ FILE: testdata/fuzzy/Taskfile.yml ================================================ version: 3 tasks: install: echo 'install' internal: internal: true cmds: - echo "internal" ================================================ FILE: testdata/fuzzy/testdata/TestFuzzyModel-fuzzy-err-run.golden ================================================ task: Task "instal" does not exist. Did you mean "install"? ================================================ FILE: testdata/fuzzy/testdata/TestFuzzyModel-fuzzy.golden ================================================ task: No tasks with description available. Try --list-all to list all tasks ================================================ FILE: testdata/fuzzy/testdata/TestFuzzyModel-intern-err-run.golden ================================================ task: Task "intern" does not exist ================================================ FILE: testdata/fuzzy/testdata/TestFuzzyModel-intern.golden ================================================ task: No tasks with description available. Try --list-all to list all tasks ================================================ FILE: testdata/fuzzy/testdata/TestFuzzyModel-not-fuzzy.golden ================================================ task: [install] echo 'install' install ================================================ FILE: testdata/generates/.gitignore ================================================ *.txt ================================================ FILE: testdata/generates/Taskfile.yml ================================================ version: '3' vars: BUILD_DIR: sh: pwd tasks: abs.txt: desc: generates dest file based on absolute paths deps: - sub/src.txt dir: sub cmds: - cat src.txt > '{{.BUILD_DIR}}/abs.txt' method: timestamp sources: - src.txt generates: - "{{.BUILD_DIR}}/abs.txt" rel.txt: desc: generates dest file based on relative paths deps: - sub/src.txt dir: sub cmds: - cat src.txt > '../rel.txt' method: timestamp sources: - src.txt generates: - "../rel.txt" sub/src.txt: desc: generate source file cmds: - mkdir -p sub - echo "hello world" > sub/src.txt method: timestamp status: - test -f sub/src.txt 'my text file.txt': desc: generate file with spaces in the name deps: [sub/src.txt] cmds: - cat sub/src.txt > 'my text file.txt' method: timestamp sources: - sub/src.txt generates: - 'my text file.txt' ================================================ FILE: testdata/generates/sub/.keep ================================================ ================================================ FILE: testdata/if/Taskfile.yml ================================================ version: '3' vars: SHOULD_RUN: "yes" ENV: "prod" FEATURE_ENABLED: "true" FEATURE_DISABLED: "false" tasks: # Basic command-level if (condition met) cmd-if-true: cmds: - cmd: echo "executed" if: "true" # Basic command-level if (condition not met) cmd-if-false: cmds: - cmd: echo "should not appear" if: "false" - echo "this runs" # Task-level if (condition met) task-if-true: if: "true" cmds: - echo "task executed" # Task-level if (condition not met) task-if-false: if: "false" cmds: - echo "should not appear" # With template variables if-with-template: cmds: - cmd: echo "Running because SHOULD_RUN={{.SHOULD_RUN}}" if: '[ "{{.SHOULD_RUN}}" = "yes" ]' # If inside for loop if-in-for-loop: cmds: - for: ["a", "b", "c"] cmd: echo "processing {{.ITEM}}" if: '[ "{{.ITEM}}" != "b" ]' # If on task call if-on-task-call: cmds: - task: subtask if: "true" subtask: internal: true cmds: - echo "subtask ran" # If combined with platforms (both must pass) if-with-platforms: cmds: - cmd: echo "condition and platform met" platforms: [linux, darwin, windows] if: "true" # Skip task call skip-task-call: cmds: - task: subtask if: "false" - echo "after skipped task call" # Task call in cmds with if condition met task-call-if-true: cmds: - task: subtask if: "true" - echo "after task call" # Task call in cmds with if condition not met task-call-if-false: cmds: - task: subtask if: "false" - echo "continues after skipped task" # Template eq - condition met template-eq-true: cmds: - cmd: echo "env is prod" if: '{{ eq .ENV "prod" }}' # Template eq - condition not met template-eq-false: cmds: - cmd: echo "should not appear" if: '{{ eq .ENV "dev" }}' - echo "this runs" # Template ne (not equal) template-ne: cmds: - cmd: echo "env is not dev" if: '{{ ne .ENV "dev" }}' # Template with boolean-like variable template-bool-true: cmds: - cmd: echo "feature enabled" if: '{{ eq .FEATURE_ENABLED "true" }}' # Template with boolean-like variable (false) template-bool-false: cmds: - cmd: echo "should not appear" if: '{{ eq .FEATURE_DISABLED "true" }}' - echo "feature was disabled" # Direct true/false from template template-direct-true: cmds: - cmd: echo "direct true works" if: '{{ .FEATURE_ENABLED }}' # Direct true/false from template (false case) template-direct-false: cmds: - cmd: echo "should not appear" if: '{{ .FEATURE_DISABLED }}' - echo "direct false skipped correctly" # Template with CLI variable override template-cli-var: cmds: - cmd: echo "MY_VAR is yes" if: '{{ eq .MY_VAR "yes" }}' # Combined template conditions with and template-and: cmds: - cmd: echo "both conditions met" if: '{{ and (eq .ENV "prod") (eq .FEATURE_ENABLED "true") }}' # Combined template conditions with or template-or: cmds: - cmd: echo "at least one condition met" if: '{{ or (eq .ENV "dev") (eq .ENV "prod") }}' # Task-level if with template task-level-template: if: '{{ eq .ENV "prod" }}' cmds: - echo "task runs in prod" # Task-level if with template (not met) task-level-template-false: if: '{{ eq .ENV "dev" }}' cmds: - echo "should not appear" # Task-level if with dynamic variable (condition met) task-if-dynamic-true: vars: ENABLE_FEATURE: sh: 'echo "true"' if: '{{ eq .ENABLE_FEATURE "true" }}' cmds: - echo "dynamic feature enabled" # Task-level if with dynamic variable (condition not met) task-if-dynamic-false: vars: ENABLE_FEATURE: sh: 'echo "false"' if: '{{ eq .ENABLE_FEATURE "true" }}' cmds: - echo "should not appear" ================================================ FILE: testdata/if/testdata/TestIf-cmd-if-false.golden ================================================ this runs ================================================ FILE: testdata/if/testdata/TestIf-cmd-if-true.golden ================================================ executed ================================================ FILE: testdata/if/testdata/TestIf-if-in-for-loop.golden ================================================ task: "if-in-for-loop" started task: [if-in-for-loop] echo "processing a" processing a task: [if-in-for-loop] if condition not met - skipped task: [if-in-for-loop] echo "processing c" processing c task: "if-in-for-loop" finished ================================================ FILE: testdata/if/testdata/TestIf-task-call-if-false.golden ================================================ task: "task-call-if-false" started task: [task-call-if-false] if condition not met - skipped task: [task-call-if-false] echo "continues after skipped task" continues after skipped task task: "task-call-if-false" finished ================================================ FILE: testdata/if/testdata/TestIf-task-call-if-true.golden ================================================ subtask ran after task call ================================================ FILE: testdata/if/testdata/TestIf-task-if-dynamic-false.golden ================================================ task: dynamic variable: "echo \"false\"" result: "false" task: if condition not met - skipped: "task-if-dynamic-false" ================================================ FILE: testdata/if/testdata/TestIf-task-if-dynamic-true.golden ================================================ dynamic feature enabled ================================================ FILE: testdata/if/testdata/TestIf-task-if-false.golden ================================================ task: if condition not met - skipped: "task-if-false" ================================================ FILE: testdata/if/testdata/TestIf-task-if-true.golden ================================================ task executed ================================================ FILE: testdata/if/testdata/TestIf-task-level-template-false.golden ================================================ task: if condition not met - skipped: "task-level-template-false" ================================================ FILE: testdata/if/testdata/TestIf-task-level-template.golden ================================================ task runs in prod ================================================ FILE: testdata/if/testdata/TestIf-template-and.golden ================================================ both conditions met ================================================ FILE: testdata/if/testdata/TestIf-template-bool-false.golden ================================================ feature was disabled ================================================ FILE: testdata/if/testdata/TestIf-template-bool-true.golden ================================================ feature enabled ================================================ FILE: testdata/if/testdata/TestIf-template-cli-var.golden ================================================ MY_VAR is yes ================================================ FILE: testdata/if/testdata/TestIf-template-direct-false.golden ================================================ direct false skipped correctly ================================================ FILE: testdata/if/testdata/TestIf-template-direct-true.golden ================================================ direct true works ================================================ FILE: testdata/if/testdata/TestIf-template-eq-false.golden ================================================ task: "template-eq-false" started task: [template-eq-false] if condition not met - skipped task: [template-eq-false] echo "this runs" this runs task: "template-eq-false" finished ================================================ FILE: testdata/if/testdata/TestIf-template-eq-true.golden ================================================ env is prod ================================================ FILE: testdata/if/testdata/TestIf-template-ne.golden ================================================ env is not dev ================================================ FILE: testdata/if/testdata/TestIf-template-or.golden ================================================ at least one condition met ================================================ FILE: testdata/ignore_errors/Taskfile.yml ================================================ version: '3' tasks: task-should-pass: cmds: - exit 1 ignore_error: true task-should-fail: cmds: - exit 1 cmd-should-pass: cmds: - cmd: exit 1 ignore_error: true cmd-should-fail: cmds: - cmd: exit 1 ================================================ FILE: testdata/ignore_nil_elements/cmds/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - echo "string-slice-1" - ================================================ FILE: testdata/ignore_nil_elements/deps/Taskfile.yml ================================================ version: '3' tasks: default: deps: - - task: dep dep: cmds: - echo "string-slice-1" ================================================ FILE: testdata/ignore_nil_elements/includes/Taskfile.yml ================================================ version: '3' includes: inc: inc.yml tasks: default: cmds: - task: inc:default ================================================ FILE: testdata/ignore_nil_elements/includes/inc.yml ================================================ version: '3' tasks: default: cmds: - - echo "string-slice-1" ================================================ FILE: testdata/ignore_nil_elements/preconditions/Taskfile.yml ================================================ version: '3' tasks: default: preconditions: - - sh: "[ 1 = 1 ]" cmds: - echo "string-slice-1" ================================================ FILE: testdata/ignore_signals/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - '{{.CLI_ARGS}}' ================================================ FILE: testdata/include_with_vars/Taskfile.yml ================================================ version: "3" includes: included1: taskfile: include/Taskfile.include1.yml vars: VAR_1: included1-var1 included2: taskfile: include/Taskfile.include2.yml vars: VAR_1: included2-var1 included3: taskfile: include/Taskfile.include3.yml tasks: task1: cmds: - task: included1:task1 - task: included2:task1 - task: included3:task1 ================================================ FILE: testdata/include_with_vars/include/Taskfile.include1.yml ================================================ version: "3" vars: VAR_1: '{{.VAR_1 | default "included-default-var1"}}' VAR_2: '{{.VAR_2 | default "included-default-var2"}}' tasks: task1: cmds: - echo "VAR_1 is {{.VAR_1}}" - echo "VAR_2 is {{.VAR_2}}" ================================================ FILE: testdata/include_with_vars/include/Taskfile.include2.yml ================================================ version: "3" vars: VAR_1: '{{.VAR_1 | default "included-default-var1"}}' VAR_2: '{{.VAR_2 | default "included-default-var2"}}' tasks: task1: cmds: - echo "VAR_1 is {{.VAR_1}}" - echo "VAR_2 is {{.VAR_2}}" ================================================ FILE: testdata/include_with_vars/include/Taskfile.include3.yml ================================================ version: "3" vars: VAR_1: '{{.VAR_1 | default "included-default-var1"}}' VAR_2: '{{.VAR_2 | default "included-default-var2"}}' tasks: task1: cmds: - echo "VAR_1 is {{.VAR_1}}" - echo "VAR_2 is {{.VAR_2}}" ================================================ FILE: testdata/include_with_vars_inside_include/Taskfile.yml ================================================ version: "3" vars: INCLUDE: include FOO: sh : echo bar includes: included1: taskfile: '{{.INCLUDE}}/Taskfile.include.yml' ================================================ FILE: testdata/include_with_vars_inside_include/include/Taskfile.include.yml ================================================ version: "3" ================================================ FILE: testdata/include_with_vars_multi_level/Taskfile.yml ================================================ version: "3" includes: lib: taskfile: lib/Taskfile.yml internal: true foo: taskfile: foo/Taskfile.yml bar: taskfile: bar/Taskfile.yml tasks: default: cmds: - task: lib:greet - task: foo:lib:greet - task: bar:lib:greet ================================================ FILE: testdata/include_with_vars_multi_level/bar/Taskfile.yml ================================================ version: "3" includes: lib: taskfile: ../lib/Taskfile.yml vars: RECEIVER: "bar" ================================================ FILE: testdata/include_with_vars_multi_level/foo/Taskfile.yml ================================================ version: "3" includes: lib: taskfile: ../lib/Taskfile.yml vars: RECEIVER: "foo" ================================================ FILE: testdata/include_with_vars_multi_level/lib/Taskfile.yml ================================================ version: "3" vars: RECEIVER: '{{ .RECEIVER | default "world" }}' tasks: greet: cmds: - echo 'Hello {{.RECEIVER}}' ================================================ FILE: testdata/included_taskfile_var_merging/Taskfile.yaml ================================================ version: "3" includes: foo: taskfile: ./foo/Taskfile.yaml bar: taskfile: ./bar/Taskfile.yaml tasks: stub: cmds: - echo 0 ================================================ FILE: testdata/included_taskfile_var_merging/bar/Taskfile.yaml ================================================ version: "3" vars: DIR: bar tasks: pwd: dir: ./{{ .DIR }} cmds: - echo "{{ .DIR }}" - pwd ================================================ FILE: testdata/included_taskfile_var_merging/foo/Taskfile.yaml ================================================ version: "3" vars: DIR: foo tasks: pwd: dir: ./{{ .DIR }} cmds: - echo "{{ .DIR }}" - pwd ================================================ FILE: testdata/includes/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes/Taskfile.yml ================================================ version: '3' includes: included: ./included included_taskfile: ./Taskfile2.yml included_without_dir: taskfile: ./module1 included_taskfile_without_dir: taskfile: ./module1/Taskfile.yml included_with_dir: taskfile: ./module2 dir: ./module2 included_taskfile_with_dir: taskfile: ./module2/Taskfile.yml dir: ./module2 included_os: ./Taskfile_{{OS}}.yml tasks: default: cmds: - task: gen - task: included:gen - task: included_taskfile:gen - task: included_without_dir:gen_file - task: included_taskfile_without_dir:gen_dir - task: included_with_dir:gen_file - task: included_taskfile_with_dir:gen_dir - task: included_os:gen gen: cmds: - echo main > main.txt ================================================ FILE: testdata/includes/Taskfile2.yml ================================================ version: '3' tasks: gen: cmds: - echo included_taskfile > included_taskfile.txt ================================================ FILE: testdata/includes/Taskfile_darwin.yml ================================================ version: '3' tasks: gen: echo 'os' > os_include.txt ================================================ FILE: testdata/includes/Taskfile_linux.yml ================================================ version: '3' tasks: gen: echo 'os' > os_include.txt ================================================ FILE: testdata/includes/Taskfile_windows.yml ================================================ version: '3' tasks: gen: echo 'os' > os_include.txt ================================================ FILE: testdata/includes/included/Taskfile.yml ================================================ version: '3' tasks: gen: cmds: - echo included_directory > included_directory.txt ================================================ FILE: testdata/includes/module1/Taskfile.yml ================================================ version: '3' tasks: gen_dir: cmds: - echo included_directory_without_dir > included_directory_without_dir.txt gen_file: cmds: - echo included_taskfile_without_dir > included_taskfile_without_dir.txt ================================================ FILE: testdata/includes/module2/Taskfile.yml ================================================ version: '3' tasks: gen_dir: cmds: - echo included_directory_with_dir > included_directory_with_dir.txt gen_file: cmds: - echo included_taskfile_with_dir > included_taskfile_with_dir.txt ================================================ FILE: testdata/includes_call_root_task/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_call_root_task/Taskfile.yml ================================================ version: '3' includes: included: Taskfile2.yml tasks: root-task: cmds: - echo "root task" > root_task.txt ================================================ FILE: testdata/includes_call_root_task/Taskfile2.yml ================================================ version: '3' tasks: call-root: cmds: - task: :root-task ================================================ FILE: testdata/includes_checksum/correct/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ../included.yml internal: true checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5 tasks: default: cmds: - task: included:default ================================================ FILE: testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden ================================================ task: [included:default] echo "Hello, World!" Hello, World! ================================================ FILE: testdata/includes_checksum/correct_remote/Taskfile.yml ================================================ version: '3' includes: included: taskfile: https://taskfile.dev internal: true checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9 tasks: default: cmds: - task: included:default ================================================ FILE: testdata/includes_checksum/included.yml ================================================ version: '3' tasks: default: cmds: - echo "Hello, World!" ================================================ FILE: testdata/includes_checksum/incorrect/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ../included.yml internal: true checksum: foo tasks: default: cmds: - task: included:default ================================================ FILE: testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden ================================================ task: The checksum of the Taskfile at "{{.TEST_DIR}}/testdata/includes_checksum/included.yml" does not match! got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5" want: "foo" ================================================ FILE: testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden ================================================ ================================================ FILE: testdata/includes_cycle/Taskfile.yml ================================================ version: '3' includes: 'one': ./one/Taskfile.yml tasks: default: cmds: - echo "called_dep" > called_dep.txt level1: cmds: - echo "hello level 1" ================================================ FILE: testdata/includes_cycle/one/Taskfile.yml ================================================ version: '3' includes: 'two': ./two/Taskfile.yml tasks: level2: cmds: - echo "hello level 2" ================================================ FILE: testdata/includes_cycle/one/two/Taskfile.yml ================================================ version: '3' includes: bad: "../../Taskfile.yml" tasks: level3: cmds: - echo "hello level 3" ================================================ FILE: testdata/includes_deps/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_deps/Taskfile.yml ================================================ version: '3' includes: included: Taskfile2.yml tasks: default: cmds: - task: included:default ================================================ FILE: testdata/includes_deps/Taskfile2.yml ================================================ version: '3' tasks: default: deps: [called_dep] cmds: - echo "default" > default.txt - task: called_task called_dep: cmds: - echo "called_dep" > called_dep.txt called_task: cmds: - echo "called_task" > called_task.txt ================================================ FILE: testdata/includes_empty/.gitignore ================================================ file.txt ================================================ FILE: testdata/includes_empty/Taskfile.yml ================================================ version: '3' includes: included: Taskfile2.yml ================================================ FILE: testdata/includes_empty/Taskfile2.yml ================================================ version: '3' vars: FILE: file.txt CONTENT: default tasks: default: cmds: - echo "{{.CONTENT}}" > {{.FILE}} ================================================ FILE: testdata/includes_flatten/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_flatten/Taskfile.multiple.yml ================================================ version: '3' includes: included: taskfile: ./included flatten: true tasks: gen: cmds: - echo "gen multiple" ================================================ FILE: testdata/includes_flatten/Taskfile.with_default.yml ================================================ version: '3' tasks: default: echo "default from included flatten" ================================================ FILE: testdata/includes_flatten/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ./included dir: ./included flatten: true with_default: taskfile: ./Taskfile.with_default.yml flatten: true tasks: from_entrypoint: echo "from entrypoint" ================================================ FILE: testdata/includes_flatten/included/Taskfile.yml ================================================ version: '3' includes: nested: taskfile: ../nested flatten: true tasks: gen: cmds: - echo "gen from included" with_deps: deps: - gen cmds: - echo "with_deps from included" pwd: desc: Print working directory cmds: - pwd ================================================ FILE: testdata/includes_flatten/nested/Taskfile.yml ================================================ version: '3' tasks: from_nested: cmds: - echo "from nested" ================================================ FILE: testdata/includes_http/child-taskfile2.yml ================================================ version: '3' includes: third-with-dir-1: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml" dir: ./dir-1 third-with-dir-2: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml" dir: ./dir-2 ================================================ FILE: testdata/includes_http/child-taskfile3.yml ================================================ version: '3' tasks: default: "true" ================================================ FILE: testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml ================================================ version: '3' includes: second-no-dir: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" second-with-dir-1: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" dir: ./dir-1 ================================================ FILE: testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml ================================================ version: '3' includes: second-with-dir-1: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" dir: ./dir-1 second-no-dir: taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml" ================================================ FILE: testdata/includes_incorrect/Taskfile.yml ================================================ version: '3' includes: included: incomplete.yml ================================================ FILE: testdata/includes_incorrect/incomplete.yml ================================================ version: '3' name: 'test ================================================ FILE: testdata/includes_internal/Taskfile.yml ================================================ version: '3' includes: included: taskfile: Taskfile2.yml internal: true tasks: task-1: cmds: - task: included:default task-2: deps: - included:default ================================================ FILE: testdata/includes_internal/Taskfile2.yml ================================================ version: '3' tasks: task-3: cmds: - echo "Hello, World!" ================================================ FILE: testdata/includes_interpolation/include/Taskfile.yml ================================================ version: "3" vars: MODULE_NAME: included includes: include: '../{{.MODULE_NAME}}/Taskfile.yml' ================================================ FILE: testdata/includes_interpolation/include_with_dir/Taskfile.yml ================================================ version: "3" vars: MODULE_NAME: included includes: include-with-dir: taskfile: '../{{.MODULE_NAME}}/Taskfile.yml' dir: '../{{.MODULE_NAME}}' ================================================ FILE: testdata/includes_interpolation/include_with_env_variable/Taskfile.yml ================================================ version: "3" includes: include-with-env-variable: '../{{.MODULE}}/Taskfile.yml' ================================================ FILE: testdata/includes_interpolation/included/Taskfile.yml ================================================ version: "3" tasks: default: cmds: - basename $(pwd) ================================================ FILE: testdata/includes_multi_level/Taskfile.yml ================================================ version: '3' includes: 'one': ./one/ tasks: default: cmds: - task: one:default - task: one:two:default - task: one:two:three:default ================================================ FILE: testdata/includes_multi_level/called_one.txt ================================================ one ================================================ FILE: testdata/includes_multi_level/called_three.txt ================================================ three ================================================ FILE: testdata/includes_multi_level/called_two.txt ================================================ two ================================================ FILE: testdata/includes_multi_level/one/Taskfile.yml ================================================ version: '3' includes: 'two': ./two/ tasks: default: echo one > called_one.txt ================================================ FILE: testdata/includes_multi_level/one/two/Taskfile.yml ================================================ version: '3' includes: 'three': ./three/Taskfile.yml tasks: default: echo two > called_two.txt ================================================ FILE: testdata/includes_multi_level/one/two/three/Taskfile.yml ================================================ version: '3' tasks: default: echo three > called_three.txt ================================================ FILE: testdata/includes_optional/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_optional/Taskfile.yml ================================================ version: '3' includes: included: taskfile: TaskfileOptional.yml optional: true tasks: default: cmds: - echo "called_dep" > called_dep.txt ================================================ FILE: testdata/includes_optional_explicit_false/Taskfile.yml ================================================ version: '3' includes: included: taskfile: TaskfileOptional.yml optional: false tasks: default: cmds: - echo "Hello, world!" ================================================ FILE: testdata/includes_optional_implicit_false/Taskfile.yml ================================================ version: '3' includes: included: TaskfileOptional.yml tasks: default: cmds: - echo "Hello, world!" ================================================ FILE: testdata/includes_rel_path/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ./included dir: ./included common: taskfile: ./common dir: ./common ================================================ FILE: testdata/includes_rel_path/common/Taskfile.yml ================================================ version: '3' tasks: pwd: pwd ================================================ FILE: testdata/includes_rel_path/included/Taskfile.yml ================================================ version: '3' includes: common: taskfile: ../common dir: ../common ================================================ FILE: testdata/includes_remote/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_remote/Taskfile.yml ================================================ version: '3' includes: first: "{{.FIRST_REMOTE_URL}}" ================================================ FILE: testdata/includes_remote/first/Taskfile.yml ================================================ version: '3' includes: second: "{{.SECOND_REMOTE_URL}}" tasks: write-file: requires: vars: [CONTENT, OUTPUT_FILE] cmd: | echo "{{.CONTENT}}" > "{{.OUTPUT_FILE}}" ================================================ FILE: testdata/includes_remote/first/second/Taskfile.yml ================================================ version: '3' tasks: write-file: requires: vars: [CONTENT, OUTPUT_FILE] cmd: | echo "{{.CONTENT}}" > "{{.OUTPUT_FILE}}" ================================================ FILE: testdata/includes_shadowed_default/Taskfile.yml ================================================ version: '3' includes: included: taskfile: Taskfile2.yml tasks: included: cmds: - echo "shadowed" > file.txt ================================================ FILE: testdata/includes_shadowed_default/Taskfile2.yml ================================================ version: '3' tasks: default: cmds: - echo "included" > file.txt ================================================ FILE: testdata/includes_shadowed_default/file.txt ================================================ shadowed ================================================ FILE: testdata/includes_silent/Taskfile-inc.yml ================================================ version: '3' silent: true tasks: hello: cmds: - echo "Hello from include" hello-silent: silent: true cmds: - echo "Hello from include silent task" hello-silent-not-set: cmds: - echo "Hello from include silent not set task" hello-silent-set-false: silent: false cmds: - echo "Hello from include silent false task" ================================================ FILE: testdata/includes_silent/Taskfile.yml ================================================ version: '3' includes: inc: Taskfile-inc.yml tasks: default: cmds: - echo "Hello from root Taskfile" - task: inc:hello - task: inc:hello-silent - task: inc:hello-silent-not-set - task: inc:hello-silent-set-false ================================================ FILE: testdata/includes_silent/testdata/TestIncludeSilent-include-taskfile-silent.golden ================================================ task: [default] echo "Hello from root Taskfile" Hello from root Taskfile Hello from include Hello from include silent task Hello from include silent not set task task: [inc:hello-silent-set-false] echo "Hello from include silent false task" Hello from include silent false task ================================================ FILE: testdata/includes_unshadowed_default/Taskfile.yml ================================================ version: '3' includes: included: taskfile: Taskfile2.yml ================================================ FILE: testdata/includes_unshadowed_default/Taskfile2.yml ================================================ version: '3' tasks: default: cmds: - echo "included" > file.txt ================================================ FILE: testdata/includes_unshadowed_default/file.txt ================================================ included ================================================ FILE: testdata/includes_with_excludes/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ./included/Taskfile.yml excludes: - foo included_flatten: taskfile: ./included/Taskfile.yml flatten: true excludes: - bar tasks: default: cmds: - echo "called_dep" > called_dep.txt ================================================ FILE: testdata/includes_with_excludes/included/Taskfile.yml ================================================ version: '3' tasks: foo: echo foo bar: echo bar ================================================ FILE: testdata/includes_yaml/.gitignore ================================================ *.txt ================================================ FILE: testdata/includes_yaml/Custom.ext ================================================ version: '3' includes: included: ./included custom: ./included/custom.yaml tasks: default: cmds: - task: gen - task: included:gen - task: custom:gen gen: cmds: - echo main > main.txt ================================================ FILE: testdata/includes_yaml/included/Taskfile.yaml ================================================ version: '3' tasks: gen: cmds: - echo included_with_yaml_extension > included_with_yaml_extension.txt ================================================ FILE: testdata/includes_yaml/included/custom.yaml ================================================ version: '3' tasks: gen: cmds: - echo included_with_custom_file > included_with_custom_file.txt ================================================ FILE: testdata/init/.gitignore ================================================ *.yml ================================================ FILE: testdata/interactive_vars/.taskrc.yml ================================================ interactive: true ================================================ FILE: testdata/interactive_vars/Taskfile.yml ================================================ version: '3' tasks: # Simple text input prompt greet: desc: Greet someone by name requires: vars: - NAME cmds: - echo "Hello, {{.NAME}}!" # Enum selection (dropdown menu) deploy: desc: Deploy to an environment requires: vars: - name: ENVIRONMENT enum: [dev, staging, prod] cmds: - echo "Deploying to {{.ENVIRONMENT}}..." # Multiple variables at once release: desc: Create a release with version and environment requires: vars: - VERSION - name: ENVIRONMENT enum: [dev, staging, prod] cmds: - echo "Releasing {{.VERSION}} to {{.ENVIRONMENT}}" # Nested dependencies - all prompts happen upfront full-deploy: desc: Full deployment pipeline with nested deps deps: - task: build - task: test cmds: - task: deploy build: requires: vars: - name: BUILD_MODE enum: [debug, release] cmds: - echo "Building in {{.BUILD_MODE}} mode..." test: requires: vars: - name: TEST_SUITE enum: [unit, integration, e2e, all] cmds: - echo "Running {{.TEST_SUITE}} tests..." # Variable already set - no prompt shown greet-world: desc: Greet the world (no prompt needed) vars: NAME: World requires: vars: - NAME cmds: - echo "Hello, {{.NAME}}!" # Complex scenario with multiple levels pipeline: desc: Run the full CI/CD pipeline cmds: - task: setup - task: build - task: test - task: deploy setup: requires: vars: - PROJECT_NAME cmds: - echo "Setting up project {{.PROJECT_NAME}}..." # Docker example with multiple selections docker-build: desc: Build a Docker image requires: vars: - IMAGE_NAME - IMAGE_TAG - name: PLATFORM enum: [linux/amd64, linux/arm64, linux/arm/v7] cmds: - echo "Building {{.IMAGE_NAME}}:{{.IMAGE_TAG}} for {{.PLATFORM}}" # Database migration example db-migrate: desc: Run database migrations requires: vars: - name: DIRECTION enum: [up, down] - name: DATABASE enum: [postgres, mysql, sqlite] cmds: - echo "Running {{.DIRECTION}} migrations on {{.DATABASE}}" ================================================ FILE: testdata/internal_task/Taskfile.yml ================================================ version: '3' tasks: task-1: cmds: - task: task-3 task-2: deps: - task-3 task-3: internal: true cmds: - echo "Hello, World!" ================================================ FILE: testdata/json_list_format/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" desc: "task description" ================================================ FILE: testdata/json_list_format/testdata/TestJsonListFormat.golden ================================================ { "tasks": [ { "name": "foobar", "task": "foo", "desc": "task description", "summary": "", "aliases": [], "up_to_date": false, "location": { "line": 4, "column": 3, "taskfile": "{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml" } } ], "location": "{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml" } ================================================ FILE: testdata/label_error/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" cmds: - "false" ================================================ FILE: testdata/label_error/testdata/TestLabel-label_in_error-err-run.golden ================================================ task: Failed to run task "foobar": exit status 1 ================================================ FILE: testdata/label_error/testdata/TestLabel-label_in_error.golden ================================================ task: [foobar] false ================================================ FILE: testdata/label_list/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" desc: "task description" ================================================ FILE: testdata/label_list/testdata/TestNoLabelInList.golden ================================================ task: Available tasks for this project: * foo: task description ================================================ FILE: testdata/label_status/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" status: - "false" ================================================ FILE: testdata/label_status/testdata/TestLabel-status-err-status.golden ================================================ task: Task "foobar" is not up-to-date ================================================ FILE: testdata/label_status/testdata/TestLabel-status.golden ================================================ ================================================ FILE: testdata/label_summary/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" desc: description status: - echo "I'm ok" ================================================ FILE: testdata/label_summary/testdata/TestLabel-label_in_summary.golden ================================================ task: Task "foobar" is up to date ================================================ FILE: testdata/label_summary/testdata/TestLabel-summary.golden ================================================ task: foobar description ================================================ FILE: testdata/label_uptodate/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" status: - echo "I'm ok" ================================================ FILE: testdata/label_uptodate/testdata/TestLabel-up_to_date.golden ================================================ task: Task "foobar" is up to date ================================================ FILE: testdata/label_var/Taskfile.yml ================================================ version: '3' vars: BAR: baz tasks: foo: label: "foo{{.BAR}}" status: - echo "I'm ok" ================================================ FILE: testdata/label_var/testdata/TestLabel-var.golden ================================================ task: Task "foobaz" is up to date ================================================ FILE: testdata/list_desc_interpolation/Taskfile.yml ================================================ version: '3' vars: FOO: foo BAR: bar tasks: foo: desc: "task has desc with {{.FOO}}-var" bar: desc: "task has desc with {{.BAR}}-var" ================================================ FILE: testdata/list_desc_interpolation/testdata/TestListDescInterpolation.golden ================================================ task: Available tasks for this project: * bar: task has desc with bar-var * foo: task has desc with foo-var ================================================ FILE: testdata/list_mixed_desc/Taskfile.yml ================================================ version: '3' tasks: foo: label: "foobar" desc: "foo has desc and label" voo: label: "voo has no desc" doo: label: "doo has desc, no label" ================================================ FILE: testdata/list_mixed_desc/testdata/TestListAllShowsNoDesc.golden ================================================ task: Available tasks for this project: * doo: * foo: foo has desc and label * voo: ================================================ FILE: testdata/list_mixed_desc/testdata/TestListCanListDescOnly.golden ================================================ task: Available tasks for this project: * foo: foo has desc and label ================================================ FILE: testdata/output_group/Taskfile.yml ================================================ version: '3' output: group: begin: '::group::{{ .TASK }}' end: '::endgroup::' tasks: hello: cmds: - echo 'Hello!' bye: deps: - hello cmds: - echo 'Bye!' ================================================ FILE: testdata/output_group_error_only/Taskfile.yml ================================================ version: '3' silent: true output: group: error_only: true tasks: passing: echo 'passing-output' failing: cmds: - task: passing - echo 'passing-output-2' - echo 'passing-output-3' - echo 'failing-output' && exit 1 ================================================ FILE: testdata/params/Taskfile.yml ================================================ version: '3' vars: PORTUGUESE_HELLO_WORLD: Olá, mundo! GERMAN: Hello tasks: default: vars: SPANISH: ¡Holla mundo! PORTUGUESE: "{{.PORTUGUESE_HELLO_WORLD}}" GERMAN: "Welt!" deps: - task: write-file vars: {CONTENT: Dependence1} - task: write-file vars: {CONTENT: Dependence2} - task: write-file vars: {CONTENT: "{{.SPANISH|replace \"mundo\" \"dependencia\"}}"} cmds: - task: write-file vars: {CONTENT: Hello} - task: write-file vars: {CONTENT: "$echo 'World'"} - task: write-file vars: {CONTENT: "!"} - task: write-file vars: {CONTENT: "{{.SPANISH}}"} - task: write-file vars: {CONTENT: "{{.PORTUGUESE}}"} - task: write-file vars: {CONTENT: "{{.GERMAN}}"} - task: non-default write-file: cmds: - echo {{.CONTENT}} non-default: vars: PORTUGUESE: "{{.PORTUGUESE_HELLO_WORLD}}" cmds: - task: write-file vars: {CONTENT: "{{.PORTUGUESE}}"} ================================================ FILE: testdata/params/dep1.txt ================================================ Dependence1 ================================================ FILE: testdata/params/dep2.txt ================================================ Dependence2 ================================================ FILE: testdata/params/exclamation.txt ================================================ ! ================================================ FILE: testdata/params/german.txt ================================================ Welt! ================================================ FILE: testdata/params/hello.txt ================================================ Hello ================================================ FILE: testdata/params/portuguese.txt ================================================ Olá, mundo! ================================================ FILE: testdata/params/portuguese2.txt ================================================ Olá, mundo! ================================================ FILE: testdata/params/spanish-dep.txt ================================================ ¡Holla dependencia! ================================================ FILE: testdata/params/spanish.txt ================================================ ¡Holla mundo! ================================================ FILE: testdata/params/testdata/TestParams.golden ================================================ ! Dependence1 Dependence2 Hello Olá, mundo! Olá, mundo! Welt! World ¡Holla dependencia! ¡Holla mundo! ================================================ FILE: testdata/params/world.txt ================================================ World ================================================ FILE: testdata/platforms/Taskfile.yml ================================================ version: '3' tasks: build-windows: deps: [failed-var-other-platform] platforms: [windows] cmds: - echo 'Running task on windows' build-darwin: deps: [failed-var-other-platform] platforms: [darwin] cmds: - echo 'Running task on darwin' build-linux: deps: [failed-var-other-platform] platforms: [linux] cmds: - echo 'Running task on linux' build-freebsd: deps: [failed-var-other-platform] platforms: [freebsd] cmds: - echo 'Running task on freebsd' build-blank-os: deps: [failed-var-other-platform] platforms: [] cmds: - echo 'Running command' build-multiple: deps: [failed-var-other-platform] platforms: [] cmds: - cmd: echo 'Running command' - cmd: echo 'Running on Windows' platforms: [windows] - cmd: echo 'Running on Darwin' platforms: [darwin] build-amd64: deps: [failed-var-other-platform] platforms: [amd64] cmds: - echo "Running command on amd64" build-arm64: deps: [failed-var-other-platform] platforms: [arm64] cmds: - echo "Running command on arm64" build-mixed: deps: [failed-var-other-platform] cmds: - cmd: echo 'building on windows/arm64' platforms: [windows/arm64] - cmd: echo 'building on linux/amd64' platforms: [linux/amd64] - cmd: echo 'building on darwin' platforms: [darwin] failed-var-other-platform: platforms: [__test__] env: EXAMPLE_VAR: {sh: exit 1} vars: EXAMPLE_VAR: {sh: exit 2} ================================================ FILE: testdata/precondition/Taskfile.yml ================================================ version: '3' tasks: foo: preconditions: - test -f foo.txt impossible: preconditions: - sh: "[ 1 = 0 ]" msg: "1 != 0 obviously!" depends_on_impossible: deps: - impossible executes_failing_task_as_cmd: cmds: - task: impossible ================================================ FILE: testdata/precondition/foo.txt ================================================ ================================================ FILE: testdata/precondition/testdata/TestPrecondition-a_precondition_has_been_met.golden ================================================ ================================================ FILE: testdata/precondition/testdata/TestPrecondition-a_precondition_was_not_met-err-run.golden ================================================ task: Failed to run task "impossible": task: precondition not met ================================================ FILE: testdata/precondition/testdata/TestPrecondition-a_precondition_was_not_met.golden ================================================ task: 1 != 0 obviously! ================================================ FILE: testdata/precondition/testdata/TestPrecondition-precondition_in_cmd_fails_the_task-err-run.golden ================================================ task: Failed to run task "executes_failing_task_as_cmd": task: Failed to run task "impossible": task: precondition not met ================================================ FILE: testdata/precondition/testdata/TestPrecondition-precondition_in_cmd_fails_the_task.golden ================================================ task: 1 != 0 obviously! ================================================ FILE: testdata/precondition/testdata/TestPrecondition-precondition_in_dependency_fails_the_task-err-run.golden ================================================ task: Failed to run task "depends_on_impossible": task: Failed to run task "impossible": task: precondition not met ================================================ FILE: testdata/precondition/testdata/TestPrecondition-precondition_in_dependency_fails_the_task.golden ================================================ task: 1 != 0 obviously! ================================================ FILE: testdata/prefix_uptodate/Taskfile.yml ================================================ version: '3' tasks: foo: prefix: "foobar" status: - echo "I'm ok" ================================================ FILE: testdata/prefix_uptodate/testdata/TestPrefix-up_to_dat_with_no_output_style.golden ================================================ task: Task "foo" is up to date ================================================ FILE: testdata/prefix_uptodate/testdata/TestPrefix-up_to_date.golden ================================================ task: Task "foobar" is up to date ================================================ FILE: testdata/prompt/Taskfile.yml ================================================ version: 3 tasks: foo: prompt: Do you want to continue? cmds: - echo 'foo' bar: cmds: - task: show-prompt show-prompt: prompt: Do you want to continue? cmds: - echo 'show-prompt' multi-prompt: prompt: - Do you want to continue? - Are you sure? cmds: - echo 'multi-prompt' ================================================ FILE: testdata/prompt/testdata/TestPromptAssumeYes---yes_flag_should_skip_prompt.golden ================================================ Do you want to continue? [assuming yes] task: [foo] echo 'foo' foo ================================================ FILE: testdata/prompt/testdata/TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError-err-run.golden ================================================ task: Failed to run task "foo": task: Task "foo" cancelled by user ================================================ FILE: testdata/prompt/testdata/TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError.golden ================================================ Do you want to continue? [y/N]: ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task-err-run.golden ================================================ task: Failed to run task "foo": task: Task "foo" cancelled by user ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task.golden ================================================ Do you want to continue? [y/N]: ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task-err-run.golden ================================================ task: Failed to run task "foo": task: Task "foo" cancelled by user ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task.golden ================================================ Do you want to continue? [y/N]: ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_long_approval-test_long_approval.golden ================================================ Do you want to continue? [y/N]: task: [foo] echo 'foo' foo ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_short_approval-test_short_approval.golden ================================================ Do you want to continue? [y/N]: task: [foo] echo 'foo' foo ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_stops_task-test_stops_task-err-run.golden ================================================ task: Failed to run task "foo": task: Task "foo" cancelled by user ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_stops_task-test_stops_task.golden ================================================ Do you want to continue? [y/N]: ================================================ FILE: testdata/prompt/testdata/TestPromptInSummary-test_uppercase_approval-test_uppercase_approval.golden ================================================ Do you want to continue? [y/N]: task: [foo] echo 'foo' foo ================================================ FILE: testdata/prompt/testdata/TestPromptWithIndirectTask.golden ================================================ Do you want to continue? [y/N]: task: [show-prompt] echo 'show-prompt' show-prompt ================================================ FILE: testdata/requires/Taskfile.yml ================================================ version: '3' tasks: default: - task: missing-var missing-var: requires: vars: - FOO cmd: echo "{{.FOO}}" var-defined-in-task: vars: FOO: bar requires: vars: - FOO cmd: echo "{{.FOO}}" validation-var-dynamic: vars: FOO: sh: echo "one" requires: vars: - name: FOO enum: ['one', 'two'] validation-var: requires: vars: - ENV - name: FOO enum: ['one', 'two'] require-before-compile: requires: vars: [ MY_VAR ] cmd: | {{range .MY_VAR | splitList " " }} echo {{.}} {{end}} ================================================ FILE: testdata/requires/testdata/TestRequires-fails_validation-err-run.golden ================================================ task: Task "validation-var" cancelled because it is missing required variables: - FOO has an invalid value : 'bar' (allowed values : [one two]) ================================================ FILE: testdata/requires/testdata/TestRequires-fails_validation.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-passes_validation.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-require_before_compile-err-run.golden ================================================ task: Task "require-before-compile" cancelled because it is missing required variables: MY_VAR ================================================ FILE: testdata/requires/testdata/TestRequires-require_before_compile.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_missing-err-run.golden ================================================ task: Task "missing-var" cancelled because it is missing required variables: FOO ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_missing.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation#01.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation-err-run.golden ================================================ task: Task "validation-var" cancelled because it is missing required variables: ENV, FOO (allowed values: [one two]) ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation.golden ================================================ ================================================ FILE: testdata/requires/testdata/TestRequires-required_var_ok.golden ================================================ task: [missing-var] echo "bar" bar ================================================ FILE: testdata/requires/testdata/TestRequires-var_defined_in_task.golden ================================================ task: [var-defined-in-task] echo "bar" bar ================================================ FILE: testdata/run/.gitignore ================================================ *.txt ================================================ FILE: testdata/run/Taskfile.yml ================================================ version: '3.7' run: when_changed tasks: generate-hash: - rm -f hash.txt - task: input-content vars: { CONTENT: '1' } - task: input-content vars: { CONTENT: '2' } - task: input-content vars: { CONTENT: '2' } input-content: deps: - task: create-output vars: { CONTENT: '1' } cmds: - echo {{.CONTENT}} >> hash.txt create-output: run: once cmds: - echo starting {{.CONTENT}} >> hash.txt deploy: cmds: - rm -rf wildcard.txt - task: deploy:infra - task: deploy:js - task: deploy:go deploy:*: run: once cmd: echo "Deploy {{index .MATCH 0}}" >> wildcard.txt ================================================ FILE: testdata/run_once_shared_deps/Taskfile.yml ================================================ version: '3' includes: service-a: ./service-a service-b: ./service-b tasks: build: deps: - service-a:build - service-b:build ================================================ FILE: testdata/run_once_shared_deps/library/Taskfile.yml ================================================ version: '3' tasks: build: run: once cmds: - echo "build library" sources: - src/**/* ================================================ FILE: testdata/run_once_shared_deps/service-a/Taskfile.yml ================================================ version: '3' includes: library: taskfile: ../library/Taskfile.yml dir: ../library tasks: build: run: once deps: [library:build] cmds: - echo "build a" sources: - src/**/* ================================================ FILE: testdata/run_once_shared_deps/service-a/src/imasource.go ================================================ package main ================================================ FILE: testdata/run_once_shared_deps/service-b/Taskfile.yml ================================================ version: '3' includes: library: taskfile: ../library/Taskfile.yml dir: ../library tasks: build: run: once deps: [library:build] cmds: - echo "build b" sources: - src/**/* ================================================ FILE: testdata/run_once_shared_deps/service-b/src/imasource.go ================================================ package main ================================================ FILE: testdata/run_when_changed/Taskfile.yml ================================================ version: '3' includes: service-a: ./service-a service-b: ./service-b tasks: start: cmds: - task: service-a:start - task: service-b:start ================================================ FILE: testdata/run_when_changed/library/Taskfile.yml ================================================ version: '3' tasks: login: run: when_changed cmds: - echo "login server={{.SERVER}} user={{.USER}}" ================================================ FILE: testdata/run_when_changed/service-a/Taskfile.yml ================================================ version: '3' includes: library: taskfile: ../library/Taskfile.yml dir: ../library tasks: start: cmds: - task: library:login vars: SERVER: fubar USER: fubar - task: library:login vars: SERVER: foo USER: foo ================================================ FILE: testdata/run_when_changed/service-b/Taskfile.yml ================================================ version: '3' includes: library: taskfile: ../library/Taskfile.yml dir: ../library tasks: start: cmds: - task: library:login vars: SERVER: fubar USER: fubar - task: library:login vars: SERVER: bar USER: bar ================================================ FILE: testdata/shopts/command_level/Taskfile.yml ================================================ version: '3' silent: true tasks: pipefail: cmds: - cmd: set -o | grep pipefail set: [pipefail] globstar: cmds: - cmd: shopt | grep globstar shopt: [globstar] ================================================ FILE: testdata/shopts/global_level/Taskfile.yml ================================================ version: '3' silent: true set: [pipefail] shopt: [globstar] tasks: pipefail: cmds: - set -o | grep pipefail globstar: cmds: - shopt | grep globstar ================================================ FILE: testdata/shopts/task_level/Taskfile.yml ================================================ version: '3' silent: true tasks: pipefail: set: [pipefail] cmds: - set -o | grep pipefail globstar: shopt: [globstar] cmds: - shopt | grep globstar ================================================ FILE: testdata/short_task_notation/Taskfile.yml ================================================ version: '3' tasks: default: - task: string-slice - task: string string-slice: - echo "string-slice-1" - echo "string-slice-2" string: echo "string" ================================================ FILE: testdata/silent/Taskfile.yml ================================================ version: '3' tasks: silent: desc: "silent" silent: true cmds: - exit 0 chatty: desc: "chatty" silent: false cmds: - exit 0 # Test combinations of silent and chatty tasks task-test-silent-calls-chatty-non-silenced: silent: true cmds: - task: chatty task-test-silent-calls-chatty-silenced: silent: true cmds: - task: chatty silent: true task-test-no-cmds-calls-chatty-silenced: silent: false cmds: - task: chatty silent: true task-test-chatty-calls-chatty-non-silenced: silent: false cmds: - cmd: exit 0 - task: chatty task-test-chatty-calls-chatty-silenced: silent: false cmds: - cmd: exit 0 - task: chatty silent: true task-test-chatty-calls-silenced-cmd: silent: false cmds: - cmd: exit 0 silent: true # Now test with dependencies. task-test-is-silent-depends-on-chatty-non-silenced: silent: true deps: [chatty, silent] task-test-is-silent-depends-on-chatty-silenced: silent: true deps: - task: chatty silent: true - task: silent silent: false task-test-is-chatty-depends-on-chatty-silenced: silent: false deps: - task: chatty silent: true - task: silent silent: false ================================================ FILE: testdata/single_cmd_dep/.gitignore ================================================ *.txt ================================================ FILE: testdata/single_cmd_dep/Taskfile.yml ================================================ version: "3" tasks: foo: deps: [bar] cmd: echo foo > foo.txt bar: echo bar > bar.txt ================================================ FILE: testdata/special_vars/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ./included dir: ./included tasks: print-task: aliases: [echo-task] cmds: - echo {{.TASK}} print-root-dir: echo {{.ROOT_DIR}} print-root-taskfile: echo {{.ROOT_TASKFILE}} print-taskfile: echo {{.TASKFILE}} print-taskfile-dir: echo {{.TASKFILE_DIR}} print-task-version: echo {{.TASK_VERSION}} print-task-alias: aliases: [echo-task-alias] cmds: - echo "{{.ALIAS}}" print-task-alias-default: echo "{{.ALIAS}}" print-task-dir: dir: 'foo' cmd: echo {{.TASK_DIR}} ================================================ FILE: testdata/special_vars/included/Taskfile.yml ================================================ version: '3' tasks: print-task: aliases: [echo-task] cmds: - echo {{.TASK}} print-root-dir: echo {{.ROOT_DIR}} print-taskfile: echo {{.TASKFILE}} print-taskfile-dir: echo {{.TASKFILE_DIR}} print-task-version: echo {{.TASK_VERSION}} print-task-alias: aliases: [echo-task-alias] cmds: - echo "{{.ALIAS}}" print-task-alias-default: echo "{{.ALIAS}}" ================================================ FILE: testdata/special_vars/subdir/.gitkeep ================================================ ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden ================================================ included:print-task ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/included ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/included/Taskfile.yml ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/Taskfile.yml ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/foo ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden ================================================ print-task ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/Taskfile.yml ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-root-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-task.golden ================================================ included:print-task ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-taskfile-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/included ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/included/Taskfile.yml ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-root-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-root-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/Taskfile.yml ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-task-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/foo ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-task.golden ================================================ print-task ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-taskfile-dir.golden ================================================ {{.TEST_DIR}}/testdata/special_vars ================================================ FILE: testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-taskfile.golden ================================================ {{.TEST_DIR}}/testdata/special_vars/Taskfile.yml ================================================ FILE: testdata/split_args/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - cmd: echo '{{splitArgs .CLI_ARGS | len}}' ================================================ FILE: testdata/status/.gitignore ================================================ *.txt ================================================ FILE: testdata/status/Taskfile.yml ================================================ version: '3' tasks: gen-foo: cmds: - touch foo.txt sources: - ./foo.txt status: - test 1 = 0 gen-bar: cmds: - touch bar.txt sources: - ./bar.txt status: - test 1 = 1 gen-silent-baz: silent: true cmds: - touch baz.txt sources: - ./baz.txt ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_1_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_2_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_3_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_4_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_5.golden ================================================ task: [gen-bar] touch bar.txt ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-bar_6.golden ================================================ task: Task "gen-bar" is up to date ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-baz_2.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-baz_3.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-baz_4_verbose.golden ================================================ task: "gen-silent-baz" started task: Task "gen-silent-baz" is up to date ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-baz_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-foo_1_silent.golden ================================================ ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-foo_2.golden ================================================ task: [gen-foo] touch foo.txt ================================================ FILE: testdata/status/testdata/TestStatus-run_gen-foo_3.golden ================================================ task: [gen-foo] touch foo.txt ================================================ FILE: testdata/status_vars/.gitignore ================================================ generated.txt ================================================ FILE: testdata/status_vars/Taskfile.yml ================================================ version: '3' tasks: build-checksum: sources: - ./source.txt status: - echo "{{.CHECKSUM}}" build-ts: method: timestamp sources: - ./source.txt status: - echo '{{.TIMESTAMP.Unix}}' - echo '{{.TIMESTAMP}}' ================================================ FILE: testdata/status_vars/source.txt ================================================ Hello, World! ================================================ FILE: testdata/summary/Taskfile.yml ================================================ version: '3' tasks: task-with-summary: deps: [dependent-task-1, dependent-task-2] summary: | summary of task-with-summary - line 1 line 2 line 3 cmds: - echo 'task-with-summary was executed' - echo 'another command' - exit 0 other-task-with-summary: summary: summary of other-task-with-summary cmds: - echo 'other-task-with-summary was executed' dependent-task-1: cmds: - echo 'dependent-task-1 was executed' dependent-task-2: cmds: - echo 'dependent-task-2 was executed' ================================================ FILE: testdata/summary/task-with-summary.txt ================================================ task: task-with-summary summary of task-with-summary - line 1 line 2 line 3 dependencies: - dependent-task-1 - dependent-task-2 commands: - echo 'task-with-summary was executed' - echo 'another command' - exit 0 task: other-task-with-summary summary of other-task-with-summary commands: - echo 'other-task-with-summary was executed' ================================================ FILE: testdata/summary-vars-requires/Taskfile-with-env.yml ================================================ version: 3 vars: GLOBAL_VAR: "I am a global var" env: GLOBAL_ENV: "I am a global env" tasks: test-env: desc: Task with vars and env vars: LOCAL_VAR: "I am a local var" env: LOCAL_ENV: "I am a local env" DATABASE_URL: "postgres://localhost/mydb" requires: vars: - API_KEY cmds: - echo "Testing env vars" ================================================ FILE: testdata/summary-vars-requires/Taskfile-with-globals.yml ================================================ version: 3 vars: GLOBAL_VAR: "I am global" ANOTHER_GLOBAL: "Also global" tasks: test-globals: desc: Task with global and local vars vars: LOCAL_VAR: "I am local" requires: vars: - REQUIRED_VAR cmds: - echo {{ .GLOBAL_VAR }} {{ .LOCAL_VAR }} ================================================ FILE: testdata/summary-vars-requires/Taskfile.yml ================================================ version: 3 tasks: mytask: desc: It does things summary: | It does things and has optional and required variables. vars: OPTIONAL_VAR: "hello" requires: vars: - REQUIRED_VAR cmds: - cmd: echo {{ .OPTIONAL_VAR }} {{ .REQUIRED_VAR }} with-sh-var: desc: Task with shell variable vars: DYNAMIC_VAR: sh: echo "world" STATIC_VAR: "hello" cmds: - echo {{ .DYNAMIC_VAR }} no-vars: desc: Task without variables cmds: - echo "no vars here" only-requires: desc: Task with only requires requires: vars: - NEEDED_VAR cmds: - echo {{ .NEEDED_VAR }} ================================================ FILE: testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden ================================================ task: with-sh-var Task with shell variable vars: DYNAMIC_VAR: sh: echo "world" STATIC_VAR: "hello" commands: - echo ================================================ FILE: testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden ================================================ task: mytask It does things and has optional and required variables. vars: OPTIONAL_VAR: "hello" requires: vars: - REQUIRED_VAR commands: - echo hello ================================================ FILE: testdata/taskfile_walk/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - echo 'foo' silent: true ================================================ FILE: testdata/taskfile_walk/foo/bar/.gitkeep ================================================ ================================================ FILE: testdata/user_working_dir/Taskfile.yml ================================================ version: '3' tasks: default: cmds: - echo '{{.USER_WORKING_DIR}}' silent: true ================================================ FILE: testdata/user_working_dir_with_includes/Taskfile.yml ================================================ version: '3' includes: included: taskfile: ./included/Taskfile.yml ================================================ FILE: testdata/user_working_dir_with_includes/included/Taskfile.yml ================================================ version: '3' tasks: echo: dir: '{{.USER_WORKING_DIR}}' cmds: - pwd silent: true ================================================ FILE: testdata/user_working_dir_with_includes/somedir/.keep ================================================ ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-dotenv/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' tasks: default: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-dotenv/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-dotenv/testdata/TestVarInheritance-entrypoint-global-dotenv.golden ================================================ entrypoint-global-dotenv entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-global-vars/testdata/TestVarInheritance-entrypoint-global-vars.golden ================================================ entrypoint-global-vars entrypoint-global-vars ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-dotenv/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: entrypoint-task-call-vars called-task: dotenv: - 'called-task.env' cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-dotenv/called-task.env ================================================ VAR=entrypoint-task-call-dotenv ENV=entrypoint-task-call-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-dotenv/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-dotenv/task.env ================================================ VAR=entrypoint-task-dotenv ENV=entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-dotenv/testdata/TestVarInheritance-entrypoint-task-call-dotenv.golden ================================================ entrypoint-task-call-vars entrypoint-task-call-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-task-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: entrypoint-task-call-vars called-task: vars: VAR: entrypoint-task-call-task-vars env: ENV: entrypoint-task-call-task-vars cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-task-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-task-vars/task.env ================================================ VAR=entrypoint-task-dotenv ENV=entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-task-vars/testdata/TestVarInheritance-entrypoint-task-call-task-vars.golden ================================================ entrypoint-task-call-task-vars entrypoint-task-call-task-vars ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: entrypoint-task-call-vars called-task: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-vars/task.env ================================================ VAR=entrypoint-task-dotenv ENV=entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-call-vars/testdata/TestVarInheritance-entrypoint-task-call-vars.golden ================================================ entrypoint-task-call-vars entrypoint-global-vars ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-dotenv/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: dotenv: - 'task.env' cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-dotenv/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-dotenv/task.env ================================================ VAR=entrypoint-task-dotenv ENV=entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-dotenv/testdata/TestVarInheritance-entrypoint-task-dotenv.golden ================================================ entrypoint-global-vars entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars tasks: default: dotenv: - 'task.env' vars: VAR: entrypoint-task-vars env: ENV: entrypoint-task-vars cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-vars/task.env ================================================ VAR=entrypoint-task-dotenv ENV=entrypoint-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/entrypoint-task-vars/testdata/TestVarInheritance-entrypoint-task-vars.golden ================================================ entrypoint-task-vars entrypoint-task-vars ================================================ FILE: testdata/var_inheritance/v3/included-global-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-global-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-global-vars/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-global-vars/testdata/TestVarInheritance-included-global-vars.golden ================================================ included-global-vars included-global-vars ================================================ FILE: testdata/var_inheritance/v3/included-task/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-dotenv/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task-call-dotenv/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-dotenv/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: included-task-call-vars called-task: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task-call-dotenv/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-dotenv/testdata/TestVarInheritance-included-task-call-dotenv.golden ================================================ included-task-call-vars included-global-vars ================================================ FILE: testdata/var_inheritance/v3/included-task-call-task-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task-call-task-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-task-vars/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: included-task-call-vars called-task: vars: VAR: included-task-call-task-vars env: ENV: included-task-call-task-vars cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task-call-task-vars/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-task-vars/testdata/TestVarInheritance-included-task-call-task-vars.golden ================================================ included-task-call-task-vars included-task-call-task-vars ================================================ FILE: testdata/var_inheritance/v3/included-task-call-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task-call-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-vars/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' cmds: - task: called-task vars: VAR: included-task-call-vars called-task: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task-call-vars/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-call-vars/testdata/TestVarInheritance-included-task-call-vars.golden ================================================ included-task-call-vars included-global-vars ================================================ FILE: testdata/var_inheritance/v3/included-task-dotenv/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task-dotenv/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-dotenv/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task-dotenv/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-dotenv/testdata/TestVarInheritance-included-task-dotenv.golden ================================================ included-global-vars included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-vars/Taskfile.yml ================================================ version: '3' silent: true dotenv: - 'global.env' vars: VAR: entrypoint-global-vars env: ENV: entrypoint-global-vars includes: included: included.yml ================================================ FILE: testdata/var_inheritance/v3/included-task-vars/global.env ================================================ VAR=entrypoint-global-dotenv ENV=entrypoint-global-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-vars/included.yml ================================================ version: '3' silent: true vars: VAR: included-global-vars env: ENV: included-global-vars tasks: default: dotenv: - 'task.env' vars: VAR: included-task-vars env: ENV: included-task-vars cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/included-task-vars/task.env ================================================ VAR=included-task-dotenv ENV=included-task-dotenv ================================================ FILE: testdata/var_inheritance/v3/included-task-vars/testdata/TestVarInheritance-included-task-vars.golden ================================================ included-task-vars included-task-vars ================================================ FILE: testdata/var_inheritance/v3/shell/Taskfile.yml ================================================ version: '3' silent: true tasks: default: cmds: - 'echo "{{.VAR}}"' - 'echo "$ENV"' ================================================ FILE: testdata/var_inheritance/v3/shell/testdata/TestVarInheritance-shell.golden ================================================ shell shell ================================================ FILE: testdata/var_references/Taskfile.yml ================================================ version: '3' vars: GLOBAL_VAR: [1, 2, 2, 2, 3, 3, 4, 5] tasks: default: - task: ref-cmd - task: ref-dep - task: ref-resolver - task: ref-resolver-sh ref-cmd: vars: VAR_REF: ref: .GLOBAL_VAR cmds: - task: print-first vars: VAR: ref: .VAR_REF ref-dep: vars: VAR_REF: ref: .GLOBAL_VAR deps: - task: print-first vars: VAR: ref: .VAR_REF ref-resolver: vars: VAR_REF: ref: .GLOBAL_VAR cmds: - task: print-var vars: VAR: ref: (index .VAR_REF 0) ref-resolver-sh: vars: JSON_STRING: sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}' JSON: ref: "fromJson .JSON_STRING" VAR_REF: ref: .JSON cmds: - task: print-story vars: VAR: ref: .VAR_REF print-var: cmds: - echo "{{.VAR}}" print-first: cmds: - echo "{{index .VAR 0}}" print-story: cmds: - >- echo "{{.VAR.name}} has {{len .VAR.children}} children called {{- $children := .VAR.children -}} {{- range $i, $child := $children -}} {{- if lt $i (sub (len $children) 1)}} {{$child.name -}}, {{- else}} and {{$child.name -}} {{- end -}} {{- end -}}" ================================================ FILE: testdata/var_references/testdata/TestReference-reference_in_command.golden ================================================ 1 ================================================ FILE: testdata/var_references/testdata/TestReference-reference_in_dependency.golden ================================================ 1 ================================================ FILE: testdata/var_references/testdata/TestReference-reference_using_templating_resolver.golden ================================================ 1 ================================================ FILE: testdata/var_references/testdata/TestReference-reference_using_templating_resolver_and_dynamic_var.golden ================================================ Alice has 3 children called Bob, Charlie, and Diane ================================================ FILE: testdata/vars/.gitignore ================================================ *.txt ================================================ FILE: testdata/vars/Taskfile.yml ================================================ version: '3' dotenv: [.env] vars: VAR_A: A VAR_B: '{{.VAR_A}}B' VAR_C: '{{.VAR_B}}C' VAR_1: {sh: echo 1} VAR_2: {sh: 'echo "{{.VAR_1}}2"'} VAR_3: {sh: 'echo "{{.VAR_2}}3"'} tasks: default: - task: missing-var - task: var-order - task: dependent-sh - task: with-call - task: from-dot-env missing-var: echo '{{.NON_EXISTING_VAR}}' var-order: vars: VAR_D: '{{.VAR_C}}D' VAR_E: '{{.VAR_D}}E' VAR_F: '{{.VAR_E}}F' cmds: - echo '{{.VAR_F}}' dependent-sh: vars: VAR_4: {sh: 'echo "{{.VAR_3}}4"'} VAR_5: {sh: 'echo "{{.VAR_4}}5"'} VAR_6: {sh: 'echo "{{.VAR_5}}6"'} cmds: - echo '{{.VAR_6}}' with-call: - task: called-task vars: ABC123: '{{.VAR_C}}{{.VAR_3}}' called-task: vars: MESSAGE: Hi, {{.ABC123}}! cmds: - echo "{{.MESSAGE}}" from-dot-env: echo '{{.DOT_ENV_VAR}}' # Test that CLI variables take priority over Taskfile defaults cli-var-priority: vars: CLI_VAR: '{{.CLI_VAR | default "default_value"}}' cmds: - echo '{{.CLI_VAR}}' ================================================ FILE: testdata/vars/any/Taskfile.yml ================================================ version: '3' tasks: default: - task: map - task: nested-map - task: slice - task: ref - task: ref-sh - task: ref-dep - task: ref-resolver - task: json map: vars: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} cmds: - task: print-story vars: VAR: ref: .MAP nested-map: vars: FOO: "foo" nested: map: variables: work: "{{.FOO}}" cmds: - echo {{.nested.variables.work}} slice: vars: FOO: "foo" BAR: "bar" slice_variables_work: ["{{.FOO}}","{{.BAR}}"] cmds: - echo {{index .slice_variables_work 0}} {{index .slice_variables_work 1}} ref: vars: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} MAP_REF: ref: .MAP cmds: - task: print-story vars: VAR: ref: .MAP_REF ref-sh: vars: JSON_STRING: sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}' JSON: "fromJson {{.JSON_STRING}}" MAP_REF: ref: .JSON cmds: - task: print-story vars: VAR: ref: .MAP_REF ref-dep: vars: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} deps: - task: print-story vars: VAR: ref: .MAP ref-resolver: vars: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} MAP_REF: ref: .MAP cmds: - task: print-var vars: VAR: ref: (index .MAP_REF.children 0).name json: vars: JSON_STRING: sh: cat example.json JSON: ref: "fromJson .JSON_STRING" cmds: - task: print-story vars: VAR: ref: .JSON print-var: cmds: - echo "{{.VAR}}" print-story: cmds: - >- echo "{{.VAR.name}} has {{len .VAR.children}} children called {{- $children := .VAR.children -}} {{- range $i, $child := $children -}} {{- if lt $i (sub (len $children) 1)}} {{$child.name -}}, {{- else}} and {{$child.name -}} {{- end -}} {{- end -}}" ================================================ FILE: testdata/vars/any/example.json ================================================ { "name": "Alice", "age": 30, "children": [ { "name": "Bob", "age": 5 }, { "name": "Charlie", "age": 3 }, { "name": "Diane", "age": 1 } ] } ================================================ FILE: testdata/vars/any/example.yaml ================================================ name: Alice age: 30 children: - name: Bob age: 5 - name: Charlie age: 3 - name: Diane age: 1 ================================================ FILE: testdata/vars/testdata/TestVars-cli-var-priority-default.golden ================================================ default_value ================================================ FILE: testdata/vars/testdata/TestVars-cli-var-priority-override.golden ================================================ from_cli ================================================ FILE: testdata/vars/testdata/TestVars.golden ================================================ ABCDEF 123456 Hi, ABC123! From .env file ================================================ FILE: testdata/version/v1/Taskfile.yml ================================================ version: "1" tasks: foo: cmds: - echo "Foo" bar: cmds: - echo "Bar" ================================================ FILE: testdata/version/v2/Taskfile.yml ================================================ version: "2" tasks: foo: cmds: - echo "Foo" bar: cmds: - echo "Bar" ================================================ FILE: testdata/version/v3/Taskfile.yml ================================================ version: "3" tasks: foo: cmds: - echo "Foo" bar: cmds: - echo "Bar" ================================================ FILE: testdata/watch/.gitignore ================================================ src/* ================================================ FILE: testdata/watch/Taskfile.yaml ================================================ # https://taskfile.dev version: '3' tasks: default: sources: - "src/*" cmds: - echo "Task running!" ================================================ FILE: testdata/wildcards/Taskfile.yml ================================================ version: 3 tasks: wildcard-*: cmds: - echo "Hello {{index .MATCH 0}}" wildcard-*-*: cmds: - echo "Hello {{index .MATCH 0}}" '*-wildcard-*': cmds: - echo "Hello {{index .MATCH 0}} {{index .MATCH 1}}" # Matches is empty when you call the task name exactly (i.e. `task matches-exactly-*`) matches-exactly-*: cmds: - "echo \"I don't consume matches: {{.MATCH}}\"" start-*: aliases: - s-* vars: SERVICE: "{{index .MATCH 0}}" cmds: - echo "Starting {{.SERVICE}}" ================================================ FILE: variables.go ================================================ package task import ( "fmt" "maps" "os" "path/filepath" "strings" "github.com/joho/godotenv" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile/ast" ) // CompiledTask returns a copy of a task, but replacing variables in almost all // properties using the Go template package. func (e *Executor) CompiledTask(call *Call) (*ast.Task, error) { return e.compiledTask(call, true) } // FastCompiledTask is like CompiledTask, but it skippes dynamic variables. func (e *Executor) FastCompiledTask(call *Call) (*ast.Task, error) { return e.compiledTask(call, false) } func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) { origTask, err := e.GetTask(call) if err != nil { return nil, err } vars, err := e.Compiler.FastGetVariables(origTask, call) if err != nil { return nil, err } cache := &templater.Cache{Vars: vars} return &ast.Task{ Task: origTask.Task, Label: templater.Replace(origTask.Label, cache), Desc: templater.Replace(origTask.Desc, cache), Prompt: templater.Replace(origTask.Prompt, cache), Summary: templater.Replace(origTask.Summary, cache), Aliases: origTask.Aliases, Sources: origTask.Sources, Generates: origTask.Generates, Dir: origTask.Dir, Set: origTask.Set, Shopt: origTask.Shopt, Vars: vars, Env: nil, Dotenv: origTask.Dotenv, Silent: deepcopy.Scalar(origTask.Silent), Interactive: origTask.Interactive, Internal: origTask.Internal, Method: origTask.Method, Prefix: origTask.Prefix, IgnoreError: origTask.IgnoreError, Run: origTask.Run, IncludeVars: origTask.IncludeVars, IncludedTaskfileVars: origTask.IncludedTaskfileVars, Platforms: origTask.Platforms, Location: origTask.Location, Requires: origTask.Requires, Watch: origTask.Watch, Namespace: origTask.Namespace, Failfast: origTask.Failfast, }, nil } func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, error) { origTask, err := e.GetTask(call) if err != nil { return nil, err } var vars *ast.Vars if evaluateShVars { vars, err = e.Compiler.GetVariables(origTask, call) } else { vars, err = e.Compiler.FastGetVariables(origTask, call) } if err != nil { return nil, err } fullName := origTask.Task if matches, exists := vars.Get("MATCH"); exists { for _, match := range matches.Value.([]string) { fullName = strings.Replace(fullName, "*", match, 1) } } cache := &templater.Cache{Vars: vars} new := ast.Task{ Task: origTask.Task, Label: templater.Replace(origTask.Label, cache), Desc: templater.Replace(origTask.Desc, cache), Prompt: templater.Replace(origTask.Prompt, cache), Summary: templater.Replace(origTask.Summary, cache), Aliases: origTask.Aliases, Sources: templater.ReplaceGlobs(origTask.Sources, cache), Generates: templater.ReplaceGlobs(origTask.Generates, cache), Dir: templater.Replace(origTask.Dir, cache), Set: origTask.Set, Shopt: origTask.Shopt, Vars: vars, Env: nil, Dotenv: templater.Replace(origTask.Dotenv, cache), Silent: deepcopy.Scalar(origTask.Silent), Interactive: origTask.Interactive, Internal: origTask.Internal, Method: templater.Replace(origTask.Method, cache), Prefix: templater.Replace(origTask.Prefix, cache), IgnoreError: origTask.IgnoreError, Run: templater.Replace(origTask.Run, cache), IncludeVars: origTask.IncludeVars, IncludedTaskfileVars: origTask.IncludedTaskfileVars, Platforms: origTask.Platforms, If: templater.Replace(origTask.If, cache), Location: origTask.Location, Requires: origTask.Requires, Watch: origTask.Watch, Failfast: origTask.Failfast, Namespace: origTask.Namespace, FullName: fullName, } new.Dir, err = execext.ExpandLiteral(new.Dir) if err != nil { return nil, err } if e.Dir != "" { new.Dir = filepathext.SmartJoin(e.Dir, new.Dir) } if new.Prefix == "" { new.Prefix = new.Task } dotenvEnvs := ast.NewVars() if len(new.Dotenv) > 0 { for _, dotEnvPath := range new.Dotenv { dotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath) if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { continue } envs, err := godotenv.Read(dotEnvPath) if err != nil { return nil, err } for key, value := range envs { if _, ok := dotenvEnvs.Get(key); !ok { dotenvEnvs.Set(key, ast.Var{Value: value}) } } } } new.Env = ast.NewVars() new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil) new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil) new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil) if evaluateShVars { for k, v := range new.Env.All() { // If the variable is not dynamic, we can set it and return if v.Value != nil || v.Sh == nil { new.Env.Set(k, ast.Var{Value: v.Value}) continue } static, err := e.Compiler.HandleDynamicVar(v, new.Dir, env.GetFromVars(new.Env)) if err != nil { return nil, err } new.Env.Set(k, ast.Var{Value: static}) } } if len(origTask.Sources) > 0 && origTask.Method != "none" { var checker fingerprint.SourcesCheckable if origTask.Method == "timestamp" { checker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry) } else { checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry) } value, err := checker.Value(&new) if err != nil { return nil, err } vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value}) // Adding new variables, requires us to refresh the templaters // cache of the the values manually cache.ResetCache() } if len(origTask.Cmds) > 0 { new.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds)) for _, cmd := range origTask.Cmds { if cmd == nil { continue } if cmd.For != nil { list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) if err != nil { return nil, err } // Name the iterator variable var as string if cmd.For.As != "" { as = cmd.For.As } else { as = "ITEM" } // Create a new command for each item in the list for i, loopValue := range list { extra := map[string]any{ as: loopValue, } if len(keys) > 0 { extra["KEY"] = keys[i] } newCmd := cmd.DeepCopy() newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra) newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra) newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra) new.Cmds = append(new.Cmds, newCmd) } continue } // Defer commands are replaced in a lazy manner because // we need to include EXIT_CODE. if cmd.Defer { new.Cmds = append(new.Cmds, cmd.DeepCopy()) continue } newCmd := cmd.DeepCopy() newCmd.Cmd = templater.Replace(cmd.Cmd, cache) newCmd.Task = templater.Replace(cmd.Task, cache) newCmd.If = templater.Replace(cmd.If, cache) newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache) new.Cmds = append(new.Cmds, newCmd) } } if len(origTask.Deps) > 0 { new.Deps = make([]*ast.Dep, 0, len(origTask.Deps)) for _, dep := range origTask.Deps { if dep == nil { continue } if dep.For != nil { list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache) if err != nil { return nil, err } // Name the iterator variable var as string if dep.For.As != "" { as = dep.For.As } else { as = "ITEM" } // Create a new command for each item in the list for i, loopValue := range list { extra := map[string]any{ as: loopValue, } if len(keys) > 0 { extra["KEY"] = keys[i] } newDep := dep.DeepCopy() newDep.Task = templater.ReplaceWithExtra(dep.Task, cache, extra) newDep.Vars = templater.ReplaceVarsWithExtra(dep.Vars, cache, extra) new.Deps = append(new.Deps, newDep) } continue } newDep := dep.DeepCopy() newDep.Task = templater.Replace(dep.Task, cache) newDep.Vars = templater.ReplaceVars(dep.Vars, cache) new.Deps = append(new.Deps, newDep) } } if len(origTask.Preconditions) > 0 { new.Preconditions = make([]*ast.Precondition, 0, len(origTask.Preconditions)) for _, precondition := range origTask.Preconditions { if precondition == nil { continue } newPrecondition := precondition.DeepCopy() newPrecondition.Sh = templater.Replace(precondition.Sh, cache) newPrecondition.Msg = templater.Replace(precondition.Msg, cache) new.Preconditions = append(new.Preconditions, newPrecondition) } } if len(origTask.Status) > 0 { new.Status = templater.Replace(origTask.Status, cache) } // We only care about templater errors if we are evaluating shell variables if evaluateShVars && cache.Err() != nil { return &new, cache.Err() } return &new, nil } func asAnySlice[T any](slice []T) []any { ret := make([]any, len(slice)) for i, v := range slice { ret[i] = v } return ret } func itemsFromFor( f *ast.For, dir string, sources []*ast.Glob, generates []*ast.Glob, vars *ast.Vars, location *ast.Location, cache *templater.Cache, ) ([]any, []string, error) { var keys []string // The list of keys to loop over (only if looping over a map) var values []any // The list of values to loop over // Get the list from a matrix if f.Matrix.Len() != 0 { if err := resolveMatrixRefs(f.Matrix, cache); err != nil { return nil, nil, errors.TaskfileInvalidError{ URI: location.Taskfile, Err: err, } } return asAnySlice(product(f.Matrix)), nil, nil } // Get the list from the explicit for list if len(f.List) > 0 { return f.List, nil, nil } // Get the list from the task sources if f.From == "sources" { glist, err := fingerprint.Globs(dir, sources) if err != nil { return nil, nil, err } // Make the paths relative to the task dir for i, v := range glist { if glist[i], err = filepath.Rel(dir, v); err != nil { return nil, nil, err } } values = asAnySlice(glist) } // Get the list from the task generates if f.From == "generates" { glist, err := fingerprint.Globs(dir, generates) if err != nil { return nil, nil, err } // Make the paths relative to the task dir for i, v := range glist { if glist[i], err = filepath.Rel(dir, v); err != nil { return nil, nil, err } } values = asAnySlice(glist) } // Get the list from a variable and split it up if f.Var != "" { if vars != nil { v, ok := vars.Get(f.Var) // If the variable is dynamic, then it hasn't been resolved yet // and we can't use it as a list. This happens when fast compiling a task // for use in --list or --list-all etc. if ok && v.Value != nil && v.Sh == nil { switch value := v.Value.(type) { case string: if f.Split != "" { values = asAnySlice(strings.Split(value, f.Split)) } else { values = asAnySlice(strings.Fields(value)) } case []string: values = asAnySlice(value) case []int: values = asAnySlice(value) case []any: values = value case map[string]any: for k, v := range value { keys = append(keys, k) values = append(values, v) } default: return nil, nil, errors.TaskfileInvalidError{ URI: location.Taskfile, Err: errors.New("loop var must be a delimiter-separated string, list or a map"), } } } } } return values, keys, nil } func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) error { if matrix.Len() == 0 { return nil } for _, row := range matrix.All() { if row.Ref != "" { v := templater.ResolveRef(row.Ref, cache) switch value := v.(type) { case []any: row.Value = value default: return fmt.Errorf("matrix reference %q must resolve to a list", row.Ref) } } } return nil } // product generates the cartesian product of the input map of slices. func product(matrix *ast.Matrix) []map[string]any { if matrix.Len() == 0 { return nil } // Start with an empty product result result := []map[string]any{{}} // Iterate over each slice in the slices for key, row := range matrix.All() { var newResult []map[string]any // For each combination in the current result for _, combination := range result { // Append each element from the current slice to the combinations for _, item := range row.Value { newComb := make(map[string]any, len(combination)) // Copy the existing combination maps.Copy(newComb, combination) // Add the current item with the corresponding key newComb[key] = item newResult = append(newResult, newComb) } } // Update result with the new combinations result = newResult } return result } ================================================ FILE: watch.go ================================================ package task import ( "context" "fmt" "os" "os/signal" "path/filepath" "slices" "strings" "syscall" "time" "github.com/fsnotify/fsnotify" "github.com/puzpuzpuz/xsync/v4" "github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/fsnotifyext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/slicesext" "github.com/go-task/task/v3/taskfile/ast" ) const defaultWaitTime = 100 * time.Millisecond // watchTasks start watching the given tasks func (e *Executor) watchTasks(calls ...*Call) error { tasks := make([]string, len(calls)) for i, c := range calls { tasks[i] = c.Task } e.Logger.Errf(logger.Green, "task: Started watching for tasks: %s\n", strings.Join(tasks, ", ")) ctx, cancel := context.WithCancel(context.Background()) for _, c := range calls { go func() { err := e.RunTask(ctx, c) if err == nil { e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task) } else if !isContextError(err) { e.Logger.Errf(logger.Red, "%v\n", err) } }() } var waitTime time.Duration switch { case e.Interval != 0: waitTime = e.Interval case e.Taskfile.Interval != 0: waitTime = e.Taskfile.Interval default: waitTime = defaultWaitTime } w, err := fsnotify.NewWatcher() if err != nil { cancel() return err } defer w.Close() deduper := fsnotifyext.NewDeduper(w, waitTime) eventsChan := deduper.GetChan() closeOnInterrupt(w) go func() { for { select { case event, ok := <-eventsChan: if !ok { cancel() return } e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event) cancel() ctx, cancel = context.WithCancel(context.Background()) e.Compiler.ResetCache() for _, c := range calls { go func() { if ShouldIgnore(event.Name) { e.Logger.VerboseErrf(logger.Magenta, "task: event skipped for being an ignored dir: %s\n", event.Name) return } t, err := e.GetTask(c) if err != nil { e.Logger.Errf(logger.Red, "%v\n", err) return } baseDir := filepathext.SmartJoin(e.Dir, t.Dir) files, err := e.collectSources(calls) if err != nil { e.Logger.Errf(logger.Red, "%v\n", err) return } if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) { relPath, _ := filepath.Rel(baseDir, event.Name) e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath) return } err = e.RunTask(ctx, c) if err == nil { e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task) } else if !isContextError(err) { e.Logger.Errf(logger.Red, "%v\n", err) } }() } case err, ok := <-w.Errors: switch { case !ok: cancel() return default: e.Logger.Errf(logger.Red, "%v\n", err) } } } }() e.watchedDirs = xsync.NewMap[string, bool]() go func() { // NOTE(@andreynering): New files can be created in directories // that were previously empty, so we need to check for new dirs // from time to time. for { if err := e.registerWatchedDirs(w, calls...); err != nil { e.Logger.Errf(logger.Red, "%v\n", err) } time.Sleep(5 * time.Second) } }() <-make(chan struct{}) return nil } func isContextError(err error) bool { if taskRunErr, ok := err.(*errors.TaskRunError); ok { err = taskRunErr.Err } return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) } func closeOnInterrupt(w *fsnotify.Watcher) { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch w.Close() os.Exit(0) }() } func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error { files, err := e.collectSources(calls) if err != nil { return err } for _, f := range files { d := filepath.Dir(f) if isSet, ok := e.watchedDirs.Load(d); ok && isSet { continue } if ShouldIgnore(d) { continue } if err := w.Add(d); err != nil { return err } e.watchedDirs.Store(d, true) relPath, _ := filepath.Rel(e.Dir, d) e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath) } return nil } var ignorePaths = []string{ "/.task", "/.git", "/.hg", "/node_modules", } func ShouldIgnore(path string) bool { for _, p := range ignorePaths { if strings.Contains(path, fmt.Sprintf("%s/", p)) || strings.HasSuffix(path, p) { return true } } return false } func (e *Executor) collectSources(calls []*Call) ([]string, error) { var sources []string err := e.traverse(calls, func(task *ast.Task) error { files, err := fingerprint.Globs(task.Dir, task.Sources) if err != nil { return err } sources = append(sources, files...) return nil }) return slicesext.UniqueJoin(sources), err } type traverseFunc func(*ast.Task) error func (e *Executor) traverse(calls []*Call, yield traverseFunc) error { for _, c := range calls { task, err := e.CompiledTask(c) if err != nil { return err } for _, dep := range task.Deps { if dep.Task != "" { if err := e.traverse([]*Call{{Task: dep.Task, Vars: dep.Vars}}, yield); err != nil { return err } } } for _, cmd := range task.Cmds { if cmd.Task != "" { if err := e.traverse([]*Call{{Task: cmd.Task, Vars: cmd.Vars}}, yield); err != nil { return err } } } if err := yield(task); err != nil { return err } } return nil } ================================================ FILE: watch_test.go ================================================ //go:build watch // +build watch package task_test import ( "bytes" "context" "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/filepathext" ) func TestFileWatch(t *testing.T) { t.Parallel() const dir = "testdata/watch" _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) _ = os.RemoveAll(filepathext.SmartJoin(dir, "src")) expectedOutput := strings.TrimSpace(` task: Started watching for tasks: default task: [default] echo "Task running!" Task running! task: task "default" finished running task: [default] echo "Task running!" Task running! task: task "default" finished running `) var buff bytes.Buffer e := task.NewExecutor( task.WithDir(dir), task.WithStdout(&buff), task.WithStderr(&buff), task.WithWatch(true), ) require.NoError(t, e.Setup()) buff.Reset() dirPath := filepathext.SmartJoin(dir, "src") filePath := filepathext.SmartJoin(dirPath, "a") err := os.MkdirAll(dirPath, 0o755) require.NoError(t, err) err = os.WriteFile(filePath, []byte("test"), 0o644) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) go func() { for { select { case <-ctx.Done(): return default: err := e.Run(ctx, &task.Call{Task: "default"}) if err != nil { panic(err) } } } }() time.Sleep(200 * time.Millisecond) err = os.WriteFile(filePath, []byte("test updated"), 0o644) require.NoError(t, err) time.Sleep(200 * time.Millisecond) cancel() assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String())) } func TestShouldIgnore(t *testing.T) { t.Parallel() tt := []struct { path string expect bool }{ {"/.git/hooks", true}, {"/.github/workflows/build.yaml", false}, } for k, ct := range tt { ct := ct t.Run(fmt.Sprintf("ignore - %d", k), func(t *testing.T) { t.Parallel() require.Equal(t, task.ShouldIgnore(ct.path), ct.expect) }) } } ================================================ FILE: website/.gitignore ================================================ # Dependencies /node_modules npm-debug.log* yarn-debug.log* yarn-error.log* .vitepress/cache .vitepress/dist .task/ ================================================ FILE: website/.prettierignore ================================================ pnpm-lock.yaml ================================================ FILE: website/.vitepress/components/AuthorCard.vue ================================================ ================================================ FILE: website/.vitepress/components/BlogPost.vue ================================================ ================================================ FILE: website/.vitepress/components/HomePage.vue ================================================ ================================================ FILE: website/.vitepress/components/VPTeamMembersItem.vue ================================================ ================================================ FILE: website/.vitepress/components/Version.vue ================================================ ================================================ FILE: website/.vitepress/components.d.ts ================================================ declare module '*.vue' { import type { DefineComponent } from 'vue'; const component: DefineComponent<{}, {}, any>; export default component; } ================================================ FILE: website/.vitepress/config.ts ================================================ import { defineConfig, HeadConfig } from 'vitepress'; import githubLinksPlugin from './plugins/github-links'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'; import { groupIconMdPlugin, groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons'; import { team } from './team.ts'; import { taskDescription, taskName, ogUrl, ogImage } from './meta.ts'; import { fileURLToPath, URL } from 'node:url'; import llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms'; const version = readFileSync( resolve(__dirname, '../../internal/version/version.txt'), 'utf8' ).trim(); const urlVersion = process.env.NODE_ENV === 'development' ? { current: 'https://taskfile.dev/', next: 'http://localhost:3002/' } : { current: 'https://taskfile.dev/', next: 'https://next.taskfile.dev/' }; // https://vitepress.dev/reference/site-config export default defineConfig({ title: taskName, description: taskDescription, lang: 'en-US', head: [ // Favicon ICO for legacy browsers (auto-discovery) ['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }], // Favicon SVG for modern browsers (scalable) ['link', { rel: 'icon', href: '/img/logo.svg', type: 'image/svg+xml' }], // Apple Touch Icon for iOS devices ['link', { rel: 'apple-touch-icon', href: '/img/logo.png' }], [ 'meta', { name: 'author', content: `${team.map((c) => c.name).join(', ')}` } ], // Open Graph ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'Task' }], ['meta', { property: 'og:image', content: ogImage }], // Twitter Card ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:site', content: '@taskfiledev' }], ['meta', { name: 'twitter:image', content: ogImage }], [ 'meta', { name: 'keywords', content: 'task runner, build tool, taskfile, yaml build tool, go task runner, make alternative, cross-platform build tool, makefile alternative, automation tool, ci cd pipeline, developer productivity, build automation, command line tool, go binary, yaml configuration' } ], [ "script", { defer: "", src: "https://u.taskfile.dev/script.js", "data-website-id": "084030b0-0e3f-4891-8d2a-0c12c40f5933" } ], [ "script", { type: "application/ld+json" }, JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", "name": "Task", "url": "https://taskfile.dev/" }) ] ], transformHead({ pageData }) { const head: HeadConfig[] = [] // Canonical URL dynamique const canonicalUrl = `https://taskfile.dev/${pageData.relativePath .replace(/\.md$/, '') .replace(/index$/, '')}` head.push(['link', { rel: 'canonical', href: canonicalUrl }]) // Dynamic Open Graph and Twitter meta tags const isHome = pageData.relativePath === 'index.md'; var pageTitle = pageData.frontmatter.title || pageData.title || taskName; if (!isHome) { pageTitle = `${pageTitle} | ${taskName}`; } const pageDescription = pageData.frontmatter.description || pageData.description || taskDescription head.push(['meta', { property: 'og:title', content: pageTitle }]) head.push(['meta', { property: 'og:description', content: pageDescription }]) head.push(['meta', { property: 'og:url', content: canonicalUrl }]) head.push(['meta', { name: 'twitter:title', content: pageTitle }]) head.push(['meta', { name: 'twitter:description', content: pageDescription }]) // Noindex pour 404 if (pageData.relativePath === '404.md') { head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }]) } return head }, srcDir: 'src', cleanUrls: true, markdown: { config: (md) => { md.use(githubLinksPlugin, { baseUrl: 'https://github.com', repo: 'go-task/task' }); md.use(tabsMarkdownPlugin); md.use(groupIconMdPlugin); md.use(copyOrDownloadAsMarkdownButtons); } }, vite: { plugins: [ llmstxt({ ignoreFiles: [ 'index.md', 'team.md', 'donate.md', 'docs/styleguide.md', 'docs/contributing.md', 'docs/releasing.md', 'docs/changelog.md', 'blog/*' ] }), groupIconVitePlugin({ customIcon: { '.taskrc.yml': localIconLoader( import.meta.url, './theme/icons/task.svg' ), 'Taskfile.yml': localIconLoader( import.meta.url, './theme/icons/task.svg' ) } }) ], resolve: { alias: [ { find: /^.*\/VPTeamMembersItem\.vue$/, replacement: fileURLToPath( new URL('./components/VPTeamMembersItem.vue', import.meta.url) ) } ] } }, themeConfig: { logo: '/img/logo.svg', carbonAds: { code: 'CESI65QJ', placement: 'taskfiledev' }, search: { provider: 'algolia', options: { appId: '7IZIJ13AI7', apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc', indexName: 'taskfile' } }, nav: [ { text: 'Home', link: '/' }, { text: 'Docs', link: '/docs/guide', activeMatch: '^/docs' }, { text: 'Blog', link: '/blog', activeMatch: '^/blog' }, { text: 'Donate', link: '/donate' }, { text: 'Team', link: '/team' }, { text: process.env.NODE_ENV === 'development' ? 'Next' : `v${version}`, items: [ { items: [ { text: `v${version}`, link: urlVersion.current }, { text: 'Next', link: urlVersion.next } ] } ] } ], sidebar: { '/blog/': [ { text: '2026', collapsed: false, items: [ { text: 'New `if:` Control and Variable Prompt', link: '/blog/if-and-variable-prompt' } ] }, { text: '2025', collapsed: false, items: [ { text: 'Built-in Core Utilities', link: '/blog/windows-core-utils' } ] }, { text: '2024', collapsed: false, items: [ { text: 'Any Variables', link: '/blog/any-variables' } ] }, { text: '2023', collapsed: false, items: [ { text: 'Introducing Experiments', link: '/blog/task-in-2023' } ] } ], '/': [ { text: 'Installation', link: '/docs/installation' }, { text: 'Getting Started', link: '/docs/getting-started' }, { text: 'Guide', link: '/docs/guide' }, { text: 'Reference', collapsed: true, items: [ { text: 'Taskfile Schema', link: '/docs/reference/schema' }, { text: 'Environment', link: '/docs/reference/environment' }, { text: 'Configuration', link: '/docs/reference/config' }, { text: 'CLI', link: '/docs/reference/cli' }, { text: 'Templating', link: '/docs/reference/templating' }, { text: 'Package API', link: '/docs/reference/package' } ] }, { text: 'Experiments', collapsed: true, link: '/docs/experiments/', items: [ { text: 'Env Precedence (#1038)', link: '/docs/experiments/env-precedence' }, { text: 'Gentle Force (#1200)', link: '/docs/experiments/gentle-force' }, { text: 'Remote Taskfiles (#1317)', link: '/docs/experiments/remote-taskfiles' } ] }, { text: 'Deprecations', collapsed: true, link: '/docs/deprecations/', items: [ { text: 'Completion Scripts', link: '/docs/deprecations/completion-scripts' }, { text: 'Template Functions', link: '/docs/deprecations/template-functions' }, { text: 'Version 2 Schema (#1197)', link: '/docs/deprecations/version-2-schema' } ] }, { text: 'Taskfile Versions', link: '/docs/taskfile-versions' }, { text: 'Integrations', link: '/docs/integrations' }, { text: 'Community', link: '/docs/community' }, { text: 'Style Guide', link: '/docs/styleguide' }, { text: 'Contributing', link: '/docs/contributing' }, { text: 'Releasing', link: '/docs/releasing' }, { text: 'Changelog', link: '/docs/changelog' }, { text: 'FAQ', link: '/docs/faq' } ], // Hacky to disable sidebar for these pages '/donate': [], '/team': [] }, socialLinks: [ { icon: 'github', link: 'https://github.com/go-task/task' }, { icon: 'discord', link: 'https://discord.gg/6TY36E39UK' }, { icon: 'x', link: 'https://twitter.com/taskfiledev' }, { icon: 'bluesky', link: 'https://bsky.app/profile/taskfile.dev' }, { icon: 'mastodon', link: 'https://fosstodon.org/@task' } ], editLink: { text: 'Edit this page on GitHub', pattern: 'https://github.com/go-task/task/edit/main/website/src/:path' }, footer: { message: 'Built with Netlify' } }, sitemap: { hostname: 'https://taskfile.dev', transformItems: (items) => { return items.map((item) => ({ ...item, lastmod: new Date().toISOString() })); } } }); ================================================ FILE: website/.vitepress/meta.ts ================================================ export const taskName = 'Task'; export const taskDescription = 'A fast, cross-platform build tool inspired by Make, designed for modern workflows.'; export const ogUrl = 'https://taskfile.dev/'; export const ogImage = 'https://taskfile.dev/img/og_image.png'; ================================================ FILE: website/.vitepress/plugins/github-links.ts ================================================ import type MarkdownIt from 'markdown-it'; interface PluginOptions { repo: string; } function githubLinksPlugin( md: MarkdownIt, options: PluginOptions = {} as PluginOptions ): void { const baseUrl = 'https://github.com'; const { repo } = options; md.core.ruler.after('inline', 'github-links', (state): void => { const tokens = state.tokens; for (let i = 0; i < tokens.length; i++) { if (tokens[i].type === 'inline' && tokens[i].children) { const inlineTokens = tokens[i].children!; for (let j = 0; j < inlineTokens.length; j++) { if (inlineTokens[j].type === 'text') { let text: string = inlineTokens[j].content!; const protectedRefs: string[] = []; let protectIndex: number = 0; text = text.replace( /[\w.-]+\/[\w.-]+#(\d+)/g, (match: string): string => { const placeholder: string = `__PROTECTED_${protectIndex}__`; protectedRefs[protectIndex] = match; protectIndex++; return placeholder; } ); text = text.replace( /#(\d+)/g, `#$1` ); text = text.replace( /@([a-zA-Z0-9_-]+)(?![\w@.])/g, `@$1` ); protectedRefs.forEach((ref: string, index: number): void => { text = text.replace(`__PROTECTED_${index}__`, ref); }); if (text !== inlineTokens[j].content) { inlineTokens[j].content = text; inlineTokens[j].type = 'html_inline'; } } } } } }); } export default githubLinksPlugin; ================================================ FILE: website/.vitepress/sponsors.ts ================================================ export const sponsors = [ { tier: 'Gold Sponsors', size: 'big', items: [ { name: 'devowl', url: 'https://devowl.io/', img: '/img/devowl.io.svg' }, { name: 'GoodX', url: 'https://goodx.international/', img: '/img/goodx.svg' }, { name: 'Magic', url: 'https://magic.dev/', img: '/img/magic.png' } ] }, { tier: 'Community Sponsors', size: 'big', items: [ { name: 'Cloudsmith', url: 'https://cloudsmith.com/', img: '/img/cloudsmith.svg' } ] } ]; ================================================ FILE: website/.vitepress/team.ts ================================================ export const team = [ { slug: 'andreynering', avatar: 'https://www.github.com/andreynering.png', name: 'Andrey Nering', icon: '/img/flag-brazil.svg', title: 'Creator & Maintainer', sponsor: 'https://github.com/sponsors/andreynering', links: [ { icon: 'github', link: 'https://github.com/andreynering' }, { icon: 'discord', link: 'https://discord.com/users/310141681926275082' }, { icon: 'x', link: 'https://x.com/andreynering' }, { icon: 'bluesky', link: 'https://bsky.app/profile/andreynering.bsky.social' }, { icon: 'mastodon', link: 'https://mastodon.social/@andreynering' } ] }, { slug: 'pd93', avatar: 'https://www.github.com/pd93.png', name: 'Pete Davison', icon: '/img/flag-wales.svg', title: 'Maintainer', sponsor: 'https://github.com/sponsors/pd93', links: [ { icon: 'github', link: 'https://github.com/pd93' }, { icon: 'bluesky', link: 'https://bsky.app/profile/pd93.uk' } ] }, { slug: 'vmaerten', avatar: 'https://www.github.com/vmaerten.png', name: 'Valentin Maerten', icon: '/img/flag-france.svg', title: 'Maintainer', sponsor: 'https://github.com/sponsors/vmaerten', links: [ { icon: 'github', link: 'https://github.com/vmaerten' }, { icon: 'x', link: 'https://x.com/v_maerten' }, { icon: 'bluesky', link: 'https://bsky.app/profile/vmaerten.bsky.social' } ] } ]; ================================================ FILE: website/.vitepress/theme/custom.css ================================================ :root { --ifm-color-primary: #43aba2; --vp-home-hero-name-color: var(--ifm-color-primary); --vp-c-brand-1: var(--ifm-color-primary); --vp-c-brand-2: var(--ifm-color-primary); --vp-c-brand-3: var(--ifm-color-primary); --vp-icon-info: #3b82f6; --vp-icon-tip: #10b981; --vp-icon-warning: #f59e0b; --vp-icon-danger: #ef4444; --vp-icon-details: #6b7280; } .dark { --vp-icon-info: #93c5fd; --vp-icon-tip: #34d399; --vp-icon-warning: #fbbf24; --vp-icon-danger: #f87171; --vp-icon-details: #9ca3af; } img[src*='shields.io'] { display: inline; vertical-align: text-bottom; } img[src*='custom-icon-badges.demolab.com'] { display: inline; height: 1em; vertical-align: text-bottom; } .github-user-mention { font-weight: 700 !important; } .vp-doc .blog-post:first-of-type { margin-top: 2rem; } .blog-post { animation: fadeInUp 0.6s ease-out; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .blog-post:nth-of-type(1) { animation-delay: 0.1s; } .blog-post:nth-of-type(2) { animation-delay: 0.2s; } .blog-post:nth-of-type(3) { animation-delay: 0.3s; } .custom-block .custom-block-title::before { content: ''; display: inline-block; width: 20px; height: 20px; margin-right: 8px; vertical-align: middle; flex-shrink: 0; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain; mask-repeat: no-repeat; mask-position: center; mask-size: contain; } .custom-block.info .custom-block-title::before { background-color: var(--vp-icon-info); -webkit-mask-image: url('./icons/info.svg'); mask-image: url('./icons/info.svg'); } .custom-block.tip .custom-block-title::before { background-color: var(--vp-icon-tip); -webkit-mask-image: url('./icons/tip.svg'); mask-image: url('./icons/tip.svg'); } .custom-block.warning .custom-block-title::before { background-color: var(--vp-icon-warning); -webkit-mask-image: url('./icons/warning.svg'); mask-image: url('./icons/warning.svg'); } .custom-block.danger .custom-block-title::before { background-color: var(--vp-icon-danger); -webkit-mask-image: url('./icons/danger.svg'); mask-image: url('./icons/danger.svg'); } .custom-block.details[open] summary::before { transform: rotate(90deg); } .custom-block .custom-block-title { display: flex; align-items: center; } @supports not (mask-image: none) { .custom-block .custom-block-title::before, .custom-block.details summary::before { font-size: 18px; width: auto; height: auto; background: none !important; -webkit-mask: none !important; mask: none !important; } .custom-block.info .custom-block-title::before { content: 'ℹ️'; } .custom-block.tip .custom-block-title::before { content: '💡'; } .custom-block.warning .custom-block-title::before { content: '⚠️'; } .custom-block.danger .custom-block-title::before { content: '🔥'; } } .VPTeamPage > .VPTeamPageTitle { padding-top: 0 } ================================================ FILE: website/.vitepress/theme/index.ts ================================================ import DefaultTheme from 'vitepress/theme'; import type { Theme } from 'vitepress'; import './custom.css'; import HomePage from '../components/HomePage.vue'; import AuthorCard from '../components/AuthorCard.vue'; import BlogPost from '../components/BlogPost.vue'; import Version from '../components/Version.vue'; import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; import { h } from 'vue'; import 'virtual:group-icons.css'; import CopyOrDownloadAsMarkdownButtons from 'vitepress-plugin-llms/vitepress-components/CopyOrDownloadAsMarkdownButtons.vue'; export default { extends: DefaultTheme, Layout() { return h(DefaultTheme.Layout, null, { 'home-features-after': () => h(HomePage) }); }, enhanceApp({ app }) { app.component('AuthorCard', AuthorCard); app.component('BlogPost', BlogPost); app.component('Version', Version); app.component('CopyOrDownloadAsMarkdownButtons', CopyOrDownloadAsMarkdownButtons); enhanceAppWithTabs(app); } } satisfies Theme; ================================================ FILE: website/Taskfile.yml ================================================ version: '3' tasks: install: desc: Setup VitePress locally cmds: - pnpm install sources: - package.json - pnpm-lock.yaml default: desc: Start website deps: [install] aliases: [s, start] vars: HOST: '{{default "0.0.0.0" .HOST}}' PORT: '{{default "3001" .PORT}}' cmds: - pnpm dev --host={{.HOST}} --port={{.PORT}} lint: desc: Lint website deps: [install] cmds: - pnpm lint build: desc: Build website deps: [install] cmds: - pnpm build preview: desc: Preview Website deps: [build] aliases: [serve] vars: HOST: '{{default "localhost" .HOST}}' PORT: '{{default "3001" .PORT}}' cmds: - pnpm preview --host={{.HOST}} --port={{.PORT}} clean: desc: Clean temp directories cmds: - rm -rf ./vitepress/dist deploy:next: desc: Build and deploy next.taskfile.dev cmds: - pnpm netlify deploy --prod --site=4e13dfcf-fc0d-4bec-ad60-b918a8dc3942 deploy:prod: desc: Build and deploy taskfile.dev cmds: - pnpm netlify deploy --prod --site=e625bc6a-1cd3-465d-ad30-7bbddaeb4f31 ================================================ FILE: website/netlify.toml ================================================ [build] publish = ".vitepress/dist" command = "pnpm run build" ================================================ FILE: website/package.json ================================================ { "name": "website2", "version": "1.0.0", "description": "", "type": "module", "scripts": { "dev": "vitepress dev", "build": "vitepress build", "preview": "vitepress preview", "lint": "prettier --write ." }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/markdown-it": "^14.1.2", "@types/node": "^24.1.0", "netlify-cli": "^24.0.0", "prettier": "^3.6.2", "vitepress": "^1.6.3", "vitepress-plugin-group-icons": "^1.6.1", "vitepress-plugin-tabs": "^0.8.0", "vitepress-plugin-llms": "^1.9.1", "vue": "^3.5.18" }, "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017" } ================================================ FILE: website/prettier.config.js ================================================ /** * @see https://prettier.io/docs/configuration * @type {import("prettier").Config} */ const config = { trailingComma: 'none', singleQuote: true, overrides: [ { files: ['*.md'], options: { printWidth: 80, proseWrap: 'always' } } ] }; export default config; ================================================ FILE: website/src/blog/any-variables.md ================================================ --- title: Any Variables author: pd93 date: 2024-05-09 outline: deep editLink: false --- # Any Variables Task has always had variables, but even though you were able to define them using different YAML types, they would always be converted to strings by Task. This limited users to string manipulation and encouraged messy workarounds for simple problems. Starting from [v3.37.0][v3.37.0], this is no longer the case! Task now supports most variable types, including **booleans**, **integers**, **floats** and **arrays**! ## What's the big deal? These changes allow you to use variables in a much more natural way and opens up a wide variety of sprig functions that were previously useless. Take a look at some of the examples below for some inspiration. ### Evaluating booleans No more comparing strings to "true" or "false". Now you can use actual boolean values in your templates: ::: code-group ```yaml [Before] version: 3 tasks: foo: vars: BOOL: true # <-- Parsed as a string even though its a YAML boolean cmds: - '{{if eq .BOOL "true"}}echo foo{{end}}' ``` ```yaml [After] version: 3 tasks: foo: vars: BOOL: true # <-- Parsed as a boolean cmds: - '{{if .BOOL}}echo foo{{end}}' # <-- No need to compare to "true" ``` ::: ### Arithmetic You can now perform basic arithmetic operations on integer and float variables: ```yaml version: 3 tasks: foo: vars: INT: 10 FLOAT: 3.14159 cmds: - 'echo {{add .INT .FLOAT}}' ``` You can use any of the following arithmetic functions: `add`, `sub`, `mul`, `div`, `mod`, `max`, `min`, `floor`, `ceil`, `round` and `randInt`. Check out the [slim-sprig math documentation][slim-sprig-math] for more information. ### Arrays You can now range over arrays inside templates and use list-based functions: ```yaml version: 3 tasks: foo: vars: ARRAY: [1, 2, 3] cmds: - 'echo {{range .ARRAY}}{{.}}{{end}}' ``` You can use any of the following list-based functions: `first`, `rest`, `last`, `initial`, `append`, `prepend`, `concat`, `reverse`, `uniq`, `without`, `has`, `compact`, `slice` and `chunk`. Check out the [slim-sprig lists documentation][slim-sprig-list] for more information. ### Looping over variables using `for` Previously, you would have to use a delimiter separated string to loop over an arbitrary list of items in a variable and split them by using the `split` subkey to specify the delimiter. However, we have now added support for looping over "collection-type" variables using the `for` keyword, so now you are able to loop over list variables directly: ::: code-group ```yaml [Before] version: 3 tasks: foo: vars: LIST: 'foo,bar,baz' cmds: - for: var: LIST split: ',' cmd: echo {{.ITEM}} ``` ```yaml [After] version: 3 tasks: foo: vars: LIST: ['foo', 'bar', 'baz'] cmds: - for: var: LIST cmd: echo {{.ITEM}} ``` ::: ## What about maps? Maps were originally included in the Any Variables experiment. However, they weren't quite ready yet. Instead of making you wait for everything to be ready at once, we have released support for all other variable types and we will continue working on map support in the new "[Map Variables][map-variables]" experiment. We're looking for feedback on a couple of different proposals, so please give them a go and let us know what you think. :pray: [v3.37.0]: https://github.com/go-task/task/releases/tag/v3.37.0 [slim-sprig-math]: https://sprig.taskfile.dev/math.html [slim-sprig-list]: https://sprig.taskfile.dev/lists.html ================================================ FILE: website/src/blog/if-and-variable-prompt.md ================================================ --- title: New `if:` Control and Variable Prompt description: Introduction of the `if:` control and required variable prompts. author: vmaerten date: 2026-01-24 outline: deep editLink: false --- # New `if:` Control and Variable Prompt The [v3.47.0][release] release is here, and it brings two exciting new features to Task. Let's take a closer look at them! ## The New `if:` Control This first feature is simply the second most upvoted issue of all time (!) with 58 :thumbsup:s (!!) at the time of writing. It introduces the `if:` control, which allow you to conditionally skip the execution of certain tasks and proceeding. `if:` can be set on a task-level or command-level, and can be either a Bash command or a Go template expression. Let me show a couple of examples. Task-level with Bash expression: ```yaml version: '3' tasks: deploy: if: '[ "$CI" = "true" ]' cmds: - echo "Deploying..." - ./deploy.sh ``` Command-level with Go template expression: ```yaml version: '3' tasks: conditional: vars: ENABLE_FEATURE: "true" cmds: - cmd: echo "Feature is enabled" if: '{{eq .ENABLE_FEATURE "true"}}' - cmd: echo "Feature is disabled" if: '{{ne .ENABLE_FEATURE "true"}}' ``` For more details, please check out the [documentation][if-docs]. The [examples][if-examples] from the test suite may be useful too. ::: info We had similar functionality before, but nothing that perfectly fits this use case. There were [`sources:`][sources] and [`status:`][status], but those were meant to check if a task was up-to-date, and [`preconditions:`][preconditions], but this would halt the execution of the task instead of skipping it. ::: ## Prompt for Required Variables For backward-compatibility reasons, this feature is disabled by default. To enable it, either pass `--interactive` flag or add `interactive: true` to your `.taskrc.yml`. Once you do that, Task will basically starting prompting you in runtime for any required variables. In the example below, `NAME` will be prompted at runtime: ```yaml version: '3' tasks: # Simple text input prompt greet: desc: Greet someone by name requires: vars: - NAME cmds: - echo "Hello, {{.NAME}}!" ``` If a given variable has an enum, Task will actually show a selection menu so you can choose the right option instead of typing: ```yaml version: '3' tasks: # Enum selection (dropdown menu) deploy: desc: Deploy to an environment requires: vars: - name: ENVIRONMENT enum: [dev, staging, prod] cmds: - echo "Deploying to {{.ENVIRONMENT}}..." ``` Once again, check out the [documentation][prompt-docs] for more details, and the [prompt examples][prompt-examples] from the test suite. ## Feedback Let's us know if you have any feedback! You can find us on our [Discord server][discord]. [release]: https://github.com/go-task/task/releases/tag/v3.47.0 [vmaerten]: https://github.com/vmaerten [sources]: https://taskfile.dev/docs/guide#by-fingerprinting-locally-generated-files-and-their-sources [status]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-indicate-a-task-is-up-to-date [preconditions]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-cancel-the-execution-of-a-task-and-its-dependencies [if-docs]: https://taskfile.dev/docs/guide#conditional-execution-with-if [if-examples]: https://github.com/go-task/task/blob/main/testdata/if/Taskfile.yml [prompt-docs]: https://taskfile.dev/docs/guide#prompting-for-missing-variables-interactively [prompt-examples]: https://github.com/go-task/task/blob/main/testdata/interactive_vars/Taskfile.yml [discord]: https://discord.com/invite/6TY36E39UK ================================================ FILE: website/src/blog/index.md ================================================ --- title: Blog description: Latest news and updates from the Task team editLink: false --- ================================================ FILE: website/src/blog/task-in-2023.md ================================================ --- title: Introducing Experiments description: A look at where task is, where it's going and how we're going to get there. author: pd93 date: 2024-05-09 outline: deep editLink: false --- # Introducing Experiments Lately, Task has been growing extremely quickly and I've found myself thinking a lot about the future of the project and how we continue to evolve and grow. I'm not much of a writer, but I think one of the things we could do better is to communicate these kinds of thoughts to the community. So, with that in mind, this is the first (hopefully of many) blog posts talking about Task and what we're up to. ## :calendar: So, what have we been up to? Over the past 12 months or so, @andreynering (Author and maintainer of the project) and I (@pd93) have been working in our spare time to maintain and improve v3 of Task and we've made some amazing progress. Here are just some of the things we've released in that time: - An official [extension for VS Code][vscode-task]. - Internal Tasks (#818). - Task aliases (#879). - Looping over tasks (#1220). - A series of refactors to the core codebase to make it more maintainable and extensible. - Loads of bug fixes and improvements. - An integration with [Crowdin][crowdin]. Work is in progress on making our docs available in **7 new languages** (Special thanks to all our translators for the huge help with this!). - And much, much more! :sparkles: We're also working on adding some really exciting and highly requested features to Task such as having the ability to run remote Taskfiles (#1317). None of this would have been possible without the [150 or so (and growing) contributors][contributors] to the project, numerous sponsors and a passionate community of users. Together we have more than doubled the number of GitHub stars to over 8400 :star: since the beginning of 2022 and this continues to accelerate. We can't thank you all enough for your help and support! 🚀 [![Star History Chart](https://api.star-history.com/svg?repos=go-task/task&type=Date)](https://star-history.com/#go-task/task&Date) ## What's next? :thinking: It's extremely motivating to see so many people using and loving Task. However, in this time we've also seen an increase in the number of issues and feature requests. In particular, issues that require some kind of breaking change to Task. This isn't a bad thing, but as we grow we need to be more responsible about how we address these changes in a way that ensures stability and compatibility for existing users and their Taskfiles. At this point you're probably thinking something like: > "But you use [semantic versioning][semver] - Just release a new major version > with your breaking changes." And you'd be right... sort of. In theory, this sounds great, but the reality is that we don't have the time to commit to a major overhaul of Task in one big bang release. This would require a colossal amount of time and coordination and with full time jobs and personal lives to tend to, this is a difficult commitment to make. Smaller, more frequent major releases are also a significant inconvenience for users as they have to constantly keep up-to-date with our breaking changes. Fortunately, there is a better way. ## What's going to change? :monocle_face: Going forwards, breaking changes will be allowed into _minor_ versions of Task as "experimental features". To access these features users will need opt-in by enabling feature flags. This will allow us to release new features slowly and gather feedback from the community before making them the default behavior in a future major release. To prepare users for the next major release, we will maintain a list of [deprecated features][deprecations] and [experiments][experiments] on our docs website and publish information on how to migrate to the new behavior. You can read the [full breaking change proposal][breaking-change-proposal] and view all the [current experiments and their status][experiments-project] on GitHub including the [Gentle Force][gentle-force-experiment] and [Remote Taskfiles][remote-taskfiles-experiment] experiments. ## What will happen to v2/v3 features? v2 has been [officially deprecated][deprecate-version-2-schema]. If you're still using a Taskfile with `version: "2"` at the top we _strongly recommend_ that you upgrade as soon as possible. Removing v2 will allow us to tidy up the codebase and focus on new functionality instead. When v4 is released, we will continue to support v3 for a period of time (bug fixes etc). However, since we are moving from a backward-compatibility model to a forwards-compatibility model, **v4 itself will not be backwards compatible with v3**. ## v4 When? :eyes: :man_shrugging: When it's ready. In all seriousness, we don't have a timeline for this yet. We'll be working on the most serious deficiencies of the v3 API first and regularly evaluating the state of the project. When we feel its in a good, stable place and we have a clear upgrade path for users and a number of stable experiments, we'll start to think about v4. ## :wave: Final thoughts Task is growing fast and we're excited to see where it goes next. We hope that the steps we're taking to improve the project and our process will help us to continue to grow. As always, if you have any questions or feedback, we encourage you to comment on or open [issues][issues] and [discussions][discussions] on GitHub. Alternatively, you can join us on [Discord][discord]. I plan to write more of these blog posts in the future on a variety of Task-related topics, so make sure to check in occasionally and see what we're up to! [vscode-task]: https://github.com/go-task/vscode-task [crowdin]: https://crowdin.com [contributors]: https://github.com/go-task/task/graphs/contributors [semver]: https://semver.org [breaking-change-proposal]: https://github.com/go-task/task/discussions/1191 [experiments]: https://taskfile.dev/experiments [deprecations]: https://taskfile.dev/deprecations [deprecate-version-2-schema]: https://github.com/go-task/task/issues/1197 [issues]: https://github.com/go-task/task/issues [discussions]: https://github.com/go-task/task/discussions [discord]: https://discord.gg/6TY36E39UK [experiments-project]: https://github.com/orgs/go-task/projects/1 [gentle-force-experiment]: https://github.com/go-task/task/issues/1200 [remote-taskfiles-experiment]: https://github.com/go-task/task/issues/1317 ================================================ FILE: website/src/blog/windows-core-utils.md ================================================ --- title: 'Announcing Built-in Core Utilities for Windows' description: The journey of enhancing Windows support in Task. author: andreynering date: 2025-09-15 outline: deep editLink: false --- # Announcing Built-in Core Utilities for Windows When I started Task back in 2017, one of my biggest goals was to build a task runner that would work well on all major platforms, including Windows. At the time, I was using Windows as my main platform, and it caught my attention how much of a pain it was to get a working version of Make on Windows, for example. ## The very beginning The very first versions, which looked very prototyp-ish, already supported Windows, but it was falling back to Command Prompt (`cmd.exe`) to run commands if `bash` wasn't available in the system. That didn't mean you couldn't run Bash commands on Windows necessarily, because if you used Task inside Git Bash, it would expose `bash.exe` into your `$PATH`, which made possible for Task to use it. Outside of it, you would be out of luck, though, because running on Command Prompt meant that the commands wouldn't be really compatible. ## Adopting a shell interpreter I didn't take too much time to discover that there was [a shell interpreter for Go that was very solid][mvdan], and I quickly adopted it to ensure we would be able to run commands with consistency across all platforms. It was fun because once adopted, I had the opportunity to [make some contributions to make it more stable][mvdan-prs], which I'm sure the author appreciated. ## The lack of core utilities There was one important thing missing, though. If you needed to use any core utilities on Windows, like copying files with `cp`, moving with `mv`, creating a directory with `mkdir -p`, that likely would just fail :boom:. There were workarounds, of course. You could run `task` inside Git Bash which exposed core utils in `$PATH` for you, or you could install these core utils manually (there are a good number of alternative implementations available for download). That was still far from ideal, though. One of my biggest goals with Task is that it should "just work", even on Windows. Requiring additional setup to make things work is exactly what I wanted to avoid. ## They finally arrive! And here we are, in 2025, 8 years after the initial release. We might be late, but I'm happy nonetheless. From now on, the following core utilities will be available on Windows. This is the start. We want to add more with time. - `base64` - `cat` - `chmod` - `cp` - `find` - `gzip` - `ls` - `mkdir` - `mktemp` - `mv` - `rm` - `shasum` - `tar` - `touch` - `xargs` ## How we made this possible This was made possible via a collaboration with the maintainers of other Go projects. ### u-root/u-root We are using the core utilities implementations in Go from the [u-root][u-root] project. It wasn't as simple as it sounds because they have originally implemented every core util as a standalone `main` package, which means we couldn't just import and use them as libraries. We had some discussion and we agreed on a common [interface][uroot-interface] and [base implementation][uroot-base]. Then, I refactored one-by-one of the core utils in the list above. This is the reason we don't have all of them: there are too many! But the good news is that we can refactor more with time and include them in Task. ### mvdan/sh The other collaboration was with the maintainer of the shell interpreter. He agreed on having [an official middleware][middleware] to expose these core utilities. This means that other projects that use the shell interpreter can also benefit from this work, and as more utilities are included, those projects will benefit as well. ## Can I choose whether to use them or not? Yes. We added a new environment variable called [`TASK_CORE_UTILS`][task-core-utils] to control if the Go implementations are used or not. By default, this is `true` on Windows and `false` on other platforms. You can override it like this: ```bash # Enable, even on non-Windows platforms env TASK_CORE_UTILS=1 task ... # Disable, even on Windows env TASK_CORE_UTILS=0 task ... ``` We'll consider making this enabled by default on all platforms in the future. In the meantime, we're still using the system core utils on non-Windows platforms to avoid regressions as the Go implementations may not be 100% compatible with the system ones. ## Feedback If you have any feedback about this feature, join our [Discord server][discord] or [open an issue][gh-issue] on GitHub. Also, if Task is useful for you or your company, consider [sponsoring the project][sponsor]! [mvdan]: https://github.com/mvdan/sh [mvdan-prs]: https://github.com/mvdan/sh/pulls?q=is%3Apr+author%3Aandreynering+is%3Aclosed+sort%3Acreated-asc [u-root]: https://github.com/u-root/u-root [uroot-interface]: https://github.com/u-root/u-root/blob/main/pkg/core/command.go [uroot-base]: https://github.com/u-root/u-root/blob/main/pkg/core/base.go [middleware]: https://github.com/mvdan/sh/blob/master/moreinterp/coreutils/coreutils.go [task-core-utils]: /docs/reference/environment#task-core-utils [discord]: https://discord.com/invite/6TY36E39UK [gh-issue]: https://github.com/go-task/task/issues [sponsor]: /donate ================================================ FILE: website/src/docs/changelog.md ================================================ --- title: Changelog outline: deep editLink: false --- # Changelog ::: v-pre ## v3.49.1 - 2026-03-08 * Reverted #2632 for now, which caused some regressions. That change will be reworked (#2720, #2722, #2723). ## v3.49.0 - 2026-03-07 - Fixed included Taskfiles with `watch: true` not triggering watch mode when called from the root Taskfile (#2686, #1763 by @trulede). - Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths (#2656 by @Trim21). - Fixed remote Git Taskfiles timing out when resolving includes after accepting the trust prompt (#2669, #2668 by @vmaerten). - Fixed unclear error message when Taskfile search stops at a directory ownership boundary (#2682, #1683 by @trulede). - Fixed global variables from imported Taskfiles not resolving `ref:` values correctly (#2632 by @trulede). - Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed environment variable, making CI and container configuration easier (#2607, #1066 by @vmaerten). ## v3.48.0 - 2026-01-26 - Fixed `if:` conditions when using to check dynamic variables. Also, skip variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten). - Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by @trulede). - Included Taskfiles with `silent: true` now properly propagate silence to their tasks, while still allowing individual tasks to override with `silent: false` (#2640, #1319 by @trulede). - Added TLS certificate options for Remote Taskfiles: use `--cacert` for self-signed certificates and `--cert`/`--cert-key` for mTLS authentication (#2537, #2242 by @vmaerten). ## v3.47.0 - 2026-01-24 - Fixed remote git Taskfiles: cloning now works without explicit ref, and directory includes are properly resolved (#2602 by @vmaerten). - For `output: prefixed`, print `prefix:` if set instead of task name (#1566, #2633 by @trulede). - Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by @trulede). - Task aliases can now contain wildcards and will match accordingly (e.g., `s-*` as alias for `start-*`) (#1900, #2234 by @vmaerten). - Added conditional execution with the `if` field: skip tasks, commands, or task calls based on shell exit codes or template expressions like `{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten). - Task can now interactively prompt for missing required variables when running in a TTY, with support for enum selection menus. Enable with `--interactive` flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten). ## v3.46.4 - 2025-12-24 - Fixed regressions in completion script for Fish (#2591, #2604, #2592 by @WinkelCode). ## v3.46.3 - 2025-12-19 - Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten). ## v3.46.2 - 2025-12-18 - Fixed a regression on previous release that affected variables passed via command line (#2588, #2589 by @vmaerten). ## v3.46.1 - 2025-12-18 ### ✨ Features - A small behavior change was made to dependencies. Task will now wait for all dependencies to finish running before continuing, even if any of them fail. To opt for the previous behavior, set `failfast: true` either on your `.taskrc.yml` or per task, or use the `--failfast` flag, which will also work for `--parallel` (#1246, #2525 by @andreynering). - The `--summary` flag now displays `vars:` (both global and task-level), `env:`, and `requires:` sections. Dynamic variables show their shell command (e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by @vmaerten). - Improved performance of fuzzy task name matching by implementing lazy initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten). - Added LLM-optimized documentation via VitePress plugin, generating `llms.txt` and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten). - Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to skip confirmation prompts for specified hosts when using Remote Taskfiles (#2491, #2473 by @maciejlech). - When running in GitHub Actions, Task now automatically emits error annotations on failure, improving visibility in workflow summaries (#2568 by @vmaerten). - The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES` variable (#2577, #2479 by @semihbkgr). - Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing flags and dynamic experimental feature detection (#2532 by @vmaerten). - Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536, #1944 by @vmaerten). - Shell completion now works when Task is installed or aliased under a different binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten). - Some small fixes and improvements were made to `task --init` and to the default Taskfile it generates (#2433 by @andreynering). - Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to customize the cache directory for Remote Taskfiles (#2572 by @vmaerten). - Zsh completion now supports zstyle verbose option to show or hide task descriptions (#2571 by @vmaerten). - Task now automatically enables colored output in CI environments (GitHub Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by @vmaerten). - Added color taskrc option to explicitly enable or disable colored output globally (#2569 by @vmaerten). - Improved Git Remote Taskfiles by switching to go-getter: SSH authentication now works out of the box and `applyOf` is properly supported (#2512 by @vmaerten). ### 🐛 Fixes - Fix RPM upload to Cloudsmith by including the version in the filename to ensure unique filenames (#2507 by @vmaerten). - Fix `run: when_changed` to work properly for Taskfiles included multiple times (#2508, #2511 by @trulede). - Fixed Zsh and Fish completions to stop suggesting task names after `--` separator, allowing proper CLI_ARGS completion (#1843, #1844 by @boiledfroginthewell). - Watch mode (`--watch`) now always runs the task, regardless of `run: once` or `run: when_changed` settings (#2566, #1388 by @trulede). - Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in root-level vars section (#2403, #2397 by @trulede, @vmaerten). - Fixed a bug where `ignore_error` was ignored when using `task:` to call another task (#2552, #363 by @trulede). - Fixed Zsh completion not suggesting global tasks when using `-g`/`--global` flag (#1574, #2574 by @vmaerten). - Fixed Fish completion failing to parse task descriptions containing colons (e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten). - Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when using `for` loops in Taskfiles (#2576 by @vmaerten). ## v3.45.5 - 2025-11-11 - Fixed bug that made a generic message, instead of an useful one, appear when a Taskfile could not be found (#2431 by @andreynering). - Fixed a bug that caused an error when including a Remote Git Taskfile (#2438 by @twelvelabs). - Fixed issue where `.taskrc.yml` was not returned if reading it failed, and corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten). - Improved performance of `--list` and `--list-all` by introducing a faster compilation method that skips source globbing and checksum updates (#1322, #2053 by @vmaerten). - Fixed a concurrency bug with `output: group`. This ensures that begin/end parts won't be mixed up from different tasks (#1208, #2349, #2350 by @trulede). - Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede). - Improve error message when a Taskfile is not found (#2441, #2494 by @vmaerten). - Fixed generic error message `exit status 1` when a dependency task failed (#2286 by @GrahamDennis). - Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork maintained by the official YAML org (#2171, #2434 by @andreynering). - On Windows, the built-in version of the `rm` core utils contains a fix related to the `-f` flag (#2426, [u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464), [mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by @andreynering). ## v3.45.4 - 2025-09-17 - Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423 by @vmaerten). - Fixed a bug where `.taskrc.yml` files in parent folders were not read correctly (#2424 by @vmaerten). - Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by @vmaerten). ## v3.45.3 - 2025-09-15 - Task now includes built-in core utilities to greatly improve compatibility on Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any other common core utility will now work by default on Windows, without extra setup. This is something we wanted to address for many many years, and it's finally being shipped! [Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils). (#197, #2360 by @andreynering). - :sparkles: Built and deployed a [brand new website](https://taskfile.dev) using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by @vmaerten, @andreynering, @pd93). - Began releasing [nightly builds](https://github.com/go-task/task/releases/tag/nightly). This will allow people to test our changes before they are fully released and without having to install Go to build them (#2358 by @vmaerten). - Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or `$HOME/.taskrc.yml`. Check out our new [configuration guide](https://taskfile.dev/docs/reference/config) for more details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93). - Added experiments to the taskrc schema to clarify the expected keys and values (#2235 by @vmaerten). - Added support for new properties in `.taskrc.yml`: insecure, verbose, concurrency, remote offline, remote timeout, and remote expiry. :warning: Note: setting offline via environment variable is no longer supported. (#2389 by @vmaerten) - Added a `--nested` flag when outputting tasks using `--list --json`. This will output tasks in a nested structure when tasks are namespaced (#2415 by @pd93). - Enhanced support for tasks with wildcards: they are now logged correctly, and wildcard parameters are fully considered during fingerprinting (#1808, #1795 by @vmaerten). - Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417 by @trulede). #### Package API - Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten). #### Other news We recently released our [official GitHub Action](https://github.com/go-task/setup-task). This is based on the fantastic work by the Arduino team who created and maintained the community version. Now that this is officially adopted, fixes/updates should be more timely. We have already merged a couple of longstanding PRs in our [first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by @pd93, @shrink, @trim21 and all the previous contributors to [arduino/setup-task](https://github.com/arduino/setup-task/)). ## v3.45.0-v3.45.2 - 2025-09-15 Failed due to an issue with our release process. ## v3.44.1 - 2025-07-23 - Internal tasks will no longer be shown as suggestions since they cannot be called (#2309, #2323 by @maxmzkrcensys) - Fixed install script for some ARM platforms (#1516, #2291 by @trulede). - Fixed a regression where fingerprinting was not working correctly if the path to you Taskfile contained a space (#2321, #2322 by @pd93). - Reverted a breaking change to `randInt` (#2312, #2316 by @pd93). - Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests (#2265 by @pd93). ## v3.44.0 - 2025-06-08 - Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by @pd93). - Added new `CLI_ARGS_LIST` array variable which contains the arguments passed to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a string). (#2138, #2139, #2140 by @pd93). - Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93). - Added `task` field the `--list --json` output (#2256 by @aleksandersh). - Added the ability to [pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning) by specifying a checksum. This works with both local and remote Taskfiles (#2222, #2223 by @pd93). - When using the [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317), any credentials used in the URL will now be redacted in Task's output (#2100, #2220 by @pd93). - Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200 by @vmaerten). - Fixed a bug where taskfiles in directories containing spaces created directories in the wrong location (#2208, #2216 by @pd93). - Added support for dual JSON schema files, allowing changes without affecting the current schema. The current schemas will only be updated during releases. (#2211 by @vmaerten). - Improved fingerprint documentation by specifying that the method can be set at the root level to apply to all tasks (#2233 by @vmaerten). - Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by @wazazaby, #2271 by @andreynering). ## v3.43.3 - 2025-04-27 Reverted the changes made in #2113 and #2186 that affected the `USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and #2208. ## v3.43.2 - 2025-04-21 - Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by @vmaerten). ## v3.43.1 - 2025-04-21 - Significant improvements were made to the watcher. We migrated from [watcher](https://github.com/radovskyb/watcher) to [fsnotify](https://github.com/fsnotify/fsnotify). The former library used polling, which means Task had a high CPU usage when watching too many files. `fsnotify` uses proper the APIs from each operating system to watch files, which means a much better performance. The default interval changed from 5 seconds to 100 milliseconds, because now it configures the wait time for duplicated events, instead of the polling time (#2048 by @andreynering, #1508, #985, #1179). - The [Map Variables experiment](https://github.com/go-task/task/issues/1585) was made generally available so you can now [define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables) (#1585, #1547, #2081 by @pd93). - Wildcards can now [match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072, #2121 by @pd93). - Added the ability to [loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files). This works the same way as looping over sources (#2151 by @sedyh). - Added the ability to resolve variables when defining an include variable (#2108, #2113 by @pd93). - A few changes have been made to the [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317) (#1402, #2176 by @pd93): - Cached files are now prioritized over remote ones. - Added an `--expiry` flag which sets the TTL for a remote file cache. By default the value will be 0 (caching disabled). If Task is running in offline mode or fails to make a connection, it will fallback on the cache. - `.taskrc` files can now be used from subdirectories and will be searched for recursively up the file tree in the same way that Taskfiles are (#2159, #2166 by @pd93). - The default taskfile (output when using the `--init` flag) is now an embedded file in the binary instead of being stored in the code (#2112 by @pd93). - Improved the way we report the Task version when using the `--version` flag or `{{.TASK_VERSION}}` variable. This should now be more consistent and easier for package maintainers to use (#2131 by @pd93). - Fixed a bug where globstar (`**`) matching in `sources` only resolved the first result (#2073, #2075 by @pd93). - Fixed a bug where sorting tasks by "none" would use the default sorting instead of leaving tasks in the order they were defined (#2124, #2125 by @trulede). - Fixed Fish completion on newer Fish versions (#2130 by @atusy). - Fixed a bug where undefined/null variables resolved to an empty string instead of `nil` (#1911, #2144 by @pd93). - The `USER_WORKING_DIR` special now will now properly account for the `--dir` (`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering). - Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy). - Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten). #### Package API - The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now uses the functional options pattern (#2085, #2147, #2148 by @pd93). - The functional options for the [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) and [`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet) types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by @pd93). - [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) no longer accepts a [`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node). Instead nodes are passed directly into the [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read) method (#2169 by @pd93). - [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read) also now accepts a [`context.Context`](https://pkg.go.dev/context#Context) (#2176 by @pd93). ## v3.42.1 - 2025-03-10 - Fixed a bug where some special variables caused a type error when used global variables (#2106, #2107 by @pd93). ## v3.42.0 - 2025-03-08 - Made `--init` less verbose by default and respect `--silent` and `--verbose` flags (#2009, #2011 by @HeCorr). - `--init` now accepts a file name or directory as an argument (#2008, #2018 by @HeCorr). - Fix a bug where an HTTP node's location was being mutated incorrectly (#2007 by @jeongukjae). - Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by @vmaerten). - Use only the relevant checker (timestamp or checksum) to improve performance (#2029, #2031 by @vmaerten). - Print warnings when attempting to enable an inactive experiment or an active experiment with an invalid value (#1979, #2049 by @pd93). - Refactored the experiments package and added tests (#2049 by @pd93). - Show allowed values when a variable with an enum is missing (#2027, #2052 by @vmaerten). - Refactored how snippets in error work and added tests (#2068 by @pd93). - Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by @pd93). - Fixed a bug in the Taskfile schema where `defer` statements in the shorthand `cmds` syntax were not considered valid (#2068 by @pd93). - Refactored how task sorting functions work (#1798 by @pd93). - Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable experiments (similar to `.env`) (#1982 by @vmaerten). - Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086 by @pd93). - Allow `matrix` to use references to other variables (#2065, #2069 by @pd93). - Fixed a bug where, when a dynamic variable is provided, even if it is not used, all other variables become unavailable in the templating system within the include (#2092 by @vmaerten). #### Package API Unlike our CLI tool, [Task's package API is not currently stable](https://taskfile.dev/reference/package). In an effort to ease the pain of breaking changes for our users, we will be providing changelogs for our package API going forwards. The hope is that these changes will provide a better long-term experience for our users and allow to stabilize the API in the future. #121 now tracks this piece of work. - Bumped the minimum required Go version to 1.23 (#2059 by @pd93). - [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile) (#2011, ff8c913 by @HeCorr and @pd93) - No longer accepts an `io.Writer` (output is now the caller's responsibility). - The path argument can now be a filename OR a directory. - The function now returns the full path of the generated file. - [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo) now accepts a string instead of the arguments required to generate a snippet (#2068 by @pd93). - The caller is now expected to create the snippet themselves (see below). - [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet) and related code moved from the `errors` package to the `taskfile` package (#2068 by @pd93). - Renamed `TaskMissingRequiredVars` to [`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError) (#2052 by @vmaerten). - Renamed `TaskNotAllowedVars` to [`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError) (#2052 by @vmaerten). - The [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) is now constructed using the functional options pattern (#2082 by @pd93). - Removed our internal `logger.Logger` from the entire `taskfile` package (#2082 by @pd93). - Users are now expected to pass a custom debug/prompt functions into [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader) if they want this functionality by using the new [`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc) and [`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc) functional options. - Remove `Range` functions in the `taskfile/ast` package in favour of new iterator functions (#1798 by @pd93). - `ast.Call` was moved from the `taskfile/ast` package to the main `task` package (#2084 by @pd93). - `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the `task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93). - The `Compiler` and its `GetVariables` and `FastGetVariables` methods were moved from the `internal/compiler` package to the main `task` package (#2084 by @pd93). ## v3.41.0 - 2025-01-18 - Fixed an issue where dynamic variables were not properly logged in verbose mode (#1920, #1921 by @mgbowman). - Support `silent` for defer statements (#1877, #1879 by @danilobuerger). - Added an option to exclude some tasks from being included (#1859 by @vmaerten). - Fixed an issue where a required variable was incorrectly handled in a template function (#1950, #1962 by @vmaerten). - Expose a new `TASK_DIR` special variable, which will contain the absolute path of task directory. (#1959, #1961 by @vmaerten). - Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by @pd93, @GrahamDennis and @trim21). - Refactored internal ordered map implementation to use [github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap) (#1797 by @pd93). - Fixed a bug where variables defined at the task level were being ignored in the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko) - The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds` (#1872 by @niklasr22). - Updated [installation docs](https://taskfile.dev/installation) and added pip installation method (#935, #1989 by @pd93). - Fixed a bug where dynamic variables could not access environment variables (#630, #1869 by @rohm1 and @pd93). - Disable version check for use as an external library (#1938 by @leaanthony). ## v3.40.1 - 2024-12-06 - Fixed a security issue in `git-urls` by switching to the maintained fork `chainguard-dev/git-urls` (#1917 by @AlekSi). - Added missing `platforms` property to `cmds` that use `for` (#1915 by @dkarter). - Added misspell linter to check for misspelled English words (#1883 by @christiandins). ## v3.40.0 - 2024-11-05 - Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in for loops (#1822, #1823 by @stawii). - Added a new `TASK_OFFLINE` environment variable to configure the `--offline` flag and expose it as a special variable in the templating system (#1470, #1716 by @vmaerten and @pd93). - Fixed a bug where multiple remote includes caused all prompts to display without waiting for user input (#1832, #1833 by @vmaerten and @pd93). - When using the "[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)". experiment, you can now include Taskfiles from Git repositories (#1652 by @vmaerten). - Improved the error message when a dotenv file cannot be parsed (#1842 by @pbitty). - Fix issue with directory when using the remote experiment (#1757 by @pbitty). - Fixed an issue where a special variable was used in combination with a dotenv file (#1232, #1810 by @vmaerten). - Refactor the way Task reads Taskfiles to improve readability (#1771 by @pbitty). - Added a new option to ensure variable is within the list of values (#1827 by @vmaerten). - Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina). - Added new template function: `numCPU`, which returns the number of logical CPUs usable (#1890, #1887 by @Amoghrd). - Fixed a bug where non-nil, empty dynamic variables are returned as an empty interface (#1903, #1904 by @pd93). ## v3.39.2 - 2024-09-19 - Fix dynamic variables not working properly for a defer: statement (#1803, #1818 by @vmaerten). ## v3.39.1 - 2024-09-18 - Added Renovate configuration to automatically create PRs to keep dependencies up to date (#1783 by @vmaerten). - Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten). - Fixed a bug where ZSH and PowerShell completions did not work when using the recommended method. (#1813, #1809 by @vmaerten and @shirayu) - Fix variables not working properly for a `defer:` statement (#1803, #1814 by @vmaerten and @andreynering). ## v3.39.0 - 2024-09-07 - Added [Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence) (#1038, #1633 by @vmaerten). - Added a CI lint job to ensure that the docs are updated correctly (#1719 by @vmaerten). - Updated minimum required Go version to 1.22 (#1758 by @pd93). - Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering). - Expose a new `ALIAS` special variable, which will contain the alias used to call the current task. Falls back to the task name. (#1764 by @DanStory). - Fixed `TASK_REMOTE_DIR` environment variable not working when the path was absolute. (#1715 by @vmaerten). - Added an option to declare an included Taskfile as flattened (#1704 by @vmaerten). - Added a new [`--completion` flag](https://taskfile.dev/installation/#setup-completions) to output completion scripts for various shells (#293, #1157 by @pd93). - This is now the preferred way to install completions. - The completion scripts in the `completion` directory [are now deprecated](https://taskfile.dev/deprecations/completion-scripts/). - Added the ability to [loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix) (#1766, #1767, #1784 by @pd93). - Fixed a bug in fish completion where aliases were not displayed (#1781, #1782 by @vmaerten). - Fixed panic when having a flattened included Taskfile that contains a `default` task (#1777, #1778 by @vmaerten). - Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten). ## v3.38.0 - 2024-06-30 - Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering). - Some YAML parsing errors will now show in a more user friendly way (#1619 by @pd93). - Prefixed outputs will now be colorized by default (#1572 by @AlexanderArvidsson) - [References](https://taskfile.dev/usage/#referencing-other-variables) are now generally available (no experiments required) (#1654 by @pd93). - Templating functions can now be used in references (#1645, #1654 by @pd93). - Added a new [templating reference page](https://taskfile.dev/reference/templating/) to the documentation (#1614, #1653 by @pd93). - If using the [Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1), references are available by [prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references) (#1654 by @pd93). - If using the [Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2), the `yaml` and `json` keys are no longer available (#1654 by @pd93). - Added a new `TASK_REMOTE_DIR` environment variable to configure where cached remote Taskfiles are stored (#1661 by @vmaerten). - Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639 by @vmaerten). - Improved the readability of cached remote Taskfile filenames (#1636 by @vmaerten). - Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by @mengzhuo). - Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith). - Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93). - Fixed JSON Schema to disallow invalid properties (#1657 by @pd93). - Fixed version checks not working as intended (#872, #1663 by @vmaerten). - Fixed a bug where included tasks were run multiple times even if `run: once` was set (#852, #1655 by @pd93). - Fixed some bugs related to column formatting in the terminal (#1350, #1637, #1656 by @vmaerten). ## v3.37.2 - 2024-05-12 - Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93). - Fixed a bug where includes Taskfile variable were not being merged correctly (#1643, #1649 by @pd93). ## v3.37.1 - 2024-05-09 - Fix bug where non-string values (numbers, bools) added to `env:` weren't been correctly exported (#1640, #1641 by @vmaerten and @andreynering). ## v3.37.0 - 2024-05-08 - Released the [Any Variables experiment](https://taskfile.dev/blog/any-variables), but [_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925) (#1415, #1547 by @pd93). - Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563, #1607 by @pd93). - Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by @pd93). - Fix error when a file or directory in the project contained a special char like `&`, `(` or `)` (#1551, #1584 by @andreynering). - Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt) - Added support for `~` on ZSH completions (#1613 by @jwater7). - Added the ability to pass variables by reference using Go template syntax when the [Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is enabled (#1612 by @pd93). - Added support for environment variables in the templating engine in `includes` (#1610 by @vmaerten). ## v3.36.0 - 2024-04-08 - Added support for [looping over dependencies](https://taskfile.dev/usage/#looping-over-dependencies) (#1299, #1541 by @pd93). - When using the "[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)" experiment, you are now able to use [remote Taskfiles as your entrypoint](https://taskfile.dev/experiments/remote-taskfiles/#root-remote-taskfiles). - `includes` in remote Taskfiles will now also resolve correctly (#1347 by @pd93). - When using the "[Any Variables](https://taskfile.dev/experiments/any-variables/)" experiments, templating is now supported in collection-type variables (#1477, #1511, #1526 by @pd93). - Fixed a bug where variables being passed to an included Taskfile were not available when defining global variables (#1503, #1533 by @pd93). - Improved support to customized colors by allowing 8-bit colors and multiple ANSI attributes (#1576 by @pd93). ## v3.35.1 - 2024-03-04 - Fixed a bug where the `TASKFILE_DIR` variable was sometimes incorrect (#1522, #1523 by @pd93). - Added a new `TASKFILE` special variable that holds the root Taskfile path (#1523 by @pd93). - Fixed various issues related to running a Taskfile from a subdirectory (#1529, #1530 by @pd93). ## v3.35.0 - 2024-02-28 - Added support for [wildcards in task names](https://taskfile.dev/usage/#wildcard-arguments) (#836, #1489 by @pd93). - Added the ability to [run Taskfiles via stdin](https://taskfile.dev/usage/#reading-a-taskfile-from-stdin) (#655, #1483 by @pd93). - Bumped minimum Go version to 1.21 (#1500 by @pd93). - Fixed bug related to the `--list` flag (#1509, #1512 by @pd93, #1514, #1520 by @pd93). - Add mention on the documentation to the fact that the variable declaration order is respected (#1510 by @kirkrodrigues). - Improved style guide docs (#1495 by @iwittkau). - Removed duplicated entry for `requires` on the API docs (#1491 by @teatimeguest). ## v3.34.1 - 2024-01-27 - Fixed prompt regression on [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles/) (#1486, #1487 by @pd93). ## v3.34.0 - 2024-01-25 - Removed support for `version: 2` schemas. See the [deprecation notice on our website](https://taskfile.dev/deprecations/version-2-schema) (#1197, #1447 by @pd93). - Fixed a couple of issues in the JSON Schema + added a CI step to ensure it's correct (#1471, #1474, #1476 by @sirosen). - Added [Any Variables experiment proposal 2](https://taskfile.dev/experiments/any-variables/?proposal=2) (#1415, #1444 by @pd93). - Updated the experiments and deprecations documentation format (#1445 by @pd93). - Added new template function: `spew`, which can be used to print variables for debugging purposes (#1452 by @pd93). - Added new template function: `merge`, which can be used to merge any number of map variables (#1438, #1464 by @pd93). - Small change on the API when using as a library: `call.Direct` became `call.Indirect` (#1459 by @pd93). - Refactored the public `read` and `taskfile` packages and introduced `taskfile/ast` (#1450 by @pd93). - `ast.IncludedTaskfiles` renamed to `ast.Includes` and `orderedmap` package renamed to `omap` plus some internal refactor work (#1456 by @pd93). - Fix zsh completion script to allow lowercase `taskfile` file names (#1482 by @xontab). - Improvements on how we check the Taskfile version (#1465 by @pd93). - Added a new `ROOT_TASKFILE` special variable (#1468, #1469 by @pd93). - Fix experiment flags in `.env` when the `--dir` or `--taskfile` flags were used (#1478 by @pd93). ## v3.33.1 - 2023-12-21 - Added support for looping over map variables with the [Any Variables experiment](https://taskfile.dev/experiments/any-variables) enabled (#1435, #1437 by @pd93). - Fixed a bug where dynamic variables were causing errors during fast compilation (#1435, #1437 by @pd93) ## v3.33.0 - 2023-12-20 - Added [Any Variables experiment](https://taskfile.dev/experiments/any-variables) (#1415, #1421 by @pd93). - Updated Docusaurus to v3 (#1432 by @pd93). - Added `aliases` to `--json` flag output (#1430, #1431 by @pd93). - Added new `CLI_FORCE` special variable containing whether the `--force` or `--force-all` flags were set (#1412, #1434 by @pd93). ## v3.32.0 - 2023-11-29 - Added ability to exclude some files from `sources:` by using `exclude:` (#225, #1324 by @pd93 and @andreynering). - The [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) now prefers remote files over cached ones by default (#1317, #1345 by @pd93). - Added `--timeout` flag to the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) (#1317, #1345 by @pd93). - Fix bug where dynamic `vars:` and `env:` were being executed when they should actually be skipped by `platforms:` (#1273, #1377 by @andreynering). - Fix `schema.json` to make `silent` valid in `cmds` that use `for` (#1385, #1386 by @iainvm). - Add new `--no-status` flag to skip expensive status checks when running `task --list --json` (#1348, #1368 by @amancevice). ## v3.31.0 - 2023-10-07 - Enabled the `--yes` flag for the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) (#1317, #1344 by @pd93). - Add ability to set `watch: true` in a task to automatically run it in watch mode (#231, #1361 by @andreynering). - Fixed a bug on the watch mode where paths that contained `.git` (like `.github`), for example, were also being ignored (#1356 by @butuzov). - Fixed a nil pointer error when running a Taskfile with no contents (#1341, #1342 by @pd93). - Added a new [exit code](https://taskfile.dev/api/#exit-codes) (107) for when a Taskfile does not contain a schema version (#1342 by @pd93). - Increased limit of maximum task calls from 100 to 1000 for now, as some people have been reaching this limit organically now that we have loops. This check exists to detect recursive calls, but will be removed in favor of a better algorithm soon (#1321, #1332). - Fixed templating on descriptions on `task --list` (#1343 by @blackjid). - Fixed a bug where precondition errors were incorrectly being printed when task execution was aborted (#1337, #1338 by @sylv-io). ## v3.30.1 - 2023-09-14 - Fixed a regression where some special variables weren't being set correctly (#1331, #1334 by @pd93). ## v3.30.0 - 2023-09-13 - Prep work for Remote Taskfiles (#1316 by @pd93). - Added the [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) as a draft (#1152, #1317 by @pd93). - Improve performance of content checksumming on `sources:` by replacing md5 with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking change because checksums will be invalidated when upgrading to this release (#1325 by @ReillyBrogan). ## v3.29.1 - 2023-08-26 - Update to Go 1.21 (bump minimum version to 1.20) (#1302 by @pd93) - Fix a missing a line break on log when using `--watch` mode (#1285, #1297 by @FilipSolich). - Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering). - Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by @andarto, #1309 by @andreynering). - Fix bug on `--status` flag. Running this flag should not have side-effects: it should not update the checksum on `.task`, only report its status (#1305, #1307 by @visciang, #1313 by @andreynering). ## v3.28.0 - 2023-07-24 - Added the ability to [loop over commands and tasks](https://taskfile.dev/usage/#looping-over-values) using `for` (#82, #1220 by @pd93). - Fixed variable propagation in multi-level includes (#778, #996, #1256 by @hudclark). - Fixed a bug where the `--exit-code` code flag was not returning the correct exit code when calling commands indirectly (#1266, #1270 by @pd93). - Fixed a `nil` panic when a dependency was commented out or left empty (#1263 by @neomantra). ## v3.27.1 - 2023-06-30 - Fix panic when a `.env` directory (not file) is present on current directory (#1244, #1245 by @pd93). ## v3.27.0 - 2023-06-29 - Allow Taskfiles starting with lowercase characters (#947, #1221 by @pd93). - e.g. `taskfile.yml`, `taskfile.yaml`, `taskfile.dist.yml` & `taskfile.dist.yaml` - Bug fixes were made to the [npm installation method](https://taskfile.dev/installation/#npm). (#1190, by @sounisi5011). - Added the [gentle force experiment](https://taskfile.dev/experiments/gentle-force) as a draft (#1200, #1216 by @pd93). - Added an `--experiments` flag to allow you to see which experiments are enabled (#1242 by @pd93). - Added ability to specify which variables are required in a task (#1203, #1204 by @benc-uk). ## v3.26.0 - 2023-06-10 - Only rewrite checksum files in `.task` if the checksum has changed (#1185, #1194 by @deviantintegral). - Added [experiments documentation](https://taskfile.dev/experiments) to the website (#1198 by @pd93). - Deprecated `version: 2` schema. This will be removed in the next major release (#1197, #1198, #1199 by @pd93). - Added a new `prompt:` prop to set a warning prompt to be shown before running a potential dangerous task (#100, #1163 by @MaxCheetham, [Documentation](https://taskfile.dev/usage/#warning-prompts)). - Added support for single command task syntax. With this change, it's now possible to declare just `cmd:` in a task, avoiding the more complex `cmds: []` when you have only a single command for that task (#1130, #1131 by @timdp). ## v3.25.0 - 2023-05-22 - Support `silent:` when calling another tasks (#680, #1142 by @danquah). - Improve PowerShell completion script (#1168 by @trim21). - Add more languages to the website menu and show translation progress percentage (#1173 by @misitebao). - Starting on this release, official binaries for FreeBSD will be available to download (#1068 by @andreynering). - Fix some errors being unintendedly suppressed (#1134 by @clintmod). - Fix a nil pointer error when `version` is omitted from a Taskfile (#1148, #1149 by @pd93). - Fix duplicate error message when a task does not exists (#1141, #1144 by @pd93). ## v3.24.0 - 2023-04-15 - Fix Fish shell completion for tasks with aliases (#1113 by @patricksjackson). - The default branch was renamed from `master` to `main` (#1049, #1048 by @pd93). - Fix bug where "up-to-date" logs were not being omitted for silent tasks (#546, #1107 by @danquah). - Add `.hg` (Mercurial) to the list of ignored directories when using `--watch` (#1098 by @misery). - More improvements to the release tool (#1096 by @pd93). - Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93) - Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by @pd93). - Task now has [custom exit codes](https://taskfile.dev/api/#exit-codes) depending on the error (#1114 by @pd93). ## v3.23.0 - 2023-03-26 Task now has an [official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task) contributed by @pd93! :tada: The extension is maintained in a [new repository](https://github.com/go-task/vscode-task) under the `go-task` organization. We're looking to gather feedback from the community so please give it a go and let us know what you think via a [discussion](https://github.com/go-task/vscode-task/discussions), [issue](https://github.com/go-task/vscode-task/issues) or on our [Discord](https://discord.gg/6TY36E39UK)! > **NOTE:** The extension _requires_ v3.23.0 to be installed in order to work. - The website was integrated with [Crowdin](https://crowdin.com/project/taskfile) to allow the community to contribute with translations! [Chinese](https://taskfile.dev/zh-Hans/) is the first language available (#1057, #1058 by @misitebao). - Added task location data to the `--json` flag output (#1056 by @pd93) - Change the name of the file generated by `task --init` from `Taskfile.yaml` to `Taskfile.yml` (#1062 by @misitebao). - Added new `splitArgs` template function (`{{splitArgs "foo bar 'foo bar baz'"}}`) to ensure string is split as arguments (#1040, #1059 by @dhanusaputra). - Fix the value of `{{.CHECKSUM}}` variable in status (#1076, #1080 by @pd93). - Fixed deep copy implementation (#1072 by @pd93) - Created a tool to assist with releases (#1086 by @pd93). ## v3.22.0 - 2023-03-10 - Add a brand new `--global` (`-g`) flag that will run a Taskfile from your `$HOME` directory. This is useful to have automation that you can run from anywhere in your system! ([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), #1029 by @andreynering). - Add ability to set `error_only: true` on the `group` output mode. This will instruct Task to only print a command output if it returned with a non-zero exit code (#664, #1022 by @jaedle). - Fixed bug where `.task/checksum` file was sometimes not being created when task also declares a `status:` (#840, #1035 by @harelwa, #1037 by @pd93). - Refactored and decoupled fingerprinting from the main Task executor (#1039 by @pd93). - Fixed deadlock issue when using `run: once` (#715, #1025 by @theunrepentantgeek). ## v3.21.0 - 2023-02-22 - Added new `TASK_VERSION` special variable (#990, #1014 by @ja1code). - Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007 by @pd93). - Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93) - Added environment variable `FORCE_COLOR` support to force color output. Useful for environments without TTY (#1003 by @automation-stack) ## v3.20.0 - 2023-01-14 - Improve behavior and performance of status checking when using the `timestamp` mode (#976, #977 by @aminya). - Performance optimizations were made for large Taskfiles (#982 by @pd93). - Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html) and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins (#908, #929 by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)). - Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to choose in which platforms that given task or command will be run on. Possible values are operating system (GOOS), architecture (GOARCH) or a combination of the two. Example: `platforms: [linux]`, `platforms: [amd64]` or `platforms: [linux/amd64]`. Other platforms will be skipped (#978, #980 by @leaanthony). ## v3.19.1 - 2022-12-31 - Small bug fix: closing `Taskfile.yml` once we're done reading it (#963, #964 by @HeCorr). - Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file (#961, #971 by @pd93). - Fixed a bug where watch intervals set in the Taskfile were not being respected (#969, #970 by @pd93) - Add `--json` flag (alias `-j`) with the intent to improve support for code editors and add room to other possible integrations. This is basic for now, but we plan to add more info in the near future (#936 by @davidalpert, #764). ## v3.19.0 - 2022-12-05 - Installation via npm now supports [pnpm](https://pnpm.io/) as well ([go-task/go-npm#2](https://github.com/go-task/go-npm/issues/2), [go-task/go-npm#3](https://github.com/go-task/go-npm/pull/3)). - It's now possible to run Taskfiles from subdirectories! A new `USER_WORKING_DIR` special variable was added to add even more flexibility for monorepos (#289, #920). - Add task-level `dotenv` support (#389, #904). - It's now possible to use global level variables on `includes` (#942, #943). - The website got a brand new [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/) by [@DeronW](https://github.com/DeronW). Thanks! ## v3.18.0 - 2022-11-12 - Show aliases on `task --list --silent` (`task --ls`). This means that aliases will be completed by the completion scripts (#919). - Tasks in the root Taskfile will now be displayed first in `--list`/`--list-all` output (#806, #890). - It's now possible to call a `default` task in an included Taskfile by using just the namespace. For example: `docs:default` is now automatically aliased to `docs` (#661, #815). ## v3.17.0 - 2022-10-14 - Add a "Did you mean ...?" suggestion when a task does not exits another one with a similar name is found (#867, #880). - Now YAML parse errors will print which Taskfile failed to parse (#885, #887). - Add ability to set `aliases` for tasks and namespaces (#268, #340, #879). - Improvements to Fish shell completion (#897). - Added ability to set a different watch interval by setting `interval: '500ms'` or using the `--interval=500ms` flag (#813, #865). - Add colored output to `--list`, `--list-all` and `--summary` flags (#845, #874). - Fix unexpected behavior where `label:` was being shown instead of the task name on `--list` (#603, #877). ## v3.16.0 - 2022-09-29 - Add `npm` as new installation method: `npm i -g @go-task/cli` (#870, #871, [npm package](https://www.npmjs.com/package/@go-task/cli)). - Add support to marking tasks and includes as internal, which will hide them from `--list` and `--list-all` (#818). ## v3.15.2 - 2022-09-08 - Fix error when using variable in `env:` introduced in the previous release (#858, #866). - Fix handling of `CLI_ARGS` (`--`) in Bash completion (#863). - On zsh completion, add ability to replace `--list-all` with `--list` as already possible on the Bash completion (#861). ## v3.15.0 - 2022-09-03 - Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly requested feature (#215, #857, [Documentation](https://taskfile.dev/api/#special-variables)). - Follow symlinks on `sources` (#826, #831). - Improvements and fixes to Bash completion (#835, #844). ## v3.14.1 - 2022-08-03 - Always resolve relative include paths relative to the including Taskfile (#822, #823). - Fix ZSH and PowerShell completions to consider all tasks instead of just the public ones (those with descriptions) (#803). ## v3.14.0 - 2022-07-08 - Add ability to override the `.task` directory location with the `TASK_TEMP_DIR` environment variable. - Allow to override Task colors using environment variables: `TASK_COLOR_RESET`, `TASK_COLOR_BLUE`, `TASK_COLOR_GREEN`, `TASK_COLOR_CYAN`, `TASK_COLOR_YELLOW`, `TASK_COLOR_MAGENTA` and `TASK_COLOR_RED` (#568, #792). - Fixed bug when using the `output: group` mode where STDOUT and STDERR were being print in separated blocks instead of in the right order (#779). - Starting on this release, ARM architecture binaries are been released to Snap as well (#795). - i386 binaries won't be available anymore on Snap because Ubuntu removed the support for this architecture. - Upgrade mvdan.cc/sh, which fixes a bug with associative arrays (#785, [mvdan/sh#884](https://github.com/mvdan/sh/issues/884), [mvdan/sh#893](https://github.com/mvdan/sh/pull/893)). ## v3.13.0 - 2022-06-13 - Added `-n` as an alias to `--dry` (#776, #777). - Fix behavior of interrupt (SIGINT, SIGTERM) signals. Task will now give time for the processes running to do cleanup work (#458, #479, #728, #769). - Add new `--exit-code` (`-x`) flag that will pass-through the exit form the command being ran (#755). ## v3.12.1 - 2022-05-10 - Fixed bug where, on Windows, variables were ending with `\r` because we were only removing the final `\n` but not `\r\n` (#717). ## v3.12.0 - 2022-03-31 - The `--list` and `--list-all` flags can now be combined with the `--silent` flag to print the task names only, without their description (#691). - Added support for multi-level inclusion of Taskfiles. This means that included Taskfiles can also include other Taskfiles. Before this was limited to one level (#390, #623, #656). - Add ability to specify vars when including a Taskfile. [Check out the documentation](https://taskfile.dev/#/usage?id=vars-of-included-taskfiles) for more information (#677). ## v3.11.0 - 2022-02-19 - Task now supports printing begin and end messages when using the `group` output mode, useful for grouping tasks in CI systems. [Check out the documentation](http://taskfile.dev/#/usage?id=output-syntax) for more information (#647, #651). - Add `Taskfile.dist.yml` and `Taskfile.dist.yaml` to the supported file name list. [Check out the documentation](https://taskfile.dev/#/usage?id=supported-file-names) for more information (#498, #666). ## v3.10.0 - 2022-01-04 - A new `--list-all` (alias `-a`) flag is now available. It's similar to the exiting `--list` (`-l`) but prints all tasks, even those without a description (#383, #401). - It's now possible to schedule cleanup commands to run once a task finishes with the `defer:` keyword ([Documentation](https://taskfile.dev/#/usage?id=doing-task-cleanup-with-defer), #475, #626). - Remove long deprecated and undocumented `$` variable prefix and `^` command prefix (#642, #644, #645). - Add support for `.yaml` extension (as an alternative to `.yml`). This was requested multiple times throughout the years. Enjoy! (#183, #184, #369, #584, #621). - Fixed error when computing a variable when the task directory do not exist yet (#481, #579). ## v3.9.2 - 2021-12-02 - Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for a important regression on Windows (#619, [mvdan/sh#768](https://github.com/mvdan/sh/issues/768), [mvdan/sh#769](https://github.com/mvdan/sh/pull/769)). ## v3.9.1 - 2021-11-28 - Add logging in verbose mode for when a task starts and finishes (#533, #588). - Fix an issue with preconditions and context errors (#597, #598). - Quote each `{{.CLI_ARGS}}` argument to prevent one with spaces to become many (#613). - Fix nil pointer when `cmd:` was left empty (#612, #614). - Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains two relevant fixes: - Fix quote of empty strings in `shellQuote` (#609, [mvdan/sh#763](https://github.com/mvdan/sh/issues/763)). - Fix issue of wrong environment variable being picked when there's another very similar one (#586, [mvdan/sh#745](https://github.com/mvdan/sh/pull/745)). - Install shell completions automatically when installing via Homebrew (#264, #592, [go-task/homebrew-tap#2](https://github.com/go-task/homebrew-tap/pull/2)). ## v3.9.0 - 2021-10-02 - A new `shellQuote` function was added to the template system (`{{shellQuote "a string"}}`) to ensure a string is safe for use in shell ([mvdan/sh#727](https://github.com/mvdan/sh/pull/727), [mvdan/sh#737](https://github.com/mvdan/sh/pull/737), [Documentation](https://pkg.go.dev/mvdan.cc/sh/v3@v3.4.0/syntax#Quote)) - In this version [mvdan.cc/sh](https://github.com/mvdan/sh) was upgraded with some small fixes and features - The `read -p` flag is now supported (#314, [mvdan/sh#551](https://github.com/mvdan/sh/issues/551), [mvdan/sh#772](https://github.com/mvdan/sh/pull/722)) - The `pwd -P` and `pwd -L` flags are now supported (#553, [mvdan/sh#724](https://github.com/mvdan/sh/issues/724), [mvdan/sh#728](https://github.com/mvdan/sh/pull/728)) - The `$GID` environment variable is now correctly being set (#561, [mvdan/sh#723](https://github.com/mvdan/sh/pull/723)) ## v3.8.0 - 2021-09-26 - Add `interactive: true` setting to improve support for interactive CLI apps (#217, #563). - Fix some `nil` errors (#534, #573). - Add ability to declare an included Taskfile as optional (#519, #552). - Add support for including Taskfiles in the home directory by using `~` (#539, #557). ## v3.7.3 - 2021-09-04 - Add official support to Apple M1 (#564, #567). - Our [official Homebrew tap](https://github.com/go-task/homebrew-tap) will support more platforms, including Apple M1 ## v3.7.0 - 2021-07-31 - Add `run:` setting to control if tasks should run multiple times or not. Available options are `always` (the default), `when_changed` (if a variable modified the task) and `once` (run only once no matter what). This is a long time requested feature. Enjoy! (#53, #359). ## v3.6.0 - 2021-07-10 - Allow using both `sources:` and `status:` in the same task (#411, #427, #477). - Small optimization and bug fix: don't compute variables if not needed for `dotenv:` (#517). ## v3.5.0 - 2021-07-04 - Add support for interpolation in `dotenv:` (#433, #434, #453). ## v3.4.3 - 2021-05-30 - Add support for the `NO_COLOR` environment variable. (#459, [fatih/color#137](https://github.com/fatih/color/pull/137)). - Fix bug where sources were not considering the right directory in `--watch` mode (#484, #485). ## v3.4.2 - 2021-04-23 - On watch, report which file failed to read (#472). - Do not try to catch SIGKILL signal, which are not actually possible (#476). - Improve version reporting when building Task from source using Go Modules (#462, #473). ## v3.4.1 - 2021-04-17 - Improve error reporting when parsing YAML: in some situations where you would just see an generic error, you'll now see the actual error with more detail: the YAML line the failed to parse, for example (#467). - A JSON Schema was published [here](https://json.schemastore.org/taskfile.json) and is automatically being used by some editors like Visual Studio Code (#135). - Print task name before the command in the log output (#398). ## v3.3.0 - 2021-03-20 - Add support for delegating CLI arguments to commands with `--` and a special `CLI_ARGS` variable (#327). - Add a `--concurrency` (alias `-C`) flag, to limit the number of tasks that run concurrently. This is useful for heavy workloads. (#345). ## v3.2.2 - 2021-01-12 - Improve performance of `--list` and `--summary` by skipping running shell variables for these flags (#332). - Fixed a bug where an environment in a Taskfile was not always overridable by the system environment (#425). - Fixed environment from .env files not being available as variables (#379). - The install script is now working for ARM platforms (#428). ## v3.2.1 - 2021-01-09 - Fixed some bugs and regressions regarding dynamic variables and directories (#426). - The [slim-sprig](https://github.com/go-task/slim-sprig) package was updated with the upstream [sprig](https://github.com/Masterminds/sprig). ## v3.2.0 - 2021-01-07 - Fix the `.task` directory being created in the task directory instead of the Taskfile directory (#247). - Fix a bug where dynamic variables (those declared with `sh:`) were not running in the task directory when the task has a custom dir or it was in an included Taskfile (#384). - The watch feature (via the `--watch` flag) got a few different bug fixes and should be more stable now (#423, #365). ## v3.1.0 - 2021-01-03 - Fix a bug when the checksum up-to-date resolution is used by a task with a custom `label:` attribute (#412). - Starting from this release, we're releasing official ARMv6 and ARM64 binaries for Linux (#375, #418). - Task now respects the order of declaration of included Taskfiles when evaluating variables declaring by them (#393). - `set -e` is now automatically set on every command. This was done to fix an issue where multiline string commands wouldn't really fail unless the sentence was in the last line (#403). ## v3.0.1 - 2020-12-26 - Allow use as a library by moving the required packages out of the `internal` directory (#358). - Do not error if a specified dotenv file does not exist (#378, #385). - Fix panic when you have empty tasks in your Taskfile (#338, #362). ## v3.0.0 - 2020-08-16 - On `v3`, all CLI variables will be considered global variables (#336, #341) - Add support to `.env` like files (#324, #356). - Add `label:` to task so you can override the task name in the logs (#321, #337). - Refactor how variables work on version 3 (#311). - Disallow `expansions` on v3 since it has no effect. - `Taskvars.yml` is not automatically included anymore. - `Taskfile_{{OS}}.yml` is not automatically included anymore. - Allow interpolation on `includes`, so you can manually include a Taskfile based on operation system, for example. - Expose `.TASK` variable in templates with the task name (#252). - Implement short task syntax (#194, #240). - Added option to make included Taskfile run commands on its own directory (#260, #144) - Taskfiles in version 1 are not supported anymore (#237). - Added global `method:` option. With this option, you can set a default method to all tasks in a Taskfile (#246). - Changed default method from `timestamp` to `checksum` (#246). - New magic variables are now available when using `status:`: `.TIMESTAMP` which contains the greatest modification date from the files listed in `sources:`, and `.CHECKSUM`, which contains a checksum of all files listed in `status:`. This is useful for manual checking when using external, or even remote, artifacts when using `status:` (#216). - We're now using [slim-sprig](https://github.com/go-task/slim-sprig) instead of [sprig](https://github.com/Masterminds/sprig), which allowed a file size reduction of about 22% (#219). - We now use some colors on Task output to better distinguish message types - commands are green, errors are red, etc (#207). ## v2.8.1 - 2020-05-20 - Fix error code for the `--help` flag (#300, #330). - Print version to stdout instead of stderr (#299, #329). - Suppress `context` errors when using the `--watch` flag (#313, #317). - Support templating on description (#276, #283). ## v2.8.0 - 2019-12-07 - Add `--parallel` flag (alias `-p`) to run tasks given by the command line in parallel (#266). - Fixed bug where calling the `task` CLI only informing global vars would not execute the `default` task. - Add ability to silent all tasks by adding `silent: true` a the root of the Taskfile. ## v2.7.1 - 2019-11-10 - Fix error being raised when `exit 0` was called (#251). ## v2.7.0 - 2019-09-22 - Fixed panic bug when assigning a global variable (#229, #243). - A task with `method: checksum` will now re-run if generated files are deleted (#228, #238). ## v2.6.0 - 2019-07-21 - Fixed some bugs regarding minor version checks on `version:`. - Add `preconditions:` to task (#205). - Create directory informed on `dir:` if it doesn't exist (#209, #211). - We now have a `--taskfile` flag (alias `-t`), which can be used to run another Taskfile (other than the default `Taskfile.yml`) (#221). - It's now possible to install Task using Homebrew on Linux ([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)). ## v2.5.2 - 2019-05-11 - Reverted YAML upgrade due issues with CRLF on Windows (#201, [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)). - Allow setting global variables through the CLI (#192). ## 2.5.1 - 2019-04-27 - Fixed some issues with interactive command line tools, where sometimes the output were not being shown, and similar issues (#114, #190, #200). - Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3. ## v2.5.0 - 2019-03-16 - We moved from the taskfile.org domain to the new fancy taskfile.dev domain. While stuff is being redirected, we strongly recommend to everyone that use [this install script](https://taskfile.dev/#/installation?id=install-script) to use the new taskfile.dev domain on scripts from now on. - Fixed to the ZSH completion (#182). - Add [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task) (#180). ## v2.4.0 - 2019-02-21 - Allow calling a task of the root Taskfile from an included Taskfile by prefixing it with `:` (#161, #172). - Add flag to override the `output` option (#173). - Fix bug where Task was persisting the new checksum on the disk when the Dry Mode is enabled (#166). - Fix file timestamp issue when the file name has spaces (#176). - Mitigating path expanding issues on Windows (#170). ## v2.3.0 - 2019-01-02 - On Windows, Task can now be installed using [Scoop](https://scoop.sh/) (#152). - Fixed issue with file/directory globing (#153). - Added ability to globally set environment variables (#138, #159). ## v2.2.1 - 2018-12-09 - This repository now uses Go Modules (#143). We'll still keep the `vendor` directory in sync for some time, though; - Fixing a bug when the Taskfile has no tasks but includes another Taskfile (#150); - Fix a bug when calling another task or a dependency in an included Taskfile (#151). ## v2.2.0 - 2018-10-25 - Added support for [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles) (#98) - This should be considered experimental. For now, only including local files is supported, but support for including remote Taskfiles is being discussed. If you have any feedback, please comment on #98. - Task now have a dedicated documentation site: https://taskfile.org - Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To check the source code, just take a look at the [docs](https://github.com/go-task/task/tree/main/docs) directory of this repository. Contributions to the documentation is really appreciated. ## v2.1.1 - 2018-09-17 - Fix suggestion to use `task --init` not being shown anymore (when a `Taskfile.yml` is not found) - Fix error when using checksum method and no file exists for a source glob (#131) - Fix signal handling when the `--watch` flag is given (#132) ## v2.1.0 - 2018-08-19 - Add a `ignore_error` option to task and command (#123) - Add a dry run mode (`--dry` flag) (#126) ## v2.0.3 - 2018-06-24 - Expand environment variables on "dir", "sources" and "generates" (#116) - Fix YAML merging syntax (#112) - Add ZSH completion (#111) - Implement new `output` option. Please check out the [documentation](https://github.com/go-task/task#output-syntax) ## v2.0.2 - 2018-05-01 - Fix merging of YAML anchors (#112) ## v2.0.1 - 2018-03-11 - Fixes panic on `task --list` ## v2.0.0 - 2018-03-08 Version 2.0.0 is here, with a new Taskfile format. Please, make sure to read the [Taskfile versions](https://github.com/go-task/task/blob/main/TASKFILE_VERSIONS.md) document, since it describes in depth what changed for this version. - New Taskfile version 2 (#77) - Possibility to have global variables in the `Taskfile.yml` instead of `Taskvars.yml` (#66) - Small improvements and fixes ## v1.4.4 - 2017-11-19 - Handle SIGINT and SIGTERM (#75); - List: print message with there's no task with description; - Expand home dir ("~" symbol) on paths (#74); - Add Snap as an installation method; - Move examples to its own repo; - Watch: also walk on tasks called on on "cmds", and not only on "deps"; - Print logs to stderr instead of stdout (#68); - Remove deprecated `set` keyword; - Add checksum based status check, alternative to timestamp based. ## v1.4.3 - 2017-09-07 - Allow assigning variables to tasks at run time via CLI (#33) - Added support for multiline variables from sh (#64) - Fixes env: remove square braces and evaluate shell (#62) - Watch: change watch library and few fixes and improvements - When use watching, cancel and restart long running process on file change (#59 and #60) ## v1.4.2 - 2017-07-30 - Flag to set directory of execution - Always echo command if is verbose mode - Add silent mode to disable echoing of commands - Fixes and improvements of variables (#56) ## v1.4.1 - 2017-07-15 - Allow use of YAML for dynamic variables instead of $ prefix - `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello` - Add `--list` (or `-l`) flag to print existing tasks - OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`, etc) - Consider task up-to-date on equal timestamps (#49) - Allow absolute path in generates section (#48) - Bugfix: allow templating when calling deps (#42) - Fix panic for invalid task in cyclic dep detection - Better error output for dynamic variables in Taskvars.yml (#41) - Allow template evaluation in parameters ## v1.4.0 - 2017-07-06 - Cache dynamic variables - Add verbose mode (`-v` flag) - Support to task parameters (overriding vars) (#31) (#32) - Print command, also when "set:" is specified (#35) - Improve task command help text (#35) ## v1.3.1 - 2017-06-14 - Fix glob not working on commands (#28) - Add ExeExt template function - Add `--init` flag to create a new Taskfile - Add status option to prevent task from running (#27) - Allow interpolation on `generates` and `sources` attributes (#26) ## v1.3.0 - 2017-04-24 - Migrate from os/exec.Cmd to a native Go sh/bash interpreter - This is a potentially breaking change if you use Windows. - Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for your commands, even on Windows. - Add "ToSlash" and "FromSlash" to template functions - Use functions defined on github.com/Masterminds/sprig - Do not redirect stdin while running variables commands - Using `context` and `errgroup` packages (this will make other tasks to be cancelled, if one returned an error) ## v1.2.0 - 2017-04-02 - More tests and Travis integration - Watch a task (experimental) - Possibility to call another task - Fix "=" not being recognized in variables/environment variables - Tasks can now have a description, and help will print them (#10) - Task dependencies now run concurrently - Support for a default task (#16) ## v1.1.0 - 2017-03-08 - Support for YAML, TOML and JSON (#1) - Support running command in another directory (#4) - `--force` or `-f` flag to force execution of task even when it's up-to-date - Detection of cyclic dependencies (#5) - Support for variables (#6, #9, #14) - Operation System specific commands and variables (#13) ## v1.0.0 - 2017-02-28 - Add LICENSE file ::: ================================================ FILE: website/src/docs/community.md ================================================ --- title: Community description: Task community contributions, installation methods, and integrations maintained by third parties outline: deep --- # Community Some of the work to improve the Task ecosystem is done by the community, be it installation methods or integrations with code editor. I (the author) am thankful for everyone that helps me to improve the overall experience. ## Integrations Many of our integrations are contributed and maintained by the community. You can view the full list of community integrations [here](./integrations.md#community-integrations). ## Installation methods Some installation methods are maintained by third party: - [Arch Linux](https://archlinux.org/packages/extra/x86_64/go-task/) - [AUR](https://aur.archlinux.org/packages/go-task-git) by @C0rn3j - [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json) - [Fedora](https://packages.fedoraproject.org/pkgs/golang-github-task/go-task/) - [Nix](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix) - [Conda](https://github.com/conda-forge/go-task-feedstock/) ## More Also, thanks for all the [code contributors](https://github.com/go-task/task/graphs/contributors), [financial contributors](https://opencollective.com/task), all those who [reported bugs](https://github.com/go-task/task/issues?q=is%3Aissue) and [answered questions](https://github.com/go-task/task/discussions). If you know something that is missing in this document, please submit a pull request. ================================================ FILE: website/src/docs/contributing.md ================================================ --- title: Contributing description: Comprehensive guide for contributing to the Task project, including setup, development, testing, and submitting PRs outline: deep --- # Contributing Contributions to Task are very welcome, but we ask that you read this document before submitting a PR. ::: info This document applies to the core [Task][task] repository _and_ [Task for Visual Studio Code][vscode-task]. ::: ## Before you start - **Check existing work** - Is there an existing PR? Are there issues discussing the feature/change you want to make? Please make sure you consider/address these discussions in your work. - **Backwards compatibility** - Will your change break existing Taskfiles? It is much more likely that your change will merged if it backwards compatible. Is there an approach you can take that maintains this compatibility? If not, consider opening an issue first so that API changes can be discussed before you invest your time into a PR. - **Experiments** - If there is no way to make your change backward compatible then there is a procedure to introduce breaking changes into minor versions. We call these "[experiments](./experiments/index.md)". If you're intending to work on an experiment, then please read the [experiments workflow](./experiments/index.md#workflow) document carefully and submit a proposal first. ## 1. Setup - **Go** - Task is written in [Go][go]. We always support the latest two major Go versions, so make sure your version is recent enough. - **Node.js** - [Node.js][nodejs] is used to host Task's documentation server and is required if you want to run this server locally. It is also required if you want to contribute to the Visual Studio Code extension. - **Pnpm** - [Pnpm][pnpm] is the Node.js package manager used by Task. ## 2. Making changes - **Code style** - Try to maintain the existing code style where possible. Go code should be formatted and linted by [`golangci-lint`][golangci-lint]. This wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of linters. We recommend that you take a look at the [golangci-lint docs][golangci-lint-docs] for a guide on how to setup your editor to auto-format your code. Any Markdown or TypeScript files should be formatted and linted by [Prettier][prettier]. This style is enforced by our CI to ensure that we have a consistent style across the project. You can use the `task lint` command to lint the code locally and the `task lint:fix` command to try to automatically fix any issues that are found. You can also use the `task fmt` command to auto-format the files if your editor doesn't do it for you. - **Documentation** - Ensure that you add/update any relevant documentation. See the [updating documentation](#updating-documentation) section below. - **Tests** - Ensure that you add/update any relevant tests and that all tests are passing before submitting the PR. See the [writing tests](#writing-tests) section below. ### Running your changes To run Task with working changes, you can use `go run ./cmd/task`. To run a development build of task against a test Taskfile in `testdata`, you can use `go run ./cmd/task --dir ./testdata/ `. To run Task for Visual Studio Code, you can open the project in VSCode and hit F5 (or whatever you debug keybind is set to). This will open a new VSCode window with the extension running. Debugging this way is recommended as it will allow you to set breakpoints and step through the code. Otherwise, you can run `task package` which will generate a `.vsix` file that can be used to manually install the extension. ### Updating documentation Task uses [Vitepress][vitepress] to host a documentation server. The code for this is located in the core Task repository. This can be setup and run locally by using `task website` (requires `nodejs` & `pnpm`). All content is written in Markdown and is located in the `website/src` directory. All Markdown documents should have an 80 character line wrap limit (enforced by Prettier). When making a change, consider whether a change to the [Usage Guide](/docs/guide) is necessary. This document contains descriptions and examples of how to use Task features. If you're adding a new feature, try to find an appropriate place to add a new section. If you're updating an existing feature, ensure that the documentation and any examples are up-to-date. Ensure that any examples follow the [Taskfile Styleguide](./styleguide.md). If you added a new command or flag, ensure that you add it to the [CLI Reference](./reference/cli.md). New fields also need to be added to the [Schema Reference](./reference/schema.md) and [JSON Schema][json-schema]. The descriptions for fields in the docs and the schema should match. ### Writing tests A lot of Task's tests are held in the `task_test.go` file in the project root and this is where you'll most likely want to add new ones too. Most of these tests also have a subdirectory in the `testdata` directory where any Taskfiles/data required to run the tests are stored. When making a changes, consider whether new tests are required. These tests should ensure that the functionality you are adding will continue to work in the future. Existing tests may also need updating if you have changed Task's behavior. You may also consider adding unit tests for any new functions you have added. The unit tests should follow the Go convention of being location in a file named `*_test.go` in the same package as the code being tested. ## 3. Committing your code Try to write meaningful commit messages and avoid having too many commits on the PR. Most PRs should likely have a single commit (although for bigger PRs it may be reasonable to split it in a few). Git squash and rebase is your friend! If you're not sure how to format your commit message, check out [Conventional Commits][conventional-commits]. This style is not enforced, but it is a good way to make your commit messages more readable and consistent. ## 4. Submitting a PR - **Describe your changes** - Ensure that you provide a comprehensive description of your changes. - **Issue/PR links** - Link any previous work such as related issues or PRs. Please describe how your changes differ to/extend this work. - **Examples** - Add any examples or screenshots that you think are useful to demonstrate the effect of your changes. - **Draft PRs** - If your changes are incomplete, but you would like to discuss them, open the PR as a draft and add a comment to start a discussion. Using comments rather than the PR description allows the description to be updated later while preserving any discussions. ## FAQ > I want to contribute, where do I start? Take a look at the list of [open issues for Task][task-open-issues] or [Task for Visual Studio Code][vscode-task-open-issues]. We have a [good first issue][good-first-issue] label for simpler issues that are ideal for first time contributions. All kinds of contributions are welcome, whether its a typo fix or a shiny new feature. You can also contribute by upvoting/commenting on issues, helping to answer questions or contributing to other [community projects](./community.md). > I'm stuck, where can I get help? If you have questions, feel free to ask them in the `#help` forum channel on our [Discord server][discord-server] or open a [Discussion][discussion] on GitHub. --- [task]: https://github.com/go-task/task [vscode-task]: https://github.com/go-task/vscode-task [go]: https://go.dev [gofumpt]: https://github.com/mvdan/gofumpt [gci]: https://github.com/daixiang0/gci [golangci-lint]: https://golangci-lint.run [golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/ [prettier]: https://prettier.io [nodejs]: https://nodejs.org/en/ [pnpm]: https://pnpm.io/ [vitepress]: https://vitepress.dev [json-schema]: https://github.com/go-task/task/blob/main/website/src/public/schema.json [task-open-issues]: https://github.com/go-task/task/issues [vscode-task-open-issues]: https://github.com/go-task/vscode-task/issues [good-first-issue]: https://github.com/go-task/task/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 [discord-server]: https://discord.gg/6TY36E39UK [discussion]: https://github.com/go-task/task/discussions [conventional-commits]: https://www.conventionalcommits.org [mdx]: https://mdxjs.com/ ================================================ FILE: website/src/docs/deprecations/completion-scripts.md ================================================ --- title: 'Completion Scripts' description: Deprecation of direct completion scripts in Task’s Git directory outline: deep --- # Completion Scripts ::: danger This deprecation breaks the following functionality: - Any direct references to the completion scripts in the Task git repository ::: Direct use of the completion scripts in the `completion/*` directory of the [github.com/go-task/task][task] Git repository is deprecated. Any shell configuration that directly refers to these scripts will potentially break in the future as the scripts may be moved or deleted entirely. Any configuration should be updated to use the [new method for generating shell completions][completions] instead. [completions]: /docs/installation#setup-completions [task]: https://github.com/go-task/task ================================================ FILE: website/src/docs/deprecations/index.md ================================================ --- title: Deprecations description: Guide to deprecated features in Task and how to migrate to the new alternatives outline: deep --- # Deprecations As Task evolves, it occasionally outgrows some of its functionality. This can be because they are no longer useful, because another feature has replaced it or because of a change in the way that Task works internally. When this happens, we mark the functionality as deprecated. This means that it will be removed in a future version of Task. This functionality will continue to work until that time, but we strongly recommend that you do not implement this functionality in new Taskfiles and make a plan to migrate away from it as soon as possible. You can view a full list of active deprecations in the "Deprecations" section of the sidebar. ================================================ FILE: website/src/docs/deprecations/template-functions.md ================================================ --- title: 'Template Functions' description: Deprecation of some templating functions in Task, with guidance on their replacements. outline: deep --- # Template Functions ::: danger This deprecation breaks the following functionality: - A small set of templating functions ::: The following templating functions are deprecated. Any replacement functions are listed besides the function being removed. | Deprecated function | Replaced by | | ------------------- | ----------- | | `IsSH` | - | | `FromSlash` | `fromSlash` | | `ToSlash` | `toSlash` | | `ExeExt` | `exeExt` | ================================================ FILE: website/src/docs/deprecations/template.md ================================================ --- # This is a template for deprecation documentation # Copy this page and fill in the details as necessary title: '--- Template ---' description: Template for documenting deprecated features in Task draft: true # Hide in production outline: deep --- # {Name of Deprecated Feature} (#{Issue}) ::: danger This deprecation breaks the following functionality: - {list any existing functionality that will be broken by this deprecation} - {if there are no breaking changes, remove this admonition} ::: {Short description of the feature/behavior and why it is being deprecated} {Short explanation of any replacement features/behaviors and how users should migrate to it} ================================================ FILE: website/src/docs/deprecations/version-2-schema.md ================================================ --- title: 'Version 2 Schema (#1197)' description: Deprecation of Taskfile schema version 2 and migration to version 3 outline: deep --- # Version 2 Schema (#1197) ::: danger This deprecation breaks the following functionality: - Any Taskfiles that use the version 2 schema - `Taskvar.yml` files ::: The Taskfile version 2 schema was introduced in March 2018 and replaced by version 3 in August 2019. In May 2023 [we published a deprecation notice][deprecation-notice] for the version 2 schema on the basis that the vast majority of users had already upgraded to version 3 and removing support for version 2 would allow us to tidy up the codebase and focus on new functionality instead. In December 2023, the final version of Task that supports the version 2 schema ([v3.33.0][v3.33.0]) was published and all legacy code was removed from Task's main branch. To use a more recent version of Task, you will need to ensure that your Taskfile uses the version 3 schema instead. A list of changes between version 2 and version 3 are available in the [Task v3 Release Notes][v3.0.0]. [v3.0.0]: https://github.com/go-task/task/releases/tag/v3.0.0 [v3.33.0]: https://github.com/go-task/task/releases/tag/v3.33.0 [deprecation-notice]: https://github.com/go-task/task/issues/1197 ================================================ FILE: website/src/docs/experiments/env-precedence.md ================================================ --- title: 'Env Precedence (#1038)' description: Experiment to change the precedence of environment variables in Task outline: deep --- # Env Precedence (#1038) ::: warning All experimental features are subject to breaking changes and/or removal _at any time_. We strongly recommend that you do not use these features in a production environment. They are intended for testing and feedback only. ::: ::: danger This experiment breaks the following functionality: - environment variable will take precedence over OS environment variables ::: ::: info To enable this experiment, set the environment variable: `TASK_X_ENV_PRECEDENCE=1`. Check out [our guide to enabling experiments](./index.md#enabling-experiments) for more information. ::: Before this experiment, the OS variable took precedence over the task environment variable. This experiment changes the precedence to make the task environment variable take precedence over the OS variable. Consider the following example: ```yml version: '3' tasks: default: env: KEY: 'other' cmds: - echo "$KEY" ``` Running `KEY=some task` before this experiment, the output would be `some`, but after this experiment, the output would be `other`. If you still want to get the OS variable, you can use the template function env like follow : `{{env "OS_VAR"}}`. ```yml version: '3' tasks: default: env: KEY: 'other' cmds: - echo "$KEY" - echo {{env "KEY"}} ``` Running `KEY=some task`, the output would be `other` and `some`. Like other variables/envs, you can also fall back to a given value using the default template function: ```yml MY_ENV: '{{.MY_ENV | default "fallback"}}' ``` ================================================ FILE: website/src/docs/experiments/gentle-force.md ================================================ --- title: 'Gentle Force (#1200)' description: Experiment to modify the behavior of the --force flag in Task outline: deep --- # Gentle Force (#1200) ::: warning All experimental features are subject to breaking changes and/or removal _at any time_. We strongly recommend that you do not use these features in a production environment. They are intended for testing and feedback only. ::: ::: danger This experiment breaks the following functionality: - The `--force` flag ::: ::: info To enable this experiment, set the environment variable: `TASK_X_GENTLE_FORCE=1`. Check out [our guide to enabling experiments](./index.md#enabling-experiments) for more information. ::: The `--force` flag currently forces _all_ tasks to run regardless of the status checks. This can be useful, but we have found that most of the time users only expect the direct task they are calling to be forced and _not_ all of its dependant tasks. This experiment changes the `--force` flag to only force the directly called task. All dependant tasks will have their statuses checked as normal and will only run if Task considers them to be out of date. A new `--force-all` flag will also be added to maintain the current behavior for users that need this functionality. If you want to migrate, but continue to force all dependant tasks to run, you should replace all uses of the `--force` flag with `--force-all`. Alternatively, if you want to adopt the new behavior, you can continue to use the `--force` flag as you do now! ================================================ FILE: website/src/docs/experiments/index.md ================================================ --- title: Experiments description: Guide to Task’s experimental features and how to use them outline: deep --- # Experiments ::: warning All experimental features are subject to breaking changes and/or removal _at any time_. We strongly recommend that you do not use these features in a production environment. They are intended for testing and feedback only. ::: In order to allow Task to evolve quickly, we sometimes roll out breaking changes to minor versions behind experimental flags. This allows us to gather feedback on breaking changes before committing to a major release. This process can also be used to gather feedback on important non-breaking features before their design is completed. This document describes the [experiment workflow](#workflow) and how you can get involved. You can view the full list of active experiments in the sidebar submenu to the left of the page and click on each one to find out more about it. ## Enabling Experiments Task uses environment variables to detect whether or not an experiment is enabled. All of the experiment variables will begin with the same `TASK_X_` prefix followed by the name of the experiment. You can find the exact name for each experiment on their respective pages in the sidebar. If the variable is set `=1` then it will be enabled. Some experiments may have multiple proposals, in which case, you will need to set the variable equal to the number of the proposal that you want to enable (`=2`, `=3` etc). There are three main ways to set the environment variables for an experiment. Which method you use depends on how you intend to use the experiment: 1. Prefixing your task commands with the relevant environment variable(s). For example, `TASK_X_{FEATURE}=1 task {my-task}`. This is intended for one-off invocations of Task to test out experimental features. 2. Adding the relevant environment variable(s) in your "dotfiles" (e.g. `.bashrc`, `.zshrc` etc.). This will permanently enable experimental features for your personal environment. ```shell # ~/.bashrc export TASK_X_FEATURE=1 ``` 3. Creating a `.env` or a `.taskrc.yml` file in the same directory as your root Taskfile.\ The `.env` file should contain the relevant environment variable(s), while the `.taskrc.yml` file should use a YAML format where each experiment is defined as a key with a corresponding value. This allows you to enable an experimental feature at a project level. If you commit this file to source control, then other users of your project will also have these experiments enabled. If both files are present, the values in the `.taskrc.yml` file will take precedence. ::: code-group ```yaml [.taskrc.yml] experiments: FEATURE: 1 ``` ```shell [.env] TASK_X_FEATURE=1 ``` ::: ## Workflow Experiments are a way for us to test out new features in Task before committing to them in a major release. Because this concept is built around the idea of feedback from our community, we have built a workflow for the process of introducing these changes. This ensures that experiments are given the attention and time that they need and that we are getting the best possible results out of them. The sections below describe the various stages that an experiment must go through from its proposal all the way to being released in a major version of Task. ### 1. Proposal All experimental features start with a proposal in the form of a GitHub issue. If the maintainers decide that an issue has enough support and is a breaking change or is complex/controversial enough to require user feedback, then the issue will be marked with the `status: proposal` label. At this point, the issue becomes a proposal and a period of consultation begins. During this period, we request that users provide feedback on the proposal and how it might effect their use of Task. It is up to the discretion of the maintainers to decide how long this period lasts. ### 2. Draft Once a proposal's consultation ends, a contributor may pick up the work and begin the initial implementation. Once a PR is opened, the maintainers will ensure that it meets the requirements for an experimental feature (i.e. flags are in the right format etc) and merge the feature. Once this code is released, the status will be updated via the `status: draft` label. This indicates that an implementation is now available for use in a release and the experiment is open for feedback. ::: info During the draft period, major changes to the implementation may be made based on the feedback received from users. There are _no stability guarantees_ and experimental features may be abandoned _at any time_. ::: ### 3. Candidate Once an acceptable level of consensus has been reached by the community and feedback/changes are less frequent/significant, the status may be updated via the `status: candidate` label. This indicates that a proposal is _likely_ to accepted and will enter a period for final comments and minor changes. ### 4. Stable Once a suitable amount of time has passed with no changes or feedback, an experiment will be given the `status: stable` label. At this point, the functionality will be treated like any other feature in Task and any changes _must_ be backward compatible. This allows users to migrate to the new functionality without having to worry about anything breaking in future releases. This provides the best experience for users migrating to a new major version. ### 5. Released When making a new major release of Task, all experiments marked as `status: stable` will move to `status: released` and their behaviors will become the new default in Task. Experiments in an earlier stage (i.e. not stable) cannot be released and so will continue to be experiments in the new version. ### Abandoned / Superseded If an experiment is unsuccessful at any point then it will be given the `status: abandoned` or `status: superseded` labels depending on which is more suitable. These experiments will be removed from Task. ================================================ FILE: website/src/docs/experiments/remote-taskfiles.md ================================================ --- title: 'Remote Taskfiles (#1317)' description: Experimentation for using Taskfiles stored in remote locations outline: deep --- # Remote Taskfiles (#1317) ::: warning All experimental features are subject to breaking changes and/or removal _at any time_. We strongly recommend that you do not use these features in a production environment. They are intended for testing and feedback only. ::: ::: info To enable this experiment, set the environment variable: `TASK_X_REMOTE_TASKFILES=1`. Check out [our guide to enabling experiments](./index.md#enabling-experiments) for more information. ::: ::: danger Never run remote Taskfiles from sources that you do not trust. ::: This experiment allows you to use Taskfiles which are stored in remote locations. This applies to both the root Taskfile (aka. Entrypoint) and also when including Taskfiles. Task uses "nodes" to reference remote Taskfiles. There are a few different types of node which you can use: ::: code-group ```text [HTTP/HTTPS] https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml ``` ```text [Git over HTTP] https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main ``` ```text [Git over SSH] git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main ``` ::: ## Node Types ### HTTP/HTTPS `https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml` This is the most basic type of remote node and works by downloading the file from the specified URL. The file must be a valid Taskfile and can be of any name. If a file is not found at the specified URL, Task will append each of the supported file names in turn until it finds a valid file. If it still does not find a valid Taskfile, an error is returned. ### Git over HTTP `https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main` This type of node works by downloading the file from a Git repository over HTTP/HTTPS. The first part of the URL is the base URL of the Git repository. This is the same URL that you would use to clone the repo over HTTP. - You can optionally add the path to the Taskfile in the repository by appending `//` to the URL. - You can also optionally specify a branch or tag to use by appending `?ref=` to the end of the URL. If you omit a reference, the default branch will be used. ### Git over SSH `git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main` This type of node works by downloading the file from a Git repository over SSH. The first part of the URL is the user and base URL of the Git repository. This is the same URL that you would use to clone the repo over SSH. To use Git over SSH, you need to make sure that your SSH agent has your private SSH keys added so that they can be used during authentication. - You can optionally add the path to the Taskfile in the repository by appending `//` to the URL. - You can also optionally specify a branch or tag to use by appending `?ref=` to the end of the URL. If you omit a reference, the default branch will be used. Task has an example remote Taskfile in our repository that you can use for testing and that we will use throughout this document: ```yaml version: '3' tasks: default: cmds: - task: hello hello: cmds: - echo "Hello Task!" ``` ## Specifying a remote entrypoint By default, Task will look for one of the supported file names on your local filesystem. If you want to use a remote file instead, you can pass its URI into the `--taskfile`/`-t` flag just like you would to specify a different local file. For example: ::: code-group ```shell [HTTP/HTTPS] $ task --taskfile https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml task: [hello] echo "Hello Task!" Hello Task! ``` ```shell [Git over HTTP] $ task --taskfile https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main task: [hello] echo "Hello Task!" Hello Task! ``` ```shell [Git over SSH] $ task --taskfile git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main task: [hello] echo "Hello Task!" Hello Task! ``` ::: ## Including remote Taskfiles Including a remote file works exactly the same way that including a local file does. You just need to replace the local path with a remote URI. Any tasks in the remote Taskfile will be available to run from your main Taskfile. ::: code-group ```yaml [HTTP/HTTPS] version: '3' includes: my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml ``` ```yaml [Git over HTTP] version: '3' includes: my-remote-namespace: https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main ``` ```yaml [Git over SSH] version: '3' includes: my-remote-namespace: git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main ``` ::: ```shell $ task my-remote-namespace:hello task: [hello] echo "Hello Task!" Hello Task! ``` ### Authenticating using environment variables The Taskfile location is processed by the templating system, so you can reference environment variables in your URL if you need to add authentication. For example: ```yaml version: '3' includes: my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml ``` ## Security ### Automatic checksums Running commands from sources that you do not control is always a potential security risk. For this reason, we have added some automatic checks when using remote Taskfiles: 1. When running a task from a remote Taskfile for the first time, Task will print a warning to the console asking you to check that you are sure that you trust the source of the Taskfile. If you do not accept the prompt, then Task will exit with code `104` (not trusted) and nothing will run. If you accept the prompt, the remote Taskfile will run and further calls to the remote Taskfile will not prompt you again. 2. Whenever you run a remote Taskfile, Task will create and store a checksum of the file that you are running. If the checksum changes, then Task will print another warning to the console to inform you that the contents of the remote file has changed. If you do not accept the prompt, then Task will exit with code `104` (not trusted) and nothing will run. If you accept the prompt, the checksum will be updated and the remote Taskfile will run. Sometimes you need to run Task in an environment that does not have an interactive terminal, so you are not able to accept a prompt. In these cases you are able to tell task to accept these prompts automatically by using the `--yes` flag or the `--trust` flag. The `--trust` flag allows you to specify trusted hosts for remote Taskfiles, while `--yes` applies to all prompts in Task. You can also configure trusted hosts in your [taskrc configuration](#trusted-hosts) using `remote.trusted-hosts`. Before enabling automatic trust, you should: 1. Be sure that you trust the source and contents of the remote Taskfile. 2. Consider using a pinned version of the remote Taskfile (e.g. A link containing a commit hash) to prevent Task from automatically accepting a prompt that says a remote Taskfile has changed. ### Manual checksum pinning Alternatively, if you expect the contents of your remote files to be a constant value, you can pin the checksum of the included file instead: ```yaml version: '3' includes: included: taskfile: https://taskfile.dev checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9 ``` This will disable the automatic checksum prompts discussed above. However, if the checksums do not match, Task will exit immediately with an error. When setting this up for the first time, you may not know the correct value of the checksum. There are a couple of ways you can obtain this: 1. Add the include normally without the `checksum` key. The first time you run the included Taskfile, a `.task/remote` temporary directory is created. Find the correct set of files for your included Taskfile and open the file that ends with `.checksum`. You can copy the contents of this file and paste it into the `checksum` key of your include. This method is safest as it allows you to inspect the downloaded Taskfile before you pin it. 2. Alternatively, add the include with a temporary random value in the `checksum` key. When you try to run the Taskfile, you will get an error that will report the incorrect expected checksum and the actual checksum. You can copy the actual checksum and replace your temporary random value. ### TLS Task currently supports both `http` and `https` URLs. However, the `http` requests will not execute by default unless you run the task with the `--insecure` flag. This is to protect you from accidentally running a remote Taskfile that is downloaded via an unencrypted connection. Sources that are not protected by TLS are vulnerable to man-in-the-middle attacks and should be avoided unless you know what you are doing. #### Custom Certificates If your remote Taskfiles are hosted on a server that uses a custom CA certificate (e.g., a corporate internal server), you can specify the CA certificate using the `--cacert` flag: ```shell task --taskfile https://internal.example.com/Taskfile.yml --cacert /path/to/ca.crt ``` For servers that require client certificate authentication (mTLS), you can provide a client certificate and key: ```shell task --taskfile https://secure.example.com/Taskfile.yml \ --cert /path/to/client.crt \ --cert-key /path/to/client.key ``` ::: warning Encrypted private keys are not currently supported. If your key is encrypted, you must decrypt it first: ```shell openssl rsa -in encrypted.key -out decrypted.key ``` ::: These options can also be configured in the [configuration file](#configuration). ## Caching & Running Offline Whenever you run a remote Taskfile, the latest copy will be downloaded from the internet and cached locally. This cached file will be used for all future invocations of the Taskfile until the cache expires. Once it expires, Task will download the latest copy of the file and update the cache. By default, the cache is set to expire immediately. This means that Task will always fetch the latest version. However, the cache expiry duration can be modified by setting the `--expiry` flag. If for any reason you lose access to the internet or you are running Task in offline mode (via the `--offline` flag or `TASK_OFFLINE` environment variable), Task will run the any available cached files _even if they are expired_. This means that you should never be stuck without the ability to run your tasks as long as you have downloaded a remote Taskfile at least once. By default, Task will timeout requests to download remote files after 10 seconds and look for a cached copy instead. This timeout can be configured by setting the `--timeout` flag and specifying a duration. For example, `--timeout 5s` will set the timeout to 5 seconds. By default, the cache is stored in the Task temp directory (`.task`). You can override the location of the cache by using the `--remote-cache-dir` flag, the `remote.cache-dir` option in your [configuration file](#cache-dir), or the `TASK_REMOTE_DIR` environment variable. This way, you can share the cache between different projects. You can force Task to ignore the cache and download the latest version by using the `--download` flag. You can use the `--clear-cache` flag to clear all cached remote files. ## Configuration This experiment adds a new `remote` section to the [configuration file](../reference/config.md). - **Type**: `object` - **Description**: Remote configuration settings for handling remote Taskfiles ```yaml remote: insecure: false offline: false timeout: "30s" cache-expiry: "24h" cache-dir: ~/.task trusted-hosts: - github.com - gitlab.com cacert: "" cert: "" cert-key: "" ``` #### `insecure` - **Type**: `boolean` - **Default**: `false` - **Description**: Allow insecure connections when fetching remote Taskfiles - **CLI equivalent**: `--insecure` - **Environment variable**: `TASK_REMOTE_INSECURE` ```yaml remote: insecure: true ``` #### `offline` - **Type**: `boolean` - **Default**: `false` - **Description**: Work in offline mode, preventing remote Taskfile fetching - **CLI equivalent**: `--offline` - **Environment variable**: `TASK_REMOTE_OFFLINE` ```yaml remote: offline: true ``` #### `timeout` - **Type**: `string` - **Default**: 10s - **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` - **Description**: Timeout duration for remote operations (e.g., '30s', '5m') - **CLI equivalent**: `--timeout` - **Environment variable**: `TASK_REMOTE_TIMEOUT` ```yaml remote: timeout: "1m" ``` #### `cache-expiry` - **Type**: `string` - **Default**: 0s (no cache) - **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$` - **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h', '24h') - **CLI equivalent**: `--expiry` - **Environment variable**: `TASK_REMOTE_CACHE_EXPIRY` ```yaml remote: cache-expiry: "6h" ``` #### `cache-dir` - **Type**: `string` - **Default**: `.task` - **Description**: Directory where remote Taskfiles are cached. Can be an absolute path (e.g., `/var/cache/task`) or relative to the Taskfile directory. - **CLI equivalent**: `--remote-cache-dir` - **Environment variable**: `TASK_REMOTE_CACHE_DIR` ```yaml remote: cache-dir: ~/.task ``` #### `trusted-hosts` - **Type**: `array of strings` - **Default**: `[]` (empty list) - **Description**: List of trusted hosts for remote Taskfiles. Hosts in this list will not prompt for confirmation when downloading Taskfiles - **CLI equivalent**: `--trusted-hosts` - **Environment variable**: `TASK_REMOTE_TRUSTED_HOSTS` (comma-separated) ```yaml remote: trusted-hosts: - github.com - gitlab.com - raw.githubusercontent.com - example.com:8080 ``` Hosts in the trusted hosts list will automatically be trusted without prompting for confirmation when they are first downloaded or when their checksums change. The host matching includes the port if specified in the URL. Use with caution and only add hosts you fully trust. You can also specify trusted hosts via the command line: ```shell # Trust specific host for this execution task --trusted-hosts github.com -t https://github.com/user/repo.git//Taskfile.yml # Trust multiple hosts (comma-separated) task --trusted-hosts github.com,gitlab.com -t https://github.com/user/repo.git//Taskfile.yml # Trust a host with a specific port task --trusted-hosts example.com:8080 -t https://example.com:8080/Taskfile.yml ``` #### `cacert` - **Type**: `string` - **Default**: `""` - **Description**: Path to a custom CA certificate file for TLS verification ```yaml remote: cacert: "/path/to/ca.crt" ``` #### `cert` - **Type**: `string` - **Default**: `""` - **Description**: Path to a client certificate file for mTLS authentication ```yaml remote: cert: "/path/to/client.crt" ``` #### `cert-key` - **Type**: `string` - **Default**: `""` - **Description**: Path to the client certificate private key file ```yaml remote: cert-key: "/path/to/client.key" ``` ================================================ FILE: website/src/docs/experiments/template.md ================================================ --- title: '--- Template ---' --- # \{Name of Experiment\} (#\{Issue\}) ::: warning All experimental features are subject to breaking changes and/or removal _at any time_. We strongly recommend that you do not use these features in a production environment. They are intended for testing and feedback only. ::: ::: warning This experiment breaks the following functionality: - \{list any existing functionality that will be broken by this experiment\} - \{if there are no breaking changes, remove this admonition\} ::: :::info To enable this experiment, set the environment variable: `TASK_X_{feature}=1`. Check out [our guide to enabling experiments ][enabling-experiments] for more information. ::: \{Short description of the feature\} \{Short explanation of how users should migrate to the new behavior\} [enabling-experiments]: /docs/experiments/#enabling-experiments ================================================ FILE: website/src/docs/faq.md ================================================ --- title: FAQ description: Frequently asked questions about Task, including ETAs, shell limitations, and Windows compatibility outline: deep --- # FAQ This page contains a list of frequently asked questions about Task. ## When will \ be released? / ETAs Task is _free_ and _open source_ project maintained by a small group of volunteers with full time jobs and lives outside of the project. Because of this, it is difficult to predict how much time we will be able to dedicate to the project in advance and we don't want to make any promises that we can't keep. For this reason, we are unable to provide ETAs for new features or releases. We make a "best effort" to provide regular releases and fix bugs in a timely fashion, but sometimes our personal lives must take priority. ETAs are probably the number one question we (and maintainers of other open source projects) get asked. We understand that you are passionate about the project, but it can be overwhelming to be asked this question so often. Please be patient and avoid asking for ETAs. The best way to speed things up is to contribute to the project yourself. We always appreciate new contributors. If you are interested in contributing, check out the [contributing guide](./contributing.md). ## Why won't my task update my shell environment? This is a limitation of how shells work. Task runs as a subprocess of your current shell, so it can't change the environment of the shell that started it. This limitation is shared by other task runners and build tools too. A common way to work around this is to create a task that will generate output that can be parsed by your shell. For example, to set an environment variable on your shell you can write a task like this: ```yaml my-shell-env: cmds: - echo "export FOO=foo" - echo "export BAR=bar" ``` Now run `eval $(task my-shell-env)` and the variables `$FOO` and `$BAR` will be available in your shell. ## I can't reuse my shell in a task's commands Task runs each command as a separate shell process, so something you do in one command won't effect any future commands. For example, this won't work: ```yaml version: '3' tasks: foo: cmds: - a=foo - echo $a # outputs "" ``` To work around this you can either use a multiline command: ```yaml version: '3' tasks: foo: cmds: - | a=foo echo $a # outputs "foo" ``` Or for more complex multi-line commands it is recommended to move your code into a separate file and call that instead: ```yaml version: '3' tasks: foo: cmds: - ./foo-printer.bash ``` ```shell #!/bin/bash a=foo echo $a ``` ## Are shell core utilities available on Windows? The most common ones, yes. And we might add more in the future. This is possible because Task compiles a small set of core utilities in Go and enables them by default on Windows for greater compatibility. It's possible to control whether these builtin core utilities are used or not with the [`TASK_CORE_UTILS`](/docs/reference/environment#task-core-utils) environment variable: ```bash # Enable, even on non-Windows platforms env TASK_CORE_UTILS=1 task ... # Disable, even on Windows env TASK_CORE_UTILS=0 task ... ``` This is the list of core utils that are currently available: * `base64` * `cat` * `chmod` * `cp` * `find` * `gzip` * `ls` * `mkdir` * `mktemp` * `mv` * `rm` * `shasum` * `tar` * `touch` * `xargs` * (more might be added in the future) ================================================ FILE: website/src/docs/getting-started.md ================================================ --- title: Getting Started description: Guide for getting started with Task outline: deep --- # Getting Started The following guide will help introduce you to the basics of Task. We'll cover how to create a Taskfile, how to write a basic task and how to call it. If you haven't installed Task yet, head over to our [installation guide](installation). ## Creating your first Taskfile Once Task is installed, you can create your first Taskfile by running: ```shell task --init ``` This will create a file called `Taskfile.yml` in the current directory. If you want to create the file in another directory, you can pass an absolute or relative path to the directory into the command: ```shell task --init ./subdirectory ``` Or if you want the Taskfile to have a specific name, you can pass in the name of the file: ```shell task --init Custom.yml ``` This will create a Taskfile that looks something like this: ```yaml [Taskfile.yml] version: '3' vars: GREETING: Hello, World! tasks: default: desc: Print a greeting message cmds: - echo "{{.GREETING}}" silent: true ``` As you can see, all Taskfiles are written in [YAML format](https://yaml.org/). The `version` attribute specifies the minimum version of Task that can be used to run this file. The `vars` attribute is used to define variables that can be used in tasks. In this case, we are creating a string variable called `GREETING` with a value of `Hello, World!`. Finally, the `tasks` attribute is used to define the tasks that can be run. In this case, we have a task called `default` that echoes the value of the `GREETING` variable. The `silent` attribute is set to `true`, which means that the task metadata will not be printed when the task is run - only the output of the commands. ## Calling a task To call the task, invoke `task` followed by the name of the task you want to run. In this case, the name of the task is `default`, so you should run: ```shell task default ``` Note that we don't have to specify the name of the Taskfile. Task will automatically look for a file called `Taskfile.yml` (or any of Task's [supported file names](/docs/guide#supported-file-names)) in the current directory. Additionally, tasks with the name `default` are special. They can also be run without specifying the task name. If you created a Taskfile in a different directory, you can run it by passing the absolute or relative path to the directory as an argument using the `--dir` flag: ```shell task --dir ./subdirectory ``` Or if you created a Taskfile with a different name, you can run it by passing the name of the Taskfile as an argument using the `--taskfile` flag: ```shell task --taskfile Custom.yml ``` ## Adding a build task Let's create a task to build a program in Go. Start by adding a new task called `build` below the existing `default` task. We can then add a `cmds` attribute with a single command to build the program. Task uses [mvdan/sh](https://github.com/mvdan/sh), a native Go sh interpreter. So you can write sh/bash-like commands - even in environments where `sh` or `bash` are usually not available (like Windows). Just remember any executables called must be available as a built-in or in the system's `PATH`. When you're done, it should look something like this: ```yaml version: '3' vars: GREETING: Hello, World! tasks: default: desc: Print a greeting message cmds: - echo "{{.GREETING}}" silent: true build: cmds: - go build ./cmd/main.go ``` Call the task by running: ```shell task build ``` That's about it for the basics, but there's _so much_ more that you can do with Task. Check out the rest of the documentation to learn more about all the features Task has to offer! We recommend taking a look at the [usage guide](/docs/guide) next. Alternatively, you can check out our reference docs for the [Taskfile schema](reference/schema) and [CLI](reference/cli). ================================================ FILE: website/src/docs/guide.md ================================================ --- outline: deep --- # Guide ## Running Taskfiles Specific Taskfiles can be called by specifying the `--taskfile` flag. If you don't specify a Taskfile, Task will automatically look for a file with one of the [supported file names](#supported-file-names) in the current directory. If you want to search in a different directory, you can use the `--dir` flag. ### Supported file names Task looks for files with the following names, in order of priority: - `Taskfile.yml` - `taskfile.yml` - `Taskfile.yaml` - `taskfile.yaml` - `Taskfile.dist.yml` - `taskfile.dist.yml` - `Taskfile.dist.yaml` - `taskfile.dist.yaml` The `.dist` variants allow projects to have one committed file (`.dist`) while still allowing individual users to override the Taskfile by adding an additional `Taskfile.yml` (which would be in your `.gitignore`). ### Running a Taskfile from a subdirectory If a Taskfile cannot be found in the current working directory, it will walk up the file tree until it finds one (similar to how `git` works). When running Task from a subdirectory like this, it will behave as if you ran it from the directory containing the Taskfile. You can use this functionality along with the special `{{.USER_WORKING_DIR}}` variable to create some very useful reusable tasks. For example, if you have a monorepo with directories for each microservice, you can `cd` into a microservice directory and run a task command to bring it up without having to create multiple tasks or Taskfiles with identical content. For example: ```yaml version: '3' tasks: up: dir: '{{.USER_WORKING_DIR}}' preconditions: - test -f docker-compose.yml cmds: - docker-compose up -d ``` In this example, we can run `cd ` and `task up` and as long as the `` directory contains a `docker-compose.yml`, the Docker composition will be brought up. ### Running a global Taskfile If you call Task with the `--global` (alias `-g`) flag, it will look for your home directory instead of your working directory. In short, Task will look for a Taskfile that matches `$HOME/{T,t}askfile.{yml,yaml}` . This is useful to have automation that you can run from anywhere in your system! ::: info When running your global Taskfile with `-g`, tasks will run on `$HOME` by default, and not on your working directory! As mentioned in the previous section, the `{{.USER_WORKING_DIR}}` special variable can be very handy here to run stuff on the directory you're calling `task -g` from. ```yaml version: '3' tasks: from-home: cmds: - pwd from-working-directory: dir: '{{.USER_WORKING_DIR}}' cmds: - pwd ``` ::: ### Reading a Taskfile from stdin Taskfile also supports reading from stdin. This is useful if you are generating Taskfiles dynamically and don't want write them to disk. To tell task to read from stdin, you must specify the `-t/--taskfile` flag with the special `-` value. You may then pipe into Task as you would any other program: ```shell task -t - < ./Taskfile.yml # OR cat ./Taskfile.yml | task -t - ``` ## Environment variables ### Task You can use `env` to set custom environment variables for a specific task: ```yaml version: '3' tasks: greet: cmds: - echo $GREETING env: GREETING: Hey, there! ``` Additionally, you can set global environment variables that will be available to all tasks: ```yaml version: '3' env: GREETING: Hey, there! tasks: greet: cmds: - echo $GREETING ``` ::: info `env` supports expansion and retrieving output from a shell command just like variables, as you can see in the [Variables](#variables) section. ::: ### .env files You can also ask Task to include `.env` like files by using the `dotenv:` setting: ::: code-group ```shell [.env] KEYNAME=VALUE ``` ```shell [testing/.env] ENDPOINT=testing.com ``` ::: ```yaml version: '3' env: ENV: testing dotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env'] tasks: greet: cmds: - echo "Using $KEYNAME and endpoint $ENDPOINT" ``` When the same variable is defined in multiple dotenv files, the **first file in the list takes precedence**. This allows you to set up override patterns by placing higher-priority files first: ```yaml version: '3' dotenv: - .env.local # Highest priority - local developer overrides - .env.{{.ENV}} # Environment-specific settings - .env # Base defaults (lowest priority) ``` Dotenv files can also be specified at the task level: ```yaml version: '3' env: ENV: testing tasks: greet: dotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env'] cmds: - echo "Using $KEYNAME and endpoint $ENDPOINT" ``` Environment variables specified explicitly at the task-level will override variables defined in dotfiles: ```yaml version: '3' env: ENV: testing tasks: greet: dotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env'] env: KEYNAME: DIFFERENT_VALUE cmds: - echo "Using $KEYNAME and endpoint $ENDPOINT" ``` ::: info Please note that you are not currently able to use the `dotenv` key inside included Taskfiles. ::: ## Including other Taskfiles If you want to share tasks between different projects (Taskfiles), you can use the importing mechanism to include other Taskfiles using the `includes` keyword: ```yaml version: '3' includes: docs: ./documentation # will look for ./documentation/Taskfile.yml docker: ./DockerTasks.yml ``` The tasks described in the given Taskfiles will be available with the informed namespace. So, you'd call `task docs:serve` to run the `serve` task from `documentation/Taskfile.yml` or `task docker:build` to run the `build` task from the `DockerTasks.yml` file. Relative paths are resolved relative to the directory containing the including Taskfile. ### OS-specific Taskfiles You can include OS-specific Taskfiles by using a templating function: ```yaml version: '3' includes: build: ./Taskfile_{{OS}}.yml ``` ### Directory of included Taskfile By default, included Taskfile's tasks are run in the current directory, even if the Taskfile is in another directory, but you can force its tasks to run in another directory by using this alternative syntax: ```yaml version: '3' includes: docs: taskfile: ./docs/Taskfile.yml dir: ./docs ``` ::: info The included Taskfiles must be using the same schema version as the main Taskfile uses. ::: ### Optional includes Includes marked as optional will allow Task to continue execution as normal if the included file is missing. ```yaml version: '3' includes: tests: taskfile: ./tests/Taskfile.yml optional: true tasks: greet: cmds: - echo "This command can still be successfully executed if ./tests/Taskfile.yml does not exist" ``` ### Internal includes Includes marked as internal will set all the tasks of the included file to be internal as well (see the [Internal tasks](#internal-tasks) section below). This is useful when including utility tasks that are not intended to be used directly by the user. ```yaml version: '3' includes: tests: taskfile: ./taskfiles/Utils.yml internal: true ``` ### Flatten includes You can flatten the included Taskfile tasks into the main Taskfile by using the `flatten` option. It means that the included Taskfile tasks will be available without the namespace. ::: code-group ```yaml [Taskfile.yml] version: '3' includes: lib: taskfile: ./Included.yml flatten: true tasks: greet: cmds: - echo "Greet" - task: foo ``` ```yaml [Included.yml] version: '3' tasks: foo: cmds: - echo "Foo" ``` ::: If you run `task -a` it will print : ```sh task: Available tasks for this project: * greet: * foo ``` You can run `task foo` directly without the namespace. You can also reference the task in other tasks without the namespace. So if you run `task greet` it will run `greet` and `foo` tasks and the output will be : ```text Greet Foo ``` If multiple tasks have the same name, an error will be thrown: ::: code-group ```yaml [Taskfile.yml] version: '3' includes: lib: taskfile: ./Included.yml flatten: true tasks: greet: cmds: - echo "Greet" - task: foo ``` ```yaml [Included.yml] version: '3' tasks: greet: cmds: - echo "Foo" ``` ::: If you run `task -a` it will print: ```text task: Found multiple tasks (greet) included by "lib" ``` If the included Taskfile has a task with the same name as a task in the main Taskfile, you may want to exclude it from the flattened tasks. You can do this by using the [`excludes` option](#exclude-tasks-from-being-included). ### Exclude tasks from being included You can exclude tasks from being included by using the `excludes` option. This option takes the list of tasks to be excluded from this include. ::: code-group ```yaml [Taskfile.yml] version: '3' includes: included: taskfile: ./Included.yml excludes: [foo] ``` ```yaml [Included.yml] version: '3' tasks: foo: echo "Foo" bar: echo "Bar" ``` ::: `task included:foo` will throw an error because the `foo` task is excluded but `task included:bar` will work and display `Bar`. It's compatible with the `flatten` option. ### Vars of included Taskfiles You can also specify variables when including a Taskfile. This may be useful for having a reusable Taskfile that can be tweaked or even included more than once: ```yaml version: '3' includes: backend: taskfile: ./taskfiles/Docker.yml vars: DOCKER_IMAGE: backend_image frontend: taskfile: ./taskfiles/Docker.yml vars: DOCKER_IMAGE: frontend_image ``` ### Namespace aliases When including a Taskfile, you can give the namespace a list of `aliases`. This works in the same way as [task aliases](#task-aliases) and can be used together to create shorter and easier-to-type commands. ```yaml version: '3' includes: generate: taskfile: ./taskfiles/Generate.yml aliases: [gen] ``` ::: info Vars declared in the included Taskfile have preference over the variables in the including Taskfile! If you want a variable in an included Taskfile to be overridable, use the [default function](https://sprig.taskfile.dev/defaults.html): `MY_VAR: '{{.MY_VAR | default "my-default-value"}}'`. ::: ## Internal tasks Internal tasks are tasks that cannot be called directly by the user. They will not appear in the output when running `task --list|--list-all`. Other tasks may call internal tasks in the usual way. This is useful for creating reusable, function-like tasks that have no useful purpose on the command line. ```yaml version: '3' tasks: build-image-1: cmds: - task: build-image vars: DOCKER_IMAGE: image-1 build-image: internal: true cmds: - docker build -t {{.DOCKER_IMAGE}} . ``` ## Task directory By default, tasks will be executed in the directory where the Taskfile is located. But you can easily make the task run in another folder, informing `dir`: ```yaml version: '3' tasks: serve: dir: public/www cmds: # run http server - caddy ``` If the directory does not exist, `task` creates it. ## Task dependencies > Dependencies run in parallel, so dependencies of a task should not depend one > another. If you want to force tasks to run serially, take a look at the > [Calling Another Task](#calling-another-task) section below. You may have tasks that depend on others. Just pointing them on `deps` will make them run automatically before running the parent task: ```yaml version: '3' tasks: build: deps: [assets] cmds: - go build -v -i main.go assets: cmds: - esbuild --bundle --minify css/index.css > public/bundle.css ``` In the above example, `assets` will always run right before `build` if you run `task build`. A task can have only dependencies and no commands to group tasks together: ```yaml version: '3' tasks: assets: deps: [js, css] js: cmds: - esbuild --bundle --minify js/index.js > public/bundle.js css: cmds: - esbuild --bundle --minify css/index.css > public/bundle.css ``` If there is more than one dependency, they always run in parallel for better performance. ::: tip You can also make the tasks given by the command line run in parallel by using the `--parallel` flag (alias `-p`). Example: `task --parallel js css`. ::: If you want to pass information to dependencies, you can do that the same manner as you would to [call another task](#calling-another-task): ```yaml version: '3' tasks: default: deps: - task: echo_sth vars: { TEXT: 'before 1' } - task: echo_sth vars: { TEXT: 'before 2' } silent: true cmds: - echo "after" echo_sth: cmds: - echo {{.TEXT}} ``` ### Fail-fast dependencies By default, Task waits for all dependencies to finish running before continuing. If you want Task to stop executing further dependencies as soon as one fails, you can set `failfast: true` on your [`.taskrc.yml`][config] or for a specific task: ```yaml # .taskrc.yml failfast: true # applies to all tasks ``` ```yaml # Taskfile.yml version: '3' tasks: default: deps: [task1, task2, task3] failfast: true # applies only to this task ``` Alternatively, you can use `--failfast`, which also work for `--parallel`. ## Platform specific tasks and commands If you want to restrict the running of tasks to explicit platforms, this can be achieved using the `platforms:` key. Tasks can be restricted to a specific OS, architecture or a combination of both. On a mismatch, the task or command will be skipped, and no error will be thrown. The values allowed as OS or Arch are valid `GOOS` and `GOARCH` values, as defined by the Go language [here](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). The `build-windows` task below will run only on Windows, and on any architecture: ```yaml version: '3' tasks: build-windows: platforms: [windows] cmds: - echo 'Running command on Windows' ``` This can be restricted to a specific architecture as follows: ```yaml version: '3' tasks: build-windows-amd64: platforms: [windows/amd64] cmds: - echo 'Running command on Windows (amd64)' ``` It is also possible to restrict the task to specific architectures: ```yaml version: '3' tasks: build-amd64: platforms: [amd64] cmds: - echo 'Running command on amd64' ``` Multiple platforms can be specified as follows: ```yaml version: '3' tasks: build: platforms: [windows/amd64, darwin] cmds: - echo 'Running command on Windows (amd64) and macOS' ``` Individual commands can also be restricted to specific platforms: ```yaml version: '3' tasks: build: cmds: - cmd: echo 'Running command on Windows (amd64) and macOS' platforms: [windows/amd64, darwin] - cmd: echo 'Running on all platforms' ``` ## Calling another task When a task has many dependencies, they are executed concurrently. This will often result in a faster build pipeline. However, in some situations, you may need to call other tasks serially. In this case, use the following syntax: ```yaml version: '3' tasks: main-task: cmds: - task: task-to-be-called - task: another-task - echo "Both done" task-to-be-called: cmds: - echo "Task to be called" another-task: cmds: - echo "Another task" ``` Using the `vars` and `silent` attributes you can choose to pass variables and toggle [silent mode](#silent-mode) on a call-by-call basis: ```yaml version: '3' tasks: greet: vars: RECIPIENT: '{{default "World" .RECIPIENT}}' cmds: - echo "Hello, {{.RECIPIENT}}!" greet-pessimistically: cmds: - task: greet vars: { RECIPIENT: 'Cruel World' } silent: true ``` The above syntax is also supported in `deps`. ::: tip NOTE: If you want to call a task declared in the root Taskfile from within an [included Taskfile](#including-other-taskfiles), add a leading `:` like this: `task: :task-name`. ::: ## Prevent unnecessary work ### By fingerprinting locally generated files and their sources If a task generates something, you can inform Task the source and generated files, so Task will prevent running them if not necessary. ```yaml version: '3' tasks: build: deps: [js, css] cmds: - go build -v -i main.go js: cmds: - esbuild --bundle --minify js/index.js > public/bundle.js sources: - src/js/**/*.js generates: - public/bundle.js css: cmds: - esbuild --bundle --minify css/index.css > public/bundle.css sources: - src/css/**/*.css generates: - public/bundle.css ``` `sources` and `generates` can be files or glob patterns. When given, Task will compare the checksum of the source files to determine if it's necessary to run the task. If not, it will just print a message like `Task "js" is up to date`. `exclude:` can also be used to exclude files from fingerprinting. Sources are evaluated in order, so `exclude:` must come after the positive glob it is negating. ```yaml version: '3' tasks: css: sources: - mysources/**/*.css - exclude: mysources/ignoreme.css generates: - public/bundle.css ``` If you prefer these check to be made by the modification timestamp of the files, instead of its checksum (content), just set the `method` property to `timestamp`. This can be done at two levels: At the task level for a specific task: ```yaml version: '3' tasks: build: cmds: - go build . sources: - ./*.go generates: - app{{exeExt}} method: timestamp ``` At the root level of the Taskfile to apply it globally to all tasks: ```yaml version: '3' method: timestamp # Will be the default for all tasks tasks: build: cmds: - go build . sources: - ./*.go generates: - app{{exeExt}} ``` In situations where you need more flexibility the `status` keyword can be used. You can even combine the two. See the documentation for [status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an example. ::: info By default, task stores checksums on a local `.task` directory in the project's directory. Most of the time, you'll want to have this directory on `.gitignore` (or equivalent) so it isn't committed. (If you have a task for code generation that is committed it may make sense to commit the checksum of that task as well, though). If you want these files to be stored in another directory, you can set a `TASK_TEMP_DIR` environment variable in your machine. It can contain a relative path like `tmp/task` that will be interpreted as relative to the project directory, or an absolute or home path like `/tmp/.task` or `~/.task` (subdirectories will be created for each project). ```shell export TASK_TEMP_DIR='~/.task' ``` ::: ::: info Each task has only one checksum stored for its `sources`. If you want to distinguish a task by any of its input variables, you can add those variables as part of the task's label, and it will be considered a different task. This is useful if you want to run a task once for each distinct set of inputs until the sources actually change. For example, if the sources depend on the value of a variable, or you if you want the task to rerun if some arguments change even if the source has not. ::: ::: tip The method `none` skips any validation and always runs the task. ::: ::: info For the `checksum` (default) or `timestamp` method to work, it is only necessary to inform the source files. When the `timestamp` method is used, the last time of the running the task is considered as a generate. ::: ### Using programmatic checks to indicate a task is up to date Alternatively, you can inform a sequence of tests as `status`. If no error is returned (exit status 0), the task is considered up-to-date: ```yaml version: '3' tasks: generate-files: cmds: - mkdir directory - touch directory/file1.txt - touch directory/file2.txt # test existence of files status: - test -d directory - test -f directory/file1.txt - test -f directory/file2.txt ``` Normally, you would use `sources` in combination with `generates` - but for tasks that generate remote artifacts (Docker images, deploys, CD releases) the checksum source and timestamps require either access to the artifact or for an out-of-band refresh of the `.checksum` fingerprint file. Two special variables `{{.CHECKSUM}}` and `{{.TIMESTAMP}}` are available for interpolation within `cmds` and `status` commands, depending on the method assigned to fingerprint the sources. Only `source` globs are fingerprinted. Note that the `{{.TIMESTAMP}}` variable is a "live" Go `time.Time` struct, and can be formatted using any of the methods that `time.Time` responds to. See [the Go Time documentation](https://golang.org/pkg/time/) for more information. You can use `--force` or `-f` if you want to force a task to run even when up-to-date. Also, `task --status [tasks]...` will exit with a non-zero [exit code](/docs/reference/cli#exit-codes) if any of the tasks are not up-to-date. `status` can be combined with the [fingerprinting](#by-fingerprinting-locally-generated-files-and-their-sources) to have a task run if either the the source/generated artifacts changes, or the programmatic check fails: ```yaml version: '3' tasks: build:prod: desc: Build for production usage. cmds: - composer install # Run this task if source files changes. sources: - composer.json - composer.lock generates: - ./vendor/composer/installed.json - ./vendor/autoload.php # But also run the task if the last build was not a production build. status: - grep -q '"dev"{{:}} false' ./vendor/composer/installed.json ``` ### Using programmatic checks to cancel the execution of a task and its dependencies In addition to `status` checks, `preconditions` checks are the logical inverse of `status` checks. That is, if you need a certain set of conditions to be _true_ you can use the `preconditions` stanza. `preconditions` are similar to `status` lines, except they support `sh` expansion, and they SHOULD all return 0. ```yaml version: '3' tasks: generate-files: cmds: - mkdir directory - touch directory/file1.txt - touch directory/file2.txt # test existence of files preconditions: - test -f .env - sh: '[ 1 = 0 ]' msg: "One doesn't equal Zero, Halting" ``` Preconditions can set specific failure messages that can tell a user what steps to take using the `msg` field. If a task has a dependency on a sub-task with a precondition, and that precondition is not met - the calling task will fail. Note that a task executed with a failing precondition will not run unless `--force` is given. Unlike `status`, which will skip a task if it is up to date and continue executing tasks that depend on it, a `precondition` will fail a task, along with any other tasks that depend on it. ```yaml version: '3' tasks: task-will-fail: preconditions: - sh: 'exit 1' task-will-also-fail: deps: - task-will-fail task-will-still-fail: cmds: - task: task-will-fail - echo "I will not run" ``` ### Conditional execution with `if` The `if` attribute allows you to conditionally skip tasks or commands based on a shell command's exit code. Unlike `preconditions` which fail and stop execution, `if` simply skips the task or command when the condition is not met and continues with the rest of the Taskfile. #### Task-level `if` When `if` is set on a task, the entire task is skipped if the condition fails: ```yaml version: '3' tasks: deploy: if: '[ "$CI" = "true" ]' cmds: - echo "Deploying..." - ./deploy.sh ``` #### Command-level `if` When `if` is set on a command, only that specific command is skipped: ```yaml version: '3' tasks: build: cmds: - cmd: echo "Building for production" if: '[ "$ENV" = "production" ]' - cmd: echo "Building for development" if: '[ "$ENV" = "development" ]' - go build ./... ``` #### Using templates in `if` conditions You can use Go template expressions in `if` conditions. Template expressions like `{{eq .VAR "value"}}` evaluate to `true` or `false`, which are valid shell commands (`true` exits with 0, `false` exits with 1): ```yaml version: '3' tasks: conditional: vars: ENABLE_FEATURE: "true" cmds: - cmd: echo "Feature is enabled" if: '{{eq .ENABLE_FEATURE "true"}}' - cmd: echo "Feature is disabled" if: '{{ne .ENABLE_FEATURE "true"}}' ``` #### Using `if` with `for` loops When used inside a `for` loop, the `if` condition is evaluated for each iteration: ```yaml version: '3' tasks: process-items: cmds: - for: ['a', 'b', 'c'] cmd: echo "processing {{.ITEM}}" if: '[ "{{.ITEM}}" != "b" ]' ``` This will output: ``` processing a processing c ``` #### `if` vs `preconditions` | Aspect | `if` | `preconditions` | |--------|------|-----------------| | On failure | Skips (continues) | Fails (stops) | | Message | Only in verbose mode | Always shown | | Use case | "Run if possible" | "Must be true" | Use `if` when you want optional conditional execution that shouldn't stop the workflow. Use `preconditions` when the condition must be met for the task to make sense. ### Limiting when tasks run If a task executed by multiple `cmds` or multiple `deps` you can control when it is executed using `run`. `run` can also be set at the root of the Taskfile to change the behavior of all the tasks unless explicitly overridden. Supported values for `run`: - `always` (default) always attempt to invoke the task regardless of the number of previous executions - `once` only invoke this task once regardless of the number of references - `when_changed` only invokes the task once for each unique set of variables passed into the task ```yaml version: '3' tasks: default: cmds: - task: generate-file vars: { CONTENT: '1' } - task: generate-file vars: { CONTENT: '2' } - task: generate-file vars: { CONTENT: '2' } generate-file: run: when_changed deps: - install-deps cmds: - echo {{.CONTENT}} install-deps: run: once cmds: - sleep 5 # long operation like installing packages ``` ### Ensuring required variables are set If you want to check that certain variables are set before running a task then you can use `requires`. This is useful when might not be clear to users which variables are needed, or if you want clear message about what is required. Also some tasks could have dangerous side effects if run with un-set variables. Using `requires` you specify an array of strings in the `vars` sub-section under `requires`, these strings are variable names which are checked prior to running the task. If any variables are un-set then the task will error and not run. Environmental variables are also checked. Syntax: ```yaml requires: vars: [] # Array of strings ``` ::: info Variables set to empty zero length strings, will pass the `requires` check. ::: Example of using `requires`: ```yaml version: '3' tasks: docker-build: cmds: - 'docker build . -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}}' # Make sure these variables are set before running requires: vars: [IMAGE_NAME, IMAGE_TAG] ``` ### Ensuring required variables have allowed values If you want to ensure that a variable is set to one of a predefined set of valid values before executing a task, you can use requires. This is particularly useful when there are strict requirements for what values a variable can take, and you want to provide clear feedback to the user when an invalid value is detected. To use `requires`, you specify an array of allowed values in the vars sub-section under requires. Task will check if the variable is set to one of the allowed values. If the variable does not match any of these values, the task will raise an error and stop execution. This check applies both to user-defined variables and environment variables. Example of using `requires`: ```yaml version: '3' tasks: deploy: cmds: - echo "deploying to {{.ENV}}" requires: vars: - name: ENV enum: [dev, beta, prod] ``` If `ENV` is not one of 'dev', 'beta' or 'prod' an error will be raised. ::: info This is supported only for string variables. ::: ### Prompting for missing variables interactively If you want Task to prompt users for missing required variables instead of failing, you can enable interactive mode in your `.taskrc.yml`: ```yaml # ~/.taskrc.yml interactive: true ``` When enabled, Task will display an interactive prompt for any missing required variable. For variables with an `enum`, a selection menu is shown. For variables without an enum, a text input is displayed. ```yaml # Taskfile.yml version: '3' tasks: deploy: requires: vars: - name: ENVIRONMENT enum: [dev, staging, prod] - VERSION cmds: - echo "Deploying {{.VERSION}} to {{.ENVIRONMENT}}" ``` ```shell $ task deploy ? Select value for ENVIRONMENT: ❯ dev staging prod ? Enter value for VERSION: 1.0.0 Deploying 1.0.0 to prod ``` If the variable is already set (via CLI, environment, or Taskfile), no prompt is shown: ```shell $ task deploy ENVIRONMENT=prod VERSION=1.0.0 Deploying 1.0.0 to prod ``` ::: info Interactive prompts require a TTY (terminal). Task automatically detects non-interactive environments like GitHub Actions, GitLab CI, and other CI pipelines where stdin/stdout are not connected to a terminal. In these cases, prompts are skipped and missing variables will cause an error as usual. You can enable prompts from the command line with `--interactive` or by setting `interactive: true` in your `.taskrc.yml`. ::: ## Variables Task allows you to set variables using the `vars` keyword. The following variable types are supported: - `string` - `bool` - `int` - `float` - `array` - `map` ::: info Defining a map requires that you use a special `map` subkey (see example below). ::: ```yaml version: 3 tasks: foo: vars: STRING: 'Hello, World!' BOOL: true INT: 42 FLOAT: 3.14 ARRAY: [1, 2, 3] MAP: map: { A: 1, B: 2, C: 3 } cmds: - 'echo {{.STRING}}' # Hello, World! - 'echo {{.BOOL}}' # true - 'echo {{.INT}}' # 42 - 'echo {{.FLOAT}}' # 3.14 - 'echo {{.ARRAY}}' # [1 2 3] - 'echo {{index .ARRAY 0}}' # 1 - 'echo {{.MAP}}' # map[A:1 B:2 C:3] - 'echo {{.MAP.A}}' # 1 ``` Variables can be set in many places in a Taskfile. When executing [templates][templating-reference], Task will look for variables in the order listed below (most important first): - Variables declared in the task definition - Variables given while calling a task from another (See [Calling another task](#calling-another-task) above) - Variables of the [included Taskfile](#including-other-taskfiles) (when the task is included) - Variables of the [inclusion of the Taskfile](#vars-of-included-taskfiles) (when the task is included) - Global variables (those declared in the `vars:` option in the Taskfile) - Environment variables Example of sending parameters with environment variables: ```shell $ TASK_VARIABLE=a-value task do-something ``` ::: tip A special variable `.TASK` is always available containing the task name. ::: Since some shells do not support the above syntax to set environment variables (Windows) tasks also accept a similar style when not at the beginning of the command. ```shell $ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!" ``` Example of locally declared vars: ```yaml version: '3' tasks: print-var: cmds: - echo "{{.VAR}}" vars: VAR: Hello! ``` Example of global vars in a `Taskfile.yml`: ```yaml version: '3' vars: GREETING: Hello from Taskfile! tasks: greet: cmds: - echo "{{.GREETING}}" ``` Example of a `default` value to be overridden from CLI: ```yaml version: '3' tasks: greet_user: desc: 'Greet the user with a name.' vars: USER_NAME: '{{.USER_NAME| default "DefaultUser"}}' cmds: - echo "Hello, {{.USER_NAME}}!" ``` ```shell $ task greet_user task: [greet_user] echo "Hello, DefaultUser!" Hello, DefaultUser! $ task greet_user USER_NAME="Bob" task: [greet_user] echo "Hello, Bob!" Hello, Bob! ``` ### Dynamic variables The below syntax (`sh:` prop in a variable) is considered a dynamic variable. The value will be treated as a command and the output assigned. If there are one or more trailing newlines, the last newline will be trimmed. ```yaml version: '3' tasks: build: cmds: - go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go vars: GIT_COMMIT: sh: git log -n 1 --format=%h ``` This works for all types of variables. ### Referencing other variables Templating is great for referencing string values if you want to pass a value from one task to another. However, the templating engine is only able to output strings. If you want to pass something other than a string to another task then you will need to use a reference (`ref`) instead. ::: code-group ```yaml [Templating Engine] version: 3 tasks: foo: vars: FOO: [A, B, C] # <-- FOO is defined as an array cmds: - task: bar vars: FOO: '{{.FOO}}' # <-- FOO gets converted to a string when passed to bar bar: cmds: - 'echo {{index .FOO 0}}' # <-- FOO is a string so the task outputs '91' which is the ASCII code for '[' instead of the expected 'A' ``` ```yaml [Reference] version: 3 tasks: foo: vars: FOO: [A, B, C] # <-- FOO is defined as an array cmds: - task: bar vars: FOO: ref: .FOO # <-- FOO gets passed by reference to bar and maintains its type bar: cmds: - 'echo {{index .FOO 0}}' # <-- FOO is still a map so the task outputs 'A' as expected ``` ::: This also works the same way when calling `deps` and when defining a variable and can be used in any combination: ```yaml version: 3 tasks: foo: vars: FOO: [A, B, C] # <-- FOO is defined as an array BAR: ref: .FOO # <-- BAR is defined as a reference to FOO deps: - task: bar vars: BAR: ref: .BAR # <-- BAR gets passed by reference to bar and maintains its type bar: cmds: - 'echo {{index .BAR 0}}' # <-- BAR still refers to FOO so the task outputs 'A' ``` All references use the same templating syntax as regular templates, so in addition to calling `.FOO`, you can also pass subkeys (`.FOO.BAR`) or indexes (`index .FOO 0`) and use functions (`len .FOO`) as described in the [templating-reference][templating-reference]: ```yaml version: 3 tasks: foo: vars: FOO: [A, B, C] # <-- FOO is defined as an array cmds: - task: bar vars: FOO: ref: index .FOO 0 # <-- The element at index 0 is passed by reference to bar bar: cmds: - 'echo {{.FOO}}' # <-- FOO is just the letter 'A' ``` ### Parsing JSON/YAML into map variables If you have a raw JSON or YAML string that you want to process in Task, you can use a combination of the `ref` keyword and the `fromJson` or `fromYaml` templating functions to parse the string into a map variable. For example: ```yaml version: '3' tasks: task-with-map: vars: JSON: '{"a": 1, "b": 2, "c": 3}' FOO: ref: 'fromJson .JSON' cmds: - echo {{.FOO}} ``` ```txt map[a:1 b:2 c:3] ``` ## Looping over values Task allows you to loop over certain values and execute a command for each. There are a number of ways to do this depending on the type of value you want to loop over. ### Looping over a static list The simplest kind of loop is an explicit one. This is useful when you want to loop over a set of values that are known ahead of time. ```yaml version: '3' tasks: default: cmds: - for: ['foo.txt', 'bar.txt'] cmd: cat {{ .ITEM }} ``` ### Looping over a matrix If you need to loop over all permutations of multiple lists, you can use the `matrix` property. This should be familiar to anyone who has used a matrix in a CI/CD pipeline. ```yaml version: '3' tasks: default: silent: true cmds: - for: matrix: OS: ['windows', 'linux', 'darwin'] ARCH: ['amd64', 'arm64'] cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" ``` This will output: ```txt windows/amd64 windows/arm64 linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 ``` You can also use references to other variables as long as they are also lists: ```yaml version: '3' vars: OS_VAR: ['windows', 'linux', 'darwin'] ARCH_VAR: ['amd64', 'arm64'] tasks: default: cmds: - for: matrix: OS: ref: .OS_VAR ARCH: ref: .ARCH_VAR cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" ``` ### Looping over your task's sources or generated files You are also able to loop over the sources of your task or the files it generates: ::: code-group ```yaml [Sources] version: '3' tasks: default: sources: - foo.txt - bar.txt cmds: - for: sources cmd: cat {{ .ITEM }} ``` ```yaml [Generates] version: '3' tasks: default: generates: - foo.txt - bar.txt cmds: - for: generates cmd: cat {{ .ITEM }} ``` ::: This will also work if you use globbing syntax in `sources` or `generates`. For example, if you specify a source for `*.txt`, the loop will iterate over all files that match that glob. Paths will always be returned as paths relative to the task directory. If you need to convert this to an absolute path, you can use the built-in `joinPath` function. There are some [special variables](/docs/reference/templating#special-variables) that you may find useful for this. ::: code-group ```yaml [Sources] version: '3' tasks: default: vars: MY_DIR: /path/to/dir dir: '{{.MY_DIR}}' sources: - foo.txt - bar.txt cmds: - for: sources cmd: cat {{joinPath .MY_DIR .ITEM}} ``` ```yaml [Generates] version: '3' tasks: default: vars: MY_DIR: /path/to/dir dir: '{{.MY_DIR}}' generates: - foo.txt - bar.txt cmds: - for: generates cmd: cat {{joinPath .MY_DIR .ITEM}} ``` ::: ### Looping over variables To loop over the contents of a variable, use the `var` key followed by the name of the variable you want to loop over. By default, string variables will be split on any whitespace characters. ```yaml version: '3' tasks: default: vars: MY_VAR: foo.txt bar.txt cmds: - for: { var: MY_VAR } cmd: cat {{.ITEM}} ``` If you need to split a string on a different character, you can do this by specifying the `split` property: ```yaml version: '3' tasks: default: vars: MY_VAR: foo.txt,bar.txt cmds: - for: { var: MY_VAR, split: ',' } cmd: cat {{.ITEM}} ``` You can also loop over arrays and maps directly: ```yaml version: 3 tasks: foo: vars: LIST: [foo, bar, baz] cmds: - for: var: LIST cmd: echo {{.ITEM}} ``` When looping over a map we also make an additional `{{.KEY}}` variable available that holds the string value of the map key. Remember that maps are unordered, so the order in which the items are looped over is random. All of this also works with dynamic variables! ```yaml version: '3' tasks: default: vars: MY_VAR: sh: find -type f -name '*.txt' cmds: - for: { var: MY_VAR } cmd: cat {{.ITEM}} ``` ### Renaming variables If you want to rename the iterator variable to make it clearer what the value contains, you can do so by specifying the `as` property: ```yaml version: '3' tasks: default: vars: MY_VAR: foo.txt bar.txt cmds: - for: { var: MY_VAR, as: FILE } cmd: cat {{.FILE}} ``` ### Looping over tasks Because the `for` property is defined at the `cmds` level, you can also use it alongside the `task` keyword to run tasks multiple times with different variables. ```yaml version: '3' tasks: default: cmds: - for: [foo, bar] task: my-task vars: FILE: '{{.ITEM}}' my-task: cmds: - echo '{{.FILE}}' ``` Or if you want to run different tasks depending on the value of the loop: ```yaml version: '3' tasks: default: cmds: - for: [foo, bar] task: task-{{.ITEM}} task-foo: cmds: - echo 'foo' task-bar: cmds: - echo 'bar' ``` ### Looping over dependencies All of the above looping techniques can also be applied to the `deps` property. This allows you to combine loops with concurrency: ```yaml version: '3' tasks: default: deps: - for: [foo, bar] task: my-task vars: FILE: '{{.ITEM}}' my-task: cmds: - echo '{{.FILE}}' ``` It is important to note that as `deps` are run in parallel, the order in which the iterations are run is not guaranteed and the output may vary. For example, the output of the above example may be either: ```shell foo bar ``` or ```shell bar foo ``` ## Forwarding CLI arguments to commands If `--` is given in the CLI, all following parameters are added to a special `.CLI_ARGS` variable. This is useful to forward arguments to another command. The below example will run `yarn install`. ```shell $ task yarn -- install ``` ```yaml version: '3' tasks: yarn: cmds: - yarn {{.CLI_ARGS}} ``` ## Wildcard arguments Another way to parse arguments into a task is to use a wildcard in your task's name. Wildcards are denoted by an asterisk (`*`) and can be used multiple times in a task's name to pass in multiple arguments. Matching arguments will be captured and stored in the `.MATCH` variable and can then be used in your task's commands like any other variable. This variable is an array of strings and so will need to be indexed to access the individual arguments. We suggest creating a named variable for each argument to make it clear what they contain: ```yaml version: '3' tasks: start:*:*: vars: SERVICE: '{{index .MATCH 0}}' REPLICAS: '{{index .MATCH 1}}' cmds: - echo "Starting {{.SERVICE}} with {{.REPLICAS}} replicas" start:*: vars: SERVICE: '{{index .MATCH 0}}' cmds: - echo "Starting {{.SERVICE}}" ``` This call matches the `start:*` task and the string "foo" is captured by the wildcard and stored in the `.MATCH` variable. We then index the `.MATCH` array and store the result in the `.SERVICE` variable which is then echoed out in the cmds: ```shell $ task start:foo Starting foo ``` You can use whitespace in your arguments as long as you quote the task name: ```shell $ task "start:foo bar" Starting foo bar ``` If multiple matching tasks are found, the first one listed in the Taskfile will be used. If you are using included Taskfiles, tasks in parent files will be considered first. ```shell $ task start:foo:3 Starting foo with 3 replicas ``` Using wildcards with aliases Wildcards also work with aliases. If a task has an alias, you can use the alias name with wildcards to capture arguments. For example: ```yaml version: '3' tasks: start:*: aliases: [run:*] vars: SERVICE: "{{index .MATCH 0}}" cmds: - echo "Running {{.SERVICE}}" ``` In this example, you can call the task using the alias run:*: ```shell $ task run:foo Running foo ``` ## Doing task cleanup with `defer` With the `defer` keyword, it's possible to schedule cleanup to be run once the task finishes. The difference with just putting it as the last command is that this command will run even when the task fails. In the example below, `rm -rf tmpdir/` will run even if the third command fails: ```yaml version: '3' tasks: default: cmds: - mkdir -p tmpdir/ - defer: rm -rf tmpdir/ - echo 'Do work on tmpdir/' ``` If you want to move the cleanup command into another task, that is possible as well: ```yaml version: '3' tasks: default: cmds: - mkdir -p tmpdir/ - defer: { task: cleanup } - echo 'Do work on tmpdir/' cleanup: rm -rf tmpdir/ ``` ::: info Due to the nature of how the [Go's own `defer` work](https://go.dev/tour/flowcontrol/13), the deferred commands are executed in the reverse order if you schedule multiple of them. ::: A special variable `.EXIT_CODE` is exposed when a command exited with a non-zero [exit code](/docs/reference/cli#exit-codes). You can check its presence to know if the task completed successfully or not: ```yaml version: '3' tasks: default: cmds: - defer: echo '{{if .EXIT_CODE}}Failed with {{.EXIT_CODE}}!{{else}}Success!{{end}}' - exit 1 ``` ## Help Running `task --list` (or `task -l`) lists all tasks with a description. The following Taskfile: ```yaml version: '3' tasks: build: desc: Build the go binary. cmds: - go build -v -i main.go test: desc: Run all the go tests. cmds: - go test -race ./... js: cmds: - esbuild --bundle --minify js/index.js > public/bundle.js css: cmds: - esbuild --bundle --minify css/index.css > public/bundle.css ``` would print the following output: ```shell * build: Build the go binary. * test: Run all the go tests. ``` If you want to see all tasks, there's a `--list-all` (alias `-a`) flag as well. ## Display summary of task Running `task --summary task-name` will show a summary of a task. The following Taskfile: ```yaml version: '3' tasks: release: deps: [build] summary: | Release your project to github It will build your project before starting the release. Please make sure that you have set GITHUB_TOKEN before starting. cmds: - your-release-tool build: cmds: - your-build-tool ``` with running `task --summary release` would print the following output: ``` task: release Release your project to github It will build your project before starting the release. Please make sure that you have set GITHUB_TOKEN before starting. dependencies: - build commands: - your-release-tool ``` If a summary is missing, the description will be printed. If the task does not have a summary or a description, a warning is printed. Please note: _showing the summary will not execute the command_. ## Task aliases Aliases are alternative names for tasks. They can be used to make it easier and quicker to run tasks with long or hard-to-type names. You can use them on the command line, when [calling sub-tasks](#calling-another-task) in your Taskfile and when [including tasks](#including-other-taskfiles) with aliases from another Taskfile. They can also be used together with [namespace aliases](#namespace-aliases). ```yaml version: '3' tasks: generate: aliases: [gen] cmds: - task: gen-mocks generate-mocks: aliases: [gen-mocks] cmds: - echo "generating..." ``` ## Overriding task name Sometimes you may want to override the task name printed on the summary, up-to-date messages to STDOUT, etc. In this case, you can just set `label:`, which can also be interpolated with variables: ```yaml version: '3' tasks: default: cmds: - task: print vars: MESSAGE: hello - task: print vars: MESSAGE: world print: label: 'print-{{.MESSAGE}}' cmds: - echo "{{.MESSAGE}}" ``` ## Warning Prompts Warning Prompts are used to prompt a user for confirmation before a task is executed. Below is an example using `prompt` with a dangerous command, that is called between two safe commands: ```yaml version: '3' tasks: example: cmds: - task: not-dangerous - task: dangerous - task: another-not-dangerous not-dangerous: cmds: - echo 'not dangerous command' another-not-dangerous: cmds: - echo 'another not dangerous command' dangerous: prompt: This is a dangerous command... Do you want to continue? cmds: - echo 'dangerous command' ``` ```shell ❯ task dangerous task: "This is a dangerous command... Do you want to continue?" [y/N] ``` Prompts can be a single value or a list of prompts, like below: ```yaml version: '3' tasks: example: cmds: - task: dangerous dangerous: prompt: - This is a dangerous command... Do you want to continue? - Are you sure? cmds: - echo 'dangerous command' ``` Warning prompts are called before executing a task. If a prompt is denied Task will exit with [exit code](/docs/reference/cli#exit-codes) 205. If approved, Task will continue as normal. ```shell ❯ task example not dangerous command task: "This is a dangerous command. Do you want to continue?" [y/N] y dangerous command another not dangerous command ``` To skip warning prompts automatically, you can use the `--yes` (alias `-y`) option when calling the task. By including this option, all warnings, will be automatically confirmed, and no prompts will be shown. ::: warning Tasks with prompts always fail by default on non-terminal environments, like a CI, where an `stdin` won't be available for the user to answer. In those cases, use `--yes` (`-y`) to force all tasks with a prompt to run. ::: ## Silent mode Silent mode disables the echoing of commands before Task runs it. For the following Taskfile: ```yaml version: '3' tasks: echo: cmds: - echo "Print something" ``` Normally this will be printed: ```shell echo "Print something" Print something ``` With silent mode on, the below will be printed instead: ```shell Print something ``` There are four ways to enable silent mode: - At command level: ```yaml version: '3' tasks: echo: cmds: - cmd: echo "Print something" silent: true ``` - At task level: ```yaml version: '3' tasks: echo: cmds: - echo "Print something" silent: true ``` - Globally at Taskfile level: ```yaml version: '3' silent: true tasks: echo: cmds: - echo "Print something" ``` - Or globally with `--silent` or `-s` flag If you want to suppress STDOUT instead, just redirect a command to `/dev/null`: ```yaml version: '3' tasks: echo: cmds: - echo "This will print nothing" > /dev/null ``` ## Dry run mode Dry run mode (`--dry`) compiles and steps through each task, printing the commands that would be run without executing them. This is useful for debugging your Taskfiles. ## Ignore errors You have the option to ignore errors during command execution. Given the following Taskfile: ```yaml version: '3' tasks: echo: cmds: - exit 1 - echo "Hello World" ``` Task will abort the execution after running `exit 1` because the status code `1` stands for `EXIT_FAILURE`. However, it is possible to continue with execution using `ignore_error`: ```yaml version: '3' tasks: echo: cmds: - cmd: exit 1 ignore_error: true - echo "Hello World" ``` `ignore_error` can also be set for a task, which means errors will be suppressed for all commands. Nevertheless, keep in mind that this option will not propagate to other tasks called either by `deps` or `cmds`! ## Output syntax By default, Task just redirects the STDOUT and STDERR of the running commands to the shell in real-time. This is good for having live feedback for logging printed by commands, but the output can become messy if you have multiple commands running simultaneously and printing lots of stuff. To make this more customizable, there are currently three different output options you can choose: - `interleaved` (default) - `group` - `prefixed` To choose another one, just set it to root in the Taskfile: ```yaml version: '3' output: 'group' tasks: # ... ``` The `group` output will print the entire output of a command once after it finishes, so you will not have live feedback for commands that take a long time to run. When using the `group` output, you can optionally provide a templated message to print at the start and end of the group. This can be useful for instructing CI systems to group all of the output for a given task, such as with [GitHub Actions' `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines) or [Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?expand=1&view=azure-devops&tabs=bash#formatting-commands). ```yaml version: '3' output: group: begin: '::group::{{.TASK}}' end: '::endgroup::' tasks: default: cmds: - echo 'Hello, World!' silent: true ``` ```shell $ task default ::group::default Hello, World! ::endgroup:: ``` When using the `group` output, you may swallow the output of the executed command on standard output and standard error if it does not fail (zero exit code). ```yaml version: '3' silent: true output: group: error_only: true tasks: passes: echo 'output-of-passes' errors: echo 'output-of-errors' && exit 1 ``` ```shell $ task passes $ task errors output-of-errors task: Failed to run task "errors": exit status 1 ``` The `prefix` output will prefix every line printed by a command with `[task-name] ` as the prefix, but you can customize the prefix for a command with the `prefix:` attribute: ```yaml version: '3' output: prefixed tasks: default: deps: - task: print vars: { TEXT: foo } - task: print vars: { TEXT: bar } - task: print vars: { TEXT: baz } print: cmds: - echo "{{.TEXT}}" prefix: 'print-{{.TEXT}}' silent: true ``` ```shell $ task default [print-foo] foo [print-bar] bar [print-baz] baz ``` ::: tip The `output` option can also be specified by the `--output` or `-o` flags. ::: ## CI Integration ### Colored output Task automatically enables colored output when running in CI environments (`CI=true`). Most CI providers set this variable automatically. You can also force colored output with `FORCE_COLOR=1` or disable it with `NO_COLOR=1`. ### Error annotations When running in GitHub Actions (`GITHUB_ACTIONS=true`), Task automatically emits error annotations when a task fails. These annotations appear in the workflow summary, making it easier to spot failures without scrolling through logs. ```shell ::error title=Task 'build' failed::exit status 1 ``` This feature requires no configuration and works automatically. ## Interactive CLI application When running interactive CLI applications inside Task they can sometimes behave weirdly, especially when the [output mode](#output-syntax) is set to something other than `interleaved` (the default), or when interactive apps are run in parallel with other tasks. The `interactive: true` tells Task this is an interactive application and Task will try to optimize for it: ```yaml version: '3' tasks: default: cmds: - vim my-file.txt interactive: true ``` If you still have problems running an interactive app through Task, please open an issue about it. ## Short task syntax Starting on Task v3, you can now write tasks with a shorter syntax if they have the default settings (e.g. no custom `env:`, `vars:`, `desc:`, `silent:` , etc): ```yaml version: '3' tasks: build: go build -v -o ./app{{exeExt}} . run: - task: build - ./app{{exeExt}} -h localhost -p 8080 ``` ## `set` and `shopt` It's possible to specify options to the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html) and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins. This can be added at global, task or command level. ```yaml version: '3' set: [pipefail] shopt: [globstar] tasks: # `globstar` required for double star globs to work default: echo **/*.go ``` ::: info Keep in mind that not all options are available in the [shell interpreter library](https://github.com/mvdan/sh) that Task uses. ::: ## Watch tasks With the flags `--watch` or `-w` task will watch for file changes and run the task again. This requires the `sources` attribute to be given, so task knows which files to watch. The default watch interval is 100 milliseconds, but it's possible to change it by either setting `interval: '500ms'` in the root of the Taskfile or by passing it as an argument like `--interval=500ms`. This interval is the time Task will wait for duplicated events. It will only run the task again once, even if multiple changes happen within the interval. Also, it's possible to set `watch: true` in a given task and it'll automatically run in watch mode: ```yaml version: '3' interval: 500ms tasks: build: desc: Builds the Go application watch: true sources: - '**/*.go' cmds: - go build # ... ``` ::: info Note that when setting `watch: true` to a task, it'll only run in watch mode when running from the CLI via `task my-watch-task`, but won't run in watch mode if called by another task, either directly or as a dependency. ::: ::: warning The watcher can misbehave in certain scenarios, in particular for long-running servers. There is a [known bug](https://github.com/go-task/task/issues/160) where child processes of the running might not be killed appropriately. It's advised to avoid running commands as `go run` and prefer `go build [...] && ./binary` instead. If you are having issues, you might want to try tools specifically designed for live-reloading, like [Air](https://github.com/air-verse/air/). Also, be sure to [report any issues](https://github.com/go-task/task/issues/new?template=bug_report.yml) to us. ::: [config]: /docs/reference/config [gotemplate]: https://golang.org/pkg/text/template/ [templating-reference]: /docs/reference/templating ================================================ FILE: website/src/docs/installation.md ================================================ --- title: Installation description: Installation methods for Task outline: deep --- # Installation Task offers many installation methods. Check out the available methods below. ## Official Package Managers These installation methods are maintained by the Task team and are always up-to-date. :::info Package Repository Hosting [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) Package repository hosting for deb/rpm/apk is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence. ::: ### [dnf](https://docs.fedoraproject.org/en-US/quick-docs/dnf) ![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=fff) ![CentOS](https://img.shields.io/badge/CentOS-002260?logo=centos&logoColor=F0F0F0) ![Fedora](https://img.shields.io/badge/Red_Hat-EE0000?logo=redhat&logoColor=white) {#dnf} [[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Arpm)] If you Set up the repository by running : ```shell curl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.rpm.sh' | sudo -E bash ``` Then you can install Task with: ```shell dnf install task ``` ### [apt](https://doc.ubuntu-fr.org/apt) ![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=Ubuntu&logoColor=white) ![Debian](https://img.shields.io/badge/debian-red?logo=debian&logoColor=orange&color=darkred) ![Linux Mint](https://img.shields.io/badge/Linux%20Mint-87CF3E?logo=linuxmint&logoColor=fff) {#apt} [[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Adeb)] If you Set up the repository by running: ```shell curl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.deb.sh' | sudo -E bash ``` Then you can install Task with: ```shell apt install task ``` ### [apk](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) ![Alpine Linux](https://img.shields.io/badge/Alpine_Linux-0D597F?logo=alpinelinux&logoColor=fff) {#apk} [[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Aalpine)] Set up the repository by running: ```shell curl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.alpine.sh' | sudo -E bash ``` Then you can install Task with: ```shell apk add task ``` ### [Homebrew](https://brew.sh) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#homebrew} Task is available via our official Homebrew tap [[source](https://github.com/go-task/homebrew-tap/blob/main/Formula/go-task.rb)]: ```shell brew install go-task/tap/go-task ``` Alternatively it can be installed from the official Homebrew repository [[package](https://formulae.brew.sh/formula/go-task)] [[source](https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/go-task.rb)] by running: ```shell brew install go-task ``` ### [Snap](https://snapcraft.io/task) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#snap} Task is available on [Snapcraft](https://snapcraft.io/task) [[source](https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml)], but keep in mind that your Linux distribution should allow classic confinement for Snaps to Task work correctly: ```shell sudo snap install task --classic ``` ### [npm](https://www.npmjs.com) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#npm} Npm can be used as cross-platform way to install Task globally or as a dependency of your project [[package](https://www.npmjs.com/package/@go-task/cli)] [[source](https://github.com/go-task/task/blob/main/package.json)]: ```shell npm install -g @go-task/cli ``` ### [WinGet](https://github.com/microsoft/winget-cli) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#winget} Task is available via the [community repository](https://github.com/microsoft/winget-pkgs) [[source](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/Task/Task)]: ```shell winget install Task.Task ``` ## Community-Maintained Package Managers ::: warning Community Maintained These installation methods are maintained by the community and may not always be up-to-date with the latest Task version. The Task team does not directly control these packages. ::: ### [Mise](https://mise.jdx.dev/) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#mise} Mise is a cross-platform package manager that acts as a "frontend" to a variety of other package managers "backends" such as `asdf`, `aqua` and `ubi`. If using Mise, we recommend using the `aqua` or `ubi` backends to install Task as these install directly from our GitHub releases. ::: code-group ```shell [aqua] mise use -g aqua:go-task/task@latest mise install ``` ```shell [ubi] mise use -g ubi:go-task/task mise install ``` ::: ### [Macports](https://macports.org) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) {#macports} Task repository is tracked by Macports [[package](https://ports.macports.org/port/go-task/details/)] [[source](https://github.com/macports/macports-ports/blob/master/devel/go-task/Portfile)]: ```shell port install go-task ``` ### [pip](https://pip.pypa.io) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#pip} Like npm, pip can be used as a cross-platform way to install Task [[package](https://pypi.org/project/go-task-bin)] [[source](https://github.com/Bing-su/pip-binary-factory/tree/main/task)]: ```shell pip install go-task-bin ``` ### [Chocolatey](https://chocolatey.org) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#chocolatey} [[package](https://community.chocolatey.org/packages/go-task)] [[source](https://github.com/Starz0r/ChocolateyPackagingScripts/blob/master/src/go-task_gh_build.py)] ```shell choco install go-task ``` ### [Scoop](https://scoop.sh) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#scoop} [[source](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json)] ```shell scoop install task ``` ### Arch ([pacman](https://wiki.archlinux.org/title/Pacman)) ![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=arch-linux&logoColor=fff) {#arch} [[package](https://archlinux.org/packages/extra/x86_64/go-task/)] [[source](https://gitlab.archlinux.org/archlinux/packaging/packages/go-task)] ```shell pacman -S go-task ``` ### Fedora ([dnf](https://docs.fedoraproject.org/en-US/quick-docs/dnf)) ![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=fff) {#fedora-community} [[package](https://packages.fedoraproject.org/pkgs/golang-github-task/go-task/)] [[source](https://src.fedoraproject.org/rpms/golang-github-task)] ```shell dnf install go-task ``` ### FreeBSD ([Ports](https://ports.freebsd.org/cgi/ports.cgi)) ![FreeBSD](https://img.shields.io/badge/FreeBSD-990000?logo=freebsd&logoColor=fff) {#freebsd} [[package](https://cgit.freebsd.org/ports/tree/devel/task)] [[source](https://cgit.freebsd.org/ports/tree/devel/task/Makefile)] ```shell pkg install task ``` ### [Nix](https://nixos.org) ![Nix](https://img.shields.io/badge/Nix-5277C3?logo=nixos&logoColor=fff) ![NixOS](https://img.shields.io/badge/NixOS-5277C3?logo=nixos&logoColor=fff) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) {#nix} [[source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)] ```shell nix-env -iA nixpkgs.go-task ``` ### [pacstall](https://github.com/pacstall/pacstall) ![Debian](https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=fff) ![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=fff) {#pacstall} [[package](https://pacstall.dev/packages/go-task-deb)] [[source](https://github.com/pacstall/pacstall-programs/blob/master/packages/go-task-deb/go-task-deb.pacscript)] ```shell pacstall -I go-task-deb ``` ### [pkgx](https://pkgx.sh) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#pkgx} [[package](https://pkgx.dev/pkgs/taskfile.dev)] [[source](https://github.com/pkgxdev/pantry/blob/main/projects/taskfile.dev/package.yml)] ```shell pkgx task ``` or, if you have pkgx integration enabled: ```shell task ``` ## Get The Binary ### Binary You can download the binary from the [releases page on GitHub](https://github.com/go-task/task/releases) and add to your `$PATH`. DEB, RPM and APK packages are also available. The `task_checksums.txt` file contains the SHA-256 checksum for each file. ### Install Script We also have an [install script](https://github.com/go-task/task/blob/main/install-task.sh) which is very useful in scenarios like CI. Many thanks to [GoDownloader](https://github.com/goreleaser/godownloader) for enabling the easy generation of this script. By default, it installs on the `./bin` directory relative to the working directory: ```shell sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d ``` It is possible to override the installation directory with the `-b` parameter. On Linux, common choices are `~/.local/bin` and `~/bin` to install for the current user or `/usr/local/bin` to install for all users: ```shell sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin ``` ::: warning On macOS and Windows, `~/.local/bin` and `~/bin` are not added to `$PATH` by default. ::: By default, it installs the latest version available. You can also specify a tag (available in [releases](https://github.com/go-task/task/releases)) to install a specific version: ```shell sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d v3.36.0 ``` Parameters are order specific, to set both installation directory and version: ```shell sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin v3.42.1 ``` ### GitHub Actions We have an [official GitHub Action](https://github.com/go-task/setup-task) to install Task in your GitHub workflows. This repository is forked from the fantastic project by the Arduino team. Check out the repository for more examples and configuration. ```yaml - name: Install Task uses: go-task/setup-task@v1 ``` ## Build From Source ### Go Modules Ensure that you have a supported version of [Go](https://golang.org) properly installed and setup. You can find the minimum required version of Go in the [go.mod](https://github.com/go-task/task/blob/main/go.mod#L3) file. You can then install the latest release globally by running: ```shell go install github.com/go-task/task/v3/cmd/task@latest ``` Or you can install into another directory: ```shell env GOBIN=/bin go install github.com/go-task/task/v3/cmd/task@latest ``` ::: tip For CI environments we recommend using the [install script](#install-script) instead, which is faster and more stable, since it'll just download the latest released binary. ::: ## Setup completions Some installation methods will automatically install completions too, but if this isn't working for you or your chosen method doesn't include them, you can run `task --completion ` to output a completion script for any supported shell. There are a couple of ways these completions can be added to your shell config: ### Option 1. Load the completions in your shell's startup config (Recommended) This method loads the completion script from the currently installed version of task every time you create a new shell. This ensures that your completions are always up-to-date. If your executable isn’t named task, set the `TASK_EXE` environment variable before running eval. ::: code-group ```shell [bash] # ~/.bashrc # export TASK_EXE='go-task' if needed eval "$(task --completion bash)" ``` ```shell [zsh] # ~/.zshrc # export TASK_EXE='go-task' if needed eval "$(task --completion zsh)" ``` ```shell [fish] # ~/.config/fish/config.fish # export TASK_EXE='go-task' if needed task --completion fish | source ``` ```powershell [powershell] # $PROFILE\Microsoft.PowerShell_profile.ps1 Invoke-Expression (&task --completion powershell | Out-String) ``` ::: ### Option 2. Copy the script to your shell's completions directory This method requires you to manually update the completions whenever Task is updated. However, it is useful if you want to modify the completions yourself. ::: code-group ```shell [bash] task --completion bash > /etc/bash_completion.d/task ``` ```shell [zsh] task --completion zsh > /usr/local/share/zsh/site-functions/_task ``` ```shell [fish] task --completion fish > ~/.config/fish/completions/task.fish ``` ::: ### Zsh customization The Zsh completion supports the standard `verbose` zstyle to control whether task descriptions are shown. By default, descriptions are displayed. To show only task names without descriptions, add this to your `~/.zshrc` (after the completion is loaded): ```shell zstyle ':completion:*:*:task:*' verbose false ``` ================================================ FILE: website/src/docs/integrations.md ================================================ --- title: Integrations description: Official and community integrations for Task, including VS Code, JSON schemas, and other tools outline: deep --- # Integrations ## Visual Studio Code Extension Task has an [official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task). The code for this project can be found in [our GitHub repository](https://github.com/go-task/vscode-task). To use this extension, you must have Task v3.45.3+ installed on your system. This extension provides the following features (and more): - View tasks in the sidebar. - Run tasks from the sidebar and command palette. - Go to definition from the sidebar and command palette. - Run last task command. - Multi-root workspace support. - Initialize a Taskfile in the current workspace. To get autocompletion and validation for your Taskfile, see the [Schema](#schema) section below. ![Task for Visual Studio Code](https://github.com/go-task/vscode-task/blob/main/res/preview.png?raw=true) ### Configuration namespace change In v1.0.0 of the extension, the configuration namespace was changed from `task` to `taskfile` in order to fix [an issue](https://github.com/go-task/vscode-task/issues/56). ![Configuration namespace change warning](../public/img/config-namespace-change.png) If you receive a warning like the one above, you will need to update your settings to use the new `taskfile` namespace instead: ![Configuration namespace diff](../public/img/config-namespace-diff.png) ## Schema This was initially created by @KROSF in [this Gist](https://gist.github.com/KROSF/c5435acf590acd632f71bb720f685895) and is now officially maintained in [this file](https://github.com/go-task/task/blob/main/website/src/public/schema.json) and made available at https://taskfile.dev/schema.json. This schema can be used to validate Taskfiles and provide autocompletion in many code editors: ### Visual Studio Code To integrate the schema into VS Code, you need to install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) by Red Hat. Any `Taskfile.yml` in your project should automatically be detected and validation/autocompletion should work. If this doesn't work or you want to manually configure it for files with a different name, you can add the following to your `settings.json`: ```json // settings.json { "yaml.schemas": { "https://taskfile.dev/schema.json": [ "**/Taskfile.yml", "./path/to/any/other/taskfile.yml" ] } } ``` You can also configure the schema directly inside of a Taskfile by adding the following comment to the top of the file: ```yaml # yaml-language-server: $schema=https://taskfile.dev/schema.json version: '3' ``` You can find more information on this in the [YAML language server project](https://github.com/redhat-developer/yaml-language-server). ## AI/LLM Assistants Task documentation is optimized for AI assistants like Claude Code, Cursor, and other LLM-powered development tools through the [VitePress LLMs plugin](https://github.com/okineadev/vitepress-plugin-llms). This integration provides: - Structured documentation in LLM-friendly formats - Context-optimized content for AI assistants - Automatic generation of `llms.txt` and `llms-full.txt` files - Enhanced discoverability of Task features for AI tools AI assistants can access Task documentation through: - **[llms.txt](https://taskfile.dev/llms.txt)**: Lightweight overview of Task documentation - **[llms-full.txt](https://taskfile.dev/llms-full.txt)**: Complete documentation with all content These files are automatically generated and kept in sync with the documentation, ensuring AI assistants always have access to the latest Task features and usage patterns. ## Community Integrations In addition to our official integrations, there is an amazing community of developers who have created their own integrations for Task: - [Sublime Text Plugin](https://packagecontrol.io/packages/Taskfile) [[source](https://github.com/biozz/sublime-taskfile)] by @biozz - [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/17058-taskfile) [[source](https://github.com/lechuckroh/task-intellij-plugin)] by @lechuckroh - [mk](https://github.com/pycontribs/mk) command line tool recognizes Taskfiles natively. - [fzf-make](https://github.com/kyu08/fzf-make) fuzzy finder with preview window for make, pnpm, yarn, just & task. If you have made something that integrates with Task, please feel free to open a PR to add it to this list. ================================================ FILE: website/src/docs/reference/cli.md ================================================ --- title: Command Line Interface Reference description: Complete reference for Task CLI commands, flags, and exit codes permalink: /reference/cli/ outline: deep --- # Command Line Interface Reference Task has multiple ways of being configured. These methods are parsed, in sequence, in the following order with the highest priority last: - [Configuration files](./config.md) - [Environment variables](./environment.md) - _Command-line flags_ In this document, we will look at the last of the three options, command-line flags. All CLI commands override their configuration file and environment variable equivalents. ## Format Task commands have the following syntax: ```bash task [options] [tasks...] [-- CLI_ARGS...] ``` ::: tip If `--` is given, all remaining arguments will be assigned to a special `CLI_ARGS` variable. ::: ## Commands ### `task [tasks...]` Run one or more tasks defined in your Taskfile. ```bash task build task test lint task deploy --force ``` ### `task --list` List all available tasks with their descriptions. ```bash task --list task -l ``` ### `task --list-all` List all tasks, including those without descriptions. ```bash task --list-all task -a ``` ### `task --init` Create a new Taskfile.yml in the current directory. ```bash task --init task -i ``` ::: tip Combine `--list` or `--list-all` with `--silent` (`-ls` or `-as` for shortants) to list only the task names in each line. Useful for scripting with `grep` or similar. ::: ## Options ### General #### `-h, --help` Show help information. ```bash task --help ``` #### `--version` Show Task version. ```bash task --version ``` #### `-v, --verbose` Enable verbose mode for detailed output. - **Config equivalent**: [`verbose`](./config.md#verbose) - **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose) ```bash task build --verbose ``` #### `-s, --silent` Disable command echoing. - **Config equivalent**: [`silent`](./config.md#silent) - **Environment variable**: [`TASK_SILENT`](./environment.md#task-silent) ```bash task deploy --silent ``` #### `--disable-fuzzy` Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. - **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy) - **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy) ```bash task buidl --disable-fuzzy # Output: Task "buidl" does not exist # (without "Did you mean 'build'?" suggestion) ``` ### Execution Control #### `-F, --failfast` Stop executing dependencies as soon as one of them fails. - **Config equivalent**: [`failfast`](./config.md#failfast) - **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast) ```bash task build --failfast ``` #### `-f, --force` Force execution even when the task is up-to-date. ```bash task build --force ``` #### `-n, --dry` Compile and print tasks without executing them. - **Environment variable**: [`TASK_DRY`](./environment.md#task-dry) ```bash task deploy --dry ``` #### `-p, --parallel` Execute multiple tasks in parallel. ```bash task test lint --parallel ``` #### `-C, --concurrency ` Limit the number of concurrent tasks. Zero means unlimited. - **Config equivalent**: [`concurrency`](./config.md#concurrency) - **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency) ```bash task test --concurrency 4 ``` #### `-x, --exit-code` Pass through the exit code of failed commands. ```bash task test --exit-code ``` ### File and Directory #### `-d, --dir ` Set the directory where Task will run and look for Taskfiles. ```bash task build --dir ./backend ``` #### `-t, --taskfile ` Specify a custom Taskfile path. ```bash task build --taskfile ./custom/Taskfile.yml ``` #### `-g, --global` Run the global Taskfile from `$HOME/Taskfile.{yml,yaml}`. ```bash task backup --global ``` ### Output Control #### `-o, --output ` Set output style. Available modes: `interleaved`, `group`, `prefixed`. ```bash task test --output group ``` #### `--output-group-begin