#!/usr/bin/env bash # Check a version of the plugin before it is released on GitHub or # submitted to WordPress.org. Changes nothing: it builds both zips into a # temporary folder (scripts/build-release.sh) and reads them and the Git ref. # # Errors stop a release. Warnings need a look and a decision; most matter only # for WordPress.org. Notes are for information. # # Usage: scripts/preflight-release.sh [--ref REF] [--offline] [--strict] [--no-docker] # --ref REF Commit, branch or tag to check (default: HEAD). # --offline Skip checks that ask WordPress.org (latest WordPress version, # slug, contributor profiles). # --strict Fail on warnings too (use before a WordPress.org submission). # --no-docker Lint PHP with the local php instead of PHP 7.4 in Docker. # # Exit status: 0 when there are no errors (and, with --strict, no warnings). # Plugin Check runs separately: scripts/plugin-check.sh. set -euo pipefail readonly UPDATER_HEADERS='GitHub Plugin URI|Primary Branch|Release Asset' # Development files that must never be in a release zip (paths inside the slug folder). readonly DEV_FILES='^[^/]+/(\.git|\.agents|\.wordpress-org|\.distignore|\.distignore-wporg|\.gitattributes|\.gitignore|\.woodpecker\.yml|\.github|\.editorconfig|\.gitleaks\.toml|\.aidevops\.json|composer\.(json|lock)|phpcs\.xml(\.dist)?|phpstan(-baseline|-plugin)?\.neon(\.dist)?|vendor|AGENTS\.md|CONTRIBUTING\.md|DEVELOPMENT\.md|LAUNCH\.md|SECURITY\.md|STANDARDS\.md|RELEASING\.md|ROADMAP\.md|STABILITY\.md|TESTING\.md|scripts|dist|node_modules|reference-plugins|project-documents)(/|$)|(^|/)(\.DS_Store|__MACOSX|Thumbs\.db)(/|$)|\.(bak|log|orig|swp)$' readonly README_MAX_BYTES=10240 readonly SHORT_DESC_MAX=150 readonly MAX_TAGS=5 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR # shellcheck source=scripts/lib/plugin.sh disable=SC1091 # followed only with -x . "$SCRIPT_DIR/lib/plugin.sh" # Set from the main file at the ref (plugin_identity). SLUG="" MAIN_FILE="" VERSION_CONSTANT="" # Paths only in the GitHub build (.distignore-wporg): the GitHub updater. UPDATER_FILES="" ERRORS=0 WARNINGS=0 TMP_DIR="" OFFLINE=0 USE_DOCKER=1 CHANGELOG_TXT="" die() { local message="$1" printf 'preflight: %s\n' "$message" >&2 exit 2 } usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' return 0 } cleanup() { if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then rm -rf "$TMP_DIR" fi return 0 } ok() { local message="$1" printf ' ok %s\n' "$message" return 0 } err() { local message="$1" printf ' ERROR %s\n' "$message" ERRORS=$((ERRORS + 1)) return 0 } warn() { local message="$1" printf ' warn %s\n' "$message" WARNINGS=$((WARNINGS + 1)) return 0 } note() { local message="$1" printf ' note %s\n' "$message" return 0 } section() { local title="$1" printf '\n%s\n' "$title" return 0 } # Value of "Key: value" in a header (plugin file comment or readme), case-insensitive. field() { local text="$1" local key="$2" printf '%s\n' "$text" | awk -v k="$key" ' BEGIN { k = tolower(k) } { line = $0 sub(/^[ \t*]*/, "", line) i = index(line, ":") if (i > 0 && tolower(substr(line, 1, i - 1)) == k) { v = substr(line, i + 1) gsub(/^[ \t]+|[ \t\r]+$/, "", v) print v exit } }' return 0 } # 1 if version a < b (numeric parts), else 0. version_lt() { local a="$1" local b="$2" awk -v a="$a" -v b="$b" 'BEGIN { na = split(a, x, "."); nb = split(b, y, "."); n = (na > nb) ? na : nb for (i = 1; i <= n; i++) { if ((x[i] + 0) < (y[i] + 0)) { print 1; exit } if ((x[i] + 0) > (y[i] + 0)) { print 0; exit } } print 0 }' return 0 } # HTTP status of a URL (000 when offline or unreachable). http_status() { local url="$1" local code # curl prints 000 itself when it cannot connect, and exits non-zero. code="$(curl -sL -o /dev/null -m 20 -w '%{http_code}' "$url" || true)" printf '%s' "${code:-000}" return 0 } check_versions() { local plugin_header="$1" local readme="$2" local main_php="$3" section "Versions and headers" local version constant stable version="$(field "$plugin_header" "Version")" constant="$(printf '%s\n' "$main_php" | sed -nE "/define\([[:space:]]*['\"]${VERSION_CONSTANT}['\"]/{s/.*,[[:space:]]*['\"]([^'\"]+)['\"].*/\1/p;q;}")" stable="$(field "$readme" "Stable tag")" if printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then ok "Version: $version (numbers only, so GitHub updates and WordPress.org treat it as stable)" else err "Version: '$version' is not X.Y.Z; pre-release versions on main are offered to sites still using Git Updater" fi if [ "$constant" = "$version" ]; then ok "version constant matches ($constant)"; else err "version constant is '$constant', Version: is '$version'"; fi if [ "$stable" = "$version" ]; then ok "readme.txt Stable tag matches"; else err "readme.txt Stable tag is '$stable', Version: is '$version'"; fi if [ "$(printf '%s' "$stable" | tr '[:upper:]' '[:lower:]')" = "trunk" ]; then err "Stable tag: trunk is not allowed for new plugins"; fi local key header_value readme_value for key in "Requires at least" "Requires PHP"; do header_value="$(field "$plugin_header" "$key")" readme_value="$(field "$readme" "$key")" if [ -z "$header_value" ]; then err "$MAIN_FILE has no '$key' header (WordPress reads it from the plugin file)" elif [ -n "$readme_value" ] && [ "$header_value" != "$readme_value" ]; then err "$key: $header_value in $MAIN_FILE, $readme_value in readme.txt" else ok "$key: $header_value" fi done if printf '%s\n' "$main_php" | grep -Eiq '^[[:space:]*]*Update URI:'; then err "Update URI header found: WordPress.org rejects it and Plugin Check reports an updater" else ok "no Update URI header" fi local domain license plugin_uri author_uri domain="$(field "$plugin_header" "Text Domain")" if [ "$domain" = "$SLUG" ]; then ok "Text Domain: $domain"; else err "Text Domain is '$domain'; it must be the slug ($SLUG) for language packs"; fi license="$(field "$plugin_header" "License")" if printf '%s' "$license" | grep -Eiq 'GPL'; then ok "License: $license"; else err "License '$license' is not GPL-compatible as written"; fi plugin_uri="$(field "$plugin_header" "Plugin URI")" author_uri="$(field "$plugin_header" "Author URI")" if [ -n "$plugin_uri" ] && [ "$plugin_uri" = "$author_uri" ]; then warn "Plugin URI and Author URI are the same ($plugin_uri); WordPress.org asks for a Plugin URI unique to the plugin, or none" fi VERSION="$version" return 0 } # Changelog text (a readme section or changelog.txt): an entry for the version, # and no Unreleased section left. check_changelog() { local label="$1" local text="$2" local version="$3" if printf '%s\n' "$text" | grep -Eq "^= *v?$version *="; then ok "$label changelog has $version" else warn "$label changelog has no '= $version =' entry" fi if printf '%s\n' "$text" | grep -Eiq '^= *unreleased *='; then warn "$label changelog still has an Unreleased section; name it $version when releasing" fi return 0 } check_readme() { local readme="$1" local plugin_header="$2" local version="$3" section "readme.txt" local bytes bytes="$(printf '%s' "$readme" | wc -c | tr -d ' ')" if [ "$bytes" -le "$README_MAX_BYTES" ]; then ok "size ${bytes} bytes ($((README_MAX_BYTES - bytes)) left)" else warn "size ${bytes} bytes; WordPress.org says over 10 KB may cause errors (keep the current changelog, move older entries to changelog.txt, trim the description)" fi local name readme_name name="$(field "$plugin_header" "Plugin Name")" readme_name="$(printf '%s\n' "$readme" | sed -n '1s/^===[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*===.*/\1/p')" if [ "$readme_name" = "$name" ]; then ok "name matches the plugin header ($name)"; else warn "readme name '$readme_name' differs from Plugin Name '$name'"; fi local short short="$(printf '%s\n' "$readme" | awk 'NR == 1 { next } !h && /^[ \t]*$/ { h = 1; next } h && /^==/ { exit } h && !/^[ \t]*$/ { print; exit }')" if [ -z "$short" ]; then err "no short description (the line after the header)" elif [ "${#short}" -le "$SHORT_DESC_MAX" ]; then ok "short description ${#short} characters" else warn "short description ${#short} characters; WordPress.org cuts it at $SHORT_DESC_MAX" fi local tags tag_count tags="$(field "$readme" "Tags")" tag_count="$(printf '%s\n' "$tags" | awk -F',' '{ print ($0 == "") ? 0 : NF }')" if [ "$tag_count" -ge 1 ] && [ "$tag_count" -le "$MAX_TAGS" ]; then ok "$tag_count tags"; else warn "$tag_count tags; WordPress.org shows at most $MAX_TAGS"; fi local tested tested="$(field "$readme" "Tested up to")" if printf '%s' "$tested" | grep -Eq '^[0-9]+\.[0-9]+$'; then ok "Tested up to: $tested" else err "Tested up to '$tested' should be a major version such as 6.8" fi if [ "$OFFLINE" -eq 0 ]; then local latest latest_major # The first offer is the latest release; later ones are older branches. latest="$(curl -s -m 20 'https://api.wordpress.org/core/version-check/1.7/' | grep -o '"current":"[0-9.]*"' | head -n 1 | cut -d'"' -f4 || true)" latest_major="$(printf '%s' "$latest" | cut -d. -f1-2)" if [ -z "$latest_major" ]; then note "could not read the latest WordPress version" elif [ "$(version_lt "$tested" "$latest_major")" = "1" ]; then warn "Tested up to $tested, latest WordPress is $latest; test and raise it" else ok "Tested up to is the latest WordPress ($latest)" fi fi check_changelog "readme.txt" "$(printf '%s\n' "$readme" | awk '/^== *[Cc]hangelog *==/ { c = 1; next } c && /^== / { exit } c')" "$version" if [ -n "$CHANGELOG_TXT" ]; then check_changelog "changelog.txt" "$CHANGELOG_TXT" "$version" fi if printf '%s\n' "$readme" | grep -Eiq '^== *upgrade notice *=='; then if printf '%s\n' "$readme" | awk '/^== *[Uu]pgrade [Nn]otice *==/ { u = 1; next } u && /^== / { exit } u' | grep -Eq "^= *v?$version *="; then ok "upgrade notice has $version" else note "no upgrade notice for $version (optional)" fi fi return 0 } check_wporg() { local readme="$1" local plugin_header="$2" section "WordPress.org listing" local name derived name="$(field "$plugin_header" "Plugin Name")" derived="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')" if [ "$derived" = "$SLUG" ]; then ok "the slug WordPress.org makes from '$name' is $SLUG" else warn "WordPress.org makes the slug from Plugin Name: '$name' becomes '$derived', not '$SLUG'. Ask for '$SLUG' in the submission notes (it can change only before approval), or the text domain will not match" fi if printf '%s' "$name" | grep -Eiq '^(wordpress|wp|woocommerce|woo|gutenberg)([^a-z]|$)'; then warn "Plugin Name starts with a trademark ('$name'); WordPress.org does not allow that" fi if [ "$OFFLINE" -eq 1 ]; then note "offline: slug and contributor profiles not checked" return 0 fi local info info="$(curl -s -m 20 "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Bslug%5D=$SLUG&request%5Bfields%5D%5Bsections%5D=0" || true)" if printf '%s' "$info" | grep -q '"error"'; then note "slug $SLUG is not listed on WordPress.org yet" elif [ -n "$info" ]; then note "slug $SLUG is already listed on WordPress.org; check it is this plugin" fi local contributors user status contributors="$(field "$readme" "Contributors")" if [ -z "$contributors" ]; then err "readme.txt has no Contributors" fi for user in $(printf '%s' "$contributors" | tr ',' ' '); do status="$(http_status "https://profiles.wordpress.org/$user/")" case "$status" in 200) ok "contributor $user has a WordPress.org profile" ;; 000) note "could not check contributor $user" ;; *) warn "contributor '$user' has no WordPress.org profile (HTTP $status); Contributors must be WordPress.org usernames, case-sensitive" ;; esac done return 0 } # Lint PHP (PHP 7.4 in Docker when possible) and JS in an unpacked build. check_syntax() { local dir="$1" local label="$2" local out php_label local expected expected="$(find "$dir" -name '*.php' | wc -l | tr -d ' ')" if [ "$USE_DOCKER" -eq 1 ] && command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then local image="php:7.4-cli" if docker image inspect wordpress:php7.4-apache >/dev/null 2>&1; then image="wordpress:php7.4-apache"; fi out="$(docker run --rm -v "$dir:/src:ro" "$image" sh -c 'find /src -name "*.php" -exec php -l {} \;' 2>&1 || true)" php_label="PHP 7.4 syntax ($image)" elif command -v php >/dev/null 2>&1; then out="$(find "$dir" -name '*.php' -exec php -l {} \; 2>&1 || true)" php_label="PHP syntax ($(php -r 'echo PHP_VERSION;'); not 7.4)" else out="" php_label="" warn "$label: no PHP to lint with" fi if [ -n "$php_label" ]; then # Count the files php -l passed, so a lint that never ran cannot pass. local passed problems passed="$(printf '%s\n' "$out" | grep -c '^No syntax errors' || true)" problems="$(printf '%s\n' "$out" | grep -v '^No syntax errors' | grep -v '^[[:space:]]*$' || true)" if [ -z "$problems" ] && [ "$passed" -eq "$expected" ] && [ "$expected" -gt 0 ]; then ok "$label: $php_label, $passed files" elif [ -z "$problems" ]; then err "$label: PHP lint checked $passed of $expected files" else err "$label: $php_label errors:" printf '%s\n' "$problems" | sed 's/^/ /' fi fi if command -v node >/dev/null 2>&1; then out="$(find "$dir" -name '*.js' -exec node --check {} \; 2>&1 || true)" if [ -z "$out" ]; then ok "$label: JS syntax"; else err "$label: JS syntax errors:"; printf '%s\n' "$out" | sed 's/^/ /'; fi else warn "$label: node not found, JS not checked" fi return 0 } # Shared checks for a release zip; prints its unpacked folder in UNPACKED. check_zip() { local zip_path="$1" local label="$2" local dir="$TMP_DIR/unpacked-$label" local list tops dev size list="$(unzip -Z1 "$zip_path")" tops="$(printf '%s\n' "$list" | cut -d/ -f1 | sort -u | tr '\n' ' ')" if [ "$tops" = "$SLUG " ]; then ok "$label: one $SLUG/ folder"; else err "$label: top level is '$tops', must be only $SLUG/"; fi if printf '%s\n' "$list" | grep -qx "$SLUG/$MAIN_FILE"; then ok "$label: $SLUG/$MAIN_FILE present"; else err "$label: $SLUG/$MAIN_FILE missing"; fi dev="$(printf '%s\n' "$list" | grep -E "$DEV_FILES" || true)" if [ -z "$dev" ]; then ok "$label: no development files"; else err "$label: development files: $(printf '%s' "$dev" | tr '\n' ' ')"; fi size="$(wc -c <"$zip_path" | tr -d ' ')" note "$label: $(basename "$zip_path"), $((size / 1024)) KB, $(printf '%s\n' "$list" | grep -cv '/$') files" mkdir -p "$dir" unzip -q "$zip_path" -d "$dir" check_syntax "$dir" "$label" UNPACKED="$dir/$SLUG" return 0 } check_builds() { local github_zip="$1" local wporg_zip="$2" local version="$3" section "Release zips" if [ "$(basename "$github_zip")" = "$SLUG-$version.zip" ]; then ok "GitHub asset name $SLUG-$version.zip (sites pick assets starting with $SLUG)" else err "GitHub asset is $(basename "$github_zip"), expected $SLUG-$version.zip" fi case "$(basename "$wporg_zip")" in "$SLUG"*) err "WordPress.org zip name starts with $SLUG; sites could install it from GitHub" ;; *) ok "WordPress.org zip name does not start with $SLUG" ;; esac UNPACKED="" check_zip "$github_zip" "github" local github_dir="$UNPACKED" path if [ -z "$UPDATER_FILES" ]; then note "no .distignore-wporg: both builds hold the same files" fi for path in $UPDATER_FILES; do if [ -e "$github_dir/$path" ]; then ok "github: has $path"; else err "github: $path missing (listed in .distignore-wporg)"; fi done if grep -Eq "^[[:space:]*]*GitHub Plugin URI:" "$github_dir/$MAIN_FILE"; then ok "github: has the GitHub Plugin URI header"; else err "github: no GitHub Plugin URI header, sites cannot update it from GitHub"; fi check_zip "$wporg_zip" "wporg" local wporg_dir="$UNPACKED" for path in $UPDATER_FILES; do if [ -e "$wporg_dir/$path" ]; then err "wporg: $path must not be in the WordPress.org build"; else ok "wporg: no $path"; fi done if grep -Eq "^[[:space:]*]*($UPDATER_HEADERS):" "$wporg_dir/$MAIN_FILE"; then err "wporg: GitHub updater header lines still in $MAIN_FILE"; else ok "wporg: no GitHub updater header lines"; fi local hits hits="$(grep -rEl --include='*.php' --include='*.js' 'gu_override_dot_org|api\.github\.com/repos|Plugin_Upgrader|Theme_Upgrader|site_transient_update_plugins|auto_update_(plugin|theme)' "$wporg_dir" 2>/dev/null | sed "s|^$wporg_dir/||" || true)" if [ -z "$hits" ]; then ok "wporg: no code that installs or updates plugins from elsewhere" else warn "wporg: check these install or update code (guideline 8 allows only WordPress.org sources): $(printf '%s' "$hits" | tr '\n' ' ')" fi section "Remote assets and services (WordPress.org build)" hits="$(grep -rEn "(wp_(enqueue|register)_(script|style)|/dev/null | sed "s|^$wporg_dir/||" | cut -c1-160 || true)" if [ -z "$hits" ]; then ok "no scripts or styles loaded from other sites"; else warn "scripts or styles from other sites (guideline 8: ship them in the plugin unless they are part of a service):"; printf '%s\n' "$hits" | sed 's/^/ /'; fi local readme_text hosts host missing="" readme_text="$(cat "$wporg_dir/readme.txt")" hosts="$(grep -rEoh 'https?://[A-Za-z0-9.-]+\.[a-z]{2,}' "$wporg_dir/includes" "$wporg_dir/blocks" 2>/dev/null | sed -E 's|https?://||; s|^www\.||' | sort -u || true)" for host in $hosts; do case "$host" in w3.org | gnu.org | wordpress.org | *.wordpress.org | w.org | *.w.org | wp.org | *.wp.org | example.com | *.example.com) continue ;; esac if ! printf '%s' "$readme_text" | grep -Fqi "$host"; then missing="$missing $host"; fi done if [ -z "$missing" ]; then ok "every host in includes/ and blocks/ is named in readme.txt" else note "hosts in code not named in readme.txt (fine if they are only links; services the plugin contacts need an External services entry):$missing" fi # Checks for parts only some plugins have; each runs when its files exist. # A plugin's own checks: scripts/preflight-plugin.sh defines plugin_preflight, # which gets the unpacked GitHub build and can use section, ok, warn and err. if [ -f "$SCRIPT_DIR/preflight-plugin.sh" ]; then # shellcheck source=/dev/null . "$SCRIPT_DIR/preflight-plugin.sh" plugin_preflight "$github_dir" fi if [ -f "$SCRIPT_DIR/replaced-plugins.php" ]; then check_replaced_count "$github_dir" fi return 0 } # README.md's " replaces **N plugins**" line matches the # 'replaces' entries in the code (scripts/replaced-plugins.php). check_replaced_count() { local dir="$1" section "Replaced plugins" if ! command -v php >/dev/null 2>&1; then warn "php not found, the replaced plugins count in README.md was not checked" return 0 fi local out if out="$(php "$SCRIPT_DIR/replaced-plugins.php" --check "$dir" 2>&1)"; then ok "$out" else err "$out" fi return 0 } # Core files match the starter's (scripts/sync-core.sh --check), when a # checkout of the starter is at hand. A warning, not an error: the starter's # checkout may be behind, and only a person can tell which side is right. check_core_files() { section "Core files" local out status=0 out="$(bash "$SCRIPT_DIR/sync-core.sh" --check 2>&1)" || status=$? case "$status" in 0) ok "$(printf '%s\n' "$out" | tail -n 1)" ;; 1) warn "$(printf '%s\n' "$out" | sed -n '2,$p' | tr '\n' ' ')" ;; *) note "not compared: ${out#sync-core: }" ;; esac return 0 } check_git() { local ref="$1" local sha="$2" local version="$3" section "Git" local tag_sha if tag_sha="$(git rev-parse --verify --quiet "refs/tags/v$version^{commit}")"; then if [ "$tag_sha" = "$sha" ]; then ok "tag v$version is this commit"; else warn "tag v$version exists on another commit (${tag_sha:0:12}); bump the version"; fi else note "no tag v$version yet" fi local main_sha if main_sha="$(git rev-parse --verify --quiet "refs/remotes/origin/main^{commit}")"; then if git merge-base --is-ancestor "$sha" "$main_sha"; then ok "$ref is on origin/main"; else note "$ref is not on origin/main yet; release only from main"; fi fi if [ "$ref" = "HEAD" ] && [ -n "$(git status --porcelain --untracked-files=no)" ]; then note "uncommitted changes are not checked (the build comes from HEAD)" fi return 0 } main() { local ref="HEAD" local strict=0 local arg while [ $# -gt 0 ]; do arg="$1" case "$arg" in --ref) [ $# -ge 2 ] || die "--ref needs a value" ref="$2" shift ;; --offline) OFFLINE=1 ;; --strict) strict=1 ;; --no-docker) USE_DOCKER=0 ;; -h | --help) usage return 0 ;; *) die "unknown argument: $arg" ;; esac shift done local root sha root="$(git rev-parse --show-toplevel)" || die "run this inside a checkout of the plugin" cd "$root" sha="$(git rev-parse --verify --quiet "$ref^{commit}")" || die "not a commit: $ref" plugin_identity "$sha" || die "cannot tell which plugin this is at $ref" SLUG="$PLUGIN_SLUG" MAIN_FILE="$PLUGIN_MAIN_FILE" VERSION_CONSTANT="${PLUGIN_CONST}_VERSION" UPDATER_FILES="$(plugin_wporg_only "$sha")" trap cleanup EXIT TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/$SLUG-preflight.XXXXXX")" local main_php readme plugin_header main_php="$(git show "$sha:$MAIN_FILE")" readme="$(git show "$sha:readme.txt")" || die "readme.txt missing at $ref" if git cat-file -e "$sha:changelog.txt" 2>/dev/null; then CHANGELOG_TXT="$(git show "$sha:changelog.txt")" fi plugin_header="$(printf '%s\n' "$main_php" | sed -n '1,/\*\//p')" printf '%s preflight: %s (%s)\n' "$PLUGIN_NAME" "$ref" "${sha:0:12}" VERSION="" check_versions "$plugin_header" "$readme" "$main_php" check_readme "$readme" "$plugin_header" "$VERSION" check_wporg "$readme" "$plugin_header" local zips github_zip wporg_zip zips="$("$root/scripts/build-release.sh" --ref "$sha" --out "$TMP_DIR/dist" --quiet)" || die "build failed" github_zip="$(printf '%s\n' "$zips" | sed -n 1p)" wporg_zip="$(printf '%s\n' "$zips" | sed -n 2p)" check_builds "$github_zip" "$wporg_zip" "$VERSION" if [ -f "$SCRIPT_DIR/sync-core.sh" ]; then check_core_files fi check_git "$ref" "$sha" "$VERSION" printf '\n%s error(s), %s warning(s).\n' "$ERRORS" "$WARNINGS" if [ "$ERRORS" -gt 0 ]; then printf 'Not ready: fix the errors.\n' return 1 fi if [ "$strict" -eq 1 ] && [ "$WARNINGS" -gt 0 ]; then printf 'Not ready (--strict): resolve or accept the warnings.\n' return 1 fi printf 'Next: scripts/plugin-check.sh (Plugin Check on both zips).\n' return 0 } main "$@"