← back to Designer Wallcoverings
pending-approval/fix-coastal-mist-BA469.sh
116 lines
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# GATED PROD WRITE — run by Steve only. Default = DRY RUN (prints, changes nothing).
#
# Fixes live Shopify product "Portofino Bamboo - Coastal Mist Wallcovering"
# (designerwallcoverings.com), which currently shows a Baldwin B7469 automotive
# oil filter. Root cause: importer fuzzy-matched real SKU BA469 -> file B7469.
#
# Real product : Washington Wallcoverings BA469 "Smoke Gray Sisal" (Best of Asia IV).
# Image : coastal-mist-hires.jpg (full-res 960x1200), uploaded as a base64
# attachment so Shopify rehosts on the DW CDN.
# Alt text : OUR info only ("Portofino Bamboo - Coastal Mist Wallcovering |
# Phillipe Romano") — source vendor never exposed.
#
# Three steps: (1) add the real image, (2) delete the oil-filter image,
# (3) strip the wrong `Red` tag and `Needs-Image` (keep color:Smoke + all else).
#
# To execute for real: CONFIRM=1 bash fix-coastal-mist-BA469.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SHOP="designer-laboratory-sandbox.myshopify.com"
API="2024-10"
PRODUCT_ID="7808267157555"
OIL_FILTER_IMAGE_ID="37091234611251"
NEW_IMAGE_FILE="$(dirname "$0")/coastal-mist-hires.jpg"
NEW_IMAGE_ALT="Portofino Bamboo - Coastal Mist Wallcovering | Phillipe Romano"
TOKEN="${SHOPIFY_ADMIN_TOKEN:-}"
if [ -z "$TOKEN" ]; then
TOKEN="$(grep -hE '^SHOPIFY_ADMIN_TOKEN=' "$HOME/Projects/secrets-manager/.env" 2>/dev/null | head -1 | cut -d= -f2-)"
fi
[ -z "$TOKEN" ] && { echo "ERROR: SHOPIFY_ADMIN_TOKEN not in env or secrets-manager/.env"; exit 1; }
BASE="https://$SHOP/admin/api/$API"
H_TOK=(-H "X-Shopify-Access-Token: $TOKEN") # always passed as a quoted array — never eval'd
H_JSON=(-H "Content-Type: application/json")
DRY=1; [ "${CONFIRM:-0}" = "1" ] && DRY=0
say(){ printf '\n\033[1m%s\033[0m\n' "$*"; }
DESC(){ echo " [dry-run] would: $*"; } # human description only — never prints the token
say "Target: $PRODUCT_ID on $SHOP (api $API) | mode: $([ "$DRY" = 1 ] && echo DRY-RUN || echo LIVE)"
say "0) Current state"
curl -s "${H_TOK[@]}" "$BASE/products/$PRODUCT_ID.json" \
| python3 -c 'import json,sys;p=json.load(sys.stdin)["product"];print(" images:",[(i["id"],i["src"].split("/")[-1]) for i in p["images"]]);print(" tags :",p["tags"])'
say "1) Add full-res image as base64 attachment (Shopify hosts it on its CDN)"
[ -f "$NEW_IMAGE_FILE" ] || { echo "ERROR: image file not found: $NEW_IMAGE_FILE"; exit 1; }
# idempotency: skip if our image is already attached (so a re-run can't duplicate it)
ALREADY="$(curl -s "${H_TOK[@]}" "$BASE/products/$PRODUCT_ID.json" | python3 -c 'import json,sys;print("yes" if any("portofino-bamboo-coastal-mist" in (i.get("src") or "") for i in json.load(sys.stdin)["product"]["images"]) else "no")')"
if [ "$ALREADY" = "yes" ]; then echo " already attached — skipping add"; SKIP1=1; else SKIP1=0; fi
PAYLOAD="$(mktemp /tmp/cm-img.XXXXXX)"
python3 - "$NEW_IMAGE_FILE" "$NEW_IMAGE_ALT" > "$PAYLOAD" <<'PY'
import base64, json, sys
path, alt = sys.argv[1], sys.argv[2]
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
print(json.dumps({"image": {"attachment": b64, "filename": "portofino-bamboo-coastal-mist.jpg",
"position": 1, "alt": alt}}))
PY
if [ "$SKIP1" = 1 ]; then
rm -f "$PAYLOAD"
elif [ "$DRY" = 1 ]; then
DESC "POST $BASE/products/$PRODUCT_ID/images.json (base64 attachment, alt: $NEW_IMAGE_ALT)"
echo " [dry-run] payload built at $PAYLOAD ($(wc -c < "$PAYLOAD") bytes)"
rm -f "$PAYLOAD"
else
RESP="$(curl -s -w $'\n%{http_code}' -X POST "$BASE/products/$PRODUCT_ID/images.json" "${H_TOK[@]}" "${H_JSON[@]}" --data-binary @"$PAYLOAD")"
CODE="${RESP##*$'\n'}"; BODY="${RESP%$'\n'*}"
rm -f "$PAYLOAD"
echo " POST HTTP $CODE"
case "$CODE" in 200|201) echo "$BODY" | python3 -c 'import json,sys;i=json.load(sys.stdin).get("image",{});print(" added image id",i.get("id"),i.get("width"),"x",i.get("height"),i.get("src"))';;
*) echo " ERROR body: $BODY"; exit 1;; esac
fi
say "2) Delete the oil-filter image ($OIL_FILTER_IMAGE_ID)"
if [ "$DRY" = 1 ]; then
DESC "DELETE $BASE/products/$PRODUCT_ID/images/$OIL_FILTER_IMAGE_ID.json"
else
curl -s -X DELETE "$BASE/products/$PRODUCT_ID/images/$OIL_FILTER_IMAGE_ID.json" "${H_TOK[@]}" -o /dev/null -w ' delete HTTP %{http_code}\n'
fi
say "3) Strip wrong tags (Red, Needs-Image) — reads LIVE tags first, removes only these"
CUR_TAGS="$(curl -s "${H_TOK[@]}" "$BASE/products/$PRODUCT_ID.json" | python3 -c 'import json,sys;print(json.load(sys.stdin)["product"]["tags"])')"
NEW_TAGS="$(python3 - "$CUR_TAGS" <<'PY'
import sys
cur=[t.strip() for t in sys.argv[1].split(",") if t.strip()]
drop={"red","needs-image"}
print(", ".join(t for t in cur if t.lower() not in drop))
PY
)"
echo " before: $CUR_TAGS"
echo " after : $NEW_TAGS"
if [ "$DRY" = 1 ]; then
DESC "PUT $BASE/products/$PRODUCT_ID.json tags -> (the 'after' line above)"
else
TAGJSON="$(python3 -c 'import json,sys;print(json.dumps({"product":{"id":int(sys.argv[1]),"tags":sys.argv[2]}}))' "$PRODUCT_ID" "$NEW_TAGS")"
curl -s -X PUT "$BASE/products/$PRODUCT_ID.json" "${H_TOK[@]}" "${H_JSON[@]}" -d "$TAGJSON" -o /dev/null -w ' tag update HTTP %{http_code}\n'
fi
say "4) Verify"
if [ "$DRY" = 0 ]; then
curl -s "${H_TOK[@]}" "$BASE/products/$PRODUCT_ID.json" \
| python3 -c 'import json,sys;p=json.load(sys.stdin)["product"];print(" images:",[i["src"].split("/")[-1] for i in p["images"]]);print(" tags :",p["tags"])'
else
echo " (dry-run: skipped verify)"
fi
say "Done ($([ "$DRY" = 1 ] && echo 'DRY-RUN — re-run with CONFIRM=1 to apply' || echo 'LIVE applied'))."
# ── Notes ────────────────────────────────────────────────────────────────────
# • Curl always receives the auth header via the "${H_TOK[@]}" array (never eval),
# so the token is correctly quoted and never printed.
# • To keep Needs-Image set for now, edit the drop set in step 3 to {"red"}.