diff --git a/.github/workflows/schema-check.yml b/.github/workflows/schema-check.yml new file mode 100644 index 000000000..22ca7d96c --- /dev/null +++ b/.github/workflows/schema-check.yml @@ -0,0 +1,70 @@ +name: ProseMirror Schema Check + +# Fails when schemas/prosemirror/ is out of date — i.e. an editor extension +# change (or a TipTap dependency bump) landed without a regenerated schema. +# The export is byte-deterministic, so a plain diff is a reliable check. +# See schemas/prosemirror/README.md. + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + schema-drift: + runs-on: ubuntu-latest + env: + TIPTAP_PRO_TOKEN: ${{ secrets.TIPTAP_PRO_TOKEN }} + FONTAWESOME_NPM_AUTH_TOKEN: ${{ secrets.FONTAWESOME_NPM_AUTH_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check registry secrets + run: | + if [ -z "$TIPTAP_PRO_TOKEN" ] || [ -z "$FONTAWESOME_NPM_AUTH_TOKEN" ]; then + echo "::error::Missing TIPTAP_PRO_TOKEN and/or FONTAWESOME_NPM_AUTH_TOKEN repo secrets; npm ci needs them for the private TipTap Pro and Font Awesome registries." + exit 1 + fi + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + # Same registry mapping developers keep in their local .npmrc; the + # ${VAR} references are expanded by npm itself from the job env. + - name: Configure private npm registries + run: | + { + echo '@tiptap-pro:registry=https://registry.tiptap.dev/' + echo '//registry.tiptap.dev/:_authToken=${TIPTAP_PRO_TOKEN}' + echo '@awesome.me:registry=https://npm.fontawesome.com/' + echo '@fortawesome:registry=https://npm.fontawesome.com/' + echo '//npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN}' + } > .npmrc + + - name: Install dependencies + run: npm ci + + - name: Export ProseMirror schemas + run: npm run schema:export + + - name: Fail on schema drift + run: | + if [ -n "$(git status --porcelain -- schemas/prosemirror/)" ]; then + echo '::error::schemas/prosemirror/ is out of date. Run `npm run schema:export` and commit the result (see schemas/prosemirror/README.md).' + echo + echo 'Drift:' + git status --porcelain -- schemas/prosemirror/ + git diff -- schemas/prosemirror/ + exit 1 + fi + echo 'Schemas are up to date.' diff --git a/package.json b/package.json index 70bd28c0c..78c302209 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "prettier --write --ignore-path .gitignore", "eslint --fix --no-warn-ignored" ], - "**/*.{ts,tsx}": "bash -c 'npm run type-check'" + "**/*.{ts,tsx}": "bash -c 'npm run type-check'", + "{components/Editor/extensions/**/*.{ts,tsx},components/Comment/lib/**/*.{ts,tsx},scripts/export-prosemirror-schema.ts}": "bash -c 'npm run schema:export && git add schemas/prosemirror'" } } diff --git a/schemas/prosemirror/README.md b/schemas/prosemirror/README.md index 8d92aa79d..6e9a0b9e9 100644 --- a/schemas/prosemirror/README.md +++ b/schemas/prosemirror/README.md @@ -31,8 +31,15 @@ Output is deterministic — rerunning without extension changes produces byte-identical files. **Any change to an editor's extensions (adding, removing, or reconfiguring — configuration can alter attribute defaults) must be accompanied by a regenerated schema**, and the backend copy updated. -Enforcement via CI diff is a planned follow-up; until then this is by -convention. + +Two layers enforce this: + +- **Pre-commit**: committing a change under `components/Editor/extensions/`, + `components/Comment/lib/`, or to the export script regenerates the schemas + and stages them automatically (see `lint-staged` in `package.json`). +- **CI**: `.github/workflows/schema-check.yml` reruns the export on every PR + and fails on any diff in this directory. It also catches drift the + pre-commit globs can't see — e.g. a TipTap version bump changing a spec. ## What the export contains