Skip to content

Release: merge beta into main #1226

Release: merge beta into main

Release: merge beta into main #1226

Workflow file for this run

name: Code Quality
on:
# ⚠️ The `push:` trigger was MISSING from this workflow, and it is the only
# repo in the fleet where that was true (checked against all 22 others; every
# one of them carries this same list).
#
# Three jobs in the shared quality workflow run ONLY on `push`:
#
# SBOM `github.ref` must be main/beta/development
# Features Extract `github.event_name != 'pull_request'`
# Coverage Baseline Check `github.event_name == 'push'`
#
# With no push trigger, none of them could ever fire here. The last push run
# on `development` is 23289135240, from **2026-03-19** — five months of
# merges during which the SBOM was never regenerated, `docs/features.json`
# was never checked against `openspec/specs/`, and the coverage ratchet never
# ran.
#
# This is the hardest version of the problem to see: the inputs all say
# `true`, the PR checks are green, and the Quality Report on every PR looks
# complete — because on a PR those three jobs are *correctly* skipped. There
# is no red, no skip anyone would question, and no run at all to inspect. You
# can only find it by asking what the LAST PUSH RUN was, which nothing
# prompts you to do.
#
# `enable-coverage-guard` was switched on here in the previous commit; without
# this trigger its push-side half would have been dead on arrival.
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
- beta
- development
pull_request:
branches: [main, beta, development]
# Same family of defect as the missing `push:` above, one step further along:
# there was no way to run this workflow ON PURPOSE either.
#
# This repo and `larpingapp` were the ONLY two of the sixteen fleet apps
# without a manual trigger — checked on 2026-08-10 by reading
# `.github/workflows/code-quality.yml` on `development` in all sixteen; the
# other fourteen already carry it.
#
# The cost was not "slightly less convenient". Every fleet-wide gate sweep run
# in this programme is a `workflow_dispatch` fan-out, so this repo was not
# failing those sweeps and was not passing them — it was ABSENT from the
# results table. In a table of fourteen verdicts an unmeasured repo looks
# exactly like one that was never a problem: silence read as health, which is
# precisely how the five-month `push:` outage documented above survived.
#
# A dispatch here is also worth more than a re-run of CI. The shared workflow
# scopes `workflow_dispatch` to the FULL repository (there is no PR target and
# no previous pushed tip to diff against, so ADR-020 diff-scoping has nothing
# to scope to and the gates scan the entire tree). A push run on `development`
# typically covers a single commit's files; this is the only way to ask "what
# is the state of the whole app?" without opening a pull request. Expect it to
# be redder than a PR — that is the honest answer, not a regression.
workflow_dispatch:
# Permission CEILING for the called quality pipeline, NOT a grant. Copied
# verbatim from openconnector, where this exact block is live on `development`
# with all ~30 quality jobs green — so it is a measured ceiling, not a guess.
#
# ⚠️ Most jobs in the callee declare NO permissions of their own (only
# journeydoc-capture and features-extract do), which means they inherit THIS
# ceiling exactly. Every entry below is load-bearing:
#
# contents: write journeydoc-capture and update-baseline `git push`
# the regenerated screenshots / coverage baseline
# actions: write journeydoc-capture re-dispatches downstream work
# (a GITHUB_TOKEN push does not fire workflow_run)
# issues / pull-requests: write
# the Quality Report comment — `github.rest.issues
# .createComment`, which 403s under contents+packages
# read alone (quality.yml notes this at its own step)
# packages: read pulling org images for the Nextcloud test matrix
#
# ⚠️ Do NOT "tighten" any of these to `read`. GitHub statically validates the
# called workflow's DECLARED job permissions against this grant — including for
# jobs an `if:` will skip — so capping one makes the whole call fail to START:
# zero jobs, no annotations, and a Code Quality run that is absent rather than
# red. That is quieter than the outage it would cause.
permissions:
contents: write
actions: write
issues: write
pull-requests: write
packages: read
jobs:
quality:
uses: ConductionNL/.github/.github/workflows/quality.yml@main
with:
app-name: stackiq
php-version: "8.3"
# THE LIST IS THE WHOLE DECLARED RANGE. appinfo/info.xml declares
# <nextcloud min-version="32" max-version="34"/>, so 32, 33 and 34 each get
# a leg. Adopting NC 34 by REPLACING the list left 32 and 33 advertised to
# the App Store with no job touching them — the declared floor became the
# untested end, which is the same drift as never testing 34, reversed.
# stable34 leads because newman, playwright and journeydoc-capture all read
# `fromJSON(inputs.nextcloud-test-refs)[0]` as their single server, and 34
# is the major this app had never been exercised on.
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-sbom: true
# ⚠️ `Conduction/openregister` 404s — the org is `ConductionNL`. The
# earlier org-ref fix corrected the `uses:` line above but not this
# EMBEDDED repo reference, because a `uses:`-grep does not see it. With a
# dead ref the openregister dependency was never checked out, so every
# job that needs it ran against a missing app.
# `ref` was absent, so this checked out openregister's DEFAULT branch
# (`main`). Every other app in this fleet is developed against
# openregister `development`, and the e2e suite drives OpenRegister's
# object API directly (tests/e2e/workflows/_fixtures.ts), so testing
# against `main` measures a different backend than the one this app is
# written for. Pinned to `development` to match.
additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]'
# Newman disabled: tests/magic-mapper-import.postman_collection.json was
# written against a dev env with URL rewriting + a fixed disk layout — it
# hits bare paths like `/configurations` and uploads files from
# `/var/www/html/custom_apps/openregister/...`, neither of which exists on
# the CI runner (NC lives under `server/apps/`, no rewrite → `/index.php/...`).
# Re-enable after the collection is rewritten to the CI base_url/paths +
# a seed step provisions the `stackiq` register/`module` schema.
enable-newman: false
newman-collection-path: "tests"
# ── 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.
# We ship tests/e2e/playwright.config.ts precisely so lookup (2) hits it.
# The run step passes no `--project`, so the ROOT config would also run
# its `visual` project — pixel baselines whose own header says a CI Linux
# runner cannot byte-match a dev-container PNG. The tests/e2e config
# declares only `smoke` + `chromium`.
enable-playwright: true
playwright-test-path: tests/e2e
# `occ app:enable stackiq` runs InitializeSettings, but that
# repair step has no user session (OpenRegister RBAC can deny it),
# swallows its own exception as a warning, and imports with
# `force: false` — so a fresh install can come up with no `stackiq`
# register at all and nothing exits non-zero. The script imports
# explicitly over the admin API and fails loudly if the registers,
# schemas, or the app's own register→schema mapping still aren't there.
# cwd for this step is the Nextcloud server root.
playwright-seed-command: 'bash apps/stackiq/tests/e2e/ci-seed.sh'
# Integration Tests (Newman) stays OFF here, deliberately. The
# `enable-newman: false` further up is not a default nobody chose: it
# records that the collection's requests hard-code
# `/var/www/html/custom_apps/openregister/...` paths that do not exist on
# a CI runner (Nextcloud lives under `server/apps/`, no rewrite), and that
# it assumes a seeded `stackiq` register / `module` schema. Turning
# it on would produce a guaranteed red whose cause is already written
# down. Flipped back on in the commit that rewrites the collection to the
# CI base_url and adds the seed step.
#
# Noted while checking this, because it is the kind of thing that reads as
# working when it is not: the validator counts collections RECURSIVELY
# (`find`) while the run step globs them FLAT (`for collection in
# *.postman_collection.json` after `cd`). A `newman-collection-path` that
# only satisfies the validator passes validation and then runs nothing.
# The configured `tests` path does satisfy both here.
# ── Frontend Check legs ──────────────────────────────────────────────
# `frontend-checks` defaults to `[]`, and an empty list means the shared
# workflow emits NO "Frontend Check" job at all — so these three
# validators ran nowhere while the run still looked complete.
# Measured on this tree before enabling: ALL THREE PASS. They are enabled
# to keep them passing, not because they are currently broken.
# `test:l10n:parity` is deliberately NOT added: measured on this tree it
# is short 404+ translations across the required locales. That is a
# translation backlog, and a permanently-red leg is one that gets switched
# off again.
# `test` / `test:unit` are NOT listed: "Frontend Tests (unit)" runs them.
#
# `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, 188 of 235 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", "check:vue-demi", "test:l10n", "format", "check:schema-l10n", "check:l10n-js"]'
# ── 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` = 13.01, this repo's own
# measured coverage (4077 of 31330 statements) read from clover.xml in the
# `coverage-report` artifact of run 30911570224. That is the lowest figure
# in the fleet by a wide margin and is recorded here as measured, not
# rounded up: the ratchet's job is to stop it falling further, and a
# baseline set above the truth would fail on the first honest run.
enable-coverage-guard: true
# ── Hydra mechanical gates ───────────────────────────────────────────
# `enable-hydra-gates` defaults to FALSE, so this tier had 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) — this
# repo felt both, the second one blocking #434.
# 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.
#
# `enable-axe` is deliberately still NOT set — a vanilla Nextcloud 34
# reports serious/critical violations on core's OWN routes that DOM
# scoping does not remove. Enabling axe is a separate decision.