Release: merge beta into main - #432
Merged
Merged
Conversation
…e3.1 Drops the USE_LOCAL_LIB dependency on a library source checkout. 2.0.7 could not be used: the barrel re-exported each component's default straight from its .vue2.js script module, so webpack redirected past the wrapper that runs `script.render = render`, dropped the barrel, and app-root-level components mounted with no render function — a blank page with zero console errors. 2.1.0-vue3.1 anchors that export module-locally so the wiring is reached by construction. Verified by building against the PUBLISHED tarball with the local-source symlink moved aside, deploying, and loading the app authenticated.
Vue 2 required the key on each child of a `<template v-for>`; Vue 3 keys the whole fragment, so a key on a child is IGNORED and the list renders unkeyed — losing the identity Vue needs to patch rows correctly on reorder. CalendarWidget's agenda groups and FilesWidget's path breadcrumbs both used the Vue-2 form.
78 of 570 unit tests failed for six distinct Vue-2 -> Vue-3 mechanisms,
none of which surface as a compile error:
* `render(h)` — Vue 3 no longer passes `h` to the render function; it is
imported from `vue`, and the vnode data object is flat rather than
Vue 2's nested `{ attrs: {…} }`. Fixed in the shared
`@conduction/nextcloud-vue` stub and the DashboardConfigModal spec.
* `Vue.use(PiniaVuePlugin)` — there is no global `Vue` to install onto,
and a Pinia instance IS an app plugin under Vue 3. The v1 top-level
`pinia` mount option is now `global.plugins: [pinia]`; handled in the
existing vueTestUtilsCompat adapter alongside the other hoisted keys.
* Listener fallthrough — Vue 3 merges listeners into `$attrs` as `onClick`
and auto-applies them to a stub's root element. A stub that also does
`@click="$emit('click')"` therefore fires the parent handler TWICE.
The wizard advanced two steps per click and the shell hamburger toggled
itself shut. Declaring `emits: ['click']` removes `onClick` from
`$attrs`, restoring a single path. Applied to every affected stub.
* `v-model` contract — the prop is `modelValue` and the event
`update:modelValue` (was `value`/`input`, and `checked`/`update:checked`
for NcCheckboxRadioSwitch). Stubs on the old contract never wrote back,
so forms silently stayed empty.
* VTU v2 API — `wrapper.destroy()` is `unmount()`, `findAll()` returns a
plain array rather than a WrapperArray with `.wrappers`, and v1's
`scopedSlots` is now `slots` (silently ignored, so every slot in
BeheerTabs rendered empty).
* `reactive()` identity — `state.selectedWidget` hands back a Proxy, so a
bare `toBe` compared proxy to raw. `toRaw` keeps it an identity check.
Also raises `hookTimeout` to match `testTimeout`: several admin specs
resolve their component with `await import()` inside `beforeEach`, and the
first call pays Vite's full SFC transform cost, overrunning the 10s
default with "Hook timed out" and no assertion failure.
`@nextcloud/eslint-config@8` resolves eslint-plugin-vue's **Vue 2** preset
(visible via `eslint --print-config`: `vue/no-reserved-props` arrives as
`{ vueVersion: 2 }`). Four of the 17 errors were that preset forbidding
syntax Vue 3 *requires*:
- `vue/no-v-for-template-key` — Vue 2 banned a key on `<template v-for>`;
Vue 3 keys the fragment there. Replaced with the Vue-3 counterpart
`vue/no-v-for-template-key-on-child`.
- `vue/no-v-model-argument` — `v-model:arg` is Vue 3's replacement for
Vue 2's `.sync`.
`no-multiple-template-root` and `valid-v-bind-sync` are off for the same
reason. Switching wholesale to `@nextcloud/eslint-config/vue3` is not an
option here: it references `@typescript-eslint/*` rules whose plugin this
project does not register, and ESLint crashes on load.
The `vue/no-deprecated-*` family is enabled as errors, because these are
Vue-2 idioms the Vue 3 compiler ignores rather than rejects — the failure
mode is a dead listener at runtime. It immediately found one:
`DashboardRowActions` bound `@click.native.stop`, and `.native` no longer
exists in Vue 3. The component's own docblock already described the
binding as `@click.stop`, so the modifier was stale as well as inert.
`@spec openspec/...` is registered via `definedTags`. It is this repo's
ADR-003 / ADR-020 traceability tag with its own enforcing gate
(`composer lint:spec-annotations`), so the linter needed to learn the tag
rather than the convention being bent — that alone was 746 of the 1242
warnings.
Remaining errors were genuine style violations in three files, autofixed.
…ng focus
`WidgetMovePanel.vue` — the WCAG 2.1 SC 2.1.1 keyboard equivalent of
GridStack's pointer-only drag — was fully implemented, unit-tested, and
never mounted. Its only references in `src/` were its own definition and a
doc comment in `WidgetContextMenu.vue` describing wiring that was never
done: passing tests against a component the app never renders.
The pieces that existed: `WidgetContextMenu` already declared and emitted
`move`, and `nudgePlacement()` in `useGridManager.js` already implemented
the geometry as a pure function returning the clamped rect plus push-down
side effects. Only the seam between them was missing:
- `useGridManager` gains an `onMove` option and `triggerMove()`,
mirroring the existing edit/remove/visibility-rules triggers.
- `Views.vue` binds `@move`, mounts `<WidgetMovePanel>`, and persists a
confirmed rect through `updatePlacements()` — folding the moved
placement and any pushed placements into ONE call so the layout is
written atomically on the same debounced path drag already uses.
Separately, `placementItemKey()` interpolated `updatedAt` and a JSON dump
of `styleConfig` into the per-item render key. Every persist therefore
changed the key, Vue tore down and recreated the grid item's DOM node, and
whatever was focused inside it lost focus. That defeats keyboard
repositioning even once wired: after a single arrow-key move focus fell
back to the document and a second move needed a full re-navigation of the
grid. The key is now the placement id alone — a content change re-renders
through prop updates without needing a new key.
Two distinct causes behind the jsdoc warnings.
The larger one was structural: ~60 methods carried a COMPLETE docblock
immediately followed by a second, `@spec`-only block. Only the block
adjacent to the function is read, so the real `@param`/`@return` docs
were invisible to both the linter and any reader's tooling. Merging the
`@spec` tag into the block above it recovers the existing documentation
rather than rewriting it.
The rest were genuinely undocumented parameters, concentrated in the HTTP
wrapper layer (`services/api.js`) and the admin group-priority editor.
Written out properly — no `@param {*}` filler.
Completes eslint to 0 errors / 0 warnings across `src/`.
The last three warnings were `vue/no-v-html`. Both sites are legitimate and
stay, with a scoped disable and the reason recorded:
- DashboardFooter renders admin-authored HTML that only reaches the
client through `FooterService::sanitiseHtml()`.
- NewsWidget renders third-party RSS, sanitised server-side by
`NewsWidgetService::sanitiseSummaryHtml()` and again through DOMPurify
in `formattedSummary()` after truncation.
A `disable`/`enable` pair is used rather than `disable-next-line` because
the rule reports on the `v-html` attribute's own line, which is several
lines below the element's opening tag in both files.
`npm run lint` is a four-step chain, and its third step was failing on a
pre-existing REQ-INIT-003 violation in `src/public.js`. That call was worse
than a style breach — it was dead. It read a `public-share-token`
initial-state key that no PHP ever provides (`PageController::publicShare`
renders the template with no `provideInitialState` call), so it always
returned its fallback and the URL path parse below it did the real work,
exactly as `templates/public.php` documents. The comment above it asserted
the opposite. Removed the dead read and documented the actual source; the
page is anonymous, so adding the key to the initial-state contract would
have meant a server change with no consumer.
NewsWidget renders feed summaries through `v-html`, so how they are shortened is a security property rather than a formatting detail — and the file had no unit spec at all. Covers what the previous raw-offset slice got wrong: the budget counts visible characters rather than markup bytes, a cut that lands inside a tag or attribute can no longer emit a truncated anchor, and the `rel="noopener noreferrer"` REQ-NEWS-005 forces survives truncation. Also pins the client-side re-sanitise, so script/img/onerror payloads are stripped even if the server-side pass were bypassed.
Carries the CnDashboardGrid keyboard a11y this app's WCAG 2.1 SC 2.1.1 e2e coverage depends on, plus three crash fixes — notably CnLockedBanner, whose `message` prop used a `default()` factory reading `this.lockedBy`. Vue 3 invokes default factories with no `this`, so it threw on mount and white-screened any page rendered while another session held a lock. Pinned explicitly rather than via `npm install`: the lockfile had vue3.2 resolved, and the caret range does not force a prerelease bump on its own.
The stub's docblock explained why it exists — the published CJS bundle `require()`s `.vue` files Vite cannot transform — but not what accepting it gives up. The alias in vitest.config.js redirects EVERY import of `@conduction/nextcloud-vue` to the stub, so not one of this suite's 624 unit tests runs against the real library. A green unit run is evidence that the stub works, not that the library does; the suite structurally cannot catch a breaking prop change, a renamed event, or a component that renders nothing. The last case is not hypothetical. Earlier in this migration the library shipped a dist whose render wiring had been tree-shaken away and every `Cn` component rendered as a silent comment node — blank app, zero console errors — while the unit suite stayed fully green, because these stubs render fine. Only the browser caught it. Recorded at both places a reader lands: the stub docblock and the alias. Library integration is the Playwright suite's job, and nothing else's.
…k fixture
- src/composables/useGridManager.js: DEFAULT_MENU_HEIGHT/WIDTH were sized
for the original 3-button context menu (132px/150px). The popover has
since grown to 5 items (Edit, Move, Visibility rules…, Remove, Cancel)
and measures ~232.5x156.5px in practice, so the viewport-edge clamp
under-estimated its footprint and let the real popover render past the
clamped edge (or push ctx-remove out of the fixed-position viewport
entirely). Bumped both constants above the measured size with margin.
- tools/deploy-to-launchpad.sh: two pre-existing bugs hit while
redeploying to verify the above.
1. The JS-bundle rename loop computes `new` from `f` via a sed that is
now a no-op (source/target app ids are both "launchpad"), so
`sed ... "$f" > "$new"` opened-and-truncated its own input before
reading it, and the trailing `mv "$f.LICENSE.txt" "$new.LICENSE.txt"`
failed outright ("are the same file"). Now stages sed's output in a
temp file and only renames the LICENSE sidecar when the path
actually differs.
2. `custom_apps/launchpad` is itself a bind mount on this dev box;
`rm -rf "$DEST"` deletes every file underneath but then fails on the
mount point itself ("Device or resource busy") — AFTER already
wiping the directory's contents, leaving the shared dev instance's
launchpad app empty until the next successful deploy. Now clears
$DEST's contents (`find "$DEST" -mindepth 1 -delete`) instead of the
directory itself.
- tests/e2e/fixtures/acknowledgements.ts (new) +
tests/e2e/dashboard-acknowledgements.spec.ts: REQ-ACK-001 ships no
admin-facing form UI for declaring an acknowledgement requirement in
this pass (config path is @e2e excluded) — nothing in the app seeds a
compulsory widget with an outstanding acknowledgement for the e2e specs
to observe. Added ensureOutstandingAcknowledgement(), which seeds one
through the real, authorised PUT /api/widgets/{id} contract (same
pattern as fixtures/role-feature-permissions.ts) and wired it into a
beforeAll so REQ-ACK-002 (forced-delivery gate) and REQ-ACK-004
(read-receipt report) have real state to exercise. Idempotent: reuses
the marked placement and bumps acknowledgementContentVersion each run
so a prior run's receipt never leaves it satisfied.
- package.json / package-lock.json: consume
@conduction/nextcloud-vue@2.1.0-vue3.4 (already in progress before this
session).
…l safety
- src/main.js: nc-vue's CnNcWidgetWidget self-registers `nc-widget` with
`form: null` ("CnNcWidgetWidgetForm is not yet present in this tree" —
stale; CnNcDashboardWidgetForm + CnNcWidgetGridPicker DO exist in this
nc-vue version, just never wired to the registration). listWidgetTypes()
filters null-form entries, so Add Widget's type picker silently never
offered "Nextcloud widget". Complete the registration from launchpad
using nc-vue's own public last-registration-wins registry API — the
same pattern nc-vue itself uses to re-register table/object-list/map.
- src/views/Views.vue: CnNcDashboardWidgetForm injects a `widgets` catalog
for its grid picker; nothing provided it, so it always saw the inject
default ([]) and rendered empty regardless of the form fix above. The
widget store already fetches this exact Nextcloud-native dashboard-widget
list (loadAvailableWidgets — its own comment notes it feeds
CnNcWidgetWidget's runtime renderer). provide('widgets', ...) it down
from Views.vue's setup() so the picker has real data.
- src/styles/workspace.css: CnLabelWidget renders a flex container with a
<span> text child; flex items default to min-width:auto, which floors
the span's size at its longest unbreakable word regardless of the
component's own overflow-wrap:break-word, so a very long single word
overflows the widget cell. Patched with an additive (non-conflicting)
min-width:0 rule, the same flex-child pattern already used for
.launchpad-workspace above it in this file.
- tests/e2e/widget-context-menu.spec.ts: right-edge and bottom-edge popover
tests picked ".grid-stack-item.first()" and clicked via raw
page.mouse.click(x, y) at its boundingBox() coordinates. GridStack
positions items via gs-x/gs-y, not DOM order, so on this long-lived,
heavily-populated shared dashboard the "first" item can sit well below
the fold; toBeVisible() doesn't catch that (it's a CSS-visibility check,
not a viewport-scroll check), so the click silently landed off-screen
and the popover never opened. scrollIntoViewIfNeeded() before reading
the box fixes it without touching any assertion.
tools/deploy-to-launchpad.sh resolved its target container with `docker ps -qf name=nextcloud` — a SUBSTRING match. On a box that also runs a shared `nextcloud` dev container (used by other apps/worktrees), that substring silently matched the WRONG container: one that bind-mounts `custom_apps/launchpad` straight onto a real host checkout. Every "successful" deploy this session wrote through that mount onto disk instead of reaching `lp-vue3-e2e` (the container the e2e suite actually points at via NC_BASE_URL=http://localhost:8098) at all — the suite kept testing a stale bundle while every app-source fix silently landed on a real checkout instead of a disposable instance. Fixes: - Resolve the target via `LAUNCHPAD_DEPLOY_CONTAINER` (default `lp-vue3-e2e`), verified by exact name/id via `docker inspect`, never a substring filter. - Refuse to deploy — loudly, before touching anything — if the resolved container's app directory (or any ancestor of it) is a BIND mount. A bind mount means the container is wired onto a real host path; a volume or a plain in-container directory is fine. This is the general guard: even with the container-name bug fixed, nothing should silently write through to someone's checkout again. Verified both directions against the live containers on this box: LAUNCHPAD_DEPLOY_CONTAINER=nextcloud is now refused (bind mount detected at custom_apps/launchpad), and the lp-vue3-e2e default proceeds (volume only, no bind mount in its ancestry).
Picks up nc-vue PR #558 (fix(CnIndexPage): scroll the table, not the page column, in table view), merged into feat/vue-3 this morning. Folded into the same re-baseline cycle as the deploy-target fix so there is one clean measurement against the library version the app will actually ship with, rather than two rounds.
Picks up the CnWalkthrough persistence fix: completionConfigKey was schema-declared, documented, and spec-mandated, but had neither a read nor a write path wired up (grep only hit the schema file) — so dismissal was localStorage-only and the tour reopened in every fresh browser context. Full e2e re-run at this pin: 73 passed / 1 failed / 4 skipped.
…vue3.7 - tests/e2e/widget-context-menu.spec.ts: the right/bottom-edge popover tests read boundingBox() immediately after page.setViewportSize(), but that resize triggers a GridStack column reflow and every .grid-stack-item animates to its new position over GridStack's own 300ms CSS transition (.grid-stack-animate, gridstack.css). A box read mid-transition describes an in-flight frame, not the rest position — harmless for a click well inside a widget, but these tests deliberately click within 20-30px of the edge, so the animation delta was enough to land the click in the grid gutter and never open the popover. Measured 2-in-3 failure rate before; added waitForStableBox() (polls until two consecutive reads match, no fixed sleep) and verified 3/3 clean runs after. - tests/e2e/fixtures/secondary-user.ts (new): throwaway-user provisioning (OCS API), known-password reset for a pre-seeded account, and a loginAs() that authenticates a SECOND, genuinely separate session. storageState: undefined is load-bearing: browser.newContext() otherwise inherits playwright.config's top-level use.storageState (the shared admin session), so every login attempt was silently already authenticated as admin and never hit the login form at all. - tests/e2e/runtime-shell-canEdit.spec.ts: enabled the two empty-state scenarios (provision a real zero-dashboard account, toggle allowUserDash, assert). Also fixed setAllowUserDashboards: it was issuing its PUT through the cookie-authenticated built-in `request` fixture, which Nextcloud's CSRF check rejects outright on a state-changing route (measured: 412) — the failure was swallowed by a console.warn instead of failing the test, so the "false" scenario ran against whatever the flag happened to still be. Rewritten to use its own Basic-Auth + OCS-APIRequest admin context (matching allow-personal-dashboards-flag.spec.ts) and to throw on a non-OK response instead of warning. - tests/e2e/active-dashboard-resolution.spec.ts: enabled the fresh-user empty-state scenario using the same throwaway-user fixture. - tests/e2e/dashboard-sharing.spec.ts: investigated and restored the recipient-visibility skip with an accurate reason (two independently confirmed causes, neither a stale selector): DashboardResolver (lib/Service/DashboardResolver.php) only resolves a user's active dashboard from owned/group/template rows and never considers dashboards merely shared to them, so a recipient with no dashboard of their own lands on the empty state regardless of what's shared; and the ADR-023 action-authorization matrix defaults every action to admin-only, with nothing on this instance ever broadening it — confirmed live via OCSForbiddenException on the account's first non-admin AJAX call. Both are real product/environment considerations for a follow-up change, not something one spec's fixture should paper over. - package.json / package-lock.json: consume @conduction/nextcloud-vue@2.1.0-vue3.7 (nested-modal stacking fix). Checked launchpad's modal usage — every NcModal/NcDialog/Cn*Modal is a sibling in Views.vue's template, none nested inside another, so this is expected to be a no-op here; full e2e run confirms no regression.
feat(vue3): migrate Launchpad to Vue 3 and @conduction/nextcloud-vue 2.1.0-vue3.7
…admin action baseline
Defect 1 — a share was inert. DashboardMapper::findVisibleToUser() unions
exactly three buckets (owned rows, group_shared rows in the user's groups,
the 'default' sentinel); share rows were in none of them. A recipient with
no dashboard of their own landed on the empty state and the shared
dashboard was unreachable everywhere: not in the switcher, never a
resolution candidate, not fetchable by id.
DashboardResolver now owns the share lookup (findSharedLevels,
findSharedDashboards, tryGetSharedDashboard) and DashboardService folds it
into getVisibleToUser(), resolveActiveDashboard() (new last-resort step 6b)
and getEffectiveDashboard() (before the writing template step). Precedence:
a share is the LAST candidate, so it can never displace anything the user
owns, reaches through a group, or explicitly selected. Shared results carry
the SHARE's permission level, not the owning row's.
Defect 2 — the app was unusable by non-admins on a fresh install. The
ADR-023 seed mapped every declared action to ["admin"], so a non-admin hit
'Action dashboard.list requires admin rights' on the first AJAX call —
including the "Create your first dashboard" CTA the empty state shows to
exactly those users. Added a GROUP_ALL_USERS ('@ALL') sentinel, granted it
to the ordinary end-user surface in the seed, kept every administrative
action admin-only, and added a version-gated ApplyActionBaseline repair
step so already-installed instances get the baseline too without
overwriting admin customisation.
Tests: 28 new PHPUnit tests, all verified to fail against the unfixed code
(Defect 2 fails behaviourally with the real OCSForbiddenException).
…nd the @ALL matrix column Both suites verified to FAIL against origin/development's components: - DashboardSwitcherSidebar: 4 of 5 new tests fail (shared rows leak into the primary-group section, no data-section="shared", switch emits no shared discriminator, section order is [group, default, user]). - ActionAuthMatrix: 2 of 6 fail (displayGroups is ['admin','editors'] — the @ALL column the shipped baseline needs is never rendered, so an admin can neither see nor revoke the non-admin grant).
…ipient e2e The server-side fix alone was not enough end-to-end. Every source-aware getter in the dashboard store filters by an EXPLICIT source value, and Views.vue's sidebarGroupDashboards only concatenated the group and default buckets — so 'shared' rows survived the server-rendered initial state but vanished the moment the store refreshed from /api/dashboards/visible. Added a sharedWithMeDashboards getter, wired it into the sidebar input and into activeDashboardSource so the row cog gates owner-only entries correctly. e2e: un-skipped 'recipient sees the shared dashboard in their switcher' (both stated blockers are now fixed) and added two ADR-023 scenarios in runtime-shell-canEdit.spec.ts — an ordinary user may call the end-user API surface but not instance analytics, and the empty-state Create CTA the app itself offers actually returns 200 and persists. Store spec verified failing against origin/development's store (rc=1); 30/30 pass with the getter.
…ally runs A repair step only executes when NC sees a version increase. The e2e instance was confirmed live to still hold the all-admin matrix with `actions_baseline_version` unset while installed_version already equalled info.xml — so without this bump `occ upgrade` is a no-op, the baseline never reaches an existing install, and the fix would have looked shipped while changing nothing.
toHaveCount(0) on .workspace-shell__empty passes trivially on a blank page, before Vue mounts. Wait for the app to render EITHER the shell or the empty state first, then assert it is not the empty state.
e2e caught what every unit test here missed. DashboardFactory sets
STATUS_DRAFT on every dashboard it creates (all 15 rows on the e2e
instance are drafts), and filterByPublicationState() hides drafts from
non-owners — so the previous commit appended the share and then filtered
it straight back out. Live: GET /api/dashboards/visible returned
{items:[]} for the recipient while GET /api/dashboard returned the
dashboard, because tryGetSharedDashboard() never went through the filter.
Two paths, opposite answers, and the switcher showed nothing.
Shares are now appended AFTER the publication filter and are exempt from
the draft/scheduled hide: a share row is the owner's explicit, named
grant to that specific user — the same class of entitlement that already
exempts the owner and admins, and what PermissionService::resolveAccessLevel()
already honours. A user WITHOUT a share still cannot see a draft
(testUnsharedDraftStaysHiddenFromNonOwners). Due scheduled rows still
materialise in memory via the extracted materialiseDueSchedule().
Root cause of the miss: the unit fixtures called setPublicationStatus(
STATUS_PUBLISHED) explicitly, so they were unrepresentative of what the
factory produces. The new test builds through DashboardFactory and
asserts the factory default, so a change there fails a unit test rather
than a browser.
Also fixes a made-up URL in the new ADR-023 e2e test: it hit
/api/analytics/instance-summary (404) instead of the real
/api/admin/analytics/summary — a 404 proves nothing about authorization.
…est non-admins
There is NO authorization hole. The reported 200 from the admin-only
instance-analytics endpoint was my own test authenticating as admin.
pwRequest.newContext() inherits playwright.config's top-level
use.storageState — the shared ADMIN session cookie from global-setup — so
Nextcloud authenticated the request by cookie and never looked at
httpCredentials at all. This is the exact trap loginAs() already documents
for browser.newContext() in this same file. Measured three ways against
the deployed code: a freshly provisioned zero-group user gets 403 on
/api/admin/analytics/{summary,dashboards/top,export}; unauthenticated gets
401; only an admin gets 200.
Consequences and fixes:
- storageState: undefined, so the context is genuinely the throwaway user.
- httpCredentials send:'always' — NC replies with a bare 401 carrying no
WWW-Authenticate header, so Playwright's default challenge-response mode
has nothing to respond to and would never send the credentials.
- the end-user-surface assertions were not.toBe(403), which also passes on
401 and 404; they now assert toBe(200) so 'never authenticated' and
'wrong URL' can no longer read as success. Both false greens really
happened while writing this spec.
- the Create CTA expects 200 or 201; 201 Created is correct for that POST.
401/403/404/5xx still fail.
loginAs(): click() also waits for the navigation it schedules, bounded by
actionTimeout (10s), NOT navigationTimeout (60s). The NC login POST +
redirect + hydration exceeds 10s on a loaded box, so the click failed while
the login had SUCCEEDED — the saved snapshot at failure shows a fully
authenticated page (#app-dashboard, Applications nav, Settings menu), not
the login form. Handed the wait to the explicit waitForSelector('#header')
that already followed it with its own 45s budget. Not a timeout increase:
if the login really fails, #header never appears and it still fails.
…bac-baseline fix(sharing,rbac): make a shared dashboard actually reachable, and a fresh install usable by non-admins
Fix Conduction → ConductionNL org reference (all workflows failed with "workflow was not found") and rename the branch-protection caller job `check` → `branch-protection` to satisfy the org ruleset's required check.
… for two months (#21) * ci: point reusable-workflow calls at ConductionNL, not the non-existent Conduction org All 8 reusable-workflow calls referenced `Conduction/.github`. That GitHub org does not exist (`GET /orgs/Conduction` is a 404) — the 2026-06-01 rename went the wrong way. Actions cannot resolve the ref, so every run produced ZERO jobs and failed instantly. No ESLint, PHPCS, PHPMD, PHPStan, Psalm, licence scan, security scan, SBOM, PHPUnit or Newman has run on this repo since. The tell: in `gh run list --json name`, an unresolved run's `name` is the raw path (`.github/workflows/code-quality.yml`) rather than the workflow's declared `name:` (`Code Quality`). Two workflows additionally pinned `@feature/openspec-project-sync`, a branch that does not exist on ConductionNL/.github either — those would still not have resolved after an org-only fix. Both now use `@main`. An earlier branch (`feature/workflow-update`, commit c6b8424) claimed to fix this but only touched `branch-protection.yml`; `code-quality.yml` was left broken and its run still produced 0 jobs. `.forgejo/workflows/` is deliberately untouched — on Codeberg the org really is `Conduction`, so those refs are correct. * fix(deps): resync package-lock.json so `npm ci` resolves again Every npm-based quality job (ESLint, Stylelint, License (npm), Security (npm)) died before running a single check: npm error `npm ci` can only install packages when your package.json and npm error package-lock.json ... are in sync. npm error Missing: pinia@4.0.2 from lock file npm error Missing: vite@8.2.0 from lock file ... + the whole esbuild/rolldown/lightningcss platform-binary fan-out Cause: `@nextcloud/vue` carries a nested `vue-router@5.2.0` whose `peerDependenciesMeta`-optional peers are `pinia: ^3.0.4 || ^4.0.2` and `vite: ^7.3.0 || ^8.0.0`. Those versions have since been published, so npm's ideal tree now includes them while the committed lockfile predates them. The lockfile was simply never regenerated — nobody noticed, because the workflow that would have caught it produced zero jobs. Regenerated with the npm major this repo pins (`engines.npm: ^10.0.0`) on Node 20. npm@11 must NOT be used here: it reports this lockfile as "up to date" and produces one that npm@10 `ci` then rejects. The diff is additive and does not touch the app's own dependency graph: - 0 packages removed - 0 nested version changes - 1 version change: @napi-rs/wasm-runtime 1.1.6 -> 1.2.2 (a wasm shim) - ~60 additions, all platform binaries pulled in by the optional peers above Vue, pinia (top-level 2.3.1), @nextcloud/vue and @conduction/nextcloud-vue are all unchanged, so the produced bundle — and the e2e runs verified against it — are unaffected. Fixed by resyncing rather than by `--legacy-peer-deps` or an `overrides` block: suppressing the resolution would leave the lockfile lying about the tree that `npm ci` actually builds. * fix(quality): clear everything the revived Code Quality gates surfaced With the workflow refs and lockfile fixed, all 23 jobs actually run. This clears what they found. ESLint and Stylelint needed nothing once `npm ci` worked — they were failing at install, not on findings. PHPCS: 277 errors -> 0 Applied the repo's own `phpcbf` for the 237 mechanical ones (docblock alignment, parameter spacing, operator spacing), then hand-fixed the rest: 22 ternaries rewritten as statements (`Squiz.PHP.DisallowInlineIf`), 5 uncapitalised inline comments, 3 doc short-descriptions, 3 over-length lines, 2 missing `@param` tags on IframeService::__construct(), and a missing short description. On DisallowInlineIf specifically: launchpad's phpcs.xml is byte-identical to openbuild's apart from the description line, and openbuild has zero violations of this sniff. It is the fleet standard and it is met elsewhere, so the ternaries were rewritten rather than the rule relaxed. PHPStan: 2 errors -> 0 (both real) - PublicShareService::renderShareContent() is documented `@return array{share, dashboard}` but actually returns a third key, `placements`, which PublicShareController::show() reads. The declared contract omitted a key the only caller depends on. Corrected the shape and imported WidgetPlacement. - LiveTileService::extractValue() looked unreachable to PHPStan because it cannot model PHP filling an unmatched alternation group with `''`. The branch was genuinely reachable, but discriminating on the group that is actually absent (`$token[2]`) rather than the one that is empty is both clearer and analysable. No suppression. PHPMD: 37 violations -> 5 Fixed outright: 2 UndefinedVariable (PHPMD cannot see `preg_match`'s by-ref `matches:` out-param — declared it explicitly), 3 ElseExpression, 2 LongVariable, 1 CountInLoopExpression, and 4 complexity offenders decomposed into named helpers (readBaseline, checkFramable, extractValue, diffCopy, updateSettings). Suppressed per-site WITH a written reason, using this codebase's existing `@SuppressWarnings` convention — not a blanket ignore, and each still fails if the justification stops holding: - 6 UnusedFormalParameter: `changeSchema()` / `preSchemaChange()` override SimpleMigrationStep, whose signature Nextcloud fixes. The parameters cannot be removed. - 8 StaticAccess: every call is to a genuinely stateless `public static` helper (ResponseHelper, AnalyticsService::periodToDateRange, UniqueViewerDedup::utcDateFor, TileClicksTableBuilder::create). Injecting their owners to reach them would add collaborators used for nothing else and push two of these classes further over CouplingBetweenObjects. NOT fixed — stated plainly rather than hidden 5 PHPMD violations remain and `PHP Quality (phpmd)` stays red: 3x CouplingBetweenObjects (TileAnalyticsController, LiveTileService, WeatherService — all at exactly 13, threshold 13) and 2x ExcessiveClassComplexity (AdminSettingsService 58, HealthPingService 61). These need real architectural change — splitting services and reducing constructor dependencies — which should not ride along in a CI-restoration PR. I did NOT add a phpmd baseline or relax the thresholds to bury them. * fix(deps): pin @conduction/nextcloud-vue 2.1.0-vue3.13 to get off proprietary vue3-apexcharts The revived licence gate caught a real exposure: `vue3-apexcharts` was MIT up to and including 1.8.0, and at 1.9.0 (2025-10-13) moved to a proprietary dual-license — free only under $2M USD annual revenue, forbidding "sublicensing under different terms" (which is what redistributing inside an EUPL-1.2 app is) and requiring a paid OEM licence for "No-code dashboards", "Embedded BI tools" and "White-labeled apps or SDKs". Not the same as the `apexcharts` entry in .license-overrides.json: that one is a genuine license-checker misread of an MIT package, and the core apexcharts@4.7.0 is still MIT. Only the Vue 3 wrapper changed licence. The identical `Custom: <image-url>` symptom made them look alike. The dependency is transitive through @conduction/nextcloud-vue, and the fix already existed upstream: 2.1.0-vue3.7 -> vue3-apexcharts ~1.10.0 (proprietary) 2.1.0-vue3.13 -> vue3-apexcharts ~1.8.0 (MIT) This repo declared `^2.1.0-vue3.7`. A caret does not move an already-resolved prerelease in a lockfile, so the pin held us on the proprietary line even though a fixed release had shipped. Now pinned EXACTLY, without a caret — a caret floats prereleases, and that is how this drifted in the first place. Verified from the lockfile rather than the manifest: - node_modules/vue3-apexcharts: 1.10.0 -> 1.8.0 - exactly one copy in the tree; zero 1.9.0+ entries at any nesting depth - installed package.json now declares `"license": "MIT"` (was `"see LICENSE in LICENSE"`), which is what license-checker reads No override was added. .license-overrides.json is unchanged and contains no vue3-apexcharts entry — the gate passes because the dependency changed. Lockfile churn is exactly two entries: the nc-vue bump and the apexcharts downgrade. 0 additions, 0 removals, 0 nested version changes. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Aligns launchpad on the current nc-vue release, pinned EXACTLY (no caret).
Verified from the LOCKFILE (not package.json):
- `@conduction/nextcloud-vue` resolves to exactly `2.1.0-vue3.16`,
a single instance, no nested duplicate.
- `vue3-apexcharts` resolves to `1.8.0` — below the 1.9.0 boundary at
which that package became proprietary and stopped permitting
sublicensing, which our EUPL-1.2 apps require.
Opt-ins evaluated:
- `@nextcloud/initial-state` `overrides` entry — not present, nothing
to drop.
- local `vue/no-multiple-template-root: 'off'` — DELIBERATELY KEPT.
vue3.16 does switch this rule off in nc-vue's shared eslint preset,
which is why the fleet sweep removes the local disable elsewhere.
launchpad, however, does not extend that preset: it corrects the
inverted Vue-2 rules individually on top of `@nextcloud` (removing
the local disable was verified with `eslint --print-config` to take
the rule from `[0]` straight to `[2]`, i.e. it would ARM a rule that
forbids valid Vue 3 fragment syntax). Only a comment recording that
finding is added. The line can go when launchpad adopts the shared
preset.
- e2e base-URL resolver — NOT switched to nc-vue's shared resolver.
launchpad's `playwright.config.ts` and several specs still carry a
`process.env.NC_BASE_URL ?? 'http://localhost:8080'` fallback, and
`localhost:8080` is the SHARED development container. Swapping the
resolver underneath a live 8080 default is a change of blast radius,
not a version alignment, so it is left for its own PR.
…bject grants
Task 8.3 of openregister's object-level-sharing-and-private-scope. The task was
written as "migrate the derived sharedWith list to the primitive" — there was no
list to migrate. `sharedWith` is declared in launchpad_register.json, seeded as []
by Version002000Date20260519000000, and named in ManifestController's docblock,
and NOTHING READS IT. Only the `owner` filter ever ran. A shared dashboard has
never appeared in anybody's manifest.
The `$seen` dedup map in fetchUserDashboards() is the tell: it exists to reconcile
two sources, and there was only ever one.
So this is net-new rather than a migration. fetchGrantedDashboards() asks
OpenRegister's ObjectGrantResolver for the object UUIDs granted to the caller with
`read`, loads them in one IN(...) query, and folds them through the same $seen map
so a dashboard both owned and granted appears once. The docblock now describes
what the code does.
WHY ADDITIVE, rather than dropping the owner filter and letting RBAC decide.
Letting RBAC decide is tidier, and it is what the OpenRegister `private` scope is
for. But it is only safe once the `dashboard` schema actually carries
`scope: private`, and a register-descriptor change lands through a repair step on
upgrade — so there is necessarily a window, and on any instance where that import
did not apply an indefinite one, in which the schema is still unscoped. An
unfiltered findAll() against an unscoped schema returns EVERY user's dashboards.
The owner filter therefore stays and grants only ever ADD rows. The failure mode
of this shape is a missing dashboard; the failure mode of the other is a
cross-tenant leak in the manifest. Giving the dashboard schema `scope: private` is
a separate, breaking policy change and is deliberately not in this commit.
`read` is the verb, because appearing in a manifest is a read. The resolver
answers only for the five core permission verbs and refuses the rest, so this
cannot silently widen.
Fails soft: an OpenRegister release predating the sharing primitive simply has no
such class, and the manifest degrades to owned-only — exactly its behaviour
before this change. That path is tested, and its control is the test above it:
with the resolver present the same fixture yields two pages, so one page means
the resolver's absence is what dropped it rather than a fixture that never had a
granted row.
The over-cap path warns rather than truncating silently, because a truncated
manifest that logged nothing is indistinguishable from "nothing is shared with
this user".
The tests earned their keep immediately: they caught array_values() where
array_keys() was needed. grantedObjectUuidsFor() returns uuid => bitmask, so the
first version queried `uuid IN (1)`, matched nothing, and would have shipped a
feature that silently returned zero shared dashboards — the exact failure the
tests assert against.
ALSO FIXED, both pre-existing and both found on the way here.
1. 94 call sites of `$qb->select(selects: ...)`. select() is VARIADIC, so a named
argument arrives as ['selects' => ...] — a string-keyed array — and core's
`count($selects) === 1 && is_array($selects[0])` unwrap reads a key that does
not exist, emitting "Undefined array key 0" on every call. The string form
worked by luck, since quoteColumnNames() maps over values; that is why 94
sites survived unnoticed. The ARRAY form was genuinely broken —
Version001006Date20260430130000 passed ['id','share_type','share_with'] where
the unwrap was the thing meant to flatten it, so an array went in as a column
name. Note that our own PHPCS standard ("all arguments in calls to internal
code must use named parameters") is what manufactured this; the rule needs a
variadic exemption or every new mapper reintroduces it.
This was carried in openregister's task list as a pre-existing OpenRegister
warning. It is not in OpenRegister at all — the backtrace lands in launchpad's
DashboardMapper, reached from OR's tests only because a tearDown() user
deletion fires launchpad's UserDeletedListener.
2. `composer psalm` and `composer test:all` both ended in `|| echo '...skipping'`,
so both ALWAYS EXITED 0 and neither could ever fail check:strict. A PR
breaking all 1513 unit tests would have reported success. Replaced with the
`if [ -f ... ]` form, which still skips explicitly when a tool is genuinely
absent but propagates a real failure. Verified rather than assumed: the new
form exits 1 on a deliberately failing test where the old form exits 0.
Verification: 1513 unit tests pass (3 skipped) — unchanged from baseline, so the
94 select() edits regressed nothing. 5 new tests. phpcs error-severity clean on
lib, identical to baseline. phpstan "no errors". psalm "no errors found".
…be nested Found by live-verifying the grant feature on a real instance, which is the only reason it was found at all: every unit test passed, because they double ObjectService and therefore confirm the call shape I invented rather than the one OpenRegister accepts. Measured on the dev instance, admin owning two dashboards: no owner filter at all -> 2 'owner' => 'admin' -> 0 <- what shipped '@self.owner' => 'admin' (dotted) -> 0 <- my first fix, also wrong '@self' => ['owner' => 'admin'] (nested) -> 2 <- correct '@self' => ['owner' => 'nobody-xyz'] -> 0 <- control OpenRegister splits filters into metadata filters — the magic table's `_`-prefixed columns, addressed as a NESTED `@self` array — and property filters, matched against the schema's own properties. A bare `owner` is read as a property filter on an `owner` property, which the dashboard schema does not have, so it matched nothing. `GET /apps/launchpad/api/manifest` therefore returned `pages: []` to EVERY user, including the owner of the dashboards. Dotted `@self.owner` fails the same way; only the nested array works. The last row is the control, and it is the point: without it, "nested returns 2" is indistinguishable from "the filter is ignored and returns everything". Same class of bug in the granted query, fixed the same way: `filters['uuid']` would have degraded to a property filter, so it now uses the top-level `ids` config key, which OpenRegister matches against `_uuid` OR `_slug`. Both spellings are now pinned by assertions rather than left to comments, since each one fails SILENTLY — returning zero rows, never an error. RBAC and multitenancy are NOT involved, which was worth ruling out rather than assuming: with no owner filter, `_rbac: true, _multitenancy: true` returns both rows, so the caller's own scoping was already correct. LIVE END-TO-END, every row with its HTTP status checked: admin (owns both) 200 both dashboards recipient, no grant 200 [] recipient, granted read on 1 of 2 200 exactly that one recipient, grant revoked 200 [] The recipient seeing exactly ONE of the two owned dashboards is the assertion that matters: it is the positive result and the negative control in the same measurement. The revoke row proves the grant is what admitted it. A note on how nearly this was mis-verified: my first recipient check reported `pages: []` and I read it as "no shared dashboards". It was a 403 body, `{"error":"Forbidden"}`, and the probe did `d.get('pages', [])` — so a refusal rendered as an empty success. Checking the status code is what exposed it. The 403 itself was instance state, not a defect: launchpad's stored action matrix predates PR #19's `@all` baseline, so `manifest.index` is still admin-only there.
Bumps [vue](https://github.com/vuejs/core) from 3.5.41 to 3.5.42. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](vuejs/core@v3.5.41...v3.5.42) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.42 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [phpmetrics/phpmetrics](https://github.com/phpmetrics/PhpMetrics) from 2.9.1 to 2.11.0. - [Release notes](https://github.com/phpmetrics/PhpMetrics/releases) - [Changelog](https://github.com/phpmetrics/PhpMetrics/blob/master/CHANGELOG.md) - [Commits](phpmetrics/PhpMetrics@v2.9.1...v2.11.0) --- updated-dependencies: - dependency-name: phpmetrics/phpmetrics dependency-version: 2.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…408) Bumps [phpcsstandards/phpcsextra](https://github.com/PHPCSStandards/PHPCSExtra) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/PHPCSStandards/PHPCSExtra/releases) - [Changelog](https://github.com/PHPCSStandards/PHPCSExtra/blob/develop/CHANGELOG.md) - [Commits](PHPCSStandards/PHPCSExtra@1.5.0...1.5.1) --- updated-dependencies: - dependency-name: phpcsstandards/phpcsextra dependency-version: 1.5.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…407) Bumps [squizlabs/php_codesniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) from 3.13.6 to 4.0.4. - [Release notes](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) - [Changelog](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/4.x/CHANGELOG-3.x.md) - [Commits](PHPCSStandards/PHP_CodeSniffer@3.13.6...4.0.4) --- updated-dependencies: - dependency-name: squizlabs/php_codesniffer dependency-version: 4.0.4 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.27.0 to 3.28.0. - [Release notes](https://github.com/twigphp/Twig/releases) - [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG) - [Commits](twigphp/Twig@v3.27.0...v3.28.0) --- updated-dependencies: - dependency-name: twig/twig dependency-version: 3.28.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.8 to 2.2.9. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [nextcloud/ocp](https://github.com/nextcloud-deps/ocp) from 34.0.2 to 34.0.3. - [Commits](nextcloud-deps/ocp@v34.0.2...v34.0.3) --- updated-dependencies: - dependency-name: nextcloud/ocp dependency-version: 34.0.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…260830084126 chore(sync): carry beta back into development
beta held 19 commit(s) development did not. Merged with -s ours: development's tree is kept BYTE FOR BYTE and only the ancestry is recorded. That is the payload -- without it the merge base never moves and the next development -> beta promotion conflicts on the version file exactly as before. 13 of 19 promotion PRs were CONFLICTING for this reason. Nothing is silently imported. What beta holds and development does not, and which this deliberately does NOT bring over: lib/Settings/launchpad_register.json Those are dead Forgejo/Codeberg CI (removed from development on 2026-08-24/25 by 'chore(ci): remove dead Forgejo/Codeberg CI configuration'), generated Docusaurus build output, and community-health files that never existed on development. Each can be added deliberately if wanted; resurrecting them as a side effect of a sync is how a merge silently undoes a decision.
…0841 chore(sync): record beta's ancestry on development
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Two dependabot bumps landed on development on 2026-08-30 without CI, and each leaves the tree unresolvable: - @vitest/coverage-v8 went to 4.1.11 while vitest and @vitest/ui stayed on ^3.2.7, so coverage-v8 peers vitest 4.1.11 against @vitest/ui's vitest 3.2.7 - stylelint went to 17.14.1, which @nextcloud/stylelint-config 2.4.0 cannot peer: it wants ^15.6.0 Both are reverted to the version the rest of their own ecosystem is on, rather than bumping the ecosystem, because a vitest 3 to 4 move is a separate change that deserves its own testing. Verified with npm ci --dry-run from the committed lock: exit 0. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…426) * fix(deps): hoist @vue/server-renderer so the unit suite can run Every one of the 63 frontend test files failed, with no tests executed at all: Error: Cannot find package '@vue/server-renderer' imported from node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js The package was installed, but NESTED at node_modules/vue/node_modules/@vue/server-renderer rather than hoisted. @vue/test-utils declares it as a PEER dependency ('3.x'), and a peer is resolved upward from the importing package's own directory -- so @vue/test-utils looked for node_modules/@vue/server-renderer, which did not exist. Nothing was missing from the lockfile; it was in the wrong place. Declaring it directly pins it at the top level, which is where a peer has to be. Version tracks vue itself (^3.5.42). Verified locally: reproduced the failure first (1 file, 'no tests'), then after the change the full suite runs -- 63 files passed, 682 tests passed, 0 failed. * fix(stylelint): extend the config the app actually declares stylelint exited 78 -- a CONFIGURATION failure, not a lint finding: Could not find "stylelint-config-recommended". stylelint.config.js extended 'stylelint-config-recommended-vue', which was never declared in package.json. It was present only transitively, and the config it in turn extends, stylelint-config-recommended, was not installed at all. launchpad already declares @nextcloud/stylelint-config ^2.4.0, and that is what openregister, opencatalogi, dossiq and shillinq all extend. This points the config at the package the app declares and the fleet uses, rather than adding two more dependencies to prop up an outlier. That made stylelint RUN, which surfaced 44 real violations the configuration error had been hiding. 42 were auto-fixable (rule-empty-line-before, plus a few over-indented selector continuation lines) and were fixed with --fix; the diff outside css/ is whitespace only. The last two were a genuine CSS bug in css/header-override.css: background-color: #ffffff !important; background-image: none !important; background: #ffffff !important; <- discards both of the above The shorthand alone already sets the colour and resets background-image to none, so keeping only it preserves the computed result EXACTLY. Keeping the longhands instead would not have, because the shorthand also resets the other background sub-properties. Verified: stylelint now exits 0. * fix(stylelint): let Prettier own whitespace, and stop the fixer loop The previous commit made stylelint run, and it and Prettier then disagreed about the same six lines. Prettier indents a wrapped selector list; stylelint's `indentation` rule demanded 0 tabs there. Running either fixer broke the other check: npm run stylelint:fix -> Frontend Check (format) fails npm run format:fix -> Vue Quality (stylelint) fails Both `indentation` and `string-quotes` are DEPRECATED in stylelint 15 -- it prints a deprecation warning for each on every run -- precisely because formatters do this better. Turning them off resolves the conflict in favour of the tool that owns formatting and leaves stylelint judging what only it can judge: CSS semantics. The three files are re-formatted to Prettier's shape. Verified: stylelint exit 0 AND prettier exit 0 together, with the two deprecation warnings gone. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Release: merge development into beta
Dependabot proposed stylelint 17 on its own, which cannot work: it takes coordinated bumps, and the one that actually blocks it is easy to miss. - @nextcloud/stylelint-config -> ^3.2.2, the version that peers stylelint ^17.9.1 - stylelint -> ^17.9.1 - stylelint-config-recommended-scss -> ^17.0.1 and -recommended-vue -> ^1.6.1 where this app pins them - stylelint-webpack-plugin -> ^5.1.0 where this app uses it. This is the blocker: 5.0.1 peers stylelint only to ^16, and 5.1.0 is the first release that accepts ^17. The findings stylelint 17 then reports are fixed rather than silenced: `word-break: break-word` is deprecated and `overflow-wrap: break-word` is what it actually meant, the deprecated `clip` property becomes `clip-path`, and stylelint --fix's logical-property rewrites (`text-align: left` -> `start`) are correct for RTL. Verified: npm ci, npm run stylelint and npm run build all exit 0. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
August 30, 2026 11:26
Dependabot bumped `@vitest/coverage-v8` to 4 on its own in several apps and left `vitest` and `@vitest/ui` on 3. coverage-v8 4 peers vitest 4.1.11 exactly, so a split trio cannot resolve at all: that is what took launchpad's npm ci from green to red. The three move together here, to 4.1.11, which is the current published version of all of them. Verified: npm install and the app's own test script both exit 0. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…260830125059 chore(sync): carry beta back into development
E2E went from 127 passed / 0 failed to 127 passed / 11 failed between two runs 36 minutes apart: 10:45 4d3819d @nextcloud/vue 9.9.0 E2E success 11:21 b0a322a @nextcloud/vue 9.11.0 E2E failure Nothing about the tests changed -- both failing specs (conditional-visibility-editor, dashboard-sharing) were last touched on 2026-08-13. What changed is the lockfile. package.json asked for ^9.5.0, so regenerating the lockfile for an unrelated stylelint PR silently moved @nextcloud/vue two minor versions. 9.10.0 reworked NcSelect: fix(NcSelect): floating label design using NcTextField #8570 fix(NcSelect): truncate long selected labels ... #8829 All 11 failures share one signature, and it is not 'element missing'. The locator RESOLVES, Playwright logs 'attempting click action', and then times out on 'waiting for element to be visible, enabled and stable': - waiting for locator('.vs__dropdown-option').nth(2) locator resolved to <li role="option" class="vs__dropdown-option">... attempting click action -> Timeout 10000ms exceeded An option that exists but never settles is what a re-laying-out floating label produces. ~9.9.0 holds the last version whose NcSelect these tests were written against. This is deliberately a PIN, not a fix: adapting the specs to the new NcSelect is real work and should be its own change, made against the new component on purpose rather than as a side effect of a lockfile regeneration. Verified locally: 9.9.0 installed, 682 unit tests pass, format exits 0. The E2E itself only runs in CI, and that is the check this is aimed at. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Release: merge development into beta
main held 28 commit(s) beta did not. Merged with -s ours: beta's tree is kept BYTE FOR BYTE and only the ancestry is recorded, so the beta -> main promotion stops conflicting on files where beta is simply newer. Not brought over -- beta is hundreds of commits ahead of main, so these are the OLDER copies, and several are dead Forgejo/Codeberg CI that development deliberately removed: .forgejo/workflows/release-beta.yml .forgejo/workflows/release-stable.yml .github/dependabot.yml .github/workflows/branch-policy.yml .github/workflows/branch-protection.yml .github/workflows/code-quality.yml .github/workflows/documentation.yml .github/workflows/issue-triage.yml .github/workflows/openspec-sync.yml .github/workflows/release-beta.yml .github/workflows/release-stable.yml .github/workflows/sync-to-beta.yml appinfo/info.xml docs/features.json
Contributor
Quality Report — ConductionNL/launchpad @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 526/526 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-30 17:23 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stable release:
betaholds 418 commit(s)maindoes not.Merged with
--merge, never--squash. Squashing a promotion rewrites the carried commits into onebetadoes not contain, so the branches diverge again immediately and main's own commits read as reverted.A failing
… / releasecheck on this pull request is the App Store publish step, not a quality gate. Eight fleet apps cannot publish today: seven have no signing key, and thematiq's certificate carries its old app id (Nextcloud issues one certificate per id, CN = the id). The GitHub release and tag are still created. Every other check must be green for this to merge.