Tools in the tools input can now be pinned to a checksum using the tool:version@sha256:<hash> or tool:version@sha512:<hash> syntax. The downloaded tool is verified against the checksum on all platforms, including when it is served from the tools cache, and it is removed along with its cache entry if the verification fails. Checksum verification is supported for tools downloaded as phar archives. Specifying a checksum for tools set up using composer packages or custom package scripts results in an error. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
324 lines
12 KiB
Bash
324 lines
12 KiB
Bash
# Variables
|
|
export composer_home="$HOME/.composer"
|
|
export composer_bin="$composer_home/vendor/bin"
|
|
export composer_json="$composer_home/composer.json"
|
|
export composer_lock="$composer_home/composer.lock"
|
|
skip_composer_github_auth=false
|
|
|
|
# Function to extract tool version.
|
|
get_tool_version() {
|
|
tool=$1
|
|
param=$2
|
|
alp="[a-zA-Z0-9\.]"
|
|
version_regex="[0-9]+((\.{1}$alp+)+)(\.{0})(-$alp+){0,1}"
|
|
if [ "$tool" = "composer" ]; then
|
|
composer_alias_version="$(grep -Ea "const\sBRANCH_ALIAS_VERSION" "${tool_path_dir:?}/composer" | grep -Eo "$version_regex")"
|
|
if [[ -n "$composer_alias_version" ]]; then
|
|
composer_version="$composer_alias_version+$(grep -Ea "const\sVERSION" "$tool_path_dir/composer" | grep -Eo "$alp+" | tail -n 1)"
|
|
else
|
|
composer_version="$(grep -Ea "const\sVERSION" "$tool_path_dir/composer" | grep -Eo "$version_regex")"
|
|
fi
|
|
echo "$composer_version" | sudo tee /tmp/composer_version
|
|
elif [ -n "$param" ]; then
|
|
$tool "$param" 2>/dev/null | sed -Ee "s/[Cc]omposer(.)?$version_regex//g" | grep -Eo "$version_regex" | head -n 1
|
|
fi
|
|
}
|
|
|
|
# Function to configure composer
|
|
configure_composer() {
|
|
tool_path=$1
|
|
sudo ln -sf "$tool_path" "$tool_path.phar"
|
|
php -r "try {\$p=new Phar('$tool_path.phar', 0);exit(0);} catch(Exception \$e) {exit(1);}"
|
|
if [ $? -eq 1 ]; then
|
|
add_log "${cross:?}" "composer" "Could not download composer"
|
|
exit 1
|
|
fi
|
|
if ! [ -d "$composer_home" ]; then
|
|
sudo -u "$(id -un)" -g "$(id -gn)" mkdir -p -m=00755 "$composer_home"
|
|
else
|
|
sudo chown -R "$(id -un)":"$(id -gn)" "$composer_home"
|
|
fi
|
|
if ! [ -e "$composer_json" ]; then
|
|
echo '{}' | tee "$composer_json" >/dev/null
|
|
chmod 644 "$composer_json"
|
|
fi
|
|
get_tool_version composer >/dev/null
|
|
set_composer_env
|
|
add_path "$composer_bin"
|
|
set_composer_auth
|
|
}
|
|
|
|
# Function to merge auth.json fragments.
|
|
update_auth_json() {
|
|
local auth_file="$composer_home/auth.json"
|
|
local merged
|
|
[[ -f "$auth_file" ]] && merged=$(<"$auth_file") || merged='{}'
|
|
for frag in "$@"; do
|
|
local obj="{$frag}"
|
|
merged=$(jq -n --argjson b "$merged" --argjson n "$obj" '
|
|
if $n|has("http-basic") then
|
|
(($b["http-basic"]//{}) + $n["http-basic"]) as $hb
|
|
| ($b + $n) | .["http-basic"] = $hb
|
|
else
|
|
$b + $n
|
|
end
|
|
')
|
|
done
|
|
printf '%s' "$merged" > "$composer_home/auth.json"
|
|
}
|
|
|
|
# Function to check if public GitHub token authentication is possible.
|
|
can_access_public_github() {
|
|
curl --fail -s -H "Authorization: token $1" 'https://api.github.com/' >/dev/null 2>&1
|
|
}
|
|
|
|
composer_gh_auth_no_op() {
|
|
local message
|
|
message="$(<"${src:?}"/configs/composer-gh-auth-warn)"
|
|
message="${message//%s/$composer_version}"
|
|
if [ "${fail_fast:-false}" = "true" ]; then
|
|
add_log "${cross:?}" "composer" "$message"
|
|
else
|
|
echo "::warning::$message"
|
|
fi
|
|
}
|
|
|
|
# Function to setup authentication in composer.
|
|
set_composer_auth() {
|
|
token="${COMPOSER_TOKEN:-$GITHUB_TOKEN}"
|
|
if [ -n "${COMPOSER_AUTH_JSON:-}" ]; then
|
|
if printf '%s' "$COMPOSER_AUTH_JSON" | jq -e . >/dev/null; then
|
|
printf '%s' "$COMPOSER_AUTH_JSON" | tee "$composer_home/auth.json" >/dev/null
|
|
else
|
|
add_log "${cross:?}" "composer" "Could not parse COMPOSER_AUTH_JSON as valid JSON"
|
|
fi
|
|
fi
|
|
if [ "$skip_composer_github_auth" = "true" ]; then
|
|
composer_gh_auth_no_op
|
|
fi
|
|
composer_auth=()
|
|
if [ -n "$PACKAGIST_TOKEN" ]; then
|
|
composer_auth+=( '"http-basic": {"repo.packagist.com": { "username": "token", "password": "'"$PACKAGIST_TOKEN"'"}}' )
|
|
fi
|
|
if [ -n "$token" ]; then
|
|
write_token=true
|
|
if [ "$skip_composer_github_auth" = "true" ]; then
|
|
write_token=false
|
|
fi
|
|
if [ "$GITHUB_SERVER_URL" != "https://github.com" ]; then
|
|
can_access_public_github "$token" || write_token=false
|
|
fi
|
|
if [ "$write_token" = 'true' ]; then
|
|
composer_auth+=( '"github-oauth": {"github.com": "'"$token"'"}' )
|
|
fi
|
|
fi
|
|
if ((${#composer_auth[@]})); then
|
|
update_auth_json "${composer_auth[@]}"
|
|
fi
|
|
}
|
|
|
|
# Function to set composer environment variables.
|
|
set_composer_env() {
|
|
composer_env="${src:?}"/configs/composer.env
|
|
if [ -n "$COMPOSER_PROCESS_TIMEOUT" ]; then
|
|
sed_arg="s/COMPOSER_PROCESS_TIMEOUT.*/COMPOSER_PROCESS_TIMEOUT=$COMPOSER_PROCESS_TIMEOUT/"
|
|
sed -i "$sed_arg" "$composer_env" 2>/dev/null || sed -i '' "$sed_arg" "$composer_env"
|
|
fi
|
|
add_env_path "$composer_env"
|
|
if [ -n "$COMPOSER_ALLOW_PLUGINS" ]; then
|
|
echo "$COMPOSER_ALLOW_PLUGINS" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | while IFS= read -r plugin; do
|
|
composer global config --no-plugins "allow-plugins.$plugin" true >/dev/null 2>&1
|
|
done
|
|
fi
|
|
}
|
|
|
|
# Function to identify latest-like URLs that should bypass the persistent cache.
|
|
is_mutable_tool_url() {
|
|
local tool_url=$1
|
|
local mutable_url_regex='(^|[/?#._=-])(latest|stable|preview|snapshot|nightly|master)([/?#._=-]|$)|/releases/latest/download/'
|
|
local version_like_regex='(^|[^0-9])[0-9]+\.[0-9]+([.-][0-9A-Za-z]+)*'
|
|
[[ "$tool_url" =~ $mutable_url_regex ]] && return 0
|
|
[[ "$tool_url" =~ \.phar([?#].*)?$ && ! "$tool_url" =~ $version_like_regex ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Helper function to configure tools.
|
|
add_tools_helper() {
|
|
tool=$1
|
|
extensions=()
|
|
if [ "$tool" = "blackfire-player" ]; then
|
|
extensions+=(uuid)
|
|
elif [ "$tool" = "box" ]; then
|
|
extensions+=(iconv mbstring phar sodium)
|
|
elif [ "$tool" = "codeception" ]; then
|
|
extensions+=(json mbstring)
|
|
sudo ln -s "$scoped_dir"/vendor/bin/codecept "$scoped_dir"/vendor/bin/codeception 2>/dev/null || true
|
|
elif [ "$tool" = "composer" ]; then
|
|
configure_composer "$tool_path"
|
|
elif [ "$tool" = "cs2pr" ]; then
|
|
sudo sed -i 's/\r$//; s/exit(9)/exit(0)/' "$tool_path" 2>/dev/null ||
|
|
sudo sed -i '' 's/\r$//; s/exit(9)/exit(0)/' "$tool_path"
|
|
elif [ "$tool" = "deployer" ]; then
|
|
sudo ln -s "$tool_path" "$tool_path_dir"/deployer 2>/dev/null || true
|
|
sudo ln -s "$tool_path" "$tool_path_dir"/dep 2>/dev/null || true
|
|
elif [ "$tool" = "phan" ]; then
|
|
extensions+=(fileinfo ast)
|
|
elif [ "$tool" = "phinx" ]; then
|
|
extensions+=(mbstring)
|
|
elif [ "$tool" = "phive" ]; then
|
|
extensions+=(curl mbstring xml)
|
|
elif [[ "$tool" =~ phpc(bf|s) ]]; then
|
|
extensions+=(tokenizer simplexml xmlwriter)
|
|
elif [[ "$tool" =~ phpc(bf|s) ]]; then
|
|
extensions+=(tokenizer xmlwriter simplexml)
|
|
elif [ "$tool" = "php-cs-fixer" ]; then
|
|
extensions+=(json tokenizer)
|
|
elif [ "$tool" = "phpDocumentor" ]; then
|
|
extensions+=(ctype hash json fileinfo iconv mbstring simplexml xml)
|
|
sudo ln -s "$tool_path" "$tool_path_dir"/phpdocumentor 2>/dev/null || true
|
|
sudo ln -s "$tool_path" "$tool_path_dir"/phpdoc 2>/dev/null || true
|
|
elif [ "$tool" = "phpunit" ]; then
|
|
extensions+=(dom json libxml mbstring xml xmlwriter)
|
|
elif [ "$tool" = "phpunit-bridge" ]; then
|
|
extensions+=(dom pdo tokenizer xmlwriter xmlreader)
|
|
elif [[ "$tool" =~ phpunit(-polyfills)?$ ]]; then
|
|
if [ -e "$tool_path_dir"/phpunit ] && [ -d "$composer_bin" ]; then
|
|
sudo cp "$tool_path_dir"/phpunit "$composer_bin"
|
|
fi
|
|
elif [ "$tool" = "vapor-cli" ]; then
|
|
extensions+=(fileinfo json mbstring zip simplexml)
|
|
sudo ln -s "$scoped_dir"/vendor/bin/vapor "$scoped_dir"/vendor/bin/vapor-cli 2>/dev/null || true
|
|
elif [ "$tool" = wp-cli ]; then
|
|
sudo ln -s "$tool_path" "$tool_path_dir"/"${tool%-*}" 2>/dev/null || true
|
|
fi
|
|
for extension in "${extensions[@]}"; do
|
|
add_extension "$extension" extension >/dev/null 2>&1
|
|
done
|
|
}
|
|
|
|
# Function to setup a remote tool.
|
|
add_tool() {
|
|
url=$1
|
|
tool=$2
|
|
ver_param=$3
|
|
checksum=
|
|
local arg
|
|
for arg in "$@"; do
|
|
[[ "$arg" =~ ^sha(256|512):[0-9a-fA-F]+$ ]] && checksum="$arg"
|
|
done
|
|
[[ "$ver_param" =~ ^sha(256|512): ]] && ver_param=
|
|
if [ "$tool" = "composer" ]; then
|
|
skip_composer_github_auth="${4:-false}"
|
|
[[ "$skip_composer_github_auth" =~ ^sha(256|512): ]] && skip_composer_github_auth=false
|
|
fi
|
|
tool_path="$tool_path_dir/$tool"
|
|
if ! [ -d "$tool_path_dir" ]; then
|
|
sudo mkdir -p "$tool_path_dir"
|
|
fi
|
|
if ! [ -d "$tool_cache_path_dir" ]; then
|
|
sudo mkdir -p "$tool_cache_path_dir"
|
|
fi
|
|
add_path "$tool_path_dir" verify
|
|
add_path "$tool_cache_path_dir"
|
|
IFS="," read -r -a url <<<"$url"
|
|
cache_key=$(get_sha256 "${url[0]}" | head -c 16)
|
|
cache_path="$tool_cache_path_dir/${tool}-${cache_key}"
|
|
use_cache=true
|
|
is_mutable_tool_url "${url[0]}" && use_cache=false
|
|
status_code="200"
|
|
if [ "$use_cache" = "true" ] && [ -f "$cache_path" ]; then
|
|
sudo cp -a "$cache_path" "$tool_path"
|
|
else
|
|
[ -f "$tool_path" ] && sudo cp -a "$tool_path" "$tool_path.bak"
|
|
status_code=$(get -v -e "$tool_path" "${url[@]}")
|
|
if [ "$status_code" != "200" ] && [[ "${url[0]}" =~ .*github.com.*releases.*latest.* ]]; then
|
|
url[0]="${url[0]//releases\/latest\/download/releases/download/$(get -s -n "" "$(echo "${url[0]}" | cut -d '/' -f '1-5')/releases" | grep -Eo -m 1 "([0-9]+\.[0-9]+\.[0-9]+)/$(echo "${url[0]}" | sed -e "s/.*\///")" | cut -d '/' -f 1)}"
|
|
status_code=$(get -v -e "$tool_path" "${url[0]}")
|
|
fi
|
|
if [ "$status_code" = "200" ]; then
|
|
[ "$use_cache" = "true" ] && sudo cp -a "$tool_path" "$cache_path"
|
|
elif [ -f "$tool_path.bak" ]; then
|
|
sudo mv "$tool_path.bak" "$tool_path"
|
|
fi
|
|
sudo rm -f "$tool_path.bak"
|
|
fi
|
|
if [ "$status_code" = "200" ] && [ -n "$checksum" ] && ! verify_checksum "$tool_path" "$checksum"; then
|
|
sudo rm -f "$tool_path" "$cache_path"
|
|
status_code="checksum_mismatch"
|
|
fi
|
|
if [ "$status_code" = "200" ]; then
|
|
add_tools_helper "$tool"
|
|
tool_version=$(get_tool_version "$tool" "$ver_param")
|
|
sudo ln -sfn "$tool_path" "$tool_cache_path_dir/$tool" 2>/dev/null || true
|
|
add_log "${tick:?}" "$tool" "Added $tool $tool_version"
|
|
else
|
|
if [ "$tool" = "composer" ]; then
|
|
export fail_fast=true
|
|
fi
|
|
if [ "$status_code" = "checksum_mismatch" ]; then
|
|
add_log "$cross" "$tool" "Checksum verification failed for $tool"
|
|
elif [ "$status_code" = "404" ]; then
|
|
add_log "$cross" "$tool" "Failed to download $tool from ${url[*]}"
|
|
else
|
|
add_log "$cross" "$tool" "Could not setup $tool"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Function to setup a tool using composer in a different scope.
|
|
add_composer_tool_helper() {
|
|
tool=$1
|
|
release=$2
|
|
prefix=$3
|
|
scope=$4
|
|
composer_args=$5
|
|
enable_extensions curl mbstring openssl
|
|
tool_version=${release##*:}; [ "$tool_version" = "$tool" ] && tool_version="*"
|
|
if [ "$scope" = "global" ]; then
|
|
sudo rm -f "$composer_lock" >/dev/null 2>&1 || true
|
|
if composer global show "$prefix$tool" "$tool_version" -a 2>&1 | grep -qE '^type *: *composer-plugin' && [ -n "$composer_args" ]; then
|
|
composer global config --no-plugins allow-plugins."$prefix$tool" true >/dev/null 2>&1
|
|
fi
|
|
composer global require "$prefix$release" "$composer_args" >/dev/null 2>&1
|
|
composer global show "$prefix$tool" 2>&1 | grep -E ^versions | sudo tee /tmp/composer.log >/dev/null 2>&1
|
|
else
|
|
scoped_dir="$composer_bin/_tools/$tool-$(echo -n "$release" | shasum -a 256 | cut -d ' ' -f 1)"
|
|
if ! [ -d "$scoped_dir" ]; then
|
|
mkdir -p "$scoped_dir"
|
|
echo '{}' | tee "$scoped_dir/composer.json" >/dev/null
|
|
if composer show "$prefix$tool" "$tool_version" -d "$scoped_dir" -a 2>&1 | grep -qE '^type *: *composer-plugin' && [ -n "$composer_args" ]; then
|
|
composer config -d "$scoped_dir" --no-plugins allow-plugins."$prefix$tool" true >/dev/null 2>&1
|
|
fi
|
|
composer require "$prefix$release" -d "$scoped_dir" "$composer_args" >/dev/null 2>&1
|
|
composer show "$prefix$tool" -d "$scoped_dir" 2>&1 | grep -E ^versions | sudo tee /tmp/composer.log >/dev/null 2>&1
|
|
fi
|
|
add_path "$scoped_dir"/vendor/bin
|
|
fi
|
|
}
|
|
|
|
# Function to setup a tool using composer.
|
|
add_composer_tool() {
|
|
tool=$1
|
|
release=$2
|
|
prefix=$3
|
|
scope=$4
|
|
composer_args=
|
|
composer_major_version=$(cut -d'.' -f 1 /tmp/composer_version)
|
|
if [ "$composer_major_version" != "1" ]; then
|
|
composer_args="--ignore-platform-req=ext-*"
|
|
if [[ "$tool" =~ prestissimo|composer-prefetcher ]]; then
|
|
echo "::warning:: Skipping $tool, as it does not support Composer $composer_version. Specify composer:v1 in tools to use $tool"
|
|
add_log "$cross" "$tool" "Skipped"
|
|
return
|
|
fi
|
|
fi
|
|
add_composer_tool_helper "$tool" "$release" "$prefix" "$scope" "$composer_args"
|
|
tool_version=$(get_tool_version cat /tmp/composer.log)
|
|
([ -s /tmp/composer.log ] && add_log "$tick" "$tool" "Added $tool $tool_version"
|
|
) || add_log "$cross" "$tool" "Could not setup $tool"
|
|
add_tools_helper "$tool"
|
|
if [ -e "$composer_bin/composer" ]; then
|
|
sudo cp -a "$tool_path_dir/composer" "$composer_bin"
|
|
fi
|
|
}
|