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
10 changes: 6 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ jobs:

- name: Resolve newsletter popup asset path
run: |
remote_file=$(./scripts/derive-newsletter-remote-path.sh "${THREEW_PROD_REMOTE_THEME_DIR}")
echo "NEWSLETTER_REMOTE_FILE=${remote_file}" >> "${GITHUB_ENV}"
shop_file=$(./scripts/derive-newsletter-remote-path.sh "${THREEW_PROD_REMOTE_THEME_DIR}" shop)
local_sha256=$(sha256sum deploy/production/3w-newsletter-popup/assets/newsletter-popup.js | awk '{print $1}')
echo "NEWSLETTER_REMOTE_FILE_SHOP=${shop_file}" >> "${GITHUB_ENV}"
echo "NEWSLETTER_LOCAL_SHA256=${local_sha256}" >> "${GITHUB_ENV}"

- name: Deploy newsletter popup fix with rollback evidence
env:
Expand All @@ -112,11 +114,11 @@ jobs:
./scripts/deploy-file.sh \
--target production \
--local-file deploy/production/3w-newsletter-popup/assets/newsletter-popup.js \
--remote-file "${NEWSLETTER_REMOTE_FILE}" \
--remote-file "${NEWSLETTER_REMOTE_FILE_SHOP}" \
--backup-file "${BACKUP_FILE}" \
--verified-file "${VERIFIED_FILE}" \
--expected-remote-sha256 f3873bf2cdef7967a6d227a66d46ce6076a42ec6b660416e0b6a8379f1134cc3 \
--expected-remote-sha256 b6af9603cef2cf5cc82541ef256a720a157c2c2e9456eef55ca059711c6e207b
--expected-remote-sha256 "${NEWSLETTER_LOCAL_SHA256}"

- name: Verify newsletter popup bytes at the origin
run: |
Expand Down
2 changes: 1 addition & 1 deletion docs/deployment-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,5 @@ Do not add `.env`, passwords, deploy keys, or host credentials to git.
- `scripts/deploy-theme.sh` loads `.env` for local runs only. GitHub Actions supplies the same variable names from GitHub Secrets.
- The npm audit gate omits development dependencies because the WordPress build toolchain currently reports dev-only advisories that require breaking tool upgrades; production dependency exposure is still gated, while CodeQL, Gitleaks, linting, builds and the popup regression cover PR quality/security.
- The production deploy job is attached to the `production` environment so GitHub can enforce approvals before secrets are exposed to the runner.
- `scripts/deploy-file.sh` accepts only a reviewed remote path, safe path characters and an allowlist of expected remote hashes. Use it for bounded production assets only; do not replace plugin directories with the theme mirror command.
- `scripts/deploy-file.sh` accepts only a bounded remote path with safe path segments and an allowlist of expected remote hashes. Use it for bounded production assets only; do not replace plugin directories with the theme mirror command.
- If SFTP key auth is required later, update `scripts/deploy-theme.sh` to support a production-specific key secret materialized into a temporary runner file, then pass `THREEW_SSH_KEY_PATH` to the script.
2 changes: 1 addition & 1 deletion docs/newsletter-popup-fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The replacement is verified by `tests/e2e/newsletter-popup.spec.js` at desktop a

## Deployment and rollback

The production workflow reruns the desktop/mobile popup regression, deploys the theme as before, then uses `scripts/derive-newsletter-remote-path.sh` to derive the plugin path from relative, site-root-relative or absolute production theme directories. It validates the expected `wp-content/themes/3w-2025` path components before returning a plugin path. `scripts/deploy-file.sh` then:
The production workflow reruns the desktop/mobile popup regression, deploys the theme as before, then uses `scripts/derive-newsletter-remote-path.sh` to derive the shop plugin path from relative, site-root-relative or absolute production theme directories. It validates the expected `wp-content/themes/3w-2025` path components and inserts only the bounded `shop` site directory. `scripts/deploy-file.sh` then:

1. downloads the current remote JS as a rollback artifact;
2. refuses to overwrite any hash except the reviewed original or this exact override (the latter makes retries idempotent);
Expand Down
13 changes: 13 additions & 0 deletions scripts/deploy-file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ if [[ ! -f "$LOCAL_FILE" ]]; then
exit 1
fi

