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
90 changes: 90 additions & 0 deletions tests/wordpress-release-artifact-version-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail

# Smoke test for wordpress/scripts/release/verify-artifact-version.sh:
# the guard that refuses to publish a release ZIP whose internal plugin/theme
# version does not match the version the release is shipping (the
# data-machine-socials v0.14.0 stale-asset incident).

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERIFY="${ROOT_DIR}/wordpress/scripts/release/verify-artifact-version.sh"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

make_plugin_zip() {
local zip_path="$1"
local version="$2"
local stage="${TMP_DIR}/stage-$RANDOM"
mkdir -p "${stage}/test-plugin"
cat > "${stage}/test-plugin/test-plugin.php" <<EOF
<?php
/**
* Plugin Name: Test Plugin
* Version: ${version}
*/
EOF
(cd "${stage}" && zip -q -r "${zip_path}" "test-plugin/")
rm -rf "${stage}"
}

make_theme_zip() {
local zip_path="$1"
local version="$2"
local stage="${TMP_DIR}/stage-$RANDOM"
mkdir -p "${stage}/test-theme"
cat > "${stage}/test-theme/style.css" <<EOF
/*
Theme Name: Test Theme
Version: ${version}
*/
EOF
(cd "${stage}" && zip -q -r "${zip_path}" "test-theme/")
rm -rf "${stage}"
}

# 1. Matching plugin version passes and prints the version.
make_plugin_zip "${TMP_DIR}/match.zip" "1.2.3"
got="$(bash "${VERIFY}" "${TMP_DIR}/match.zip" "1.2.3")"
if [[ "${got}" != "1.2.3" ]]; then
echo "expected matched version output '1.2.3', got '${got}'" >&2
exit 1
fi

# 2. Mismatched plugin version fails (the stale-artifact case).
make_plugin_zip "${TMP_DIR}/stale.zip" "0.8.1"
if bash "${VERIFY}" "${TMP_DIR}/stale.zip" "0.14.0" 2>/dev/null; then
echo "stale artifact (0.8.1 vs expected 0.14.0) was not rejected" >&2
exit 1
fi

# 3. Theme style.css fallback passes on match.
make_theme_zip "${TMP_DIR}/theme.zip" "2.0.0"
got="$(bash "${VERIFY}" "${TMP_DIR}/theme.zip" "2.0.0")"
if [[ "${got}" != "2.0.0" ]]; then
echo "expected theme version output '2.0.0', got '${got}'" >&2
exit 1
fi

# 4. Theme mismatch fails.
if bash "${VERIFY}" "${TMP_DIR}/theme.zip" "2.1.0" 2>/dev/null; then
echo "stale theme artifact (2.0.0 vs expected 2.1.0) was not rejected" >&2
exit 1
fi

# 5. ZIP without any version header fails rather than passing silently.
stage="${TMP_DIR}/stage-noheader"
mkdir -p "${stage}/mystery"
echo "<?php // no headers" > "${stage}/mystery/file.php"
(cd "${stage}" && zip -q -r "${TMP_DIR}/noheader.zip" "mystery/")
if bash "${VERIFY}" "${TMP_DIR}/noheader.zip" "1.0.0" 2>/dev/null; then
echo "artifact without a version header was not rejected" >&2
exit 1
fi

# 6. Missing artifact path fails.
if bash "${VERIFY}" "${TMP_DIR}/does-not-exist.zip" "1.0.0" 2>/dev/null; then
echo "missing artifact was not rejected" >&2
exit 1
fi

echo "wordpress release artifact version smoke passed"
30 changes: 30 additions & 0 deletions wordpress/scripts/release/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,35 @@ fi

echo "Built ${ARTIFACT_PATH}" >&2

# Assert the ZIP we just built actually contains the version this release
# is shipping. Catches stale artifacts (e.g. a pre-existing build/*.zip that
# was restored instead of rebuilt) before they can reach the publish step.
# The expected version comes from the release payload when homeboy invokes
# this action; standalone dry-runs fall back to the on-disk header so the
# check still validates build output against source.
EXPECTED_VERSION=""
if [[ -n "${HOMEBOY_SETTINGS_JSON:-}" ]]; then
EXPECTED_VERSION="$(echo "${HOMEBOY_SETTINGS_JSON}" | jq -r '.release.version // empty')"
fi
if [[ -z "${EXPECTED_VERSION}" ]]; then
for candidate in *.php; do
[[ -f "${candidate}" ]] || continue
if grep -qi 'Plugin Name:' "${candidate}"; then
EXPECTED_VERSION="$(grep -i -m1 'Version:' "${candidate}" | sed 's/.*[Vv]ersion:[[:space:]]*//' | tr -d '[:space:]')"
break
fi
done
if [[ -z "${EXPECTED_VERSION}" ]] && [[ -f "style.css" ]]; then
EXPECTED_VERSION="$(grep -i -m1 'Version:' "style.css" | sed 's/.*[Vv]ersion:[[:space:]]*//' | tr -d '[:space:]')"
fi
fi

