feat(library): add Entity-Relationship diagrams (Chen and crow's-foot notations) #382
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Health Checks | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| detect-changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| library: ${{ steps.filter.outputs.library }} | |
| should-skip: ${{ steps.skip-check.outputs.should_skip }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - id: skip-check | |
| uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5 | |
| with: | |
| do_not_skip: '["workflow_dispatch"]' | |
| - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 | |
| id: filter | |
| if: steps.skip-check.outputs.should_skip != 'true' | |
| with: | |
| filters: | | |
| code: | |
| - 'library/**' | |
| - 'standalone/**' | |
| - 'vscode-extension/**' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| - '.npmrc' | |
| - '.nvmrc' | |
| - 'tsconfig*.json' | |
| - '.github/workflows/pr-health-checks.yml' | |
| - '.github/actions/**' | |
| library: | |
| - 'library/**' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| - '.github/workflows/pr-health-checks.yml' | |
| - '.github/actions/**' | |
| # No path filter: a stale CDN version can drift in via a doc edit OR a bump, | |
| # so check every PR. Pure Node — no pnpm install needed. | |
| version-sync-check: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Set up Node | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 | |
| with: | |
| node-version-file: ".nvmrc" | |
| - name: Check README + docs version sync | |
| run: node scripts/sync-library-version.mjs --check | |
| - name: Check release-note taxonomy sync | |
| run: node scripts/check-release-taxonomy.mjs | |
| lint-and-format-check: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup pnpm + Node + install | |
| uses: ./.github/actions/setup | |
| - name: Run Lint | |
| run: pnpm run lint | |
| - name: Check format | |
| run: pnpm run format:check | |
| - name: Check build | |
| run: pnpm run build | |
| - name: Verify the DEV perf probe is stripped from the library build | |
| # Asserts the import.meta.env.DEV-gated instrumentation dead-code- | |
| # eliminates from library/dist. Kept as its own step rather than in the | |
| # library build script so the Docker image build (which doesn't copy | |
| # scripts/) isn't affected. | |
| run: node scripts/check-no-perf-hooks.mjs | |
| - name: Typecheck docs | |
| # `docs/` imports `@tumaet/apollon` for the live embed, so its | |
| # `tsc --noEmit` resolves through the library's exports map and | |
| # needs `library/dist/` to exist — keep this step after build. | |
| run: pnpm --filter @tumaet/apollon-docs run typecheck | |
| - name: Run unit tests | |
| run: pnpm run test | |
| - name: Run webapp unit tests | |
| # Root `pnpm run test` covers only the library; run the webapp's vitest | |
| # suite here too (it resolves @tumaet/apollon through the dist built | |
| # above) so webapp logic like the autosaver is actually gated. | |
| run: pnpm --filter @tumaet/webapp run test | |
| # Root `pnpm run test` covers only the library. The server's vitest suite | |
| # (incl. the PDF-conversion regression test) spawns the compiled worker and | |
| # provisions Redis via testcontainers, so it gets a dedicated job: build the | |
| # library + server first (the worker runs from dist and imports | |
| # @tumaet/apollon), then run the suite against the Docker daemon the runner | |
| # already provides. | |
| server-tests: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup pnpm + Node + install | |
| uses: ./.github/actions/setup | |
| - name: Build library and server | |
| run: pnpm run build:lib && pnpm run build:server | |
| - name: Run server tests | |
| run: pnpm --filter @tumaet/server run test | |
| e2e-tests: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup pnpm + Node + install | |
| uses: ./.github/actions/setup | |
| - name: Build | |
| run: pnpm run build:lib | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| id: playwright-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| # Browser-specific key: the chromium and firefox jobs share this cache | |
| # path but install different browsers, so they must not share a key | |
| # (a firefox-only cache would make this job skip its chromium install). | |
| key: playwright-chromium-${{ runner.os }}-v1.59.1-${{ hashFiles('standalone/webapp/package.json') }} | |
| - name: Install Playwright browsers | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: pnpm --filter @tumaet/webapp exec playwright install chromium | |
| # Shared with perf-firefox-tests; the dpkg-lock hang it guards is | |
| # documented in the composite action. | |
| - name: Install Playwright system deps (bounded + retried) | |
| uses: ./.github/actions/playwright-install-deps | |
| with: | |
| browser: chromium | |
| - name: Run E2E tests | |
| run: pnpm --filter @tumaet/webapp exec playwright test tests/e2e/ | |
| # No chromium document-growth budget: the metric is engine-independent and | |
| # the Firefox budget (perf-firefox-tests below) is the per-PR guard for the | |
| # exam-freeze regression, since Firefox is the engine the freeze was | |
| # reported on and its GC surfaces unbounded growth sooner. | |
| - name: Upload Playwright report | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-report-e2e | |
| path: standalone/webapp/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright test results | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-test-results-e2e | |
| path: standalone/webapp/test-results/ | |
| retention-days: 14 | |
| # Document-growth budget on Firefox too: its GC surfaces unbounded-growth | |
| # regressions sooner. (The functional e2e suite isn't on Firefox yet — it has | |
| # Firefox-only rendering quirks that need separate triage.) | |
| # | |
| # Bare runner, NOT the Playwright container: the container's bundled Firefox | |
| # crashes (exitCode=1) under GitHub Actions' container sandbox, while the host | |
| # runner launches it reliably. Do not "simplify" this into the container. | |
| perf-firefox-tests: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup pnpm + Node + install | |
| uses: ./.github/actions/setup | |
| - name: Build | |
| run: pnpm run build:lib | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| id: playwright-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| # Firefox-specific key — must differ from the chromium jobs' key (same | |
| # cache path, different browser) so neither poisons the other's cache. | |
| key: playwright-firefox-${{ runner.os }}-v1.59.1-${{ hashFiles('standalone/webapp/package.json') }} | |
| - name: Install Playwright Firefox | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: pnpm --filter @tumaet/webapp exec playwright install firefox | |
| # Shared with e2e-tests; the dpkg-lock hang it guards is documented in | |
| # the composite action. | |
| - name: Install Playwright system deps (bounded + retried) | |
| uses: ./.github/actions/playwright-install-deps | |
| with: | |
| browser: firefox | |
| - name: Run Firefox performance budget tests | |
| run: pnpm --filter @tumaet/webapp run test:perf:firefox | |
| - name: Upload Playwright report | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-report-perf-firefox | |
| path: standalone/webapp/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright test results | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-test-results-perf-firefox | |
| path: standalone/webapp/test-results/ | |
| retention-days: 14 | |
| # Library has `engines.node: ">=22"` for its published consumers — back the | |
| # promise with a build+test on Node 22 LTS so a Node-24-only feature can't | |
| # silently land in the npm tarball. | |
| library-node22-compat: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.library == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Setup pnpm + Node 22 + install | |
| # Node 22 (the library's published floor) instead of the repo's .nvmrc, | |
| # and engine-strict off so the root `>=24.15.0` gate doesn't reject it. | |
| uses: ./.github/actions/setup | |
| with: | |
| node-version: "22" | |
| engine-strict: "false" | |
| - name: Build library | |
| run: pnpm --filter @tumaet/apollon run build | |
| - name: Run library unit tests | |
| run: pnpm --filter @tumaet/apollon run test | |
| - name: Verify ESM exports-map resolution | |
| # The library is ESM-only. `import.meta.resolve` (sync, no side | |
| # effects) validates every subpath in the exports map — actually | |
| # evaluating the module would require jsdom. | |
| working-directory: standalone/server | |
| run: | | |
| node --input-type=module -e ' | |
| const root = import.meta.resolve("@tumaet/apollon"); | |
| const react = import.meta.resolve("@tumaet/apollon/react"); | |
| const internals = import.meta.resolve("@tumaet/apollon/internals"); | |
| const css = import.meta.resolve("@tumaet/apollon/style.css"); | |
| console.log({ root, react, internals, css }); | |
| ' | |
| - name: Lint published package shape (publint) | |
| working-directory: library | |
| run: pnpm dlx publint | |
| # NOTE: arethetypeswrong/cli (attw) is intentionally not run here. | |
| # attw 0.16.x–0.18.x ship a buggy fflate Gunzip streaming workaround | |
| # in @arethetypeswrong/core's `extractTarball` that overwrites the | |
| # decompressed buffer on each chunk callback instead of accumulating. | |
| # Any tarball that decompresses across multiple fflate chunks (ours | |
| # is ~750 KB → 3 chunks) crashes with `Cannot read properties of | |
| # undefined (reading 'filename')`. The standalone subpath | |
| # deliberately bundles peers (~2.4 MB) so the tarball is over | |
| # threshold by design. `publint` above plus the `import.meta.resolve` | |
| # exports-map check below cover the same ground. | |
| - name: Enforce bundle-size budget | |
| working-directory: library | |
| # Budgets in library/package.json#size-limit. Fails on regression | |
| # of >10% (size-limit default) per artifact. | |
| run: pnpm run size | |
| # Pinned Playwright container so screenshot baselines diff against one Linux | |
| # rendering stack regardless of contributor OS (see | |
| # docs/contributor/development/visual-tests.md). Runs per-PR so a rendering | |
| # change refreshes its own baselines in the same PR, instead of merging | |
| # unguarded and surfacing later as an ownerless failure. | |
| # | |
| # Gated on `code`, not a curated render-only path filter: rendering output is | |
| # not statically enumerable (an app-shell layout edit shifts every diagram | |
| # baseline), so anything that could touch pixels must run. Docs-only PRs skip. | |
| visual-regression-tests: | |
| needs: [detect-changes] | |
| if: needs.detect-changes.outputs.should-skip != 'true' && needs.detect-changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| container: | |
| image: mcr.microsoft.com/playwright:v1.59.1-noble | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| # The container ships an older Node; the composite installs Node from | |
| # .nvmrc via setup-node instead of curl-ing NodeSource on every run. | |
| - name: Setup pnpm + Node + install | |
| uses: ./.github/actions/setup | |
| - name: Build | |
| run: pnpm run build:lib | |
| - name: Run visual regression tests | |
| run: pnpm --filter @tumaet/webapp run test:visual | |
| # Point an author whose UI change is intentional at the way to refresh | |
| # baselines, right in the Checks UI. No extra permissions needed. | |
| - name: Explain how to refresh baselines | |
| if: ${{ failure() }} | |
| run: | | |
| { | |
| echo "### Visual regression failed" | |
| echo "" | |
| echo "If this diff is an **intentional** UI change, refresh the baselines:" | |
| echo "- A maintainer runs **Actions → Update Visual Baselines → Run workflow** against this PR's branch — it regenerates snapshots inside this exact pinned container and commits them back." | |
| echo "- Or regenerate locally in the container — see \`docs/contributor/development/visual-tests.md\`." | |
| echo "" | |
| echo "Otherwise download the \`playwright-report-visual\` artifact to inspect the actual-vs-expected diff." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload Playwright report | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-report-visual | |
| path: standalone/webapp/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright test results | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: playwright-test-results-visual | |
| path: standalone/webapp/test-results/ | |
| retention-days: 14 | |
| # Gate job — set this as the required status check in branch protection. | |
| # Correctly handles skipped jobs (docs-only, duplicate runs). | |
| pr-health-gate: | |
| name: PR Health Gate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| needs: | |
| [ | |
| detect-changes, | |
| version-sync-check, | |
| lint-and-format-check, | |
| server-tests, | |
| library-node22-compat, | |
| e2e-tests, | |
| perf-firefox-tests, | |
| visual-regression-tests, | |
| ] | |
| if: always() | |
| steps: | |
| - name: Evaluate results | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then | |
| echo "One or more checks failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "One or more checks were cancelled" | |
| exit 1 | |
| fi | |
| echo "All checks passed or were skipped" |