Skip to content

Release: merge development into beta - #18

Open
github-actions[bot] wants to merge 186 commits into
betafrom
development
Open

Release: merge development into beta#18
github-actions[bot] wants to merge 186 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

WilcoLouwerse and others added 7 commits March 24, 2026 15:02
Release: promote beta to stable — initial CI pipeline and app scaffold
…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>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 321c395

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.

WilcoLouwerse and others added 22 commits April 16, 2026 18:31
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.
…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>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 5833744

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
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 4df288e

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.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ ea09749

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.

rubenvdlinde and others added 4 commits August 14, 2026 11:10
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
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 6e4d854

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.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 8360226

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.

rubenvdlinde and others added 2 commits August 14, 2026 18:19
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
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ c7d92f1

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
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 461d6f2

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.

Conduction Release Bot and others added 3 commits August 15, 2026 19:51
…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
Comment on lines +23 to +36
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
Comment on lines +25 to +42
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
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ a1f14fd

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.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 0417c3a

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.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 97ad940

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).
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ c7c45c0

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.
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/nextcloud-app-template @ 2d3df05

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants