Skip to content

fix(approval-routes): repair the parafering seam and unshadow the reg… #3290

fix(approval-routes): repair the parafering seam and unshadow the reg…

fix(approval-routes): repair the parafering seam and unshadow the reg… #3290

Workflow file for this run

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.