Release: merge development into beta for the fleet-wide beta checkup - #279
Merged
Conversation
…-> 2026-08-01) (#103) The lockfile pinned roave/security-advisories to a commit from 2026-03-20, so the metapackage's conflict rules — and therefore the protection against installing known-vulnerable dependency versions — were frozen at that date. Refreshed to the 2026-08-01 tip. composer audit --locked: clean before and after.
Jobs without timeout-minutes fall back to GitHub's 360-minute default, so a hung runner burns six hours of Actions minutes before it is reaped. Bounds are derived from observed run durations and left deliberately loose: a timeout that fires under normal contention is worse than no timeout, because it turns a slow run into a phantom defect. Jobs that only call a reusable workflow (job-level `uses:`) are untouched -- they inherit their bound from the called workflow.
…he app-local proxy (ADR-080) StoreController now extends AppHost's GenericStoreControllerBase and inherits search() plus the SSRF-guarded, redirect-refusing, token-private fetch. RemoteTemplateStoreService (331 lines) and its test file are deleted; the behaviour and the SSRF controls live in OpenRegister's GenericStoreServiceTest. Install stays here, and only install — cloning a template into a local virtual app is OpenBuild-specific and differently authorized from the connector-adapter and agent-template installs in other apps. Three things the tests caught that review would not have: 1. The suite could not load StoreController at all — `extends` is resolved by the AUTOLOADER, not the container, and the OR AppHost classes are stubbed here rather than autoloaded. Added stubs for GenericStoreService, StoreDescriptor and GenericStoreControllerBase. This is the same mechanism by which a missing sibling app 500s EVERY route in production, which is why ADR-080 restricts subclassing to apps declaring openregister a hard <app> dependency — this app qualifies (8 controllers already type-hint OR classes). 2. The search test's `getParam` mock returned one value for ANY key, so `kind` silently received the query string. Made it key-aware; the action reads `q` and `kind` separately. 3. tests/stubs/openregister-stubs.php defines a NO-OP SecurityService, because the real guard does DNS lookups that fail for .test fixture hostnames. Any "SSRF negative control" written against that stub passes regardless of what the guard does. Corrected the docblock to say so, and to point at the OpenRegister suite where the real guard is exercised. The new store stub is deliberately non-behaving for the same reason. 743/743 unit tests, 7/7 on the store controller.
StoreController is a plain Controller injecting GenericStoreService, with its
own ~30-line search() action, rather than subclassing a cross-app base. The
inheritance broke phpstan ('extends unknown class', which it refuses to let you
ignore), psalm, and the unit suite's class loading — one stub entry now covers
the injected type-hint instead.
phpstan OK, psalm no errors, 743/743 tests.
…settings refactor(store): consume OpenRegister's GenericStoreService, delete the app-local proxy (ADR-080)
…110) Bumps @conduction/nextcloud-vue 2.1.0-vue3.16 -> 3.0.0-vue3.2, pinned exactly. The major bump is narrow. Diffing the packed tarballs, 3.0.0-vue3.2 removes exactly three symbols against every 2.1.0-vue3.x baseline (.13/.15/.16/.17/.19): CnFlowCanvas, CnEditFlowsModal and CnFlowCanvasModal — the old flow-authoring surface, which moved into OpenRegister's one flow store. From .16 it also ADDS CnFlowDetail, CnFlowEditModal and CnObjectAccessTab, and exports useFlowStore from the barrel. Nothing else in src/ changed: the only other edited file is CnOpenBuildEditButton, which internally swapped the two removed dialogs for CnFlowEditModal. No OpenBuild source imported any of the three removed symbols, so the bump needs no code change here. Verified in the built bundle rather than by assertion: after the rebuild neither removed symbol appears in js/, while CnFlowEditModal does — the positive control for that grep. Separately, four peers nc-vue has declared since at least 2.1.0-vue3.13 were never declared by this app and were resolving only by luck of hoisting: @vueuse/core, axe-core, dexie and marked. Declared them at the versions OpenRegister and OpenConnector already use. Verified from the lockfile, not package.json: nextcloud-vue is exactly 3.0.0-vue3.2, and vue3-apexcharts resolves to 1.8.0 — below the 1.9.0 that turned proprietary and cannot be sublicensed under EUPL-1.2. @nextcloud/l10n resolves to 3.4.1, so this app is not exposed to the v2 boot-killer where nc-vue's pre-bundled dialogs call the v3-only getGettextBuilder().detectLanguage(). Gates, all after a full rm -rf of node_modules and package-lock.json: npm install (npm 11) then npm ci (npm 10) both exit 0 on a 780 KB / 1717-package lockfile, eslint exit 0, 1364 unit tests in 140 files passing, and webpack exit 0 with only the pre-existing asset-size warnings. The build was run with USE_LOCAL_LIB=false so it measured the published package: this repo sits next to a nextcloud-vue checkout, and webpack.config.js aliases the sibling by default, which would have measured the wrong code entirely. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…104) * build(quality): run the Hydra gates as part of composer check:strict openbuild is the pilot for conduction/hydra-gates. The gates have until now only run inside hydra's own containers, which means an agent finishing a task here could report done without any of the 61 mechanical gates having looked at its diff. Wiring them into check:strict makes "gates pass" part of the definition of done in the same command everything else already runs. The gates are diff-scoped per ADR-020, so this does NOT import openbuild's inherited debt. A full-repo run of this tree fails 16 gates today (img-alt, button-name, table-headers, spec-anchor-existence and friends); scoped to a PR's own diff, only what that PR touched is enforced. `composer gates:full` is available for the audit view, and is deliberately not what check:strict runs. check:strict keeps its 0/1 contract, but the gate exit code is preserved separately and reported, because it carries the failure COUNT and flows route on it. 99 is distinguished from a gate failure in the summary: it means the gates could not run at all, which is a configuration error and not a clean tree. The dependency is declared against a `path` repository at ../hydra, which is the fleet's checkout layout. That is the part of this change that is provisional — see the PR description; how hydra-gates should be DISTRIBUTED (private Packagist, a Satis mirror, or a split public repo) is an open decision, and a path repository is the shape that is verifiable today without one. * build(quality): lock conduction/hydra-gates at dev stability A path repository derives its version from the checked-out branch, so the default minimum-stability of 'stable' rejects a '*' constraint outright: 'found conduction/hydra-gates[dev-main, ...] but it does not match your minimum-stability'. '@dev' is the constraint that resolves, and it keeps working once the package is eventually tagged. * build(quality): resolve hydra-gates from the public package, not a path repo The `path` repository at `../hydra` is what blocked this PR. It works on a developer machine and fails in CI with `Source path "../hydra" is not found`, because the fleet's sibling-checkout layout does not exist in a CI job and hydra is private, so no runner can fetch it. conduction/hydra-gates now lives in ConductionNL/.github, which is PUBLIC and already owns the shared workflows (ConductionNL/.github#131). Pointing at it needs no credentials in this repo or on any runner. - repositories: `path ../hydra` -> `vcs https://github.com/ConductionNL/.github.git` with `"no-api": true`, so composer clones over git instead of the GitHub API and a rate-limited unauthenticated runner cannot become a failed install. - require-dev: `@dev` -> `^1.0`, resolving to the v1.0.0 tag. That also drops the dev-stability requirement the previous commit needed, because a path repository derives its version from the checked-out branch and a tag does not. - composer.lock pins commit fdad2546f2ac68aa64be5fecef898784c1847538. Nothing else changes. `composer gates`, `composer gates:full` and the gate handling inside `check:strict` are untouched, including the part that captures the gate exit code separately: it carries the FAILURE COUNT, and 99 ("could not run at all") is still reported distinctly from a gate failure so a configuration error can never read as a clean tree. Also merged origin/development in. The branch predated the fleet-wide timeout-minutes work, so it was carrying a silent revert of the bounds on exporter-e2e.yml and pull-request-lint-check.yaml. * ci(quality): actually run the gates in CI, not just in a local composer script Wiring the gates into `composer check:strict` was a local-only change. Nothing in this repository's CI invokes check:strict — no workflow does, verified by grepping .github/workflows — so this PR would have merged green with the 61 gates never having executed on any diff. A gate that runs only on a developer's machine is not a gate, and a green that never ran it is the failure mode the gates exist to catch. The shared quality workflow now ships a `hydra-gates` job (ConductionNL/.github#131), opt-in per repo and defaulting to off. openbuild is the pilot, so it opts in. `hydra-gates-ref` is pinned to the v1.0.0 tag rather than tracking `main`, so a change to the gates cannot move this repo's verdict without a commit here to move the pin. The job resolves the diff base from the PR's real target branch, checks out the app with fetch-depth: 0 (a shallow clone has no base to diff against), and reports exit 99 — "could not run at all" — distinctly from "N gates failed". * build(quality): bump hydra-gates to v1.0.1 for the gate-4 audit fix The first real CI run of the gates on this PR failed on gate-4: `composer-audit: FAIL — CVEs or advisories`. There are no CVEs — auditing this repo's lock properly reports "No security vulnerability advisories found." `composer audit` with no vendor/ present does not audit the lock. On the runner's composer it errors and exits non-zero, which gate-4 reported as a security finding. On composer 2.7.x the same situation prints "No packages - skipping audit" and exits 0 — a silent fail-open that passes having audited nothing. Fixed in the package (ConductionNL/.github#135, released as v1.0.1): gate-4 audits `--locked`, treats an exit 0 that audited no packages as a failure, and reports "could not run" in those words rather than as a CVE finding. Both the workflow pin and composer.lock move to v1.0.1 so the two paths into the gates cannot disagree about which version this repo is on. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Picks up the ADR-079 admin-settings link-out: the gear foldout's 'Admin settings' entry becomes an admin-gated LINK to /settings/admin/<app> instead of a modal, and ADR-079 Step 2's organisation credential broker home on CnAdminSettingsShell. Verified the feature is genuinely in the published tarball rather than trusting a green Release run: vue3.2 ships 0 files containing showAdminSettingsLink and 9 containing the removed cnOpenAdminSettings; vue3.4 ships the new API. Live-verified end-to-end on openconnector (ConductionNL/integriq#1136): the foldout renders Settings > Personal settings + Admin settings -> /settings/admin/openconnector, which returns 401 for an anonymous caller. Lockfile updated with --package-lock-only; CI builds and runs this app's own e2e against it.
…ent (#113) The comment introduced in #104 read "the 61 Hydra mechanical gates". That number was never corrected here to a newer one on purpose — correcting it would only reset the clock on the same failure mode. The count has been wrong in both directions: the prose said 61 while only 59 gates actually reported, because gate-33 (axe-core) and gate-24 skip silently. A hard-coded digit sitting in a comment is exactly how that 61/59 gap survived unnoticed — nothing in CI compares the prose to the runner, so the number can only ever drift, and a stale number overstates the coverage this repo has. So the comment now carries no number at all and points at the composer package `conduction/hydra-gates` (ConductionNL/.github, hydra-gates/) as the single source of truth for which gates exist and how many report. Comment-only change: no workflow input, pin or behaviour is touched. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…ion` (#116) GitHub names a reusable-workflow status context `<caller-job-id> / <called-job-name>`, so this caller reported as `check / check-branch` while the org ruleset requires `branch-protection / check-branch`. development is the head branch for the `development -> beta` release PRs, so its tip decides the context name on those PRs.
…stallable standalone apps RESCUED FROM CODEBERG. ExportService::buildScaffoldMap() landed on codeberg.org/Conduction/openbuild as 4a47d797 (2026-07-26) and never reached GitHub. Codeberg is being retired, so it would have been destroyed with the remote. Searched all 230 origin refs for 'buildScaffoldMap' in lib/Service/ExportService.php — zero hits. Three of the four Codeberg-only openbuild commits DID land on GitHub, via merge 001ea41 'import Codeberg feature line onto GitHub'. Blob-identity checked at both tips for PageDesignerHost.vue, BuilderHost.vue, AgentsController.php, GeneratedDocumentController.php, the three listeners and CHANGELOG.md — all IDENTICAL. This method is what that import missed. The port is purely additive — 73 insertions, 0 deletions across the three files — so GitHub's copies were strict subsets and taking the Codeberg blobs reproduces them exactly. All six helpers the method calls (prepareScratchDir, copyTemplate, resolvePlaceholders, bundleDataRegisterSchemas, listFilesSorted, rrmdir) are verified present on GitHub's ExportService. ONE DELIBERATE CHANGE from the Codeberg original. Its docblock read: @SPEC openspec/changes/federated-config-sharing/specs/federated-config-sharing/spec.md That path does not exist on GitHub — and does not exist on the CODEBERG tip either. It was a dangling anchor from the moment it was written, and would fail gate-58 (spec-anchor-existence). Re-pointed at the github-app-sync spec, which is the change this method actually serves (GitHubAppSyncService folds the scaffold into the config-set repo publish) and which does exist here.
The bump itself was always fine; the lockfile was written by the wrong npm.
CI runs Node 20.20.2 / npm 10.8.2. My workstation runs Node 22 / npm 11.13.0,
and an npm-11-written lockfile is REJECTED by npm 10:
npm error code EUSAGE
`npm ci` can only install packages when your package.json and
package-lock.json are in sync.
Missing: pinia@4.0.2 from lock file
Missing: vite@8.2.0 from lock file
npm 11 prunes entries npm 10 still requires — regenerating under Node 20 adds
~1350 lines straight back. Verified `npm ci --dry-run` rc=0 under node:20
before pushing.
Worth noting the misdiagnosis this corrects: `npm ci` passed locally under
npm 11 on the same lockfile, so "works on my machine" was true and useless. The
package versions were never wrong (pinia 2.3.1 / vite 5.4.21, identical to what
development already locked) — only the lockfile FORMAT was.
chore(deps): @conduction/nextcloud-vue 3.0.0-vue3.4
… calls phpmd's TooManyPublicMethods fired on this class the moment buildScaffoldMap() was rescued, and the finding was pointing at something real: of 12 public methods, only two — generateAppZip() and scratchTreeDir() — had a production caller (both RunExportJob). The other nine were public purely so the two test files could reach them. Nine methods are now private: copyTemplate, resolvePlaceholders, packageZip, listFilesSorted, isBinary, prepareScratchDir, getOrCreateAppDataDir, rrmdir, bundleDataRegisterSchemas. Both test files now drive the real entry points instead. Every behaviour the old tests asserted directly on a step is now asserted through its effect on what generateAppZip() / buildScaffoldMap() produce — placeholder resolution, ASCII entry ordering, the snapshot helper-file skip, data-register bundling, scratch-tree cleanup — plus three assertions the old tests did not make at all. Suite: 743 -> 746 tests, 2245 -> 2342 assertions. No baseline entry was added: phpmd is now clean on this class, and the report set-diffs empty against development.
…ldmap feat(publish): rescue buildScaffoldMap() — openbuild-app repos are installable standalone apps
Codeberg is retired; ConductionNL is GitHub-only. GitHub Actions never executes .forgejo/**, so these workflows contributed zero status checks. Deleted 6 of 8, each with a GitHub counterpart (code-quality.yml, documentation.yml, release-beta.yml, release-stable.yml, exporter-e2e.yml). KEPT app-tests.yml + tests.yml -- tests.yml carries the only definition of the l10n extraction gate and the coverage ratchets (no .github/workflows/ l10n.yml here, enable-coverage-guard unset), and openspec/changes/openbuild-nl-locale-parity targets that file. Also repointed the .forgejo reference in tests/e2e/visual/README.md.
… button (#121) `@nextcloud/vue` 9 renamed NcButton's visual prop `type` -> `variant` and repurposed `type` as the NATIVE button type, with `default: "button"`. `nativeType` was removed outright — it was the v8 name. This repo declares ^9.5.0 but the lockfile resolves 9.9.0 (the caret does not move an already-resolved version, so the lockfile is what ships). The settings Save button passed BOTH v8 spellings (`type="primary" native-type="submit"`). Precisely what that does: • `native-type` is undeclared on v9, so it is inert. • `type="primary"` is consumed as the NATIVE button type. "primary" is not a valid one, and HTML's invalid-value default for <button type> is the Submit Button state — so the form DOES still save. • but `variant` never receives "primary", so it keeps its "secondary" default and the button renders unstyled. So this was mis-styled rather than dead. Fixed anyway: the markup is invalid and the shape is one upstream change away from breaking outright. Verified in place: the button sits inside `<form @submit.prevent="save">`. Behaviour is proven in openconnector#1140, which mounts the real NcButton 9.9.0 and pins all three spellings with negative controls. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Remove the 'if [ -f vendor/bin/phpunit ]' guard that made test:unit/test:all always exit 0. Measured: 746 tests now run and the suite fails when an assertion is broken.
chore(deps): @conduction/nextcloud-vue 3.0.0-vue3.6
…pping (#123) * ci(quality): enable the Code Quality gates this repo was silently skipping A skipped job and a passing job are indistinguishable in the Quality Report. Every gate turned on here reported 'skipped' in every run. Each newly-enabled leg was measured against this tree BEFORE being enabled; the results are in the PR description. Legs that were measured failing are enabled anyway - the defects are pre-existing, and the only thing that changed is that CI can now see them. Journeydoc Capture and enable-axe are deliberately NOT enabled. * ci(quality): remove duplicate with: keys; respect documented newman opt-outs A second key in the same with: block is accepted last-one-wins by every YAML parser involved, so the file reads as configured while the value is decided somewhere else in it. Where enable-newman: false already carried a specific, still-live reason (OR React/Async await + SQLite REGEXP; ZGW API incomplete; collections hard-coding non-CI URLs and unseeded registers), it is left off and the reason is quoted rather than overridden. Where it was a bare line with no comment, the stale line is removed instead of shadowed.
…an install (#125) lib/Settings/openbuild_register.json declared no components.registers section. OpenRegister's ImportHandler creates a Register row from that key and nowhere else on the main/beta lines (ImportHandler.php:1514) -- so a clean install created 15 schemas and ZERO registers, and then skipped all 7 seed objects, whose @self.register is resolved through the registersMap that section populates. Every GET /api/objects/openbuild/<schema> 404s with "Register not found: 'openbuild'". Verified on a disposable Nextcloud 34 + PostgreSQL + OpenRegister main (0.2.19), importing the merged register document (base + register.d/*.json, merged exactly as SettingsService::deepMergeConfig does) into a VIRGIN OpenRegister with zero registers: configurations/import -> HTTP 200 register 'openbuild' -> present, id/version 0.9.0 15/15 expected schema slugs present AND bound into the register 15/15 GET /api/objects/openbuild/<schema> -> HTTP 200 The slug is derived, not invented: lib/ and src/ address `objects/openbuild/<schema>` in 41 places and all 7 seed objects carry `@self.register: "openbuild"`. The schema list is by SLUG, not by components.schemas key -- and for openbuild every one of the 15 differs (`Application` -> `application`, `ApplicationTemplate` -> `application-template`, `HelloMessage` -> `hello-message`, `TestCase` -> `rule-test-case`, ...). ImportHandler keys its schemasMap by $schema->getSlug() (ImportHandler.php:1397/1477), so a register listing the KEYS binds ZERO schemas while still looking correctly declared; that is exactly what the first draft of this change did, and only the live check caught it. info.version 0.8.0 -> 0.9.0 so OpenRegister's version-gated importFromApp actually re-imports. appinfo/info.xml already lists the register-importing InitializeSettings step under <install> as well as <post-migration>, so openbuild does NOT have the second, independent gap where the import is post-migration-only and therefore never runs on a fresh install (Installer::installAppLastSteps() guards both migration blocks with `if ($previousVersion !== '')`). NOTE for reviewers: openbuild's appinfo/routes.php references OCA\OpenRegister\AppHost\Routes, which exists only on OpenRegister development. With OpenRegister main installed, enabling openbuild makes EVERY route on the instance return 500 ("Class OCA\OpenRegister\AppHost\Routes not found") -- NC's router reflects every controller during route matching. That is pre-existing, unrelated to this change, and is why openbuild's CI pins OpenRegister development; it is reported separately. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* chore(deps): move to @conduction/nextcloud-vue 2.2.0-vue3.1
The 3.0.0-vue3.* line is being withdrawn from npm. The major was cut from a
real BREAKING CHANGE footer (the retired action-list flow editors), but it
applied to a prerelease channel only our own apps consume, so the line is
resumed at 2.x rather than carried forward. 2.2.0-vue3.1 is a superset of
3.0.0-vue3.6 — it additionally carries the recovered Vue 2 -> Vue 3 component
conversion and the CnGraphCanvas port/loop work. Despite the lower version
number this is not a downgrade in content.
The lockfile is regenerated with npm 10.8.2, matching CI's node 20 toolchain.
Local npm 11 prunes optional entries that do not apply to the current platform,
which makes CI's `npm ci` fail with "Missing: ... from lock file"; running
`npm ci` locally does not reproduce it because npm 11 accepts its own lockfile.
Verified: `npx npm@10.8.2 ci --dry-run` exits 0, and `USE_LOCAL_LIB=false
npm run build` exits 0 with no unresolved modules. The USE_LOCAL_LIB=false is
load-bearing — this repo's webpack aliases @conduction/nextcloud-vue to a
sibling ../nextcloud-vue/src checkout when one exists, so a plain build can
silently compile the sibling's source instead of the package under test.
* fix(l10n): add the 111 source strings missing from en.json
test:l10n was already failing on development before this branch: 111 keys are
used via t('openbuild', '...') in source but absent from l10n/en.json, so the
gate could not pass on any PR.
Generated with the repo's own remedy, `node tests/l10n/check-l10n.js --write`,
which extracts the used strings with key === English source. All 111 additions
are identity-mapped (verified), so this adds no translation content and changes
no rendered English text — it only makes the keys declarable for translators.
This check has no locale-parity stage, so en.json is the whole requirement here
and test:l10n now exits 0 (1288 -> 1399 keys).
---------
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* chore(deps): move to @conduction/nextcloud-vue 2.2.0-vue3.3
Picks up the two releases that landed after 2.2.0-vue3.1:
2.2.0-vue3.2 four dashboard defects — date-range chip shows its dates and
calendar-aligned presets, a dangling labelResolve no longer
renders a raw UUID, and the table's "View all" pins to the
bottom instead of scrolling away
2.2.0-vue3.3 gridstack's stylesheet now ships with the library that
requires it; CnFormDialog splits over-long schema descriptions
behind an info popover; CnContextMenu closes again on outside
press and stops hijacking every popper with a cursor transform
Lockfile regenerated with npm 10.8.2 to match CI's node 20 toolchain — local
npm 11 prunes optional entries that do not apply to the current platform, which
makes CI's `npm ci` fail with "Missing: ... from lock file". Running `npm ci`
locally does not reproduce it, because npm 11 accepts its own lockfile.
Verified: `npx npm@10.8.2 ci --dry-run` exits 0, and `USE_LOCAL_LIB=false
npm run build` exits 0 with no unresolved modules and no reference to a sibling
nextcloud-vue checkout. USE_LOCAL_LIB=false is load-bearing: webpack aliases
@conduction/nextcloud-vue to ../nextcloud-vue/src when that sibling exists, so
a plain build can silently compile the sibling instead of the package under test.
* fix(deps): restore the optional lockfile entries npm 11 pruned
The verification build ran `npm install` under local npm 11 AFTER the lockfile
had been regenerated with npm 10.8.2, which silently re-pruned the optional
entries that do not apply to this platform — per-arch esbuild/rolldown/
lightningcss binaries and @nextcloud/vue's optional pinia and vite.
CI runs npm 10.8.2, whose `npm ci` requires those entries, so it failed at
Install dependencies and took every dependent job with it.
Regenerated with `npx npm@10.8.2 install --package-lock-only` and this time
nothing runs npm 11 against it afterwards. Verified with
`npx npm@10.8.2 ci --dry-run` (exit 0).
---------
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* ci(quality): move the hydra-gates pin v1.0.1 -> v1.2.0 v1.0.1's gate-5 (route-auth) could not tell "this routed method has no auth attribute" from "I could not resolve the controller class", and reported both as the former. It also read route names through `'[a-z_]+#...'`, so every camelCase route slug was invisible to it in either direction, and its 20-line attribute lookback could borrow the PREVIOUS method's attribute. Fixed in ConductionNL/.github#162 (closing #153). v1.2.0 also carries the runner's COVERAGE accounting, which v1.0.1 does not have at all: v1.0.1's summary printed only "N gate(s) failed", so a gate that never ran was indistinguishable from one that passed. EXPECT NEW FINDINGS. gate-5 now sees code it never saw. Red is the honest outcome of a measurement that previously was not taken; nothing here is baselined or suppressed to avoid it. * ci(quality): advance the hydra-gates pin v1.2.0 -> v1.3.0 v1.2.0 added the COVERAGE accounting but not the declarations that accounting asks for. v1.0.1/v1.2.0 have no `_skip` vocabulary, so a gate whose prerequisite is absent emits nothing and require-full-coverage (default ON) counts it as DID NOT RUN. v1.3.0 ships 36 not-applicable declarations; measured on doriath PR #160, Hydra Gates went failure -> success on the pin alone. Lands this repo on the same pin as the rest of the fleet.
…1.2 (#134) Three test files declared SPDX-License-Identifier: AGPL-3.0-or-later while every other licence signal in the repo (LICENSE, composer.json, package.json, appinfo/info.xml and every lib/**.php @license tag) says EUPL-1.2. - tests/e2e/docs-screenshots.spec.ts - tests/e2e/visual/_visual-helpers.ts - tests/e2e/visual/openbuild.visual.spec.ts NOT changed: lib/Resources/template/appinfo/info.xml still says <licence>agpl</licence>. That file is inside the vendored nextcloud-app-template snapshot, which docs/releasing.md says must be refreshed wholesale by rsync and never hand-edited, and whose .snapshot-meta.json records the exact upstream commit it came from. Upstream already says EUPL-1.2, so the correct fix is a snapshot refresh, not an edit here. See the PR body. Header-only change; PHPUnit (746 tests) and vitest (1364 tests) are identical before and after. gate-28: PASS -> PASS. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… inert exclude-pattern (#129) * chore(phpmd): burn down 46 baselined findings and fix the inert exclude-pattern Part of ConductionNL/.github#155 (fleet PHPMD suppression audit). `composer phpmd` reports 0 today. With phpmd.baseline.xml deleted it reports 195. The gate is live: openbuild's Code Quality workflow calls the shared ConductionNL/.github quality.yml, where `enable-phpmd` defaults to true, so the phpmd leg genuinely runs on every push and PR — the baseline is hiding findings from a check that really executes. Two things worth recording about the baseline file itself: * Its 174 entries suppress 195 findings, because a PHPMD baseline entry is scoped to a (rule, file) pair — optionally a method — never to a line. A class-scoped entry is therefore an open licence: every FUTURE violation of that rule in that file is silently suppressed too. "174 entries" is not "174 known problems", and the shortfall is granularity, not staleness. * PHPMD auto-discovers phpmd.baseline.xml from the working directory. Dropping --baseline-file from the composer script changes nothing; only deleting the file disables it. What this commit retires (measured, PHPMD 2.15.0 / PHP 8.4.22, baseline deleted both times, --exclude '*/Resources/template/*' on every leg): 195 -> 149. * ElseExpression 27 -> 1. * MissingImport 3 -> 0 (fully-qualified `new \RuntimeException` in AppOverrideService). * ShortVariable 9 -> 0 ($m/$ca/$cb/$i -> $matches/$condA/$condB/$seq/$cursor). * LongVariable 5 -> 0; the two renamed constructor-promoted properties are also renamed at their named-argument call sites in the unit tests. * UnusedFormalParameter 3 -> 1. Two were genuinely dead and are REMOVED, with their call sites updated: AppOverrideService::userVersionSlug($appId) and AppRepoParser::decodeChannelEntry($path). * CyclomaticComplexity 41 -> 40, from the two consolidating extractions below. The ElseExpression work is behaviour-preserving throughout: initialise-then- override where both branches were pure, guard-and-return where they were not, `continue` inside loops, and extractions where both arms had to write so exactly one could run. No ternaries were introduced. Two traps from the brief fired for real, and re-measuring after every rule is what caught them: 1. THE EXTRACTION TIPS THE CLASS. Three extractions from AppOverrideService pushed BooleanArgumentFlag 13->14, ExcessiveClassLength 7->8 and TooManyMethods 4->5 — the class sits exactly at those thresholds. All three were restructured to need no new method (a `continue` guard in deepMergeDelta, an early return in upsert), and the one that genuinely could not be — upsertUserDelta, where both arms write a different object — was LEFT AS AN ELSE rather than traded for three other findings. That is the single remaining ElseExpression. 2. REMOVING AN `else` ADDS COMPLEXITY. Converting the two elevate-or-not if/elses in MigrateToVersionedModel::migrateOne() gave it a NEW NPathComplexity finding (336), and converting the two update-or-create if/elses in AutomationCompilerService::applyRuleSet() took its CyclomaticComplexity from 9 to 11. Both were fixed STRUCTURALLY rather than by relaxing anything: each pair of duplicated branches was folded into one guarded helper (`runElevated()`, `saveOrUpdate()`), which removes the elses AND lowers the host method's complexity below where it started. A third, the state chain in run(), keeps its `else if` and only drops the bare `else` — PHPMD flags the bare `else` only, so that costs no extra path, whereas two independent ifs would have doubled an already-flagged NPath. Also applies the fleet-standard scoped-ruleset shape. The `<exclude-pattern>*Migration*</exclude-pattern>` nested inside the UnusedFormalParameter `<rule>` was INERT — PHPMD 2.15 honours exclude-pattern only as a direct child of `<ruleset>`. The rule now lives alone in phpmd-unusedparams.xml with a top-level exclusion, run as a second leg with neither leg able to short-circuit the other, and `--exclude '*/Resources/template/*'` preserved on BOTH legs so the analysed file set is unchanged. Stated plainly: openbuild has no lib/Migration directory, so this retires ZERO findings here. It is adopted for fleet uniformity. phpmd.baseline.xml is RETAINED. 149 findings remain and deleting it would redden a gate that really runs. What remains, and why: * CyclomaticComplexity 40 / NPathComplexity 25 / ExcessiveClassComplexity 23 / ExcessiveMethodLength 11 / ExcessiveClassLength 7 — genuine debt in the automation-compiler, copilot and app-override services. * CouplingBetweenObjects 15, TooManyMethods 4, ExcessiveParameterList 3, TooManyPublicMethods 1 — class-level; need collaborator extraction, which AppOverrideService's thresholds show cannot be done piecemeal. * BooleanArgumentFlag 13 — genuine debt; each is an API change. * StaticAccess 4 — `\OCP\Server::get()` service-locator calls in three broker paths plus OpenRegister's `Bootstrap::register` in AppInfo\Application; the Bootstrap one is a bootstrap-time static by design. * ElseExpression 1 — AppOverrideService::upsertUserDelta, see trap 1 above. * UnusedFormalParameter 1 — INTERFACE-MANDATED. SeedHelloWorldFixture::execute() implements Symfony\Component\Console\Command\Command::execute(InputInterface $input, OutputInterface $output). `$input` is unused but cannot be dropped. * LongClassName 1 — UnsupportedAutomationCombinationException. Deliberately NOT renamed: the name is a normative identifier in openspec/specs/automation-designer/spec.md and in three archived changes. Renaming it in code alone would desynchronise the spec, and rewriting archived changes is out of scope. Verified: phpunit 746 tests / 2342 assertions before AND after, both green. phpcs exits 0 (the renames left stale docblock alignment; fixed with the repo's own phpcbf, 24 errors, no remaining errors). psalm and phpstan both exit 0. Both phpmd legs exit 0 with the baseline in place, so CI stays green. No suppression, baseline entry, threshold change or skip was added anywhere. * chore(phpmd): keep this PR single-purpose — revert the else-fixes in 6 files Touching these six files pulls three PRE-EXISTING hydra-gate failures into the diff-scoped gate run, all of them in code this PR did not write: * gate-2 forbidden-patterns — the deliberate, documented error_log() in AppInfo\Application::register(), which carries its own phpcs:ignore and a comment explaining that no PSR logger exists that early. Removing an else necessarily moves those lines into the diff. The call must stay; the gate has no per-call waiver; so the file must stay untouched. * gate-46 spec-anchor-existence — 17 dead @SPEC targets across ExportsController, AutomationsController, AppInfo\Application, RuleActionDispatcher, AutomationCompilerService and TemplateSeedService. They point at openspec/changes/automation-designer/... which was ARCHIVED to openspec/changes/archive/2026-07-11-automation-designer/... and at #req-autd-NNN anchors that exist in NO version of the spec, canonical or archived — the canonical spec uses '### Requirement: <prose>' headings. Repairing them means deciding, per tag, which Requirement each method now implements. That is spec-traceability work for the automation-designer owner; guessing it here would encode WRONG links, which is worse than a dead one. * gate-49 controller-exception-translation — AutomationsController::recompileAndRespond() lacks a try/catch or @throws. The gates are FILE-scoped (_filter_grep_by_scope keys on the path), so any edit to one of these files puts all of its pre-existing findings in scope. Reverting is the only way to keep a PHPMD suppression burn-down from silently becoming a spec-metadata rewrite. Cost, measured: openbuild goes 149 -> 161 with the baseline deleted (still 195 -> 161). Specifically this gives back 11 ElseExpression, 1 ShortVariable and 1 LongVariable, and drops the AutomationCompilerService::saveOrUpdate() consolidation, so CyclomaticComplexity returns to 41. Follow-up filed in the PR body: those three gate failures are real and now precisely diagnosed, but they belong to a separate change. * fix(phpmd): narrow the UFP exclude to the app's own lib/ directory Positive-controlled (PHPMD 2.15, PHP 8.4, absolute paths): with */lib/X/* a probe in lib/Service/X/ is reported; with */X/* it is silently swallowed. Both correctly exclude the app's own directory, so the narrow form is strictly safer. Matches the fleet-canonical shape in nextcloud-app-template #125, doriath #157, larpingapp #263 and planix #315.
#135) The embedded template snapshot's appinfo/info.xml hardcoded `<licence>agpl</licence>` while the very same file's description read "Free and open source under the EUPL-1.2 license". Every app OpenBuild has ever generated inherited that contradiction, and nothing failed. The `{{license}}` token was already wired end to end — ExportJobService -> RunExportJob -> PlaceholderResolver, defaulting to EUPL-1.2 at all three layers — and reached exactly one file, src/manifest.json. info.xml never consumed it. `<licence>{{license}}</licence>` closes that, and as a side effect a caller who picks a licence now gets the licence they picked. EUPL-1.2 is the schema-correct spelling, not a preference: the Nextcloud appstore's own info.xsd enumerates `EUPL-1.2` as an allowed <licence> value. It does NOT enumerate `eupl` — which is what upstream nextcloud-app-template's `main` branch currently ships, and is separately invalid. A test first, and shown to fail. testGeneratedAppDeclaresTheRequestedLicence() exports a real app and asserts info.xml declares EUPL-1.2 and does not declare agpl. Against the pre-fix snapshot it failed with the generated app's info.xml in the message; after the one-line change all 3 integration tests and all 746 unit tests pass. Nothing in the suite covered this before: the existing unresolved-placeholder assertion matches /\{\{[a-zA-Z]+\}\}/, and a hardcoded wrong value contains no placeholder to be left unresolved. Why this is a one-line Edit and not the whole-tree refresh docs/releasing.md prescribes. The refresh is unsafe as written, measured three ways: 1. There is no upstream ref this tree fast-forwards from. The declared sourceCommit 7ee06aae is not an ancestor of upstream main OR development; `git branch -r --contains` places it only on two abandoned wsl-rescue/* branches. 2. The snapshot carries OpenBuild-only fixes upstream never received — at least 10 commits have edited files inside lib/Resources/template/ since, among them c87d8c4 "fix(template): the generated app could not be built at all (#39)" and 12da26f "fix(export): make exported app a Tier-4 manifest consumer (ADR-024)". `rsync --delete` reverts both. 3. The dialects differ. OpenBuild resolves {{token}}; upstream's info.xml uses {APP_NAME}/{APP_SUMMARY}/{APP_DESCRIPTION}. PlaceholderResolver does not know that dialect and the unresolved-placeholder assertion does not match it, so a refreshed tree would ship literal {APP_NAME} into every generated app and no test would fail. .snapshot-meta.json and docs/releasing.md now say so at the point of use, so the next agent does not run the rsync on the strength of the doc alone. docs/releasing.md also now records that the "CI drift check" it describes does not exist — no workflow mentions drift or nextcloud-app-template — which is why the snapshot sat 86 days stale shipping the wrong licence. .path-manifest.txt no longer lists .snapshot-meta.json: the regeneration command in docs/releasing.md excludes it, so the checked-in manifest disagreed with its own generator. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
* ci: run the E2E (Playwright) job — it has never executed `enable-playwright` defaults to false and code-quality.yml never set it, so the shared workflow's `E2E Tests (Playwright)` job has reported `skipped` on every run this repo has produced, while 50 spec files sat in tests/e2e/. Unlike `enable-newman: false`, nothing recorded that as a decision. Adds `enable-playwright: true` + `playwright-test-path: tests/e2e`, and the config that path selects. The workflow runs its chosen config with NO `--project`, so the root config would have run `docs-capture` (owned by the Journeydoc Capture job) and `visual` (PNG baselines its own header says a CI Linux runner cannot byte-match) alongside the regression suite. tests/e2e/playwright.config.ts declares only `chromium`, and writes its report/traces to the app root — the paths the workflow's upload steps look at. The root config is untouched. `playwright-seed-command` supplies the two pieces of instance setup global-setup.ts can only do through a docker container (it refuses to guess one, by design): disabling the wizard's 10/hour user rate limit, and seeding the hello-world fixture via the test-only occ command. * ci: run on Postgres — OpenRegister cannot run on SQLite Measured on run 31018480923: the E2E seed died before a single spec ran with Failed to seed hello-world fixture: … SQLSTATE[HY000]: General error: 1 no such table: information_schema.columns OR's MagicTableHandler reads `information_schema.columns` to decide whether an object table needs creating; Postgres and MySQL ship that view, SQLite does not have it. Same family as documented Newman blocker #2 (`no such function: REGEXP`, also OR's MagicMapper, also SQLite-only). `pgsql` is the shared workflow's own default — the repos whose E2E is green (opencatalogi, doriath) set no `database:` at all. Stated explicitly rather than deleted so the reason survives. * fix(seed): the hybrid example could never be seeded Measured on run 31029961494, the first time the E2E job has ever run: Failed to seed hello-world fixture: A hybrid app's description is read-only — it mirrors the installed Nextcloud app it customizes. HybridMetadataLockListener locks slug/name/description/productionVersion on a hybrid app and fires on ObjectUpdatingEvent only — creation with the locked identity is explicitly allowed. seedHybridExample() created the Application, created the version, then UPDATED the Application to attach productionVersion: an update touching two locked fields at once, rejected every time. So the hybrid fixture has been unseedable since that listener shipped, and nothing said so — globalSetup swallows a seed failure as a warning, and the E2E job had never run in CI. Mint the Application UUID up front, create the version against it, then write the Application in ONE create carrying its full identity and the production pointer. The lock is satisfied by construction, not bypassed. The forward reference is safe: OR validates relation targets only for `$ref` properties carrying `validateReference`, and ApplicationVersion.application has neither. * ci(e2e): make the Playwright job produce a verdict, and fix the deep-link root cause The first run of this suite in repo history (run 31030663352, job 92390852268) was CANCELLED at the shared workflow's 45-minute cap after 45m16s. A cancellation is not a verdict: no tally, and — because Playwright writes its HTML report only after the last test — `actions/upload-artifact` found nothing ("No files were found with the provided path: .../playwright-report/"), so there were no traces or screenshots either. What it did measure before it died, read out of the job log: COLLECTED 249 tests in 50 files REACHED 83 unique tests (98 attempts, 15 of them retries) pass 4 fail 68 skip 11 slowest PASSING test 3.5s (the four passes: 0.4s, 0.4s, 2.9s, 3.5s) time in failures 29.6 min of 42.5 BUDGET — tightened only in the strict direction * retries 1 -> 0. A retry can only convert red to green; it bought nothing and doubled the cost of every failure, which is what blew the budget. * per-test timeout 60s -> 30s. Measured: the slowest test that PASSED took 3.5s, so 30s is ~8.5x it, and still above the largest wait a spec asks for itself (`waitForSelector(…, 20_000)`) plus navigation, so a genuine failure still reports its own message. Six component-blocks failures had been sitting on the full 60s. * NEW globalTimeout of 36 min, below the job's 45. Playwright now stops itself, prints the tally, and flushes the report so the artifact uploads. * workers stays 1. Four independent classes of cross-talk make parallelism unsafe (shared `hello-world` slug with three writers, exact-count assertions, the brute-force throttle, and the `_limit=5000` backend bottleneck). It is documented in the config so it is not raised later to save wall clock. ROOT CAUSE OF THE FAILURES — `htaccess.IgnoreFrontController` openbuild builds its router as `createWebHistory(generateUrl('/apps/openbuild'))`, and `generateUrl()` prefixes `/index.php` unless the served page reports `modRewriteWorking`. A freshly `maintenance:install`ed instance does not set it, so the router base is `/index.php/apps/openbuild` while every spec navigates to the pretty `/apps/openbuild/...`. They disagree, vue-router matches nothing, and the SPA mounts and renders perfectly — on the DASHBOARD. No 404, no console error, no failing request. The pass/fail pattern is the proof. The only UI tests that passed were builder-host's first two, whose inner router is based at `/apps/openbuild/builder/<slug>` — the exact URL the spec opens — so its DEFAULT route is the page under assertion. builder-host's third test, the only one that needs the `/messages/new` SUB-path to survive, failed with the rest. Everything else that passed was a pure `request.get()` API test. The CI router already serves pretty URLs (the workflow's own front-controller check proves it), so setting the flag states something TRUE about the instance rather than working around anything. SEED — adopt tests/e2e/ci-seed.sh (ported from #111) `playwright-seed-command` becomes `bash apps/openbuild/tests/e2e/ci-seed.sh`, replacing the two-command inline string. Beyond the pretty-URL fix it: forces the register import over the admin HTTP API (the `InitializeSettings` repair step runs session-less, so OR's RBAC denies it and the step downgrades the throw to a warning) and VERIFIES all 15 schema slugs; pre-marks the admin as a returning user so nc-vue's first-visit walkthrough and support dialog stop swallowing clicks in every fresh Playwright context — via the product's own preference mechanism, and deliberately NOT for the rbac-* fixture users, whose specs assert exactly that; proves the hello-world fixture over HTTP rather than trusting the occ exit code; and GATES on the served bundle actually being JavaScript, because a missing bundle returns HTTP 200 text/html, not 404. HARNESS HONESTY — ported from #111 * tests/e2e/support/baseUrl.ts accepts `BASE_URL` (which is what the shared workflow exports) and no longer defaults to localhost:8080 off CI — that is the SHARED dev container, and this suite WRITES. * tests/e2e/global-setup.ts fails LOUDLY on CI when admin or an rbac-* session cannot be minted. An absent storage state does not error; the spec just runs UNAUTHENTICATED, and rbac-403 / versionRouting then pass for exactly the wrong reason. * tests/e2e/.gitignore — the root ignores are root-anchored and do not match these paths. APP DEFECTS FIXED * HybridMetadataLockListener no longer locks `productionVersion` (the canonical spec REQUIRES a hybrid app to point at its delta version, so the lock made every create/republish path fail at its last step) or `description` (never in the spec's lock list, and OR's PUT-semantic saveObject delivers an unmentioned field as null, which the lock read as a deliberate edit). Unit test updated. * register.d/10-business-rules.json: dropped an empty `"condities": {}` — OR rejects both `{}` and `null` for object properties, so the key has to be omitted. * fix(seed): the production-pointer write silently wiped hello-world's permissions MEASURED VERDICT that this fixes (run 31040914410, job 92425229324) — the first run of this suite that finished on its own: COLLECTED 249 REACHED 249 pass 154 fail 19 skip 76 23.6 min (the previous run was CANCELLED at the 45-minute cap having reached 83 of 249 with 4 passes, and uploaded no report at all). Of the 19 failures, 14 share ONE cause, and it is not a permissions bug in the product — it is a partial write in the seeder. OR's `saveObject()` update path is PUT-semantic: `SaveObject::fillMissingSchemaPropertiesWithNull()` sets every schema property absent from the payload to null. Step 3 of the hello-world seed exists only to attach `productionVersion`, and it listed four fields. `permissions` was not one of them — so the owner block step 1 had just written was nulled a moment later, and the fixture came out owned by NOBODY (`allowAdminBypass` is false, so an empty block denies everyone). From each failing spec's side that is indistinguishable from the product refusing a legitimate owner: * spec-coverage/openbuild-runtime REQ-OBR-005 x4 — `locator.fill` on `<textarea readonly … data-testid="openbuild-editor-textarea">`; `:readonly` is bound to `obAppRole === 'viewer' || 'none'`. * REQ-OBR-006b, REQ-OBR-008b — the owner-only Settings menu entry absent, and "an owner must get an editable manifest" failing on the same readonly. * copilot-panel — `403 {"error":"forbidden","message":"You do not have owner or editor access to application 'hello-world'."}` * automations x7 — the edit modal never closes because the save 403s; the failure ARIA snapshot shows `alert: "Could not save the automation."`, and the four later scenarios then cascade on the row that was never created. (Automation ops require WRITE_ROLES ['owners','editors'].) The fix carries `permissions` forward in that write, with a comment naming the PUT semantics so it is not trimmed again as "redundant". PROOF IT CAN FAIL — new tests/Unit/Command/SeedHelloWorldFixtureTest.php The field-preservation assertion the failure mode demands: it asserts the LAST Application write still carries `permissions.owners`. A test that only checked the field the write MEANT to change passes on the broken code. Mutation-proved locally against PHP 8.4: * remove `permissions` from the pointer write -> 1 failure ("Failed asserting that null is identical to Array [0 => 'user:admin']") * replace the minted UUID with a constant -> 2 failures (v4 shape, and each run minting a distinct id) * restore HybridMetadataLockListener's old LOCKED_FIELDS -> 3 failures in that listener's own suite All restored; 751 tests green locally (8 errors are `ZipArchive` missing from the throwaway PHP container, absent in CI). ALSO * tests/e2e/spec-coverage/dashboard-overview.spec.ts — `a[href$="/apps/openbuild/"]` matches TWO links, and `.first()` is Nextcloud's GLOBAL app-menu entry, which is labelled "OpenBuild", not the in-app "Dashboard" nav entry. Read straight off the failure's ARIA snapshot. Scoped to `#app-navigation-vue` — the id NcAppNavigation renders and the target of the page's own "Skip to app navigation" link, containing all four in-app entries. A narrower locator; the label assertion is unchanged. The other three entries had been passing only because no global menu link ends in `/applications`, `/templates` or `/features-roadmap`. * SeedHelloWorldFixture: two comment lines re-indented — PHPCS reads `// x` as an inline comment with 3 leading spaces and errors ("use block comment if you need indentation"). That was the whole of the red `PHP Quality (phpcs)` job. * fix: hoist the seed permissions to a constant, and floor the bundle gate on size MEASURED (run 31046063272, job 92443507627) — the previous commit's effect: COLLECTED 249 REACHED 249 pass 169 fail 4 skip 76 18.8 min against 154/19/76 in 23.6 min before it. Carrying `permissions` through the production-pointer write cleared 15 failures, and the run also got 4.8 minutes faster because fourteen of them were no longer sitting on a timeout. That commit's explanation lived inside `execute()`, which two gates objected to and both were right: * PHPCS — `// 403 {...}` is an inline comment with 3 leading spaces ("use block comment if you need indentation"). * PHPMD — ExcessiveMethodLength: `execute()` reached 125 lines against a threshold of 100. Fixed by giving the block a better home rather than trimming the reasoning: a `SEED_PERMISSIONS` class constant carrying the full docblock, referenced from both Application writes. That also removes the duplicated literal, so the two writes can no longer drift apart — which is the failure mode the comment is about. BUNDLE GATE — content type alone cannot catch a truncated bundle `ci-seed.sh` asserted the served bundle's Content-Type was JavaScript. A zero-byte or truncated bundle still serves `200 application/javascript`, so that gate passes over precisely the failure it exists to catch — reported live on a sibling repo where it did exactly that. Added a floor on the byte count curl already reports (measured on run 31040914410: 13,552,436 bytes; floor set at 100,000 so it fires on truncation without becoming a second size budget that silently drifts). REMAINING FAILURES — 4, all root-caused, none of them a budget artifact: * docudesk-document-templates ×3 — `/apps/docudesk/api/templates` 404s because docudesk is not installed on the runner. The third failure is self-documenting: the page renders "Docudesk is not installed or enabled on this instance" alongside the warning the spec was looking for. The fix is adding docudesk to `additional-apps` so these RUN; that input feeds every job in the workflow including six PHPUnit matrix cells, so it belongs in its own change, not here, and NOT behind a ninth skip. * page-editor-coverage REQ-PEC-006 — `selectOption('openbuild-hello-world-production')` on a `<select aria-invalid="true">` with "did not find some options". The per-version data register that name refers to does not exist: the seeded ApplicationVersion records `register: 'openbuild-hello-world'` as metadata only. A real fixture gap in what the seeder provisions, and which service ought to create that register is a product question, not a guess. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…kflow needs The Hydra Gates job fails with a message that says outright it is not about this repository: hydra-gates-ref <old> does not contain: scripts/lib/check_spec_anchors.py scripts/lib/check_form_labels.py scripts/lib/check_license_triangle.py The reusable workflow floats on @main and calls those scripts BY PATH inside the PINNED package, so a pin older than the scripts cannot run the gates that implement them. A pinned ref is a silent expiry date on every upstream change, and the failure reports on the pin while saying nothing about the code. v1.5.0 is the first tag containing all of them, verified by reading each path at that tag rather than assuming the newest tag has everything. Swept across the fleet: 11 of 13 repos were pinned below v1.5.0 and every one of them was failing this way.
…elopment/marked-18.0.9 build(deps): bump marked from 12.0.2 to 18.0.9
…elopment/cyclonedx/cyclonedx-npm-6.0.1 build(deps-dev): bump @cyclonedx/cyclonedx-npm from 4.2.1 to 6.0.1
…ment/nextcloud/ocp-34.0.3 build(deps-dev): bump nextcloud/ocp from 34.0.2 to 34.0.3
…ment/twig/twig-3.28.0 build(deps-dev): bump twig/twig from 3.27.1 to 3.28.0
…ment/phpmetrics/phpmetrics-2.11.0 build(deps-dev): bump phpmetrics/phpmetrics from 2.9.1 to 2.11.0
…tservice-lazily fix: event listeners cannot constructor-inject a published OpenRegister interface
…st (#271) * fix(tests): stub the OpenRegister FlowService for AppChannelApplierTest The unit-test bootstrap resolves OpenRegister types via stubs so PHPUnit's MockBuilder can build them outside a real container. FlowChannelProvisioner depends on FlowService (ADR-065's sanctioned flow entry point) but no stub existed for it, so all 18 AppChannelApplierTest cases failed on a clean development checkout with "Class or interface OCA\OpenRegister\Service\Flow\FlowService does not exist" rather than exercising the intended assertions. * fix(phpcs): inline @var docblocks must use /* */, not /** */ CI's phpcs gate runs against all of lib/ on every PR, so this pre-existing violation (present on development itself, unrelated to the FlowService stub this branch adds) was blocking this PR's own check. Same convention already followed elsewhere in the codebase (e.g. RuleActionDispatcher.php). --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* fix(adr-083): guard the three unguarded OpenRegister reaches gate-66 (openregister-dependency-shape) reported 3 violations, all the same shape: `objectService()` looked the service up in the container with nothing establishing that OpenRegister is installed first. BEHAVIOUR IS UNCHANGED. Every caller already wraps the reach in a catch that logs and returns null, so an instance without OpenRegister degrades exactly as it does today. What changes is that the dependency is declared where a reader — and the gate — can see it, instead of being implied by a catch several methods away, outside the window the gate can correlate. class_exists() rather than a settings service, because none of these listeners injects one and adding a constructor dependency purely to ask a yes/no question is the wrong trade. ADR-083 lists class_exists as an accepted way to establish availability precisely because it answers the same question the DI container would otherwise have answered fatally. Measured with a control on the same tree and checker: before: 3 violations after: 0 violations, 138 files checked php -l clean on all three.⚠️ gate-61 (6 listeners doing synchronous work with no deferral or justification) is NOT addressed here. Each needs its own judgement about whether the work must be synchronous — that is a separate change, not a docblock sweep. * style(phpcs): replace the inline @var docblocks with assert() narrowing phpcs forbids a `/** @var */` docblock inside a method body ("Inline doc block comments are not allowed"), while psalm needs that narrowing or the declared `: ObjectServiceInterface` return becomes a MixedReturnStatement — the two tools disagreed on the same three lines. `assert($service instanceof ObjectServiceInterface)` satisfies both: psalm narrows on it, phpcs sees a statement rather than a comment, and production pays nothing because zend.assertions is off.⚠️ These three errors are PRE-EXISTING on development, not introduced here — verified by running phpcs against origin/development, which reports the same 3. The old permissive gate mapped phpcs exit 1 to success, so they shipped unnoticed; .github#483 now makes them fatal, and this PR touches those exact files.⚠️ They also mean my pre-#483 fleet sweep UNDER-REPORTED. I claimed "0 phpcs errors across 21 repos"; openbuild had 3. The CI log did say so — three per-file "FOUND 1 ERROR" lines — but my summing collapsed them with `sort -u` before counting, so three files reporting one error each read as one distinct line. Count occurrences, not distinct lines. Verified after the fix: phpcs 0 errors in lib/, gate-66 0 violations, php -l clean on all three, psalm exit 0 with no Listener findings. * fix(coverage): mark the ADR-083 class_exists() guards as uncoverable The unit-test bootstrap's OpenRegister stubs (tests/stubs/openregister-stubs.php) make class_exists('OCA\OpenRegister\Service\ObjectService') true unconditionally in this suite, so the "OpenRegister genuinely absent" branch this change adds can only ever fire on a real instance — there is no way to exercise it from a unit test without faking class_exists() itself. @codeCoverageIgnoreStart/End excludes it from the coverage-ratchet's statement count instead of leaving it as an uncovered addition. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
github-store.spec.ts deliberately covers only the read side (search/ browse) against the shared dev instance, by design — a write path must never touch shared data. POST /apps/openbuild/api/shop/github/ install (ShopController::githubInstall()), which installs a real app-repo-format-v2 repo end to end (register + connectors/automations/ flows + skills + agents channels), had only ever been exercised by hand, once, in the throwaway session that produced #264 — never by a repeatable, automated check. app-channel-application's own spec already documents this exact scenario ("Installing from the shop applies its channels") but carried no @e2e proof until now. Runs against the disposable instance only (PLAYWRIGHT_BASE_URL), gated on the same GitHub-credential capability probe github-store.spec.ts uses — skips cleanly with a stated reason when no credential is granted, rather than failing or fabricating a pass. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
) * fix(export): find an application's agents in hermiq's register too FlowAndAgentExportBundler::bundleAgents() queries only register: openbuild for an application's agents, so an agent living in hermiq's own register (real for hydra-console, whose agents include hermiq-seeded and hand-created ones) was silently invisible to every export — no exception, no reported skip, just an export with no agents. Adds a fallback: when openbuild's own agent schema finds nothing and hermiq is installed, a second query runs against hermiq's register for the same applicationSlug, guarded via IAppManager the same way SkillChannelDelegate already treats hermiq as optional. hermiq's richer agent shape travels into the export unmodified, never coerced into openbuild's own narrower schema. Live-verified on a fresh isolated instance against real backfilled hermiq-register agents: the old code silently found zero agents for hydra-console despite three real matches; the new code finds all three and correctly excludes a fourth carrying a different applicationSlug. * fix(tests): PHPCS compliance and coverage for the hermiq-register agent fallback - Add testTheFallbackDegradesToEmptyWhenHermiqsOwnLookupThrows to close a coverage-ratchet gap on findHermiqRegisterAgentsOrEmpty()'s catch branch. - Convert positional arguments to named parameters throughout the new fallback test coverage and the touched ExportServiceTest constructor call, per this repo's PHPCS convention. - Fix a pre-existing PHPCS error in DocumentGenerationListener.php (double-asterisk inline @var docblock, disallowed) encountered while bringing this PR's touched files to a clean phpcs run. * fix(tests): ExporterEndToEndTest's FlowAndAgentExportBundler double needs appManager The exporter-e2e job constructs FlowAndAgentExportBundler directly with the pre-this-PR 3-argument signature (flowMapper, objectService, logger). This PR's new hermiq-register fallback needs an IAppManager to check whether hermiq is installed, so the constructor gained a required appManager parameter — this call site was the one other place in the repo building the class directly and needed updating alongside ExportServiceTest's already-fixed one. * fix(phpcs): inline @var docblocks must use /* */, not /** */ Same pre-existing lib/-wide violation as DocumentGenerationListener.php in the previous commit — CI's phpcs gate scans all of lib/, and these two files hit the same rule. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…rite (#273) DocumentGenerationListener and AutomationApprovalTriggerListener both fire on ObjectCreatedEvent/ObjectUpdatedEvent/ObjectDeletedEvent and scan the whole openbuild-register automation schema with an unbounded findAll() to find matching automations — the exact OpenRegisterFlowResolver failure mode gate-61 (ADR-078) measures against, just scoped to a smaller register. A real install's configured automations number in the tens; the bound exists to cap worst case, not because that count is expected. Full async deferral was considered and rejected here: these listeners already query synchronously by design (the trigger needs to resolve before dispatching the matching automation's action), and a bound is the honest fix for "unbounded," not a workaround for it. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…debar race
`development` E2E was red on one test of 192:
✘ app-icon-management — removing an icon (REQ-OBICON-004)
Error: ApplicationIconTab must mount IconUploadSection
30 × locator resolved to <div class="ob-icon-section">…</div>
- unexpected value "hidden"
Thirty resolutions is the whole diagnosis. The locator never reported
"not found" — the component HAD mounted. It was present and hidden, and
the wait was hopeless from its first poll.
openIconsTab already wraps open-sidebar → click-tab → assert-panel in
`.toPass()`, because the file documents that the detail page re-closes the
sidebar while it is still hydrating. The section assertion sat AFTER that
retry. So when the sidebar re-closes in the window between the panel being
seen and the section being checked, the section is in the DOM inside a
closed sidebar and nothing can reopen it — the assertion can only wait out
its budget, which is exactly what the trace shows.
Moving it inside the same block does not soften the check: the section
must still become visible. It just lets the idempotent open-and-click that
already guards the tab recover the sidebar when it closes late.
The twin helper in tests/e2e/iconUpload.spec.ts gets the same treatment.
It had no retry at all — an instant `isVisible()` probe (which does not
wait) followed by a click — and passed only because
`dismissFirstVisitOverlays()` burns a few seconds ahead of it. The
app-icon-management copy already called that out as "luck, not a guard";
this makes it a guard. `.app-sidebar__toggle` is a TOGGLE, so the old
shape could also click the sidebar shut.
Not reproduced locally: openbuild's e2e is built for a disposable
container and its own support/baseUrl.ts warns that pointing it at the
shared :8080 writes fixtures onto other people's instance. The change is
reasoned from the CI trace and mirrors the remedy this file already
applies one level up; CI is the verification.
Closes #268
…-the-retry fix(e2e): assert the icon section inside the retry that guards the sidebar race
…oken v1.8.0 shipped an ObjectServiceInterface WITHOUT patchObject() and with updateObject() still summarised as "Apply a partial update" — the wording that sent a consumer down the erasing path. The correction landed on main two days after the tag; every app has been pinned to the broken copy since. It is not confined to this repo: hydra-gates claims OCA\OpenRegister\Contract\ in its composer autoload, a LONGER psr-4 prefix than openregister's own OCA\OpenRegister\ -> lib/, so the gate package wins. Nine repos vendor it, so under OC_App::loadApps() whichever app registers first defines the contract for the whole instance. Measured on a running instance, softwarecatalog's vendor directory was supplying openregister's interface, and updating openregister ALONE did not change the winner — which is why this lands across the fleet rather than in one repo.
chore(deps): take hydra-gates v1.8.1 — the contract v1.8.0 shipped broken
…-unstable.20260819215516 # Conflicts: # appinfo/info.xml
…60819215516 chore(release): 0.6.1-unstable.20260819215516
`};//end match` closes a match EXPRESSION, not a declaration, so Squiz.Commenting.ClosingDeclarationComment does not claim it and the ordinary inline-comment rules apply: space after //, initial capital, full stop. php_codesniffer 3.13.6 did not flag this; 4.0.4 does, and it is the only thing standing between this repo and the phpcs 4 bump. Measured on decidesk: with this one line fixed, 4.0.4 goes from exit 3 (2 errors) to exit 0. The 1,833 `}//end method()` comments elsewhere are the recognised closing form and are untouched.
style(phpcs): terminate the match-expression end comment (unblocks the phpcs 4 bump)
The stylelint config packages declare stylelint >=16 as a peer, so none can move alone — each single-package dependabot PR left the tree unresolvable and stylelint aborted before linting a file. Bumped together: stylelint ^17.14.1, @nextcloud/stylelint-config ^3.2.2, stylelint-config-recommended-scss ^17.0.1, stylelint-config-recommended-vue ^1.6.1 (the version @nextcloud/stylelint-config@3.2.2 peer-requires). package-lock.json is REGENERATED rather than patched. The old lock pinned stylelint-scss 5.3.2, whose peer is 'stylelint ^14.5.1 || ^15.0.0', and npm will not move a transitive pin in place — every incremental install failed ERESOLVE against it. The regeneration moves 14 packages across a major, and all 14 are inside stylelint's own subtree (stylelint-scss, cosmiconfig, css-tree, meow, globby, html-tags, write-file-atomic …), which is what a 15 -> 17 upgrade of this tool is. Also fixes the one deprecated CSS declaration stylelint 17 promotes to an error: word-break: break-word -> overflow-wrap: break-word. npm run stylelint now exits 0.
…bump build(deps-dev): bump stylelint to 17 as one coupled upgrade
…elopment/nextcloud/axios-2.6.0 build(deps): bump @nextcloud/axios from 2.5.2 to 2.6.0
Bumps [squizlabs/php_codesniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) from 3.13.6 to 4.0.4. - [Release notes](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) - [Changelog](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/4.x/CHANGELOG-3.x.md) - [Commits](PHPCSStandards/PHP_CodeSniffer@3.13.6...4.0.4) --- updated-dependencies: - dependency-name: squizlabs/php_codesniffer dependency-version: 4.0.4 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Two user-visible defects are fixed by this bump. TWO AI-COMPANION HEXES ON EVERY PAGE. The companion singleton landed in 2.7.0. Below that the host app's own companion never stands down, so any page of this app rendered a second hex 8px from hermiq's — measured on a running instance: openconnector (2.7.1) showed ONE, openbuild (2.6.3) showed TWO, both visible at 52x60, from two separate mounts. THE DETAIL PAGE RECLOSED ITS SIDEBAR WHILE HYDRATING. CnDetailPage set sidebarSeeded and never read it, so 'open' was re-applied on every sync and each reactive change during hydration reset it to the prop default. Fixed in nextcloud-vue#711; that is the cause behind openbuild#268 and, on the evidence, #188. Lockfile only — the existing caret already allowed this. Three-line diff (version, resolved, integrity).
…ment/squizlabs/php_codesniffer-4.0.4 build(deps-dev): bump squizlabs/php_codesniffer from 3.13.6 to 4.0.4
chore(deps): take @conduction/nextcloud-vue 2.8.2 (was 2.8.0)
Brings beta up to date with development (737 commits) for tonight's
fleet-wide beta checkup.
Conflicts resolved, all in development's favour:
.forgejo/workflows/release-{beta,stable}.yml — DELETED on development as
part of the Codeberg/Forgejo CI retirement; modified on beta by earlier
dev-merge reverts. Codeberg is retired, so the deletions stand.
src/builder.js, src/manifest.json — development is substantially ahead,
not merely different: builder.js is 456 lines against beta's 251, and the
manifest has migrated to the MDI icon vocabulary (ViewDashboardOutline)
per ADR-077 while beta still carries the old icon-category-* classes.
Beta's only unique src commit is a chore (version bump, layers icon,
manifest, docs build) that development has since superseded.
.gitignore, appinfo/info.xml — files development actively maintains.
Beta-only files that did not conflict are preserved.
Contributor
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 624/624 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-20 21:21 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings beta up to date with development for tonight's fleet-wide beta checkup (team decision, 2026-08-20), so tomorrow's review runs against current code on the online beta environment.
Why this PR and not the auto sync PR: the automatic
development -> betaPR was CONFLICTING and could not merge. This branch is that same merge, with the conflict resolved.Resolution policy: for files development actively maintains, development's version wins — that is what a release-channel sync means. Beta-only files that did not conflict are preserved untouched.
The per-conflict reasoning is in the merge commit message.
Also carried to beta: the converged
release.yml(beta's pre-convergence workflow has noworkflow_dispatchtrigger), and the app signing path that started working today once theNEXTCLOUD_SIGNING_KEY/CERTsecrets landed — the first successful releases since 2026-08-17.