Files
iptvnator/docs/architecture/nx-workspace-boundaries.md
T
4gray cfa602d5b1 ci: cut PR runner waste and harden workflow permissions (#1226)
Pipeline audit follow-up: reduce wasted runner time on PRs and tighten CI
security, without reducing what actually gets validated.

Runner-time waste:
- Concurrency with PR-only cancel-in-progress on CI, E2E, and docker-build,
  so a new push cancels the previous commit's still-running checks. Non-PR
  runs use the unique run_id as the group, because GitHub keeps at most one
  pending run per group even with cancel-in-progress: false — a shared ref
  group could silently drop a queued master run.
- paths-ignore for docs-only changes (Markdown, docs/, .plans/, .codex/,
  .claude/) on the Electron build matrix and the E2E suites; E2E also skips
  apps/website/**. The build workflow keeps apps/website/** because its Linux
  job builds the website to verify AppStream assets. Tag pushes are
  unaffected: GitHub does not evaluate paths filters for tags.
- PRs lint affected projects only; master pushes keep the full run-many.
  Lint-global inputs (eslint.config.mjs, tools/eslint/**) now mark all 41
  lint projects affected, including the run-commands targets database and
  packaging, so the max-lines baseline cannot be widened without lint.

Hardening:
- Explicit least-privilege permissions on CI, E2E, and build-and-make; the
  create-release job keeps its job-level contents: write. The repository
  default workflow token was switched to read-only.
- New actionlint job (image pinned by digest, shellcheck at warning+), with
  the shared-anchor false positive suppressed in .github/actionlint.yaml.
  Fixed one real finding: unquoted $GITHUB_OUTPUT.
- .github/dependabot.yml: weekly cadence, minor+patch grouped per ecosystem
  (npm, GitHub Actions, Docker), majors stay individual PRs.

Docs updated: CLAUDE.md, docs/architecture/nx-workspace-boundaries.md, and
docs/architecture/validation-map.md now describe affected-lint on PRs and the
E2E path-filter exceptions.
2026-07-25 14:37:40 +02:00

3.6 KiB

Nx Workspace Boundaries

This document records the current monorepo boundary conventions for IPTVnator.

Fresh Worktree Bootstrap

Install dependencies before using Nx discovery or targets:

pnpm install --frozen-lockfile
pnpm nx show projects

pnpm nx show projects depends on the workspace-local Nx packages under node_modules. In a fresh worktree without dependencies it will fail before it can inspect project metadata.

Project Tags

Every Nx project should carry three tag families in project.json:

  1. scope:* - ownership area, for example scope:portal, scope:workspace, scope:shared, scope:electron, scope:e2e, or scope:dev-tools.
  2. domain:* - product/runtime domain, for example domain:xtream, domain:stalker, domain:m3u, domain:playback, domain:web, or domain:shared-runtime.
  3. type:* - architectural role, for example type:app, type:e2e, type:dev-app, type:feature, type:ui, type:data-access, type:util, type:tool, or type:website.

eslint.config.mjs uses these tags with @nx/enforce-module-boundaries. When adding a project, choose tags before adding imports so dependency direction is clear from the start.

Import Aliases

Use scoped @iptvnator/* aliases from tsconfig.base.json.

Examples:

import { SettingsStore } from '@iptvnator/services';
import { Playlist } from '@iptvnator/shared/interfaces';
import { DialogService } from '@iptvnator/ui/components';

Do not introduce legacy bare aliases such as:

  • components
  • m3u-state
  • m3u-utils
  • services
  • shared-interfaces
  • shared-portals
  • remote-control
  • database
  • database-schema
  • database-path-utils
  • workspace-dashboard-feature
  • workspace-dashboard-data-access

The lint config blocks these aliases so new code uses the same visible namespace and ownership convention.

Buildable libraries that have a local package.json should use the same public name as their scoped alias. Nx uses package.json.name when it rewrites buildable dependency paths to dist/ during @nx/js:tsc builds.

Dependency Direction

  • type:feature may use type:feature, type:ui, type:data-access, and type:util.
  • type:ui may use type:ui, type:data-access, and type:util.
  • type:data-access may use type:data-access and type:util.
  • type:util may use only type:util.

If a change needs a dependency in the opposite direction, move the shared contract into a lower-level library instead of weakening boundaries.

Portal collection orchestration that reads/writes favorites, recent items, live playback, or EPG data belongs in libs/portal/shared/data-access, not libs/portal/shared/util. That keeps pure collection helpers importable by Xtream/Stalker data-access libraries while allowing shared UI to use provider-specific collection services without creating cycles.

Note: workspace-shell-util (libs/workspace/shell/util) is tagged type:data-access despite its path. It exports injectable services such as WorkspaceStartupPreferencesService that depend on @iptvnator/services, and it must stay eagerly importable from apps/web/src/app/app.routes.ts without pulling the lazy-loaded workspace shell feature bundle into the initial chunk.

CI Enforcement

The Lint job in .github/workflows/ci.yml runs pnpm nx affected --target=lint on PRs and pnpm nx run-many --target=lint --all on master pushes, so @nx/enforce-module-boundaries violations, legacy bare-alias imports, and max-lines violations fail CI. Root config or lockfile changes mark every project affected, so the boundary rules cannot be dodged on PRs. Run pnpm run lint locally before pushing.