# These local paths are also interpolated into lftp commands. The deployment
# workflow uses repository and runner-temp paths that fit this conservative set.
for local_path in "$LOCAL_FILE" "$BACKUP_FILE" "$VERIFIED_FILE"; do
if [[ ! "$local_path" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "Local path contains unsupported characters: $local_path" >&2
exit 1
fi
done

# Remote paths are interpolated into an lftp command stream. Limit the accepted
# alphabet so a caller cannot turn a path into another command.
if [[ ! "$REMOTE_FILE" =~ ^[A-Za-z0-9._/-]+$ || "$REMOTE_FILE" != */* || "$REMOTE_FILE" == *"//"* ]]; then
Expand Down Expand Up @@ -123,6 +132,10 @@ SSL_VERIFY=${THREEW_SSL_VERIFY:-yes}
FTP_USER=${THREEW_FTP_USER:-u659513315.thrwdiststaging}
FTP_PASS=${THREEW_FTP_PASS:-}
SSH_KEY_PATH=${THREEW_SSH_KEY_PATH:-}
if [[ -n "$SSH_KEY_PATH" && ! "$SSH_KEY_PATH" =~ ^[A-Za-z0-9._/-]+$ ]]; then
echo "SSH key path contains unsupported characters" >&2
exit 1
fi
TARGET_LABEL=staging

if [[ "$TARGET" == "production" ]]; then
Expand Down
11 changes: 8 additions & 3 deletions scripts/derive-newsletter-remote-path.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
# Derive the live newsletter-popup asset from the configured 3w-2025 theme path.
set -euo pipefail

if [[ $# -ne 1 || -z "$1" ]]; then
echo "Usage: derive-newsletter-remote-path.sh REMOTE_THEME_DIR" >&2
if [[ $# -lt 1 || $# -gt 2 || -z "$1" ]]; then
echo "Usage: derive-newsletter-remote-path.sh REMOTE_THEME_DIR [SITE_SUBDIRECTORY]" >&2
exit 1
fi

THEME_DIR=${1%/}
SITE_SUBDIRECTORY=${2:-}
if [[ -n "$SITE_SUBDIRECTORY" && ! "$SITE_SUBDIRECTORY" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Site subdirectory must be one safe path segment" >&2
exit 1
fi
if [[ ! "$THEME_DIR" =~ ^[A-Za-z0-9._/-]+$ || "$THEME_DIR" == *"//"* ]]; then
echo "Remote theme directory contains unsupported characters" >&2
exit 1
Expand Down Expand Up @@ -43,4 +48,4 @@ case "$SITE_ROOT" in
/) SITE_PREFIX="/" ;;
*) SITE_PREFIX="${SITE_ROOT}/" ;;
esac
printf '%s%s\n' "$SITE_PREFIX" "wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js"
printf '%s%s%s\n' "$SITE_PREFIX" "${SITE_SUBDIRECTORY:+${SITE_SUBDIRECTORY}/}" "wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js"
2 changes: 2 additions & 0 deletions tests/scripts/deploy-file.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ fi
# Theme paths may be relative, site-root relative or absolute.
[[ $("$DERIVE_PATH_SCRIPT" wp-content/themes/3w-2025) == 'wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
[[ $("$DERIVE_PATH_SCRIPT" public_html/wp-content/themes/3w-2025/) == 'public_html/wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
[[ $("$DERIVE_PATH_SCRIPT" wp-content/themes/3w-2025 shop) == 'shop/wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
[[ $("$DERIVE_PATH_SCRIPT" /home/site/public_html/wp-content/themes/3w-2025) == '/home/site/public_html/wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
[[ $("$DERIVE_PATH_SCRIPT" /home/site/public_html/wp-content/themes/3w-2025 shop) == '/home/site/public_html/shop/wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
[[ $("$DERIVE_PATH_SCRIPT" /wp-content/themes/3w-2025) == '/wp-content/plugins/3w-newsletter-popup/assets/newsletter-popup.js' ]]
for unsafe_theme_path in \
themes/3w-2025 \
Expand Down
Loading