From db5a401d3f69f70a2e6bf695de1ab56a5d2fafe8 Mon Sep 17 00:00:00 2001 From: Ben Kalsky Date: Tue, 15 Sep 2026 00:19:05 +0300 Subject: [PATCH 1/3] ci: README badge 3.8.2 -> 3.9.5, a check that every version declaration agrees, and release-drift waits for the release on push The README badge said 3.8.2 while SKILL.md, package.json and the README's own "Current version" line said 3.9.5: nothing compared them. ci.yml gains a "Version declarations agree" job - SKILL.md version, README badge, package.json, and any "Current version: **x.y.z**" line must match - on pull requests too, so a bump that forgets one is caught before merge. release-drift.yml runs on push to main and starts the moment the merge lands, while the release is cut AFTER the merge. Failing on the first lookup turned every ordinary release into a red "SKILL.md version has a release" check on main's head that no later run could replace - here, v3.9.5 was cut 52 min after its merge and main's head has carried that red check since (hostinger-mcp v1.2.1: 19 s after, same result). On push the check now waits for the release, up to 20 x 30 s; a bump still unreleased after ten minutes is the drift the check exists for and fails exactly as before. Schedule and dispatch runs do not wait. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 32 +++++++++++++++++++ .github/workflows/release-drift.yml | 49 ++++++++++++++++++++--------- README.md | 2 +- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c81d96..fe82c5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,38 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' jobs: + versions: + name: Version declarations agree + # The README badge sat at 1.4.0 while this skill shipped 1.5.0, 1.6.0 and + # 1.6.1 (wordpress-api-pro's at 3.8.2 through 3.9.5): nothing compared + # them. Every place that states the version has to say the same thing, + # on pull requests too, so the bump that forgets one is caught before merge. + runs-on: ubuntu-latest + env: + SKILL: wordpress-api-pro/SKILL.md + steps: + - uses: actions/checkout@v4 + - name: SKILL.md, README badge, package.json and "Current version" lines agree + run: | + set -euo pipefail + skill_v=$(grep -m1 '^version:' "$SKILL" | sed 's/^version:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs) + [ -n "$skill_v" ] || { echo "::error::no version: in $SKILL"; exit 1; } + mismatch=0 + # Color-agnostic: match the semver itself, not the badge color suffix. + badge_v=$(grep -oE 'badge/version-[0-9]+\.[0-9]+\.[0-9]+' README.md | head -1 | sed 's|badge/version-||' || true) + [ "$badge_v" = "$skill_v" ] || { echo "::error::README badge says ${badge_v:-}, $SKILL says $skill_v"; mismatch=1; } + pkg_v="" + if [ -f package.json ]; then + pkg_v=$(jq -r .version package.json) + [ "$pkg_v" = "$skill_v" ] || { echo "::error::package.json says $pkg_v, $SKILL says $skill_v"; mismatch=1; } + fi + # Every "Current version: **x.y.z**" line in the README, if any. + for v in $(grep -oE 'Current version: \*\*[0-9]+\.[0-9]+\.[0-9]+\*\*' README.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true); do + [ "$v" = "$skill_v" ] || { echo "::error::README 'Current version' says $v, $SKILL says $skill_v"; mismatch=1; } + done + [ "$mismatch" = 0 ] || { echo "::error::bump every declaration in the same commit"; exit 1; } + echo "SKILL.md, README badge${pkg_v:+, package.json} agree on $skill_v" + test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest diff --git a/.github/workflows/release-drift.yml b/.github/workflows/release-drift.yml index 2702d8a..c6c0051 100644 --- a/.github/workflows/release-drift.yml +++ b/.github/workflows/release-drift.yml @@ -94,20 +94,41 @@ jobs: # Collapsing both into "missing" would report "cut the release" after # a transient API blip, sending someone to create a release that # already exists. - ERR=$(mktemp) - set +e - DRAFT=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$VERSION" \ - --jq '.draft' 2>"$ERR") - RC=$? - set -e - if [ "$RC" -eq 0 ]; then - STATE=$DRAFT - elif grep -qiE '404|not found' "$ERR"; then - STATE=missing - else - echo "::error::Could not query releases - this is a lookup failure, not a missing release:" - cat "$ERR" >&2 - exit 1 + lookup() { + ERR=$(mktemp) + set +e + DRAFT=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$VERSION" \ + --jq '.draft' 2>"$ERR") + RC=$? + set -e + if [ "$RC" -eq 0 ]; then + STATE=$DRAFT + elif grep -qiE '404|not found' "$ERR"; then + STATE=missing + else + echo "::error::Could not query releases - this is a lookup failure, not a missing release:" + cat "$ERR" >&2 + exit 1 + fi + } + lookup + # On a push to main the release is cut AFTER the merge - seconds later + # when the release flow is scripted, longer by hand - and this run + # starts the moment the push lands. Failing on the first lookup turned + # every ordinary release into a red check on main's head that no later + # run could replace (measured: hostinger-mcp v1.2.1 was cut 19 s after + # the merge and the push run had already failed; wordpress-api-pro + # v3.9.5, 52 min after). So on push, wait for the release: up to + # 20 x 30 s. A bump still unreleased after ten minutes is the drift + # this check exists for and fails exactly as before. Schedule and + # dispatch runs do not wait - nothing is in flight for them. + if [ "$GITHUB_EVENT_NAME" = "push" ]; then + for attempt in $(seq 1 20); do + [ "$STATE" = missing ] || break + echo "no v$VERSION release yet (attempt $attempt/20) - waiting 30 s for the release flow" + sleep 30 + lookup + done fi case "$STATE" in false) diff --git a/README.md b/README.md index 3e3f855..ab38b39 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![OpenClaw Skill](https://img.shields.io/badge/OpenClaw-Skill-purple) ![WordPress](https://img.shields.io/badge/WordPress-REST_API-21759b) ![License: MIT--0](https://img.shields.io/badge/License-MIT--0-green) -![Version](https://img.shields.io/badge/version-3.8.2-blue) +![Version](https://img.shields.io/badge/version-3.9.5-blue) A production-grade **Claude Code & OpenClaw skill** for managing WordPress content via the REST API — posts, pages, media, WooCommerce, Elementor, SEO meta, ACF, JetEngine — with explicit safety boundaries for agentic use. From cad63bd6fd1b2818de759b03134605ae482d036a Mon Sep 17 00:00:00 2001 From: Ben Kalsky Date: Tue, 15 Sep 2026 00:25:16 +0300 Subject: [PATCH 2/3] ci: release-drift waits through a draft too; the version check reads the whole semver (Codex round 1) release-drift.yml: the push-time wait broke out of its loop on a DRAFT and failed at once, so a flow that creates the release as a draft and publishes it after its ClawHub publish succeeded (cloudways-mcp's release.yml), or a draft cut by hand before the merge and published moments after, still got the red check the wait was added to prevent. On push the loop now waits through missing AND draft; a draft still standing after the window fails below, as before. ci.yml: the badge regex took only the x.y.z core, and shields.io writes a dash inside a badge value as "--" (version-1.2.0--rc.1-blue renders "1.2.0-rc.1", measured), so a consistent prerelease bump would have been rejected. The badge is read whole and the "--" folded back; the "Current version" line likewise. Verified on a synthetic tree at 1.2.0-rc.1 (accepted) and with the badge left at 1.1.0 (rejected), and on this tree. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/release-drift.yml | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe82c5d..79399a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,12 @@ jobs: skill_v=$(grep -m1 '^version:' "$SKILL" | sed 's/^version:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs) [ -n "$skill_v" ] || { echo "::error::no version: in $SKILL"; exit 1; } mismatch=0 - # Color-agnostic: match the semver itself, not the badge color suffix. - badge_v=$(grep -oE 'badge/version-[0-9]+\.[0-9]+\.[0-9]+' README.md | head -1 | sed 's|badge/version-||' || true) + # Color-agnostic, and the WHOLE semver: shields.io writes a dash inside + # the value as "--" (version-1.2.0--rc.1-blue renders "1.2.0-rc.1", + # measured), so take every "--" segment after the core and fold it + # back. A prerelease is a legitimate version here - release-drift.yml + # accepts one - and the check must not reject a consistent bump to it. + badge_v=$(grep -oE 'badge/version-[0-9]+\.[0-9]+\.[0-9]+(--[0-9A-Za-z.+]+)*' README.md | head -1 | sed 's|badge/version-||; s|--|-|g' || true) [ "$badge_v" = "$skill_v" ] || { echo "::error::README badge says ${badge_v:-}, $SKILL says $skill_v"; mismatch=1; } pkg_v="" if [ -f package.json ]; then @@ -41,7 +45,7 @@ jobs: [ "$pkg_v" = "$skill_v" ] || { echo "::error::package.json says $pkg_v, $SKILL says $skill_v"; mismatch=1; } fi # Every "Current version: **x.y.z**" line in the README, if any. - for v in $(grep -oE 'Current version: \*\*[0-9]+\.[0-9]+\.[0-9]+\*\*' README.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true); do + for v in $(grep -oE 'Current version: \*\*[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+-]*\*\*' README.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+-]*' || true); do [ "$v" = "$skill_v" ] || { echo "::error::README 'Current version' says $v, $SKILL says $skill_v"; mismatch=1; } done [ "$mismatch" = 0 ] || { echo "::error::bump every declaration in the same commit"; exit 1; } diff --git a/.github/workflows/release-drift.yml b/.github/workflows/release-drift.yml index c6c0051..f04d653 100644 --- a/.github/workflows/release-drift.yml +++ b/.github/workflows/release-drift.yml @@ -124,8 +124,19 @@ jobs: # dispatch runs do not wait - nothing is in flight for them. if [ "$GITHUB_EVENT_NAME" = "push" ]; then for attempt in $(seq 1 20); do - [ "$STATE" = missing ] || break - echo "no v$VERSION release yet (attempt $attempt/20) - waiting 30 s for the release flow" + # Wait through a DRAFT as well as a miss: a release flow that + # creates the release as a draft and publishes it only after its + # ClawHub publish succeeded (cloudways-mcp's release.yml) shows a + # draft for the whole publish, and a draft cut by hand before the + # merge is published moments after it. Both are the interval this + # wait is for. A draft still standing after the window fails + # below, as before. + case "$STATE" in + missing) what="no v$VERSION release yet" ;; + true) what="v$VERSION is still a draft" ;; + *) break ;; + esac + echo "$what (attempt $attempt/20) - waiting 30 s for the release flow" sleep 30 lookup done From 69dad4862635407050bc131854d60b636f405f62 Mon Sep 17 00:00:00 2001 From: Ben Kalsky Date: Tue, 15 Sep 2026 00:30:42 +0300 Subject: [PATCH 3/3] ci: the badge regex keeps build metadata too (Codex round 2) The suffix pattern accepted only segments beginning with the shields "--" (a prerelease), so a consistent bump to 1.2.0+build.7 - build metadata with no prerelease - read as 1.2.0 from the badge and was rejected. After the x.y.z core the regex now takes every character a prerelease or build suffix can carry, "--" included, and stops at the first single dash (the colour separator); "--" is folded back afterwards. Measured on shields.io: version-1.2.0+build.7-blue renders "1.2.0+build.7" and version-1.2.0+build--2-blue "1.2.0+build-2". Verified on synthetic trees at 1.2.0-rc.1, 1.2.0+build.7, 1.2.0-rc.1+build.7, 1.2.0+build-2 and 1.2.0 (accepted), a stale badge and a badge missing the suffix (rejected), and on this tree. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79399a2..689070f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,12 +32,16 @@ jobs: skill_v=$(grep -m1 '^version:' "$SKILL" | sed 's/^version:[[:space:]]*//' | tr -d '"' | tr -d "'" | xargs) [ -n "$skill_v" ] || { echo "::error::no version: in $SKILL"; exit 1; } mismatch=0 - # Color-agnostic, and the WHOLE semver: shields.io writes a dash inside - # the value as "--" (version-1.2.0--rc.1-blue renders "1.2.0-rc.1", - # measured), so take every "--" segment after the core and fold it - # back. A prerelease is a legitimate version here - release-drift.yml - # accepts one - and the check must not reject a consistent bump to it. - badge_v=$(grep -oE 'badge/version-[0-9]+\.[0-9]+\.[0-9]+(--[0-9A-Za-z.+]+)*' README.md | head -1 | sed 's|badge/version-||; s|--|-|g' || true) + # Color-agnostic, and the WHOLE semver: after the x.y.z core take + # every character a prerelease or build-metadata suffix can carry - + # shields.io writes a dash inside the value as "--" and a "+" as + # itself (measured: version-1.2.0--rc.1-blue renders "1.2.0-rc.1", + # version-1.2.0+build.7-blue "1.2.0+build.7", and + # version-1.2.0+build--2-blue "1.2.0+build-2") - and stop at the first + # single dash, which is the colour separator. Then fold "--" back. A + # prerelease is a legitimate version here - release-drift.yml accepts + # one - and the check must not reject a consistent bump to it. + badge_v=$(grep -oE 'badge/version-[0-9]+\.[0-9]+\.[0-9]+(--|[0-9A-Za-z.+])*' README.md | head -1 | sed 's|badge/version-||; s|--|-|g' || true) [ "$badge_v" = "$skill_v" ] || { echo "::error::README badge says ${badge_v:-}, $SKILL says $skill_v"; mismatch=1; } pkg_v="" if [ -f package.json ]; then