Release: merge development into beta - #18
Conversation
Release: promote beta to stable — initial CI pipeline and app scaffold
Merge beta into main
…uctors Issues caught by every code review on apps built from this template: - <licence>agpl</licence> → <licence>eupl</licence> - OC.requestToken → getRequestToken() from @nextcloud/auth - Added SPDX-License-Identifier: EUPL-1.2 to all PHP/JS/Vue files - Constructor properties: private → private readonly
The app store only recognises agpl/apache/mit — not eupl. Source files use SPDX EUPL-1.2 headers (the actual licence). info.xml uses agpl for store compatibility only.
…uth dep Fixes from planix: correct .phphunit → .phpunit typo, remove duplicate glob patterns, and add @nextcloud/auth for getCurrentUser() support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ✅ 215/215 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 15:18 UTC
Download the full PDF report from the workflow artifacts.
Replace the individual conduction symlink inside openspec/schemas/ with a single symlink at openspec/schemas pointing to hydra schemas directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…asset only) (#24) The central Quality workflow (ConductionNL/.github#34) now publishes SBOMs exclusively as release assets — see SECURITY.md "Software Bill of Materials". This PR cleans up the per-app remnants: - delete .github/workflows/sbom.yml (the central job replaces it) - delete the checked-in sbom.cdx.json (release asset is the source of truth) - gitignore SBOM files so future generations don't accidentally land in repo Stable URL for clients: https://github.com/ConductionNL/nextcloud-app-template/releases/latest/download/sbom.cdx.json Co-authored-by: SBOM Cleanup <ops@conduction.nl>
Path-based codeowner mapping per the OR-abstraction-audit follow-up (2026-05-03). PRs that touch each domain auto-request review from the matching owners; first-to-approve unblocks per the org ruleset.
Wires the ConductionNL bundling pattern from ADR-004 (Build / bundling) into the template so apps cloned from this repo get a working dashboard widget out of the box and never trip the appName/devtool/duplicate-framework pitfalls that landed across opencatalogi/pipelinq/procest/docudesk. What is added: - webpack.config.js: optimization.splitChunks with stable-filename shared chunks for Vue + @nextcloud/vue + pinia + icons + @conduction/nextcloud-vue. Each entry-point keeps only entry-specific code; shared chunks load once. - lib/Dashboard/ExampleWidget.php: minimal IWidget. load() attaches shared chunks BEFORE the per-widget bundle (vendor → nc-vue → widget). Comments explain why and reference ADR-004. - src/exampleWidget.js: webpack entry that registers the Vue renderer via OCA.Dashboard.register. Hard-coded id matches Widget::getId() from PHP. - src/views/widgets/ExampleWidget.vue: minimal NcDashboardWidget that fetches /api/items via @nextcloud/axios with try/catch + graceful empty state. - AppInfo/Application.php: registerDashboardWidget(ExampleWidget::class). - README: 'Adding a dashboard widget' how-to listing the 5 registration points and pointing at ADR-004 for the full rationale. Apps that don't need a dashboard widget delete: - lib/Dashboard/ + src/exampleWidget.js + src/views/widgets/ - the registerDashboardWidget(...) line in Application.php - the exampleWidget entry in webpack.config.js The splitChunks block is harmless with only main + adminSettings entries (produces small shared chunks that two entries reuse) and starts paying off the moment a widget is added.
Spec the canonical Tier-4 scaffolding for the JSON manifest renderer pattern in nextcloud-app-template. Codifies hydra ADR-024's "new apps MUST adopt the manifest from inception" requirement at the source — the template — rather than retrofitting per app. Includes proposal, design, tasks, and 10 REQ-TMV1-* requirements covering manifest contents, bootstrap pattern, registry contract, webpack alias, dependency floor, and the manifest-first README quickstart.
Add the canonical template manifest with 4 example pages (one each of type dashboard / index / detail / settings) and 4 menu entries. Declares openregister as the default dependency. Settings page demonstrates the version-info rich-section widget. Add src/customComponents.js as the empty-by-default registry contract with a single example placeholder (CustomExample) so the registry's role is visible to first-time cloners. The manifest does NOT reference CustomExample by default — it only ships as documentation. Add tests/validate-manifest.js (copied from decidesk's reference) for Ajv-based schema validation. Wired up via npm run check:manifest. Trim l10n/en.json to a clean baseline aligned with the new manifest strings; add an empty l10n/en_US.json placeholder.
Adopt decidesk's mount-survivable bootstrap pattern (commits 50e4df7c
+ 866ff132) as the template default:
main.js:
- Import bundledManifest from './manifest.json' and customComponents
from './customComponents.js'.
- Build vue-router routes from manifest.pages[*].{id,route} via a
routesFromManifest() helper that uses a shallow-cloned
CnPageRenderer ({ ...CnPageRenderer }) — required because Vue 2's
Vue.extend() mutates the component options object with a _Ctor
cache, which throws against the lib's frozen barrel exports.
- Pass shallow-cloned defaultPageTypes and customComponents to
App.vue as props.
- Mount on #content immediately, NOT inside loadTranslations'
callback (NC dev installs commonly 404 the /l10n/<locale>.json
route, which would silently kill boot). Translation load is
fire-and-forget; strings fall back to English on miss.
App.vue:
- Mount <CnAppRoot> with manifest + customComponents + pageTypes
props, app-id, translateForApp closure, and permissions array.
- Provide an objectSidebarState reactive channel via provide() and
mount <CnObjectSidebar> in the #sidebar slot — the standard
pattern for CnDetailPage → host-rendered sidebar.
settings.js + AdminRoot.vue:
- Keep the Nextcloud admin app-settings webpack entry-point (a
distinct surface from the manifest's type:'settings' SPA page).
Replace the deleted views/settings/AdminRoot.vue with a minimal
placeholder NcSettingsSection that documents the divergence.
Delete the legacy shell:
- src/router/index.js (routes built from manifest at boot).
- src/navigation/MainMenu.vue (CnAppNav replaces it).
- src/views/Dashboard.vue (manifest type:'dashboard' replaces it).
- src/views/settings/ (manifest type:'settings' replaces it).
…README
package.json:
- @conduction/nextcloud-vue ^0.1.0-beta.3 → ^1.0.0-beta.12 (the
published lib version with the Vue.extend frozen-component fix).
- @nextcloud/router ^2.0.1 → ^3.1.0 — required by @nextcloud/vue
8.37+ (NcDashboardWidget / NcAvatar import getBaseUrl, missing
from router 2.x).
- Add ajv ^8.17.1 + ajv-formats ^3.0.1 devDependencies for the
manifest validator.
- Add scripts.check:manifest → node tests/validate-manifest.js
(satisfies the fleet adoption spec's build-time validation gate).
webpack.config.js:
- Add @nextcloud/axios$ alias to force the lib's transitive axios
import to resolve to the app's installed copy (decidesk pattern,
commit ed34703c). Without the $ exact-match suffix webpack walks
up to the lib's own node_modules and loads a second axios
instance, breaking shared interceptors / CSRF tokens.
eslint.config.js:
- Override no-console / n/no-process-exit / n/shebang for the
tests/validate-manifest.js Node CLI script.
README.md:
- Lead with manifest-first messaging in the intro paragraph and
in the OpenRegister callout.
- Add an "Adding a page (manifest-first)" section that documents
the page-type table and tells cloners to edit src/manifest.json
rather than writing per-page Vue files. Custom Vue components
are only required for type:"custom" pages.
- Add a "Renaming the app" section listing the files where the
app id appears (the manifest itself does NOT carry the id).
- Update the directory-structure diagram to reflect the new
layout (manifest.json, customComponents.js, no router/, no
navigation/).
feat: scaffold the JSON manifest renderer pattern as the template default (template-manifest-v1)
…copy with placeholders The SCSS webpack rule was added to webpack.config.js in a prior PR (commit 209224e "chore: bump nextcloud-vue + router, add axios alias"), but the actual sass + sass-loader packages were never added to devDependencies. Result: running 'npm run build' on a fresh clone of the development branch produces a 'Module parse failed: Unexpected token' error whenever nextcloud-vue components ship <style lang=scss> blocks. Fix: - package.json devDependencies: add sass ^1.99.0, sass-loader ^16.0.8 (matching the versions already in opencatalogi + decidesk) - package-lock.json regenerated via 'npm install --package-lock-only' Separately, appinfo/info.xml currently ships marketing copy for the template itself (Nextcloud App Template, A template for creating new Nextcloud apps, plus a 13-line feature-list description). This copy survives '/app-create' scaffolding because the placeholder substitution phase only rewrites identifier-shaped tokens (app-template -> {APP_ID}, AppTemplate -> {APP_NAMESPACE}). Result: every newly scaffolded app ships with the template's own description until a human remembers to rewrite it. Fix: - appinfo/info.xml: replace summary + description content with {APP_NAME}, {APP_SUMMARY}, {APP_DESCRIPTION} placeholders so the /app-create skill can auto-substitute them from openspec/app-config.json during scaffolding. Companion PR in ConductionNL/market-intelligence updates the .claude/skills/app-create placeholder-replacement-guide.md so the new placeholders get processed automatically.
…-placeholders fix: add sass + sass-loader to devDeps + use placeholders for info.xml marketing copy
… deps PR #28 (merge 4601d2c) shipped a package-lock.json that was missing 11 transitive dependencies of pinia (devtools-kit, devtools-shared, mitt, perfect-debounce, speakingurl, superjson, copy-anything, is-what, rfdc, plus a nested @nextcloud/dialogs > pinia entry). Result: `npm ci` failed on every CI quality job, taking down 5 checks at once (Security npm, License npm, ESLint, Stylelint, SBOM). Fix: regenerate the lockfile with `npm install` (no package.json changes). Verified locally: - `npm ci` now succeeds - `npm run lint` passes - `npm run stylelint` passes - `npm audit --audit-level=high` reports 0 high/critical (25 low/moderate)
chore: regenerate package-lock.json to fix quality CI failures
…hecks (#32) Adds a Spec Validation workflow + three checks that catch the class of bug seen in scholiq Wave 2 *before* it reaches development: - tests/validate-json-strict.js — strict JSON parse of src/manifest.json + lib/Settings/*_register.json; FAILS on duplicate keys (git merges JSON line-by-line; two adds at the same key but different file positions produce no textual conflict, just a doc with a dup key, and json_decode keeps the last → silent schema/page loss) and on `appendOnly` nested inside an x-openregister block (OpenRegister's Schema::hydrate only reads a top-level appendOnly, so a nested one is silently dropped). - tests/validate-register.js — structural checks on the register seed: every schema has slug/type/required/properties; slug uniqueness; lifecycle `requires:` references a PHP class that exists under lib/ (catches scholiq's missing CoursePublishGuard); a 'schema looks clobbered / is a stub' heuristic (≤3 props + no x-openregister-*). Optional deep check against OR's configurations/validate endpoint when OR_BASE_URL + OR_BASIC_AUTH are set. - package.json: check:register, check:json-strict, check:specs scripts. - .github/workflows/spec-validation.yml: runs `npm run check:specs` on every push/PR. Add 'Spec Validation / validate' to the branch-protection ruleset's required checks to make it block merges. - README: documents the new checks and why they matter. Every app scaffolded from this template inherits the gate.
* feat: adopt the Features & Roadmap menu Ships the in-product Features & Roadmap page wired into the manifest so apps scaffolded from this template inherit the Settings-section "Features & roadmap" entry (powered by OpenRegister's github-issue-proxy, UI from @conduction/nextcloud-vue). Mirrors the per-app adoptions (procest/decidesk/ pipelinq/scholiq/openbuilt). - src/views/FeaturesRoadmap.vue — thin wrapper around CnFeaturesAndRoadmapView; the `repo` fallback is `ConductionNL/nextcloud-app-template` — cloners change it (or provide it via IInitialState `<appId>::features_roadmap_*`) - customComponents.js — register it as the `FeaturesRoadmap` custom component - manifest.json — add the `FeaturesRoadmap` custom page (route /features-roadmap) + a `FeaturesRoadmapMenu` entry in the settings section - bump @conduction/nextcloud-vue ^1.0.0-beta.12 → ^1.0.0-beta.35; pin @nextcloud/axios ~2.5.2 (+ overrides) — 2.6.0 ships a broken `exports` field that breaks @nextcloud/vue under webpack (cf. ConductionNL/openregister#1489) Refs: ConductionNL/hydra#251 * chore: refresh package-lock.json for the @conduction/nextcloud-vue ^1.0.0-beta.35 bump
…ate (#31) Wires the AI Chat Companion MCP-tool pattern (hydra ADR-034/035) into the template so new Conduction apps get it by default. - lib/Mcp/ExampleToolProvider.php — heavily-commented copy-me provider with two trivial example tools: app-template.ping and app-template.describeApp - lib/AppInfo/Application.php — registers the provider under the alias OCA\OpenRegister\Mcp\IMcpToolProvider::{appId} - tests/Stubs/Mcp/IMcpToolProvider.php — stub interface until openregister PR #1466 ships the real one; wired via composer autoload-dev + bootstraps - tests/Unit/Mcp/ExampleToolProviderTest.php — contract test (7 cases) - README: new "AI Chat Companion / MCP tools" section - psalm.xml: allow the OCA\OpenRegister\Mcp\IMcpToolProvider cross-app ref Widget mount: the template already mounts CnAppRoot from @conduction/nextcloud-vue (^1.0.0-beta.12), which renders the companion FAB once nextcloud-vue beta.31 (CnAiCompanion) is published — no bump needed.
…ings) (#34) - Change SettingsMenu entry to action: "user-settings" / icon: "Cog"; drop route + section. CnAppNav invokes cnOpenUserSettings inject (CnAppRoot) which opens NcAppSettingsDialog. The Settings type:'settings' page stays for direct-URL /settings access. - App.vue: import NcAppSettingsSection; add #user-settings slot to CnAppRoot with a placeholder NcAppSettingsSection — replace with real settings content when scaffolding a new app. - README: one-line note in the manifest/menu section documenting the pattern. - Upgrade @conduction/nextcloud-vue from beta.30 → beta.35 (schema 1.5.0) to support action: "user-settings" — package.json already declared ^1.0.0-beta.35; node_modules was behind. - validate-manifest PASS (0 Ajv errors, schema 1.5.0); lint 0 errors; build succeeds.
Keeps the app template on the current lib — CnIndexPage store-backed self-fetch (nc-vue #223) + columns[].formatter / .widget / .aggregate + pages[].config.filter (#219/#221/#222) — so scaffolded apps start at the right version.
chore(deps): bump @conduction/nextcloud-vue to ^1.0.0-beta.40
…fold (#36) Every new Conduction app currently inherits only a documentation.yml deploy-workflow stub with no Docusaurus site behind it. This adds the default so a scaffolded app is born docs-ready: - docs/ on @conduction/docusaurus-preset 2.6.1 — createConfig/BRAND_THEME, brand <DetailHero>/<WidgetShelf> landing page, intro.md, custom.css, sidebars.js, package-lock.json, static/CNAME (app-template.conduction.nl), static/img/logo.svg. - journeydoc scaffold (hydra ADR-030) — tutorials/{_category_,user/,admin/} with two stock stories (user "open the app for the first time" → Dashboard, admin "manage settings" → Admin Settings), tests/e2e/docs-screenshots.spec.ts capture suite, and playwright.config.ts with chromium + docs-capture projects. - .github/workflows/documentation.yml — deploy from `development` to app-template.conduction.nl (was: `documentation` branch / app-template.app). - .gitignore — docs/ build artefacts, docs/i18n/nl/, playwright outputs. - README.md — Screenshots section now points at the journeydoc workflow, directory-structure block lists docs/ + tests/e2e/ + playwright.config.ts, and a Documentation section describes the docs-site build + deploy. Build verified locally (`npm ci --legacy-peer-deps && npm run build`).
…Vue-mount failure) (#37) webpack.config.js uses splitChunks with `enforce: true` cacheGroups that emit two shared chunks (`<appId>-shared-vendor`, `<appId>-shared-nc-vue`). The main and adminSettings entry bundles wrap their Vue mount in `__webpack_require__.O(0, [shared chunks], …)`, which only fires once every listed chunk has registered itself on `self.webpackChunk<appId>`. With only the entry script in `addScript()`, the shared chunks never load, the mount callback never fires, and the app silently renders nothing. Mirrors the canonical fix in zaakafhandelapp#206. ExampleWidget.php already loaded the shared chunks correctly; this brings the page and admin-settings templates into line. Because this is the scaffold template, every newly-generated app inherits the fix.
…ate from code CI/CD) (#38)
…the fleet migration recipe (#142) * chore: wire up conduction/coding-standard, .editorconfig, NC 34 and the stylelint glob Config only. The reformat is the NEXT commit on purpose, so that .git-blame-ignore-revs can name a commit that contains nothing but whitespace. * style: reformat with nextcloud/coding-standard — whitespace only, no behaviour change Applied by `composer cs:fix`. Tabs, same-line braces, `(int)$x`, `'a' . 'b'`, ordered imports — Nextcloud's dialect, which this app now passes unchanged. Isolated from the configuration change on purpose so .git-blame-ignore-revs can name a commit that touches nothing but formatting. Reviewing it line by line is not a useful activity; the useful review is the previous commit. * chore: ignore the reformat commit in git blame 96b2382 touches ~22 files and changes no behaviour. Without this, every line it reflowed attributes to it and the real author is one --skip away. GitHub honours the file automatically; locally it needs `git config blame.ignoreRevsFile .git-blame-ignore-revs` once. * fix: unpin hydra-gates-ref — the gates are consumed at the tip This repo pinned hydra-gates-ref: v1.3.0. gate-65, added in ConductionNL/.github#375, flags it, and it is right to: a pin is a silent expiry date. 22 repos once sat on v1.0.1 while 16 gates were dead fleet-wide and every one reported PASS (.github#159); a default later flipped at @main then reached those same old runners and turned them red on gates they had no subject matter for (#173). Both directions come from the two halves of one system moving independently. The shared workflow is consumed at @main, so the gate package must be too — then a gate fix reaches this repo with no commit in this repo. conduction/hydra-gates is still constrained to dev-feat/centralised-quality-config in composer.json, which gate-65 also flags, correctly. That one becomes ^1.0 when #375 merges. * chore: consume conduction/hydra-gates at ^1.0, not a branch ConductionNL/.github#375 is merged and tagged v1.7.0, so the temporary dev-feat/centralised-quality-config constraint can go — along with the VCS repositories block, since both packages are on Packagist now. gate-65 reported two findings on this branch and now reports none: 15 rules checked, zero deviations. The gate found its own author's pins first, which is the behaviour you want from it. * fix(ci): resync composer.lock and test the whole declared NC range composer.lock pinned conduction/hydra-gates to dev-feat/centralised-quality-config while composer.json requires ^1.0, so the lock did not satisfy composer.json. `composer install` therefore exited 4 ('lock file is not up to date') in EVERY PHP job — before any tool ran. That is the single cause of all 14 red checks on this PR, including Security and License, which touch no linter at all. Regenerated with a targeted update: hydra-gates dev-branch -> v1.7.1. coding-standard stays v1.0.0, nextcloud/ocp stays v34.0.2. `composer validate --no-check-all --no-check-publish` now passes. Deliberately WITHOUT --ignore-platform-reqs: that flag also discards the `config.platform.php = 8.3` override and lets the solver pick symfony/string v8.1.2, which requires php >=8.4.1 — a lock that cannot install on the PHP 8.3 test leg. The platform override alone is enough. nextcloud-test-refs now covers 32-34, the full range info.xml declares. stable34 stays element 0 because newman/playwright/journeydoc read [0] as their single server. * fix(appinfo): put info.xml elements in the order the XSD requires The shared workflow gained an 'info.xml lint' job that validates against appstore's own info.xsd. It reported 5 errors here, none of them introduced by this PR — appinfo/info.xml is byte-identical to development apart from min-version 28 -> 32. Three were pure element ORDER, and order is not cosmetic: <info>, <dependencies> and <repair-steps> are all modelled as xs:sequence, so a correct element in the wrong position is a hard validation error. <dependencies> php now precedes nextcloud <info> repair-steps now precedes settings, navigations follows it <repair-steps> install now follows post-migration Reordering carries no runtime meaning — Nextcloud resolves all of these by element name — so this is only what the schema demands. Note the third error was MASKED by the second: fixing the top-level order is what first let the validator descend into <repair-steps> and reach post-migration at all. Verified with lxml against the exact XSD the job fetches, with the pre-fix file as a positive control: it reproduces CI's five errors line-for-line, and the fixed file is down to three. The remaining three are one root cause and are NOT fixed here: the app id 'app-template' contains a hyphen, which appstore's pattern '[a-z]+[a-z0-9_]*[a-z0-9]+' rejects (and which the navigation <route> pattern rejects for the same reason). That is a 235-occurrence / 84-file rename covering the runtime APP_ID constant, DI service ids, routes and e2e URLs — far outside this PR. 'info.xml lint' therefore stays RED, pre-existing. * fix(ci): update the vendored coverage-guard to the merge-base-comparing version PHPUnit (PHP 8.3, NC stable34) was the only one of the six legs to fail, and it failed with the suite GREEN — 'OK (20 tests, 54 assertions)', printed twice. It is the single leg the coverage ratchet runs on, because the shared workflow gates those steps on php-version == inputs.php-version AND nextcloud-ref == fromJSON(nextcloud-test-refs)[0]. The actual error: scripts/coverage-guard.php predates merge-base comparison. This repo shipped the 59-line copy, md5 493a42ed, which is byte-identical to openregister@MAIN — the stale branch. The current script is 265 lines, md5 3184a800, identical on portaliq, openregister and decidesk @development, and app-neutral. Installed verbatim. CI's exact probe now passes locally: php scripts/coverage-guard.php --capabilities | grep -qx against -> against / update-baseline / capabilities Attribution: this is caused BY this PR, not pre-existing. Turning on enable-coverage-guard is what makes these steps run at all — on development both coverage jobs report 'skipped'. The PR enabled the ratchet while shipping a script too old to satisfy it. Under --against the committed .coverage-baseline (23.55) is informational only; the measured merge-base value is the floor. * fix(appinfo): rename the app id from `app-template` to `apptemplate` The App Store's own `info.xsd` rejects the hyphen. On PR #142 the `info.xml lint` job failed with three errors from one cause: appinfo/info.xml:4: Element 'id': 'app-template' is not accepted by the pattern '[a-z]+[a-z0-9_]*[a-z0-9]+' appinfo/info.xml:66: Element 'id': same appinfo/info.xml:68: Element 'route': 'app-template.dashboard.page' is not accepted by '[0-9a-zA-Z_]+(\.[0-9a-zA-Z_]+){2}' `apptemplate` satisfies both patterns. `app_template` would too, but every one of the 18 core Conduction apps is a single lowercase word, and this repo is the cookie-cutter every new app is generated from — so the separator-free form is the one worth seeding. The app id is NOT the repository name. `nextcloud-app-template` stays exactly as it is: clone paths, remotes, `ConductionNL/…` and `codeberg.org/…` URLs, the docs hostname `app-template.conduction.nl` (`docs/static/CNAME`, the `cname:` input of `documentation.yml`, `docusaurus.config.js`'s `url`), and the archived change docs under `openspec/changes/archive/`. The PHP namespace `AppTemplate` also stays: `info.xml` declares it explicitly via `<namespace>`, which is what Nextcloud reads first. Moved with the id: - `Application::APP_ID`, `SettingsSection::getID()` + its `imagePath()` app name, `AdminSettings::getSection()`, the deep-link registration - `MetricsController::METRIC_PREFIX` — a Prometheus name derived from the id - the navigation `<id>` and `<route>` (`apptemplate.dashboard.page`, which still resolves to `dashboard#page` in `appinfo/routes.php`) - `webpack.config.js`'s `appId`, so the emitted bundles are `apptemplate-*.js` — the names `Util::addScript($appId, $appId . '-main')` asks for; and the dashboard widget id in `src/exampleWidget.js`, which must equal `ExampleWidget::getId()` - every `t('apptemplate', …)` / `loadState` / `register` l10n domain, every `/apps/apptemplate/…` URL, the `#apptemplate-settings` mount id and the `.apptemplate-admin-settings` CSS prefix - `app-name:` / `app-id:` in the GitHub + Forgejo workflows, the `custom_apps/apptemplate` mount path in `openspec/config.yaml`, and the `dev-link` symlink in the Makefile — Nextcloud loads an app from `custom_apps/<app-id>`, so a stale path means the app silently never loads - `package.json`, `package-lock.json`, `composer.json`, `phpcs.xml`, `eslint.config.js`, `openspec/app-config.json` - the canonical specs under `openspec/specs/` and the two unarchived changes, which assert the id in URLs and metric names Two files are named after the id and were renamed, not just edited: - `lib/Settings/app_template_register.json` -> `apptemplate_register.json`. `SettingsService` builds the path as `getAppPath(APP_ID) . '/lib/Settings/' . APP_ID . '_register.json'`, which resolved to `app-template_register.json` — a file that never existed. The `is_file()` guard turned that into `$data = []`, so the register import ran on an empty payload and logged success. The rename makes the derived path hit the file for the first time. - `tests/integration/app-template.postman_collection.json` -> `apptemplate.postman_collection.json` (Newman globs the directory). `tests/validate-register.js` derived the PHP namespace by camel-casing the `<id>`, which gave `OCA\AppTemplate` only because the old id had a hyphen to split on; under `apptemplate` it would have produced `OCA\Apptemplate` and mis-resolved every `requires:` class reference. It now reads `<namespace>` first and falls back to the camel-case, matching `App::buildAppNamespace()`. Verified: `xmllint`-equivalent XSD validation against the same schema the CI job fetches fails with those three errors on the parent commit and passes here; `git grep app-template` leaves only repository references, the docs slug, and the archived changes; PHP lint clean (28 files); PHPCS 0 errors / 34 warnings, identical to the parent commit; PHPUnit 20/20 (54 assertions) inside a Nextcloud 34 container; `npm run build`, `lint`, `stylelint`, `check:specs` and `check:manifest` all pass. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 738/738 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-13 08:12 UTC
Download the full PDF report from the workflow artifacts.
…gates v1.7.3 (#144) The template is the cookie-cutter every new Conduction app is generated from, and it was the last place in the fleet still carrying its own copy of the PHPMD ruleset and the PHPStan base — so every app scaffolded from it today started out already behind the 18 core apps. - composer.lock: conduction/hydra-gates v1.7.1 -> v1.7.3 - phpmd.xml: 124-line local ruleset -> 9-line stub referencing vendor/conduction/hydra-gates/quality-config/phpmd.xml - phpmd-unusedparams.xml: deleted; the second leg of the `phpmd` script now points at the vendored copy. Both legs and the worst-exit-code behaviour are preserved. - phpstan.neon: includes the shared phpstan-base.neon; keeps only this app's own baseline. The two excludePaths for vendor-bin and lib/Resources/template go — this repo has neither directory, and a non-existent entry ABORTS PHPStan 2.x. - docs/canonical-files.md: the PHPMD/PHPStan rows described byte-synced content; they are pointers now. Verified by the effective ruleset and the resolved parameters, not by exit code.
…3.16 (#145) * chore(deps): pin @conduction/nextcloud-vue to 2.2.0-vue3.16 The template sat on 2.2.0-vue3.3 while the 18 core apps had moved to 2.2.0-vue3.16 — thirteen prereleases behind, so every app scaffolded from it started on a component library the fleet no longer runs. Installed with npx npm@10.8.2 (npm 11 poisons the lockfile and `npm ci` then fails in CI on jobs that never touch Vue). Zero call-site migrations were needed, matching the rest of the fleet. npm run build exit 0, 2 asset-size warnings (identical to before) npm run lint exit 0 npm run stylelint exit 0 npm run check:specs exit 0 * feat(format): adopt Nextcloud's prettier config, and give it a CI leg The template shipped a 38-line `.prettierrc` that nothing invoked: no `prettier` dependency, no npm script, no workflow reference. It was inert in CI and NOT inert in editors — `.vscode/settings.json` sets prettier as the `[css]` formatter, and the file asked for 2-space indent and double quotes on `.ts`, both of which `@nextcloud/eslint-config` then flags. Every app scaffolded from the template inherited that. It is replaced by the setup the pilot established (ConductionNL/larpingapp#321): `prettier` + `@nextcloud/prettier-config` + `eslint-config-prettier`, a `format` / `format:fix` script pair over a RESTRICTED glob, and `.prettierignore`. `eslint-config-prettier` is spread as the LAST element of eslint.config.js so it can only turn rules off. Without it, eslint reports 18 problems here (14 `curly`, 3 `operator-linebreak`, 1 `vue/html-self-closing`) — every one a layout rule fighting prettier. The glob is `**/*.{js,ts,vue,css,scss}`, never `.`: measured on this tree, unrestricted prettier would rewrite 112 files including 22 under `openspec/` (parsed by gate-16 and gate-19), 38 `l10n/*.json` and the OpenRegister register JSON under `lib/Settings/`. `format` is added to `frontend-checks` in code-quality.yml. The shared quality workflow runs eslint and stylelint but has no prettier job, so without that leg this would be another formatter enforced only by a developer's keystrokes. prettier --check clean (25 files reformatted) eslint exit 0, all 21 vue/no-deprecated-* rules still ON stylelint exit 0 check:specs exit 0 npm run build exit 0 (2 pre-existing asset-size warnings) npm ci from clean exit 0
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-13 09:48 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 738/738 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-13 09:57 UTC
Download the full PDF report from the workflow artifacts.
Brings the template onto the stack Nextcloud's own apps run (nextcloud/forms is the reference): eslint 10, flat config, ESM. New apps scaffolded from this repo now start there instead of on eslint 8. Follows ConductionNL/larpingapp#325, which piloted this. `eslint.config.mjs` here is the fleet's canonical copy — the only parts an app should change are the app-specific globals and file-scoped exemptions at the end. FOUR THINGS THAT HAVE TO BE RIGHT TOGETHER ------------------------------------------ 1. Node 22. `@nextcloud/eslint-config@9` declares `engines.node: ^22.14 || ^24 || >=26` and imports `findPackageJSON` from `node:module`, first available in 22.14. On Node 20 eslint dies before linting a single file. The shared workflows now default to Node 22 (ConductionNL/.github#450); `.nvmrc` and `engines.node` say the same here. 2. The two peer dependencies. `vue-eslint-parser` is NOT a dependency of @nextcloud/eslint-config — it is a peer of the `eslint-plugin-vue@10` it bundles, so the app must supply it at ^10, and `@typescript-eslint/parser` must be resolvable at the top level because vue-eslint-parser requires it by name. Get this wrong and `typescript-eslint/base` — the only other config object claiming `**/*.vue` — parses every SFC as TypeScript, every `.vue` file fails with `Parsing error`, and since eslint reports a parse failure as ONE finding and lints nothing else in the file, the whole Vue layer goes unchecked while the problem count looks small. 3. An `overrides` entry for nc-vue's eslint peer. It declares `eslint: "^8.56.0 || ^9.0.0"` as an OPTIONAL peer — optional means npm will not install it, NOT that a mismatched version is accepted — so a fresh `npm install` fails with ERESOLVE against eslint 10. `"@conduction/nextcloud-vue": { "eslint": "$eslint" }` resolves it declaratively, with no --force and no --legacy-peer-deps. The constraint is inert for us: it exists because nc-vue ships an eslint preset this config no longer imports. Remove the override once nc-vue widens the range. 4. The jsdoc override must carry NC's own `ignores`. v9 registers the jsdoc plugin ONLY inside `nextcloud/documentation/*`, and every one of those blocks ignores `**/tests/**`, `**/*.spec.*` and friends — Nextcloud does not require JSDoc in tests. Naming a `jsdoc/*` rule for a test file therefore references a plugin that is not registered there and eslint refuses to run at all: with `lint: "eslint src tests"` this took out ALL 12 files under tests/ while src/ was fine. conductionVue3Fixes AND FlatCompat ARE GONE ------------------------------------------- Measured with --print-config against v9: 21 of 21 `vue/no-deprecated-*` are enabled, and `vue/no-v-model-argument` / `vue/no-v-for-template-key` — the two Vue-2 rules the preset had to switch off because they are inverted under Vue 3 — are not enabled at all. Nothing left for it to correct. CODE FIXED RATHER THAN RULES RELAXED ------------------------------------ NC sets `no-console: [2,{}]` with no allowances and `no-unused-vars` with `ignoreRestSiblings: false` / `caughtErrors: 'all'`. Fleet policy is that we may be STRICTER than Nextcloud, never merely different: - 5 console statements -> `src/logger.js` (@nextcloud/logger), the logger Nextcloud's own apps use; it tags each line with app id and acting user. - `let x = null` before a try/catch that always assigns -> no initialiser (`no-useless-assignment`). - `v == null` -> `v === null || v === undefined` (`eqeqeq`). - a rethrow inside catch now carries `{ cause: err }` (`preserve-caught-error`), so the original stack survives. - `./_app-url` -> `./_app-url.ts` (`import-extensions/extensions`), verified with `playwright test --list`: still 10 tests in 3 files. - ExampleWidget's `title` prop was declared, passed by src/exampleWidget.js and never read (`vue/no-unused-properties`). It is now bound to the table's `ariaLabel`, which gives the list an accessible name it lacked under `hideHeader` — the dashboard chrome already draws the visible heading, so rendering it again would duplicate it. - 9 template attributes kebab -> camelCase, by --fix. This is NC's v9 house style, verified with --print-config: `vue/attribute-hyphenation: [2,"never"]`. The only widening is in the tests block: `_`/`__` throwaway bindings. varsIgnorePattern is `^_+$` — underscores ONLY, so a real name that merely starts with `_` is still reported — while argsIgnorePattern is `^_`, which is exactly what NC's own TypeScript block uses.⚠️ `perfectionist/sort-imports` MOVED TWO SPDX HEADERS into the middle of an import block (it carries a leading comment with its import). Restored to the top of `src/store/modules/{object,settings}.js`. Worth checking on every app that runs --fix; the two other files whose SPDX sits below line 3 were verified as pre-existing and legitimate (inside a `<script>` block and a docblock). VERIFIED -------- npm run lint PASS (eslint src tests --max-warnings 0 — no warnings) npm run format PASS npm run stylelint PASS npm run check:specs PASS npm run build PASS 30 files linted, all 8 .vue parsed, 0 fatal positive control 7 planted violations in a .vue probe are caught
…er run `tests/l10n/check-l10n-parity.js` and its 36 locale files are on development, but the only workflow invoking them lives in .forgejo/workflows/. Codeberg was retired on 2026-08-04 and Forgejo Actions reported total_count: 0 tasks for these repos — so the gate has existed on disk and gated nothing, for its whole life. Ported to .github/workflows/ with the GitHub runner and actions. Verified the gate can fail before wiring it up: blanking one value in l10n/de.json makes the script exit 1 and name the key; restoring it exits 0. Currently green on development — 36 required locales, full parity.
ci(l10n): run the translation-parity gate on GitHub, where it has never run
build(lint): migrate to eslint 10 + @nextcloud/eslint-config 9
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-14 11:45 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-14 11:59 UTC
Download the full PDF report from the workflow artifacts.
The range read `>=10`, a leftover from resolving the install conflict iteratively while migrating. Harmless in itself, but this is the file every new app is scaffolded from and the other 17 repos all carry `^10.8.1` — an unbounded range in the template would quietly hand the next major to whoever scaffolds an app on the day it ships.
build(deps): pin eslint to ^10.8.1 in the template
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-14 16:32 UTC
Download the full PDF report from the workflow artifacts.
* ci: fast structural checks on every branch * ci: close the branch-trigger gap * ci: scope the JSON check — JSONC configs are not a defect * ci: scope the marker check to code — prose that documents a conflict is not one * ci: the JSON check reached a template's editor settings
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-14 17:58 UTC
Download the full PDF report from the workflow artifacts.
…ization
`x-openregister.publicRead` / `publicWrite` are not part of OpenRegister's
schema contract. Nothing reads them — zero consumers in openregister's lib, its
JS, its migrations, or as a property on the Schema entity. A schema marked
`"publicRead": false` was never protected by that line.
THIS TEMPLATE IS WHERE THEY COME FROM. It shipped them on its example schema
and shipped ZERO `authorization` blocks, so every app scaffolded from it
inherited the pattern that does nothing and never saw the one that works. They
have reached hermiq, petstore, spectr, openbuild and portaliq.
It is also a large part of why 504 of 571 fleet schemas declare no
`authorization` at all: the scaffold does not include one.
CHANGED
-------
* the example schema swaps `x-openregister: {publicRead, publicWrite}` for
`authorization: { read: ["authenticated"] }` — the safe answer, and one that
is a real decision rather than an inert key;
* README gains a short section on who can read a schema, showing the four
shapes actually in fleet use (authenticated, public, public-with-match, named
group) and stating plainly that publicRead/publicWrite are not a thing;
* `validate-register.js` counts `authorization` as a declaration in its
"too thin to be real" heuristic.
THE VALIDATOR CHANGE IS NOT COSMETIC. Before it, removing the bogus
`x-openregister` block MADE THE WARNING FIRE — the tooling actively rewarded
keeping the forbidden key. Declaring who may read a schema is at least as
strong a sign of a real schema as declaring a seed.
AND MY FIRST VERSION OF IT WAS BROKEN
-------------------------------------
`s.authorization && …` yields `undefined` when the key is absent, and
`undefined === false` is FALSE — so the strict comparison silently disabled the
whole heuristic. It passed the template and ALSO passed a genuine stub.
Caught by feeding it a real stub rather than trusting the green. Now wrapped in
Boolean(), and verified in both directions: the template passes clean, a schema
with 2 properties and nothing declared warns, and the template passes clean
again afterwards.
CI's Frontend Check (format) flagged tests/validate-register.js. Pure reflow — the multi-line if collapses to one line under @nextcloud/prettier-config. The heuristic is re-verified after the reformat, in both directions: the register passes clean, a genuine stub still warns.
…not-publicread fix(register): the template taught publicRead and never taught authorization
| name: l10n translation parity (European languages) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '24' | ||
|
|
||
| - name: Assert every required locale is at full parity | ||
| run: node tests/l10n/check-l10n-parity.js |
| runs-on: ubuntu-latest | ||
| # Observed fleet-wide: median 0.5 min, max 3.2 min (n=162). Deliberately loose. | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci --no-audit --no-fund | ||
|
|
||
| - name: Validate specs (json-strict + manifest + register) | ||
| run: npm run check:specs |
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ❌ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-15 19:45 UTC
Download the full PDF report from the workflow artifacts.
The app pinned the exact prerelease 2.2.0-vue3.16, which is now deprecated: the Vue 3 line was folded into the mainline 2.x release series and ships as 2.3.0 on the `latest` dist-tag. A caret range replaces the exact pin so future 2.x releases roll out without a per-app edit. Drops the `overrides.@conduction/nextcloud-vue.eslint` entry. That override existed only because the old prerelease declared `eslint: ^8.56.0 || ^9.0.0` and could not see the app's eslint 10; 2.3.0 declares `|| ^10.0.0` itself, so the peer resolves without help. No API change: 252 components in and 252 out, no export removed, one added (the BSN validators). Peer ranges are otherwise identical.
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 721/721 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-15 19:52 UTC
Download the full PDF report from the workflow artifacts.
* chore(security): enable the npm supply-chain cooldown on npm 11 Sets `min-release-age=2` and `min-release-age-exclude[]=@conduction/*`, raises `engines.npm` to ^11.0.0, and regenerates the lockfile under npm 11. The .npmrc comment here has described a cooldown for months and it has never been in effect. `min-release-age` does not exist in npm 10 — `npm config get min-release-age` answers `undefined` — and every Node 22 release bundles npm 10, so the setting was read by nothing. Most repos also had it at 0, which disables it outright. @conduction/* is exempt because without the exemption the cooldown does not fail loudly, it silently resolves backwards: measured 2026-08-15, an install of @conduction/nextcloud-vue on release day picked 2.0.7 instead of 2.3.0 and exited 0. The lock is regenerated under npm 11 and iterated to a fixed point. Where the tree changed rather than its metadata, that is npm 10 -> 11 reconciling a lock shaped by the older resolver, not the cooldown — verified by regenerating with the cooldown enabled and disabled and getting identical trees. Verified: npm ci exit 0 under npm 11.19.0, @conduction/nextcloud-vue resolves to 2.3.0, gate-84 conformance passes. * ci: re-run against the merged shared workflow `gh run rerun` replays the workflow version resolved when the run was created, so a reusable workflow referenced as @main is NOT re-resolved — every re-run after ConductionNL/.github#469 merged still executed Node 22 with npm 10.9.8, where `min-release-age` does not exist and `npm ci` cannot read an npm-11 lockfile. Only a new run picks up the merged workflow. This empty commit is that trigger. * ci: run every npm job on Node 24 The shared quality.yml moved to Node 24 (ConductionNL/.github#469), but this repo's OWN workflows did not, and they run `npm ci` too. Node 20 and 22 both bundle npm 10, which cannot install from an npm 11 lockfile — it exits EUSAGE with 'Missing: <pkg> from lock file'. Measured here: Lint Check and Spec Validation went red on exactly that while every shared-workflow job passed. pull-request-lint-check.yaml had no setup-node AT ALL, so it silently inherited the runner default. That is the harder half to notice: nothing in the file named a Node version, so nothing looked wrong. Left alone deliberately: api-test-coverage.yml stays on Node 20 where it exists — it runs `npm install -g newman`, a global tool install with no lockfile, so the npm major is irrelevant there. l10n.yml likewise: its only 'npm ci' is inside a comment saying it needs none.
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 720/720 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-16 01:51 UTC
Download the full PDF report from the workflow artifacts.
…154) Adopts the canonical script from ConductionNL/.github (quality-config/coverage-guard.php). The whole-project comparison fires on measurement noise. doriath#240 was a PR whose entire diff was `webpack.config.js` — no PHP at all — and the guard failed it: identical denominator (13723), both runs reporting exactly `Tests: 948, Assertions: 3051, Skipped: 1`, and six covered statements of run-to-run xdebug variance between them. The measured `--against` floor cancels driver variance (xdebug vs pcov), as its header says. It does not cancel run-to-run variance within one driver, and the ratchet has no tolerance. Scoping the comparison to the PHP a change actually touches keeps full strength where a regression matters and makes the noise unreachable by construction — a diff with no PHP cannot fail. New `changed-files` capability; the shared workflow PROBES for it rather than assuming, so an un-updated copy keeps the previous behaviour instead of silently accepting and ignoring the flag. Script only — no behaviour change until the workflow passes `--changed-files`. Byte-identical to the canonical copy (md5 5be122aad209da030c79b22a133232fb).
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 720/720 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-16 10:28 UTC
Download the full PDF report from the workflow artifacts.
* fix(quality): close nine of ten Hydra gates in the template `development` has been red since 2026-08-16, and because this repo is the SCAFFOLD every new app is created from, each of these violations was being copied into every app made from it on day one. Ten gates were failing. Nine are closed here; the tenth (gate-19 e2e-coverage) is advisory fleet-wide as of .github#477 — the run that reported it as blocking predates that change. ## Security / correctness - gate-5 + gate-30 MetricsController::index carried NO auth attribute at all, so its protection came only from Nextcloud's admin-required default — correct, but invisible to the router and to an audit. It now declares #[NoCSRFRequired] (a scraper cannot hold a CSRF token) and states its admin-only posture in prose, which is what gate-30 accepts and what openregister's own GenericMetricsController does. It is deliberately NOT #[PublicPage]: publishing instance metrics to anonymous callers to satisfy a gate would be a security regression, not a fix. - gate-5 SettingsController::create — a settings WRITE — likewise had no attribute. It now declares #[AuthorizedAdminSetting(AdminSettings::class)]. That attribute is typed `class-string<IDelegatedSettings>` and this template's AdminSettings implemented plain ISettings, so the honest attribute did not type-check and there was no correct way to satisfy gate-5 on a settings write. AdminSettings now implements IDelegatedSettings — as decidesk's already does — which fixes the cause rather than the symptom, for this repo and everything scaffolded from it. ## Accessibility - gate-32 the widget footer's "View all" was an <a> with @click.prevent and no href: unreachable by keyboard, announcing no role (WCAG 2.2 AA 2.1.1, 4.1.2). It is a <button>, which gets focus, Enter and Space for free. A four-property scoped reset keeps the library's appearance, so the change is behavioural and not visual. ## Architecture - gate-63 the manifest declared an in-app `type: "settings"` page at /settings while the app ALSO ships a Nextcloud admin section — two homes for one concern, and an ADR-079 D1 violation. The in-app page is removed; app configuration lives at /settings/admin/apptemplate. src/settings.js described itself as "DISTINCT from the manifest's type:settings page", which is exactly the duplication ADR-079 forbids; that comment now records the decision instead. ## Coverage - gate-25 the two preference endpoints are the only ones here carrying a real JSON contract, and they now have contract tests pinning status codes and response shapes — including the 401 and 400 paths, and the unset-reads-as-null translation a caller depends on. The two dashboard routes render the SPA shell and have no JSON contract to test, so they carry a reasoned @contract exclude pointing at the e2e suite instead. The new tests were checked against a positive control: an expectation was deliberately broken, the suite failed, and it passed again once restored. A test that has never failed is not known to work. - gate-26 FeaturesRoadmap has a real visual baseline rather than an exclusion. It is a presentational page whose actual failure modes — a dropped stylesheet, a collapsed grid, an overflowing card — leave the DOM intact and the console empty, so assertion-based tests pass while the page is visibly broken. Scoped to the app's content region, not the Nextcloud chrome, which changes between server versions. - gate-51 two example schema properties gained human-readable titles. ## Two e2e assertions that removing the page would have broken - app-shell.spec.ts asserted `pages.length === 5`; it is now 4, with the reason recorded so the next change to it is deliberate. - The same spec navigated to `settings` in its built-in-widget sweep. Left alone it would have kept "passing" against the catch-all's dashboard render — a dead route looking exactly like a working one. It now visits features-roadmap, a real page carrying the same widgets. - gate-58 the docs-capture spec waited on 'networkidle', which never settles on Nextcloud: it burned its full timeout and continued anyway. It now waits for the settings form it actually needs. Verified locally: psalm, phpstan, phpcs, cs:check, prettier, eslint, stylelint, webpack build, all five check:* validators, and 26 unit tests with 68 assertions. * fix(gitignore): stop substring globs from swallowing source files The previous commit's contract tests were NOT in it. `git add` dropped tests/unit/Controller/PreferencesControllerTest.php silently — no error, no mention in `git status` — because .gitignore carried **/*references* and "PReferencesController" contains "references". The same rule was already ignoring lib/Controller/PreferencesController.php itself. Six more substring globs sat beside it and are the identical trap waiting: **/*Analysis* **/*encoding* **/clearCache* **/update*Settings* **/rebase* **/setup* `**/update*Settings*` swallows an UpdateSettingsCommand, `**/clearCache*` a ClearCacheTest, `**/setup*` a setup.ts. All removed. What the block was FOR is kept: the patterns that remain all contain a SPACE, which is what makes them safe — they catch stray files accidentally named with a sentence ("PR something"), and no PHP class or spec file can contain a space, so they cannot reach source. This matters beyond one repo. This file is copied into every app scaffolded from this template, so each of them inherited a .gitignore that discards plausibly-named source files without saying so. A file that was never added looks exactly like a file that was. Verified: the test file is no longer ignored, it is committed, and the 26 unit tests still pass. * fix(quality): rate-limit the public health endpoint, add composer cooldown Three findings CI reported that the locally-vendored gates (v1.8.0) are too old to run. gate-82 — /api/health is #[PublicPage] by design so Prometheus and K8s probes can poll it without auth, which also means anyone on the network can poll it, and every call reaches through to isOpenRegisterAvailable(). Without a ceiling a health endpoint is a free amplifier pointed at the very thing it reports on. #[AnonRateLimit(limit: 60, period: 60)]: chosen to fit the real consumers rather than as a round number — a blackbox exporter and a liveness+readiness pair poll every 10-30s, so several probes sit comfortably inside it, and the limit is per remote address so they do not share a budget. gate-93 — .github/dependabot.yml had an npm entry and NO composer entry, so PHP dependencies updated with no cooldown at all. Added at 2 days with conduction/* excluded; the npm entry's single day predated the gate's 2-day floor and is raised to match. Coverage ratchet — the two IDelegatedSettings methods added in the previous commit were 2 new statements with no test, which dropped coverage of the touched files by 1.39%. The guard is right, and the fix is a test rather than filler: AdminSettingsTest pins both return values, because both are load-bearing. getName() returning null means "use the section's own name", and getAuthorizedAppConfig() returning an EMPTY map is what keeps #[AuthorizedAdminSetting] scoped to full admins — if it ever returns keys, that widens who may write settings, and this test is where that gets noticed rather than shipped. Verified locally: phpcs, psalm, phpstan clean; 30 unit tests, 73 assertions. * test(e2e): fix three failures my manifest change introduced Playwright reported 4 failed / 7 passed. All three causes are mine. 1. STRICT-MODE VIOLATION on `locator('main')`. The FeaturesRoadmap page renders its own <main> inside Nextcloud's, so the locator matched TWO elements and Playwright failed the test outright. It only surfaced when the dead `settings` route was replaced with a real page — the old route fell through to the catch-all, which renders a single-<main> dashboard. Scoped to `#content`, the element the app actually mounts into. 2. A ROUTE ASSERTION THAT WOULD HAVE LIED. 'every manifest page renders its own content' still listed `settings` and expected /Application information/. With that page removed the route falls through to the catch-all, so the spec would have been asserting against the dashboard while claiming to cover a settings page. Now visits features-roadmap and expects its own title. 3. A GUESSED SELECTOR. My docs-capture wait used '#apptemplate-admin-settings, .apptemplate-admin, form' — none of which exist — so it timed out after 30s having waited for nothing. The real mount point is #apptemplate-settings (templates/settings/admin.php). The visual baseline is scoped to #content for the same reason, which is also the right region: the Nextcloud chrome around it changes between server versions and is not this app's to pin. * test(e2e): commit the FeaturesRoadmap visual baseline The visual spec had no baseline, so Playwright wrote the actual and failed the run — the documented first-run behaviour, and the reason the E2E job was the last red cell. THE BASELINE IS THE ONE CI RENDERED, not one produced here. Font rendering and sub-pixel antialiasing differ between this WSL box and the CI container, so a locally-generated PNG would have failed on the very next run for reasons that have nothing to do with the page. It is lifted from the playwright-traces artifact of the failing run (FeaturesRoadmap-actual.png, chromium/linux). I LOOKED AT IT BEFORE COMMITTING IT. A screenshot baseline locks in whatever it depicts, so accepting one unseen would pin a broken page as correct forever. It shows the navigation, the "Features" header with both actions, the documentation callout, the "No features documented yet" empty state and the full roadmap sidebar — a page working as intended, with the empty state being genuine (this template ships no implemented capabilities to list). One thing checked rather than assumed: the nav still shows a "Settings" entry after the in-app settings page was removed. It is not a dead link — src/manifest.json's `menu` never contained a Settings item, so that entry comes from the shared app shell and points at the Nextcloud admin section, which is exactly where ADR-079 puts app configuration.
Quality Report — ConductionNL/nextcloud-app-template @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 103/103 | |||
| npm | ✅ | ✅ 720/720 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-18 23:39 UTC
Download the full PDF report from the workflow artifacts.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.