Skip to content

fix(shell): the floating controls move with the backdrop they sit abo… #893

fix(shell): the floating controls move with the backdrop they sit abo…

fix(shell): the floating controls move with the backdrop they sit abo… #893

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, 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:
# SUFFIXED BY EVENT NAME, not just by `-push`.
#
# The previous expression gave a push on `development` its own lane
# (`-push`) but left EVERYTHING ELSE sharing `quality-development` — and
# that is not a quiet lane: `Sync to Beta` keeps a PR open whose head_ref
# IS `development`, so its run computes the same group and is re-triggered
# on every merge.
#
# A `workflow_dispatch` therefore shared a group with that PR and was
# cancelled by it. Measured on shillinq 2026-08-21: dispatch 32487948678
# cancelled by pull_request run 32490160836 (head_branch `development`).
# A run someone deliberately asked for could essentially never complete.
#
# That reaches past ad-hoc verification: the fleet gate-drift sweep
# (.github#523) dispatches per app with `--ref development`, because
# `schedule:` cannot choose a branch. Under the old group those runs are
# cancelled and report neither pass nor fail — and a routine that produces
# no verdict is indistinguishable from one that never ran.
#
# This is hermiq's form, already live there. Pull requests keep the bare
# group (so a PR still supersedes its own earlier run); push, dispatch and
# schedule each get their own lane.
#
# THE BRANCH RESTRICTION IS GONE, because it contradicted the sentence above.
#
# The suffix used to apply only when `ref_name` was `main` or `development`,
# so on every OTHER branch push and pull_request computed the SAME group —
# and `cancel-in-progress` made them kill each other. That became reachable
# when the push allow-list widened on 2026-08-14 to include `feat/**`,
# `fix/**`, `perf/**`, `refactor/**` and `chore/**`: those branches now get
# both a push run and a pull_request run for one commit.
#
# `quality / Quality Report` is a `needs:`-gated aggregator and reports
# FAILURE when its dependencies are CANCELLED, so the collision shows up as a
# red gate on a PR that was never actually evaluated — and re-running collides
# the same way. Measured on openregister#2821: a push run left queued and a
# pull_request run cancelled, 18 seconds apart, on one commit.
#
# A branch name is not a unique lane when two event types can each produce a
# run for it, so the event is now always part of the key.
group: quality-${{ github.head_ref || github.ref_name }}${{ github.event_name != 'pull_request' && format('-{0}', github.event_name) || '' }}
# PUSH RUNS ARE NOT CANCELLED — and this has to be said HERE, not only in the
# shared workflow. .github#597 set `cancel-in-progress` on quality.yml itself,
# but a caller's own concurrency cancels the whole run before the called
# workflow's setting can apply, so that fix reached only the apps that declare
# no concurrency of their own. Measured 2026-08-28 over push runs on
# `development` since #597: 0 of 11 cancelled where the caller was silent, 7 of
# 13 (54%) cancelled where the caller still said `true`.
#
# An integration branch needs a verdict per commit: the run being cancelled is
# the only thing that would have said whether what just landed is sound, and
# its replacement is cancelled too. `pull_request` keeps cancelling, where
# superseding really is correct.
cancel-in-progress: ${{ github.event_name != 'push' }}
# 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:
if: (github.event_name != 'pull_request' || github.head_ref != 'development')
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.
#
# THE LIST IS THE WHOLE DECLARED RANGE. This comment previously said
# "launchpad's own appinfo/info.xml floor stays at 29 … the NC32 constraint
# here is a property of the CI fixture, not of launchpad's code" — that is
# no longer true of the file it describes. info.xml on this branch declares
# <nextcloud min-version="32" max-version="34"/>, so 32 is the app's own
# floor, not a fixture artefact, and 32, 33 and 34 each get a leg.
nextcloud-test-refs: '["stable34", "stable32", "stable33"]'
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
# Enforce E2E skip discipline. Re-added: #354 set this and #356 removed
# it 16 minutes later while rewriting this file's triggers -- not a
# revert, just a casualty of two edits landing together.
#
# It is now backed by a real measurement rather than an assumption. The
# gate finally EXECUTES here (it needed hydra-gates v1.10.0 via #359 and
# the path fix in ConductionNL/.github#595), and on development's current
# head it reported:
#
# 0/137 tests skipped (0.0%) across 30 spec files
# V1 spec files executing ZERO tests : 0
# V2 skips deferring to a deploy state CI decides: 0
# V3 skips/fixmes with no reason recorded : 0
# Every skip names a real absence and every spec file ran something.
#
# So launchpad passes the gate on real data, and turning it on can only
# keep it that way. tests/e2e/docs-screenshots.spec.ts does NOT trip the
# zero-test rule, which was the open question when this was first tried.
e2e-skip-blocking: 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.
#
# `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, 237 of 279 tracked
# frontend files in scope (l10n/ and docs/ excluded via .prettierignore /
# .gitignore, which prettier 3 also reads).
# `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", "format", "check:schema-l10n"]'
# ── 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