if [[ -n "${EXPECTED_VERSION}" ]]; then
bash "${SCRIPT_DIR}/verify-artifact-version.sh" "${ARTIFACT_PATH}" "${EXPECTED_VERSION}" >/dev/null
echo "Verified ${ARTIFACT_PATH} contains version ${EXPECTED_VERSION}" >&2
else
echo "Warning: could not determine expected version; skipping artifact version verification" >&2
fi

jq -cn --arg path "${ARTIFACT_PATH}" \
'[{path: $path, type: "wordpress-zip", platform: null}]'
10 changes: 10 additions & 0 deletions wordpress/scripts/release/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ fi

apply_github_host_env "${GITHUB_HOST}"

# Assert the artifact's internal version matches the release tag before
# uploading. This is the last chokepoint before a ZIP becomes the GitHub
# Release asset that deploys consume — a stale artifact here means silent
# production rollback (data-machine-socials v0.14.0 shipped a v0.8.1 zip
# for 6 days). Strip the leading "v" and any monorepo "<component>-v"
# prefix from the tag to get the bare semver.
EXPECTED_VERSION="${TAG##*v}"
bash "${SCRIPT_DIR}/verify-artifact-version.sh" "${ARTIFACT_PATH}" "${EXPECTED_VERSION}" >/dev/null
echo "Verified ${ARTIFACT_PATH} contains version ${EXPECTED_VERSION} (tag ${TAG})" >&2

echo "Uploading ${ARTIFACT_PATH} to ${REPO_SLUG} release ${TAG}..." >&2

gh release upload "${TAG}" "${ARTIFACT_PATH}" \
Expand Down
79 changes: 79 additions & 0 deletions wordpress/scripts/release/verify-artifact-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail

# Verify that a packaged WordPress plugin/theme ZIP contains the expected
# version before it is published. Guards against stale artifacts reaching
# GitHub Release assets — the data-machine-socials v0.14.0 incident shipped
# a v0.8.1 zip (restored from a stale git-tracked blob during release
# recovery) as the release asset, and production silently ran rolled-back
# code for 6 days because nothing in the pipeline ever opened the zip.
#
# Usage:
# verify-artifact-version.sh <artifact.zip> <expected-version>
#
# <expected-version> is the bare semver (no leading "v"). The internal
# version is read from the plugin main-file header (any top-level
# "<root>/<name>.php" containing a "Plugin Name:" header) or, for themes,
# from "<root>/style.css".
#
# Exits 0 and prints the matched version on stdout when versions agree.
# Exits 1 with a diagnostic on stderr when they do not, or when no version
# header can be located inside the artifact.

ARTIFACT_PATH="${1:-}"
EXPECTED_VERSION="${2:-}"

if [[ -z "${ARTIFACT_PATH}" ]] || [[ -z "${EXPECTED_VERSION}" ]]; then
echo "Usage: verify-artifact-version.sh <artifact.zip> <expected-version>" >&2
exit 1
fi

if [[ ! -f "${ARTIFACT_PATH}" ]]; then
echo "Error: artifact '${ARTIFACT_PATH}' does not exist" >&2
exit 1
fi

if ! command -v unzip >/dev/null 2>&1; then
echo "Error: unzip is required to verify artifact versions" >&2
exit 1
fi

extract_version_header() {
# Pull "Version: x.y.z" out of header text on stdin.
grep -i -m1 '^[[:space:]*]*Version:' | sed 's/.*[Vv]ersion:[[:space:]]*//' | tr -d '[:space:]'
}

ARTIFACT_VERSION=""

# Plugin probe: top-level PHP files inside the wrapper directory that carry
# a "Plugin Name:" header.
while IFS= read -r entry; do
header="$(unzip -p "${ARTIFACT_PATH}" "${entry}" 2>/dev/null | head -n 60 || true)"
if echo "${header}" | grep -qi 'Plugin Name:'; then
candidate="$(echo "${header}" | extract_version_header)"
if [[ -n "${candidate}" ]]; then
ARTIFACT_VERSION="${candidate}"
break
fi
fi
done < <(unzip -Z1 "${ARTIFACT_PATH}" | grep -E '^[^/]+/[^/]+\.php$' || true)

# Theme fallback: style.css at the wrapper root.
if [[ -z "${ARTIFACT_VERSION}" ]]; then
style_entry="$(unzip -Z1 "${ARTIFACT_PATH}" | grep -E '^[^/]+/style\.css$' | head -n 1 || true)"
if [[ -n "${style_entry}" ]]; then
ARTIFACT_VERSION="$(unzip -p "${ARTIFACT_PATH}" "${style_entry}" 2>/dev/null | head -n 60 | extract_version_header || true)"
fi
fi

if [[ -z "${ARTIFACT_VERSION}" ]]; then
echo "Error: could not locate a plugin/theme version header inside ${ARTIFACT_PATH}" >&2
exit 1
fi

if [[ "${ARTIFACT_VERSION}" != "${EXPECTED_VERSION}" ]]; then
echo "Error: artifact ${ARTIFACT_PATH} contains version ${ARTIFACT_VERSION} but the release expects ${EXPECTED_VERSION} — refusing to ship a stale artifact" >&2
exit 1
fi

echo "${ARTIFACT_VERSION}"