Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/spec-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: API Spec Drift

# The `api` skill's references are generated from Deepgram's public OpenAPI and AsyncAPI
# specs, which change without anything in this repo noticing — the checked-in output once
# sat ~5 weeks behind. This job regenerates weekly and reports drift.
#
# REPORT ONLY. It never commits regenerated output: regeneration is a reviewed change
# (`bun run scripts/fetch-specs.ts … && bun run scripts/generate-skills.ts`), and an
# auto-commit would land unreviewed API copy in a skill agents read as reference.
#
# Known limitation: `generate-skills.ts` overwrites the reference files it emits but does
# not prune ones it no longer emits, so a reference file for a *removed* endpoint group
# shows up as no drift here. Additions and edits are detected. This job is deliberately
# tolerant of that — it reports what it can see rather than asserting the tree is clean.

on:
schedule:
# Mondays, 13:00 UTC.
- cron: "0 13 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
check-drift:
name: Check generated api skill against live specs
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install --frozen-lockfile

# Use the .yml URLs. `dpgr.am/openapi.yaml` (.yaml) serves the marketing homepage
# with HTTP 200, and fetch-specs.ts only checks `response.ok`, so a .yaml typo
# writes HTML into specs/ and fails later at parse() with a confusing error.
- name: Fetch live specs
run: bun run scripts/fetch-specs.ts https://dpgr.am/openapi.yml https://dpgr.am/asyncapi.yml

# Cheap guard against the trap above, so a bad fetch reports as a fetch problem.
- name: Verify specs are YAML, not HTML
run: |
for f in specs/openapi.yml specs/asyncapi.yml; do
if head -c 200 "$f" | grep -qi '<!doctype html\|<html'; then
echo "::error::$f looks like HTML, not a spec — check the fetch URL"
exit 1
fi
done

- name: Regenerate api skill references
run: bun run scripts/generate-skills.ts

- name: Report drift
id: drift
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# Stage first, then diff --cached. `git diff` alone ignores untracked files, so
# a brand-new reference file (a whole new API domain — the drift most worth
# knowing about) would report as clean.
git add -A -- skills/api/references

if git diff --cached --quiet -- skills/api/references; then
echo "skills/api/references is up to date with the live specs."
exit 0
fi

echo "Drift detected in skills/api/references:"
git diff --cached --stat -- skills/api/references

{
echo "## API spec drift detected"
echo
echo "\`skills/api/references/\` is behind the live specs."
echo
echo "Regenerate with:"
echo
echo '```bash'
echo 'bun run scripts/fetch-specs.ts https://dpgr.am/openapi.yml https://dpgr.am/asyncapi.yml'
echo 'bun install && bun run scripts/generate-skills.ts'
echo '```'
echo
echo "<details><summary>Diff stat</summary>"
echo
echo '```'
git diff --cached --stat -- skills/api/references
echo '```'
echo
echo "</details>"
echo
echo "[Workflow run]($RUN_URL)"
} > body.md

cat body.md >> "$GITHUB_STEP_SUMMARY"

# One rolling issue rather than a new one every week.
existing=$(gh issue list --state open --label spec-drift --limit 1 --json number --jq '.[0].number')
if [ -n "$existing" ]; then
gh issue comment "$existing" --body-file body.md
echo "Commented on existing issue #$existing"
else
gh label create spec-drift --color FBCA04 --description "Generated api skill is behind the live specs" 2>/dev/null || true
gh issue create --title "API spec drift: skills/api/references is behind the live specs" \
--label spec-drift --body-file body.md
fi

# Fail visibly so the scheduled run is red in the Actions tab.
exit 1
110 changes: 110 additions & 0 deletions .github/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Validate Skills

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: validate-skills-${{ github.ref }}
cancel-in-progress: true

jobs:
# Blocking. Hermetic — no network, so it cannot flake.
validate:
name: Validate skills and manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install --frozen-lockfile

# Parses every skills/*/SKILL.md with yaml@2.x (the parser the `skills` installer
# uses) and diffs marketplace.json's local plugin against the filesystem.
- name: Validate skills
run: bun run scripts/validate-skills.ts

# First-party authoring lint from deepgram/ci-tools (skill_lint.py): description
# quality, inline code, allowed-tools coverage, model/effort/version. Complements the
# job above, which owns parse-correctness and manifest sync.
#
# deepgram/ci-tools is PRIVATE, so the default GITHUB_TOKEN cannot check it out — it
# fails with "Repository not found". Until a read token is provisioned as the
# CI_TOOLS_READ_TOKEN secret, this job reports the gap and passes rather than failing
# every PR on a missing credential. Once the secret exists it lints and blocks, with
# no workflow change needed.
lint-skills:
name: Lint skills (ci-tools)
runs-on: ubuntu-latest
env:
CI_TOOLS_READ_TOKEN: ${{ secrets.CI_TOOLS_READ_TOKEN }}
steps:
- name: Check for a ci-tools read token
id: gate
run: |
if [ -n "$CI_TOOLS_READ_TOKEN" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::warning::Skipping the ci-tools skill lint: deepgram/ci-tools is private and no CI_TOOLS_READ_TOKEN secret is set."
{
echo "## ci-tools skill lint skipped"
echo
echo "\`deepgram/ci-tools\` is a private repository, so the default \`GITHUB_TOKEN\`"
echo "cannot check it out. Add a read-scoped token as the \`CI_TOOLS_READ_TOKEN\`"
echo "repository secret to enable this lint — no workflow change is needed."
echo
echo "The blocking \`Validate skills and manifest\` job is unaffected."
} >> "$GITHUB_STEP_SUMMARY"
fi

- if: steps.gate.outputs.available == 'true'
uses: actions/checkout@v4

- name: Check out ci-tools
if: steps.gate.outputs.available == 'true'
uses: actions/checkout@v4
with:
repository: deepgram/ci-tools
ref: v3
path: .ci-tools
token: ${{ secrets.CI_TOOLS_READ_TOKEN }}

- if: steps.gate.outputs.available == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Lint skills
if: steps.gate.outputs.available == 'true'
working-directory: .ci-tools
run: make lint PROJECT_DIR="$GITHUB_WORKSPACE"

# Non-blocking by design. The SDK plugins' `.agents/skills/*` paths resolve against
# remote github sources, so they cannot be checked from this checkout — this job asks
# the live repos instead. It is `continue-on-error` so a GitHub outage (or an
# in-flight SDK fix) never blocks a PR here; read its log to see what it found.
remote-sdk-skills:
name: Remote SDK plugin skills (report only)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- run: bun install --frozen-lockfile

- name: Check remote SDK plugin skill paths
env:
GH_TOKEN: ${{ github.token }}
run: bun run scripts/validate-skills.ts --remote
Loading
Loading