From 55d1e666b14549cebd27d7834c9ea9f6d498586b Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:06:28 +0000 Subject: [PATCH 1/6] ci: harden WordPress release sync workflows --- .github/workflows/build.yml | 6 +- .github/workflows/push-releases.yml | 111 +++++++++++++++++++++++++ .github/workflows/update-wordpress.yml | 11 ++- 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/push-releases.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fd83a7..2b06aa9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -127,8 +127,10 @@ jobs: push-to-wordpress: needs: release - if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }} - uses: Orinks/orinks-github-releases/.github/workflows/push-releases.yml@master + if: ${{ needs.release.result == 'success' }} + uses: ./.github/workflows/push-releases.yml + with: + bust_cache_first: false secrets: WP_PUSH_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} WP_SITE_URL: ${{ secrets.WP_SITE_URL }} diff --git a/.github/workflows/push-releases.yml b/.github/workflows/push-releases.yml new file mode 100644 index 0000000..f4e45a0 --- /dev/null +++ b/.github/workflows/push-releases.yml @@ -0,0 +1,111 @@ +name: Push releases to WordPress + +on: + workflow_call: + inputs: + bust_cache_first: + description: "Bust WordPress cache before retrying push" + required: false + default: false + type: boolean + secrets: + WP_PUSH_TOKEN: + required: true + WP_SITE_URL: + required: true + workflow_dispatch: + inputs: + bust_cache_first: + description: "Bust WordPress cache before retrying push" + required: false + default: false + type: boolean + +jobs: + push: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Validate required secrets + env: + WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} + WP_URL: ${{ secrets.WP_SITE_URL }} + run: | + test -n "$WP_TOKEN" || { echo "Missing WP_PUSH_TOKEN"; exit 1; } + test -n "$WP_URL" || { echo "Missing WP_SITE_URL"; exit 1; } + + - name: Push releases to WordPress (single retry) + env: + GH_TOKEN: ${{ github.token }} + WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} + WP_URL: ${{ secrets.WP_SITE_URL }} + REPO: ${{ github.repository }} + BUST_CACHE_FIRST: ${{ inputs.bust_cache_first || false }} + run: | + set -euo pipefail + + push_attempt() { + gh api "repos/$REPO/releases/latest" -H "Accept: application/vnd.github+json" > /tmp/latest.json 2>/dev/null || echo "null" > /tmp/latest.json + gh api "repos/$REPO/releases?per_page=20" -H "Accept: application/vnd.github+json" > /tmp/all.json + + python3 - <<'PYEOF' + import json, os, urllib.request + + with open('/tmp/latest.json') as f: + latest = json.load(f) + + with open('/tmp/all.json') as f: + all_releases = json.load(f) + + payload = json.dumps({ + 'repo': os.environ['REPO'], + 'releases': all_releases[:20], + 'latest': latest if isinstance(latest, dict) and latest.get('tag_name') else None, + }).encode() + + req = urllib.request.Request( + os.environ['WP_URL'].rstrip('/') + '/wp-json/ogr/v1/push-releases', + data=payload, + headers={ + 'Content-Type': 'application/json', + 'X-OGR-Token': os.environ['WP_TOKEN'], + }, + method='POST', + ) + with urllib.request.urlopen(req, timeout=30) as r: + body = json.load(r) + print(f"Pushed {body['count']} releases for {body['repo']}") + PYEOF + } + + maybe_bust_cache() { + if [ "$BUST_CACHE_FIRST" != "true" ]; then + return 0 + fi + + echo "Optional cache nudge enabled. Calling bust-cache endpoint before retry..." + python3 - <<'PYEOF' + import json, os, urllib.request + + req = urllib.request.Request( + os.environ['WP_URL'].rstrip('/') + '/wp-json/ogr/v1/bust-cache', + data=b'{}', + headers={ + 'Content-Type': 'application/json', + 'X-OGR-Token': os.environ['WP_TOKEN'], + }, + method='POST', + ) + with urllib.request.urlopen(req, timeout=20) as r: + body = json.load(r) + print("Cache nudge response:", body) + PYEOF + } + + push_attempt || { + echo "First push attempt failed." + maybe_bust_cache || true + echo "Retrying once in 10s..." + sleep 10 + push_attempt + } diff --git a/.github/workflows/update-wordpress.yml b/.github/workflows/update-wordpress.yml index 71b625c..f32d288 100644 --- a/.github/workflows/update-wordpress.yml +++ b/.github/workflows/update-wordpress.yml @@ -3,10 +3,19 @@ name: Update WordPress on release on: release: types: [published] + workflow_dispatch: + inputs: + bust_cache_first: + description: "Bust WordPress cache before retrying push" + required: false + default: false + type: boolean jobs: push-to-wordpress: - uses: Orinks/orinks-github-releases/.github/workflows/push-releases.yml@master + uses: ./.github/workflows/push-releases.yml + with: + bust_cache_first: ${{ inputs.bust_cache_first || false }} secrets: WP_PUSH_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} WP_SITE_URL: ${{ secrets.WP_SITE_URL }} From 928786400fa9c5e3a63037d7dd6701928ca3323a Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:12:10 +0000 Subject: [PATCH 2/6] ci: simplify wp push workflow script --- .github/scripts/push_releases.py | 95 +++++++++++++++++++++++++++++ .github/workflows/push-releases.yml | 78 +---------------------- 2 files changed, 96 insertions(+), 77 deletions(-) create mode 100644 .github/scripts/push_releases.py diff --git a/.github/scripts/push_releases.py b/.github/scripts/push_releases.py new file mode 100644 index 0000000..8b3104d --- /dev/null +++ b/.github/scripts/push_releases.py @@ -0,0 +1,95 @@ +"""Push GitHub release metadata to the WordPress site.""" + +from __future__ import annotations + +import json +import os +import subprocess +import time +import urllib.request +from typing import Any + +REPO = os.environ["REPO"] +WP_URL = os.environ["WP_URL"].rstrip("/") +WP_TOKEN = os.environ["WP_TOKEN"] +BUST_CACHE_FIRST = os.environ.get("BUST_CACHE_FIRST", "false").lower() == "true" +HEADERS = { + "Content-Type": "application/json", + "X-OGR-Token": WP_TOKEN, +} + + +def gh_json(endpoint: str) -> Any: + """Run `gh api` and return the parsed JSON payload.""" + command = [ + "gh", + "api", + endpoint, + "-H", + "Accept: application/vnd.github+json", + ] + result = subprocess.run(command, capture_output=True, text=True, check=True) + return json.loads(result.stdout) + + +def fetch_payload() -> dict[str, Any]: + latest = gh_json(f"repos/{REPO}/releases/latest") + all_releases = gh_json(f"repos/{REPO}/releases?per_page=20") + return { + "repo": REPO, + "releases": all_releases[:20], + "latest": latest if isinstance(latest, dict) and latest.get("tag_name") else None, + } + + +def push_releases(payload: dict[str, Any]) -> dict[str, Any]: + """Transmit release payload to the WordPress endpoint.""" + data = json.dumps(payload).encode() + req = urllib.request.Request( + f"{WP_URL}/wp-json/ogr/v1/push-releases", + data=data, + headers=HEADERS, + method="POST", + ) + with urllib.request.urlopen(req, timeout=30) as response: + return json.load(response) + + +def bust_cache() -> dict[str, Any]: + """Hit the optional bust-cache endpoint so the next push lands on fresh data.""" + req = urllib.request.Request( + f"{WP_URL}/wp-json/ogr/v1/bust-cache", + data=b"{}", + headers=HEADERS, + method="POST", + ) + with urllib.request.urlopen(req, timeout=20) as response: + return json.load(response) + + +def main() -> None: + payload = fetch_payload() + + def attempt() -> dict[str, Any]: + return push_releases(payload) + + for attempt_num in range(2): + try: + result = attempt() + print(f"Pushed {result['count']} releases for {result['repo']}") + return + except Exception as exc: + print("First push attempt failed." if attempt_num == 0 else "Second push attempt failed.") + if attempt_num == 0: + if BUST_CACHE_FIRST: + print("Cache nudge enabled. Hitting bust-cache before retry...") + bust_result = bust_cache() + print("Cache nudge response:", bust_result) + print("Retrying once in 10s...") + time.sleep(10) + continue + raise + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/push-releases.yml b/.github/workflows/push-releases.yml index f4e45a0..462dd81 100644 --- a/.github/workflows/push-releases.yml +++ b/.github/workflows/push-releases.yml @@ -30,82 +30,6 @@ jobs: env: WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} WP_URL: ${{ secrets.WP_SITE_URL }} - run: | - test -n "$WP_TOKEN" || { echo "Missing WP_PUSH_TOKEN"; exit 1; } - test -n "$WP_URL" || { echo "Missing WP_SITE_URL"; exit 1; } - - - name: Push releases to WordPress (single retry) - env: - GH_TOKEN: ${{ github.token }} - WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} - WP_URL: ${{ secrets.WP_SITE_URL }} - REPO: ${{ github.repository }} - BUST_CACHE_FIRST: ${{ inputs.bust_cache_first || false }} run: | set -euo pipefail - - push_attempt() { - gh api "repos/$REPO/releases/latest" -H "Accept: application/vnd.github+json" > /tmp/latest.json 2>/dev/null || echo "null" > /tmp/latest.json - gh api "repos/$REPO/releases?per_page=20" -H "Accept: application/vnd.github+json" > /tmp/all.json - - python3 - <<'PYEOF' - import json, os, urllib.request - - with open('/tmp/latest.json') as f: - latest = json.load(f) - - with open('/tmp/all.json') as f: - all_releases = json.load(f) - - payload = json.dumps({ - 'repo': os.environ['REPO'], - 'releases': all_releases[:20], - 'latest': latest if isinstance(latest, dict) and latest.get('tag_name') else None, - }).encode() - - req = urllib.request.Request( - os.environ['WP_URL'].rstrip('/') + '/wp-json/ogr/v1/push-releases', - data=payload, - headers={ - 'Content-Type': 'application/json', - 'X-OGR-Token': os.environ['WP_TOKEN'], - }, - method='POST', - ) - with urllib.request.urlopen(req, timeout=30) as r: - body = json.load(r) - print(f"Pushed {body['count']} releases for {body['repo']}") - PYEOF - } - - maybe_bust_cache() { - if [ "$BUST_CACHE_FIRST" != "true" ]; then - return 0 - fi - - echo "Optional cache nudge enabled. Calling bust-cache endpoint before retry..." - python3 - <<'PYEOF' - import json, os, urllib.request - - req = urllib.request.Request( - os.environ['WP_URL'].rstrip('/') + '/wp-json/ogr/v1/bust-cache', - data=b'{}', - headers={ - 'Content-Type': 'application/json', - 'X-OGR-Token': os.environ['WP_TOKEN'], - }, - method='POST', - ) - with urllib.request.urlopen(req, timeout=20) as r: - body = json.load(r) - print("Cache nudge response:", body) - PYEOF - } - - push_attempt || { - echo "First push attempt failed." - maybe_bust_cache || true - echo "Retrying once in 10s..." - sleep 10 - push_attempt - } + python3 .github/scripts/push_releases.py From 8b981abd9e0a48542559e0d8047b55efea57e567 Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:13:09 +0000 Subject: [PATCH 3/6] ci: checkout repo for wp push script --- .github/workflows/push-releases.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/push-releases.yml b/.github/workflows/push-releases.yml index 462dd81..2cb8da5 100644 --- a/.github/workflows/push-releases.yml +++ b/.github/workflows/push-releases.yml @@ -26,6 +26,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: + - name: Checkout repo + uses: actions/checkout@v4 - name: Validate required secrets env: WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} From e6dc1961b9ec660c7407fbcba1949d92c9dc534d Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:14:09 +0000 Subject: [PATCH 4/6] ci: pass repo info to wp push script --- .github/workflows/push-releases.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/push-releases.yml b/.github/workflows/push-releases.yml index 2cb8da5..d8ab865 100644 --- a/.github/workflows/push-releases.yml +++ b/.github/workflows/push-releases.yml @@ -32,6 +32,8 @@ jobs: env: WP_TOKEN: ${{ secrets.WP_PUSH_TOKEN }} WP_URL: ${{ secrets.WP_SITE_URL }} + REPO: ${{ github.repository }} + BUST_CACHE_FIRST: ${{ inputs.bust_cache_first || false }} run: | set -euo pipefail python3 .github/scripts/push_releases.py From 614a777d292bc7d9b64315fb08cf8e7d22e0330f Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:14:55 +0000 Subject: [PATCH 5/6] ci: fetch GitHub releases via API --- .github/scripts/push_releases.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/scripts/push_releases.py b/.github/scripts/push_releases.py index 8b3104d..1e05fed 100644 --- a/.github/scripts/push_releases.py +++ b/.github/scripts/push_releases.py @@ -4,7 +4,6 @@ import json import os -import subprocess import time import urllib.request from typing import Any @@ -13,23 +12,23 @@ WP_URL = os.environ["WP_URL"].rstrip("/") WP_TOKEN = os.environ["WP_TOKEN"] BUST_CACHE_FIRST = os.environ.get("BUST_CACHE_FIRST", "false").lower() == "true" +GH_TOKEN = os.environ.get("GITHUB_TOKEN") HEADERS = { "Content-Type": "application/json", "X-OGR-Token": WP_TOKEN, } +GH_API_HEADERS: dict[str, str] = { + "Accept": "application/vnd.github+json", +} +if GH_TOKEN: + GH_API_HEADERS["Authorization"] = f"Bearer {GH_TOKEN}" def gh_json(endpoint: str) -> Any: - """Run `gh api` and return the parsed JSON payload.""" - command = [ - "gh", - "api", - endpoint, - "-H", - "Accept: application/vnd.github+json", - ] - result = subprocess.run(command, capture_output=True, text=True, check=True) - return json.loads(result.stdout) + url = f"https://api.github.com/{endpoint}" + request = urllib.request.Request(url, headers=GH_API_HEADERS) + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) def fetch_payload() -> dict[str, Any]: From cb47555820594ce1e0f58ae134845343ee323395 Mon Sep 17 00:00:00 2001 From: Orinks <38449772+Orinks@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:16:03 +0000 Subject: [PATCH 6/6] ci: handle missing releases gracefully --- .github/scripts/push_releases.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/scripts/push_releases.py b/.github/scripts/push_releases.py index 1e05fed..2127e66 100644 --- a/.github/scripts/push_releases.py +++ b/.github/scripts/push_releases.py @@ -5,6 +5,7 @@ import json import os import time +import urllib.error import urllib.request from typing import Any @@ -24,16 +25,23 @@ GH_API_HEADERS["Authorization"] = f"Bearer {GH_TOKEN}" -def gh_json(endpoint: str) -> Any: +def gh_json(endpoint: str, allow_missing: bool = False) -> Any: url = f"https://api.github.com/{endpoint}" request = urllib.request.Request(url, headers=GH_API_HEADERS) - with urllib.request.urlopen(request, timeout=30) as response: - return json.load(response) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + except urllib.error.HTTPError as exc: + if allow_missing and exc.code == 404: + return None + raise def fetch_payload() -> dict[str, Any]: - latest = gh_json(f"repos/{REPO}/releases/latest") - all_releases = gh_json(f"repos/{REPO}/releases?per_page=20") + latest = gh_json(f"repos/{REPO}/releases/latest", allow_missing=True) + all_releases = gh_json(f"repos/{REPO}/releases?per_page=20", allow_missing=True) + if not isinstance(all_releases, list): + all_releases = [] return { "repo": REPO, "releases": all_releases[:20],