Skip to content

[ci] Guard the coding-agent skill's bundled YAML schema against drift - #969

Merged
wenjin272 merged 2 commits into
apache:mainfrom
weiqingy:d6-skill-schema-drift-guard
Aug 9, 2026
Merged

[ci] Guard the coding-agent skill's bundled YAML schema against drift#969
wenjin272 merged 2 commits into
apache:mainfrom
weiqingy:d6-skill-schema-drift-guard

Conversation

@weiqingy

@weiqingy weiqingy commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Linked issue: #968

Purpose of change

The flink-agents-dev skill ships a copy of docs/yaml-schema.json at dev/agent-skills/flink-agents-dev/assets/yaml-schema.json, and records the blob SHA it was taken from in assets/yaml-contracts.yaml. Nothing regenerates either one, and the two existing schema checks (test_specs.py, SchemaParityTest) stop at docs/yaml-schema.json, so re-exporting the schema leaves both stale with no failing test.

The stale copy teaches agents a schema the repository no longer has. The stale blob_sha is the quieter half: SKILL.md and references/local-development.md both instruct agents to trust the bundled schema only when it describes the same revision as the checkout, and that comparison is decided by exactly this value.

tools/check-skill-schema.py asserts the bundled copy and the recorded SHA both match docs/yaml-schema.json. It runs from the existing lint job next to Check AGENTS.md freshness, scoped to main pushes and PRs targeting main since the bundled skill only exists on main. Its Python side is regex-only and stdlib-only for the same reason check-agents-md.py is: that job has no Python environment beyond the interpreter.

The SHA itself comes from git hash-object rather than from hashing the file's bytes, so it goes through git's clean filters. A worktree-content hash is only the blob SHA where no filter applies: under core.autocrlf these JSON files are checked out CRLF, and the check would then report a current pin as stale and prescribe a SHA that the Linux CI rejects.

The versioned schemas beside it (assets/yaml-schemas/release-0.3.0.json) pin released refs and are expected to differ, so only the unversioned main contract is checked.

docs/content/docs/development/yaml.md documents the regeneration command, so it now also says to refresh the bundled copy. Without that, following the documented steps literally leaves CI red.

Tests

No unit test. The check is itself the test, so it was verified by mutation instead: each pin was desynced independently and the check confirmed red, then restored.

Mutation Expected Result
unmodified tree green green
docs/yaml-schema.json edited, both copies left behind red, 2 failures red, both reported
bundled copy edited alone red, 1 failure red
blob_sha edited alone red, 1 failure red
main contract loses its blob_sha, 0.3.0 keeps one red, must not read the sibling SHA red, 'main' contract has no blob_sha
bundled copy deleted red red, names the missing path
release-0.3.0.json edited green, out of scope green

The fifth row is why the manifest is read with a bounded match rather than a document-wide search for blob_sha. An unbounded search falls through to the 0.3.0 contract's SHA and the guard passes on a manifest that has lost the value it is supposed to check.

The hashing path was checked separately in a scratch repository with core.autocrlf=true, committing LF content and checking it back out: the byte hash and the blob SHA diverge there, and only git hash-object returns the committed blob. All seven rows above were re-run afterwards and are unchanged. Two further cases were covered by hand, since the check now depends on git being present: run outside a git repository it still reports the correct SHA, and with git off PATH it exits with a plain message rather than a traceback.

Also run locally: tools/check-license.sh (RAT passed), python3 tools/check-agents-md.py.

API

No public API change. New file is a repository tooling script.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

The flink-agents-dev skill ships a copy of docs/yaml-schema.json and records
the blob SHA it was taken from in yaml-contracts.yaml. Nothing regenerates
either, so both go stale the moment the schema is re-exported: agents are then
taught a schema the repository no longer has, under a SHA that misreports which
revision it describes.

Add tools/check-skill-schema.py, run from the existing lint job, asserting the
bundled copy and the recorded SHA both match docs/yaml-schema.json. The
versioned schemas beside it pin released refs and are deliberately excluded.
@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Aug 5, 2026
Comment thread .github/workflows/ci.yml
- name: Check AGENTS.md freshness
run: python3 tools/check-agents-md.py
- name: Check bundled YAML schema freshness
run: python3 tools/check-skill-schema.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow also runs for release-* branches, but the new step always validates the contract whose source.ref is main. If a schema change is backported to a release branch, the check either leaves that branch's CI red or asks us to record the release branch's blob SHA as main provenance. This is a realistic path: release-0.3 has already received schema-changing backports. Could we restrict this step to main pushes and PRs targeting main, or make contract selection branch-aware?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Scoped it to main: if: github.ref == 'refs/heads/main' || github.base_ref == 'main'.

One thing I found while checking, in case it changes your read. Release branches carry their own ci.yml, and this step only exists on main, so nothing runs there today. release-0.3 doesn't even have the older Check AGENTS.md freshness step. The skill directory isn't on release-0.3 at all either, so a backported step would just fail with does not exist.

Your premise still holds though. Those backports are real (0c8da869 and 0bb16280 both touch docs/yaml-schema.json on release-0.3), so this would bite as soon as someone syncs the workflow across branches. Seemed worth guarding either way.

I skipped the branch-aware option since the skill only ships one unversioned schema, so there's nothing to select between. Is that what you had in mind, or were you picturing the manifest carrying per-branch entries at some point?

Comment thread tools/check-skill-schema.py Outdated
def blob_sha(path: Path) -> str:
"""Return the git blob SHA of a file, matching `git hash-object <path>`."""
data = path.read_bytes()
return hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hashing read_bytes() gives a worktree-content hash, which is not always the Git blob SHA. With core.autocrlf=true and no .gitattributes, these JSON files are checked out as CRLF, so this reports the recorded LF blob SHA as stale and suggests a SHA that Linux CI will reject. Could we hash through Git's clean filters (for example, git hash-object --stdin --path=docs/yaml-schema.json) or enforce LF for these files?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, thanks. Reproduced it in a scratch repo with core.autocrlf=true: the byte hash comes out 4ca505... while the real blob is 8d6b85..., so the check calls a good pin stale and then hands you a SHA that CI rejects.

blob_sha() now runs git hash-object -- <path>, which applies whatever clean filter the path's attributes select. Nothing moves on an LF checkout, both files still hash to 183cc7ac... and the mutation cases behave the same.

I went with hashing through git rather than a .gitattributes pin, mostly because it covers any filter and not just line endings, and it keeps line-ending policy out of this PR. Would you want the .gitattributes too, or is going through git enough on its own?

…main

Hashing the worktree bytes is only the git blob SHA where no clean filter
applies. Under core.autocrlf the schema files are checked out CRLF, so the
recorded LF SHA read as stale and the printed remediation prescribed a SHA git
would never store. blob_sha() now delegates to git hash-object, which applies
whatever filter the path's attributes select.

The bundled skill exists only on main, so the workflow step is now scoped to
main pushes and PRs targeting main rather than running wherever the workflow
triggers.
@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. and removed doc-included Your PR already contains the necessary documentation updates. labels Aug 8, 2026

@wenjin272 wenjin272 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing my comments. LGTM.

@wenjin272
wenjin272 merged commit 995b569 into apache:main Aug 9, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants