Skip to content

Catalog drift

Catalog drift #2

Workflow file for this run

name: Catalog drift
# Re-run the catalog sync scripts on a schedule and open (or update) a single
# PR when upstream docs have drifted. Manual `workflow_dispatch` lets a human
# kick a fresh check without waiting for the cron.
#
# The sync scripts always rewrite `fetchedAt` even when content is unchanged
# — by design, so the catalog records when it was last looked at — so we
# normalise that field out before deciding "is this real drift?". If the
# only change is the timestamp, the working tree is restored to HEAD and no
# PR is opened.
on:
schedule:
# Mondays 09:00 UTC. Weekly is enough for documentation drift; the
# window also gives upstream a few business days to settle after any
# weekend churn before we PR ourselves.
- cron: "0 9 * * 1"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
# Don't let two drift runs race; the second would try to recreate the same
# branch and stomp the first's PR.
concurrency:
group: catalog-drift
cancel-in-progress: false
jobs:
drift:
name: Sync and PR on drift
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "npm"
- name: Install npm deps
run: npm ci
- name: Run sync scripts
run: |
npm run sync:settings
npm run sync:env-vars
npm run sync:hooks
npm run sync:sub-agents
npm run sync:mcp
npm run sync:permissions
npm run sync:keybindings
npm run sync:cli-reference
# `fetchedAt` is rewritten on every sync run — strip it before
# comparing so we don't open noise PRs that only bump a timestamp.
# If real content changed, leave the working tree as-is so
# create-pull-request picks up the diff (timestamp included, since the
# PR is meaningful). If only the timestamp changed, restore HEAD so
# the working tree is clean and the PR step skips.
- name: Detect real drift
id: detect
run: |
set -euo pipefail
real_drift=false
drifted_files=()
for f in settings env-vars hooks sub-agents mcp permissions keybindings cli-reference; do
head_norm=$(git show HEAD:catalog/$f.json | jq 'del(.fetchedAt)')
new_norm=$(jq 'del(.fetchedAt)' catalog/$f.json)
if [ "$head_norm" != "$new_norm" ]; then
real_drift=true
drifted_files+=("catalog/$f.json")
fi
done
if [ "$real_drift" = false ]; then
echo "Only fetchedAt changed; restoring catalog/ from HEAD."
git checkout HEAD -- catalog/
else
echo "Real drift in: ${drifted_files[*]}"
fi
echo "real_drift=$real_drift" >> "$GITHUB_OUTPUT"
# Newline-joined list for use in the PR body. printf %s\\n preserves
# the array layout when there are multiple files.
{
echo "drifted_files<<EOF"
printf "%s\n" "${drifted_files[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Open or update drift PR
if: steps.detect.outputs.real_drift == 'true'
uses: peter-evans/create-pull-request@v8
with:
branch: chore/catalog-drift
# Update the same branch on subsequent runs so the PR moves with
# upstream rather than spawning a new one each Monday.
base: main
commit-message: |
chore: sync catalog with upstream docs
Auto-generated by .github/workflows/catalog-drift.yml.
title: "chore: sync catalog with upstream docs"
body: |
Auto-generated by **Catalog drift** ([workflow](https://github.com/${{ github.repository }}/actions/workflows/catalog-drift.yml)).
Upstream sources changed; this PR re-runs the sync scripts:
- `npm run sync:settings`
- `npm run sync:env-vars`
- `npm run sync:hooks`
- `npm run sync:sub-agents`
- `npm run sync:mcp`
- `npm run sync:permissions`
- `npm run sync:keybindings`
- `npm run sync:cli-reference`
Drifted catalogs:
```
${{ steps.detect.outputs.drifted_files }}
```
Review the diff, then merge to land. If something looks wrong,
close this PR and dig into the offending sync script — the
`fetchedAt`-only case is normalised out before this body is
generated, so anything here is real content drift.
labels: |
chore
catalog-drift
delete-branch: true