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
76 changes: 57 additions & 19 deletions .github/actions/_lib/git-route.sh
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
#!/usr/bin/env bash
# Shared git routing: an url.insteadOf rewrite that presents a token for one
# https host (optionally one namespace under it), expressed as GIT_CONFIG_*
# environment entries so it dies with the job and never lands in a gitconfig
# file. Source it, then call:
# Shared git routing: url.insteadOf rewrites that present a token for one
# https host, expressed as GIT_CONFIG_* environment entries so they die with
# the job and never land in a gitconfig file. Source it, then call:
#
# git_route_lines <token> <host> <username> <path> <start-index>
# git_remap_lines <token> <host> <username> <from> <to> <start-index>
#
# It validates every piece (the token first, before it is masked or embedded),
# prints `::add-mask::<token>` to stdout for the runner to redact, and prints
# the environment lines the caller appends to $GITHUB_ENV or to a sealed
# container's env-file:
# git_route_lines routes the host, optionally one namespace under it:
# GIT_CONFIG_COUNT=<start-index + 1>
# GIT_CONFIG_KEY_<start-index>=url.https://<username>:<token>@<host>/<path>/.insteadOf
# GIT_CONFIG_VALUE_<start-index>=https://<host>/<path>/
# CARGO_NET_GIT_FETCH_WITH_CLI=true
# cargo's libgit2 path ignores git config rewrites; the git CLI honors them.
# A validation failure prints ::error:: and returns 1 without echoing the token.
git_route_lines() {
local token="$1" host="${2:-github.com}" username="${3:-x-access-token}" path="${4:-}" n="${5:-0}"
# git_remap_lines sends fetches of <from>, an https URL on any host, to <to>,
# a repository path on the routed host, with the token presented:
# GIT_CONFIG_COUNT=<start-index + 1>
# GIT_CONFIG_KEY_<start-index>=url.https://<username>:<token>@<host>/<to>.insteadOf
# GIT_CONFIG_VALUE_<start-index>=<from>
# git applies the longest matching insteadOf value, so a remap outranks a
# namespace rewrite that matches the same URL.
#
# Both validate every piece (the token first, before it is masked or
# embedded), print `::add-mask::<token>` to stdout for the runner to redact,
# then the environment lines the caller appends to $GITHUB_ENV or to a sealed
# container's env-file. cargo's libgit2 path ignores git config rewrites; the
# git CLI honors them. A validation failure prints ::error:: and returns 1
# without echoing the token.

# One or more /-separated segments of the forge login/group/repository charset.
_GIT_ROUTE_PATH_RE='^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?(/[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)*$'
_GIT_ROUTE_HOST_RE='^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:[0-9]{1,5})?$'

# The pieces every entry embeds: token, host, userinfo name, start index.
_git_route_check() {
local token="$1" host="$2" username="$3" n="$4"
[ -n "$token" ] || { echo "::error::token input is empty" >&2; return 1; }
# The token ends up inside one environment line: a newline would write a line
# of its own, and `::add-mask::` would only have masked the first line. The
Expand All @@ -27,30 +42,53 @@ git_route_lines() {
echo "::error::the token contains characters this action cannot embed in a git URL (whitespace, or one of @:/?#%); percent-encode it, or mint one without them" >&2
return 1
fi
if ! [[ "$host" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:[0-9]{1,5})?$ ]]; then
if ! [[ "$host" =~ $_GIT_ROUTE_HOST_RE ]]; then
echo "::error::host '${host}' is not a valid host[:port]" >&2
return 1
fi
if ! [[ "$username" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::error::username '${username}' is not a valid userinfo name" >&2
return 1
fi
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
echo "::error::GIT_CONFIG_COUNT '${n}' is not a number" >&2
return 1
fi
}

git_route_lines() {
local token="$1" host="${2:-github.com}" username="${3:-x-access-token}" path="${4:-}" n="${5:-0}"
_git_route_check "$token" "$host" "$username" "$n" || return 1
local prefix="https://${host}/"
if [ -n "$path" ]; then
# One or more /-separated segments of the forge login/group charset.
if ! [[ "$path" =~ ^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?(/[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)*$ ]]; then
if ! [[ "$path" =~ $_GIT_ROUTE_PATH_RE ]]; then
echo "::error::path '${path}' is not a valid namespace path" >&2
return 1
fi
prefix="https://${host}/${path}/"
fi
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
echo "::error::GIT_CONFIG_COUNT '${n}' is not a number" >&2
return 1
fi
printf '::add-mask::%s\n' "$token"
printf 'GIT_CONFIG_COUNT=%s\n' "$((n + 1))"
printf 'GIT_CONFIG_KEY_%s=url.https://%s:%s@%s.insteadOf\n' "$n" "$username" "$token" "${prefix#https://}"
printf 'GIT_CONFIG_VALUE_%s=%s\n' "$n" "$prefix"
printf 'CARGO_NET_GIT_FETCH_WITH_CLI=true\n'
}

git_remap_lines() {
local token="$1" host="${2:-github.com}" username="${3:-x-access-token}" from="${4:-}" to="${5:-}" n="${6:-0}"
_git_route_check "$token" "$host" "$username" "$n" || return 1
# An https URL prefix without credentials, query or fragment.
local from_re='^https://[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?(:[0-9]{1,5})?(/[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)+$'
if ! [[ "$from" =~ $from_re ]]; then
echo "::error::remap source '${from}' is not an https URL of host and path segments" >&2
return 1
fi
if ! [[ "$to" =~ $_GIT_ROUTE_PATH_RE ]]; then
echo "::error::remap target '${to}' is not a repository path on ${host}" >&2
return 1
fi
printf '::add-mask::%s\n' "$token"
printf 'GIT_CONFIG_COUNT=%s\n' "$((n + 1))"
printf 'GIT_CONFIG_KEY_%s=url.https://%s:%s@%s/%s.insteadOf\n' "$n" "$username" "$token" "$host" "$to"
printf 'GIT_CONFIG_VALUE_%s=%s\n' "$n" "$from"
}
82 changes: 82 additions & 0 deletions .github/actions/_lib/install-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Install one binary from a pinned release archive. Source it, then:
#
# release_target the runner's musl target triple
# release_sha <sha256-x86_64> <sha256-aarch64> the pin for the runner's architecture
# install_release <url> <sha256> <bin> <dest-dir> downloads, verifies and installs the binary
#
# install_release downloads the .tar.gz to a temporary directory (through
# retry_transient), verifies it against the pinned sha256 before anything is
# extracted, and installs the file named <bin> into <dest-dir>, wherever the
# archive keeps it, so layouts with and without a top-level directory both
# work. A malformed pin, a checksum mismatch or an archive without that file
# prints ::error:: and returns 1; nothing lands in <dest-dir> then.
#
# The static musl builds run on any Linux of their architecture, inside a job
# container too, which is why this installer knows no other target.

# shellcheck source=retry-transient.sh disable=SC1091
source "$(dirname "${BASH_SOURCE[0]}")/retry-transient.sh"

release_target() {
local os arch
os=$(uname -s)
arch=$(uname -m)
[ "$os" = Linux ] || {
echo "::error::pinned releases install on Linux runners only (got $os)" >&2
return 1
}
case "$arch" in
x86_64) echo x86_64-unknown-linux-musl ;;
aarch64 | arm64) echo aarch64-unknown-linux-musl ;;
*)
echo "::error::no pinned build for architecture $arch" >&2
return 1
;;
esac
}

release_sha() {
local target
target=$(release_target) || return 1
case "$target" in
x86_64-*) echo "$1" ;;
aarch64-*) echo "$2" ;;
esac
}

