Skip to content

Release: merge beta into main - #722

Closed
rubenvdlinde wants to merge 868 commits into
mainfrom
release/beta-to-main-20260830122902
Closed

Release: merge beta into main#722
rubenvdlinde wants to merge 868 commits into
mainfrom
release/beta-to-main-20260830122902

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Stable release. beta was 866 commits ahead of main.

Conflicts were resolved to beta's side, including its refactors: where beta had removed a file, the removal stands rather than resurrecting a stale copy from main. No conflicts.

git merge -X theirs settles content conflicts but leaves modify/delete ones unmerged — beta deleted the file, so there is no "theirs" blob to take. Those were resolved by honouring the deletion.

Verified before pushing: the commit has exactly two parents, and no conflicted path was left unresolved. Files main keeps that beta never had (archived openspec docs, whitespace-only differences) are preserved — "beta wins" governs conflicts, not additions.

A failing … / release check here is the App Store publish step, not a quality gate: 7 apps have no signing key and thematiq's certificate carries its old app id. The GitHub release and tag are still created.

rubenvdlinde and others added 30 commits August 19, 2026 21:46
…60819175834

chore(release): 0.1.1-unstable.20260819175834
…ked-18.0.9

build(deps): bump marked from 12.0.2 to 18.0.9
…tcloud/axios-2.6.0

build(deps): bump @nextcloud/axios from 2.5.2 to 2.6.0
…ie-4.4.5

build(deps): bump dexie from 4.4.4 to 4.4.5
Renames every remaining SCHOLIQ_-prefixed constant (SCHOLIQ_REGISTER,
SCHOLIQ_NATIVE_COLLECTION_SCHEMA) to LEARNIQ_*, declaration and every
self::/static:: use together in the same edit. Renames the private
isScholiqXapiStatement() method to isLearniqXapiStatement() in
XapiCompletionHandler and LessonProgressHandler, and every call site.

Also fixes stale "Scholiq" prose in docblocks, comments, and log/error
messages across lib/ (class headers, register/event-filter descriptions,
stale scholiq_register.json file references, and two factually-wrong
autoloader-ordering comments that still described the OLD app id's
alphabetical position relative to openregister).

Deliberately NOT renamed: the wire/format identifiers kept for backward
compatibility (scholiq-json, scholiq-native, scholiq-learning-record,
scholiqField, scholiqNative, _scholiqRecordId, FORMAT_SCHOLIQ_JSON, the
course-<id>_scholiq.json export filename), the AVG processing-activity
register codes (scholiq-ai-features and siblings), the three repair
steps' OLD_APP_ID/OLD_SLUG/REGISTER_SLUG_PREFIX constants and their
historical-fact prose, @SPEC tags pointing at real openspec/ paths that
still exist under the old name, and two newly-identified persisted
external identifiers with no migration path: the NC Files folder prefix
'Scholiq/{tenant}/...' (CoursePackageFileWriter, LearningRecordBundleWriter,
LearningRecordImportIntakeService) and the NC group-name prefix
'scholiq-cohort-' generated by RolloverService::groupName().
Renames SCHOLIQ_SCHEMAS -> LEARNIQ_SCHEMAS (and its scholiqResults ->
learniqResults local variable) in LearniqNotificationSettings.vue, and
fixes stale "Scholiq" prose, CSS class/id names (scholiq-notif-settings__*,
scholiq-settings__*, scholiq-dashboards__*, scholiq-admin, scholiq-domain-
dashboard, scholiq-dataexchange-settings__*), stale lib/Settings/
scholiq_register.json file references, and user-facing settings copy
across the Vue views, manifest.json (Prometheus metric help text and
dashboard/manifest _note fields), main.js, icons.js and app.css.

Verified before renaming: every touched CSS class/id is component-scoped
with no cross-file selector or test dependency (checked via grep across
src/ and tests/ before each rename).

