From faf42a96b7a5a49079318a5a06c38d3a0ed2552a Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Mon, 29 Jun 2026 15:18:45 +0100 Subject: [PATCH 1/3] Bootstrap pnpm in testbox scripts --- scripts/test-pr-check-windows.ps1 | 78 +++++++++++++++++++++++++++++++ scripts/test-pr-check.sh | 38 +++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/scripts/test-pr-check-windows.ps1 b/scripts/test-pr-check-windows.ps1 index 7994817781..b6f3d6bc7b 100644 --- a/scripts/test-pr-check-windows.ps1 +++ b/scripts/test-pr-check-windows.ps1 @@ -2,6 +2,76 @@ $ErrorActionPreference = "Stop" Set-Location (git rev-parse --show-toplevel) +function Find-NodeBin { + param([Parameter(Mandatory = $true)][string]$VersionPrefix) + + $roots = @() + if ($env:RUNNER_TOOL_CACHE) { + $roots += $env:RUNNER_TOOL_CACHE + } + $roots += @( + "C:\hostedtoolcache\windows", + "C:\hostedtoolcache", + "C:\actions-runner\_work\_tool" + ) + + foreach ($root in $roots) { + $nodeRoot = Join-Path $root "node" + if (-not (Test-Path $nodeRoot)) { + continue + } + + $match = Get-ChildItem -Path $nodeRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name.StartsWith($VersionPrefix) } | + Sort-Object Name | + Select-Object -Last 1 + + if ($match) { + $bin = Join-Path $match.FullName "x64" + if (Test-Path (Join-Path $bin "node.exe")) { + return $bin + } + } + } + + return $null +} + +function Ensure-Pnpm { + $pnpmVersion = if ($env:PNPM_VERSION) { $env:PNPM_VERSION } else { "10.33.2" } + $npmPrefix = Join-Path $env:USERPROFILE ".npm-global" + $env:PATH = "$npmPrefix;$env:PATH" + + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + return + } + + if (Get-Command corepack -ErrorAction SilentlyContinue) { + try { + Invoke-Native corepack prepare "pnpm@$pnpmVersion" --activate + } catch { + Write-Warning "corepack could not activate pnpm: $_" + } + } + + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + return + } + + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + throw "Unable to find pnpm or npm on PATH." + } + + New-Item -ItemType Directory -Force -Path $npmPrefix | Out-Null + Invoke-Native npm config set prefix $npmPrefix + Invoke-Native npm install -g "pnpm@$pnpmVersion" + $env:PATH = "$npmPrefix;$env:PATH" + + if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { + throw "Unable to install pnpm." + } +} + function Start-Section { param([Parameter(Mandatory = $true)][string]$Title) @@ -39,6 +109,14 @@ function Invoke-Section { } } +$node20Bin = Find-NodeBin "20.20" +if ($node20Bin) { + $env:PATH = "$node20Bin;$env:PATH" +} + +Ensure-Pnpm +Invoke-Native pnpm --version + Invoke-Section "Install CLI dependencies" { Invoke-Native pnpm install --frozen-lockfile --filter trigger.dev... } diff --git a/scripts/test-pr-check.sh b/scripts/test-pr-check.sh index 7ea105e2f9..a55e71e0dc 100755 --- a/scripts/test-pr-check.sh +++ b/scripts/test-pr-check.sh @@ -39,6 +39,36 @@ find_node_bin() { find /opt/hostedtoolcache/node -maxdepth 3 -type d -path "*/${version_prefix}*/x64/bin" 2>/dev/null | sort -V | tail -n 1 } +ensure_pnpm() { + local pnpm_version="${PNPM_VERSION:-10.33.2}" + local npm_prefix="${HOME}/.npm-global" + export PATH="${npm_prefix}/bin:${PATH}" + + if command -v pnpm >/dev/null 2>&1; then + return 0 + fi + + if command -v corepack >/dev/null 2>&1; then + corepack prepare "pnpm@${pnpm_version}" --activate || true + fi + + if command -v pnpm >/dev/null 2>&1; then + return 0 + fi + + if ! command -v npm >/dev/null 2>&1; then + echo "Unable to find pnpm or npm on PATH." >&2 + return 1 + fi + + mkdir -p "${npm_prefix}" + npm config set prefix "${npm_prefix}" + npm install -g "pnpm@${pnpm_version}" + export PATH="${npm_prefix}/bin:${PATH}" + + command -v pnpm >/dev/null 2>&1 +} + with_node() { local node_bin="$1" shift @@ -125,6 +155,7 @@ run_sdk_runtime_compat_tests() { } export -f find_node_bin +export -f ensure_pnpm export -f with_node export -f run_webapp_unit_tests export -f run_package_unit_tests @@ -146,6 +177,13 @@ export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=8192}" NODE20_BIN="${NODE20_BIN:-$(find_node_bin 20.20)}" NODE22_BIN="${NODE22_BIN:-$(find_node_bin 22.12)}" +if [[ -n "${NODE20_BIN}" ]]; then + export PATH="${NODE20_BIN}:${PATH}" +fi + +ensure_pnpm +pnpm --version + run_section "Install dependencies" pnpm install --frozen-lockfile run_section "Generate Prisma client" pnpm run generate From 21d7d37a01072fbc34c53fff1e243973febc24bc Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Mon, 29 Jun 2026 16:37:12 +0100 Subject: [PATCH 2/3] fix: blacksmith testbox scripts --- scripts/blacksmith.md | 6 + scripts/test-pr-check-windows.ps1 | 6 + scripts/test-pr-check.sh | 183 +++++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 3 deletions(-) diff --git a/scripts/blacksmith.md b/scripts/blacksmith.md index 691e930edb..d32dbf8e23 100644 --- a/scripts/blacksmith.md +++ b/scripts/blacksmith.md @@ -24,6 +24,12 @@ Run the PR checks against your current working tree: blacksmith testbox run --id "scripts/test-pr-check.sh" ``` +The script prints a ✅/❌ line after each section and a final summary with durations. It fails fast by default. To keep going and collect all failures before exiting non-zero, run: + +```bash +blacksmith testbox run --id "TEST_PR_CHECK_CONTINUE_ON_ERROR=1 scripts/test-pr-check.sh" +``` + ## Windows PR checks Testbox The Windows PR Testbox covers the Windows CLI v3 e2e matrix row. diff --git a/scripts/test-pr-check-windows.ps1 b/scripts/test-pr-check-windows.ps1 index b6f3d6bc7b..3d3f40c655 100644 --- a/scripts/test-pr-check-windows.ps1 +++ b/scripts/test-pr-check-windows.ps1 @@ -1,5 +1,7 @@ $ErrorActionPreference = "Stop" +# Used inside Blacksmith Testbox runners to run full CI test suite + Set-Location (git rev-parse --show-toplevel) function Find-NodeBin { @@ -109,6 +111,10 @@ function Invoke-Section { } } +if (-not $env:CI) { + $env:CI = "true" +} + $node20Bin = Find-NodeBin "20.20" if ($node20Bin) { $env:PATH = "$node20Bin;$env:PATH" diff --git a/scripts/test-pr-check.sh b/scripts/test-pr-check.sh index a55e71e0dc..8d1318bd67 100755 --- a/scripts/test-pr-check.sh +++ b/scripts/test-pr-check.sh @@ -1,21 +1,102 @@ #!/usr/bin/env bash set -euo pipefail +# Used inside Blacksmith Testbox runners to run full CI test suite + cd "$(git rev-parse --show-toplevel)" +declare -a SECTION_NAMES=() +declare -a SECTION_STATUSES=() +declare -a SECTION_DURATIONS=() +SUMMARY_PRINTED=0 + +format_duration() { + local seconds="$1" + printf "%dm%02ds" "$((seconds / 60))" "$((seconds % 60))" +} + section() { local title="$1" echo "" - echo "::group::${title}" + if [[ "${TEST_PR_CHECK_USE_GITHUB_GROUPS:-}" == "1" ]]; then + echo "::group::${title}" + else + echo "▶ ${title}" + fi } end_section() { - echo "::endgroup::" + if [[ "${TEST_PR_CHECK_USE_GITHUB_GROUPS:-}" == "1" ]]; then + echo "::endgroup::" + fi +} + +record_section() { + local title="$1" + local status="$2" + local duration="$3" + + SECTION_NAMES+=("${title}") + SECTION_STATUSES+=("${status}") + SECTION_DURATIONS+=("${duration}") } +print_summary() { + if [[ "${SUMMARY_PRINTED}" == "1" || "${#SECTION_NAMES[@]}" -eq 0 ]]; then + return 0 + fi + + SUMMARY_PRINTED=1 + echo "" + echo "PR check summary" + echo "================" + + local failures=0 + local index + for ((index = 0; index < ${#SECTION_NAMES[@]}; index++)); do + local icon="✅" + if [[ "${SECTION_STATUSES[$index]}" != "0" ]]; then + icon="❌" + failures=$((failures + 1)) + fi + + printf "%s %-42s %8s\n" \ + "${icon}" \ + "${SECTION_NAMES[$index]}" \ + "$(format_duration "${SECTION_DURATIONS[$index]}")" + done + + if [[ "${failures}" -gt 0 ]]; then + echo "" + echo "${failures} section(s) failed." + fi +} + +has_failures() { + local index + for ((index = 0; index < ${#SECTION_STATUSES[@]}; index++)); do + if [[ "${SECTION_STATUSES[$index]}" != "0" ]]; then + return 0 + fi + done + + return 1 +} + +on_exit() { + local status="$?" + print_summary + exit "${status}" +} + +trap on_exit EXIT + run_section() { local title="$1" shift + local start + start="$(date +%s)" + section "$title" local status=0 @@ -25,7 +106,23 @@ run_section() { status=$? fi + local duration + duration="$(($(date +%s) - start))" + record_section "${title}" "${status}" "${duration}" + end_section + + if [[ "${status}" == "0" ]]; then + echo "✅ ${title} passed in $(format_duration "${duration}")" + return 0 + fi + + echo "❌ ${title} failed in $(format_duration "${duration}")" + + if [[ "${TEST_PR_CHECK_CONTINUE_ON_ERROR:-}" == "1" ]]; then + return 0 + fi + return "${status}" } @@ -69,6 +166,73 @@ ensure_pnpm() { command -v pnpm >/dev/null 2>&1 } +find_tool_bin() { + local name="$1" + shift + + local candidate + for candidate in "$@"; do + if [[ -x "${candidate}/${name}" ]]; then + printf '%s\n' "${candidate}" + return 0 + fi + done + + if [[ -d /opt/hostedtoolcache ]]; then + find /opt/hostedtoolcache -maxdepth 6 -type f -name "${name}" -perm -u+x 2>/dev/null | + head -n 1 | + xargs -r dirname + fi +} + +ensure_bun() { + export PATH="${HOME}/.bun/bin:${PATH}" + + if command -v bun >/dev/null 2>&1; then + return 0 + fi + + local bin_dir + bin_dir="$(find_tool_bin bun "${HOME}/.bun/bin")" + if [[ -n "${bin_dir}" ]]; then + export PATH="${bin_dir}:${PATH}" + return 0 + fi + + if ! command -v curl >/dev/null 2>&1; then + echo "Unable to find bun on PATH, and curl is unavailable to install it." >&2 + return 1 + fi + + curl -fsSL https://bun.sh/install | bash + export PATH="${HOME}/.bun/bin:${PATH}" + command -v bun >/dev/null 2>&1 +} + +ensure_deno() { + export PATH="${HOME}/.deno/bin:${PATH}" + + if command -v deno >/dev/null 2>&1; then + return 0 + fi + + local bin_dir + bin_dir="$(find_tool_bin deno "${HOME}/.deno/bin")" + if [[ -n "${bin_dir}" ]]; then + export PATH="${bin_dir}:${PATH}" + return 0 + fi + + if ! command -v curl >/dev/null 2>&1; then + echo "Unable to find deno on PATH, and curl is unavailable to install it." >&2 + return 1 + fi + + curl -fsSL https://deno.land/install.sh | sh + export PATH="${HOME}/.deno/bin:${PATH}" + command -v deno >/dev/null 2>&1 +} + with_node() { local node_bin="$1" shift @@ -156,6 +320,9 @@ run_sdk_runtime_compat_tests() { export -f find_node_bin export -f ensure_pnpm +export -f find_tool_bin +export -f ensure_bun +export -f ensure_deno export -f with_node export -f run_webapp_unit_tests export -f run_package_unit_tests @@ -165,6 +332,7 @@ export -f run_cli_e2e_tests export -f run_sdk_node_compat_tests export -f run_sdk_runtime_compat_tests +export CI="${CI:-true}" export DATABASE_URL="${DATABASE_URL:-postgresql://postgres:postgres@localhost:5432/postgres}" export DIRECT_URL="${DIRECT_URL:-postgresql://postgres:postgres@localhost:5432/postgres}" export SESSION_SECRET="${SESSION_SECRET:-secret}" @@ -182,13 +350,18 @@ if [[ -n "${NODE20_BIN}" ]]; then fi ensure_pnpm +ensure_bun +ensure_deno pnpm --version +bun --version +deno --version run_section "Install dependencies" pnpm install --frozen-lockfile -run_section "Generate Prisma client" pnpm run generate run_section "Format check" pnpm exec oxfmt --check . run_section "Lint" pnpm exec oxlint . + +run_section "Generate Prisma client" pnpm run generate run_section "Typecheck" pnpm run typecheck run_section "Check exports" pnpm run check-exports @@ -206,4 +379,8 @@ else fi run_section "SDK Bun/Deno/Cloudflare compatibility tests" run_sdk_runtime_compat_tests "${NODE20_BIN}" +if has_failures; then + exit 1 +fi + echo "All Linux PR checks completed." From 8e6ca6574b9f80b63f05924587b036317dcba86b Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Mon, 29 Jun 2026 16:46:00 +0100 Subject: [PATCH 3/3] respond to review comments --- scripts/test-pr-check.sh | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/scripts/test-pr-check.sh b/scripts/test-pr-check.sh index 8d1318bd67..c45fbee00b 100755 --- a/scripts/test-pr-check.sh +++ b/scripts/test-pr-check.sh @@ -179,10 +179,15 @@ find_tool_bin() { done if [[ -d /opt/hostedtoolcache ]]; then - find /opt/hostedtoolcache -maxdepth 6 -type f -name "${name}" -perm -u+x 2>/dev/null | - head -n 1 | - xargs -r dirname + local found + found="$(find /opt/hostedtoolcache -maxdepth 6 -type f -name "${name}" -perm -u+x 2>/dev/null | head -n 1)" + if [[ -n "${found}" ]]; then + dirname "${found}" + return 0 + fi fi + + return 0 } ensure_bun() { @@ -193,20 +198,13 @@ ensure_bun() { fi local bin_dir - bin_dir="$(find_tool_bin bun "${HOME}/.bun/bin")" - if [[ -n "${bin_dir}" ]]; then + if bin_dir="$(find_tool_bin bun "${HOME}/.bun/bin")" && [[ -n "${bin_dir}" ]]; then export PATH="${bin_dir}:${PATH}" return 0 fi - if ! command -v curl >/dev/null 2>&1; then - echo "Unable to find bun on PATH, and curl is unavailable to install it." >&2 - return 1 - fi - - curl -fsSL https://bun.sh/install | bash - export PATH="${HOME}/.bun/bin:${PATH}" - command -v bun >/dev/null 2>&1 + echo "Unable to find bun on PATH. The pr-testbox.yml warmup should install Bun; check the setup-bun step or the Testbox PATH." >&2 + return 1 } ensure_deno() { @@ -217,20 +215,13 @@ ensure_deno() { fi local bin_dir - bin_dir="$(find_tool_bin deno "${HOME}/.deno/bin")" - if [[ -n "${bin_dir}" ]]; then + if bin_dir="$(find_tool_bin deno "${HOME}/.deno/bin")" && [[ -n "${bin_dir}" ]]; then export PATH="${bin_dir}:${PATH}" return 0 fi - if ! command -v curl >/dev/null 2>&1; then - echo "Unable to find deno on PATH, and curl is unavailable to install it." >&2 - return 1 - fi - - curl -fsSL https://deno.land/install.sh | sh - export PATH="${HOME}/.deno/bin:${PATH}" - command -v deno >/dev/null 2>&1 + echo "Unable to find deno on PATH. The pr-testbox.yml warmup should install Deno; check the setup-deno step or the Testbox PATH." >&2 + return 1 } with_node() {