This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new 995b5695 [ci] Guard the coding-agent skill's bundled YAML schema 
against drift (#969)
995b5695 is described below

commit 995b56951c170ba30cb089da55d4e1dad8cb2553
Author: Weiqing Yang <[email protected]>
AuthorDate: Sat Aug 8 22:10:45 2026 -0700

    [ci] Guard the coding-agent skill's bundled YAML schema against drift (#969)
---
 .github/workflows/ci.yml              |   3 +
 docs/content/docs/development/yaml.md |   2 +
 tools/check-skill-schema.py           | 123 ++++++++++++++++++++++++++++++++++
 3 files changed, 128 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8af40c19..1bfdbae0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,6 +49,9 @@ jobs:
         run: ./tools/lint.sh -c
       - name: Check AGENTS.md freshness
         run: python3 tools/check-agents-md.py
+      - name: Check bundled YAML schema freshness
+        if: github.ref == 'refs/heads/main' || github.base_ref == 'main'
+        run: python3 tools/check-skill-schema.py
 
   install_sh_tests:
     name: install.sh tests (${{ matrix.os }})
diff --git a/docs/content/docs/development/yaml.md 
b/docs/content/docs/development/yaml.md
index b0521978..7e8f69e2 100644
--- a/docs/content/docs/development/yaml.md
+++ b/docs/content/docs/development/yaml.md
@@ -641,6 +641,8 @@ Because Pydantic models are easier to author and evolve 
than raw JSON Schema, th
 python -m flink_agents.api.yaml.specs > docs/yaml-schema.json
 ```
 
+The coding-agent skill under `dev/agent-skills/` bundles a copy of the schema 
so it can work offline, so refresh that copy in the same change. `python3 
tools/check-skill-schema.py` reports whether it is current and prints the two 
edits needed when it is not.
+
 Continuous tests then verify cross-runtime consistency:
 
 - the JSON Schema exported by the Pydantic specs matches the checked-in 
`docs/yaml-schema.json`;
diff --git a/tools/check-skill-schema.py b/tools/check-skill-schema.py
new file mode 100644
index 00000000..360be864
--- /dev/null
+++ b/tools/check-skill-schema.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+#################################################################################
+"""Assert the coding-agent skill still bundles the current YAML schema.
+
+The skill ships a copy of docs/yaml-schema.json so it can answer YAML questions
+offline, and records in yaml-contracts.yaml the git blob SHA that copy was 
taken
+from. Nothing regenerates either one, so both drift silently the moment the
+schema is re-exported. A stale copy teaches agents a schema the repository no
+longer has, and a stale blob SHA misreports which revision the copy describes.
+
+Only the unversioned "main" contract is checked. The versioned schemas beside 
it
+pin released refs, so they are expected to differ from the working tree.
+
+Regex-only and stdlib-only, hashing through git, so it runs on any Python 3
+without a build step.
+"""
+
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+DOCS_SCHEMA = REPO_ROOT / "docs" / "yaml-schema.json"
+ASSETS = REPO_ROOT / "dev" / "agent-skills" / "flink-agents-dev" / "assets"
+BUNDLED_SCHEMA = ASSETS / "yaml-schema.json"
+MANIFEST = ASSETS / "yaml-contracts.yaml"
+
+
+def blob_sha(path: Path) -> str:
+    """Return the blob SHA git would store for a file.
+
+    Hashing the worktree bytes directly is only correct where no clean filter
+    applies. Under core.autocrlf these JSON files are checked out CRLF, and
+    their hash then never matches the LF blob SHA recorded in the manifest, so
+    the check reports a current pin as stale and prescribes a SHA git would not
+    store. Delegating to git applies whatever filter the path's attributes
+    select.
+    """
+    try:
+        result = subprocess.run(
+            ["git", "hash-object", "--", str(path)],
+            cwd=REPO_ROOT,
+            capture_output=True,
+            text=True,
+        )
+    except FileNotFoundError:
+        sys.exit("error: git is required to hash the schema files, but is not 
on PATH")
+    if result.returncode != 0:
+        sys.exit(
+            f"error: git hash-object failed for {path.relative_to(REPO_ROOT)}: 
"
+            f"{result.stderr.strip()}"
+        )
+    return result.stdout.strip()
+
+
+def recorded_sha(manifest: str) -> str:
+    """Return the blob SHA the manifest's "main" contract was copied from."""
+    # Bounded to the "main" block so a sibling contract's SHA cannot be read by
+    # mistake when the main entry loses its own.
+    block = re.search(r"^  main:\n((?:    .*\n|\n)*)", manifest, re.MULTILINE)
+    if not block:
+        sys.exit(f"error: no 'main' contract in 
{MANIFEST.relative_to(REPO_ROOT)}")
+    match = re.search(r"^\s+blob_sha:\s*([0-9a-f]{40})\s*$", block.group(1), 
re.MULTILINE)
+    if not match:
+        sys.exit(f"error: 'main' contract has no blob_sha in 
{MANIFEST.relative_to(REPO_ROOT)}")
+    return match.group(1)
+
+
+def main() -> int:
+    for path in (DOCS_SCHEMA, BUNDLED_SCHEMA, MANIFEST):
+        if not path.is_file():
+            sys.exit(f"error: {path.relative_to(REPO_ROOT)} does not exist")
+
+    expected = blob_sha(DOCS_SCHEMA)
+    bundled = blob_sha(BUNDLED_SCHEMA)
+    recorded = recorded_sha(MANIFEST.read_text(encoding="utf-8"))
+
+    failures = []
+    if bundled != expected:
+        failures.append(
+            f"{BUNDLED_SCHEMA.relative_to(REPO_ROOT)} is {bundled}, expected 
{expected}"
+        )
+    if recorded != expected:
+        failures.append(
+            f"{MANIFEST.relative_to(REPO_ROOT)} records {recorded}, expected 
{expected}"
+        )
+
+    if failures:
+        print("The bundled YAML schema is out of sync with 
docs/yaml-schema.json:", file=sys.stderr)
+        for failure in failures:
+            print(f"  - {failure}", file=sys.stderr)
+        print(
+            f"\nRefresh both:\n"
+            f"  cp {DOCS_SCHEMA.relative_to(REPO_ROOT)} 
{BUNDLED_SCHEMA.relative_to(REPO_ROOT)}\n"
+            f"  # then set blob_sha: {expected} under the 'main' contract in\n"
+            f"  # {MANIFEST.relative_to(REPO_ROOT)}",
+            file=sys.stderr,
+        )
+        return 1
+
+    print(f"Bundled YAML schema matches docs/yaml-schema.json ({expected}).")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

Reply via email to