fix(approval-routes): repair the parafering seam and unshadow the registry endpoint #3289
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Code Quality | |
| on: | |
| push: | |
| # DEFAULT BRANCHES ONLY. `pull_request` below carries every other branch. | |
| # | |
| # This was an allow-list of branch prefixes, and that was a gate with a | |
| # SILENT hole: a branch matching nothing got no CI at all, and its last | |
| # visible status was whatever it inherited — indistinguishable, on every | |
| # dashboard, from a branch that passed. Two live examples, both found | |
| # 2026-08-14: `perf/**` was uncovered in openconnector, where a merge | |
| # carrying unresolved conflict markers and 84 failing tests was pushed and | |
| # nothing ran; and `feat/**` was uncovered in openregister, because the | |
| # list said `feature/**`. | |
| # | |
| # The comment that stood here said adding prefixes was not the durable fix, | |
| # and that the durable fix was to let the pull_request trigger gate it. | |
| # THIS IS THAT CHANGE. | |
| # | |
| # What forced it now: a push to a branch with an open PR ran the SAME 34 | |
| # jobs TWICE on the same commit. `concurrency` cannot dedupe them — the | |
| # group is suffixed by event name deliberately (.github#540: a | |
| # default-branch push carries jobs a PR run does not, and a dispatch must | |
| # not be cancellable by a standing release PR), so the two events sit in | |
| # different lanes BY DESIGN and both run to completion. Measured fleet-wide | |
| # 2026-08-25..27, 659 of 2,106 Code Quality runs were that duplicate — 31% | |
| # of the fleet's most expensive workflow, re-deciding a commit another run | |
| # was already deciding. The account ceiling is 60 concurrent jobs (Team | |
| # plan); the fleet was measured at 53 running with 1,528 jobs queued behind | |
| # them, the oldest run 7 hours old and not yet started. | |
| # | |
| # NO BRANCH LOSES ITS FLOOR. merge-hygiene.yml runs on `'**'` — every | |
| # branch anyone pushes, no prefix list to forget — and it is the check | |
| # `development` actually requires. That is the smoke alarm; this workflow | |
| # is the fire brigade and belongs on the PR. Of 668 feature-branch push | |
| # runs in that window, only NINE were on a branch with no PR run beside | |
| # them. | |
| # | |
| # The default branches STAY: their push runs are not duplicates, they are | |
| # the only carrier of Coverage Baseline Check, SBOM and Features Extract, | |
| # none of which run on a pull_request event. | |
| branches: | |
| - main | |
| - development | |
| pull_request: | |
| branches: [main, master, development, beta] | |
| workflow_dispatch: | |
| jobs: | |
| quality: | |
| uses: ConductionNL/.github/.github/workflows/quality.yml@main | |
| with: | |
| app-name: decidiq | |
| php-version: "8.3" | |
| php-test-versions: '["8.3", "8.4"]' | |
| # stable31 is REMOVED because it tested an impossible configuration, not | |
| # because we are trimming coverage. `additional-apps` below installs | |
| # openregister, which declares min-version="32" (ConductionNL/openregister#2384), | |
| # so on NC31 `occ app:enable openregister` refuses with "not compatible with | |
| # this version of the server". The shared workflow runs that as | |
| # `php occ app:enable "$name" || echo "::warning::Failed to enable $name"`, | |
| # so the failure is a WARNING and the job continues without its data layer, | |
| # then dies ~70s later on missing schemas — which reads like an app fault. | |
| # | |
| # Order mattered as much as membership: the newman, playwright and | |
| # journeydoc-capture jobs each check out the server at | |
| # `fromJSON(inputs.nextcloud-test-refs)[0]`, so stable31 sitting FIRST put | |
| # all three on the one version openregister cannot load. | |
| # | |
| # THE LIST IS THE WHOLE DECLARED RANGE. An earlier revision of this comment | |
| # said "stable33 is deliberately NOT added: this removes an impossible leg, | |
| # it does not widen the matrix" — but the same change also dropped stable32, | |
| # which was NOT impossible, it was the declared floor. appinfo/info.xml | |
| # declares <nextcloud min-version="32" max-version="34"/>, so 32, 33 and 34 | |
| # each get a leg; anything narrower advertises a range to the App Store that | |
| # no job touches. | |
| nextcloud-test-refs: '["stable34", "stable32", "stable33"]' | |
| enable-psalm: true | |
| enable-phpstan: true | |
| enable-phpmetrics: true | |
| enable-frontend: true | |
| enable-eslint: true | |
| enable-phpunit: true | |
| enable-newman: true | |
| # Every Newman collection seeds its own fixtures through OpenRegister's | |
| # object API (`/apps/openregister/api/objects/decidiq/<schema>`) — ADR-022 | |
| # keeps plain CRUD there rather than in decidiq controllers. Without | |
| # OpenRegister checked out the seed POSTs answer 404, every downstream id | |
| # interpolates to the empty string, and the collections fail wholesale on | |
| # a cause that has nothing to do with the code under test (measured: | |
| # 206 of 282 assertions failed in run 30899265429). | |
| # | |
| # `ref: development` matches the rest of the fleet (opencatalogi, | |
| # openconnector, procest, softwarecatalog, scholiq, pipelinq). It is not | |
| # interchangeable with `main`: decidiq's appinfo/routes.php builds on | |
| # `\OCA\OpenRegister\AppHost\Routes`, which does not exist on OpenRegister | |
| # `main` (0.2.19) at all — only on `development`. | |
| additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]' | |
| # Two collections (decidiq-meeting-agenda, decidiq-user-settings) carry | |
| # no collection-level `baseUrl`/`noAuthBase`/`adminUser`/`adminPass`, and | |
| # the workflow's ad-hoc fallback defines `base_url`-style names instead. | |
| # Every request in those two therefore left `{{baseUrl}}` unresolved and | |
| # errored before it was sent — 42 assertions failing with no server | |
| # involved. The committed environment file supplies all four to every | |
| # collection uniformly. | |
| newman-environment-path: tests/integration/decidiq-environment.json | |
| enable-sbom: true | |
| # ── E2E browser tests ──────────────────────────────────────────────── | |
| # `enable-playwright` defaults to FALSE and was never set here, so the | |
| # "E2E Tests (Playwright)" job has reported `skipped` on every run this | |
| # repo has ever produced — while the tree ships a root | |
| # `playwright.config.ts` and 28 gating spec files under `tests/e2e/` | |
| # (spec-coverage/, workflows/, integration-registry.spec.ts). A skipped | |
| # job renders in the Quality Report exactly like a passing one, so the | |
| # whole browser tier was invisible rather than absent. | |
| # | |
| # `playwright-test-path` does double duty in the shared workflow: | |
| # 1. it is the directory the "Validate Playwright tests exist" step | |
| # counts *.spec.ts in; | |
| # 2. it is the FIRST place the run step looks for a config — | |
| # `${playwright-test-path}/playwright.config.ts`, falling back to | |
| # the repo root only if that file is absent. | |
| # We ship tests/e2e/playwright.config.ts precisely so lookup (2) hits it. | |
| # The run step passes no `--project`, so the ROOT config would run all | |
| # three of its projects — including `visual` (pixel baselines whose own | |
| # header states a CI Linux runner cannot byte-match a dev-container | |
| # baseline) and `docs-capture` (journeydoc screenshot re-shoots, which | |
| # have their own dedicated job). The tests/e2e config declares only the | |
| # `chromium` regression project, and writes its report/output to the app | |
| # root, where the workflow's upload steps actually look. | |
| # | |
| # OpenRegister is already checked out for Newman above (`additional-apps`) | |
| # and the Playwright job honours the same input — which it must: the specs | |
| # read and seed fixtures through `/apps/openregister/api/objects/decidiq/ | |
| # <schema>` and assert on `window.OCA.OpenRegister.integrations`. | |
| enable-playwright: true | |
| playwright-test-path: tests/e2e | |
| # OpenRegister being INSTALLED is not the same as decidesk's register | |
| # being IMPORTED, and the difference is silent. `occ app:enable decidiq` | |
| # runs a repair step that is supposed to import | |
| # `lib/Settings/decidesk_register.json` + the 24 `register.d/` fragments, | |
| # but an IRepairStep has no user session, OpenRegister's RBAC denies the | |
| # write as 'Anonymous', and the step catches \Throwable and downgrades it | |
| # to a warning — so `occ app:enable` exits 0 with no register at all. | |
| # In that state every UI spec times out on an empty list and every | |
| # `expect(resp.ok()).toBe(true)` against | |
| # /apps/openregister/api/objects/decidiq/<schema> fails with a message | |
| # that accuses the selector, never the missing import. | |
| # | |
| # ci-seed.sh does the import explicitly over the admin HTTP API (which | |
| # has a real session), forced, then VERIFIES the register slug, 18 schema | |
| # slugs, four object collections, and that the SPA bundle actually serves | |
| # as JavaScript. A bad provision becomes ONE loud step failure instead of | |
| # two dozen misleading spec failures. | |
| # | |
| # It also sets and reads back `htaccess.IgnoreFrontController`. Without | |
| # it, `occ maintenance:install` leaves that flag FALSE, JS `generateUrl` | |
| # therefore prefixes `/index.php`, and decidiq's | |
| # `createWebHistory(generateUrl('/apps/decidiq'))` router base becomes | |
| # `/index.php/apps/decidiq` while every spec navigates to | |
| # `/apps/decidiq/...`. vue-router only strips a base the path starts | |
| # with, so nothing matched and the catch-all `redirect: '/'` landed EVERY | |
| # deep link on the dashboard. | |
| # | |
| # cwd for this step is the Nextcloud server root. | |
| playwright-seed-command: 'bash apps/decidiq/tests/e2e/ci-seed.sh' | |
| # ── Frontend Check legs ────────────────────────────────────────────── | |
| # `frontend-checks` defaults to `[]`, and an empty list means the shared | |
| # workflow emits NO "Frontend Check" job at all — so these two validators | |
| # ran nowhere while the run still looked complete. Both are self-contained | |
| # `node` scripts, which is what a leg has to be (each leg is a fresh job | |
| # with its own checkout + `npm ci`). | |
| # `test:unit` is NOT listed: the shared "Frontend Tests (unit)" job | |
| # already falls back to it when there is no `test` script, and this repo | |
| # has none — its 282 vitest tests are already covered there. | |
| # `test:l10n:parity` is NOT listed either: measured on this tree it is | |
| # short 289+ translations across the required locales. That is a | |
| # translation backlog, and a permanently-red leg is one that gets | |
| # switched off again. | |
| # | |
| # `format` (prettier --check) is listed because the shared workflow has NO | |
| # prettier job of its own — `quality.yml` runs eslint and stylelint and | |
| # mentions prettier ZERO times. This repo already carries | |
| # `@nextcloud/prettier-config` and a `format` script, so without this leg | |
| # `npm run format` never runs outside a developer's editor and the tree | |
| # drifts straight back out of format between merges — the same inert- | |
| # formatter failure mode that made the old `.prettierrc` worth deleting. | |
| # Centralising the config never stopped drift; the gate does. | |
| # Measured on this tree before enabling: PASSES, 197 of 203 tracked | |
| # frontend files in scope (docs/ excluded via .prettierignore; build | |
| # output via .gitignore, which prettier 3 also reads). | |
| # `check:nav-ceiling` (added with the nav-ceiling-gate change) enforces | |
| # ADR-004's six-item top-level navigation ceiling: it rebuilds the | |
| # effective menu the same way src/main.js's buildManifest pipeline | |
| # does (base manifest + every manifest.d/*.json fragment + menu- | |
| # layout.json) and fails on a primary-nav count over 6, or on any | |
| # fragment top-level menu entry menu-layout.json never explicitly | |
| # placed (relocation / removal / settingsSection lift). Without this, | |
| # ADR-004's ceiling had no mechanical enforcement — 22 independent | |
| # fragments each added their own top-level entry and the nav grew | |
| # back to 44 items with no single commit that visibly broke the rule. | |
| # `check:l10n-js` regenerates l10n/<locale>.js from the JSON catalogue and | |
| # fails when the committed file is stale. Nextcloud serves ONLY the JS half | |
| # to a browser — raw JSON out of an app directory is a 404 — so a catalogue | |
| # that exists only as JSON renders English in the entire UI while every | |
| # server-rendered string is translated, with no error anywhere. | |
| # `check:schema-l10n` is a RATCHET, not a gate. Every string inside a form | |
| # comes from the schema and is a key in THIS app's catalogue; an absent key | |
| # renders the English source inside an otherwise translated form, silently. | |
| # The fleet had 30,459 such strings, so this records the current count and | |
| # fails only when it GROWS — burning it down stays an ordinary PR. | |
| frontend-checks: '["check:manifest", "check:nav-ceiling", "test:l10n", "format", "check:l10n-js", "check:schema-l10n"]' | |
| # ── Coverage ratchet ───────────────────────────────────────────────── | |
| # `enable-coverage-guard` defaults to FALSE, which is why both | |
| # "Coverage Baseline Protection" (PR side) and "Coverage Baseline Check" | |
| # (push side) have only ever reported `skipped`. It needs two inputs this | |
| # repo did not have, both added in this commit: | |
| # `scripts/coverage-guard.php` (byte-identical to the copies in | |
| # openregister and procest) and `.coverage-baseline` = 57.66, this repo's | |
| # own measured coverage (8687 of 15065 statements) read from clover.xml | |
| # in the `coverage-report` artifact of run 30911223203. | |
| enable-coverage-guard: true | |
| # ── Hydra mechanical gates ─────────────────────────────────────────── | |
| # `enable-hydra-gates` defaults to FALSE, so this tier has never executed | |
| # here — the job reported `skipped`, which the Quality Report renders | |
| # identically to a pass. .github#149 is what made this viable: gate-7 | |
| # (no-admin-idor) now follows delegation, so the 11 MinutesController-style | |
| # methods whose guards are reached through `staffAction()` → `requireStaff()` | |
| # are no longer flagged. | |
| # `enable-axe` deliberately NOT set: a vanilla Nextcloud 34 already carries | |
| # serious/critical violations on core's OWN routes that DOM scoping does | |
| # not remove. Enabling axe is a separate decision. | |
| enable-hydra-gates: true | |
| # No `hydra-gates-ref` here on purpose. The shared workflow defaults it | |
| # to @main, and this workflow is itself consumed at @main, so the two | |
| # sides move together and a gate fix reaches this repo without a commit | |
| # in this repo. A pin is a silent expiry date: 22 repos sat on v1.0.1 and | |
| # 16 gates were dead fleet-wide while every one reported PASS (.github#159), | |
| # and a default flipped at @main later reached those old runners and made | |
| # them red on gates they had no subject matter for (.github#173). | |
| # To hold this repo still for a specific reason, set the input explicitly | |
| # and say why — it is still honoured. To roll back for everyone, revert on | |
| # ConductionNL/.github main. |