diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 7fdee07130..0835543c22 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -16,7 +16,8 @@ GitHub Actions workflows and deployment. Full runs take 30-60+ min across Linux, | Workflow | Triggers | What it does | Time | |----------|----------|--------------|------| -| `fw-lite.yaml` | FwLite code changes | Build .NET, run tests, build viewer, publish apps | ~40 min | +| `pr.yaml` (`PR CI`) | Every PR to develop (also `labeled`/`unlabeled`) | The **PR tier**: detects affected projects with nx, then runs only the affected Linux jobs (.NET build + affected tests, viewer checks, viewer Playwright on Chromium, extension lint); escalation tiers add full-tier jobs. See [PR tier](#pr-tier-pryaml) | target < 10 min | +| `fw-lite.yaml` | FwLite code changes | Build .NET, run tests, build viewer, publish apps (the **full tier**, develop only; pull requests run `pr.yaml`) | ~40 min | | `lexbox-api.yaml` | Called by others | Build API, run unit tests, build Docker image | ~15 min | | `lexbox-ui.yaml` | Called by others | Build SvelteKit UI, build Docker image | ~10 min | | `lexbox-fw-headless.yaml` | Called by others | Build FwHeadless Docker image | ~10 min | @@ -37,7 +38,7 @@ GitHub Actions workflows and deployment. Full runs take 30-60+ min across Linux, | Workflow | Purpose | |----------|---------| | `develop-api.yaml` | Quick API build for PRs | -| `develop-ui.yaml` | Quick UI build for PRs | +| `develop-ui.yaml` | Quick UI build for PRs (`frontend/**` except the viewer, which `pr.yaml` covers) | | `develop-fw-headless.yaml` | Quick FwHeadless build for PRs | --- @@ -100,13 +101,24 @@ flowchart TD ### FwLite -`fw-lite.yaml` runs on its own for CI (develop pushes, PRs, manual dispatch). Releases call it from `release-pipeline.yaml` with `release: true` and the shared `version`/`semver-version`; that's the only way to publish the GitHub/Play Store release. Its own manual dispatch can't release (it declares no inputs). The LexBox and FW Lite releases run in parallel; neither blocks the other. +`fw-lite.yaml` runs on its own for CI (develop pushes and manual dispatch; pull requests run `pr.yaml` instead). Releases call it from `release-pipeline.yaml` with `release: true` and the shared `version`/`semver-version`; that's the only way to publish the GitHub/Play Store release. Its own manual dispatch can't release (it declares no inputs). The LexBox and FW Lite releases run in parallel; neither blocks the other. - Core .NET build/tests run on Linux (`FwLiteCore.slnf`); MAUI build/tests on Windows only - Has its own test suite - Publishes standalone apps, not Docker images - Does NOT deploy to K8s +### PR tier (`pr.yaml`) + +`PR CI` is the trimmed, Linux-only job set every pull request runs; `fw-lite.yaml` and `platform.bible-extension.yaml` are the full tier, on develop pushes only. Its summary job `PR CI result` is the check to require on develop: a job skipped as "not affected" counts as passed. + +- **Affected detection** is the nx graph (`nx` + `@nx/dotnet` at the repo root, graph-only: nothing builds or caches through nx). `.github/scripts/affected.sh` prints one JSON object (`projects`, `viewer`, `extension`, `everything`, `tiers`, `fallback`); `task ci:affected` runs the same script locally against `origin/develop` (`AFFECTED_BASE=` to override) and must give the same answer as the `detect` job. Any detector failure falls back to everything affected with a warning annotation. Rules live in the graph: `nx.json` `sharedGlobals` lists the shared files (props, `global.json`, solution files, lockfiles, nx config) that mark everything affected; `frontend/viewer/project.json` and `platform.bible-extension/project.json` register the two npm projects. +- **.NET**: `dotnet` builds `FwLiteCore.slnf` whole (EF-model and generated-TS drift checks stay unconditional) and tests only the affected test projects through a generated `FwLiteAffectedTests.slnf` (`.github/scripts/write-test-slnf.mjs`) with `--filter "Category!=Slow&Category!=Benchmark"`. `Slow` marks tests over ~20 s (`Sena3SyncTests`, `CanSyncRandomEntries`, `BulkCreateEntriesPerformance`, `GetEntryIndex_LargeList_ReturnsCorrectIndex`); the `slow-tests` tier runs them in a parallel job. +- **Viewer**: two jobs so wall clock is max, not sum: `viewer-checks` (i18n extract drift, viewer-only svelte-check and eslint, prettier, vitest on Chromium only via `VITEST_CHROMIUM_ONLY`, vite build) and `viewer-ui-tests` (Playwright, Chromium; `ci:webkit` adds WebKit). Firefox vitest stays on develop. +- **Escalation tiers** (`.github/ci-tiers//project.json`, README there): `slow-tests`, `maui-windows`, `e2e` fire by path rule; `apple`, `android`, `webkit` by label only; `full` on `pr.yaml` itself. A `ci:` label does the same (`ci:full` = all) and re-triggers the run; shared-file changes run the whole PR tier but never escalate. When develop goes red on something the PR tier skipped, the fix is a one-line rule edit. +- **CI calls the script directly**, not `task ci:affected` (Taskfile prefixes output with the task name). +- History: dotnet-affected was prototyped against nx for this role (branch `claude/proto-affected-dotnet-affected-febda6`, commit `978a92bb7`) and lost: it is three files and ~3 s, but it missed a `Directory.Packages.props` bump entirely and cannot see the viewer; nx's MSBuild evaluation inputs attribute shared props to every project and the viewer is a real graph node. Delete that branch once `pr.yaml` is merged. + --- ## Key Concepts diff --git a/.github/ci-tiers/README.md b/.github/ci-tiers/README.md new file mode 100644 index 0000000000..b3ce3c3bf7 --- /dev/null +++ b/.github/ci-tiers/README.md @@ -0,0 +1,33 @@ +# CI escalation tiers + +Each folder here is an nx pseudo-project named `tier-`. It has no code and +runs nothing: it exists so that `nx show projects --affected` (via +`.github/scripts/affected.sh`) lists `tier-` when a pull request should +pull that tier's full-tier jobs into its PR run. The `ci:` label does the +same thing by hand, and `ci:full` sets every tier. + +A tier's rule is in its `project.json`, in one of two forms: + +- **Path rule (the default):** a `ci-affected` target whose `inputs` are + `{workspaceRoot}/...` globs. The tier fires when a file matching a glob + changes, and only then (an edit upstream of those paths does not fire it). + The target is never run; only its `inputs` are read. +- **Project rule:** `implicitDependencies` on project names. The tier fires when + any listed project is affected, **transitively**: nx walks the reverse graph, + so a change upstream of a listed project (e.g. `MiniLcm`, `LexCore`, the + analyzers) fires the tier too. Use it when that is what you want. + +Label-only tiers (`apple`, `android`, `webkit`) have an empty rule and fire only +by label. + +Two rules on top of the graph: + +- A shared file (`nx.json`'s `sharedGlobals`) never reaches a tier, so it runs + the whole PR tier, still Linux-only. nx itself marks every tier when `nx.json`, + the root `package.json` or the root lockfile changes, so the script leaves + those files out when it asks nx for tiers. +- Editing a tier's `project.json` fires that tier (its own folder changed), so a + rule change proves itself on the PR that makes it. + +A missed escalation during the shadow phase becomes a one-line change to the +tier's `project.json`, reviewed like any other PR. diff --git a/.github/ci-tiers/android/project.json b/.github/ci-tiers/android/project.json new file mode 100644 index 0000000000..66c76d4ac8 --- /dev/null +++ b/.github/ci-tiers/android/project.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-android", + "tags": ["ci-tier"], + "_comment": "Escalation: Android build. Label-only (ci:android) by default; add implicitDependencies here to give it a path rule.", + "implicitDependencies": [] +} diff --git a/.github/ci-tiers/apple/project.json b/.github/ci-tiers/apple/project.json new file mode 100644 index 0000000000..8ff09bc89a --- /dev/null +++ b/.github/ci-tiers/apple/project.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-apple", + "tags": ["ci-tier"], + "_comment": "Escalation: iOS simulator + Mac Catalyst build. Label-only (ci:apple) by default; add implicitDependencies here to give it a path rule.", + "implicitDependencies": [] +} diff --git a/.github/ci-tiers/e2e/project.json b/.github/ci-tiers/e2e/project.json new file mode 100644 index 0000000000..c7a550912d --- /dev/null +++ b/.github/ci-tiers/e2e/project.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-e2e", + "tags": ["ci-tier"], + "_comment": "Escalation: k8s-backed end-to-end tests. Path rule: a direct edit on sync or auth paths (first cut; tune during the shadow phase). (Label: ci:e2e.)", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": [ + "{workspaceRoot}/backend/FwLite/FwLiteProjectSync/**/*", + "{workspaceRoot}/backend/FwLite/LcmCrdt/**/*", + "{workspaceRoot}/backend/FwLite/FwLiteShared/Auth/**/*", + "{workspaceRoot}/backend/FwLite/FwLiteShared/Sync/**/*", + "{workspaceRoot}/frontend/viewer/tests/e2e/**/*", + "{workspaceRoot}/.github/actions/setup-k8s/**/*" + ] + } + } +} diff --git a/.github/ci-tiers/full/project.json b/.github/ci-tiers/full/project.json new file mode 100644 index 0000000000..a5ae6b5d39 --- /dev/null +++ b/.github/ci-tiers/full/project.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-full", + "tags": ["ci-tier"], + "_comment": "Escalation: every full-tier job. Path-only rule: fires when the PR workflow itself changes. The target is never run; only its inputs matter.", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": ["{workspaceRoot}/.github/workflows/pr.yaml"] + } + } +} diff --git a/.github/ci-tiers/maui-windows/project.json b/.github/ci-tiers/maui-windows/project.json new file mode 100644 index 0000000000..fbfa1392d0 --- /dev/null +++ b/.github/ci-tiers/maui-windows/project.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-maui-windows", + "tags": ["ci-tier"], + "_comment": "Escalation: Windows MAUI build + MAUI tests. Path rule: a direct edit under the MAUI projects. (Label: ci:maui-windows.)", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": ["{workspaceRoot}/backend/FwLite/FwLiteMaui*/**/*"] + } + } +} diff --git a/.github/ci-tiers/slow-tests/project.json b/.github/ci-tiers/slow-tests/project.json new file mode 100644 index 0000000000..bca4d98e42 --- /dev/null +++ b/.github/ci-tiers/slow-tests/project.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-slow-tests", + "tags": ["ci-tier"], + "_comment": "Escalation: the Slow-trait .NET tests. Path rule: a direct edit in a sync project, the sync helpers, or a test project holding Slow tests. (Label: ci:slow-tests.)", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": [ + "{workspaceRoot}/backend/FwLite/FwLiteProjectSync*/**/*", + "{workspaceRoot}/backend/FwLite/LcmCrdt*/**/*", + "{workspaceRoot}/backend/FwLite/FwDataMiniLcmBridge*/**/*", + "{workspaceRoot}/backend/FwLite/MiniLcm/SyncHelpers/**/*", + "{workspaceRoot}/backend/FwLite/MiniLcm.Tests/**/*" + ] + } + } +} diff --git a/.github/ci-tiers/webkit/project.json b/.github/ci-tiers/webkit/project.json new file mode 100644 index 0000000000..02cbcaae81 --- /dev/null +++ b/.github/ci-tiers/webkit/project.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "tier-webkit", + "tags": ["ci-tier"], + "_comment": "Escalation: viewer Playwright tests on WebKit. Label-only (ci:webkit) by default; add implicitDependencies here to give it a path rule.", + "implicitDependencies": [] +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 48af786bab..850f911f97 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -63,6 +63,23 @@ updates: development-dependencies: dependency-type: development + # --- Repo-root npm: nx + @nx/dotnet, used only for affected detection (pr.yaml) --- + - package-ecosystem: npm + directory: / + target-branch: develop + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 2 + labels: + - dependencies + groups: + # nx and its plugins must move together (exact pins, same version). + nx: + patterns: + - nx + - "@nx/*" + # --- GitHub Actions --- - package-ecosystem: github-actions # Workflows plus composite actions (.github/actions/*/action.y*ml) diff --git a/.github/scripts/affected.sh b/.github/scripts/affected.sh new file mode 100755 index 0000000000..68e3c68d71 --- /dev/null +++ b/.github/scripts/affected.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Prints ONE JSON object describing what the current branch affects, for the PR +# workflow's detect job and for `task ci:affected` (same answer in both places). +# +# { +# "projects": [".NET project names affected, transitively, per nx's graph"], +# "viewer": true|false, // nx project "viewer" (frontend/viewer) affected +# "extension": true|false, // nx project "extension" (platform.bible-extension) affected +# "everything": true|false, // every real project is affected (a shared file changed): +# // run the whole PR tier +# "tiers": ["slow-tests", ...], // escalation tiers whose tier- pseudo-project +# // is affected (.github/ci-tiers//project.json) +# "fallback": null | "reason" // set when detection failed; everything is then true +# } +# +# Diff range: merge-base of ${AFFECTED_BASE:-origin/develop} and ${AFFECTED_HEAD:-HEAD}, +# to that head. Only committed changes count, like GitHub's three-dot PR diff. +# Needs a full-depth checkout (fetch-depth: 0) and no token: everything is local git. +# +# Every rule lives in the nx graph (nx.json sharedGlobals, project.json files for +# the viewer, the extension and the tiers); this script only runs +# `nx show projects --affected` and shapes its answer. No globs here. +# +# Any failure still prints valid JSON with "everything": true and a "fallback" +# reason, and exits 0, so a broken detector can only over-run the PR tier, never +# skip it. Escalation tiers then come from labels only. + +set -u + +cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" || exit 0 + +base_ref="${AFFECTED_BASE:-origin/develop}" +head_ref="${AFFECTED_HEAD:-HEAD}" + +fallback() { + # $1: reason (kept to printable ASCII, quotes stripped, so it is safe inside JSON) + local reason + reason=$(printf '%s' "$1" | tr -cd '[:print:]' | tr -d '"\\' | cut -c1-300) + printf '{"projects":[],"viewer":true,"extension":true,"everything":true,"tiers":[],"fallback":"%s"}\n' "$reason" + exit 0 +} + +# nx prints its JSON array as the last stdout line; banners and errors go to stderr, +# which is left visible so a fallback can be diagnosed from the log. +nx_json() { + local out + out=$("$nx" "$@" --json) || return 1 + out=$(printf '%s\n' "$out" | grep '^\[' | tail -n 1) + [ -n "$out" ] || return 1 + printf '%s' "$out" +} + +merge_base=$(git merge-base "$base_ref" "$head_ref" 2>&1) \ + || fallback "git merge-base $base_ref $head_ref failed: $merge_base" + +nx=./node_modules/.bin/nx +[ -x "$nx" ] || fallback "nx is not installed: run 'pnpm install' at the repo root" + +all=$(nx_json show projects) \ + || fallback "nx show projects failed" +affected=$(nx_json show projects --affected --base="$merge_base" --head="$head_ref") \ + || fallback "nx show projects --affected failed" + +# nx marks every project, tiers included, when nx.json, the root package.json or the root +# lockfile changes, so tiers are decided on the diff without those files: each tier fires by +# its own rule only. sharedGlobals never reach a tier (no `default` input), so they don't escalate. +tier_files=$(git diff --name-only --no-renames "$merge_base" "$head_ref" -- \ + ':!nx.json' ':!package.json' ':!pnpm-lock.yaml' ':!pnpm-workspace.yaml') \ + || fallback "git diff $merge_base $head_ref failed" +tier_affected=$(nx_json show projects --affected --files="$(printf '%s' "$tier_files" | paste -sd, -)") \ + || fallback "nx show projects --affected --files failed" + +# Assemble the result with node (nx already requires it), so names are escaped properly. +ALL="$all" AFFECTED="$affected" TIER_AFFECTED="$tier_affected" node -e ' + const isTier = (n) => n.startsWith("tier-"); + const isSpecial = (n) => n === "viewer" || n === "extension"; + const all = JSON.parse(process.env.ALL); + const affected = JSON.parse(process.env.AFFECTED); + const real = (names) => names.filter((n) => !isTier(n)); + const projects = affected.filter((n) => !isTier(n) && !isSpecial(n)).sort(); + const everything = real(all).every((n) => affected.includes(n)); + const tiers = JSON.parse(process.env.TIER_AFFECTED).filter(isTier) + .map((n) => n.slice("tier-".length)).sort(); + process.stdout.write(JSON.stringify({ + projects, + viewer: affected.includes("viewer"), + extension: affected.includes("extension"), + everything, + tiers, + fallback: null, + }) + "\n"); +' || fallback "could not assemble JSON from nx output" diff --git a/.github/scripts/write-test-slnf.mjs b/.github/scripts/write-test-slnf.mjs new file mode 100644 index 0000000000..15942bf261 --- /dev/null +++ b/.github/scripts/write-test-slnf.mjs @@ -0,0 +1,16 @@ +// Writes FwLiteAffectedTests.slnf at the repo root: the FW Lite test projects named in $TEST_PROJECTS +// (a JSON array of project names from pr.yaml's detect job), taken from FwLiteCore.slnf so the paths +// and the solution reference stay in one place. `dotnet test FwLiteAffectedTests.slnf --no-build` +// then runs exactly those projects, concurrently, after a whole-FwLiteCore.slnf build. +import {readFileSync, writeFileSync} from 'node:fs'; + +const wanted = new Set(JSON.parse(process.env.TEST_PROJECTS ?? '[]')); +const core = JSON.parse(readFileSync('FwLiteCore.slnf', 'utf8')); +const name = (p) => p.split('\\').pop().replace(/\.csproj$/, ''); +const projects = core.solution.projects.filter((p) => wanted.has(name(p))); +const missing = [...wanted].filter((w) => !projects.some((p) => name(p) === w)); +if (missing.length) throw new Error(`not in FwLiteCore.slnf: ${missing.join(', ')}`); +if (projects.length === 0) throw new Error('TEST_PROJECTS selected no test project'); + +writeFileSync('FwLiteAffectedTests.slnf', JSON.stringify({solution: {path: core.solution.path, projects}}, null, 2) + '\n'); +console.log(`FwLiteAffectedTests.slnf: ${projects.map(name).join(', ')}`); diff --git a/.github/workflows/develop-ui.yaml b/.github/workflows/develop-ui.yaml index 86d229fd88..b4bcf45325 100644 --- a/.github/workflows/develop-ui.yaml +++ b/.github/workflows/develop-ui.yaml @@ -3,6 +3,8 @@ on: push: paths: - 'frontend/**' + # The viewer (FW Lite) is covered by pr.yaml / fw-lite.yaml and ships via FwLiteShared/wwwroot, not the UI image. + - '!frontend/viewer/**' - '.github/workflows/develop-ui.yaml' - '.github/workflows/lexbox-ui.yaml' - '.github/workflows/deploy.yaml' @@ -11,6 +13,7 @@ on: pull_request: paths: - 'frontend/**' + - '!frontend/viewer/**' - '.github/workflows/develop-ui.yaml' - '.github/workflows/lexbox-ui.yaml' - '.github/workflows/deploy.yaml' diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index f68ce2f44f..7da1755841 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -31,26 +31,11 @@ on: - 'global.json' branches: - develop - pull_request: - paths: - - 'backend/FwLite/**' - - 'backend/Directory.Packages.props' - - 'backend/Harmony*.props' - - 'frontend/viewer/**' - - 'platform.bible-extension/**' - - '.github/workflows/fw-lite.yaml' - - '.github/actions/setup-k8s/**' - - 'FwLiteCore.slnf' - - 'FwLiteOnly.slnf' - - 'global.json' - branches: - - develop - - feat/sync-morph-types #just for now ensure PRs to this branch have checks run -# Only cancel superseded PR runs; never cancel develop or release builds. +# Never cancel develop or release builds. Pull requests run pr.yaml (the PR tier), not this workflow. concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: false permissions: contents: read @@ -202,6 +187,12 @@ jobs: working-directory: frontend run: | pnpm install + - name: svelte-check (viewer) + working-directory: frontend/viewer + run: pnpm run check + - name: eslint (viewer) + working-directory: frontend/viewer + run: pnpm run lint - name: Ensure all i18n strings have been extracted working-directory: frontend/viewer shell: bash diff --git a/.github/workflows/platform.bible-extension.yaml b/.github/workflows/platform.bible-extension.yaml index 3978b29f57..a2a89edcc5 100644 --- a/.github/workflows/platform.bible-extension.yaml +++ b/.github/workflows/platform.bible-extension.yaml @@ -6,12 +6,6 @@ on: - ".github/workflows/platform.bible-extension.yaml" branches: - develop - pull_request: - paths: - - "platform.bible-extension/**" - - ".github/workflows/platform.bible-extension.yaml" - branches: - - develop # Modified from https://github.com/paranext/paranext-extension-template/blob/main/.github/workflows/lint.yml diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000000..6ef808c32f --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,689 @@ +name: PR CI +# The PR tier: Linux-only, affected-only checks for every pull request, under 10 minutes for a +# one-project change. `detect` asks the nx graph what the diff touches (.github/scripts/affected.sh, +# same answer as `task ci:affected` locally); the gated jobs run only for what is affected; +# escalation tiers (.github/ci-tiers/README.md) pull full-tier jobs in by path rule or by a +# `ci:` label (`ci:full` = all). `summary` is the one check to require at cut-over. +# Everything else (publishing, signing, stores, Firefox, WebKit by default) stays on develop in +# fw-lite.yaml and platform.bible-extension.yaml. +on: + pull_request: + branches: + - develop + # labeled/unlabeled so adding `ci:` re-runs at once; the concurrency group cancels the superseded run. + types: [opened, synchronize, reopened, labeled, unlabeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + # nx's own CI default; also skips the daemon's "pull latest nx" network call on start. + NX_DAEMON: 'false' + VIEWER_BUILD_OUTPUT_DIR: backend/FwLite/FwLiteShared/wwwroot/viewer + # Same pin as fw-lite.yaml. + MAUI_WORKLOAD_VERSION: 10.0.401 + +jobs: + detect: + name: Detect affected projects + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + projects: ${{ steps.affected.outputs.projects }} + viewer: ${{ steps.affected.outputs.viewer }} + extension: ${{ steps.affected.outputs.extension }} + everything: ${{ steps.affected.outputs.everything }} + tiers: ${{ steps.affected.outputs.tiers }} + fallback: ${{ steps.affected.outputs.fallback }} + dotnet: ${{ steps.affected.outputs.dotnet }} + test-projects: ${{ steps.affected.outputs.test-projects }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history: the diff is merge-base(base branch, PR head)..PR head, computed locally + # with git alone, which also works for fork PRs (no token needed). + fetch-depth: 0 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: 'package.json' + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: './package.json' + cache: 'pnpm' + cache-dependency-path: './pnpm-lock.yaml' + - name: Install nx (repo root) + run: pnpm install --frozen-lockfile + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + # @nx/dotnet evaluates every csproj with MSBuild to build the graph. + dotnet-version: '10.x' + + - name: Compute affected projects and tiers + id: affected + env: + AFFECTED_BASE: origin/${{ github.base_ref }} + AFFECTED_HEAD: ${{ github.event.pull_request.head.sha }} + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + set -euo pipefail + result="$(bash .github/scripts/affected.sh)" + echo "affected.sh: $result" + RESULT="$result" node - <<'EOF' + const fs = require('node:fs'); + const r = JSON.parse(process.env.RESULT); + const labels = JSON.parse(process.env.PR_LABELS || '[]'); + const tierNames = fs.readdirSync('.github/ci-tiers', {withFileTypes: true}) + .filter((d) => d.isDirectory()).map((d) => d.name); + // Labels union with the graph's tiers; `full` (label or the PR workflow file) means all of them. + const tiers = new Set(r.tiers); + for (const label of labels) { + if (label === 'ci:full') tiers.add('full'); + else if (label.startsWith('ci:')) tiers.add(label.slice(3)); + } + let everything = r.everything; + if (tiers.has('full')) { everything = true; tierNames.forEach((t) => tiers.add(t)); } + // FW Lite projects = FwLiteCore.slnf; the dotnet job builds it whole and tests the affected test projects. + const core = JSON.parse(fs.readFileSync('FwLiteCore.slnf', 'utf8')).solution.projects + .map((p) => p.split('\\').pop().replace(/\.csproj$/, '')); + const coreTests = core.filter((p) => p.endsWith('.Tests')); + const dotnet = everything || r.projects.some((p) => core.includes(p)); + let testProjects = everything ? coreTests : r.projects.filter((p) => coreTests.includes(p)); + // e.g. only a leaf library changed, or a ci:slow-tests label on a PR with no .NET change + if (testProjects.length === 0) testProjects = coreTests; + const out = { + projects: JSON.stringify(r.projects), + viewer: String(r.viewer || everything), + extension: String(r.extension || everything), + everything: String(everything), + tiers: JSON.stringify([...tiers].sort()), + fallback: r.fallback ?? '', + dotnet: String(dotnet), + 'test-projects': JSON.stringify(testProjects), + }; + fs.appendFileSync(process.env.GITHUB_OUTPUT, Object.entries(out).map(([k, v]) => `${k}=${v}\n`).join('')); + if (r.fallback) console.log(`::warning::affected detection fell back to everything: ${r.fallback}`); + const rows = Object.entries(out).map(([k, v]) => `| ${k} | \`${v}\` |`).join('\n'); + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `### Affected\n\n| key | value |\n|---|---|\n${rows}\n\nLabels: \`${labels.join(', ')}\`\n`); + console.log(out); + EOF + + dotnet: + name: .NET build and tests (affected) + needs: detect + if: needs.detect.outputs.dotnet == 'true' + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Cache NuGet packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('backend/Directory.Packages.props', 'backend/Harmony*.props') }} + restore-keys: | + nuget-${{ runner.os }}- + - name: Install Task + uses: arduino/setup-task@c0bc642852239c2689f73f4ea6459c29405f3c52 # v3.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Dotnet build + # Whole FwLiteCore.slnf (~1 min): keeps the two drift checks below unconditional. Tests are the affected subset. + run: dotnet build FwLiteCore.slnf -p:BuildAndroid=false + + - name: Check for pending EF model changes + run: task fw-lite:has-pending-model-changes -- --no-build + + - name: Check for stale generated TypeScript types + run: task fw-lite:has-stale-generated-types -- --no-build + + - name: Select affected test projects + env: + TEST_PROJECTS: ${{ needs.detect.outputs.test-projects }} + run: node .github/scripts/write-test-slnf.mjs + + - name: Dotnet test (PR tier) + # Slow and Benchmark run elsewhere: the slow-tests job (same selection, Category=Slow) and develop. + # TZ forces a non-UTC timezone so date bugs are observable (see fw-lite.yaml). + env: + TZ: America/New_York + run: dotnet test FwLiteAffectedTests.slnf --logger GitHubActions --logger trx --results-directory "${{ github.workspace }}/TestResults" --no-build -p:BuildAndroid=false --filter "Category!=Slow&Category!=Benchmark" + + - name: Publish test results to TestHistory + if: always() + continue-on-error: true # telemetry only — a TestHistory outage must not fail the build + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: TestResults/**/*.trx + label: FW Lite .NET + on-no-files: ignore # build may fail before any tests run + + slow-tests: + name: .NET slow tests (tier slow-tests) + needs: detect + if: contains(fromJSON(needs.detect.outputs.tiers), 'slow-tests') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Cache NuGet packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('backend/Directory.Packages.props', 'backend/Harmony*.props') }} + restore-keys: | + nuget-${{ runner.os }}- + + - name: Dotnet build + run: dotnet build FwLiteCore.slnf -p:BuildAndroid=false + + - name: Select affected test projects + env: + TEST_PROJECTS: ${{ needs.detect.outputs.test-projects }} + run: node .github/scripts/write-test-slnf.mjs + + - name: Dotnet test (Category=Slow) + env: + TZ: America/New_York + run: dotnet test FwLiteAffectedTests.slnf --logger GitHubActions --logger trx --results-directory "${{ github.workspace }}/TestResults" --no-build -p:BuildAndroid=false --filter "Category=Slow" + + - name: Publish test results to TestHistory + if: always() + continue-on-error: true # telemetry only + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: TestResults/**/*.trx + label: FW Lite .NET slow + on-no-files: ignore + + viewer-checks: + name: Viewer checks (svelte-check, eslint, prettier, i18n, vitest, build) + needs: detect + if: needs.detect.outputs.viewer == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + checks: write # publish-unit-test-result-action + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: 'frontend/package.json' + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: './frontend/package.json' + cache: 'pnpm' + cache-dependency-path: './frontend/pnpm-lock.yaml' + - name: Prepare frontend + working-directory: frontend + run: pnpm install + + - name: Ensure all i18n strings have been extracted + working-directory: frontend/viewer + shell: bash + run: | + set -e + # GHA sets -o pipefail when shell is bash but we want to turn it off + set +o pipefail + rm -f src/locales/en.po.old + cp src/locales/en.po src/locales/en.po.old + pnpm i18n:extract + DIFFCOUNT=$(diff -u src/locales/en.po{.old,} | grep '+msgid' | wc -l) + if [ $DIFFCOUNT -gt 0 ]; then + # Unextracted strings found, fail the build + echo '***' + echo $DIFFCOUNT unextracted strings found: + echo '***' + diff -u src/locales/en.po{.old,} | grep '+msgid' | cut -c2- + echo '***' + echo Please run pnpm i18n:extract + exit 1 + fi + rm -f src/locales/en.po.old + + - name: svelte-check (viewer) + working-directory: frontend/viewer + run: pnpm run check + - name: eslint (viewer) + working-directory: frontend/viewer + run: pnpm run lint + - name: Check code formatting with Prettier + working-directory: frontend/viewer + run: pnpm run format:check + + # vitest's browser and storybook projects run under Playwright; Chromium only on the PR tier. + - name: Cache Playwright browsers + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install --with-deps chromium + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install-deps chromium + - name: vitest (unit, browser, storybook; Chromium only) + working-directory: frontend/viewer + env: + VITEST_CHROMIUM_ONLY: 'true' + run: pnpm run test --retry=2 --reporter=default --reporter=junit --outputFile.junit=test-results/vitest-results.xml + - name: Publish unit test results + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0 + if: always() + with: + check_name: UI unit Tests (PR tier) + files: ./frontend/viewer/test-results/*.xml + action_fail_on_inconclusive: true + comment_mode: off + - name: Publish test results to TestHistory + if: always() + continue-on-error: true # telemetry only — a TestHistory outage must not fail the build + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: frontend/viewer/test-results/vitest-results.xml + label: FW Lite Viewer unit + on-no-files: ignore + + - name: Build viewer + working-directory: frontend/viewer + run: pnpm run build + + viewer-ui-tests: + name: Viewer Playwright UI tests + needs: detect + if: needs.detect.outputs.viewer == 'true' || contains(fromJSON(needs.detect.outputs.tiers), 'webkit') + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + # Chromium only on the PR tier; `ci:webkit` (tier webkit) adds WebKit. + WEBKIT: ${{ contains(fromJSON(needs.detect.outputs.tiers), 'webkit') }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: 'frontend/package.json' + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: './frontend/package.json' + cache: 'pnpm' + cache-dependency-path: './frontend/pnpm-lock.yaml' + - name: Install Task + uses: arduino/setup-task@c0bc642852239c2689f73f4ea6459c29405f3c52 # v3.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Prepare frontend + working-directory: frontend + run: pnpm install + - name: Pick browsers + id: browsers + run: | + if [ "$WEBKIT" = 'true' ]; then + echo "list=chromium webkit" >> "$GITHUB_OUTPUT" + echo "args=--project=chromium --project=webkit" >> "$GITHUB_OUTPUT" + echo "key=chromium-webkit" >> "$GITHUB_OUTPUT" + else + echo "list=chromium" >> "$GITHUB_OUTPUT" + echo "args=--project=chromium" >> "$GITHUB_OUTPUT" + echo "key=chromium" >> "$GITHUB_OUTPUT" + fi + - name: Cache Playwright browsers + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.browsers.outputs.key }}-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: frontend + run: pnpm exec playwright install --with-deps ${{ steps.browsers.outputs.list }} + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend + run: pnpm exec playwright install-deps ${{ steps.browsers.outputs.list }} + - name: Run UI tests + working-directory: frontend/viewer + run: task test:ui-standalone -- ${{ steps.browsers.outputs.args }} + - name: Publish test results to TestHistory + if: always() # run even when the UI tests above failed + continue-on-error: true # telemetry only — a TestHistory outage must not fail the build + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: frontend/viewer/tests/ui/test-results/results.xml + label: FW Lite Viewer UI + on-no-files: ignore + + extension-lint: + name: Platform.Bible extension lint + needs: detect + if: needs.detect.outputs.extension == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + # Copied from platform.bible-extension.yaml (which keeps running on develop pushes). + steps: + - name: Checkout git repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: lexbox + persist-credentials: false + + - name: Checkout paranext-core repo to use its sub-packages + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: paranext-core + repository: paranext/paranext-core + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + cache: "npm" + cache-dependency-path: | + lexbox/platform.bible-extension/package-lock.json + paranext-core/package-lock.json + node-version-file: lexbox/platform.bible-extension/package.json + + - name: Install extension dependencies + working-directory: lexbox/platform.bible-extension + run: npm ci --ignore-scripts + + # --ignore-scripts skips core's preinstall (stages the gitignored dev-packages/staging/ that + # platform-bible-react imports) and postinstall (verify), so both run as explicit steps. + - name: Stage core's dev packages + working-directory: paranext-core + run: npm run stage-dev-packages + + - name: Install core dependencies + working-directory: paranext-core + run: npm ci --ignore-scripts + + - name: Verify core's staged dev packages match its lockfile + working-directory: paranext-core + run: npm run verify:dev-packages + + - name: Run format checking + working-directory: lexbox/platform.bible-extension + run: npm run format:check + + - name: Run all linters + working-directory: lexbox/platform.bible-extension + run: npm run lint + + # ---- Escalations: full-tier jobs pulled in by a tier (path rule or `ci:` label). Compile and + # ---- test only: no packaging, signing or store uploads, which stay on develop in fw-lite.yaml. + + maui-windows: + name: MAUI Windows build and tests (tier maui-windows) + needs: detect + if: contains(fromJSON(needs.detect.outputs.tiers), 'maui-windows') + runs-on: windows-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Cache NuGet packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('backend/Directory.Packages.props', 'backend/Harmony*.props') }} + restore-keys: | + nuget-${{ runner.os }}- + - name: Setup Maui + run: dotnet workload install maui-windows --version ${{ env.MAUI_WORKLOAD_VERSION }} + + - name: Build MAUI tests + run: dotnet build backend/FwLite/FwLiteMaui.Tests/FwLiteMaui.Tests.csproj -p:BuildAndroid=false + + - name: Test MAUI + run: dotnet test backend/FwLite/FwLiteMaui.Tests/FwLiteMaui.Tests.csproj --logger GitHubActions --logger trx --results-directory "${{ github.workspace }}/TestResults" --no-build -p:BuildAndroid=false + + - name: Publish MAUI test results to TestHistory + if: always() + continue-on-error: true # telemetry only + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: TestResults/**/*.trx + label: FW Lite MAUI Windows + on-no-files: ignore + + apple: + name: iOS simulator and Mac Catalyst builds (tier apple) + needs: detect + if: contains(fromJSON(needs.detect.outputs.tiers), 'apple') + runs-on: macos-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Setup Maui + run: dotnet workload install maui-ios maui-maccatalyst --version ${{ env.MAUI_WORKLOAD_VERSION }} + + # Same builds as fw-lite.yaml's build-apple, unsigned, no DMG or notarization. + - name: Build iOS (simulator) + working-directory: backend/FwLite/FwLiteMaui + run: dotnet build -f net10.0-ios -c Release -p:BuildAndroid=false -p:RuntimeIdentifier=iossimulator-arm64 + + - name: Build Mac Catalyst (unsigned) + working-directory: backend/FwLite/FwLiteMaui + run: dotnet build -f net10.0-maccatalyst -c Release -p:BuildAndroid=false -p:RuntimeIdentifier=maccatalyst-arm64 + + android: + name: Android build (tier android) + needs: detect + if: contains(fromJSON(needs.detect.outputs.tiers), 'android') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Setup Maui + run: dotnet workload install maui-android --version ${{ env.MAUI_WORKLOAD_VERSION }} + + - name: Publish Android (unsigned) + working-directory: backend/FwLite/FwLiteMaui + run: dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts -p:AndroidKeyStore=false + + e2e: + name: E2E tests on a kind cluster (tier e2e) + needs: detect + if: contains(fromJSON(needs.detect.outputs.tiers), 'e2e') + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + FW_LITE_BINARY_PATH: ${{ github.workspace }}/backend/FwLite/artifacts/publish/FwLiteWeb/release_linux-x64/FwLiteWeb + # Copied from fw-lite.yaml's e2e-test, with two differences: the viewer is built here + # instead of downloaded, and the "Test FwLite launcher" vitest step is left out. The + # smoke test below launches the same binary; the launcher's own cases stay on develop. + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true + - name: Install Task + uses: arduino/setup-task@c0bc642852239c2689f73f4ea6459c29405f3c52 # v3.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + package_json_file: 'frontend/package.json' + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: './frontend/package.json' + cache: 'pnpm' + cache-dependency-path: './frontend/pnpm-lock.yaml' + - name: Install frontend deps + working-directory: frontend + run: pnpm install + - name: Build viewer + working-directory: frontend/viewer + run: pnpm run build + + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '10.x' + - name: Publish FwLiteWeb (linux-x64) + working-directory: backend/FwLite/FwLiteWeb + run: dotnet publish -r linux-x64 --artifacts-path ../artifacts -p:PublishSingleFile=true + - name: set execute permissions + shell: bash + run: chmod +x ${{ env.FW_LITE_BINARY_PATH }} + + - uses: ./.github/actions/setup-k8s + with: + lexbox-api-tag: develop + ingress-controller-port: '6579' # http; only used by the action's own curl-verify step + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # MSAL refuses any non-https authority. ingress-nginx serves a snake-oil + # cert that FwLite trusts via --environment Development and playwright via + # ignoreHTTPSErrors. + - name: Forward ingress HTTPS port + shell: bash + run: | + kubectl port-forward service/ingress-nginx-controller 6580:443 -n languagedepot >/tmp/https-port-forward.log 2>&1 & + # Wait for the port-forward to actually be listening + for i in $(seq 1 30); do + if curl -sk https://localhost:6580 >/dev/null 2>&1; then + echo "✅ HTTPS port-forward ready on :6580" + exit 0 + fi + sleep 1 + done + echo "❌ HTTPS port-forward did not become ready" + cat /tmp/https-port-forward.log + exit 1 + + - name: Cache Playwright browsers + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('frontend/pnpm-lock.yaml') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install --with-deps chromium + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + working-directory: frontend/viewer + run: pnpm exec playwright install-deps chromium + + - name: Run E2E tests + working-directory: frontend/viewer + env: + TEST_SERVER_PROTOCOL: https + TEST_SERVER_PORT: 6580 + run: task test:e2e + - name: Publish E2E test results to TestHistory + if: always() # run even when the E2E tests above failed + continue-on-error: true # telemetry only — a TestHistory outage must not fail the build + uses: hahn-kev/TestHistory/.github/actions/upload-results@e5fc439cd7644a145c7553bc3b0077aa643aef8d # master + with: + server-url: ${{ vars.TESTHISTORY_SERVER_URL }} + project-id: ${{ vars.TESTHISTORY_FWLITE_PROJECT_ID }} + api-token: ${{ secrets.TESTHISTORY_API_TOKEN }} + files: frontend/viewer/tests/e2e/test-results/e2e-results.xml + label: FW Lite E2E + on-no-files: ignore + + - name: Upload Playwright results/traces (on failure) + if: failure() || cancelled() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: pr-e2e-test-results + if-no-files-found: ignore + path: frontend/viewer/tests/e2e/test-results/ + + - name: Capture k8s pod logs (on failure) + if: failure() || cancelled() + shell: bash + run: | + mkdir -p k8s-logs + for app in lexbox ui hg db fw-headless; do + kubectl describe pods -l "app=${app}" -n languagedepot > k8s-logs/describe-${app}.txt 2>&1 || true + kubectl logs -l "app=${app}" -n languagedepot --prefix --all-containers --tail=-1 > k8s-logs/logs-${app}.txt 2>&1 || true + done + kubectl logs -l 'app.kubernetes.io/name=ingress-nginx' -n languagedepot --prefix --all-containers --tail=-1 > k8s-logs/logs-ingress.txt 2>&1 || true + + - name: Upload k8s logs (on failure) + if: failure() || cancelled() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: pr-e2e-k8s-logs + if-no-files-found: ignore + path: k8s-logs/*.txt + + summary: + # The one check to mark required at cut-over. A skipped job (not affected, tier off) counts as passed. + name: PR CI result + if: always() + needs: [detect, dotnet, slow-tests, viewer-checks, viewer-ui-tests, extension-lint, maui-windows, apple, android, e2e] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check job results + env: + NEEDS: ${{ toJSON(needs) }} + DETECT_PROJECTS: ${{ needs.detect.outputs.projects }} + DETECT_TIERS: ${{ needs.detect.outputs.tiers }} + DETECT_EVERYTHING: ${{ needs.detect.outputs.everything }} + DETECT_FALLBACK: ${{ needs.detect.outputs.fallback }} + run: | + { + echo "### PR CI result" + echo + echo "| job | result |" + echo "|---|---|" + echo "$NEEDS" | jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"' + echo + echo "Affected .NET projects: \`$DETECT_PROJECTS\`; tiers: \`$DETECT_TIERS\`; everything: \`$DETECT_EVERYTHING\`; fallback: \`${DETECT_FALLBACK:-none}\`" + } >> "$GITHUB_STEP_SUMMARY" + failed=$(echo "$NEEDS" | jq -r 'to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key' | paste -sd ' ') + if [ -n "$failed" ]; then + echo "::error::Failed or cancelled jobs: $failed" + exit 1 + fi + echo "All jobs passed or were skipped as not affected." diff --git a/.gitignore b/.gitignore index f9286a2e5b..6e8ca694cd 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,10 @@ AGENTS.local.md .claude/scheduled_tasks.lock .claude/agent-memory-local/ screenshots/ + +# nx (affected detection only): cache/workspace-data and the root node_modules +.nx/ +/node_modules/ + +# Generated by pr.yaml (affected FW Lite test projects) +/FwLiteAffectedTests.slnf diff --git a/Taskfile.yml b/Taskfile.yml index 5d094c73c0..b74dbb75d9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -114,6 +114,12 @@ tasks: internal: true deps: [ ui:verify, fw-ui:verify ] + ci:affected: + desc: 'Print one JSON object of what this branch affects vs origin/develop (nx graph + shared-file globs); AFFECTED_BASE= overrides the base. Same script as the PR workflow.' + silent: true + cmds: + - bash .github/scripts/affected.sh + check-generated: desc: 'Runs every committed-artifact drift check CI runs (EF model x2, gql schema, OpenAPI, FwLite TS types); reports all, fails if any fail.' silent: true diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index 671b8cc465..d674e8e7d4 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -253,6 +253,7 @@ public enum ApiType // Api == FwDataApi and RoundTripApi == CrdtApi // (though, as noted above, this case doesn't perfectly reflect real usage) [Theory] + [Trait("Category", "Slow")] // 19-35 s per row in both derived classes; PR tier skips Category=Slow [InlineData(ApiType.Crdt)] [InlineData(ApiType.FwData)] [InlineData(null)] diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs index 80a20b731a..dc98c31b03 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs @@ -13,6 +13,9 @@ namespace FwLiteProjectSync.Tests; [Trait("Category", "Integration")] +// Slow: every test here syncs the sample project (65-123 s each); the PR tier skips Category=Slow +// and the slow-tests job or develop runs them. +[Trait("Category", "Slow")] [Collection(Sena3Collection.Name)] public class Sena3SyncTests : IAsyncLifetime { @@ -136,7 +139,6 @@ public async Task DryRunSync_MakesNoChanges() } [Fact] - [Trait("Category", "Slow")] [Trait("Category", "Integration")] public async Task DryRunSync_MakesTheSameChangesAsSync() { @@ -173,7 +175,6 @@ await _crdtApi.TakeProjectSnapshot() } [Fact] - [Trait("Category", "Slow")] [Trait("Category", "Integration")] public async Task SyncWithoutImport_CrdtShouldMatchFwdata() { diff --git a/backend/FwLite/LcmCrdt.Tests/BulkCreateEntriesTest.cs b/backend/FwLite/LcmCrdt.Tests/BulkCreateEntriesTest.cs index 58c2fe9a68..e99277e540 100644 --- a/backend/FwLite/LcmCrdt.Tests/BulkCreateEntriesTest.cs +++ b/backend/FwLite/LcmCrdt.Tests/BulkCreateEntriesTest.cs @@ -98,6 +98,7 @@ private async IAsyncEnumerable SeedData(int entryCount, IMiniLcmApi api) } [Fact] + [Trait("Category", "Slow")] // ~150 s; PR tier skips Category=Slow public async Task BulkCreateEntriesPerformance() { var entryCount = 20_000; diff --git a/backend/FwLite/MiniLcm.Tests/EntryIndexTestsBase.cs b/backend/FwLite/MiniLcm.Tests/EntryIndexTestsBase.cs index e994947c65..64b2719bbc 100644 --- a/backend/FwLite/MiniLcm.Tests/EntryIndexTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/EntryIndexTestsBase.cs @@ -79,6 +79,7 @@ public async Task GetEntryIndex_WithCustomSort_ReturnsCorrectIndex() } [Fact] + [Trait("Category", "Slow")] // ~90 s on the CRDT side; PR tier skips Category=Slow public async Task GetEntryIndex_LargeList_ReturnsCorrectIndex() { var entryIds = new List(); diff --git a/backend/FwLite/Taskfile.yml b/backend/FwLite/Taskfile.yml index 9ac9a14703..70111a784c 100644 --- a/backend/FwLite/Taskfile.yml +++ b/backend/FwLite/Taskfile.yml @@ -206,8 +206,9 @@ tasks: cmd: dotnet test ../../FwLiteOnly.slnf test-verified: desc: Run only tests that generate verified snapshot files - # always produces exit status 1, because the framework isn't valid for all test projects - cmd: dotnet test ../../FwLiteOnly.slnf --filter "Category=Verified" --framework net10.0 --logger "console;verbosity=minimal" + # FwLiteCore.slnf, not FwLiteOnly.slnf: the MAUI test project has no net10.0 target and no Verified + # tests, and `--framework net10.0` made the whole run exit 1 for it. + cmd: dotnet test ../../FwLiteCore.slnf --filter "Category=Verified" --logger "console;verbosity=minimal" reset-verified: desc: Reset all verified snapshot files to their state on origin/develop cmds: diff --git a/frontend/viewer/project.json b/frontend/viewer/project.json new file mode 100644 index 0000000000..afcd2ac02e --- /dev/null +++ b/frontend/viewer/project.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "viewer", + "projectType": "application", + "sourceRoot": "frontend/viewer/src", + "tags": ["pnpm"], + "_comment": "Registers the FW Lite viewer as an nx project for affected detection only. The viewer lives in the frontend/ pnpm workspace, which nx cannot see from the repo root, hence this file. pnpm scripts stay the way to build and test it; the one target below is never run.", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": [ + "default", + "{workspaceRoot}/frontend/tests/envVars.ts", + "{workspaceRoot}/frontend/tests/pages/**/*" + ], + "metadata": { + "description": "Never run. Its `default` input (which includes nx.json's sharedGlobals) makes a shared-file change mark this project affected; the frontend/tests files are imported by the viewer's Playwright tests." + } + } + } +} diff --git a/frontend/viewer/vitest.config.ts b/frontend/viewer/vitest.config.ts index 7c1568530b..9cb2cb692e 100644 --- a/frontend/viewer/vitest.config.ts +++ b/frontend/viewer/vitest.config.ts @@ -12,6 +12,14 @@ const launcherTestPattern = './tests/launcher/**/*.{test,spec}.?(c|m)[jt]s?(x)'; const manualTestPattern = '**/*.manual.{test,spec}.?(c|m)[jt]s?(x)'; const e2eTestPatterns = ['./tests/**']; +// The PR workflow (pr.yaml) sets VITEST_CHROMIUM_ONLY=true so the browser-based projects run on +// Chromium alone; locally and on develop (fw-lite.yaml) Firefox runs too. +// A function, not a shared array: vitest rejects instance objects reused across projects as duplicates. +const browserInstances = () => + process.env.VITEST_CHROMIUM_ONLY === 'true' + ? [{browser: 'chromium'}] + : [{browser: 'chromium'}, {browser: 'firefox'}]; + const sharedAlias = [ {find: '$lib', replacement: '/src/lib'}, {find: '$project', replacement: '/src/project'}, @@ -64,10 +72,7 @@ export default defineConfig({ enabled: true, headless: true, provider: playwright(), - instances: [ - {browser: 'chromium'}, - {browser: 'firefox'}, - ], + instances: browserInstances(), }, include: [browserTestPattern], exclude: [...e2eTestPatterns, ...configDefaults.exclude], @@ -97,10 +102,7 @@ export default defineConfig({ enabled: true, headless: true, provider: playwright(), - instances: [ - {browser: 'chromium'}, - {browser: 'firefox'}, - ], + instances: browserInstances(), }, setupFiles: ['./.storybook/vitest.setup.ts'], }, diff --git a/nx.json b/nx.json new file mode 100644 index 0000000000..e655b99cc6 --- /dev/null +++ b/nx.json @@ -0,0 +1,34 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "_comment": "nx is used graph-only here: `nx show projects --affected` answers which .NET projects and which pnpm packages a diff touches (see .github/scripts/affected.sh). Builds and tests still run through dotnet and pnpm; no nx targets are run and nothing is cached.", + "_sharedGlobals_comment": "namedInputs.sharedGlobals: a change to any of these marks every real project affected (the PR workflow then runs the whole PR tier). They reach a project only through a target whose `inputs` include `default`: @nx/dotnet gives every .NET project such targets (and its analyzer already lists the Directory.*/global.json files as evaluation inputs on its own); the viewer and the extension get one via the never-run `ci-affected` target in their project.json. Escalation tiers (.github/ci-tiers/*) deliberately have no such target, so shared files do not escalate.", + "defaultBase": "develop", + "plugins": [ + { + "plugin": "@nx/dotnet", + "options": {} + } + ], + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "sharedGlobals": [ + "{workspaceRoot}/global.json", + "{workspaceRoot}/LexBox.slnx", + "{workspaceRoot}/*.slnf", + "{workspaceRoot}/backend/Directory.Build.props", + "{workspaceRoot}/backend/Directory.Build.targets", + "{workspaceRoot}/backend/Directory.Packages.props", + "{workspaceRoot}/backend/Harmony*.props", + "{workspaceRoot}/frontend/pnpm-lock.yaml", + "{workspaceRoot}/frontend/pnpm-workspace.yaml", + "{workspaceRoot}/nx.json", + "{workspaceRoot}/package.json", + "{workspaceRoot}/pnpm-lock.yaml", + "{workspaceRoot}/pnpm-workspace.yaml", + "{workspaceRoot}/.github/scripts/affected.sh", + "{workspaceRoot}/.github/scripts/write-test-slnf.mjs", + "{workspaceRoot}/backend/FwLite/Taskfile.yml", + "{workspaceRoot}/backend/FwLite/scripts/**/*" + ] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..fcc5cc0a45 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "languageforge-lexbox", + "private": true, + "description": "Repo-root package: hosts nx + @nx/dotnet for affected-project detection only (graph-only; builds and tests still run through dotnet and pnpm). The web apps live in the separate pnpm workspace under frontend/.", + "packageManager": "pnpm@12.3.4", + "engines": { + "node": ">=26", + "pnpm": ">=12" + }, + "devDependencies": { + "@nx/dotnet": "23.3.0", + "nx": "23.3.0" + } +} diff --git a/platform.bible-extension/project.json b/platform.bible-extension/project.json new file mode 100644 index 0000000000..7c6b7fffc0 --- /dev/null +++ b/platform.bible-extension/project.json @@ -0,0 +1,17 @@ +{ + "$schema": "../node_modules/nx/schemas/project-schema.json", + "name": "extension", + "projectType": "application", + "sourceRoot": "platform.bible-extension/src", + "tags": ["npm"], + "_comment": "Registers the Platform.Bible extension as an nx project for affected detection only (the PR workflow's extension lint job keys on the name `extension`). npm scripts stay the way to build it; the one target below is never run.", + "targets": { + "ci-affected": { + "executor": "nx:noop", + "inputs": ["default", "{workspaceRoot}/frontend/viewer/src/lib/dotnet-types/**/*"], + "metadata": { + "description": "Never run. Its `default` input (which includes nx.json's sharedGlobals) makes a shared-file change mark this project affected; the dotnet-types are imported through tsconfig's @dotnet-types path." + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..a156821133 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1050 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +packages: + + '@pnpm/exe.darwin-arm64@12.3.4': + resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.3.4': + resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.3.4': + resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.3.4': + resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.3.4': + resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.3.4': + resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.3.4': + resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.3.4': + resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} + cpu: [x64] + os: [win32] + + pnpm@12.3.4: + resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.3.4': + optional: true + + '@pnpm/exe.darwin-x64@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64@12.3.4': + optional: true + + '@pnpm/exe.linux-x64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-x64@12.3.4': + optional: true + + '@pnpm/exe.win32-arm64@12.3.4': + optional: true + + '@pnpm/exe.win32-x64@12.3.4': + optional: true + + pnpm@12.3.4: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.3.4 + '@pnpm/exe.darwin-x64': 12.3.4 + '@pnpm/exe.linux-arm64': 12.3.4 + '@pnpm/exe.linux-arm64-musl': 12.3.4 + '@pnpm/exe.linux-x64': 12.3.4 + '@pnpm/exe.linux-x64-musl': 12.3.4 + '@pnpm/exe.win32-arm64': 12.3.4 + '@pnpm/exe.win32-x64': 12.3.4 + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@nx/dotnet': + specifier: 23.3.0 + version: 23.3.0(nx@23.3.0) + nx: + specifier: 23.3.0 + version: 23.3.0 + +packages: + + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@nx/devkit@23.3.0': + resolution: {integrity: sha512-xGSV4gCwQsVdt49VRcfxd2L31dAO/9t9Isv2bM5O89vyEzwd/tkW/IQwUzWn3OdhCnHayd37S1tDyJ5DilCv7w==} + peerDependencies: + nx: '>= 22 <= 24 || ^23.0.0-0' + + '@nx/dotnet@23.3.0': + resolution: {integrity: sha512-yxZM0a8Dr5hD2mThaCLbv9yHdfprDoQkiq3KMfcK8PLy8kPF7u+TdF2wZ8FlTcZaWKO/+Hp8JZVBk3ca+3M8TQ==} + + '@nx/nx-darwin-arm64@23.3.0': + resolution: {integrity: sha512-g06RId2Tn99ICwDFisZfDZ2vpM2bXvoCZeNtTWgwLm5mpJ4A7+akI7iXCe50CbzgE4waVRQCOAYpD1zPsKoUVQ==} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@23.3.0': + resolution: {integrity: sha512-6t9P5ZQ3LPuCf+HBDMTGtpKHjVvINL9/blpY4PucKjdC+vnNSL7uTZwng1ePenlNF0WFtLHqxUx/H1XgohRq0w==} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@23.3.0': + resolution: {integrity: sha512-B8Xuh36ZdMJ2SxpFfUITUvCZQTKoGOFHpSactxCx3ymFBUc24Z0B/hQRIYPv43icugcnp71dIwu6IvI66/aqtQ==} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@23.3.0': + resolution: {integrity: sha512-piMlCEI0tbHt1+QSgL8J1szlqBSGKV27nax8uvhTxleR2Evl3Kxa80BDIEObrUU3lz+AS4oKrcUHUeyJGPdCsQ==} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@23.3.0': + resolution: {integrity: sha512-SljVcZYwD4qpwseAUsEQmrXxE0vcPdDMEtvKlpkTProofAMeQFNEh4imw458TylOq7YeFVnNQdZcBnTO+1FeIw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-arm64-musl@23.3.0': + resolution: {integrity: sha512-UwzWPtcWxkAE8IOZl4M1eiqsa1yci+zlZ7Vv5ogozhm9jD9EHZVsgeWLrftW22OtU2u+ude/fzBeacuqUDOMtg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@nx/nx-linux-x64-gnu@23.3.0': + resolution: {integrity: sha512-PJerI1Ag/wkPh8XMBKO4Cx6Ao01QJsSk/ycFNmM7vzW19Db3OvsTSKqiql/z96zINI69MqLDZH2iwHMZXbJupQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-x64-musl@23.3.0': + resolution: {integrity: sha512-CGete35FhqkdlBZM/Oq6Ddu+yQ7UHMRV/ZYCoKeNU4/YIDIWze8VblF7PGKLU4k5JN4bRY1MmDmryHqVIMXMBA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@nx/nx-win32-arm64-msvc@23.3.0': + resolution: {integrity: sha512-T/ChrK9uzt9/29TQzc8FGkRGDJC130ltM77eKI3Z67OjyD4SKHhsiTu5GameH1BU72jcF4QEKFf74PoI9JzbuA==} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@23.3.0': + resolution: {integrity: sha512-ermx8Qy8qf0ZIEDiGPqIXxQ0OPEL8VVUOyvAsxDec8aft3pCFjX8LrFhlfnAfM6MBFs5Bnc9Xl9D8TMzFBgo9w==} + cpu: [x64] + os: [win32] + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + ejs@5.0.1: + resolution: {integrity: sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==} + engines: {node: '>=0.12.18'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nx@23.3.0: + resolution: {integrity: sha512-R7Kl+hURSGiG9uEjc19Cg3o+vGCDHqCVp6mioiBMEsOSol/LgzP+K8TVF06TnnSC8uzxMeB0ymu+LGMJGXnbbw==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.11.1 + '@swc/core': ^1.15.8 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@10.1.0: + resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + engines: {node: '>=18'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} + engines: {node: '>= 18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + + '@jest/diff-sequences@30.0.1': {} + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.9.0 + + '@nx/devkit@23.3.0(nx@23.3.0)': + dependencies: + ejs: 5.0.1 + minimatch: 10.2.5 + nx: 23.3.0 + semver: 7.8.5 + tslib: 2.8.1 + yargs-parser: 21.1.1 + + '@nx/dotnet@23.3.0(nx@23.3.0)': + dependencies: + '@nx/devkit': 23.3.0(nx@23.3.0) + ignore: 7.0.13 + tslib: 2.8.1 + transitivePeerDependencies: + - nx + + '@nx/nx-darwin-arm64@23.3.0': + optional: true + + '@nx/nx-darwin-x64@23.3.0': + optional: true + + '@nx/nx-freebsd-x64@23.3.0': + optional: true + + '@nx/nx-linux-arm-gnueabihf@23.3.0': + optional: true + + '@nx/nx-linux-arm64-gnu@23.3.0': + optional: true + + '@nx/nx-linux-arm64-musl@23.3.0': + optional: true + + '@nx/nx-linux-x64-gnu@23.3.0': + optional: true + + '@nx/nx-linux-x64-musl@23.3.0': + optional: true + + '@nx/nx-win32-arm64-msvc@23.3.0': + optional: true + + '@nx/nx-win32-x64-msvc@23.3.0': + optional: true + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@yarnpkg/lockfile@1.1.0': {} + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@4.0.3: {} + + base64-js@1.5.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.3 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.6.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@3.0.0: {} + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + ejs@5.0.1: {} + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + fs-constants@1.0.0: {} + + get-caller-file@2.0.5: {} + + has-flag@4.0.0: {} + + ieee754@1.2.1: {} + + ignore@7.0.13: {} + + ignore@7.0.6: {} + + inherits@2.0.4: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + lines-and-columns@2.0.3: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.12 + + minimist@1.2.8: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nx@23.3.0: + dependencies: + '@clack/core': 1.4.3 + '@clack/prompts': 1.7.0 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@emnapi/wasi-threads': 1.0.4 + '@jest/diff-sequences': 30.0.1 + '@napi-rs/wasm-runtime': 0.2.4 + '@tybys/wasm-util': 0.9.0 + '@yarnpkg/lockfile': 1.1.0 + '@zkochan/js-yaml': 0.0.7 + ansi-regex: 5.0.1 + ansi-styles: 4.3.0 + argparse: 2.0.1 + balanced-match: 4.0.3 + base64-js: 1.5.1 + bl: 4.1.0 + brace-expansion: 5.0.12 + buffer: 5.7.1 + bundle-name: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + clone: 1.0.4 + color-convert: 2.0.1 + color-name: 1.1.4 + default-browser: 5.2.1 + default-browser-id: 5.0.0 + defaults: 1.0.4 + define-lazy-prop: 3.0.0 + dotenv: 16.4.7 + dotenv-expand: 12.0.3 + ejs: 5.0.1 + emoji-regex: 8.0.0 + end-of-stream: 1.4.5 + escalade: 3.2.0 + escape-string-regexp: 1.0.5 + fast-string-truncated-width: 3.0.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 + figures: 3.2.0 + fs-constants: 1.0.0 + get-caller-file: 2.0.5 + has-flag: 4.0.0 + ieee754: 1.2.1 + ignore: 7.0.6 + inherits: 2.0.4 + is-docker: 3.0.0 + is-fullwidth-code-point: 3.0.0 + is-inside-container: 1.0.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + is-wsl: 3.1.0 + isexe: 2.0.0 + json5: 2.2.3 + jsonc-parser: 3.3.1 + lines-and-columns: 2.0.3 + log-symbols: 4.1.0 + mimic-fn: 2.1.0 + minimatch: 10.2.5 + minimist: 1.2.8 + npm-run-path: 4.0.1 + once: 1.4.0 + onetime: 5.1.2 + open: 10.1.0 + ora: 5.4.1 + path-key: 3.1.1 + picocolors: 1.1.1 + readable-stream: 3.6.2 + require-directory: 2.1.1 + resolve.exports: 2.0.3 + restore-cursor: 3.1.0 + run-applescript: 7.0.0 + safe-buffer: 5.2.1 + semver: 7.8.5 + signal-exit: 3.0.7 + sisteransi: 1.0.5 + smol-toml: 1.9.0 + string-width: 4.2.3 + string_decoder: 1.3.0 + strip-ansi: 6.0.1 + strip-bom: 3.0.0 + supports-color: 7.2.0 + tar-stream: 2.2.0 + tmp: 0.2.7 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + undici: 7.29.0 + util-deprecate: 1.0.2 + wcwidth: 1.0.1 + which: 3.0.1 + wrap-ansi: 7.0.0 + wrappy: 1.0.2 + y18n: 5.0.8 + yaml: 2.9.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 23.3.0 + '@nx/nx-darwin-x64': 23.3.0 + '@nx/nx-freebsd-x64': 23.3.0 + '@nx/nx-linux-arm-gnueabihf': 23.3.0 + '@nx/nx-linux-arm64-gnu': 23.3.0 + '@nx/nx-linux-arm64-musl': 23.3.0 + '@nx/nx-linux-x64-gnu': 23.3.0 + '@nx/nx-linux-x64-musl': 23.3.0 + '@nx/nx-win32-arm64-msvc': 23.3.0 + '@nx/nx-win32-x64-msvc': 23.3.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@10.1.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + resolve.exports@2.0.3: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + run-applescript@7.0.0: {} + + safe-buffer@5.2.1: {} + + semver@7.8.5: {} + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + smol-toml@1.9.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tmp@0.2.7: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + undici@7.29.0: {} + + util-deprecate@1.0.2: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + which@3.0.1: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..d866d2a294 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,20 @@ +# Repo-root pnpm config for the nx affected-detection tooling (see package.json). +# +# This is NOT the web-app workspace: that one stays at frontend/pnpm-workspace.yaml +# (pnpm picks the nearest pnpm-workspace.yaml walking up from the cwd, so commands +# run under frontend/ keep using it and its own lockfile). This file deliberately +# declares no `packages`, so the root workspace contains only the root package. +# nx discovers frontend/viewer through frontend/viewer/project.json instead. + +# Same supply-chain policy as frontend/pnpm-workspace.yaml. +minimumReleaseAge: 1440 +engineStrict: true + +# Same store layout as the frontend workspace (shared across worktrees). +virtualStoreType: global + +# nx's postinstall only pre-warms its project-graph cache; pnpm 12 refuses to +# install until each build script is allowed or denied, and graph-only use +# does not need it (nx builds the cache on first run instead). +allowBuilds: + nx: false