From 1ef2abfb1e6039acb8078ebeeabdff7ac219d6b2 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 09:38:21 +0200 Subject: [PATCH 1/9] Cover packages/js/src/ai-*/** under the ai area The Content Planner JS implementation (packages/js/src/ai-content-planner/**) fell through every area's source_paths, so the RC docs-sync agent flagged it as a coverage gap in four consecutive runs (27.6-RC4 / RC5 / RC7 / RC8). Add the per-feature `packages/js/src/ai-*/**` pattern to the `ai` area for both Yoast SEO and Yoast SEO Premium so frontend AI work is attributed to the right area instead of repeatedly re-flagged. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENT_MAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENT_MAP.md b/AGENT_MAP.md index 8d401ac1..a5ad6a4f 100644 --- a/AGENT_MAP.md +++ b/AGENT_MAP.md @@ -173,8 +173,8 @@ No currently-listed product has more than one source repo. If one is ever added ### `ai` - **Products**: wordpress-seo, wordpress-seo-premium - **Docs paths**: `docs/features/ai/**` -- **Source paths** (wordpress-seo): `src/ai/**` (current convention — all new AI feature code lives here), `src/ai-*/**` (legacy, being migrated under `src/ai/`; safe to drop once the migration completes), `src/generators/ai*`, `src/integrations/ai*` -- **Source paths** (wordpress-seo-premium): `src/ai/**` +- **Source paths** (wordpress-seo): `src/ai/**` (current convention — all new AI feature code lives here), `src/ai-*/**` (legacy, being migrated under `src/ai/`; safe to drop once the migration completes), `src/generators/ai*`, `src/integrations/ai*`, `packages/js/src/ai-*/**` (frontend code for AI features lives in per-feature `ai-/` directories under the JS package; e.g. `ai-content-planner/`) +- **Source paths** (wordpress-seo-premium): `src/ai/**`, `packages/js/src/ai-*/**` - **Symbol namespaces**: `wpseo_ai_*` - **Typical triggers**: new AI error code; new AI feature exposing a filter; change to request/retry behavior documented in `ai-errors.md`. From 8b9088558339a76a444af16a3a3ccf975d619ac1 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 09:38:55 +0200 Subject: [PATCH 2/9] Strip per-RC version-string bumps from rc.diff.filtered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every recent zero-PR run-summary called out "Version bumps in package.json, wp-seo-main.php, wp-seo.php — noise" — the agent was reading those hunks each RC just to dismiss them. Apply `-I` to the filtered diff so hunks containing *only* version-string changes are dropped at diff-compute time: -I' \* Version: ' # PHP file header (" * Version: 27.6-RC8") -I'WPSEO_VERSION' # define( 'WPSEO_VERSION', '...' ); -I'"version":' # package.json / composer.json -I'CURRENT_RELEASE' -I'MINIMUM_SUPPORTED' Hunks that mix a version line with anything else are preserved (semantics of `-I` require *all* changed lines in a hunk to match). `rc.diff.full` keeps the bumps as a cross-check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index 9288e6a9..cb8f3bf7 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -275,7 +275,18 @@ jobs: rb="${bundle_dir}/${name}" mkdir -p "$rb" git -C "sources/${name}" diff "${{ matrix.item.prev_release }}..${{ matrix.item.rc_tag }}" > "${rb}/rc.diff.full" + # `-I` drops hunks whose *every* changed line matches at least one + # of the regexes. Used here to strip the per-RC version-string bumps + # (Version: header, WPSEO_VERSION define, "version": in package.json, + # CURRENT_RELEASE / MINIMUM_SUPPORTED constants) so the agent doesn't have + # to read them and dismiss them as noise each run. `rc.diff.full` keeps + # them as a cross-check. git -C "sources/${name}" diff "${{ matrix.item.prev_release }}..${{ matrix.item.rc_tag }}" \ + -I' \* Version: ' \ + -I'WPSEO_VERSION' \ + -I'"version":' \ + -I'CURRENT_RELEASE' \ + -I'MINIMUM_SUPPORTED' \ -- \ ':(exclude)tests' \ ':(exclude)**/__tests__' \ From ad6d745f6107a97c5153b1bfbd1f55abd696b6ac Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 09:39:13 +0200 Subject: [PATCH 3/9] Add pre-agent fast-path when no new public surface is added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five of the six successful zero-PR runs in the 27.6 cycle so far burned an average of ~40 turns and ~$1/run for the agent to conclude that an incremental RC contains no new public symbols, REST routes, or CLI commands. Detect that case deterministically before invoking Claude. After the bundle is built, scan `rc.diff.filtered` for *added* lines matching: - `register_rest_route(...)` - `WP_CLI::add_command(...)` - `apply_filters('', ...)` / `do_action('', ...)` where `` is not already in `symbol-index.txt` (i.e., not already documented). If none of these hit, the workflow posts the run-summary comment itself (with the rc-docs-sync:v1 marker, diff size, symbol-index size, and the top-changed file list for human spot-check) and skips the `anthropics/claude-code-action` step entirely. The safety-net marker step is also gated on the new condition so it only fires when the agent was actually supposed to run. Validated against the 27.6 cycle's real diffs: - RC2→RC3, RC3→RC4, RC4→RC5, RC6→RC7 → has_public_surface=false (fast-path) - RC7→RC8 → has_public_surface=true (new internal-classified route — agent still needed to apply the Step 1.6 internal-vs-public discrimination) - 27.5→27.6-RC2 → has_public_surface=true (2 new routes + 1 new hook symbol) Risk: misses behavior-only changes that don't introduce new symbols. The agent prompt already flags those as uncertain "Needs human verification" cases and they haven't been observed in any zero-PR run so far. Mitigation: the fast-path comment includes the "Top changed files" list so a maintainer sweeping the tracking issue can spot anomalies without reading the diff. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 151 ++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index cb8f3bf7..0dc10651 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -336,8 +336,155 @@ jobs: Filtered diff is empty (only tests/translations/lockfiles changed). Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - name: Invoke Claude agent + # --------------------------------------------------------------------- + # Pre-agent fast-path: when the filtered diff is non-empty but contains + # no new public surface (no new register_rest_route / WP_CLI::add_command + # / apply_filters / do_action calls referencing undocumented symbols), + # post a "no doc changes" marker and skip the Claude agent invocation + # entirely. Catches the common case where a small RC contains only + # internal refactors / JS-only changes / version bumps. + # + # Risk: misses behavior-only changes that don't introduce new symbols. + # The agent prompt flags these as uncertain anyway; if this becomes a + # missed-doc source, tighten the heuristic or invoke the agent on a + # cheaper model as a spot-check. + # --------------------------------------------------------------------- + - name: Detect new public surface in filtered diff + id: detect if: steps.bundle.outputs.any_content == 'true' + env: + PRODUCT: ${{ matrix.item.product }} + RC_TAG: ${{ matrix.item.rc_tag }} + DISPLAY_NAME: ${{ matrix.item.display_name }} + BUNDLE_DIR: ${{ github.workspace }}/${{ steps.bundle.outputs.bundle_dir }} + PREV_RELEASE: ${{ matrix.item.prev_release }} + PREV_KIND: ${{ matrix.item.prev_kind }} + WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + python3 - <<'PY' + import glob, os, re, sys + + bundle_dir = os.environ['BUNDLE_DIR'] + rc_tag = os.environ['RC_TAG'] + display_name = os.environ['DISPLAY_NAME'] + prev_release = os.environ['PREV_RELEASE'] + prev_kind = os.environ['PREV_KIND'] + product = os.environ['PRODUCT'] + run_url = os.environ['WORKFLOW_RUN_URL'] + + # symbol-index.txt entries are quoted (e.g. 'wpseo_foo'); strip quotes. + symbol_index = set() + si_path = os.path.join(bundle_dir, "symbol-index.txt") + if os.path.exists(si_path): + with open(si_path) as f: + for line in f: + sym = line.strip().strip("'\"") + if sym: + symbol_index.add(sym) + + HOOK_RE = re.compile(r"(?:apply_filters|do_action)\s*\(\s*['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]") + ROUTE_RE = re.compile(r"register_rest_route\s*\(") + CLI_RE = re.compile(r"WP_CLI::add_command\s*\(") + + new_routes, new_cli, new_hook_symbols = [], [], set() + diff_files = sorted(glob.glob(os.path.join(bundle_dir, "*", "rc.diff.filtered"))) + for path in diff_files: + with open(path) as f: + for line in f: + if not line.startswith('+') or line.startswith('+++'): + continue + body = line[1:] + if ROUTE_RE.search(body): + new_routes.append(body.strip()) + if CLI_RE.search(body): + new_cli.append(body.strip()) + for m in HOOK_RE.finditer(body): + sym = m.group(1) + if sym not in symbol_index: + new_hook_symbols.add(sym) + + has_public = bool(new_routes or new_cli or new_hook_symbols) + with open(os.environ['GITHUB_OUTPUT'], 'a') as gho: + gho.write(f"has_public_surface={'true' if has_public else 'false'}\n") + + if has_public: + print("Public surface detected — agent will be invoked.") + if new_routes: + print(f" new register_rest_route lines: {len(new_routes)}") + for l in new_routes[:5]: print(f" {l}") + if new_cli: + print(f" new WP_CLI::add_command lines: {len(new_cli)}") + for l in new_cli[:5]: print(f" {l}") + if new_hook_symbols: + print(f" new (undocumented) hook symbols: {sorted(new_hook_symbols)}") + sys.exit(0) + + # No new public surface — assemble the fast-path comment body. + filtered_total = 0 + stat_entries = [] + STAT_RE = re.compile(r"^\s*(\S.*?)\s*\|\s*(\d+)") + for path in diff_files: + with open(path) as f: + filtered_total += sum(1 for _ in f) + repo_name = os.path.basename(os.path.dirname(path)) + stat_path = os.path.join(bundle_dir, repo_name, "rc.diff.stat") + if os.path.exists(stat_path): + with open(stat_path) as f: + for line in f: + m = STAT_RE.match(line) + if m: + stat_entries.append((m.group(1), int(m.group(2)))) + top = sorted(stat_entries, key=lambda p: -p[1])[:8] + + base_version = re.sub(r"-RC\d+$", "", rc_tag) + prev_desc = "incremental RC delta" if prev_kind == "rc" else "full release cycle vs. stable" + + body = [ + f"", + f"## {display_name} {base_version}", + "", + f"- **RC tag**: `{rc_tag}`", + f"- **Previous release**: `{prev_release}` ({prev_desc})", + f"- **Filtered diff size**: {filtered_total} lines", + f"- **Symbol index**: {len(symbol_index)} symbols currently documented; **0 new public symbols** in this diff", + "", + "### Outcome: 0 PRs opened (fast-path)", + "", + "The filtered diff contains no new `apply_filters` / `do_action` calls referencing undocumented symbols, no `register_rest_route(...)` registrations, and no `WP_CLI::add_command(...)` calls. Per the deterministic pre-agent fast-path, this RC introduces no public API surface and the Claude agent was not invoked.", + "", + ] + if top: + body.append("
Top changed files") + body.append("") + body.append("```") + for path, count in top: + body.append(f" {path} | {count}") + body.append("```") + body.append("") + body.append("
") + body.append("") + body.append(f"_Fast-path decision — Claude agent skipped. Workflow run: {run_url}_") + + out_path = os.path.join(bundle_dir, "fast-path-comment.md") + with open(out_path, "w") as f: + f.write("\n".join(body) + "\n") + print(f"No public surface detected — fast-path comment written to {out_path}") + print(f" filtered_total={filtered_total}, symbols_known={len(symbol_index)}") + PY + + - name: Post fast-path marker (no new public surface) + if: steps.bundle.outputs.any_content == 'true' && steps.detect.outputs.has_public_surface == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + BUNDLE_DIR: ${{ github.workspace }}/${{ steps.bundle.outputs.bundle_dir }} + run: | + set -euo pipefail + gh issue comment '${{ matrix.item.tracking_issue }}' --body-file "${BUNDLE_DIR}/fast-path-comment.md" + + - name: Invoke Claude agent + if: steps.bundle.outputs.any_content == 'true' && steps.detect.outputs.has_public_surface == 'true' uses: anthropics/claude-code-action@v1 env: PRODUCT: ${{ matrix.item.product }} @@ -397,7 +544,7 @@ jobs: --model claude-sonnet-4-6 - name: Ensure marker comment exists (safety net) - if: always() && steps.bundle.outputs.any_content == 'true' + if: always() && steps.bundle.outputs.any_content == 'true' && steps.detect.outputs.has_public_surface == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} From bde2486b4edfa8d9bdc43b6c4915fab4ac21e551 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 10:00:35 +0200 Subject: [PATCH 4/9] Also strip "pluginVersion" hunks (package.json yoast block) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local test against wordpress-seo at 27.6-RC2..RC3 showed the package.json "pluginVersion" change slipped through the version-bump filter — the "version" regex doesn't match because pluginVersion lives in the "yoast" block, separate from npm's top-level "version" field. Add an explicit '"pluginVersion":' regex so the package.json hunk is also dropped. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index 0dc10651..53b35794 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -277,14 +277,15 @@ jobs: git -C "sources/${name}" diff "${{ matrix.item.prev_release }}..${{ matrix.item.rc_tag }}" > "${rb}/rc.diff.full" # `-I` drops hunks whose *every* changed line matches at least one # of the regexes. Used here to strip the per-RC version-string bumps - # (Version: header, WPSEO_VERSION define, "version": in package.json, - # CURRENT_RELEASE / MINIMUM_SUPPORTED constants) so the agent doesn't have - # to read them and dismiss them as noise each run. `rc.diff.full` keeps - # them as a cross-check. + # (PHP file header, WPSEO_VERSION define, package.json's "version" and + # "pluginVersion" fields, CURRENT_RELEASE / MINIMUM_SUPPORTED constants) + # so the agent doesn't have to read them and dismiss them as noise each + # run. `rc.diff.full` keeps them as a cross-check. git -C "sources/${name}" diff "${{ matrix.item.prev_release }}..${{ matrix.item.rc_tag }}" \ -I' \* Version: ' \ -I'WPSEO_VERSION' \ -I'"version":' \ + -I'"pluginVersion":' \ -I'CURRENT_RELEASE' \ -I'MINIMUM_SUPPORTED' \ -- \ From 22c464f45250b34cfb760aa926186bbd3f487d79 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 11:01:23 +0200 Subject: [PATCH 5/9] Exclude .test/.spec/.stories files anywhere in the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing :(exclude)tests pathspec only matches a top-level tests/ dir, so packages/js/tests/**/*.test.js leaked into the filtered diff. The agent prompt and AGENT_MAP both list filename-suffix patterns (*.test.*, *.spec.*, *.stories.*) in the noise-exclusion convention; mirror that in the workflow. Local test against the 27.6 cycle's diffs: - RC2→RC3 filtered: 141 → 62 lines (56% smaller) - RC3→RC4 filtered: 698 → 467 lines (33% smaller) - RC4→RC5 filtered: 1804 → 1042 lines (42% smaller) - RC6→RC7 filtered: 927 → 376 lines (59% smaller) - RC7→RC8 filtered: 918 → 643 lines (30% smaller) All has_public_surface decisions unchanged (verified: 4 fast-paths and 5 agent-invocations still classified the same way), confirming the exclusion doesn't change semantics, only trims noise the agent had to read. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index 53b35794..ca329f72 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -292,6 +292,9 @@ jobs: ':(exclude)tests' \ ':(exclude)**/__tests__' \ ':(exclude)**/__snapshots__' \ + ':(exclude)**/*.test.*' \ + ':(exclude)**/*.spec.*' \ + ':(exclude)**/*.stories.*' \ ':(exclude)**/*.lock' \ ':(exclude)languages' \ ':(exclude).github' \ From dfa1998f8423603b9debfabb43b66d26a0611050 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 11:05:13 +0200 Subject: [PATCH 6/9] Fix pathspec semantics: exclude directory *contents*, add spec/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues in the existing diff-bundle exclusions: 1. ':(exclude)**/__tests__' and ':(exclude)**/__snapshots__' only match the directory entry itself, not files inside it, because git pathspec globs at non-root depth need a trailing '/**'. Latent bug (no __tests__ in any 27.6-cycle diff, so unobserved) but now fixed. 2. The yoastseo JS package puts its specs under packages/yoastseo/spec/, which wasn't excluded. A single sample-vocabulary.json fixture inside it contributes 46,806 of the ~60,000 lines in a stable→RC filtered diff — the agent reads it just to ignore it. Local test against 27.5 → 27.6-RC1 (which has the big spec fixture): Before: 59,777 filtered lines (62k full) After: 6,775 filtered lines (89% smaller) All 9 has_public_surface decisions across the 27.6 cycle remain identical (verified before/after). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index ca329f72..dfba5907 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -290,8 +290,9 @@ jobs: -I'MINIMUM_SUPPORTED' \ -- \ ':(exclude)tests' \ - ':(exclude)**/__tests__' \ - ':(exclude)**/__snapshots__' \ + ':(exclude)**/__tests__/**' \ + ':(exclude)**/__snapshots__/**' \ + ':(exclude)**/spec/**' \ ':(exclude)**/*.test.*' \ ':(exclude)**/*.spec.*' \ ':(exclude)**/*.stories.*' \ From 4ee4d6e3ef75b0e0d45d940bc93cf01c5e1d9cb6 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 11:08:17 +0200 Subject: [PATCH 7/9] Widen safety-net to fire whenever the bundle is non-empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier fast-path commit narrowed the safety-net step's condition to 'has_public_surface == true', meaning the step would not fire if the new detect step itself crashed (output unset != 'true'). That regressed the "no marker is ever missing" invariant the safety net exists to preserve. The step body already deduplicates by checking for an existing rc-docs-sync:v1 marker on the tracking issue and exiting early if found. So the safest condition is just 'any_content == true' — the step then runs after every "agent-or-fast-path-was-supposed-to-handle-this" case and is a no-op when something earlier did post a marker. Failure modes now covered: fast-path step crash, detect step crash, agent step OOM, all return-to-the-net. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index dfba5907..d19421b5 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -548,8 +548,13 @@ jobs: --max-turns 100 --model claude-sonnet-4-6 + # Safety net runs whenever the bundle was non-empty (any_content == 'true'). + # The step body deduplicates: if any earlier step already posted the marker + # (the agent itself, or the fast-path marker step), it exits early. This + # also catches the edge case where the detect step crashes — the step body + # then sees no marker exists and posts the fallback. - name: Ensure marker comment exists (safety net) - if: always() && steps.bundle.outputs.any_content == 'true' && steps.detect.outputs.has_public_surface == 'true' + if: always() && steps.bundle.outputs.any_content == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} From 702483b13c9a79dc55e6f3bf5dd45ff78bb91b1f Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 11:10:31 +0200 Subject: [PATCH 8/9] Run RC docs sync on weekdays only Saturday and Sunday runs almost always find nothing to process (RCs aren't typically cut over the weekend) and just add zero-PR noise to the tracking issue and run history. Restrict the schedule to Mon-Fri 06:00 UTC. Trade-off: an RC tagged late Friday won't be picked up until Monday morning (up to ~3 days delay). If a weekend tag needs faster turnaround, the workflow_dispatch path still works. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index d19421b5..ee388dfa 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -1,7 +1,7 @@ # .github/workflows/rc-docs-sync.yml # # Lives in: Yoast/developer -# Purpose: once a day, check each opted-in product repo for RC tags we haven't +# Purpose: every weekday, check each opted-in product repo for RC tags we haven't # processed yet. For each new RC, ask a Claude agent whether the # developer-portal docs need updates; if so, open one PR per affected # feature area (per AGENT_MAP.md). @@ -25,7 +25,7 @@ name: RC docs sync on: schedule: - - cron: '0 6 * * *' # daily at 06:00 UTC + - cron: '0 6 * * 1-5' # 06:00 UTC, Monday through Friday only workflow_dispatch: inputs: product: From 77fea3a87c6f89f2d7efd99628d688820c144cd8 Mon Sep 17 00:00:00 2001 From: Enrico Battocchi Date: Mon, 11 May 2026 11:17:56 +0200 Subject: [PATCH 9/9] Make marker-posting steps idempotent (skip if marker already exists) Both "Post no-op summary if filtered diff is empty" and "Post fast-path marker" steps unconditionally posted a comment to the tracking issue. That made manual backfills via workflow_dispatch destructive: re-running for an already-processed RC produced a duplicate marker comment. Mirror the safety-net's dedup pattern in both steps: check whether a marker for this (product, rc_tag) already exists on the tracking issue and exit cleanly if so. Now workflow_dispatch backfills are safe to re-run, including as a smoke test of this branch's code against a real past RC. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/rc-docs-sync.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-docs-sync.yml b/.github/workflows/rc-docs-sync.yml index ee388dfa..fd5eb27e 100644 --- a/.github/workflows/rc-docs-sync.yml +++ b/.github/workflows/rc-docs-sync.yml @@ -333,9 +333,17 @@ jobs: GH_REPO: ${{ github.repository }} run: | set -euo pipefail + issue='${{ matrix.item.tracking_issue }}' + product='${{ matrix.item.product }}' rc_tag='${{ matrix.item.rc_tag }}' base_version="${rc_tag%-RC*}" - gh issue comment '${{ matrix.item.tracking_issue }}' --body " + # Idempotency: skip if a marker for this (product, rc_tag) already exists. + if gh issue view "$issue" --json comments --jq '.comments[].body' \ + | grep -Eq ""; then + echo "Marker for ${product} ${rc_tag} already on issue #${issue}; skipping no-op summary." + exit 0 + fi + gh issue comment "$issue" --body " **${{ matrix.item.display_name }} ${base_version}** (RC \`${rc_tag}\`) — no doc changes needed. Filtered diff is empty (only tests/translations/lockfiles changed). @@ -486,7 +494,19 @@ jobs: BUNDLE_DIR: ${{ github.workspace }}/${{ steps.bundle.outputs.bundle_dir }} run: | set -euo pipefail - gh issue comment '${{ matrix.item.tracking_issue }}' --body-file "${BUNDLE_DIR}/fast-path-comment.md" + issue='${{ matrix.item.tracking_issue }}' + product='${{ matrix.item.product }}' + rc_tag='${{ matrix.item.rc_tag }}' + # Idempotency: if a marker for this (product, rc_tag) already exists on the + # tracking issue (e.g. from a prior scheduled run, or a workflow_dispatch + # backfill), don't post a duplicate. Same dedup pattern as the safety-net + # step below. + if gh issue view "$issue" --json comments --jq '.comments[].body' \ + | grep -Eq ""; then + echo "Marker for ${product} ${rc_tag} already on issue #${issue}; skipping fast-path comment." + exit 0 + fi + gh issue comment "$issue" --body-file "${BUNDLE_DIR}/fast-path-comment.md" - name: Invoke Claude agent if: steps.bundle.outputs.any_content == 'true' && steps.detect.outputs.has_public_surface == 'true'