Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions .github/workflows/turborepo-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ jobs:
if: ${{ matrix.settings.setup }}
run: ${{ matrix.settings.setup }}

- name: Test Windows standalone installer
if: runner.os == 'Windows'
shell: powershell
run: ./scripts/install.windows.test.ps1

- name: Build
run: ${{ matrix.settings.rust-build-env }} cargo build --profile release-turborepo -p turbo --target ${{ matrix.settings.target }}

Expand Down Expand Up @@ -543,8 +548,8 @@ jobs:
pattern: turbo-*
path: rust-artifacts

- name: Test standalone archive layout
run: node --test scripts/package-standalone-archives.test.mjs
- name: Test standalone archive layout and POSIX installer
run: node --test scripts/package-standalone-archives.test.mjs scripts/install.test.mjs

- name: Package standalone archives
run: node scripts/package-standalone-archives.mjs "${{ needs.stage.outputs.version }}"
Expand All @@ -553,7 +558,9 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: standalone-turbo-${{ needs.stage.outputs.version }}
path: standalone-artifacts/*.tar.gz
path: |
standalone-artifacts/*.tar.gz
standalone-artifacts/SHA256SUMS
if-no-files-found: error

npm-publish:
Expand Down Expand Up @@ -701,10 +708,43 @@ jobs:
gh release create "v${VERSION}" \
--title "Turborepo v${VERSION}" \
--generate-notes \
--draft \
"${NOTES_START_TAG_FLAG[@]}" \
"${PRERELEASE_FLAG[@]}"
fi

publish-standalone-assets:
name: "Publish Standalone Release Assets"
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
needs: [stage, standalone-archives, create-release-tag]
if: ${{ always() && !inputs.dry_run && needs.stage.result == 'success' && needs.standalone-archives.result == 'success' && needs.create-release-tag.result == 'success' }}
steps:
- name: Download standalone release assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: standalone-turbo-${{ needs.stage.outputs.version }}
path: standalone-artifacts

- name: Attach standalone archives and checksum manifest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.stage.outputs.version }}
run: |
if [ "$(gh release view "v${VERSION}" --json isDraft --jq .isDraft)" != "true" ]; then
echo "::error::Refusing to replace assets on published release v${VERSION}."
exit 1
fi
gh release upload "v${VERSION}" standalone-artifacts/*.tar.gz standalone-artifacts/SHA256SUMS --clobber

- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.stage.outputs.version }}
run: gh release edit "v${VERSION}" --draft=false

publish-vscode-extension:
name: "Publish VS Code Extension"
needs: [stage, js-smoke-test, create-release-tag]
Expand Down Expand Up @@ -819,8 +859,15 @@ jobs:

create-release-pr:
name: "Create Release PR"
needs: [stage, npm-publish, create-release-tag, alias-versioned-docs]
if: ${{ always() && needs.npm-publish.result == 'success' && needs.create-release-tag.result == 'success' && !inputs.dry_run }}
needs:
[
stage,
npm-publish,
create-release-tag,
publish-standalone-assets,
alias-versioned-docs
]
if: ${{ always() && needs.npm-publish.result == 'success' && needs.create-release-tag.result == 'success' && needs.publish-standalone-assets.result == 'success' && !inputs.dry_run }}
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ const config: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true
},
async headers() {
const installerHeaders = [
{ key: "Content-Type", value: "text/plain; charset=utf-8" },
{
key: "Cache-Control",
value: "public, max-age=0, must-revalidate"
}
];
return [
{ source: "/install", headers: installerHeaders },
{ source: "/install.ps1", headers: installerHeaders }
];
},
typescript: {
ignoreBuildErrors: true
},
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const proxy = createProxy({

export const config = {
matcher: [
"/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|feed.xml|sitemap.xml|robots.txt|images(?:/|$)|og-image\\.png|schema\\.json|schema\\.v\\d+\\.json|microfrontends/schema\\.json|\\.well-known/security\\.txt).*)"
"/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|feed.xml|sitemap.xml|robots.txt|images(?:/|$)|og-image\\.png|schema\\.json|schema\\.v\\d+\\.json|install(?:/|$)|install[.]ps1(?:/|$)|microfrontends/schema\\.json|\\.well-known/security\\.txt).*)"
]
};

Expand Down
249 changes: 249 additions & 0 deletions apps/docs/public/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#!/bin/sh
set -eu

usage() {
cat <<'EOF'
Install the standalone Turborepo CLI without Node.js or a JavaScript package manager.

Usage: install [--version VERSION] [--install-dir DIRECTORY]

By default, the installer uses the latest stable release and installs to
~/.local/bin. Set TURBO_VERSION or TURBO_INSTALL_DIR to configure this in CI.
EOF
}

fail() {
printf '%s\n' "$1" >&2
exit 1
}

version=${TURBO_VERSION:-}
install_dir=${TURBO_INSTALL_DIR:-}
while [ "$#" -gt 0 ]; do
case "$1" in
--version)
[ "$#" -ge 2 ] || fail "--version requires a value"
version=$2
shift 2
;;
--install-dir)
[ "$#" -ge 2 ] || fail "--install-dir requires a value"
install_dir=$2
shift 2
;;
--help|-h)
usage
exit 0
;;
--)
shift
break
;;
*)
fail "unknown option: $1"
;;
esac
done
[ "$#" -eq 0 ] || fail "unexpected argument: $1"

if existing_turbo=$(command -v turbo 2>/dev/null); then
fail "An existing \"turbo\" alias was found on PATH at $existing_turbo. Uninstall it and then retry installing the new version again."
fi

[ -n "${HOME:-}" ] || fail 'HOME is not set'
[ -n "$install_dir" ] || install_dir="$HOME/.local/bin"
case "$install_dir" in
/*) ;;
*) fail "install directory must be an absolute path: $install_dir" ;;
esac
newline=$(printf '\n_')
newline=${newline%_}
carriage_return=$(printf '\r')
case "$install_dir" in
*"$newline"*|*"$carriage_return"*|*:*)
fail 'install directory must not contain a colon or line break'
;;
esac

os=$(uname -s)
architecture=$(uname -m)
case "$os:$architecture" in
Darwin:x86_64) target=x86_64-apple-darwin ;;
Darwin:arm64|Darwin:aarch64) target=aarch64-apple-darwin ;;
Linux:x86_64|Linux:amd64) target=x86_64-unknown-linux-musl ;;
Linux:arm64|Linux:aarch64) target=aarch64-unknown-linux-musl ;;
*) fail "unsupported platform: $os/$architecture" ;;
esac

binary_name=turbo
destination="$install_dir/$binary_name"
if [ -e "$destination" ] || [ -L "$destination" ]; then
fail "$destination already exists; it was left untouched. Remove it yourself or choose another --install-dir."
fi

for utility in curl tar awk grep sed head mktemp mkdir cp chmod ln rm tr; do
command -v "$utility" >/dev/null 2>&1 || fail "required command not found: $utility"
done

work_dir=$(mktemp -d "${TMPDIR:-/tmp}/turbo-install.XXXXXX") || fail 'could not create a temporary directory'
staged_binary=
cleanup() {
if [ -n "$staged_binary" ]; then
rm -f "$staged_binary"
fi
rm -rf "$work_dir"
}
trap cleanup 0
trap 'exit 130' INT
trap 'exit 143' TERM

shell_quote() {
quoted=$(printf '%s' "$1" | sed "s/'/'\\\\''/g")
printf "'%s'" "$quoted"
}

append_path_profile() {
profile=$1
marker="# Turborepo standalone installer PATH: $install_dir"
if [ -f "$profile" ] && grep -Fqx "$marker" "$profile"; then
return 0
fi

quoted_install_dir=$(shell_quote "$install_dir")
if ! {
printf '\n%s\n' "$marker"
printf 'case ":$PATH:" in\n'
printf ' *:%s:*) ;;\n' "$quoted_install_dir"
printf ' *) PATH=%s:"$PATH" ;;\n' "$quoted_install_dir"
printf 'esac\nexport PATH\n'
} >> "$profile"; then
return 1
fi
}

download() {
curl --fail --show-error --silent --location \
--proto '=https' --proto-redir '=https' "$@"
}

if [ -z "$version" ] || [ "$version" = latest ]; then
metadata="$work_dir/release.json"
if ! download --output "$metadata" \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
'https://api.github.com/repos/vercel/turborepo/releases/latest'; then
fail 'could not resolve the latest stable release from GitHub'
fi
version=$(sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/p' "$metadata" | head -n 1)
fi

case "$version" in
v*) version=${version#v} ;;
esac
if ! printf '%s\n' "$version" | LC_ALL=C grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
fail "invalid release version: ${version:-<empty>}"
fi

archive_name="turbo-$version-$target.tar.gz"
release_url="https://github.com/vercel/turborepo/releases/download/v$version"
manifest="$work_dir/SHA256SUMS"
archive="$work_dir/$archive_name"
if ! download --output "$manifest" "$release_url/SHA256SUMS"; then
fail "could not download the checksum manifest for v$version"
fi
if ! download --output "$archive" "$release_url/$archive_name"; then
fail "could not download the standalone archive for $target at v$version"
fi

expected_checksum=$(awk -v filename="$archive_name" '
$2 == filename {
count++
if (NF == 2) digest = $1
else digest = ""
}
END {
if (count == 1) print digest
}
' "$manifest")
if ! printf '%s\n' "$expected_checksum" | LC_ALL=C grep -Eq '^[[:xdigit:]]{64}$'; then
fail "checksum manifest must contain exactly one SHA-256 entry for $archive_name"
fi
if command -v sha256sum >/dev/null 2>&1; then
actual_checksum=$(sha256sum "$archive" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual_checksum=$(shasum -a 256 "$archive" | awk '{print $1}')
else
fail 'a SHA-256 utility (sha256sum or shasum) is required'
fi
expected_checksum=$(printf '%s' "$expected_checksum" | tr '[:upper:]' '[:lower:]')
actual_checksum=$(printf '%s' "$actual_checksum" | tr '[:upper:]' '[:lower:]')
[ "$actual_checksum" = "$expected_checksum" ] || fail "SHA-256 verification failed for $archive_name"

members=$(tar -tzf "$archive") || fail "could not inspect $archive_name"
[ "$members" = "$binary_name" ] || fail "unexpected archive contents in $archive_name; expected only $binary_name"
extracted_dir="$work_dir/extracted"
mkdir "$extracted_dir"
tar -xzf "$archive" -C "$extracted_dir" || fail "could not extract $archive_name"
extracted_binary="$extracted_dir/$binary_name"
[ -f "$extracted_binary" ] && [ ! -L "$extracted_binary" ] || fail 'archive did not contain a regular turbo executable'
chmod 0755 "$extracted_binary"

mkdir -p "$install_dir" || fail "could not create install directory: $install_dir"
if [ -e "$destination" ] || [ -L "$destination" ]; then
fail "$destination already exists; it was left untouched. Remove it yourself or choose another --install-dir."
fi
staged_binary=$(mktemp "$install_dir/.turbo-install.XXXXXX") || fail "could not create a staging file in $install_dir"
if ! cp "$extracted_binary" "$staged_binary" || ! chmod 0755 "$staged_binary"; then
fail "could not stage turbo in $install_dir"
fi
if ! ln "$staged_binary" "$destination"; then
fail "could not install $destination without replacing an existing file"
fi
rm -f "$staged_binary"
staged_binary=

configure_path() {
if [ -n "${GITHUB_PATH:-}" ]; then
if ! grep -Fqx "$install_dir" "$GITHUB_PATH"; then
printf '%s\n' "$install_dir" >> "$GITHUB_PATH" || return 1
fi
return 0
fi
[ "${TURBO_NO_MODIFY_PATH:-0}" != 1 ] || return 1

shell_name=${SHELL:-}
shell_name=${shell_name##*/}
case "$shell_name" in
zsh)
append_path_profile "$HOME/.zprofile" || return 1
append_path_profile "$HOME/.zshrc" || return 1
;;
bash)
if [ -f "$HOME/.bash_profile" ]; then
login_profile="$HOME/.bash_profile"
elif [ -f "$HOME/.bash_login" ]; then
login_profile="$HOME/.bash_login"
else
login_profile="$HOME/.profile"
fi
append_path_profile "$login_profile" || return 1
append_path_profile "$HOME/.bashrc" || return 1
;;
fish)
return 1
;;
*)
append_path_profile "$HOME/.profile" || return 1
;;
esac
}

printf 'Installed turbo %s at %s\n' "$version" "$destination"
if configure_path; then
case ":${PATH:-}:" in
*":$install_dir:"*) ;;
*) printf 'PATH configured for future shells; open a new shell to run `turbo` by name.\n' ;;
esac
else
printf 'Add %s to PATH to run `turbo` by name.\n' "$install_dir"
fi
Loading
Loading