Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
184 changes: 184 additions & 0 deletions .github/workflows/sync-docs-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Sync PostHog Docs Skills

# Triggered by posthog.com after a successful deploy, nightly as a fallback,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

YES

# or manually. Fetches posthog.com/llms.txt, regenerates the posthog-docs
# skill directories and docs-skill-menu.json, and cuts a new release if anything changed

on:
repository_dispatch:
Comment thread
sarahxsanders marked this conversation as resolved.
Outdated
types: [posthog-docs-deployed]
schedule:
- cron: '0 2 * * *'
workflow_dispatch:

# One sync at a time — if posthog.com deploys rapidly, cancel the queued run
# and let the latest trigger win.
concurrency:
group: sync-docs-skills
cancel-in-progress: true

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # Full history needed for version tag lookup

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 'lts/*'

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 9

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Determine next version
Comment thread
sarahxsanders marked this conversation as resolved.
Outdated
id: version
run: |
LATEST_TAG=$(git tag -l | grep -v '^latest$' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v0.0.0"
fi
echo "Latest semver tag: ${LATEST_TAG}"
LATEST_VERSION=${LATEST_TAG#v}
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_VERSION"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
PATCH=$((PATCH + 1))
VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
echo "Next version: ${VERSION}"

- name: Build docs skills
run: pnpm run build:docs-skills

# Compare only name fields in posthog docs entries (not install commands,
# which are stable but we want to catch additions/removals/renames).
- name: Check for changes
id: diff
env:
PREV_MENU_URL: ${{ github.server_url }}/${{ github.repository }}/releases/latest/download/docs-skill-menu.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is probably fine, but asking these just in case:

  • What if the contents of skills change but the menu doesn't
  • And I guess, do we need to curl to a tmp file or can we use git? I think the docs-skill-menu.json is kept locally right (or maybe not...)

run: |
if curl -sf -o /tmp/prev-skill-menu.json "$PREV_MENU_URL" 2>/dev/null; then
jq '[.categories["posthog-docs"] // [] | .[] | .name] | sort' \
dist/skills/docs-skill-menu.json > /tmp/new-names.json
jq '[.categories["posthog-docs"] // [] | .[] | .name] | sort' \
/tmp/prev-skill-menu.json > /tmp/prev-names.json
if diff -q /tmp/prev-names.json /tmp/new-names.json > /dev/null 2>&1; then
echo "posthog-docs skills unchanged — skipping release"
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "posthog-docs skills changed"
diff /tmp/prev-names.json /tmp/new-names.json || true
echo "changed=true" >> $GITHUB_OUTPUT
fi
else
echo "No previous release found — treating as changed"
echo "changed=true" >> $GITHUB_OUTPUT
fi

# dist/ is gitignored; force-add skill-menu.json so there's a committed
# record of each sync. Tags are pushed AFTER the release succeeds so that
# "latest" never points at a commit whose ZIPs don't exist yet.
- name: Commit updated docs-skill-menu.json
if: steps.diff.outputs.changed == 'true'
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -f dist/skills/docs-skill-menu.json
git commit -m "chore: sync posthog-docs skills (${TAG})"
git push

- name: Zip skill directories and create release
if: steps.diff.outputs.changed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
EVENT_NAME: ${{ github.event_name }}
run: |
SKILL_COUNT=$(jq '.categories["posthog-docs"] | length' dist/skills/docs-skill-menu.json)

# Zip each posthog-* skill directory
cd dist/skills
for skill_dir in posthog-*/; do
skill_name="${skill_dir%/}"
zip -r "${skill_name}.zip" "${skill_dir}"
echo " zipped ${skill_name}.zip"
done
cd ../..

# gh release create also creates the version tag on GitHub, pointing at
# the commit we just pushed above. If this step fails: the commit is
# already pushed (benign — it only updates docs-skill-menu.json) but
# no tag or release exists. The "Update latest tag" step is skipped
# automatically on failure, so "latest" stays valid. The nightly cron
# will reattempt; since no version tag was pushed, it will compute the
# same version number and try again cleanly.
gh release create "${TAG}" \
--title "Release ${TAG}" \
--notes "Automated sync of PostHog docs skills.

**Trigger:** ${EVENT_NAME}
**Version:** ${VERSION}
**posthog-docs skills:** ${SKILL_COUNT}
**SHA:** $(git rev-parse HEAD)" \
dist/skills/docs-skill-menu.json \
dist/skills/posthog-*.zip

echo "Release ${TAG} created successfully (${SKILL_COUNT} skills)"

# Only move the floating "latest" tag once the release + ZIPs are confirmed live.
- name: Update latest tag
if: steps.diff.outputs.changed == 'true'
run: |
git tag -f latest
git push -f origin latest

- name: Notify on failure
if: failure()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create \
--repo "${{ github.repository }}" \
--title "sync-docs-skills failed ($(date +%Y-%m-%d))" \
--body "Workflow run failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|| echo "Issue creation failed — check the Actions run directly."

- name: Summary
if: always()
env:
CHANGED: ${{ steps.diff.outputs.changed }}
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
EVENT_NAME: ${{ github.event_name }}
run: |
if [ "${CHANGED}" == "true" ]; then
SKILL_COUNT=$(jq '.categories["posthog-docs"] | length' dist/skills/docs-skill-menu.json 2>/dev/null || echo "unknown")
echo "## Sync complete — ${TAG}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${VERSION} |" >> $GITHUB_STEP_SUMMARY
echo "| Trigger | ${EVENT_NAME} |" >> $GITHUB_STEP_SUMMARY
echo "| posthog-docs skills | ${SKILL_COUNT} |" >> $GITHUB_STEP_SUMMARY
else
echo "## No changes — skipped" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "posthog-docs skills unchanged; no release cut." >> $GITHUB_STEP_SUMMARY
fi
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,57 @@ The build script automatically discovers, orders, and generates URIs for all res
- **Version controlled**: Resources evolve with the examples

See `llm-prompts/README.md` for detailed workflow conventions.

## Docs skills

We also auto-generate one [Agent Skill](https://agentskills.io/specification) per section of the PostHog docs. These rebuild whenever posthog.com deploys — no manual work needed.

### How it works

The build script (`scripts/build-docs-skills.js`) fetches `posthog.com/llms.txt`, groups pages by section heading, then pulls down the raw markdown for every page. Each section becomes its own skill directory under `dist/skills/posthog-{section}/`, with a `SKILL.md` and a `references/` folder of subpages.

When skill names change (new sections added, old ones removed), a GitHub Actions workflow cuts a versioned release with a ZIP per skill. A nightly cron runs as a fallback, and you can always trigger it manually.

### What it generates

Run `pnpm run build:docs-skills` to generate locally:

| Output | Description |
|--------|-------------|
| `dist/skills/posthog-{section}/SKILL.md` | Skill prompt + root page content |
| `dist/skills/posthog-{section}/references/*.md` | One file per subpage |
| `dist/skills/docs-skill-menu.json` | Menu index of all generated skills |

`dist/` is gitignored — only `docs-skill-menu.json` gets force-committed by the workflow as a record of each sync. The ZIPs live exclusively in GitHub Releases.

### Distribution

Skills are published to GitHub Releases. The menu is always at:

```text
https://github.com/PostHog/context-mill/releases/latest/download/docs-skill-menu.json
```

Individual skill ZIPs follow the same pattern:

```text
https://github.com/PostHog/context-mill/releases/latest/download/posthog-{section}.zip
```

### Try it locally

```bash
# Build all sections (excludes libraries, api, endpoints by default)
pnpm run build:docs-skills

# Or just the ones you care about
node scripts/build-docs-skills.js feature-flags product-analytics

# Test in Claude Code — copy a skill into .claude/skills/
cp -r dist/skills/posthog-feature-flags .claude/skills/
# Claude Code picks it up immediately, no restart needed
```

### Why this is separate from the curated pipeline

The docs skills pipeline and the curated build (`scripts/build.js`) are intentionally independent. They write to different files (`docs-skill-menu.json` vs `skill-menu.json`), cut separate releases, and run on different cadences. Curated skills change with deliberate PRs. Docs skills sync automatically with posthog.com.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test:plugins:watch": "vitest scripts/plugins/tests",
"test:skills": "vitest run scripts/lib/tests",
"test:skills:watch": "vitest scripts/lib/tests",
"build:docs-skills": "node scripts/build-docs-skills.js",
"test": "vitest run scripts/plugins/tests scripts/lib/tests"
},
"devDependencies": {
Expand Down
Loading
Loading