diff --git a/.github/scripts/tests/test-genai-plugin-handoff.py b/.github/scripts/tests/test-genai-plugin-handoff.py new file mode 100644 index 0000000000..3de10e9aa6 --- /dev/null +++ b/.github/scripts/tests/test-genai-plugin-handoff.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +from pathlib import Path +import subprocess +import tempfile + +import yaml + + +ROOT = Path(__file__).resolve().parents[3] + + +def workflow_steps(path: str, job: str) -> list[dict]: + with (ROOT / path).open(encoding="utf-8") as stream: + workflow = yaml.safe_load(stream) + return workflow["jobs"][job]["steps"] + + +def named_step(steps: list[dict], name: str) -> dict: + for step in steps: + if step.get("name") == name: + return step + raise AssertionError(f"workflow step not found: {name}") + + +def run_script(step: dict, cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + step["run"]], + cwd=cwd, + text=True, + capture_output=True, + check=False, + ) + + +build_steps = workflow_steps(".github/workflows/ci-builds.yml", "builds") +ai_steps = workflow_steps(".github/workflows/ci-ai-gcov.yml", "tests") +stage = named_step(build_steps, "Stage GenAI plugin in test handoff") +restore = named_step(ai_steps, "Restore GenAI plugin from build handoff") +verify = named_step(ai_steps, "Verify binary and GenAI plugin") + +assert "inputs.trusted" in stage["if"] +assert "contains(matrix.type,'-genai')" in stage["if"] + +names = [step.get("name") for step in ai_steps] +assert names.index("Download build handoff") < names.index(restore["name"]) +assert names.index(restore["name"]) < names.index(verify["name"]) +assert names.index(verify["name"]) < names.index("Start infrastructure") + +with tempfile.TemporaryDirectory() as directory: + cwd = Path(directory) + repo = cwd / "proxysql" + source = repo / "plugins/genai/ProxySQL_GenAI_Plugin.so" + source.parent.mkdir(parents=True) + source.write_bytes(b"genai-plugin") + + result = run_script(stage, cwd) + assert result.returncode == 0, result.stdout + result.stderr + staged = repo / "test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + assert staged.read_bytes() == b"genai-plugin" + + source.unlink() + staged.unlink() + result = run_script(stage, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + +with tempfile.TemporaryDirectory() as directory: + cwd = Path(directory) + repo = cwd / "proxysql" + staged = repo / "test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + staged.parent.mkdir(parents=True) + staged.write_bytes(b"genai-plugin") + + result = run_script(restore, cwd) + assert result.returncode == 0, result.stdout + result.stderr + restored = repo / "plugins/genai/ProxySQL_GenAI_Plugin.so" + assert restored.read_bytes() == b"genai-plugin" + + staged.unlink() + restored.unlink() + result = run_script(restore, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + + binary = repo / "src/proxysql" + binary.parent.mkdir(parents=True) + binary.write_bytes(b"#!/bin/sh\nexit 0\n") + binary.chmod(0o755) + restored.parent.mkdir(parents=True, exist_ok=True) + restored.write_bytes(b"genai-plugin") + result = run_script(verify, cwd) + assert result.returncode == 0, result.stdout + result.stderr + + restored.unlink() + result = run_script(verify, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + +print("GenAI plugin handoff contract passed") diff --git a/.github/workflows/ci-ai-gcov.yml b/.github/workflows/ci-ai-gcov.yml index 7b8c5c4464..da5023ee08 100644 --- a/.github/workflows/ci-ai-gcov.yml +++ b/.github/workflows/ci-ai-gcov.yml @@ -118,10 +118,28 @@ jobs: done rm -f ../cache_*.tar.zst ../handoff-*.zip - - name: Verify binary + - name: Restore GenAI plugin from build handoff + run: | + set -euo pipefail + cd proxysql/ + plugin_src="test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + plugin_dest="plugins/genai/ProxySQL_GenAI_Plugin.so" + if [ ! -s "${plugin_src}" ]; then + echo "ERROR: required GenAI plugin missing from build handoff: ${plugin_src}" >&2 + exit 1 + fi + mkdir -p "$(dirname "${plugin_dest}")" + cp "${plugin_src}" "${plugin_dest}" + test -s "${plugin_dest}" + + - name: Verify binary and GenAI plugin run: | chmod +x proxysql/src/proxysql file proxysql/src/proxysql + test -s proxysql/plugins/genai/ProxySQL_GenAI_Plugin.so || { + echo "ERROR: ProxySQL_GenAI_Plugin.so was not restored" >&2 + exit 1 + } - name: Log in to GHCR and pull CI base image env: diff --git a/.github/workflows/ci-builds.yml b/.github/workflows/ci-builds.yml index 5d907e3513..021d907718 100644 --- a/.github/workflows/ci-builds.yml +++ b/.github/workflows/ci-builds.yml @@ -564,6 +564,22 @@ jobs: grep 'exited with code 0' ${LOG} || exit 1 done + - name: Stage GenAI plugin in test handoff + if: ${{ inputs.trusted && success() && steps.cache-check.outputs.cache-hit != 'true' && contains(matrix.type,'-genai') }} + run: | + set -euo pipefail + cd proxysql/ + plugin_src="plugins/genai/ProxySQL_GenAI_Plugin.so" + plugin_dest="test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + if [ ! -s "${plugin_src}" ]; then + echo "ERROR: required GenAI plugin missing after build: ${plugin_src}" >&2 + exit 1 + fi + mkdir -p "$(dirname "${plugin_dest}")" + cp -L "${plugin_src}" "${plugin_dest}" + test -s "${plugin_dest}" + ls -la "$(dirname "${plugin_dest}")" + - name: Pack bin cache with zstd-15 id: cache-pack-bin if: ${{ inputs.trusted && success() && steps.cache-check.outputs.cache-hit != 'true' }} diff --git a/docs/superpowers/plans/2026-08-16-ai-genai-plugin-handoff.md b/docs/superpowers/plans/2026-08-16-ai-genai-plugin-handoff.md new file mode 100644 index 0000000000..b149dcb00b --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-ai-genai-plugin-handoff.md @@ -0,0 +1,281 @@ +# AI GenAI Plugin Build Handoff Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver the already-built GenAI plugin from the central GenAI build to both reusable AI TAP shards. + +**Architecture:** Keep the existing `src` and `test` workflow artifacts. Add one producer step that places the GenAI plugin in the established `test/tap/tap/_runtime_libs/` handoff directory and one shared AI-consumer step that restores it to its canonical workspace path. Exercise the actual workflow shell steps against temporary files in a contract test. + +**Tech Stack:** GitHub Actions YAML, Bash, Python 3, PyYAML. + +## Global Constraints + +- Preserve the existing build matrix and ASAN label selection. +- Preserve `CI-unit-tests-asan-coverage` unchanged. +- Do not add a separate AI build or compile anything in the fan-out jobs. +- Do not change the shared `_src` cache path contract. +- Do not change the isolated test harness's plugin lookup rules. +- A missing GenAI plugin must fail at both the producer and consumer boundaries. + +--- + +### Task 1: Enforce and implement the GenAI plugin handoff + +**Files:** + +- Create: `.github/scripts/tests/test-genai-plugin-handoff.py` +- Modify: `.github/workflows/ci-builds.yml` +- Modify: `.github/workflows/ci-ai-gcov.yml` + +**Interfaces:** + +- Consumes: `plugins/genai/ProxySQL_GenAI_Plugin.so` produced by the `ubuntu24-tap-genai-gcov` central build. +- Produces: `test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so` in the test handoff and restores it as `plugins/genai/ProxySQL_GenAI_Plugin.so` in each AI shard. + +- [ ] **Step 1: Write the failing workflow behavior test** + +Create `.github/scripts/tests/test-genai-plugin-handoff.py`. Load the real workflow steps with PyYAML, run their `run:` scripts against temporary repository trees, and assert their observable file-copy and failure behavior: + +```python +#!/usr/bin/env python3 +from pathlib import Path +import subprocess +import tempfile + +import yaml + + +ROOT = Path(__file__).resolve().parents[3] + + +def workflow_steps(path: str, job: str) -> list[dict]: + with (ROOT / path).open(encoding="utf-8") as stream: + workflow = yaml.safe_load(stream) + return workflow["jobs"][job]["steps"] + + +def named_step(steps: list[dict], name: str) -> dict: + for step in steps: + if step.get("name") == name: + return step + raise AssertionError(f"workflow step not found: {name}") + + +def run_script(step: dict, cwd: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + step["run"]], + cwd=cwd, + text=True, + capture_output=True, + check=False, + ) + + +build_steps = workflow_steps(".github/workflows/ci-builds.yml", "builds") +ai_steps = workflow_steps(".github/workflows/ci-ai-gcov.yml", "tests") +stage = named_step(build_steps, "Stage GenAI plugin in test handoff") +restore = named_step(ai_steps, "Restore GenAI plugin from build handoff") +verify = named_step(ai_steps, "Verify binary and GenAI plugin") + +assert "inputs.trusted" in stage["if"] +assert "contains(matrix.type,'-genai')" in stage["if"] + +names = [step.get("name") for step in ai_steps] +assert names.index("Download build handoff") < names.index(restore["name"]) +assert names.index(restore["name"]) < names.index(verify["name"]) +assert names.index(verify["name"]) < names.index("Start infrastructure") + +with tempfile.TemporaryDirectory() as directory: + cwd = Path(directory) + repo = cwd / "proxysql" + source = repo / "plugins/genai/ProxySQL_GenAI_Plugin.so" + source.parent.mkdir(parents=True) + source.write_bytes(b"genai-plugin") + + result = run_script(stage, cwd) + assert result.returncode == 0, result.stdout + result.stderr + staged = repo / "test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + assert staged.read_bytes() == b"genai-plugin" + + source.unlink() + staged.unlink() + result = run_script(stage, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + +with tempfile.TemporaryDirectory() as directory: + cwd = Path(directory) + repo = cwd / "proxysql" + staged = repo / "test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + staged.parent.mkdir(parents=True) + staged.write_bytes(b"genai-plugin") + + result = run_script(restore, cwd) + assert result.returncode == 0, result.stdout + result.stderr + restored = repo / "plugins/genai/ProxySQL_GenAI_Plugin.so" + assert restored.read_bytes() == b"genai-plugin" + + staged.unlink() + restored.unlink() + result = run_script(restore, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + + binary = repo / "src/proxysql" + binary.parent.mkdir(parents=True) + binary.write_bytes(b"#!/bin/sh\nexit 0\n") + binary.chmod(0o755) + restored.parent.mkdir(parents=True, exist_ok=True) + restored.write_bytes(b"genai-plugin") + result = run_script(verify, cwd) + assert result.returncode == 0, result.stdout + result.stderr + + restored.unlink() + result = run_script(verify, cwd) + assert result.returncode != 0 + assert "ProxySQL_GenAI_Plugin.so" in result.stdout + result.stderr + +print("GenAI plugin handoff contract passed") +``` + +This test catches three production breaks: the central GenAI build omits the plugin, the shared AI consumer does not restore it, or infrastructure can start without an explicit plugin verification. + +- [ ] **Step 2: Run the test and verify RED** + +Run: + +```bash +python3 .github/scripts/tests/test-genai-plugin-handoff.py +``` + +Expected: FAIL with `workflow step not found: Stage GenAI plugin in test handoff` because neither handoff step exists yet. + +- [ ] **Step 3: Add the producer staging step** + +In `.github/workflows/ci-builds.yml`, after `Check build` and before cache/handoff packing, add: + +```yaml + - name: Stage GenAI plugin in test handoff + if: ${{ inputs.trusted && success() && steps.cache-check.outputs.cache-hit != 'true' && contains(matrix.type,'-genai') }} + run: | + set -euo pipefail + cd proxysql/ + plugin_src="plugins/genai/ProxySQL_GenAI_Plugin.so" + plugin_dest="test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + if [ ! -s "${plugin_src}" ]; then + echo "ERROR: required GenAI plugin missing after build: ${plugin_src}" >&2 + exit 1 + fi + mkdir -p "$(dirname "${plugin_dest}")" + cp -L "${plugin_src}" "${plugin_dest}" + test -s "${plugin_dest}" + ls -la "$(dirname "${plugin_dest}")" +``` + +- [ ] **Step 4: Add the shared consumer restore and verification steps** + +In `.github/workflows/ci-ai-gcov.yml`, immediately after `Download build handoff`, add: + +```yaml + - name: Restore GenAI plugin from build handoff + run: | + set -euo pipefail + cd proxysql/ + plugin_src="test/tap/tap/_runtime_libs/ProxySQL_GenAI_Plugin.so" + plugin_dest="plugins/genai/ProxySQL_GenAI_Plugin.so" + if [ ! -s "${plugin_src}" ]; then + echo "ERROR: required GenAI plugin missing from build handoff: ${plugin_src}" >&2 + exit 1 + fi + mkdir -p "$(dirname "${plugin_dest}")" + cp "${plugin_src}" "${plugin_dest}" + test -s "${plugin_dest}" +``` + +Rename `Verify binary` to `Verify binary and GenAI plugin` and extend it to fail explicitly when the restored plugin is missing: + +```yaml + - name: Verify binary and GenAI plugin + run: | + chmod +x proxysql/src/proxysql + file proxysql/src/proxysql + test -s proxysql/plugins/genai/ProxySQL_GenAI_Plugin.so || { + echo "ERROR: ProxySQL_GenAI_Plugin.so was not restored" >&2 + exit 1 + } +``` + +- [ ] **Step 5: Run the behavior test and verify GREEN** + +Run: + +```bash +python3 .github/scripts/tests/test-genai-plugin-handoff.py +``` + +Expected: exit 0 and `GenAI plugin handoff contract passed`. + +- [ ] **Step 6: Run regression validation** + +Run: + +```bash +.github/scripts/tests/test-resolve-tap-build-mode.bash +python3 - <<'PY' +import yaml +for path in ( + ".github/workflows/ci-builds.yml", + ".github/workflows/ci-ai-gcov.yml", +): + with open(path, encoding="utf-8") as stream: + yaml.safe_load(stream) + print(f"parsed {path}") +PY +git diff --check +``` + +Expected: all resolver cases pass, both workflows parse, and `git diff --check` exits 0. + +- [ ] **Step 7: Inspect scope and commit** + +Run: + +```bash +git status --short +git diff -- .github/workflows/ci-builds.yml \ + .github/workflows/ci-ai-gcov.yml \ + .github/scripts/tests/test-genai-plugin-handoff.py +git add .github/workflows/ci-builds.yml \ + .github/workflows/ci-ai-gcov.yml \ + .github/scripts/tests/test-genai-plugin-handoff.py +git commit -m "fix(ci): hand off GenAI plugin to AI shards" +``` + +Expected: only the two reusable workflows and their behavior test are in the implementation commit. + +### Task 2: Publish the `GH-Actions` fix for review + +**Files:** None. + +**Interfaces:** + +- Consumes: the verified `fix/ai-genai-plugin-handoff` branch. +- Produces: a draft pull request targeting `GH-Actions`. + +- [ ] **Step 1: Re-run fresh verification before publishing** + +Run the commands from Task 1, Steps 5 and 6 again and require every command to exit 0. + +- [ ] **Step 2: Push and open the pull request** + +Run: + +```bash +git push -u origin fix/ai-genai-plugin-handoff +gh pr create --draft --base GH-Actions \ + --head fix/ai-genai-plugin-handoff \ + --title "fix(ci): hand off GenAI plugin to AI shards" \ + --body-file /tmp/ai-genai-plugin-handoff-pr.md +``` + +The PR body must explain the missing-plugin root cause, the established `_runtime_libs` producer/consumer fix, local validation, and that PR 6083 requires an empty commit after merge for end-to-end verification. diff --git a/docs/superpowers/specs/2026-08-16-ai-genai-plugin-handoff-design.md b/docs/superpowers/specs/2026-08-16-ai-genai-plugin-handoff-design.md new file mode 100644 index 0000000000..e207d22248 --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-ai-genai-plugin-handoff-design.md @@ -0,0 +1,98 @@ +# AI GenAI Plugin Build Handoff Design + +## Problem + +`CI-ai-g1` and `CI-ai-g2` consume the `ubuntu24-tap-genai-gcov` build +handoff. That build successfully produces +`plugins/genai/ProxySQL_GenAI_Plugin.so`, but `ci-builds.yml` stages plugin +shared objects only for the `-tap-mysqlx` matrix entry. The AI consumer then +unpacks only the `src` and `test` handoffs and starts the isolated harness +without restoring the GenAI plugin. Infrastructure startup fails before any +AI TAP test runs. + +## Scope + +Fix only the reusable build-to-test handoff on the `GH-Actions` branch: + +- Preserve the existing build matrix and ASAN label selection. +- Preserve `CI-unit-tests-asan-coverage` unchanged. +- Do not add a separate AI build or compile anything in the fan-out jobs. +- Do not change the shared `_src` cache path contract. +- Do not change the isolated test harness's plugin lookup rules. + +## Considered Approaches + +### 1. Carry the plugin in the existing test handoff (selected) + +Stage the GenAI plugin under `test/tap/tap/_runtime_libs/` in the central +GenAI build, then restore it to `plugins/genai/` in the reusable AI consumer. +This follows the established MySQLX plugin handoff pattern and keeps the +shared source-cache path list unchanged. + +### 2. Add the plugin directly to the source handoff + +This would place a matrix-specific runtime artifact in a broadly shared +source contract and diverge from the existing plugin convention. It also +risks cache-version mismatches between the producer and numerous consumers. + +### 3. Make the test harness fall back to `_runtime_libs` + +This would couple a general-purpose local/Docker harness to a GitHub Actions +artifact layout and could hide a broken CI handoff until infrastructure +startup. The harness should continue to require the plugin at its canonical +workspace path. + +## Selected Design + +### Producer + +For every `-genai` TAP build, `ci-builds.yml` will: + +1. Create `test/tap/tap/_runtime_libs/`. +2. Require `plugins/genai/ProxySQL_GenAI_Plugin.so` to exist. +3. Copy the dereferenced shared object into `_runtime_libs`. +4. Verify and list the staged file before packing `test/`. + +The copy is mandatory for a GenAI build. It must not use `|| true`, because a +successful build that cannot provide its required runtime plugin is a corrupt +handoff and should fail at the producer boundary. + +The MySQLX-specific staging of `libpq.so.5`, `libre2.so.10`, and +`ProxySQL_MySQLX_Plugin.so` remains unchanged. + +### Consumer + +After `ci-ai-gcov.yml` unpacks the `src` and `test` handoffs, it will: + +1. Create `proxysql/plugins/genai/`. +2. Require the staged GenAI plugin in `_runtime_libs`. +3. Copy it to `proxysql/plugins/genai/ProxySQL_GenAI_Plugin.so`. +4. Verify that both the ProxySQL executable and restored plugin exist before + infrastructure startup. + +Both `CI-ai-g1` and `CI-ai-g2` reuse `ci-ai-gcov.yml`, so one consumer change +fixes both shards. + +## Failure Behavior + +- A GenAI build that does not produce the plugin fails while staging the + handoff, with the plugin path in the error. +- An AI consumer receiving an old or incomplete handoff fails in its explicit + verification step, before Docker setup. +- Non-GenAI TAP builds do not require or stage the GenAI plugin. + +## Verification + +Add a workflow-contract test that initially fails against the current +workflows and asserts: + +- the `-genai` producer stages the GenAI plugin mandatorily into + `_runtime_libs`; +- the AI consumer restores it to `plugins/genai/`; +- the consumer verifies the restored file before infrastructure startup. + +Then run the contract test, the existing TAP-mode resolver test, and YAML +parsing for the changed workflows. The end-to-end verification is a fresh PR +6083 run after this `GH-Actions` change merges; pushing an empty commit after +the merge will cause the central build and both AI shards to consume the new +workflow definitions.