Skip to content

chore: adopt Nextcloud's coding standard, .editorconfig and NC 34 #272

chore: adopt Nextcloud's coding standard, .editorconfig and NC 34

chore: adopt Nextcloud's coding standard, .editorconfig and NC 34 #272

Workflow file for this run

name: Code Quality
on:
push:
branches: [main, development, feature/**, bugfix/**, hotfix/**]
pull_request:
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 is the only carrier of the push-only jobs: "Coverage Baseline
# Check" (`github.event_name == 'push'`), "SBOM" and "Features Extract". And
# those two branches always have an open PR whose `head_ref` IS the branch
# name: the standing "Release: merge development into beta" (#22 here).
# `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 a few seconds.
#
# Measured on this repo: 14 of the last 20 `development` push runs were
# cancelled within ~70s of starting — e.g. 31047886687 (41s), 31038167672
# (68s), 31034263500 (54s), 31031059278 (42s). That duration is the
# discriminator: the shared workflow's `timeout-minutes: 45` cancellation lands
# at 45m16s–45m28s, so these are concurrency kills. This repo is the worst hit
# in the fleet.
#
# On the surviving PR run "Coverage Baseline Check" reports `skipped`, which is
# CORRECT for a pull_request event and renders exactly like a pass. So the gate
# appears on both runs and executes on neither — a dead gate of the
# permanently-pending shape.
#
# 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.
#
# Proven in openconnector#1158: its first-ever completed `development` push run
# (31048998594) executed Coverage Baseline Check, SBOM and Features Extract.
concurrency:
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
# 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 it must cover the maximum
# any nested job declares: journeydoc-capture (contents+actions write),
# update-baseline / features-extract (contents write), and the Quality
# Report PR comment (issues / pull-requests write).
permissions:
contents: write
actions: write
issues: write
pull-requests: write
jobs:
quality:
uses: ConductionNL/.github/.github/workflows/quality.yml@main
with:
app-name: launchpad
php-version: "8.3"
# Pinned, because the shared workflow's DEFAULT is '["stable31", "stable32"]'
# and stable31 CANNOT WORK here. `additional-apps` below installs
# openregister, which declares min-version="32" (its
# lib/ContextChat/ContentProvider.php implements
# OCP\ContextChat\IContentProvider, absent from core before NC32). On NC31
# `occ app:enable openregister` fails with
# App "Open Register" cannot be installed because it is not compatible
# with this version of the server.
# and the run continues anyway, because that failure is only a ::warning::.
# Every /apps/openregister/... call then returns Nextcloud's HTML 404 page.
#
# Order matters as much as membership: newman, playwright and
# journeydoc-capture all check out `fromJSON(nextcloud-test-refs)[0]` as
# their single server, so with the inherited default those three jobs ran
# entirely on the version openregister cannot load.
#
# launchpad's own appinfo/info.xml floor stays at 29: src/manifest.json
# declares no required app dependency, and launchpad's OpenRegister use is
# via AppHost, probed at runtime. The NC32 constraint here is a property of
# the CI fixture, not of launchpad's code.
nextcloud-test-refs: '["stable34"]'
enable-psalm: true
enable-phpstan: true
enable-phpmetrics: true
enable-frontend: true
enable-eslint: true
# Hard gates:
# - PHPUnit — wired in wave3.10. 4-way matrix (PHP 8.3/8.4 ×
# NC stable31/stable32) all green.
# - Newman — wired alongside the postman collection drift
# sweep (PR #123 follow-up): six real backend bugs fixed
# (Templates SQL, Feeds 2× constraint, addWidget TypeError,
# Lock-on-nonexistent, share TypeError, addRule TypeError),
# postman fixture-id wiring repaired and assertion drift
# resolved. 196 assertions / 0 failures locally.
#
# - Playwright — ENABLED, against the root suite minus a measured
# exclusion list, NOT against the four-test `tests/e2e/ci/` subset
# it used to run.
#
# The subset was a deliberate "green floor that grows", and as a
# floor it worked. What it could not do is tell anyone the truth
# about coverage. gate-19 reads the ROOT `playwright.config.ts`;
# the workflow resolves `<playwright-test-path>/playwright.config.ts`
# FIRST, so it read a different file — and nothing compared them.
# Measured 2026-08-10 (launchpad#82): **CI executed 4 tests while
# 113 existed**, and of **117 `@e2e` annotations only 9** were in
# files CI ran. gate-19 reported 71 scenarios covered; 4 had an
# executing test behind them.
#
# `tests/e2e` deliberately contains NO `playwright.config.ts`, so
# the workflow's fallback selects the root one — the same file
# gate-19 parses. The two cannot drift without an edit to that file.
# Its `testIgnore` names every excluded spec with the run that
# measured it (31367057618: 65 of 80 passed).
enable-phpunit: true
enable-newman: true
newman-environment-path: "tests/integration/local.env.json"
# Creates the non-admin account the collection's authorization assertions
# need. Without it `{{regularUser}}` stays unresolved, the request arrives
# with junk credentials, and `POST /api/role-feature-permissions
# non-admin → 403` gets a 400 instead — failing while testing nothing about
# authorization.
#
# A SCRIPT, not an inline command: the shared workflow runs this through
# `eval <value>` UNQUOTED, so any shell metacharacter is parsed at the outer
# level. Measured on the Playwright equivalent, `( … ) && ( … )` is a syntax
# error and `sh -c '… ; …'` splits at the wrong level. `bash <path>` is one
# word and cannot be mis-parsed. Path is relative to `server/`.
newman-seed-command: bash apps/launchpad/tests/integration/seed.sh
# OpenRegister must be present for the integration suite to mean anything.
# The Newman collection asserts OpenRegister-backed behaviour — the
# AppHost observability engine behind /api/health and /api/metrics, and the
# dashboard objects behind the v2 manifest — so without it the suite was
# measuring a degraded instance. The job previously reported success only
# because `composer test:all` ended in `|| echo '…skipping'` and always
# exited 0; with that removed, 17 of 220 assertions surfaced as failures.
#
# `ref: development` is REQUIRED, not cosmetic. The default is `main`, and
# `lib/Service/Rbac/ObjectGrantResolver.php` — which
# ManifestController::fetchGrantedDashboards() resolves for the shared-
# dashboard source — exists only on `development`. Pinned to `main` the
# grant lookup would fail soft to owned-only and the suite would quietly
# test less than it appears to.
additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]'
enable-playwright: true
# Names the real suite directory. It holds no config of its own, so the
# workflow falls back to the root `playwright.config.ts` — which is the
# file gate-19 reads. Same file, same testIgnore, no drift.
playwright-test-path: tests/e2e
# The grant spec needs a second, non-admin account to be the share
# recipient; a grant to yourself proves nothing.
#
# This was an inline `… user:add … || true`. The `|| true` was there for a
# real reason — the suite must survive a re-run against a warm instance —
# but it tolerated EVERYTHING, not just "already exists": a rejected
# password, a missing occ or a broken database all exited 0 and let
# Playwright start with no grantee. The script keeps the tolerance and
# narrows it to the postcondition that matters (the account exists
# afterwards), which it establishes by asking the instance. See
# tests/e2e/seed.test.sh, which asserts both arms — including the one
# that must fail.
playwright-seed-command: bash apps/launchpad/tests/e2e/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 `check:manifest` ran
# nowhere while the run still looked complete. It is a self-contained
# `node scripts/check-manifest.js`, which is what a leg has to be (each
# leg is a fresh job with its own checkout + `npm ci`).
# Measured on this tree before enabling: PASSES. It is enabled to keep it
# passing, not because it is currently broken.
# `test` is NOT listed: "Frontend Tests (unit)" already runs it.
frontend-checks: '["check:manifest"]'
# ── Coverage ratchet ─────────────────────────────────────────────────
# `enable-coverage-guard` defaults to FALSE, which is why both
# "Coverage Baseline Protection" and "Coverage Baseline Check" 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 openregister's) and `.coverage-baseline` = 49.47, this repo's own
# measured coverage (11200 of 22642 statements) read from clover.xml in
# the `coverage-report` artifact of run 30911179742.
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. Pinned to v1.0.1 so a change to the gate package
# cannot move this repo's verdict without a commit here.
# `enable-axe` deliberately NOT set: a vanilla Nextcloud 34 already carries
# serious/critical violations from core's own UI.
#
# v1.0.1 -> v1.3.0 (ConductionNL/.github#159). Two defects, one bump.
#
# 1. STALE. v1.0.1 is `f4d9756` (2026-08-03) and predates three gate
# fixes, so every Hydra Gates run this repo has ever made executed a
# script in which 16 gates reported PASS when their helper never ran
# (#147), gate-33 had no axe report to read and never said so (#148),
# and gates 6 and 7 reported PASS on an EMPTY scope (#149). A gate
# that reports PASS without running emits a tick identical to a real
# one, which is why nothing in this repo's history shows it.
#
# 2. RED. quality.yml is referenced `@main` while this package is
# PINNED, so the two can desync — and on 2026-08-05 they did. #164
# flipped `hydra-gates-require-full-coverage` to default TRUE, but
# the accounting that makes that flag survivable (NOT APPLICABLE, as
# distinct from a structural or a wiring gap) ships in the PACKAGE.
# So every pin older than `f7eaf2a` now fails the coverage assertion
# for gates it has no subject matter for. Measured diff-scoped,
# exactly as CI scopes it:
# v1.0.1 exit 98 FAIL — "GATES THAT DID NOT RUN: 24 33"
# v1.3.0 exit 0 PASS — those gates named NOT APPLICABLE
# The old pin was not merely stale, it was failing this repo's CI for
# a reason that had nothing to do with this repo.
#
# 3. DEAD AGAIN, same mechanism, third time (2026-08-06). The pin was
# the defect, not its value. quality.yml floats `@main` and executes
# gate scripts BY PATH inside the pinned package, so every new gate
# added at @main is a path that v1.3.0 does not contain:
#
# hydra-gates-ref 'v1.3.0' does not contain:
# scripts/axe-run.cjs
# scripts/lib/check_spec_anchors.py
# scripts/lib/check_form_labels.py
# scripts/lib/check_license_triangle.py
#
# The job failed for a reason that, again, had nothing to do with
# this repo — CI itself says so: "This is NOT a code-quality finding
# about your repository." Bumping the pin to today's tag would only
# reset the same expiry clock a fourth time.
#
# So: the pin is REMOVED, not bumped. The shared workflow defaults this
# input to `main` and states "CALLERS SHOULD NOT SET THIS AT ALL"; the
# rest of the fleet sets nothing (scholiq notes the omission is
# deliberate). Floating both halves keeps caller and callee in step,
# which is the only configuration in which a pinned path cannot expire.
enable-hydra-gates: true