diff --git a/.github/workflows/pull-request-from-branch-check.yaml b/.github/workflows/branch-protection.yml
similarity index 91%
rename from .github/workflows/pull-request-from-branch-check.yaml
rename to .github/workflows/branch-protection.yml
index 28065f9b5..7ef08ceae 100644
--- a/.github/workflows/pull-request-from-branch-check.yaml
+++ b/.github/workflows/branch-protection.yml
@@ -4,6 +4,8 @@ on:
pull_request:
branches: [main, beta]
+permissions: {}
+
jobs:
branch-protection:
uses: ConductionNL/.github/.github/workflows/branch-protection.yml@main
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 404bae3de..4cbe9e580 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -1,82 +1,458 @@
name: Code Quality
on:
- # `push` on main is the load-bearing one, and it was MISSING. Without it a
- # push to the release branch built nothing at all — not red, not green,
- # simply no run. Measured 2026-08-09: openconnector `main` had ZERO Code
- # Quality runs on record, so nobody could say what state the branch users
- # install was in. A branch with no runs shows no red.
- #
- # `development` has carried this trigger for some time; it never reached
- # `main` because the corrected workflow travels the release train and no
- # release has carried it across. See ConductionNL/.github#285.
push:
- branches: [main, development, feature/**, bugfix/**, hotfix/**]
+ # An ALLOW-LIST of prefixes is a gate with a hole in it, and the hole is
+ # silent: a branch whose name matches nothing simply gets no CI, and its last
+ # visible status is whatever it inherited. On 2026-08-14 a merge with
+ # unresolved conflict markers and 84 failing tests was pushed to
+ # `perf/predicted-page-fanout` — `perf/**` was not on this list, so nothing
+ # ran and nothing said so.
+ #
+ # Prefixes added rather than replaced with `**` because this workflow is not
+ # cheap (PHPUnit matrix, Newman, Playwright); running it on every scratch
+ # branch would be a real bill. The fast structural checks DO run on `**` —
+ # see merge-hygiene.yml, which exists because of that same incident.
+ #
+ # ⚠️ Adding a prefix here is not the durable fix. Anyone inventing a new one
+ # is uncovered again until someone remembers this file. The durable fix is
+ # branch protection requiring a PR into development, which is where the
+ # pull_request trigger below already gates properly.
+ branches:
+ - main
+ - development
+ - feature/**
+ - bugfix/**
+ - hotfix/**
+ - perf/**
+ - refactor/**
+ - chore/**
+ - fix/**
pull_request:
- branches: [main, master, development]
+ # Spelled out rather than left to the default so the set is reviewable.
+ # `synchronize` — a push to an open PR — is the load-bearing one: without
+ # it the suite runs once at PR-open and every later commit merges unchecked
+ # under the first run's green tick. Cost is bounded by the `concurrency`
+ # block below, which cancels the in-flight run for the same head ref.
+ types: [opened, reopened, synchronize]
+ branches: [main, beta, development]
workflow_dispatch:
+# Deduplicating a `push` run against the `pull_request` run for the SAME head
+# ref is the point of this block, and for a feature branch it is exactly right:
+# two runs of identical jobs, one of them wasted.
+#
+# It is wrong for `main` and `development`, because the push run there is NOT a
+# duplicate — it carries the push-only "Coverage Baseline Check", which is the
+# whole push side of the coverage ratchet. And those two branches always have an
+# open PR whose `head_ref` IS the branch name: the standing "Release: merge
+# development into beta". `github.head_ref` on that PR run and `github.ref_name`
+# on the push run both render `development`, so both landed in the identical
+# group `quality-development`, and `cancel-in-progress` killed whichever started
+# first — always the push run, by ~9 seconds.
+#
+# Measured on the merge of #1151 (ddd74da4): run 31045325182, event `push`,
+# CANCELLED; run 31045337494, event `pull_request`, survived. Same pattern on
+# 3b9371f1, 75190bca and f2219e09 — four for four, so the push run on
+# `development` has never reached a verdict. "Coverage Baseline Check" therefore
+# reported `skipped` on the surviving run (correctly — it is push-only) while
+# never once executing on the run that could have run it. #1151 enabled a job
+# that could not reach a verdict: a dead gate of the permanently-pending shape,
+# which is invisible because a skipped job renders like a passing one.
+#
+# Suffixing only the default-branch push keeps feature-branch dedup untouched
+# (`quality-feature/x` for both events, exactly as before) and gives the two
+# default branches' push runs a lane of their own.
concurrency:
- group: quality-${{ github.head_ref || github.ref }}
+ group: quality-${{ github.head_ref || github.ref_name }}${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'development')) && '-push' || '' }}
cancel-in-progress: true
-jobs:
- php-checks:
- name: ${{ matrix.check.name }}
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- check:
- - { name: "PHP Lint", command: "composer lint" }
- - { name: "PHPCS", command: "./vendor/bin/phpcs --standard=phpcs.xml" }
- - { name: "PHPMD", command: "./vendor/bin/phpmd lib text phpmd.xml" }
- - { name: "Psalm", command: "./vendor/bin/psalm --threads=1 --no-cache --output-format=github" }
- - { name: "PHPStan", command: "./vendor/bin/phpstan analyse --memory-limit=1G" }
- - { name: "PHPUnit", command: "./vendor/bin/phpunit --colors=always" }
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
- extensions: mbstring, xml, ctype, iconv, intl, dom, filter, gd, json, posix, zip, soap
- tools: composer:v2
+# Permission CEILING for the called quality pipeline. GitHub statically
+# validates the called workflow's declared job permissions against this
+# grant — even for jobs that are disabled — so capping any of these at
+# `read` makes the whole call fail to START (zero jobs, no annotations).
+# Union of what the nested jobs need: journeydoc-capture (contents+actions
+# write), update-baseline / features-extract (contents write), the Quality
+# Report PR comment (issues / pull-requests write), and packages: read for
+# pulling org images.
+permissions:
+ contents: write
+ actions: write
+ issues: write
+ pull-requests: write
+ packages: read
- - name: Cache Composer dependencies
- uses: actions/cache@v4
- with:
- path: vendor
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- restore-keys: ${{ runner.os }}-composer-
-
- - name: Install dependencies
- run: composer install --no-progress --prefer-dist --optimize-autoloader
+jobs:
+ quality:
+ # Skip the duplicate run a branch-creation push fires alongside the PR event.
+ if: github.event_name != 'push' || github.event.created != true
+ uses: ConductionNL/.github/.github/workflows/quality.yml@main
+ with:
+ app-name: openconnector
+ # composer.json `config.platform.php` is pinned to 8.3, so phpcs/phpmd/etc
+ # need the matching runtime or `vendor/composer/platform_check.php` aborts
+ # with `Composer dependencies require a PHP version ">= 8.3.0"`.
+ php-version: "8.3"
+ # Set explicitly, because the shared workflow defaults to
+ # `["stable31", "stable32"]` and stable31 CANNOT WORK here. openconnector
+ # declares openregister as a hard dependency, and openregister
+ # declares min-version="32". On NC31 the E2E job logs
+ # App "Open Register" cannot be installed because it is not compatible
+ # with this version of the server.
+ # then continues with a WARNING, so the run proceeds without its central
+ # dependency and fails ~70s later on missing tables — which reads like a
+ # migration fault rather than a missing app. Testing a version the app
+ # cannot support produces red that says nothing. See issue #1172.
+ #
+ # WHY THIS IS NOW stable34 AND NOT `["stable32", "stable33"]`. This PR
+ # moves the dev requirement to `nextcloud/ocp ^34`, so the stubs every
+ # static-analysis leg resolves against are NC34's. Running the suite
+ # against an older server would check the app under one set of API
+ # signatures and analyse it under another, and the two disagreeing is
+ # exactly the class of defect the analysers exist to find.
+ #
+ # ✅ THE NARROWING IS CLOSED, THE FIRST WAY. The previous revision of this
+ # comment recorded a deliberate trade: info.xml declared three server
+ # versions while the suite exercised one, leaving NC32 and NC33 unmeasured
+ # rather than known-broken. It named its own closure condition — "restoring
+ # the older refs alongside stable34 once ocp ^34 stubs are proven compatible
+ # with them" — and that condition is now met: portaliq runs stable32,
+ # stable33 and stable34 against `nextcloud/ocp ^34` and all six PHPUnit
+ # legs pass (run 31599055849). So the refs come back rather than info.xml's
+ # floor going up, and the two numbers agree again.
+ nextcloud-test-refs: '["stable34", "stable32", "stable33"]'
+ # `hydra-gates-require-full-coverage: false` is REMOVED here, exactly as
+ # the comment it replaced instructed: "Remove this line in the change
+ # that wires those producers up, so the coverage requirement arrives with
+ # the coverage."
+ #
+ # WHAT THE FLAG ACTUALLY DID. It is not a gate and it does not decide any
+ # finding. It answers one question: when a gate DID NOT RUN, is that a
+ # failure? The gate package sorts every non-run into three categories —
+ # `na` (subject matter absent: nothing is unverified), `structural`
+ # (subject matter EXISTS, nothing produced the gate's input) and `wiring`
+ # (the gate's own helper or tool is missing). Only `structural` and
+ # `wiring` count against coverage. With the flag set to false, a gate
+ # whose machinery had quietly stopped existing reported as green.
+ #
+ # That is not hypothetical in this fleet: a missing helper made gate-7
+ # report PASS over 11 real unguarded IDOR endpoints, and openregister
+ # reported `success` while its own log read "EXCEPT GATES 4 33, WHICH DID
+ # NOT RUN". A waiver here means openconnector's green does not include
+ # the coverage assertion at all.
+ #
+ # THE ORIGINAL DEFERRAL NO LONGER HOLDS, and not for the reason this PR
+ # first gave. Re-measured on `development` @5ab1c9df, FULL TREE, with the
+ # gate package cloned fresh from ConductionNL/.github@main (756fe89) —
+ # not the local submodule, which was 38 commits behind and is therefore a
+ # different program:
+ #
+ # [hydra-gates] COVERAGE: 63 of 64 declared gates reported a result
+ # (1 not applicable to this repo/diff; 63 of 63 applicable gates ran).
+ #
+ # ZERO structural skips, ZERO wiring skips. The single not-applicable was
+ # gate-33 axe-core, which declared itself so because this caller had not
+ # set `enable-axe`.
+ #
+ # THAT IS NO LONGER TRUE: `enable-axe: true` is set below, and gate-33
+ # now reports a real verdict instead of `na`. CONFIRMED IN CI on the PR
+ # that turned it on (#1211, job 93785212530):
+ #
+ # [gate-33] axe-core: PASS
+ # [hydra-gates] gate-33 axe-core: report read — 0 violation(s)
+ # present, 0 serious/critical. A PASS here is a PASS
+ # over that number, not over silence.
+ #
+ # "report read" is the part that matters: the report survived the
+ # provenance check rather than being rejected and deleted, which is the
+ # failure mode where gate-33 goes back to skipping loudly.
+ #
+ # DO NOT READ THE COVERAGE LINE AS A CONSTANT. An earlier draft of this
+ # comment predicted it would move from `63 of 64` to `64 of 64`. That
+ # was wrong, and wrong in the way this fleet keeps getting caught by:
+ # the count is a function of the DIFF, not of the repo. The same run
+ # that produced the PASS above printed
+ #
+ # [hydra-gates] COVERAGE: 28 of 64 declared gates reported a result
+ # (36 not applicable to this repo/diff; 28 of 28 applicable gates ran).
+ #
+ # because a two-file PR gives 36 gates no subject matter. The `63 of 64`
+ # figure quoted higher up came from a FULL-SCOPE run. Compare like with
+ # like or the number means nothing. What is genuinely comparable is that
+ # gate-33 is now inside the applicable set in both scopes instead of
+ # declaring itself `na`.
+ #
+ # ⚠️ AND THE SAME CAVEAT USED TO APPLY TO A GATE THAT PRINTS `PASS`.
+ # gate-16 spec-coverage takes its base from `HYDRA_GATE_BASE_REF`, and
+ # the runner supplied none on `--full`, so a full-scope run ON
+ # `development` diffed the branch against itself, inspected nothing, and
+ # printed PASS — a PASS that COUNTED toward "N of N applicable gates
+ # ran" (ConductionNL/.github#361). **#364 has since MERGED**: a
+ # full-repo run with no diff now reports
+ # `NOT APPLICABLE — full-repo run computed NO diff … This is NOT a
+ # pass.` So do not read a historical green gate-16 cell on this repo as
+ # a measurement — re-measure with an explicit base.
+ #
+ # Measured here on `development` @7c1d9c6d with the canonical package
+ # @81c8c97 (which still carried the old behaviour), changing ONLY the
+ # base:
+ #
+ # base origin/development (= what CI's --full run uses) -> count=0
+ # base origin/beta -> count=0
+ # base origin/main -> count=30
+ # --mode report (whole tree, no diff at all) -> 30
+ #
+ # A committed, untagged public method planted in lib/Service and a
+ # matching Vue method moved every one of those numbers by exactly +2 and
+ # named both plants, so the zeros above are real zeros and not a dead
+ # gate. openconnector's genuine gate-16 debt is 30 methods, all backend.
+ #
+ # Gate 4 was never starved by a producer this repo declines to switch
+ # on; it declares itself not applicable. And gate-24
+ # integration-parity — named by the first version of this comment as the
+ # real gap, which it then was — now PASSES: `scripts/check-integration-
+ # parity.sh` landed on `development` after this PR was opened, so the
+ # parity script arrived exactly as that comment said it should, ahead of
+ # the flag being dropped.
+ #
+ # PROOF THIS SETTING CAN STILL FAIL, rather than being inert. On the same
+ # tree, deleting `scripts/check-integration-parity.sh`:
+ #
+ # [gate-24] integration-parity: SKIPPED (structural) — ... this repo
+ # DOES register integration leaves ... server↔JS leaf parity is
+ # UNVERIFIED
+ # [hydra-gates] COVERAGE: 62 of 63 applicable gates ran
+ #
+ # which is precisely the shape the runner exits 98 on, and which
+ # quality.yml turns into `::error::hydra-gates passed every gate that
+ # ran, but a gate whose SUBJECT MATTER EXISTS did not report`. So this
+ # repo has live subject matter for the assertion; it is not being turned
+ # on over nothing.
+ #
+ # NOTE ON WHAT THIS DOES *NOT* CHANGE. Exit 98 is reached only when no
+ # gate FAILED — a run with failures is already red and exits with the
+ # failure count. Removing this line therefore adds a verdict where there
+ # was none; it cannot mask one. The same full-tree run still shows
+ # gate-38 and gate-57 failing on inherited debt in files no PR is
+ # touching; both are tracked separately and neither is in scope for a
+ # diff-scoped CI run.
+ enable-phpcs: true
+ enable-psalm: true
+ enable-phpstan: true
+ enable-phpmetrics: true
+ enable-frontend: true
+ enable-eslint: true
- - name: ${{ matrix.check.name }}
- run: ${{ matrix.check.command }}
- frontend-quality:
- name: Frontend Quality
- runs-on: ubuntu-latest
+ # Build the bundle in CI. Nothing here did until now, and the gap hid two
+ # real defects for weeks: @nextcloud/dialogs pinned at a vue@2.7 release
+ # inside a Vue 3 app, and fifty NcSelect/NcTextArea listeners still on the
+ # v8 `@input` contract that v9 does not emit. Both compile, lint and pass
+ # unit tests — only an actual build (and, for the listeners, using the
+ # control) shows them.
+ #
+ # A broken bundle is also silent in production: Nextcloud serves whatever
+ # js/ was last committed, so the app keeps working from a stale build
+ # while main cannot be rebuilt at all.
+ # `check:specs` and `test:l10n` are added to this list below rather than
+ # here; see the "Frontend Check legs" block further down. The value lives
+ # in exactly one place — a second `frontend-checks:` key in the same
+ # `with:` block would be accepted last-one-wins by every YAML parser
+ # involved and would read as configured while being decided elsewhere.
+ # Integration + E2E run against a live Nextcloud server with OR
+ # checked out as an additional app (collections + journeys exercise
+ # /index.php/apps/openregister/api/objects/openconnector/*).
+ enable-newman: true
+ newman-collection-path: "tests/postman"
+ newman-environment-path: "tests/postman/openconnector.postman_environment.json"
+ # WHY THIS SEED EXISTS. The collection's "01 — Fixture setup (via OR)"
+ # folder POSTs to /apps/openregister/api/objects/openconnector/;
+ # since the chain-C cutover every openconnector entity is an OpenRegister
+ # object. Without a seed step the `openconnector` register does not
+ # exist, all 8 fixture POSTs return 404, `{{fxSourceUuid}}` is never set,
+ # and every downstream request then 404s on the literal placeholder —
+ # 51 of 107 assertions (measured, run 30816264169). SEED_SCOPE=register
+ # reuses the Playwright job's tests/e2e/ci-seed.sh and stops after the
+ # register import, skipping the SPA warm-up and bundle gate that this
+ # job (which never runs `npm run build`) must not execute.
+ #
+ # This was held back until ConductionNL/.github#132 landed: the shared
+ # workflow used to start this job's `php -S` SINGLE-WORKER, and journey
+ # J2 points a Source at this very instance, so the app called back into
+ # the server while the one worker was still inside the outer request —
+ # a deadlock that hung the job until timeout-minutes (run 30821823343).
+ # #132 sets PHP_CLI_SERVER_WORKERS=8 on this job's server, which is what
+ # makes the self-referential fixtures serveable. Merged 2026-08-03.
+ newman-seed-command: 'SEED_SCOPE=register bash apps/openconnector/tests/e2e/ci-seed.sh'
+ additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]'
- steps:
- - name: Checkout
- uses: actions/checkout@v4
+ # ── E2E browser tests ────────────────────────────────────────────────
+ # `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.
+ # It used to say `tests/e2e/regression`, which holds no config, so lookup
+ # (2) fell through to the ROOT config. The run step passes no
+ # `--project`, so all four root projects ran at once — 243 tests in 1.6h,
+ # including `visual` (pixel baselines the root config itself documents as
+ # un-matchable on a CI Linux runner) and `docs-capture` (screenshot
+ # re-shoots that have their own dedicated job). `tests/e2e` makes lookup
+ # (1) hit tests/e2e/playwright.config.ts, which declares exactly one
+ # project over spec-coverage/ + regression/ + workflows/.
+ enable-playwright: true
+ playwright-test-path: tests/e2e
+ # openconnector's sources/mappings/synchronizations/jobs are OpenRegister
+ # OBJECTS — there is no `oc_openconnector_*` table for them — so with no
+ # `openconnector` register the SPA has nothing to resolve and the suite
+ # reports it as a wall of selector timeouts. `occ app:enable` is not a
+ # reliable provisioning path for it: the InitializeRegister repair step
+ # swallows its own failures as warnings and occ still exits 0. The script
+ # provisions explicitly and fails loudly when the register or its schemas
+ # are still absent. cwd for this step is the Nextcloud server root.
+ playwright-seed-command: 'bash apps/openconnector/tests/e2e/ci-seed.sh'
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
+ # ── PHPUnit ──────────────────────────────────────────────────────────
+ # `enable-phpunit` defaults to FALSE in the shared workflow and this
+ # caller never set it, so the job reported `skipped` — with its matrix
+ # placeholders still literal (`PHPUnit (PHP ${{ matrix.php-version }},
+ # NC ${{ matrix.nextcloud-ref }})`), because GitHub evaluates a matrix
+ # job's `if:` before expanding the matrix. A disabled PHPUnit therefore
+ # looks exactly like a repo with no PHP tests. This repo has phpunit.xml
+ # and a tests/ tree, and a `coverage-report` artifact from 2026-08-03
+ # proves the suite does run: 20141 of 33162 statements covered.
+ # This is also the prerequisite for the coverage ratchet — the
+ # "Coverage Baseline Check" job `needs: phpunit` and downloads its
+ # artifact, so enabling the guard without this would be a dead gate of a
+ # different shape.
+ enable-phpunit: true
- - name: Install dependencies
- run: npm ci
+ # ── Frontend Check legs ──────────────────────────────────────────────
+ # `frontend-checks` was already non-empty (`["build"]`), so the job did
+ # run here — but only the build. The validator family ran nowhere.
+ # `check:specs` is the aggregate (json-strict + manifest + register) and
+ # is listed as ONE leg rather than three, because each leg is a fresh job
+ # with its own checkout + `npm ci`.
+ # Measured on this tree before enabling: `check:specs` PASSES and
+ # `test:l10n` PASSES.
+ # `test:l10n:parity` is deliberately NOT added: measured on this tree it
+ # is short 337+ 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, 176 of 222 tracked
+ # frontend files in scope (l10n/ and docs/ excluded via .prettierignore /
+ # .gitignore, which prettier 3 also reads).
+ frontend-checks: '["build", "check:specs", "test:l10n", "format"]'
- - name: ESLint
- run: npm run lint
+ # ── 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.
+ 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.
+ #
+ # THIRD CAUSE, and the one that is failing this repo RIGHT NOW
+ # (.github#177): quality.yml@main began executing three gate helpers BY
+ # NAME — check_spec_anchors.py, check_form_labels.py and
+ # check_license_triangle.py — which exist in NO tag before v1.5.0.
+ # Verified by DIRECTORY LISTING of each tag, not by per-file lookups:
+ # those answered "present" uniformly across v1.0.0..v1.5.0, and the
+ # uniformity across independent inputs was the tell that the instrument
+ # was wrong. So the Hydra Gates job here fails at "Verify the pinned gates
+ # package satisfies this workflow", before a single gate runs, with the
+ # workflow's own words: "This is NOT a code-quality finding about your
+ # repository." Removing the pin is the repair.
+ #
+ # Unpinning also picks up v1.5.1's push scoping (.github#179): on a push to
+ # `development`, `origin/development` IS `HEAD`, so the diff was empty by
+ # construction — <= v1.4.0 passed over it (permanently green) and v1.5.0
+ # refused with exit 99 (permanently red). The scope is now
+ # `github.event.before...HEAD`, what the push actually changed.
+ # ── axe-core (gate-33) ───────────────────────────────────────────────
+ # `enable-axe` used to be deliberately unset here, on the grounds that a
+ # vanilla Nextcloud 34 reports serious/critical violations on core's OWN
+ # routes that DOM scoping does not remove. That reasoning is now out of
+ # date in the part that mattered: `axe-include-selector` defaults to
+ # `#content-vue, #content`, which scopes the analysis to the app's own
+ # rendered DOM, and the runner proves on every run that the scope is a
+ # SCOPE and not a MUTE (it injects a violation inside the container and
+ # asserts it is reported, and one outside and asserts it is not).
+ #
+ # So this is turned on against a MEASUREMENT, not a hope. Run locally
+ # with the canonical runner (ConductionNL/.github@main b8c7ead,
+ # @axe-core/playwright 4.12.1) against a dedicated Nextcloud 34 with
+ # openregister + openconnector installed and the register seeded:
+ #
+ # /index.php/apps/openconnector/ HTTP 200 passes=19 violations=0
+ # /index.php/apps/openconnector/#/sources HTTP 200 passes=17 violations=0
+ #
+ # with `axe self-test OK` and `axe scope control OK` on both. Zero
+ # violations at any impact, so zero serious/critical — gate-33 has
+ # nothing to fail on today.
+ enable-axe: true
+ # ONE ROUTE, AND THAT IS A RUNNER DEFECT, NOT A CHOICE — see
+ # ConductionNL/.github#351.
+ #
+ # `axe-run.cjs` treats a null `page.goto()` response as HTTP 0 and dies.
+ # A fragment-only navigation returns null because no document is fetched,
+ # so for a hash-routed SPA — which this app is (`createWebHashHistory()`)
+ # — every route after the first one shares a document with it and is
+ # reported as `returned HTTP 0. … Set the axe-routes input to routes this
+ # app actually serves.` The route is served fine; only its POSITION in
+ # the list decides. Proven both ways on the same instance: `#/sources`
+ # listed FIRST analysed cleanly at HTTP 200; the SAME route listed after
+ # the app root killed the run with exit 2 and wrote no report at all.
+ #
+ # That is the trap in this input: the error blames the caller, so the
+ # obvious response is to delete routes until it goes green, which
+ # silently narrows accessibility coverage to the landing page and reads
+ # as a config fix. Recording it here so the next person widens the list
+ # by fixing #351 rather than by guessing.
+ #
+ # Until #351 lands this is the app root only. It is a real verdict on a
+ # real page — gate-33 has reported SKIPPED in this repo since it was
+ # written — but it is one page, not the app.
+ axe-routes: "/index.php/apps/openconnector/"
- - name: Stylelint
- run: npm run stylelint
+ # ── Coverage ratchet ─────────────────────────────────────────────────
+ # `enable-coverage-guard` defaults to FALSE in the shared workflow, so
+ # BOTH coverage jobs — "Coverage Baseline Protection" (PR) and "Coverage
+ # Baseline Check" (push) — have only ever reported `skipped` here. A
+ # skipped job renders in the Quality Report exactly like a passing one,
+ # so the ratchet read as present while checking nothing.
+ #
+ # Turning it on needs two files this repo did not have, both now added
+ # alongside this line:
+ # * `scripts/coverage-guard.php` — copied BYTE-IDENTICAL from
+ # openregister (the same copy softwarecatalog took), so there is one
+ # implementation of the ratchet across the fleet rather than five.
+ # * `.coverage-baseline` — a bare number. It must EQUAL what CI
+ # measures on development, not approximate it: too high and the
+ # "Guard coverage baseline" step inside PHPUnit exits 1; too low and
+ # the push-side Coverage Baseline Check recomputes a higher value,
+ # finds `git diff` non-empty, and fails demanding the new number be
+ # committed. There is no safe margin in either direction.
+ # The committed value is the one CI printed, recorded as measured.
+ #
+ # This was held back until `enable-phpunit` above had produced a
+ # `coverage-report` artifact from this repo, since the guard reads the
+ # project-level metrics out of that job's clover.xml.
+ enable-coverage-guard: true
diff --git a/.github/workflows/merge-hygiene.yml b/.github/workflows/merge-hygiene.yml
new file mode 100644
index 000000000..4852cee5e
--- /dev/null
+++ b/.github/workflows/merge-hygiene.yml
@@ -0,0 +1,111 @@
+name: Merge Hygiene
+
+# WHY THIS EXISTS, and why it is separate from Code Quality.
+#
+# On 2026-08-14 a merge of origin/development was committed and PUSHED to
+# `perf/predicted-page-fanout` with UNRESOLVED CONFLICT MARKERS in two files.
+# `lib/Service/SynchronizationService.php` did not parse. Eighty-four tests were
+# red. Nothing stopped it, and nothing reported it — because Code Quality's push
+# trigger allows only `[main, development, feature/**, bugfix/**, hotfix/**]`,
+# and `perf/**` matches none of them. The branch had no CI at all, so its last
+# visible state was green from before the branch existed.
+#
+# The lesson is not "add perf/** to the list" — that fixes this branch and leaves
+# the next prefix uncovered. Any branch anyone pushes should get at least the
+# checks that take seconds, so this runs on `**` and stays deliberately cheap:
+# no matrix, no containers, no dependencies, no Playwright. It is a smoke alarm,
+# not the fire brigade. Code Quality remains the real gate on PRs.
+on:
+ push:
+ branches: ['**']
+ pull_request:
+ workflow_dispatch:
+
+concurrency:
+ group: merge-hygiene-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ hygiene:
+ name: Conflict markers and PHP syntax
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # Conflict markers, anywhere in the tree we author. A marker means a merge
+ # was committed half-finished; every downstream signal from that commit is
+ # meaningless, so this fails first and says so plainly.
+ #
+ # Anchored to line start: `<<<<<<<` inside a string, a diff fixture or a
+ # docs example is legitimate and must not fail the build. Matching only at
+ # column 0 is what git itself writes.
+ - name: No unresolved conflict markers
+ run: |
+ set -euo pipefail
+ # SCOPED TO CODE, and to paths we author. A marker is only a defect
+ # where it would break something: prose that DOCUMENTS a conflict is
+ # legitimate, and so are agent-eval artifacts that capture one as
+ # sample output. openbuild failed this gate on
+ # `.claude/skills/create-pr/evals/.../summary.md` — a correct file.
+ #
+ # That matters more than the miss it allows. A gate that fails on
+ # correct files gets switched off, and takes the checks that were
+ # working with it; a marker in a markdown file breaks nothing.
+ if git grep -nE '^(<{7}|={7}|>{7})( |$)' -- \
+ '*.php' '*.js' '*.mjs' '*.ts' '*.vue' '*.json' '*.yml' '*.yaml' '*.css' '*.scss' \
+ ':!vendor' ':!node_modules' ':!*.lock' ':!tests/fixtures' ':!.claude' \
+ ':!**/evals/**' ':!**/fixtures/**' > /tmp/markers.txt; then
+ echo "::error::Unresolved merge conflict markers are committed. This branch does not build."
+ cat /tmp/markers.txt
+ exit 1
+ fi
+ echo "No conflict markers."
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ coverage: none
+
+ # Every PHP file parses. A conflict marker is caught above, but so is any
+ # other way a file can be committed unparseable — and this is the check
+ # that would have failed within seconds of the merge landing.
+ - name: PHP syntax
+ run: |
+ set -euo pipefail
+ fail=0
+ while IFS= read -r f; do
+ php -l "$f" > /dev/null 2>&1 || { echo "::error file=$f::PHP syntax error"; php -l "$f" || true; fail=1; }
+ done < <(git ls-files '*.php' | grep -v '^vendor/' | grep -v '^tests/fixtures/')
+ exit "$fail"
+
+ # JSON that will not parse breaks register fragments and app metadata,
+ # and is the other thing a bad merge leaves behind.
+ #
+ # SCOPED TWICE, because each widening found another honest file. The
+ # first version parsed every tracked .json and died on tsconfig/eslint
+ # JSONC. The second still reached `lib/**/*.json`, which in openbuild
+ # includes an entire app TEMPLATE — `.vscode/settings.json` and all.
+ # A template is not this app's configuration, and an editor file is not
+ # loaded by anything. What is left is what OpenRegister actually reads.
+ #
+ # SCOPED, because the first version was not and failed immediately on
+ # honest files: editor and tooling configs (tsconfig, eslint, devcontainer)
+ # are JSONC — comments and trailing commas — which is valid for their
+ # consumers and invalid for a strict parser. A gate that fails on correct
+ # files is worse than no gate: it gets switched off, and takes the checks
+ # that were working with it. Only the JSON the app itself loads is checked.
+ - name: JSON parses
+ run: |
+ set -euo pipefail
+ fail=0
+ while IFS= read -r f; do
+ [ -f "$f" ] || continue
+ python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$f" \
+ || { echo "::error file=$f::invalid JSON"; fail=1; }
+ done < <(git ls-files 'composer.json' 'package.json' 'appinfo/*.json' 'lib/Settings/**/*.json' \
+ | grep -v '^vendor/' | grep -v '^node_modules/' \
+ | grep -v '/\.vscode/' | grep -v '^lib/Resources/template/')
+ exit "$fail"
diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml
deleted file mode 100644
index abec09269..000000000
--- a/.github/workflows/phpcs.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: php-cs
-
-# Disabled: superseded by code-quality.yml
-on:
- pull_request:
- branches: [never]
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.2'
- tools: cs2pr, phpcs
-
- - name: Run phpcs
- run: phpcs -q --report=checkstyle lib | cs2pr
- continue-on-error: true
\ No newline at end of file
diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml
index a31957f7b..b3abfe54d 100644
--- a/.github/workflows/pull-request-lint-check.yaml
+++ b/.github/workflows/pull-request-lint-check.yaml
@@ -4,19 +4,40 @@ on:
pull_request:
branches:
- development
- - beta
- main
+ - beta
jobs:
lint-check:
runs-on: ubuntu-latest
+ # Checkout + `npm ci` + `npm run lint`. Nothing here writes to the repo,
+ # comments on the PR, or uploads an artifact, so read is the whole need.
+ # `packages: read` is deliberately absent: .npmrc points at the public
+ # registry and package-lock.json contains zero npm.pkg.github.com entries,
+ # so the install never authenticates to GitHub Packages.
+ permissions:
+ contents: read
+ # Observed: n=28 runs, median 0.6 min, max 1.2 min. Bounded loosely so
+ # normal runner contention can never trip it.
+ timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v2
+ # This job had NO setup-node at all, so it inherited the runner default —
+ # currently Node 22, which bundles npm 10. npm 10 cannot install from the
+ # npm 11 lockfile this repo now ships: it exits EUSAGE with
+ # "Missing: from lock file". Node 24 bundles npm 11.
+ #
+ # Nothing in this file named a Node version, so nothing looked wrong.
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '24'
+
- name: Install dependencies
- run: npm i
+ run: npm ci
- name: Linting
run: npm run lint
diff --git a/.github/workflows/pull-request-quality-gate.yml b/.github/workflows/pull-request-quality-gate.yml
deleted file mode 100644
index 7d5edcb94..000000000
--- a/.github/workflows/pull-request-quality-gate.yml
+++ /dev/null
@@ -1,247 +0,0 @@
-name: Pull Request Quality Gate
-
-on:
- pull_request:
- branches:
- # - main
- # - master
- # - development
- # - dev
- # - beta
- - never
-
-jobs:
- quality-gate:
- name: Quality Gate Check
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
- extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, dom, filter, gd, json, posix, simplexml, xmlreader, xmlwriter, zip
- coverage: xdebug
- tools: composer:v2
-
- - name: Install dependencies
- run: composer install --prefer-dist --no-progress --no-interaction
-
- - name: Initialize Quality Gate Results
- run: |
- echo "QUALITY_GATE_PASSED=true" >> $GITHUB_ENV
- echo "## 🚦 Quality Gate Status" >> $GITHUB_STEP_SUMMARY
-
- - name: Check 1 - PHP Syntax
- id: syntax
- run: |
- echo "### ✅ Check 1: PHP Syntax" >> $GITHUB_STEP_SUMMARY
- if composer lint; then
- echo "✅ No syntax errors found" >> $GITHUB_STEP_SUMMARY
- echo "status=pass" >> $GITHUB_OUTPUT
- else
- echo "❌ FAILED: Syntax errors detected" >> $GITHUB_STEP_SUMMARY
- echo "status=fail" >> $GITHUB_OUTPUT
- echo "QUALITY_GATE_PASSED=false" >> $GITHUB_ENV
- fi
-
- - name: Check 2 - Coding Standards (PHPCS)
- id: phpcs
- run: |
- echo "### Check 2: Coding Standards (PHPCS)" >> $GITHUB_STEP_SUMMARY
- composer cs:check
- composer phpcs:output
- if [ -f phpcs-output.json ]; then
- ERRORS=$(jq '.totals.errors' phpcs-output.json)
- echo "errors=$ERRORS" >> $GITHUB_OUTPUT
- if [ "$ERRORS" -eq "0" ]; then
- echo "✅ PASSED: No PHPCS errors" >> $GITHUB_STEP_SUMMARY
- echo "status=pass" >> $GITHUB_OUTPUT
- else
- echo "❌ FAILED: Found $ERRORS PHPCS errors (must be 0)" >> $GITHUB_STEP_SUMMARY
- echo "status=fail" >> $GITHUB_OUTPUT
- echo "QUALITY_GATE_PASSED=false" >> $GITHUB_ENV
- fi
- fi
- continue-on-error: true
-
- - name: Check 3 - Code Quality (PHPMD)
- id: phpmd
- run: |
- echo "### Check 3: Code Quality (PHPMD)" >> $GITHUB_STEP_SUMMARY
- composer phpmd > phpmd-output.txt || true
- VIOLATIONS=$(cat phpmd-output.txt | wc -l)
- echo "violations=$VIOLATIONS" >> $GITHUB_OUTPUT
-
- # Allow up to 50 minor violations, fail if more
- if [ "$VIOLATIONS" -le "50" ]; then
- echo "✅ PASSED: $VIOLATIONS PHPMD violations (acceptable)" >> $GITHUB_STEP_SUMMARY
- echo "status=pass" >> $GITHUB_OUTPUT
- else
- echo "❌ FAILED: $VIOLATIONS PHPMD violations (max 50)" >> $GITHUB_STEP_SUMMARY
- echo "status=fail" >> $GITHUB_OUTPUT
- echo "QUALITY_GATE_PASSED=false" >> $GITHUB_ENV
- fi
- continue-on-error: true
-
- - name: Check 4 - Overall Quality Score
- id: quality
- run: |
- echo "### Check 4: Overall Quality Score" >> $GITHUB_STEP_SUMMARY
- composer phpqa:ci || true
-
- if [ -f phpqa/phpqa.json ]; then
- # Calculate composite quality score
- PHPCS_ERRORS="${{ steps.phpcs.outputs.errors }}"
- PHPMD_VIOLATIONS="${{ steps.phpmd.outputs.violations }}"
-
- # Score calculation: Start at 100, deduct points for issues
- SCORE=$(echo "scale=2; 100 - ($PHPCS_ERRORS * 2) - ($PHPMD_VIOLATIONS * 0.1)" | bc)
-
- # Ensure score doesn't go negative
- if (( $(echo "$SCORE < 0" | bc -l) )); then
- SCORE=0
- fi
-
- echo "score=$SCORE" >> $GITHUB_OUTPUT
- echo "Overall Quality Score: **$SCORE%**" >> $GITHUB_STEP_SUMMARY
-
- if (( $(echo "$SCORE >= 90" | bc -l) )); then
- echo "✅ PASSED: Quality score meets 90% threshold" >> $GITHUB_STEP_SUMMARY
- echo "status=pass" >> $GITHUB_OUTPUT
- else
- echo "❌ FAILED: Quality score ($SCORE%) below 90% threshold" >> $GITHUB_STEP_SUMMARY
- echo "status=fail" >> $GITHUB_OUTPUT
- echo "QUALITY_GATE_PASSED=false" >> $GITHUB_ENV
- fi
- else
- echo "⚠️ WARNING: Could not calculate quality score" >> $GITHUB_STEP_SUMMARY
- echo "status=unknown" >> $GITHUB_OUTPUT
- fi
- continue-on-error: true
-
- - name: Check 5 - Unit Tests
- id: tests
- run: |
- echo "### Check 5: Unit Tests" >> $GITHUB_STEP_SUMMARY
- if composer test:unit; then
- echo "✅ PASSED: All unit tests pass" >> $GITHUB_STEP_SUMMARY
- echo "status=pass" >> $GITHUB_OUTPUT
- else
- echo "⚠️ WARNING: Tests require Nextcloud environment" >> $GITHUB_STEP_SUMMARY
- echo "status=skipped" >> $GITHUB_OUTPUT
- fi
- continue-on-error: true
-
- - name: Final Quality Gate Decision
- run: |
- echo "## 🏁 Final Result" >> $GITHUB_STEP_SUMMARY
-
- if [ "$QUALITY_GATE_PASSED" = "true" ]; then
- echo "### ✅ QUALITY GATE PASSED" >> $GITHUB_STEP_SUMMARY
- echo "This PR meets all quality requirements and can be merged." >> $GITHUB_STEP_SUMMARY
- exit 0
- else
- echo "### ❌ QUALITY GATE FAILED" >> $GITHUB_STEP_SUMMARY
- echo "This PR does not meet quality requirements." >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Required actions:**" >> $GITHUB_STEP_SUMMARY
-
- if [ "${{ steps.syntax.outputs.status }}" = "fail" ]; then
- echo "- Fix PHP syntax errors" >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ "${{ steps.phpcs.outputs.status }}" = "fail" ]; then
- echo "- Fix PHPCS errors (run 'composer cs:fix')" >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ "${{ steps.phpmd.outputs.status }}" = "fail" ]; then
- echo "- Refactor code to reduce PHPMD violations" >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ "${{ steps.quality.outputs.status }}" = "fail" ]; then
- echo "- Improve overall code quality to reach 90% score" >> $GITHUB_STEP_SUMMARY
- fi
-
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Hint:** Run 'composer phpqa' locally to see detailed quality reports." >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- - name: Upload Quality Reports
- if: always()
- uses: actions/upload-artifact@v3
- with:
- name: quality-gate-reports
- path: |
- phpqa/
- phpcs-output.json
- phpmd-output.txt
- retention-days: 30
-
- - name: Post Quality Gate Summary to PR
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v7
- with:
- script: |
- const passed = '${{ env.QUALITY_GATE_PASSED }}' === 'true';
- const syntaxStatus = '${{ steps.syntax.outputs.status }}';
- const phpcsStatus = '${{ steps.phpcs.outputs.status }}';
- const phpmdStatus = '${{ steps.phpmd.outputs.status }}';
- const qualityStatus = '${{ steps.quality.outputs.status }}';
- const testsStatus = '${{ steps.tests.outputs.status }}';
-
- const qualityScore = '${{ steps.quality.outputs.score }}';
- const phpcsErrors = '${{ steps.phpcs.outputs.errors }}';
- const phpmdViolations = '${{ steps.phpmd.outputs.violations }}';
-
- const statusEmoji = (status) => {
- if (status === 'pass') return '✅';
- if (status === 'fail') return '❌';
- if (status === 'skipped') return '⚠️';
- return '❓';
- };
-
- const body = `## 🚦 Quality Gate Status: ${passed ? '✅ PASSED' : '❌ FAILED'}
-
- | Check | Status | Details |
- |-------|--------|---------|
- | PHP Syntax | ${statusEmoji(syntaxStatus)} ${syntaxStatus.toUpperCase()} | All PHP files must be valid |
- | Coding Standards (PHPCS) | ${statusEmoji(phpcsStatus)} ${phpcsStatus.toUpperCase()} | Errors: ${phpcsErrors} (must be 0) |
- | Code Quality (PHPMD) | ${statusEmoji(phpmdStatus)} ${phpmdStatus.toUpperCase()} | Violations: ${phpmdViolations} (max 50) |
- | Overall Quality Score | ${statusEmoji(qualityStatus)} ${qualityStatus.toUpperCase()} | Score: ${qualityScore}% (min 90%) |
- | Unit Tests | ${statusEmoji(testsStatus)} ${testsStatus.toUpperCase()} | Test suite status |
-
- ### Requirements for Merge
-
- ${passed ?
- '✅ **All quality checks passed!** This PR meets the requirements for merging.' :
- '❌ **Quality gate failed.** Please address the issues above before merging.'}
-
- ${!passed ? `
- ### How to Fix
-
- 1. Run \`composer cs:fix\` to auto-fix coding standards
- 2. Run \`composer phpqa\` to see detailed quality reports
- 3. Review the generated report at \`phpqa/phpqa-offline.html\`
- 4. Fix any critical issues and re-push your changes
-
- 📚 See [Quality Assurance Documentation](../docs/quality-assurance.md) for more details.
- ` : ''}
-
- 📊 Detailed reports are available in the workflow artifacts.
- `;
-
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: body
- });
- continue-on-error: true
-
diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml
deleted file mode 100644
index 82fd839fa..000000000
--- a/.github/workflows/quality-check.yml
+++ /dev/null
@@ -1,178 +0,0 @@
-name: Quality Assurance Checks
-
-on:
- pull_request:
- branches:
- # - main
- # - master
- # - development
- # - dev
- - never
- push:
- branches:
- # - main
- # - master
- # - development
- # - dev
- - never
-jobs:
- quality-check:
- name: Code Quality Analysis
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
- extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, dom, filter, gd, json, posix, simplexml, xmlreader, xmlwriter, zip
- coverage: xdebug
- tools: composer:v2
-
- - name: Validate composer.json
- run: composer validate --strict
-
- - name: Get composer cache directory
- id: composer-cache
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
-
- - name: Cache composer dependencies
- uses: actions/cache@v3
- with:
- path: ${{ steps.composer-cache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
- restore-keys: ${{ runner.os }}-composer-
-
- - name: Install dependencies
- run: composer install --prefer-dist --no-progress --no-interaction
-
- - name: Run PHP Lint
- run: composer lint
- continue-on-error: false
-
- - name: Run PHPCS (Coding Standards)
- id: phpcs
- run: |
- composer cs:check
- composer phpcs:output
- if [ -f phpcs-output.json ]; then
- ERRORS=$(jq '.totals.errors' phpcs-output.json)
- WARNINGS=$(jq '.totals.warnings' phpcs-output.json)
- SCORE=$(echo "scale=2; 100 - (($ERRORS * 2) + ($WARNINGS * 0.5))" | bc)
- echo "score=$SCORE" >> $GITHUB_OUTPUT
- echo "errors=$ERRORS" >> $GITHUB_OUTPUT
- echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
- echo "### PHPCS Results" >> $GITHUB_STEP_SUMMARY
- echo "- **Score:** $SCORE%" >> $GITHUB_STEP_SUMMARY
- echo "- **Errors:** $ERRORS" >> $GITHUB_STEP_SUMMARY
- echo "- **Warnings:** $WARNINGS" >> $GITHUB_STEP_SUMMARY
- if [ "$ERRORS" -gt "0" ]; then
- echo "❌ PHPCS found $ERRORS errors. All errors must be fixed." >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- fi
- continue-on-error: false
-
- - name: Run PHPMD (Mess Detector)
- id: phpmd
- run: |
- composer phpmd > phpmd-output.txt || true
- if [ -f phpmd-output.txt ]; then
- VIOLATIONS=$(grep -c "^" phpmd-output.txt || echo "0")
- SCORE=$(echo "scale=2; 100 - ($VIOLATIONS * 0.5)" | bc)
- echo "score=$SCORE" >> $GITHUB_OUTPUT
- echo "violations=$VIOLATIONS" >> $GITHUB_OUTPUT
- echo "### PHPMD Results" >> $GITHUB_STEP_SUMMARY
- echo "- **Score:** $SCORE%" >> $GITHUB_STEP_SUMMARY
- echo "- **Violations:** $VIOLATIONS" >> $GITHUB_STEP_SUMMARY
- if (( $(echo "$SCORE < 80" | bc -l) )); then
- echo "❌ PHPMD score is below 80%. Please review and fix violations." >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- fi
- continue-on-error: false
-
- - name: Run PHPQA (Full Quality Analysis)
- id: phpqa
- run: |
- composer phpqa:ci
- if [ -f phpqa/phpqa.json ]; then
- # Parse PHPQA results
- echo "### PHPQA Results" >> $GITHUB_STEP_SUMMARY
- echo "Full report available in artifacts." >> $GITHUB_STEP_SUMMARY
-
- # Calculate overall quality score
- # This is a composite score based on all analyzers
- PHPCS_SCORE="${{ steps.phpcs.outputs.score }}"
- PHPMD_SCORE="${{ steps.phpmd.outputs.score }}"
- OVERALL_SCORE=$(echo "scale=2; ($PHPCS_SCORE + $PHPMD_SCORE) / 2" | bc)
-
- echo "overall_score=$OVERALL_SCORE" >> $GITHUB_OUTPUT
- echo "- **Overall Quality Score:** $OVERALL_SCORE%" >> $GITHUB_STEP_SUMMARY
-
- if (( $(echo "$OVERALL_SCORE < 90" | bc -l) )); then
- echo "❌ Overall quality score ($OVERALL_SCORE%) is below required 90%." >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "✅ Quality score meets requirements!" >> $GITHUB_STEP_SUMMARY
- fi
- fi
- continue-on-error: false
-
- - name: Upload PHPQA Reports
- if: always()
- uses: actions/upload-artifact@v3
- with:
- name: phpqa-reports
- path: phpqa/
- retention-days: 30
-
- - name: Comment PR with Quality Report
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v7
- with:
- script: |
- const fs = require('fs');
- const phpcsScore = '${{ steps.phpcs.outputs.score }}';
- const phpcsErrors = '${{ steps.phpcs.outputs.errors }}';
- const phpcsWarnings = '${{ steps.phpcs.outputs.warnings }}';
- const phpmdScore = '${{ steps.phpmd.outputs.score }}';
- const phpmdViolations = '${{ steps.phpmd.outputs.violations }}';
- const overallScore = '${{ steps.phpqa.outputs.overall_score }}';
-
- const passed = parseFloat(overallScore) >= 90;
- const emoji = passed ? '✅' : '❌';
-
- const body = `## ${emoji} Code Quality Report
-
- ### Overall Score: **${overallScore}%** ${passed ? '(PASS)' : '(FAIL - Requires 90%+)'}
-
- | Analyzer | Score | Details |
- |----------|-------|---------|
- | PHPCS | ${phpcsScore}% | ${phpcsErrors} errors, ${phpcsWarnings} warnings |
- | PHPMD | ${phpmdScore}% | ${phpmdViolations} violations |
-
- ### Requirements
- - ✅ PHPCS Errors: Must be 0 (Current: ${phpcsErrors})
- - ${passed ? '✅' : '❌'} Overall Score: Must be ≥ 90% (Current: ${overallScore}%)
- - ${parseFloat(phpmdScore) >= 80 ? '✅' : '⚠️'} PHPMD Score: Should be ≥ 80% (Current: ${phpmdScore}%)
-
- ${passed ?
- '### ✅ This PR meets all quality requirements and can be merged.' :
- '### ❌ This PR does not meet quality requirements. Please fix the issues above before merging.'}
-
- 📊 Full quality reports are available in the workflow artifacts.
- `;
-
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: body
- });
-
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 738d3ddb6..000000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,124 +0,0 @@
-name: Unit Tests & Coverage
-
-on:
- pull_request:
- branches:
- # - main
- # - master
- # - development
- # - dev
- - never
- push:
- branches:
- # - main
- # - master
- # - development
- # - dev
- - never
-
-jobs:
- unit-tests:
- name: PHPUnit Tests
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- php-versions: ['8.1', '8.2', '8.3']
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Setup PHP ${{ matrix.php-versions }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-versions }}
- extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, dom, filter, gd, json, posix, simplexml, xmlreader, xmlwriter, zip
- coverage: xdebug
- tools: composer:v2
-
- - name: Install dependencies
- run: composer install --prefer-dist --no-progress --no-interaction
-
- - name: Run unit tests
- run: composer test:unit
- continue-on-error: false
-
- - name: Run integration tests
- run: composer test:integration || echo "Integration tests require Nextcloud environment"
- continue-on-error: true
-
- - name: Generate coverage report
- if: matrix.php-versions == '8.1'
- run: composer test:coverage || echo "Coverage requires Nextcloud environment"
- continue-on-error: true
-
- - name: Check coverage threshold
- if: matrix.php-versions == '8.1'
- id: coverage
- run: |
- if [ -f coverage/clover.xml ]; then
- composer coverage:check || true
- COVERAGE=$(php -r "$xml = simplexml_load_file('coverage/clover.xml'); $metrics = $xml->project->metrics; $statements = (int)$metrics['statements']; $covered = (int)$metrics['coveredstatements']; $percentage = $statements > 0 ? round(($covered / $statements) * 100, 2) : 0; echo $percentage;")
- echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT
- echo "### Test Coverage: $COVERAGE%" >> $GITHUB_STEP_SUMMARY
-
- if (( $(echo "$COVERAGE < 75" | bc -l) )); then
- echo "⚠️ Test coverage ($COVERAGE%) is below recommended 75%." >> $GITHUB_STEP_SUMMARY
- echo "warning=true" >> $GITHUB_OUTPUT
- else
- echo "✅ Test coverage meets requirements!" >> $GITHUB_STEP_SUMMARY
- echo "warning=false" >> $GITHUB_OUTPUT
- fi
- else
- echo "percentage=0" >> $GITHUB_OUTPUT
- echo "warning=true" >> $GITHUB_OUTPUT
- echo "⚠️ No coverage report generated (requires Nextcloud environment)." >> $GITHUB_STEP_SUMMARY
- fi
- continue-on-error: true
-
- - name: Upload coverage reports
- if: matrix.php-versions == '8.1'
- uses: actions/upload-artifact@v3
- with:
- name: coverage-report
- path: coverage/
- retention-days: 30
-
- - name: Comment PR with Coverage Report
- if: github.event_name == 'pull_request' && matrix.php-versions == '8.1'
- uses: actions/github-script@v7
- with:
- script: |
- const coverage = '${{ steps.coverage.outputs.percentage }}';
- const warning = '${{ steps.coverage.outputs.warning }}' === 'true';
-
- if (coverage !== '0') {
- const emoji = warning ? '⚠️' : '✅';
- const status = warning ? 'Below recommended threshold' : 'Meets requirements';
-
- const body = `## ${emoji} Test Coverage Report
-
- ### Coverage: **${coverage}%** (${status})
-
- | Threshold | Required | Current | Status |
- |-----------|----------|---------|--------|
- | Minimum | 75% | ${coverage}% | ${warning ? '⚠️ Below threshold' : '✅ Pass'} |
- | Recommended | 85% | ${coverage}% | ${parseFloat(coverage) >= 85 ? '✅ Pass' : '⚠️ Below recommended'} |
-
- ${warning ?
- '⚠️ **Warning:** Coverage is below the recommended 75% threshold. Consider adding more tests.' :
- '✅ **Success:** Test coverage meets requirements.'}
-
- 📊 Full coverage report is available in the workflow artifacts.
- `;
-
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: body
- });
- }
- continue-on-error: true
-