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())