Description of feature
The target host, the sudo setting, and the host-key policy are all process-global rather than per-invocation, which means docket can only ever talk to one server at a time within a process.
subprocess.SetDefaultHost writes a package-level defaultHost guarded by a mutex, and CallExecCommandWithContext falls back to it whenever ExecCommandInput.Host is empty. No task ever populates that field - the only Host: assignments anywhere in tasks/ are GitAuthTask's own field and port numbers in PortsTask - so in practice every dokku call routes through the global. Sudo and host-key acceptance are worse than global: resolveSshFlags in commands/ssh_flags.go bridges them through os.Setenv of DOKKU_SUDO and DOKKU_SSH_ACCEPT_NEW_HOST_KEYS, which subprocess/ssh.go reads back at argv-build time.
For the CLI as it exists this is invisible, because one invocation targets one host with one policy. It stops being invisible the moment anything wants to run two targets concurrently, or embed the tasks package as a library, or run task tests in parallel - SetExecRunner is documented as test-only and explicitly not safe under t.Parallel() for the same reason. It also quietly rules out a recipe that spans hosts, which is the natural way to express a migration or a fan-out deploy and is currently impossible to represent.
The field to carry this already exists on ExecCommandInput; what is missing is a way for a task to know what to put in it. Options worth weighing are threading a connection value through Plan() and Execute() (which pairs naturally with #424 on context), giving tasks an embedded target struct the loader populates, or replacing the global with a per-call runner the caller supplies. Whichever shape wins should cover sudo and host-key acceptance too, not just the host string, since they are part of the same decision.
The masking registry in subprocess/mask.go has the same global shape. It is less pressing, since masking is output formatting rather than routing, but it is worth settling at the same time.
Description of feature
The target host, the sudo setting, and the host-key policy are all process-global rather than per-invocation, which means docket can only ever talk to one server at a time within a process.
subprocess.SetDefaultHostwrites a package-leveldefaultHostguarded by a mutex, andCallExecCommandWithContextfalls back to it wheneverExecCommandInput.Hostis empty. No task ever populates that field - the onlyHost:assignments anywhere intasks/areGitAuthTask's own field and port numbers inPortsTask- so in practice every dokku call routes through the global. Sudo and host-key acceptance are worse than global:resolveSshFlagsincommands/ssh_flags.gobridges them throughos.SetenvofDOKKU_SUDOandDOKKU_SSH_ACCEPT_NEW_HOST_KEYS, whichsubprocess/ssh.goreads back at argv-build time.For the CLI as it exists this is invisible, because one invocation targets one host with one policy. It stops being invisible the moment anything wants to run two targets concurrently, or embed the
taskspackage as a library, or run task tests in parallel -SetExecRunneris documented as test-only and explicitly not safe undert.Parallel()for the same reason. It also quietly rules out a recipe that spans hosts, which is the natural way to express a migration or a fan-out deploy and is currently impossible to represent.The field to carry this already exists on
ExecCommandInput; what is missing is a way for a task to know what to put in it. Options worth weighing are threading a connection value throughPlan()andExecute()(which pairs naturally with #424 on context), giving tasks an embedded target struct the loader populates, or replacing the global with a per-call runner the caller supplies. Whichever shape wins should cover sudo and host-key acceptance too, not just the host string, since they are part of the same decision.The masking registry in
subprocess/mask.gohas the same global shape. It is less pressing, since masking is output formatting rather than routing, but it is worth settling at the same time.