Also corrects two DashboardAdmin.vue/DashboardTeacher.vue/main.js
comments that claimed dashboard visibility was gated on a literal
`scholiq-{role}` NC group — that group naming was already dead (per
RoleSelector.php's own comment, it "never existed on any instance");
the real mechanism is DashboardRoleService resolving dashboardRoles
from NC's admin group and the unprefixed role-backing groups. The old
comment was inaccurate under the old name too, not just stale naming.
Renames the global function scholiq_nc_base_is_safe_to_load() (and the
$scholiqNcRoot variable) to learniq_nc_base_is_safe_to_load()/
$learniqNcRoot across tests/bootstrap-nc-guard.php, bootstrap.php and
bootstrap-unit.php together, plus SCHOLIQ_E2E_SEEDED -> LEARNIQ_E2E_SEEDED
and SCHOLIQ_DOCS_CAPTURE -> LEARNIQ_DOCS_CAPTURE (both env var names, kept
in sync with the root playwright.config.ts in the next commit).

Fixes two e2e assertions that were checking live reality against the
wrong string and would fail against the deployed app: detail-pages.spec.ts
and index-pages.spec.ts asserted `page.title()` contains 'Scholiq' — the
live page title is 'Learniq - Nextcloud'. Also fixes a silently-broken
soft-check in nextcloud-app.spec.ts: a combobox-option filter searched
`/scholiq/i` while its own comment said to look for 'learniq', so the
option was never found and the click branch never ran.

Otherwise the usual stale-prose/temp-dir-prefix/mock-domain cleanup
(scholiq_test_* -> learniq_test_*, https://scholiq.example ->
https://learniq.example, etc.), plus fixing PortalContributionProviderTest
to assert 'Learniq' (matching PortalContributionProvider.php's already-
renamed manifest label — the two had drifted out of sync mid-rename).

Deliberately left unchanged: fixture data mirroring protected production
literals (the 'Scholiq/materials/...' mock file paths matching the
production NC-Files-folder-prefix decision, scholiq-cohort-* group-name
fixtures), the historical/documents-the-rename-itself mentions, GitHub
issue shorthand (scholiq#35, scholiq#265), and @e2e/@SPEC tags paired
with real openspec/ anchors.
…root config

Renames the remaining "Scholiq" prose in routes.php's AppHost-adoption
comments to "Learniq" (the app's own name, its URLs, its bespoke
SettingsController). Renames SCHOLIQ_DOCS_CAPTURE -> LEARNIQ_DOCS_CAPTURE
in the root playwright.config.ts, matching tests/e2e/playwright.config.ts's
comment and the env var tests/bootstrap already reads.

Left unchanged (deliberate, out of scope for this pass): the literal old
register-slug/app-id/app-config-namespace values in routes.php's
'scholiq-ai-features' AVG code and 'scholiq-native JSON' format mentions,
and appinfo/info.xml's codeberg.org/Conduction/nextcloud-scholiq URLs —
those are an external repo-hosting decision, not code-identifier residue,
and changing them without confirming the remote was actually renamed
would just replace one wrong URL with another.
The naming sweep touched this file, so gate-16 saw the method as changed.
Excluded with a reason: it answers 'is this event about one of our
objects' and returns '' for every negative case, so callers bail out. The
register it matches against is the app id - which is exactly why the
rename's diff surfaced it.
Scholiq becomes Learniq: declared RBAC, reachable Compliance, and the rename
development ended up with the namespace rename but NOT the app identity:
Application.php declares APP_ID 'learniq' and namespace OCA\Learniq, the
register is learniq_register.json, CI passes app-name: learniq - while
appinfo/info.xml still said <id>scholiq</id> and <namespace>Scholiq</namespace>.

Nextcloud resolves OCA\{namespace}\AppInfo\Application from info.xml, so
that combination cannot boot the app at all.

Cause: the GitHub-side merge ca67455 resolved info.xml to development's
side, reverting all 100 lines. The later release commit only touched
<version>, so the revert predates it. Because that resolution is now an
ancestor, a plain merge silently takes development's file again - which is
what happened here before this commit.

Restores the Learniq info.xml (id, namespace, repair steps, learniq
register references, and the <=128-char summaries the App Store schema
requires) while KEEPING development's release version so the release
automation is not fought.

Also carries the e2e guard for OC.generateUrl: reading it straight after
goto throws an opaque TypeError on Nextcloud's maintenance and login
pages, which both return a normal 200. Observed 2026-08-19 - a concurrent
session ran occ, the instance went into maintenance for under a minute,
and 15 tests failed pointing at application code.
Its docblock justified matching only the 'scholiq' slug by claiming the
step runs BEFORE RenameRegisterSlug 'because info.xml lists it first'.
That is false: appinfo/info.xml lists RenameRegisterSlug FIRST and
RenameDutchColumns LAST. So by the time it ran, the slug was already
'learniq', its LIKE 'scholiq%' lookup matched zero registers, and the step
reported success while migrating nothing.

Now matches BOTH prefixes, which removes the dependency on step order
entirely - an ordering assumption stated only in a comment is enforced by
nothing.

Also clears the three phpcs errors CI flagged (doc long-description
capitalisation in the three repair steps) and prettier-formats the visual
spec. phpcs errors: 3 -> 0. Repair-step tests still 11/11.
Admin-merged with a stated justification rather than a green tick.

development cannot boot: appinfo/info.xml declares <namespace>Scholiq</namespace> while the code is OCA\Learniq. Nextcloud resolves OCA\{namespace}\AppInfo\Application from that file, so the app fails to load. This PR is the fix.

CI parity, job by job (35 jobs both sides):
- development baseline: 28 success, 5 skipped, 2 failures — quality/Hydra Gates, quality/Quality Report
- this PR: 27 success, 5 skipped, 3 failures — the same two, plus one

The extra failure is self-resolving and says so itself:
  'PHPUnit is present at merge base c95cb6a but produced no clover report there — the base suite could not run at all. Refusing to report a ratchet that did not run.'
The merge base IS development's broken state. The ratchet is behaving correctly by refusing to compare against a base it could not measure, and it clears once this lands.

phpcs and prettier were failing on this branch earlier and now PASS.

Verified locally on a live instance: /apps/learniq/ 200, nav renders, Compliance present; register slug learniq; canary GradeEntry d4caa278 fingerprint unchanged; all 8 declared groups present; paired RBAC proof on one actor (Course 200 / other learner's GradeEntry 404 / create 403, with admin 200 on the same object as positive control); PHPUnit 952 tests at baseline parity; hydra-gates 2 failing (19, 61 — both pre-existing) with 63 of 63 applicable gates run.
The three remaining gate-26 failures were page.goto timeouts, not assertion
failures. Playwright's default waitUntil is 'load', which waits for every
subresource; this app's entrypoint is 11.3 MiB and the instance was at 269%
CPU with the HTML alone taking ~5s, so 60s was not enough.

Nothing is lost by using domcontentloaded: every caller already asserts a
component root is visible or asserts on content, and those carry their own
waits. Waiting for the last font was never what made these tests meaningful.

16 of 16 now pass, and the run went from 11.3 min to 3.7 min.

Note for whoever picks this up: 93 other page.goto calls across the suite
still use the default, so the same latency is there. Not changed here -
that is a suite-wide refactor, and CI's clean instance absorbs it today.
Admin-merged: the PR's own verdict is green; the block is a cancelled sibling run.

All four pull_request workflows on head 13dd41d are completed/success. Code Quality run 32316147386: 35 jobs, 30 success, 5 skipped, 0 failures — including 'quality / Quality Report -> success' and 'quality / Hydra Gates', BOTH of which the development baseline currently fails.

What blocks the button is run 32316112260 — the push-event twin of the same sha, cancelled by concurrency. Its 23 cancelled jobs and the resulting Quality Report failure land in the PR's statusCheckRollup alongside the real ones. Cancelled is not failed, and it is not a verdict on this code.

Local: 42 e2e specs pass (shell + visual + pages + credential-verify), the 16 gate-26 specs went 1 -> 16 passing and the run 11.3min -> 3.7min.
The spec chose NESTED sub-groups for Progress and Compliance, knowingly
reintroducing the 3-level depth ADR-044 exists to eliminate, for one
stated reason: 'No built-in component renders a grid of arbitrary
navigation links.'

That is no longer true. @conduction/nextcloud-vue 2.8.0 ships
CnNavCardGrid, registered as the built-in widgetKey nav-card-grid. It was
built precisely because this gap blocked ADR-044 §4 fleet-wide.

Verified in the installed package rather than assumed: 216 occurrences of
nav-card-grid in dist, including the compiled manifest validator; entries
render as native router-link/anchor, or a disabled visibly-flagged div for
an unresolvable route, which is what ADR-044 §5 requires.

So Progress and Compliance become ONE top-level entry each, routing to a
dashboard page whose single widget is a nav-card-grid - one card per
former sub-group, each keeping its own label. Nav depth stays at two.

Decision 2 stands unchanged: applyMenuRelocations DISSOLVES a relocated
group and discards its label, which is still why the other folds use
fragment nesting rather than relocations.

Bumps the nc-vue pin ^2.3.0 -> ^2.8.0 with the lockfile. An app on an
older version would render nothing for these two routes, so the bump is
part of this change, not a follow-up.
…60820004806

chore(release): 0.1.1-unstable.20260820004806
…60820045228

chore(release): 0.1.1-unstable.20260820045228
…oken

v1.8.0 shipped an ObjectServiceInterface WITHOUT patchObject() and with
updateObject() still summarised as "Apply a partial update" — the wording
that sent a consumer down the erasing path. The correction landed on main
two days after the tag; every app has been pinned to the broken copy since.

It is not confined to this repo: hydra-gates claims OCA\OpenRegister\Contract\
in its composer autoload, a LONGER psr-4 prefix than openregister's own
OCA\OpenRegister\ -> lib/, so the gate package wins. Nine repos vendor it, so
under OC_App::loadApps() whichever app registers first defines the contract
for the whole instance. Measured on a running instance, softwarecatalog's
vendor directory was supplying openregister's interface, and updating
openregister ALONE did not change the winner — which is why this lands
across the fleet rather than in one repo.
…60820050545

chore(release): 0.1.1-unstable.20260820050545
…d fragments

ADR-044 §6 prerequisite for menu-six-main-items: the 14,663-line monolith
held 24 top-level menu entries and 275 pages inline while manifest.d/ only
carried the two dashboard fragments. Splits every top-level menu id (and its
full, unmodified subtree) into one of fourteen manifest.d/*.json fragments
per openspec/changes/manifest-fragment-split/design.md Decision 3, folds
learning-dashboard.json/people-dashboard.json into learning.json/people.json
(Decision 4), and thins manifest.json to its five metadata keys plus the four
utility menu singles (Documentation, FeaturesRoadmapMenu, XapiStatementsMenu,
Rollover) that don't belong to any of the fourteen boundaries.

No behaviour change: buildManifest() output is content- and children-order-
identical before/after (verified by a throwaway pre/post deep-equal script,
not committed). The only difference is top-level menu[]/pages[] raw ARRAY
POSITION, an unavoidable artifact of buildManifest's base-processed-first
algorithm once most content leaves the base file — CnAppNav.vue's
visibleItems computed sorts top-level items by the `order` field at render
time (not array position), and pages[] order has no bearing on vue-router 4
route matching, so this does not change what renders.

src/main.js and src/menu-layout.json are untouched — require.context already
globs manifest.d/*.json.
…t-driven specs

Five specs imported src/manifest.json directly and iterated manifest.pages —
some to generate one test() per page at module-load time. After the
manifest-fragment-split, the base file only carries 4 of the 275 pages, so
those specs silently generated far fewer tests (or, for pages.visual.spec.ts,
resolved routes to null) instead of failing loudly.

Adds tests/e2e/effective-manifest.ts, which assembles the same merged
manifest src/main.js builds at runtime (buildManifest + every manifest.d/
fragment, sorted by filename like require.context, + menu-layout.json), and
switches accessibility-axe-scan.spec.ts, detail-pages.spec.ts,
index-pages.spec.ts, manifest-declarative-surfaces.spec.ts, and
pages.visual.spec.ts to import effectiveManifest instead. Test-generation
counts are restored to their pre-split values (98 index pages, 108 detail
pages), and tests/e2e/shell.spec.ts + tests/e2e/visual/pages.visual.spec.ts
pass 16/16.
chore(deps): take hydra-gates v1.8.1 — the contract v1.8.0 shipped broken
…60820052222

chore(release): 0.1.1-unstable.20260820052222
rubenvdlinde and others added 24 commits August 29, 2026 03:48
#686)

* feat(setup): a wizard that offers the demo data this app already ships

This app ships lib/Settings/*_mock_register.json - a dataset generated from its
own schemas, conformant by construction, validated by the generator's --check -
and had no way for an operator to reach it. There was no setup wizard at all.

welcome -> demo-data -> done. Nothing app-specific is invented: the only action
is the demo-data import the descriptor already supports. A wizard that asked
questions the app does not act on would be worse than none, which is why there
are no configuration steps here yet.

completed is TRUE and the demo-data step is optional, so setup never gates the
app. skip-demo-data records its outcome just as installing does: since
nextcloud-vue 2.21 an OUTSTANDING OPTIONAL step opens the wizard over every
page (nextcloud-vue#806), so a step that can never be marked done is a dialog
that never closes - the defect buildiq was failing 37 E2E specs on.

Verified: manifest validates against schema 2.26.0, gate-100 PASS, routes.php
and both PHP files parse. The template was checked on launchpad against phpcs,
phpstan, psalm and phpmd - all clean.

* fix(setup): declare the endpoints' auth, and translate the wizard's strings

Two gate findings on the previous push.

gate-5 route-auth — status() and runAction() carried no auth attribute. The
docblock said 'admin-only by Nextcloud's default for an un-attributed method',
which is true and is not a declaration: the gate exists because a missing
attribute silently makes an endpoint unreachable, and a comment cannot be
checked by middleware. Both now carry
#[AuthorizedAdminSetting(Application::APP_ID)], placed DIRECTLY above the
declaration - gate-5 walks upward from the method and a long docblock between
attribute and declaration costs the attribute its visibility, which the gate
documents as a false FAIL it had to repair.

gate-102 manifest-l10n-coverage — the wizard's title and body strings had no
l10n/nl.json key, so a Dutch user would read them in English. Added, and the
browser catalogue rebuilt where the app ships one: nl.json alone is not enough,
because the browser reads nl.js.

The catalogue edit is insertions only, proven against the same change applied
structurally - an earlier attempt on another app re-serialised the whole file
(410 lines) before being reverted.

* fix(setup): authorize against the admin settings class, and test what it guards

`AuthorizedAdminSetting` takes a `class-string<IDelegatedSettings>`, not an
app id, so `Application::APP_ID` — a plain string — was rejected by phpstan.
The apps where this shipped green (larpinq, shillinq) already pass their admin
settings class; match them.

gate-47 and the coverage ratchet were both right to fail this. The change adds
an admin-authorized endpoint pair and ~364 lines of PHP with nothing behind
them. Two assertions are worth naming:

- a FAILED install must leave the step UNDECIDED. Recording the decision in
  the catch block would close the step for an operator who asked for demo data
  and received none.
- the object count comes from the FILE, not the importer's reply, so the
  number reported is the number ASKED FOR.

Both verified by mutation on openregister: reversing each behaviour fails
exactly the test that claims to guard it. The e2e spec issues both calls from
inside the logged-in admin page, which is the only place that middleware can
be observed admitting a real session.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…licks (#689)

The ADR-111 setup step is OPTIONAL, and CnAppRoot opens the non-gating wizard
as a full modal mask while any optional non-info step is reported not-done —
in every fresh browser context, so once per spec. Merging the setup wizard
therefore turned this app's whole E2E suite red without touching a single
spec: the call log reads "locator resolved to <button ...> - attempting click
action" with <ol class="cn-wizard-dialog__progress"> named as the interceptor.
The element was found; the click never landed.

SKIPPED rather than installed, because recording the DECISION is what closes
the wizard. Installing would push the app's demo dataset into every list the
suite asserts on, which changes what the other specs measure.
`demo-data-setup-step.spec.ts` exercises the install deliberately, in
isolation.

Uses the workflow's own exported credentials rather than this script's
internals, and is tolerant of a non-200: an app whose wizard has no demo-data
step answers 400, and that is not a seeding failure.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
* fix(e2e): let the demo-data spec inherit this suite's auth

The spec declared `storageState: tests/e2e/.auth/admin.json`, which this app
does not write — `global-setup.ts` writes `test-results/.auth/admin.json`, and
the config already points `use.storageState` there for every project. The
override therefore replaced a working session with a path that does not
exist, and the whole describe failed in 2ms on ENOENT before a single
assertion ran.

That declaration was carried over from openregister, whose default project
authenticates with a Basic header instead and genuinely needs it. Everywhere
else in this fleet the spec inherits the suite's own session, which is what
the five apps it was proven on do.

* style(e2e): drop the blank line the removal left behind

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…687)

* feat(walkthrough): a first-visit tour built from this app's own menu

ADR-043. The app had no walkthrough, so a new user arrived on a shell with no
orientation at all.

Every step targets a route READ FROM THIS MANIFEST, never invented.
CnWalkthrough resolves target {kind: nav-item, ref} by querying
[data-cn-route=<ref>], which CnAppNav emits from the ROUTE - so a tour pointing
at a menu id that differs from its route still renders, as a centred anchorless
coachmark that points at nothing. Verified: every ref resolves to a page this
manifest declares.

Where a menu label is a translation KEY rather than a sentence, the copy uses
the key's last segment as a word. Printing launchpad.menu.dashboards at a user
is worse than useless, and falling back to the route gives 'Click
dashboard-detail in the menu' - an internal identifier read aloud.

Verified: manifest validates against schema 2.26.0, every target ref resolves,
and no raw translation key reaches the copy.

* fix(walkthrough): point the tour at flows, and translate its copy

gate-70 walkthrough-flows-stop - this app ships a flows page and no tour step
targeted it. The gate's own measurement is why it exists: across 20 manifests,
12 apps shipped a flows page and exactly ONE tour mentioned it, so the
automation surface was discoverable only by someone who already knew.

Detected the way the gate detects it - type:'flows' OR the migrated form, an
index over config.entitySource:'flows'. Matching on type alone would miss every
app the flow-page migration touched, which is all of them.

The step advances on route-match and sets allowManualNext. The same gate fails a
step advancing only on object-created: that turns 'here is where flows live'
into 'build an automation before you may continue', and a tour nobody can finish
is worse than one that never mentions flows.

gate-102 manifest-l10n-coverage - the tour's titles, bodies and tasks had no
l10n/nl.json key. Added, and the browser catalogue rebuilt where the app ships
one, because the browser reads nl.js rather than nl.json.

* style(walkthrough): drop the em-dash from the flows step

gate-96 (manifest-copy-style) fails this: voice.md section 8 is unambiguous —
no em-dashes, Conduction strips them fleet-wide.

The nl.json KEY is the English string, so it is renamed with it; leaving the
key behind would orphan the Dutch translation of a string that still ships.
The Dutch VALUE carried the same em-dash and gets the same treatment — the
gate only counts manifest strings, but it is the same user-facing sentence.

Note for a later sweep: this app's nl.json still holds 16 other em-dash
strings from Vue copy. They are outside this gate's scope (it reads manifest
strings) and outside this PR's.

* fix(l10n): refresh the browser catalogue after the em-dash rename

check:l10n-js reported 'Stale browser catalogue: l10n/nl.js'. Renaming the key
in nl.json without nl.js leaves the two halves of the same catalogue
disagreeing, and the runtime reads the .js one.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
2.22.1 carries the theme app-id fix (nextcloud-vue#840). CnAppRoot calls
useScopedTheme() with no slug, so this app resolved theme tokens, the
token-set catalogue and the contrast check through a hardcoded 'nldesign'
app id. thematiq is renaming to 'thematiq', and every path in that
composable degrades to default styling by design — so once a renamed build
is installed this app would render unthemed with nothing in any log.

The LOCK is what moves here. A caret range alone changes nothing, because
npm ci installs what package-lock.json pins.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…licks (#703)

@conduction/nextcloud-vue 2.22.x made the product walkthrough actually open.
A `placement: "center"` welcome step used to be parked in `_pendingAutoTour`
and never shown; the library now correctly starts it on any route. Its
`cn-walkthrough__dim--full` layer is a `role="dialog" aria-modal="true"`
overlay, so every spec that clicks behind it times out, and
`getByRole('dialog')` resolves to the dim layer rather than the modal under
test.

The marker is per USER, not per test, so leaving it unseeded also makes the
suite order-dependent: whichever spec runs first wears the tour.

Seeded before the single storageState save, which is where this suite
persists its authenticated state.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…xt (#701)

gate-103 (locale-duplication) fails this app: 28 locale pairs are byte-for-byte
copies of another language. They are not "untranslated" — they are WRONG, and a
user who picks one is shown a language they did not choose.

Identified by reading the content, not just the hashes. Six groups, one real
translation each:

  * bs, cs, mk, sk, sl, sr == hr    CROATIAN  ("Konfiguriranje postavki")
  * be, uk                 == ru    RUSSIAN   ("Настройки Learniq")
  * ca                     == es    SPANISH   ("Configurar los ajustes")
  * sv                     == da    DANISH    ("Konfigurer appindstillingerne")
  * lb                     == de    GERMAN    ("App-Einstellungen konfigurieren")
  * rm                     == it    ITALIAN   ("Configura le impostazioni")

Deleted rather than blanked, so those twelve fall back to English. English a
Slovak reader can recognise as not-their-language beats Croatian they may
mistake for it.

Verified after the deletion: zero duplicate groups in both the .json and .js
catalogues, 24 locales remain, and every kept bundle still parses.

Noted while reading, NOT fixed here: the kept `hr` bundle still says
"Postavke Scholiqa" — the pre-rename app name survives inside the translated
strings. That is a separate change.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
#702)

The first-visit walkthrough opens as a modal with a full-page dim inside
role="dialog", and it does so in EVERY fresh browser context — once per spec,
not once per suite. Merging it therefore turned specs red without touching
them: the Playwright call log resolves the target button, attempts the click,
and names the interceptor as

    <div class="cn-walkthrough__dim cn-walkthrough__dim--full">

learniq lost 7 specs to this and portaliq 2, all on development, all after the
walkthrough landed.

Marks the tour SEEN rather than disabling it. The manifest declares
`walkthrough.version: 1` with `completionConfigKey:
walkthrough_completed_version`, so storing that version is exactly what a real
operator does by finishing or dismissing the tour once — the walkthrough
itself stays shipped and testable.

Same shape as the demo-data decision this seed already settles, and tolerant of
a non-200 for the same reason: an app without a walkthrough answers 400 there,
which is not a seeding failure.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
`prettier --check` failed on the walkthrough marker seeded earlier today.
Purely a line-width wrap; the marker and its behaviour are unchanged.

Length-dependent, which is why the identical insertion passed in
shorter-named apps: 'cn-walkthrough-seen:learniq' pushes the call past the
print width where a shorter app id does not.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…rds (#696)

* feat(demo): generated demo data for every schema (ADR-111 rules 1-2)

This app declares schemas and shipped no demo data, so it opened on an empty
list: the person evaluating it had to author objects by hand against a schema
they did not know yet. Fleet-wide, 562 of 598 schemas were in that state.

🔴 GENERATED, NOT WRITTEN. Every value is derived from the schema that will
validate it — `enum` picks from the enum, `pattern` is satisfied, `format`
drives the shape, `minimum`/`maxLength` are honoured, `required` is always
populated. Hand-written demo data is wrong in a way nobody sees until the demo
(a status outside its own enum, a required field omitted) and it fails at
import, in front of whoever asked for the demo.

Produced and validated by the single copy that gate-99 also runs:
`vendor/conduction/hydra-gates/scripts/lib/generate_mock_register.py`.
Regenerate with that file; `--keep` preserves curated objects and tops up only
what is short.

🔴 IT DOES NOT INSTALL ITSELF (ADR-111 rule 3). `x-openregister.type: mock` is
imported ON DEMAND — sample data appearing on a production instance because
somebody upgraded is a data-integrity incident, not a convenience. Install it
with:

    occ openregister:descriptors:list --app=<app> --import=<register>

The setup-wizard step that offers this during first-run install (ADR-111 rule 4)
follows once OpenRegister's shared installer lands — deliberately not twenty-one
copies of the same logic.

Verified: `--check` re-validates every object against its own schema with
jsonschema and reports clean.

* fix(demo): emit real MDI icon names, so gate-60 stops rejecting the demo data

gate-60 (icon-vocabulary) failed the very PR that added demo data: the generator
emitted `Voorbeeld Icon 1` for properties named `icon`, and an icon outside the
canonical vocabulary "renders blank wherever it is not aliased locally"
(ADR-077).

🔴 THE FIX READS THE GATE'S OWN VOCABULARY FILE, never a copy of it. Carrying a
second list here is precisely how a producer and its judge drift apart, which
this package fixed once already today.

🔴 AND IT READS THE VALUES, NOT THE KEYS. The first attempt emitted the semantic
key — `documentation` — because that is what the map is keyed by. The map is
`"documentation": "BookOpenVariantOutline"`, and gate-60 accepts the MDI name:
it rejected `documentation` in the same breath as the invented name it replaced,
"a kebab-case or lowercase spelling of an MDI name resolves to nothing". Half
reading a vocabulary is not reading it.

Verified on portaliq: gate-60 PASS, zero gate failures. All seven apps
regenerate with zero validation findings.

* fix(demo-data): attribute the descriptor to the app id, not the directory

`x-openregister.app` is what the descriptor inventory resolves a register to
an app by, and the generator was writing the CHECKOUT DIRECTORY name into it.
Five of eight apps already ship an `<id>` that differs from their directory —
larpinq, learniq, humaniq, decidiq, buildiq — so those descriptors named an
app that does not exist, and the `occ openregister:descriptors:list --app=…`
command printed in their own description resolved nothing. A cross-app id is
a runtime lookup: it does not error, it silently finds nobody.

The id now comes from `<id>` in appinfo/info.xml, the stated authority,
falling back to the directory name only when there is no info.xml. The output
file is renamed to match and the directory-named one removed, so exactly one
mock descriptor remains per app — two would import demo data twice.

Register slugs are untouched: those are frozen per app.

* fix(lint): blank line between multi-line properties in LearniqDashboards

`vue/new-line-between-multi-line-property` was erroring on development
itself, not on anything this branch changed — the file here is byte-identical
to development's. Every branch cut from it inherits a red lint-check, which
is how the failure arrived on a PR that only touches a JSON descriptor.

eslint --fix, one blank line. `eslint src` now reports 0 errors (the 19
remaining warnings are pre-existing and do not gate).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Two independent faults, either of which alone stops the docs site updating.

TRIGGER. This listened on a branch called `documentation`. Nobody has pushed
to one since 2026-05-25, so every docs change merged to `development` passed
review and published nothing.

SECRETS. A reusable workflow receives no secrets by default. With none mapped,
the callee's publish step finds CF_API_TOKEN empty and skips itself on its own
guard, and the run finishes GREEN having changed nothing. Fixing only the
trigger would have produced exactly that.

The worker name is now pinned. Deriving it is the documented way to get a green
run that reaches nobody: wrangler creates the derived worker and publishes
there while the custom domains keep routing to the real one.

Where the app was renamed, `canonical-host` turns the retired hostname from a
second live copy of every page into a 301 to the same path on the current one.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
#695)

* feat(demo): generated demo data for every schema (ADR-111 rules 1-2)

This app declares schemas and shipped no demo data, so it opened on an empty
list: the person evaluating it had to author objects by hand against a schema
they did not know yet. Fleet-wide, 562 of 598 schemas were in that state.

🔴 GENERATED, NOT WRITTEN. Every value is derived from the schema that will
validate it — `enum` picks from the enum, `pattern` is satisfied, `format`
drives the shape, `minimum`/`maxLength` are honoured, `required` is always
populated. Hand-written demo data is wrong in a way nobody sees until the demo
(a status outside its own enum, a required field omitted) and it fails at
import, in front of whoever asked for the demo.

Produced and validated by the single copy that gate-99 also runs:
`vendor/conduction/hydra-gates/scripts/lib/generate_mock_register.py`.
Regenerate with that file; `--keep` preserves curated objects and tops up only
what is short.

🔴 IT DOES NOT INSTALL ITSELF (ADR-111 rule 3). `x-openregister.type: mock` is
imported ON DEMAND — sample data appearing on a production instance because
somebody upgraded is a data-integrity incident, not a convenience. Install it
with:

    occ openregister:descriptors:list --app=<app> --import=<register>

The setup-wizard step that offers this during first-run install (ADR-111 rule 4)
follows once OpenRegister's shared installer lands — deliberately not twenty-one
copies of the same logic.

Verified: `--check` re-validates every object against its own schema with
jsonschema and reports clean.

* fix(demo): emit real MDI icon names, so gate-60 stops rejecting the demo data

gate-60 (icon-vocabulary) failed the very PR that added demo data: the generator
emitted `Voorbeeld Icon 1` for properties named `icon`, and an icon outside the
canonical vocabulary "renders blank wherever it is not aliased locally"
(ADR-077).

🔴 THE FIX READS THE GATE'S OWN VOCABULARY FILE, never a copy of it. Carrying a
second list here is precisely how a producer and its judge drift apart, which
this package fixed once already today.

🔴 AND IT READS THE VALUES, NOT THE KEYS. The first attempt emitted the semantic
key — `documentation` — because that is what the map is keyed by. The map is
`"documentation": "BookOpenVariantOutline"`, and gate-60 accepts the MDI name:
it rejected `documentation` in the same breath as the invented name it replaced,
"a kebab-case or lowercase spelling of an MDI name resolves to nothing". Half
reading a vocabulary is not reading it.

Verified on portaliq: gate-60 PASS, zero gate failures. All seven apps
regenerate with zero validation findings.

* fix(demo-data): attribute the descriptor to the app id, not the directory

`x-openregister.app` is what the descriptor inventory resolves a register to
an app by, and the generator was writing the CHECKOUT DIRECTORY name into it.
Five of eight apps already ship an `<id>` that differs from their directory —
larpinq, learniq, humaniq, decidiq, buildiq — so those descriptors named an
app that does not exist, and the `occ openregister:descriptors:list --app=…`
command printed in their own description resolved nothing. A cross-app id is
a runtime lookup: it does not error, it silently finds nobody.

The id now comes from `<id>` in appinfo/info.xml, the stated authority,
falling back to the directory name only when there is no info.xml. The output
file is renamed to match and the directory-named one removed, so exactly one
mock descriptor remains per app — two would import demo data twice.

Register slugs are untouched: those are frozen per app.

* feat(flows): give the flow-detail canvas its sidebar

The manifest _note claimed the controls rendered in the NC app sidebar,
but the sidebarComponent field it described did not exist. Every
#/flows/:id -- and #/flows/new, the same route with the literal id -- drew
a bare canvas: savable and runnable, but with no way to name, describe,
trigger or step-edit the flow, because those controls all live in
CnFlowSidebar. Mirrors pipelinq#1490. ADR-110 Decision 4.

* test(stubs): assert OpenRegister stubs carry canonical parameter names

learniq calls the stubbed OpenRegister classes with named arguments, which
bind by name, so a stub whose parameter names drift from canonical passes
every local unit run and fails only in CI -- where the real app is
installed -- with an error naming the stub rather than the caller.
DeferredListenerContext was stubbed __construct(array $entries) against a
canonical __construct(?string $userId, ?string $orgUuid, array $entries).

* docs(registry): name the flow page types development actually declares

The comment named `flows` / `flow-detail`; development renamed them to an
`index` with config.entitySource 'flows' and `flow" (#669), which the merge
resolution above adopts.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
beta held 23 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:

  .forgejo/.beta-release-trigger .forgejo/issue_template/bug-report.yml .forgejo/issue_template/feature-request.yml .forgejo/issue_template/technical-task.yml .forgejo/issue_template/user-story.yml .forgejo/workflows/documentation.yml .forgejo/workflows/release-beta.yml .forgejo/workflows/release-stable.yml CODE_OF_CONDUCT.md CONTRIBUTING.md SECURITY.md SUPPORT.md

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.
…260830084323

chore(sync): carry beta back into development
…0841

chore(sync): record beta's ancestry on development
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Release: merge development into beta
beta is the release candidate, 866 commits ahead of main. Every conflict
resolved to beta's side, including its refactors: where beta had removed a
file the removal stands rather than resurrecting a stale copy from main.

Conflicts: 0 (0 took beta's content, 0 removed per
beta's refactor).
Comment on lines +17 to +44
uses: ConductionNL/.github/.github/workflows/documentation.yml@main
# A reusable workflow receives NO secrets by default. Without this block the
# callee's publish step finds CF_API_TOKEN empty, skips itself on its own
# `if:` guard, and the run finishes GREEN having changed nothing -- the
# failure that left the fleet's docs sites on May builds. The names are the
# same on both sides; the org secrets really are CF_API_TOKEN/CF_ACCOUNT_ID.
secrets:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
with:
cname: learniq.conduction.nl

- name: List directory structure
run: |
ls -la
ls -la docusaurus/

- name: Setup Node.js 18
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies and build
timeout-minutes: 3
run: |
cd docusaurus
npm run ci

- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/development'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docusaurus/build
user_name: ${{ github.actor }}
user_email: ${{ github.event.pusher.email || github.actor }}

- name: Create issue on failure
if: failure()
uses: JasonEtco/create-an-issue@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
filename: .github/DOCUMENTATION_ISSUE_TEMPLATE.md
# scholiq.conduction.nl is the retired hostname. It stays in docs-hosts so
# existing links keep resolving, and canonical-host below turns it into a
# 301 rather than a second live copy of every page.
# EVERY host this worker answers on, in FULL: wrangler reconciles the
# worker's triggers against this list, so a host left out is REMOVED and
# goes dark.
docs-hosts: scholiq.conduction.nl,learniq.conduction.nl
# The ONE hostname this site is reached on. Every other host in
# docs-hosts answers 301 to the same path here. Before this, both hostnames
# served identical content and the retired name stayed as discoverable
# as the current one.
canonical-host: learniq.conduction.nl
# PINNED. Deriving the name is how a deploy goes green and reaches
# nobody: wrangler creates the derived worker and publishes there while
# the custom domains keep routing to the real one.
worker-name: scholiq-docs
Comment on lines +38 to +46
if: github.ref == 'refs/heads/development'
uses: ConductionNL/.github/.github/workflows/release.yml@main
with:
release-type: unstable
app-name: learniq
previous-app-id: scholiq
secrets: inherit

beta:
Comment on lines +47 to +55
if: github.ref == 'refs/heads/beta'
uses: ConductionNL/.github/.github/workflows/release.yml@main
with:
release-type: beta
app-name: learniq
previous-app-id: scholiq
secrets: inherit

stable:
Comment on lines +56 to +62
if: github.ref == 'refs/heads/main'
uses: ConductionNL/.github/.github/workflows/release.yml@main
with:
release-type: stable
app-name: learniq
previous-app-id: scholiq
secrets: inherit
Comment on lines +25 to +46
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 24, because this job runs `npm ci` and the lockfile is
# generated by npm 11. Node 20 and 22 both bundle npm 10, which
# cannot install from an npm 11 lock — it exits EUSAGE with
# "Missing: <pkg> from lock file". Node 24 bundles npm 11.
node-version: '24'
cache: 'npm'

- name: Install dependencies
run: npm ci --no-audit --no-fund

- name: Validate specs (json-strict + manifest + register)
run: npm run check:specs
git merged package.json and package-lock.json independently. Both merged
without conflict, yet the PAIR came out inconsistent, so `npm ci` refused:

  npm error `npm ci` can only install packages when your package.json and
  package-lock.json are in sync.
  npm error Missing: node-fetch@3.3.2 from lock file

On buildiq that failed the 'Install app npm dependencies' step, so the E2E
suite never ran at all and reported as a red E2E check rather than as
absent coverage.

A manifest and its lockfile are one artefact and must be taken from the
same side. beta wins here, as everywhere in this release.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ 1e3b0ec

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 648/648
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-30 13:26 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/learniq @ a398f47

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 648/648
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-30 14:22 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Superseded. A newer beta-to-main promotion for this repo has merged, and beta is now an ancestor of main with no content difference between them. The large file count shown on this pull request is the stale merge base, not content it still carries: diffing a promotion against its target measures the wrong thing.

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.

3 participants