# Shared helpers for the ci/ scripts. Sourced, never executed. # # Nothing in here is specific to one plugin — ci/plugin.conf holds that. # shellcheck shell=bash ci_repo_root() { cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd } # Load ci/plugin.conf and fail loudly on anything it forgot to set. A missing # value would otherwise turn into an empty path and a test that passes by # testing nothing. ci_load_conf() { local root root="$(ci_repo_root)" # shellcheck source=/dev/null . "$root/ci/plugin.conf" # MIN_* included on purpose: a port that copies this file but forgets them # leaves the floors empty, every comparison becomes a no-op, and the hollow # checks these were added to fix come back silently — the same shape of bug. local missing= for var in PLUGIN_SLUG PLUGIN_MAIN TEXT_DOMAIN NEEDS_WOOCOMMERCE MIN_SHIPPED_PHP_FILES MIN_TRANSLATION_CALLS RELEASE_REPO; do [ -n "${!var:-}" ] || missing="$missing $var" done if [ -n "$missing" ]; then echo "ci/plugin.conf is missing:$missing" >&2 return 1 fi if [ ! -f "$root/$PLUGIN_MAIN" ]; then echo "ci/plugin.conf points PLUGIN_MAIN at '$PLUGIN_MAIN', which does not exist." >&2 return 1 fi } # The version WordPress will compare against the update feed. Read from the # plugin header, which is the only copy WordPress ever looks at. ci_plugin_version() { sed -n -E 's/^[[:space:]]*\*?[[:space:]]*Version:[[:space:]]*([^[:space:]]+).*/\1/p' \ "$(ci_repo_root)/$PLUGIN_MAIN" | head -1 } # Read any other plugin header field, e.g. ci_plugin_header 'Requires at least'. ci_plugin_header() { sed -n -E "s/^[[:space:]]*\*?[[:space:]]*$1:[[:space:]]*(.*)/\1/p" \ "$(ci_repo_root)/$PLUGIN_MAIN" | head -1 | sed -E 's/[[:space:]]+$//' } # Run a shell script against a checkout of this repository, inside a container. # # The repository goes in over a tarball and `docker cp`, never a bind mount: a # Gitea Actions job runs inside a container talking to the HOST's docker # daemon, so a host path here lands on an empty directory nobody created and # the check passes by checking nothing. The symptom reads like a typo in a path. # # Two details are load-bearing. The script is copied in as a FILE rather than # interpolated into `sh -c`, so it can contain $, quotes and newlines without # being mangled — which matters the moment it is more than one command. And # --entrypoint sh is not decoration: some images (composer:2, for one) have an # entrypoint that swallows its arguments. # # Taken from the subscriptions port, which needed it for composer. ci_in_container() { # image script-text local image="$1" script="$2" local dir cid code=0 dir=$(mktemp -d -t wpci-XXXXXX) ( cd "$(ci_repo_root)" && git ls-files -z | tar --null -T - -cf "$dir/src.tar" ) printf '%s\n' "$script" > "$dir/run.sh" cid=$(docker create --entrypoint sh -w /src "$image" \ -c 'mkdir -p /src && tar -xf /tmp/src.tar -C /src && exec sh /tmp/run.sh') docker cp "$dir/src.tar" "$cid:/tmp/src.tar" >/dev/null docker cp "$dir/run.sh" "$cid:/tmp/run.sh" >/dev/null docker start -a "$cid" || code=$? docker rm -f "$cid" >/dev/null rm -rf "$dir" return "$code" } # Run one of this repository's PHP scripts. # # Uses the local php when there is one — a laptop, GitHub's hosted runners — # and a container otherwise. The Gitea runner image has no php, and losing a # build to "php: command not found" after everything else passed is a silly way # to spend an afternoon. Measured: it happened on the very first Gitea run. ci_php() { if command -v php >/dev/null 2>&1; then php "$@" return fi ci_in_container "php:8.4-cli" "exec php $(printf '%q ' "$@")" } # Run one of this repository's python scripts, with PyYAML available. # # Same shape and the same reason as ci_php above, and it is not hypothetical: # the Gitea runner image has NO PyYAML (measured — `import yaml` raises # ModuleNotFoundError there), while every laptop and GitHub's hosted runners # have it. So a check that merely skips when the import fails is a check that # is switched off on exactly one forge — and Gitea is the forge where a # workflow that does not parse produces no run and no error to click on. # # alpine + py3-yaml rather than a python image + pip: no network install, one # apk call, the same pinned answer every time. ci_python_yaml() { if python3 -c 'import yaml' 2>/dev/null; then python3 "$@" return fi ci_in_container "alpine:3.20" \ "apk add --no-cache python3 py3-yaml >/dev/null && exec python3 $(printf '%q ' "$@")" } ci_step() { printf '\n\033[1m→ %s\033[0m\n' "$*"; } ci_fail() { printf '\033[31mFAIL\033[0m %s\n' "$*" >&2; } # GitHub and Gitea both render these as annotations on the file and line. ci_annotate() { # level file line message printf '::%s file=%s,line=%s::%s\n' "$1" "$2" "$3" "$4" }