Repository navigation
ci: validate skills, manifest sync, and api spec drift - #13
Conversation
There was no CI validating skills at all, and two classes of bug shipped because of it. A `description:` written as an unquoted YAML scalar containing a colon-space is a YAML parse error, not a lint nit. The `skills` CLI prints one dim "Skipped ... YAML parse error" line and exits 0, so an SDK installs N-1 skills while the manifest advertises N. Separately, marketplace.json drifted from what is actually on disk. Adds three things: - scripts/validate-skills.ts — parses every skills/*/SKILL.md with yaml@2.x (the parser the installer uses, so results match reality), requires name and description, requires name to equal the directory name, and diffs marketplace.json's local plugin against the filesystem in both directions. Non-zero exit on any failure. `--remote` additionally checks the SDK plugins' `.agents/skills/*` paths against the live repos via `gh api`. - .github/workflows/validate-skills.yml — on pull_request and push to main. Two blocking jobs: the validator above, and deepgram/ci-tools `make lint` (skill_lint.py) for authoring quality. The remote SDK check runs as a separate continue-on-error job so a GitHub outage cannot block a PR; its findings go to the run summary rather than log-only. - .github/workflows/spec-drift.yml — weekly. Regenerates the api skill from the live specs and reports drift on a rolling issue. Report only; it never commits regenerated output. ci-tools check-skill covers authoring quality but treats frontmatter as regex-matched text and never reads marketplace.json, so it reports zero errors for both bugs above. check-plugin is not wired: it requires .claude-plugin/plugin.json, and this repo is a marketplace repo.
deepgram/ci-tools is private, so the default GITHUB_TOKEN cannot check it out — actions/checkout fails with "Repository not found" and the job red-walls every PR on a missing credential rather than on a real skill problem. Gate the job on a CI_TOOLS_READ_TOKEN secret. Without it the job reports the gap to the run summary as a warning and passes; with it the job lints and blocks, with no workflow change needed. The blocking validator job is unaffected.
|
Merging last, deliberately, so it validates a consistent tree rather than red-walling the five skill PRs that were still in flight. Verified on the merged tree myself: And I negative-tested it rather than trusting a passing run. Injected the exact production bug — an unquoted Byte-identical to what (My first attempt reported Three findings I especially value here:
The drift job confirms the One thing needs a human with repo-admin: |
Why
.github/workflows/contained exactly one file,context7.yml, which only refreshes Context7 on release publish. Nothing validated skills. Two classes of bug shipped because of that:1. Silent skill loss. A
description:written as an unquoted YAML scalar containing a colon-space is a YAML parse error, not a lint nit. TheskillsCLI prints one dim⚠ Skipped … YAML parse errorline and then exits 0 — wrong count, install proceeds. So an SDK installs 6 of 7 skills whilemarketplace.jsonadvertises 7. Live today indeepgram-js-sdk(deepgram-js-text-intelligence) anddeepgram-rust-sdk(deepgram-rust-voice-agent); fixes open as deepgram-js-sdk#558 and deepgram-rust-sdk#182. Upstreamvercel-labs/skillshas this diagnosed in open issues #1094, #1282 and #1525 for 4+ months, so we cannot wait on them.2. Manifest/reality drift.
marketplace.jsonclaimed each SDK ships adeepgram-{lang}-maintaining-sdkskill that exists in no SDK repo. Nothing caught it.3. Spec staleness. The generated
apiskill sat ~5 weeks behind the live specs with nothing detecting it. It is behind again right now — see the drift section below.What is here
scripts/validate-skills.ts— parses everyskills/*/SKILL.mdwithyaml@2.x, the same parser the installer uses, so results match reality rather than a more lenient parser's opinion. Requiresnameanddescription, requiresnameto equal the directory name, and diffsmarketplace.json's localdeepgramplugin against the filesystem in both directions. Non-zero exit on any failure..github/workflows/validate-skills.yml—pull_request+pushtomain..github/workflows/spec-drift.yml— weekly, report-only.What
ci-toolscovers vs. what this addsci-tools:lint-skills/ thecheck-skillaction (skill_lint.py) is wired in — it covers authoring quality this PR does not duplicate: inline-code extraction,allowed-toolspermission coverage,model/effort/versionfrontmatter, draft/ready status, plugin-cache safety. Run against currentmainit reports 0 errors / 21 warnings / 42 info, so blocking on errors is safe.It needs a credential before it can actually run, and that is the one thing here that needs a decision.
deepgram/ci-toolsis a private repository, so the defaultGITHUB_TOKENcannot check it out. The first push of this branch proved it:Rather than red-wall every PR on a missing credential instead of a real skill problem, the job is gated on a
CI_TOOLS_READ_TOKENsecret: without it the job writes the gap to the run summary as a warning and passes; with it the job lints and blocks, with no workflow change needed. Someone with repo-admin access needs to provision that secret (a read-scoped PAT or a GitHub App installed onci-tools) for this lint to be live. The blocking validator job does not depend on it.It does not catch any of the three bugs above. Verified by injecting them and re-running it:
It reads frontmatter with a regex key-match (
_has_key) rather than a YAML parser, so an installer-fatal parse error is invisible to it; and its manifest cross-check targets.claude-plugin/plugin.json, which this repo does not have.check-pluginis deliberately not wired. It hard-fails here, because this is a marketplace repo, not a single-plugin repo:Adding a
plugin.jsonis a real decision about how this repo is distributed, not a CI change — flagging it rather than doing it.ci-tools:skill-ci-setupwas also evaluated and not adopted: it generates a release workflow (version-stamp → release-notes → commit-back → tag, plus optional catalog-sync), requiresplugin.json, and does no validation this PR needs. Standing up release automation for this repo is worth doing, but it is a separate change with its own blast radius.Proof it works
Passes on current
main:(a) The exact real-world bug — unquoted
descriptioncontaining a colon-spaceSame parser message the installer produces (
Nested mappings are not allowed in compact mappings at line 2, column 14) — that is the point of usingyaml@2.xrather than something more forgiving.(b) Directory renamed so
nameno longer matches(c) Manifest lists a skill that is missing from disk
(d) Skill on disk the manifest does not list
(e)
marketplace.jsonis not valid JSONAll five exit non-zero and name the file. Fixtures were throwaway copies; nothing broken was committed into
skills/.Remote SDK plugin paths
The SDK plugins'
skillsentries (.agents/skills/*) resolve against remote github sources, so they cannot be checked from a local checkout. Rather than skip them,--remoteverifies them against the live repos and, critically, parses their frontmatter too — existence alone would not have caught either production bug.It runs as a separate
continue-on-errorjob, so a GitHub outage or an in-flight SDK fix never blocks a PR in this repo. Run against the live repos it independently rediscovers both known bugs:Because a
continue-on-errorjob green-checks either way and nobody opens a passing job's log, those findings are also written to$GITHUB_STEP_SUMMARYso they are visible on the run page. Shipping another skippable warning would have repeated the original bug.Spec drift
Weekly (Mondays 13:00 UTC) plus
workflow_dispatch. Fetches the specs, regenerates, and compares against the checked-inskills/api/references/. On drift it posts to one rollingspec-driftissue and exits non-zero so the scheduled run is red.Report only — it never commits regenerated output. Regeneration is a reviewed change; an auto-commit would land unreviewed API copy in a skill agents read as reference.
It does not depend on the concurrent
scripts/generate-skills.tsfix. Verified end-to-end against the live specs on today'smain, where it correctly reports real drift:Two notes:
.ymlURLs.dpgr.am/openapi.yaml(.yaml) returns the marketing homepage with HTTP 200, andfetch-specs.tsonly checksresponse.ok, so a.yamltypo writes ~509 KB of HTML intospecs/and fails later atparse()with a confusing error. There is an explicit HTML sniff step so a bad fetch reports as a fetch problem.Gates
Observed on this PR (run 35344542309):
Validate skills and manifestpassed in 6s, and the remote job found both known SDK bugs in 23s and green-checked as designed.actionlint(which includes shellcheck onrun:blocks) is clean across all three workflows:Scope
Only three new files. No changes to
README.md,.claude-plugin/marketplace.json,AGENTS.md,CHANGELOG.md,CONTRIBUTING.md,scripts/generate-skills.ts,scripts/fetch-specs.ts, or anyskills/**file. Nopackage.jsonchange was needed —yamlis already a dependency, and the validator is invoked by path to match thebun run scripts/…convention the README, AGENTS.md and CONTRIBUTING.md already document.Three things flagged rather than changed: the missing
CI_TOOLS_READ_TOKENsecret (gates the ci-tools lint), the missing.claude-plugin/plugin.json(blockscheck-plugin), and the generator's missing prune pass (limits the drift check).