install_release() {
local url="$1" sha="$2" bin="$3" dest="$4" tmp found
[[ "$sha" =~ ^[a-f0-9]{64}$ ]] || {
echo "::error::invalid sha256 pin '$sha' for $url" >&2
return 1
}
[[ "$bin" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
echo "::error::invalid binary name '$bin'" >&2
return 1
}
tmp=$(mktemp -d)
# Download, then verify: the checksum is pinned by the caller, so a moved
# release asset fails here rather than landing an unexpected binary.
if ! retry_transient curl --proto '=https' --tlsv1.2 -sSfL "$url" -o "$tmp/archive.tar.gz"; then
rm -rf "$tmp"
echo "::error::download failed: $url" >&2
return 1
fi
if ! echo "$sha $tmp/archive.tar.gz" | sha256sum -c - >/dev/null 2>&1; then
rm -rf "$tmp"
echo "::error::sha256 mismatch for $url: the pin is $sha" >&2
return 1
fi
mkdir -p "$tmp/x"
tar -xzf "$tmp/archive.tar.gz" -C "$tmp/x"
found=$(find "$tmp/x" -type f -name "$bin" | head -n 1)
if [ -z "$found" ]; then
rm -rf "$tmp"
echo "::error::$url holds no file named $bin" >&2
return 1
fi
mkdir -p "$dest"
install -m 0755 "$found" "$dest/$bin"
rm -rf "$tmp"
}
187 changes: 187 additions & 0 deletions .github/actions/_lib/sccache-backend.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
# The sccache backend the job environment configures, and what the sccache
# action derives from it. Source it, then:
#
# sccache_backend kind, prefix variable, read/write variable, endpoint variable
# sccache_config_file the configuration file sccache reads
# sccache_namespace_valid <namespace> whether a namespace fits the grammar below
# sccache_namespace_env <namespace> NAME=VALUE that puts the objects below the backend's prefix
# sccache_rw_env <write> NAME=READ_ONLY when write is "false"
# sccache_location_kind <location> the backend kind of the cache location a server reports
# sccache_endpoint_answers <url> whether an http(s) endpoint answers at all
#
# The rules are those of sccache 0.17.0, the pinned release. A multi-level
# chain (SCCACHE_MULTILEVEL_CHAIN) comes first, then a cache in sccache's
# configuration file, which sccache merges with the environment backend by
# backend; both print as kinds without variables ("multilevel - - -",
# "file - - -"), so the action activates on them and leaves them as they
# are configured. Otherwise the backend is the first configured one in
# sccache's own fallback order (S3, Redis, Memcached, GCS, Azure, WebDAV,
# OSS, COS, then a local SCCACHE_DIR), so what the action exports lands where
# the objects go. Without any backend, the functions print nothing.

sccache_backend() {
if [ -n "${SCCACHE_MULTILEVEL_CHAIN:-}" ]; then
echo "multilevel - - -"
elif _sccache_file_caches "$(sccache_config_file)"; then
echo "file - - -"
elif [ -n "${SCCACHE_BUCKET:-}" ]; then
echo "s3 SCCACHE_S3_KEY_PREFIX SCCACHE_S3_RW_MODE SCCACHE_ENDPOINT"
elif [ -n "${SCCACHE_REDIS_ENDPOINT:-}${SCCACHE_REDIS_CLUSTER_ENDPOINTS:-}${SCCACHE_REDIS:-}" ]; then
echo "redis SCCACHE_REDIS_KEY_PREFIX SCCACHE_REDIS_RW_MODE -"
elif [ -n "${SCCACHE_MEMCACHED_ENDPOINT:-}${SCCACHE_MEMCACHED:-}" ]; then
echo "memcached SCCACHE_MEMCACHED_KEY_PREFIX SCCACHE_MEMCACHED_RW_MODE -"
elif [ -n "${SCCACHE_GCS_BUCKET:-}" ]; then
echo "gcs SCCACHE_GCS_KEY_PREFIX SCCACHE_GCS_RW_MODE -"
# Azure needs the container and the connection string together; 0.17.0
# knows no other way to authenticate.
elif [ -n "${SCCACHE_AZURE_BLOB_CONTAINER:-}" ] && [ -n "${SCCACHE_AZURE_CONNECTION_STRING:-}" ]; then
echo "azure SCCACHE_AZURE_KEY_PREFIX SCCACHE_AZURE_RW_MODE -"
elif [ -n "${SCCACHE_WEBDAV_ENDPOINT:-}" ]; then
echo "webdav SCCACHE_WEBDAV_KEY_PREFIX SCCACHE_WEBDAV_RW_MODE SCCACHE_WEBDAV_ENDPOINT"
elif [ -n "${SCCACHE_OSS_BUCKET:-}" ]; then
echo "oss SCCACHE_OSS_KEY_PREFIX SCCACHE_OSS_RW_MODE SCCACHE_OSS_ENDPOINT"
elif [ -n "${SCCACHE_COS_BUCKET:-}" ]; then
echo "cos SCCACHE_COS_KEY_PREFIX SCCACHE_COS_RW_MODE SCCACHE_COS_ENDPOINT"
elif [ -n "${SCCACHE_DIR:-}" ]; then
echo "disk SCCACHE_DIR SCCACHE_LOCAL_RW_MODE -"
fi
}

# SCCACHE_CONF when set, empty included (sccache then reads no file),
# otherwise the default path: $XDG_CONFIG_HOME/sccache/config when that is
# absolute, ~/.config/sccache/config otherwise.
sccache_config_file() {
if [ -n "${SCCACHE_CONF+x}" ]; then
printf '%s\n' "$SCCACHE_CONF"
return 0
fi
local base="${XDG_CONFIG_HOME:-}"
case "$base" in
/*) ;;
*) base="${HOME:-}/.config" ;;
esac
printf '%s\n' "$base/sccache/config"
}

# A configuration file configures a cache when it holds a [cache] or
# [cache.<kind>] table, a dotted cache.<kind> key or an inline cache table,
# or, as a .json file, a "cache" key. A disk table counts too: the
# environment's disk settings replace it as a whole, its directory included.
_sccache_file_caches() {
local file="$1"
[ -n "$file" ] && [ -f "$file" ] || return 1
case "$file" in
*.json) grep -Eq '"cache"[[:space:]]*:' "$file" ;;
*) grep -Eq "^[[:space:]]*(\[[[:space:]]*[\"']?cache[\"']?[[:space:]]*[].]|[\"']?cache[\"']?[[:space:]]*[.=])" "$file" ;;
esac
}

# Grammar: segments of [A-Za-z0-9._-] joined by "/", nothing else. That is
# what the strictest backend, an S3 object proxy admitting key characters
# only, accepts; an empty segment and a query string fall out of it, and the
# dot-only segments "." and ".." are refused by name.
sccache_namespace_valid() {
local namespace="$1"
if ! printf '%s' "$namespace" | grep -Eq '^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$'; then
echo "::error::sccache: namespace '$namespace' is not a path of [A-Za-z0-9._-] segments joined by /" >&2
return 2
fi
# Dots alone are key characters too, so "." and ".." need refusing by name.
case "/$namespace/" in
*/./* | */../*)
echo "::error::sccache: namespace '$namespace' has a dot-only segment" >&2
return 2
;;
esac
}

# Puts the job's objects under <prefix>/<namespace>; the caller exports the
# line and writes it to $GITHUB_ENV before the sccache server starts, so tiers
# or platforms share one backend without sharing objects. A namespace
# separates objects, not writers: every job that reaches the backend can still
# write any key. A local SCCACHE_DIR gains a subdirectory. A backend without
# variables (a multi-level chain, a configuration file) takes no namespace.
sccache_namespace_env() {
local namespace="$1" kind prefix_var
[ -n "$namespace" ] || return 0
sccache_namespace_valid "$namespace" || return 2
read -r kind prefix_var _ <<<"$(sccache_backend)"
[ -n "${kind:-}" ] || return 0
if [ "$prefix_var" = - ]; then
echo "::error::sccache: namespace '$namespace' needs a backend configured through SCCACHE_* variables; this one comes from $(_sccache_kind_source "$kind")" >&2
return 2
fi
local current="${!prefix_var:-}"
printf '%s=%s\n' "$prefix_var" "${current:+${current%/}/}$namespace"
}

# sccache writes by default, except GCS, which sccache itself defaults to
# READ_ONLY. With write "false" the backend's own read/write mode becomes
# READ_ONLY, so the job reads what others stored and stores nothing; with
# "true" nothing is exported but the GCS READ_WRITE that spells the default
# out, and a mode the host set stays in force either way. A backend without
# variables cannot be switched to read-only, so write "false" refuses it.
sccache_rw_env() {
local write="$1" kind rw_var
case "$write" in
true | false) ;;
*)
echo "::error::sccache: write must be \"true\" or \"false\" (got '$write')" >&2
return 2
;;
esac
read -r kind _ rw_var _ <<<"$(sccache_backend)"
[ -n "${kind:-}" ] || return 0
if [ "$rw_var" = - ]; then
if [ "$write" = false ]; then
echo "::error::sccache: write \"false\" needs a backend configured through SCCACHE_* variables; this one comes from $(_sccache_kind_source "$kind")" >&2
return 2
fi
return 0
fi
if [ "$write" = false ]; then
printf '%s=READ_ONLY\n' "$rw_var"
elif [ "$kind" = gcs ] && [ -z "${SCCACHE_GCS_RW_MODE:-}" ]; then
printf '%s=READ_WRITE\n' "$rw_var"
fi
}

_sccache_kind_source() {
case "$1" in
multilevel) echo "a multi-level chain (SCCACHE_MULTILEVEL_CHAIN)" ;;
file) echo "sccache's configuration file $(sccache_config_file)" ;;
esac
}

# The kind sccache_backend names for the cache location a started server
# reports (`sccache --show-stats`): "Local disk: …" is disk, "Multi-level …"
# a chain, and a remote store reports its scheme first ("s3, name: …"),
# where Azure's is azblob.
sccache_location_kind() {
local location="$1" scheme
case "$location" in
"Local disk"*) echo disk ;;
Multi-level*) echo multilevel ;;
*,*)
scheme="${location%%,*}"
case "$scheme" in
azblob) echo azure ;;
*) echo "$scheme" ;;
esac
;;
esac
}

# Any HTTP status counts as an answer; only a refused, reset or timed-out
# connection does not. Reachability is all the action can check without
# knowing the host's health path.
sccache_endpoint_answers() {
local url="$1" code
case "$url" in
http://* | https://*) ;;
*) return 0 ;;
esac
code=$(curl -sS -m 3 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null) || true
[ -n "$code" ] && [ "$code" != 000 ]
}
Loading
Loading