Repository navigation
Copier update: install task from uv #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,6 @@ | |
| import argparse | ||
| import os | ||
| import platform | ||
| import shlex | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
|
|
@@ -22,9 +21,9 @@ | |
| PRE_COMMIT_VERSION = "4.6.2" | ||
| TASK_VERSION = "3.53.1" | ||
| DOWNLOAD_TIMEOUT_SECONDS = 90 | ||
| # Where both uv's and Task's installers place binaries. Resolves from USERPROFILE on Windows, so it | ||
| # matches the runner's home directory without assuming its user name. Already on PATH on POSIX, but | ||
| # not on Windows, which is why uv is invoked through an absolute path there. | ||
| # Where uv places both itself and the executables of the tools it installs. Resolves from USERPROFILE | ||
| # on Windows, so it matches the runner's home directory without assuming its user name. Already on | ||
| # PATH on POSIX, but not on Windows, which is why uv is invoked through an absolute path there. | ||
| LOCAL_BIN_DIR = Path.home() / ".local" / "bin" | ||
| INSTALL_SSM_PLUGIN_BY_DEFAULT = False | ||
| # A floor rather than an exact pin: GitHub's runner images ship their own build of the plugin and only | ||
|
|
@@ -67,55 +66,60 @@ def pwsh_cmd(cmd: str) -> list[str]: | |
| return [pwsh, "-NoProfile", "-NonInteractive", "-Command", cmd] | ||
|
|
||
|
|
||
| def install_task(*, is_windows: bool) -> None: | ||
| """Install the pinned Task release into `LOCAL_BIN_DIR`. | ||
| def install_uv(uv_env: dict[str, str], *, is_windows: bool) -> None: | ||
| """Install the pinned uv release into `LOCAL_BIN_DIR`. | ||
|
|
||
| Deliberately not `npm install -g @go-task/cli`: that writes to the global prefix of whichever | ||
| node is on PATH, and in CI that is the pnpm-managed node installed by `pnpm/setup`, whose prefix | ||
| bin directory is not the one on PATH. The package installs successfully and the linked | ||
| executable is then unreachable, so `task` fails with exit status 127. | ||
| Runs regardless of `--no-python`, because uv is also how Task is installed, and every job needs | ||
| the task runner even when it has no Python environments to set up. | ||
|
|
||
| Verification goes through the absolute path because this process resolved PATH before the | ||
| install; `GITHUB_PATH` is appended so later steps in the same CI job can invoke `task` by name. | ||
| `uv_env` is mutated on Windows rather than copied: `LOCAL_BIN_DIR` is not on the runner's PATH | ||
| there, and every later uv invocation needs it in front. | ||
| """ | ||
| LOCAL_BIN_DIR.mkdir(parents=True, exist_ok=True) | ||
| if is_windows: | ||
| if platform.machine().lower() == "arm64": | ||
| windows_arch = "arm64" | ||
| else: | ||
| windows_arch = "amd64" | ||
| archive_url = ( | ||
| f"https://github.com/go-task/task/releases/download/v{TASK_VERSION}/task_windows_{windows_arch}.zip" | ||
| ) | ||
| # The bin directory is handed over as an environment variable rather than interpolated into the | ||
| # script text: PowerShell single quotes take no escapes, so an apostrophe in the home directory | ||
| # would terminate the string early. A variable reference is one argument whatever it contains. | ||
| task_env = dict(os.environ) | ||
| task_env.update({"TASK_BIN_DIR": str(LOCAL_BIN_DIR)}) | ||
| powershell_statements = [ | ||
| "$ErrorActionPreference = 'Stop'", | ||
| "$archive = Join-Path $env:TEMP 'task-release.zip'", | ||
| f"Invoke-WebRequest -UseBasicParsing -Uri '{archive_url}' -OutFile $archive", | ||
| "Expand-Archive -Path $archive -DestinationPath $env:TASK_BIN_DIR -Force", | ||
| "Remove-Item $archive", | ||
| ] | ||
| uv_env.update({"PATH": rf"{LOCAL_BIN_DIR};{uv_env['PATH']}"}) | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| pwsh_cmd("; ".join(powershell_statements)), | ||
| pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex"), | ||
| check=True, | ||
| env=task_env, | ||
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| task_path = LOCAL_BIN_DIR / "task.exe" | ||
| else: | ||
| # The installer resolves the pinned tag against the published checksums, so the archive is verified. | ||
| # The bin directory is shell-quoted because a space or apostrophe in the home directory would | ||
| # otherwise split it into multiple arguments, or unbalance the quoting outright. | ||
| _ = subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input | ||
| f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://taskfile.dev/install.sh | sh -s -- -b {shlex.quote(str(LOCAL_BIN_DIR))} v{TASK_VERSION}", | ||
| f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/install.sh | sh", | ||
| check=True, | ||
| shell=True, | ||
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| # TODO: add uv autocompletion to the shell https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion | ||
|
|
||
|
|
||
| def install_task(uv_path: str, uv_env: dict[str, str], *, is_windows: bool) -> None: | ||
| """Install the pinned Task release into `LOCAL_BIN_DIR` as a uv tool. | ||
|
|
||
| `go-task-bin` repackages the upstream release archives as one wheel per platform, so a single uv | ||
| invocation covers Windows, macOS and Linux on both x86_64 and arm64. Task publishes nothing to | ||
| PyPI itself, so this is knowingly a third-party distribution: the version is pinned and uv | ||
| records the wheel hash in the tool receipt it writes. | ||
|
|
||
| Deliberately not `npm install -g @go-task/cli`: that writes to the global prefix of whichever | ||
| node is on PATH, and in CI that is the pnpm-managed node installed by `pnpm/setup`, whose prefix | ||
| bin directory is not the one on PATH. The package installs successfully and the linked | ||
| executable is then unreachable, so `task` fails with exit status 127. | ||
|
|
||
| Verification goes through the absolute path because this process resolved PATH before the | ||
| install; `GITHUB_PATH` is appended so later steps in the same CI job can invoke `task` by name. | ||
| """ | ||
| LOCAL_BIN_DIR.mkdir(parents=True, exist_ok=True) | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| [uv_path, "tool", "install", f"go-task-bin=={TASK_VERSION}"], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: git diff -- .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja
sed -n '90,130p' .devcontainer/install-ci-tooling.py
sed -n '100,120p' template/.devcontainer/install-ci-tooling.py.jinja
rg -n 'TASK_BIN_DIR|LOCAL_BIN_DIR|task_windows|install_task|tool install|--force' .Repository: LabAutomationAndScreening/copier-python-package-template Length of output: 3334 🌐 Web query:
💡 Result: <source_evidence> Citations:
🏁 Script executed: set -eu
printf '%s\n' '--- status and diff ---'
git status --short
git diff --stat -- .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja
git diff -- .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja | sed -n '1,240p'
printf '%s\n' '--- installer constants and entry points ---'
sed -n '1,125p' .devcontainer/install-ci-tooling.py
printf '%s\n' '--- template installer ---'
sed -n '1,118p' template/.devcontainer/install-ci-tooling.py.jinja
printf '%s\n' '--- task and uv references ---'
rg -n -C 3 'TASK_VERSION|TASK|UV_VERSION|uv_path|uv tool|task\.exe|LOCAL_BIN_DIR|UV_TOOL_BIN_DIR|pipx|go-task' .devcontainer template pyproject.toml .github 2>/dev/null || true
printf '%s\n' '--- prior versions from repository history ---'
git log -4 --oneline -- .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja
for f in .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja; do
printf '%s\n' "--- $f at HEAD ---"
git show "HEAD:$f" 2>/dev/null | rg -n -C 5 'install_task|TASK_VERSION|uv tool|task\.exe|LOCAL_BIN_DIR|UV_TOOL_BIN_DIR|pipx|go-task' || true
doneRepository: LabAutomationAndScreening/copier-python-package-template Length of output: 34488 🏁 Script executed: set -eu
printf '%s\n' '--- parent installer Task implementation ---'
for f in .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja; do
printf '%s\n' "--- $f at 7ccd45f^ ---"
git show "7ccd45f^:$f" 2>/dev/null | nl -ba | sed -n '1,145p' | rg -n -C 12 'TASK|task|install|LOCAL_BIN|uv' || true
done
printf '%s\n' '--- commit change summary ---'
git show --stat --oneline 7ccd45f
git show --format=fuller --no-ext-diff 7ccd45f -- .devcontainer/install-ci-tooling.py template/.devcontainer/install-ci-tooling.py.jinja | sed -n '1,260p'
printf '%s\n' '--- pinned uv documentation/source references ---'
rg -n -C 4 'Overwriting executables|not overwrite|--force|tool install' . 2>/dev/null | head -240 || trueRepository: LabAutomationAndScreening/copier-python-package-template Length of output: 30131 🌐 Web query:
💡 Result: <source_evidence> Citations:
🌐 Web query:
💡 Result: <source_evidence> Citations:
🏁 Script executed: set -eu
url='https://raw.githubusercontent.com/astral-sh/uv/0.12.15/crates/uv/src/commands/tool/common.rs'
printf '%s\n' '--- uv 0.12.15 source status ---'
curl -fsSL "$url" | rg -n -C 8 'existing_entrypoints|Executables already exist|force|target_path.exists'Repository: LabAutomationAndScreening/copier-python-package-template Length of output: 2712 Handle the Task installer migration. An environment upgraded from the previous installer can retain an unmanaged Add
🧰 Tools🪛 ast-grep (0.45.3)[error] 113-118: Command coming from incoming request (subprocess-from-request) 🤖 Prompt for AI Agents |
||
| check=True, | ||
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| if is_windows: | ||
| task_path = LOCAL_BIN_DIR / "task.exe" | ||
| else: | ||
| task_path = LOCAL_BIN_DIR / "task" | ||
| _ = subprocess.run([str(task_path), "--version"], check=True) # noqa: S603 # this is all our own input | ||
| if "GITHUB_PATH" in os.environ: | ||
|
|
@@ -248,24 +252,8 @@ def main(): | |
| uv_path = str(LOCAL_BIN_DIR / "uv") | ||
| else: | ||
| uv_path = "uv" | ||
| install_uv(uv_env, is_windows=is_windows) | ||
| if not args.no_python: | ||
| if is_windows: | ||
| uv_env.update({"PATH": rf"{LOCAL_BIN_DIR};{uv_env['PATH']}"}) | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex"), | ||
| check=True, | ||
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| else: | ||
| _ = subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input | ||
| f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/install.sh | sh", | ||
| check=True, | ||
| shell=True, | ||
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| # TODO: add uv autocompletion to the shell https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| [ | ||
| uv_path, | ||
|
|
@@ -290,19 +278,19 @@ def main(): | |
| env=uv_env, | ||
| timeout=DOWNLOAD_TIMEOUT_SECONDS, | ||
| ) | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| [ | ||
| uv_path, | ||
| "tool", | ||
| "list", | ||
| ], | ||
| check=True, | ||
| env=uv_env, | ||
| ) | ||
| if not args.no_node: | ||
| run_node_cmds(["npm -v", f"npm install -g pnpm@{PNPM_VERSION}", "pnpm -v"], is_windows=is_windows) | ||
| # Task is installed outside the --no-node branch because CI always passes --no-node (pnpm/setup handles pnpm there), and every job still needs the task runner | ||
| install_task(is_windows=is_windows) | ||
| install_task(uv_path, uv_env, is_windows=is_windows) | ||
| _ = subprocess.run( # noqa: S603 # this is all our own input | ||
| [ | ||
| uv_path, | ||
| "tool", | ||
| "list", | ||
| ], | ||
| check=True, | ||
| env=uv_env, | ||
| ) | ||
| if INSTALL_SSM_PLUGIN_BY_DEFAULT and not args.skip_installing_ssm_plugin: | ||
| ensure_ssm_plugin(is_windows=is_windows) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: LabAutomationAndScreening/copier-python-package-template
Length of output: 18375
🌐 Web query:
uv 0.8.13 installer UV_INSTALL_DIR UV_TOOL_BIN_DIR XDG_BIN_HOME tool executable directory documentation💡 Result:
<source_evidence>
Citations:
🌐 Web query:
uv 0.12.15 installer UV_UNMANAGED_INSTALL UV_INSTALL_DIR precedence UV_TOOL_BIN_DIR XDG_BIN_HOME official documentation💡 Result:
<source_evidence>
Citations:
🌐 Web query:
site:github.com/astral-sh/uv install.sh UV_UNMANAGED_INSTALL UV_INSTALL_DIR precedence source💡 Result:
<source_evidence>
Citations:
Pin the uv and tool executable directories.
uv_envinheritsUV_INSTALL_DIR,UV_UNMANAGED_INSTALL,UV_TOOL_BIN_DIR,XDG_BIN_HOME, and related variables. These variables can placeuvor the Task executable outsideLOCAL_BIN_DIR.On Windows, later calls use
LOCAL_BIN_DIR / "uv". On both Windows and POSIX,install_taskvalidatesLOCAL_BIN_DIR / "task(.exe)". These calls can fail when an inherited variable selects another directory. POSIX invokesuvby name, so the uv call can fail when the selected install directory is not onPATH.In both installer variants, remove
UV_UNMANAGED_INSTALLfromuv_env, then set the explicit directories:XDG_BIN_HOMEand related fallback variables do not need separate normalization when these explicit settings are present.🧰 Tools
🪛 ast-grep (0.45.3)
[error] 79-84: Use of unsanitized data to create processes
Context: subprocess.run( # noqa: S603 # this is all our own input
pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex"),
check=True,
env=uv_env,
timeout=DOWNLOAD_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(os-system-unsanitized-data)
[error] 79-84: Command coming from incoming request
Context: subprocess.run( # noqa: S603 # this is all our own input
pwsh_cmd(f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex"),
check=True,
env=uv_env,
timeout=DOWNLOAD_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 86-92: Use of unsanitized data to create processes
Context: subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input
f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/install.sh | sh",
check=True,
shell=True,
env=uv_env,
timeout=DOWNLOAD_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(os-system-unsanitized-data)
[error] 86-92: Command coming from incoming request
Context: subprocess.run( # noqa: S602 # we need to set shell to true to use the pipe operator, and this is all our own input
f"curl -fsSL --connect-timeout 20 --max-time 40 --retry 3 --retry-delay 5 --retry-connrefused --proto '=https' https://astral.sh/uv/{UV_VERSION}/install.sh | sh",
check=True,
shell=True,
env=uv_env,
timeout=DOWNLOAD_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🤖 Prompt for AI Agents