/** * Bash helpers shared by every generated job script: step outputs/env files, hashFiles() and the R2-backed cache. * Forge calls (`__gitfor_api_curl`) run the curl binary resolved at script start under a scrubbed environment with * proxies and `.curlrc` disabled, so nothing a step writes to GITHUB_ENV/GITHUB_PATH can redirect the job token. */ const SOURCE = String.raw`__gitfor_path="$PATH"; __gitfor_home="$HOME" __gitfor_curl=$(command -v curl); __gitfor_tar=$(command -v tar); __gitfor_node=$(command -v node) __gitfor_timeout=$(command -v timeout); __gitfor_env=$(command -v env) __gitfor_docker=$(command -v docker); __gitfor_dockerd=$(command -v dockerd) readonly __gitfor_path __gitfor_home __gitfor_curl __gitfor_tar __gitfor_node __gitfor_timeout __gitfor_env __gitfor_docker __gitfor_dockerd __gitfor_api_curl() { env -i PATH="$__gitfor_path" HOME="$__gitfor_home" "$__gitfor_curl" -q --noproxy '*' "$@" } __gitfor_re_heredoc='^([A-Za-z_][A-Za-z0-9_-]*)<<(.+)$' __gitfor_re_kv='^([A-Za-z_][A-Za-z0-9_-]*)=(.*)$' __gitfor_read_kv() { [ -s "$1" ] || return 0 local __line __key __value __delim __more while IFS= read -r __line || [ -n "$__line" ]; do if [[ $__line =~ $__gitfor_re_heredoc ]]; then __key="#{BASH_REMATCH[1]}"; __delim="#{BASH_REMATCH[2]}"; __value=""; __more=0 while IFS= read -r __line || [ -n "$__line" ]; do [ "$__line" = "$__delim" ] && break if [ $__more = 1 ]; then __value="$__value"$'\n'"$__line"; else __value="$__line"; __more=1; fi done elif [[ $__line =~ $__gitfor_re_kv ]]; then __key="#{BASH_REMATCH[1]}"; __value="#{BASH_REMATCH[2]}" else continue fi "$2" "$__key" "$__value" "#{3:-}" done < "$1" } __gitfor_set_output() { [ -n "$3" ] || return 0; export "GITFOR_STEPOUT_#{3}__#{1//-/_}=$2"; } __gitfor_set_env() { case "$1" in *-*|__gitfor*|BASH_ENV|ENV|BASH_FUNC_*|SHELLOPTS|BASHOPTS|PS4|IFS|LD_*|DYLD_*|NODE_OPTIONS|PYTHONSTARTUP|PYTHONPATH|PYTHONHOME|PERL5OPT|PERL5LIB|PERLLIB|RUBYOPT|RUBYLIB|GIT_*|GITFOR_*|GITHUB_*|ACTIONS_*|RUNNER_*|CI|HOME|PATH|SHELL|PROMPT_COMMAND|CDPATH|GLOBIGNORE|BASHPID|UID|EUID|TMPDIR|TMP|TEMP) echo "Ignoring reserved environment variable $1 written to GITHUB_ENV"; return 0 ;; *_proxy|*_PROXY|*_Proxy|CURL_*|SSL_CERT_*|REQUESTS_CA_BUNDLE|NODE_EXTRA_CA_CERTS|XDG_*|GOFLAGS|GOPROXY|GOTOOLCHAIN|GOPRIVATE|GONOSUMDB|GONOSUMCHECK|GOINSECURE|GOENV|NODE_PATH|NPM_CONFIG_*|npm_config_*|PIP_*|CARGO_*|RUSTC_WRAPPER|RUSTFLAGS|GEM_*|BUNDLE_*|JAVA_TOOL_OPTIONS|_JAVA_OPTIONS|JDK_JAVA_OPTIONS|MAVEN_OPTS|GRADLE_OPTS|DOCKER_HOST|DOCKER_CONFIG|KUBECONFIG) echo "Ignoring environment variable $1 written to GITHUB_ENV: it could redirect network or toolchain behaviour"; return 0 ;; esac export "$1=$2" } __gitfor_reset_files() { : > "$__gitfor_tmp/output"; : > "$__gitfor_tmp/env"; : > "$__gitfor_tmp/path"; } __gitfor_after_step() { local __l __o=success __c=success __gitfor_read_kv "$__gitfor_tmp/env" __gitfor_set_env if [ -s "$__gitfor_tmp/path" ]; then while IFS= read -r __l || [ -n "$__l" ]; do if [ -n "$__l" ]; then PATH="$__l:$PATH"; fi; done < "$__gitfor_tmp/path" export PATH fi if [ -n "$1" ]; then __gitfor_read_kv "$__gitfor_tmp/output" __gitfor_set_output "$1" if [ "$2" -ne 0 ]; then __o=failure; [ "$3" = 1 ] || __c=failure; fi export "GITFOR_STEPOUTCOME_$1=$__o" "GITFOR_STEPCONCLUSION_$1=$__c" fi __gitfor_reset_files } __gitfor_sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi; } __gitfor_hash_files() ( cd "#{GITHUB_WORKSPACE:-.}" || exit 0 shopt -s globstar nullglob dotglob 2>/dev/null __list=$(for __p in "$@"; do for __f in $__p; do if [ -f "$__f" ]; then printf '%s\n' "$__f"; fi; done; done | sort -u) [ -n "$__list" ] || exit 0 printf '%s\n' "$__list" | while IFS= read -r __f; do __gitfor_sha256 "$__f" | cut -d' ' -f1; done | __gitfor_sha256 | cut -d' ' -f1 ) export -f __gitfor_sha256 __gitfor_hash_files __gitfor_urlenc() { printf '%s' "$1" | od -An -v -tx1 | tr -d ' \n' | sed 's/../%&/g'; } __gitfor_header() ( shopt -s nocasematch __v="" while IFS= read -r __l; do __l="#{__l%$'\r'}"; if [[ $__l == "$2: "* ]]; then __v="#{__l#*: }"; fi; done < "$1" printf '%s' "$__v" ) __gitfor_tar_safe() { local __listing __listing=$("$__gitfor_tar" -tzf "$1") || return 1 if printf '%s\n' "$__listing" | grep -Eq '^/|(^|/)\.\.(/|$)'; then return 1; fi return 0 } __gitfor_cache_extract() { local __stage="$__gitfor_tmp/cache-stage" __part __root __ok=0 __outer __outer=$("$__gitfor_tar" -tzf "$1" 2>/dev/null) || return 1 if printf '%s\n' "$__outer" | grep -Evq '^(w|h)\.tgz$'; then echo "Cache archive has an unexpected layout"; return 1; fi rm -rf "$__stage" && mkdir -p "$__stage" || return 1 "$__gitfor_tar" -xzf "$1" -C "$__stage" --no-same-owner || { rm -rf "$__stage"; return 1; } for __part in w h; do [ -f "$__stage/$__part.tgz" ] || continue if [ $__part = w ]; then __root="$GITHUB_WORKSPACE"; else __root="$__gitfor_home"; fi if ! __gitfor_tar_safe "$__stage/$__part.tgz"; then echo "Cache archive contains unsafe entries"; __ok=1; break; fi "$__gitfor_tar" -xzf "$__stage/$__part.tgz" -C "$__root" --no-same-owner || { __ok=1; break; } done rm -rf "$__stage" return $__ok } __gitfor_action_fetch() { local __key="$1" __sum="$2" __dir="$3" __label="$4" __file="$__gitfor_tmp/action.tgz" __got [ -f "$__dir/.gitfor-verified" ] && return 0 echo "Download action repository '$__label'" rm -rf "$__dir" "$__file" if ! __gitfor_api_curl -fsS --retry 3 -H "Authorization: Bearer $GITFOR_JOB_TOKEN" -o "$__file" \ "$GITFOR_API_URL/api/actions/archive?key=$(__gitfor_urlenc "$__key")"; then echo "Failed to download action $__label" return 1 fi __got=$(PATH="$__gitfor_path" __gitfor_sha256 "$__file" | cut -d' ' -f1) if [ "$__got" != "$__sum" ]; then echo "Action $__label failed verification: sha256 $__got, expected $__sum" rm -f "$__file" return 1 fi if ! __gitfor_tar_safe "$__file"; then echo "Action $__label contains unsafe paths" rm -f "$__file" return 1 fi mkdir -p "$__dir" && "$__gitfor_tar" -xzf "$__file" -C "$__dir" --no-same-owner --no-same-permissions || { rm -f "$__file"; return 1; } rm -f "$__file" touch "$__dir/.gitfor-verified" } __gitfor_export_state() { export "STATE_$1=$2" 2>/dev/null || true; } # Runs one phase (pre, main or post) of a JavaScript action with the runner's own Node, env and timeout # (resolved before any step ran), so a step that changes PATH can't change what later actions run under. State written to GITHUB_STATE is handed # to the action's later phases as STATE_. __gitfor_action_run() { local __idx="$1" __dir="$2" __entry="$3" __timeout="$4" __state="$__gitfor_tmp/state/$1" __code shift 4 if [ -z "$__gitfor_node" ]; then echo "Node.js is required to run JavaScript actions"; return 1; fi mkdir -p "$__gitfor_tmp/state" && : > "$__gitfor_tmp/state-new" ( __gitfor_read_kv "$__state" __gitfor_export_state export GITHUB_STATE="$__gitfor_tmp/state-new" GITHUB_ACTION_PATH="$__dir" \ GITHUB_SERVER_URL=https://github.com GITHUB_API_URL=https://api.github.com GITHUB_GRAPHQL_URL=https://api.github.com/graphql if [ "$__timeout" != 0 ]; then exec "$__gitfor_env" "$@" "$__gitfor_timeout" --kill-after=15 "$__timeout" "$__gitfor_node" "$__dir/$__entry" fi exec "$__gitfor_env" "$@" "$__gitfor_node" "$__dir/$__entry" ) __code=$? cat "$__gitfor_tmp/state-new" >> "$__state" 2>/dev/null return $__code } # Docker actions. dockerd starts on first use with the flags Cloudflare Containers require (no iptables, no IP # forwarding); action containers share the job's network (--network host), so gitfor.internal still works. __gitfor_docker_ready() { local __i if [ -z "$__gitfor_docker" ]; then echo "This step runs a Docker action, but this runner has no Docker. Cloud runners add it automatically; on a self-hosted runner, use a job image with Docker." return 1 fi "$__gitfor_docker" info >/dev/null 2>&1 && return 0 if [ -n "$__gitfor_dockerd" ] && [ "$(id -u)" = 0 ]; then echo "Starting Docker" ( "$__gitfor_dockerd" --iptables=false --ip6tables=false --ip-forward=false > "$__gitfor_tmp/dockerd.log" 2>&1 & ) for __i in $(seq 1 60); do "$__gitfor_docker" info >/dev/null 2>&1 && return 0; sleep 1; done echo "Docker did not start:"; tail -n 20 "$__gitfor_tmp/dockerd.log" return 1 fi echo "Docker is installed but its daemon is not reachable" return 1 } # Loads a registry image through the forge's image cache (pinned digest), or builds an action's Dockerfile. __gitfor_docker_image() { local - local __kind="$1" __value="$2" __dir="$3" __file="$4" __label="$5" set -o pipefail if [ "$__kind" = pull ]; then __gitfor_docker_tag="gitfor-actions/image:#{__value#sha256:}" "$__gitfor_docker" image inspect "$__gitfor_docker_tag" >/dev/null 2>&1 && return 0 echo "Pull image $__label ($__value)" __gitfor_api_curl -fsS --retry 3 -H "Authorization: Bearer $GITFOR_JOB_TOKEN" \ "$GITFOR_API_URL/api/actions/image?digest=$(__gitfor_urlenc "$__value")" | "$__gitfor_docker" load -q >/dev/null || { echo "Failed to pull image $__label"; return 1; } "$__gitfor_docker" image inspect "$__gitfor_docker_tag" >/dev/null 2>&1 || { echo "Image $__label did not load"; return 1; } return 0 fi __gitfor_docker_tag="gitfor-actions/build:$__value" "$__gitfor_docker" image inspect "$__gitfor_docker_tag" >/dev/null 2>&1 && return 0 echo "Build container for action $__label" "$__gitfor_docker" build -q -t "$__gitfor_docker_tag" -f "$__dir/$__file" "$__dir" >/dev/null || { echo "Failed to build $__label"; return 1; } } __gitfor_docker_run() { local __idx="$1" __kind="$2" __value="$3" __dir="$4" __file="$5" __label="$6" __timeout="$7" __entry="$8" __argc="$9" local __state="$__gitfor_tmp/state/$1" __name __code __pair __v shift 9 local __args=("#{@:1:$__argc}") shift "$__argc" __gitfor_docker_ready || return 1 __gitfor_docker_image "$__kind" "$__value" "$__dir" "$__file" "$__label" || return 1 mkdir -p "$__gitfor_tmp/state" "$RUNNER_TEMP/_github_home" && : > "$__gitfor_tmp/state-new" __name="gitfor-step-$__idx-$RANDOM$RANDOM" ( __gitfor_read_kv "$__state" __gitfor_export_state local __names=() for __pair in "$@"; do __names+=(-e "#{__pair%%=*}"); done for __v in $(compgen -e); do case "$__v" in GITHUB_WORKSPACE|GITHUB_OUTPUT|GITHUB_ENV|GITHUB_PATH|GITHUB_STATE|GITHUB_STEP_SUMMARY|GITHUB_EVENT_PATH|RUNNER_TEMP|RUNNER_TOOL_CACHE) ;; STATE_*|GITHUB_*|RUNNER_*) __names+=(-e "$__v") ;; esac done local __run=("$__gitfor_docker" run --rm --name "$__name" --network host -v "$GITHUB_WORKSPACE:/github/workspace" -w /github/workspace -v "$__gitfor_tmp:/github/gitfor" -v "$RUNNER_TEMP:/github/runner_temp" -v "$RUNNER_TEMP/_github_home:/github/home" "#{__names[@]}" -e CI=true -e GITHUB_ACTIONS=true -e HOME=/github/home -e GITHUB_WORKSPACE=/github/workspace -e RUNNER_TEMP=/github/runner_temp -e GITHUB_OUTPUT=/github/gitfor/output -e GITHUB_ENV=/github/gitfor/env -e GITHUB_PATH=/github/gitfor/path -e GITHUB_STATE=/github/gitfor/state-new -e GITHUB_STEP_SUMMARY=/github/gitfor/step-summary -e GITHUB_EVENT_PATH=/github/gitfor/event.json -e GITHUB_SERVER_URL=https://github.com -e GITHUB_API_URL=https://api.github.com) if [ -n "$__entry" ]; then __run+=(--entrypoint "$__entry"); fi __run+=("$__gitfor_docker_tag" "#{__args[@]}") if [ "$__timeout" != 0 ]; then exec "$__gitfor_env" "$@" "$__gitfor_timeout" --kill-after=15 "$__timeout" "#{__run[@]}" fi exec "$__gitfor_env" "$@" "#{__run[@]}" ) __code=$? "$__gitfor_docker" rm -f "$__name" >/dev/null 2>&1 cat "$__gitfor_tmp/state-new" >> "$__state" 2>/dev/null return $__code } __gitfor_cache_restore() { local __id="$1" __key="$2" __version="$3" __query __r __hit __matched __headers="$__gitfor_tmp/cache-headers" shift 3 __query="key=$(__gitfor_urlenc "$__key")&version=$__version" for __r in "$@"; do [ -n "$__r" ] && __query="$__query&restore=$(__gitfor_urlenc "$__r")"; done rm -f "$__gitfor_tmp/cache.tgz" "$__headers" if ! __gitfor_api_curl -fsS -H "Authorization: Bearer $GITFOR_JOB_TOKEN" -D "$__headers" -o "$__gitfor_tmp/cache.tgz" \ "$GITFOR_API_URL/api/actions/cache?$__query" 2>/dev/null; then echo "Cache not found for key: $__key" __gitfor_set_output cache-hit false "$__id" return 1 fi __hit=$(__gitfor_header "$__headers" x-gitfor-cache-hit) __matched=$(__gitfor_header "$__headers" x-gitfor-cache-key) __matched=$(printf '%b' "#{__matched//%/\\x}") if ! __gitfor_cache_extract "$__gitfor_tmp/cache.tgz"; then rm -f "$__gitfor_tmp/cache.tgz" echo "Failed to extract the cache archive" __gitfor_set_output cache-hit false "$__id" return 1 fi rm -f "$__gitfor_tmp/cache.tgz" echo "Cache restored from key: $__matched" __gitfor_set_output cache-matched-key "$__matched" "$__id" if [ "$__hit" = exact ]; then __gitfor_set_output cache-hit true "$__id"; return 0; fi __gitfor_set_output cache-hit false "$__id" return 1 } __gitfor_cache_save() { local __key="$1" __version="$2" __archive="$__gitfor_tmp/cache-save.tgz" __stage="$__gitfor_tmp/cache-save" __p __code __sum local __work=() __home=() __parts=() shift 2 for __p in "$@"; do case "$__p" in "~") __home+=(.) ;; "~/"*) __home+=("#{__p#"~/"}") ;; /*) echo "Cache not saved: absolute path $__p is not supported (use a workspace or ~ path)"; return 0 ;; *) __work+=("$__p") ;; esac done for __p in "#{__work[@]}" "#{__home[@]}"; do case "/$__p/" in */../*) echo "Cache not saved: path $__p must not contain '..'"; return 0 ;; esac done rm -rf "$__stage" && mkdir -p "$__stage" || return 0 if [ #{#__work[@]} -gt 0 ]; then "$__gitfor_tar" -czf "$__stage/w.tgz" -C "$GITHUB_WORKSPACE" -- "#{__work[@]}" 2>/dev/null || { rm -rf "$__stage"; echo "Cache not saved: some cached paths do not exist"; return 0; } __parts+=(w.tgz) fi if [ #{#__home[@]} -gt 0 ]; then "$__gitfor_tar" -czf "$__stage/h.tgz" -C "$__gitfor_home" -- "#{__home[@]}" 2>/dev/null || { rm -rf "$__stage"; echo "Cache not saved: some cached paths do not exist"; return 0; } __parts+=(h.tgz) fi "$__gitfor_tar" -czf "$__archive" -C "$__stage" -- "#{__parts[@]}" || { rm -rf "$__stage"; return 0; } rm -rf "$__stage" __sum=$(PATH="$__gitfor_path" __gitfor_sha256 "$__archive" 2>/dev/null) || { rm -f "$__archive"; echo "Cache not saved: sha256sum is not available"; return 0; } __code=$(__gitfor_api_curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $GITFOR_JOB_TOKEN" \ -H 'content-type: application/gzip' -H "x-gitfor-content-sha256: #{__sum%% *}" -T "$__archive" \ "$GITFOR_API_URL/api/actions/cache?key=$(__gitfor_urlenc "$__key")&version=$__version") || __code=000 rm -f "$__archive" case "$__code" in 2*) echo "Cache saved with key: $__key" ;; 409) echo "Cache entry for key $__key already exists; not saving" ;; 413) echo "Cache not saved: the archive exceeds the cache size limit" ;; *) echo "Cache not saved (HTTP $__code)" ;; esac }`; /** Written with `#{` for bash `${` so the template literal does not interpolate. */ export const RUNTIME_FUNCTIONS = SOURCE.replaceAll("#{", "${");