refactor(rename): app_versions -> versioniq, with the app-id data migration - #187
Merged
Merged
Conversation
added 6 commits
August 22, 2026 17:16
Phase 3 of the fleet rename: the app id itself. `<id>`, the PHP namespace
(OCA\AppVersions -> OCA\Versioniq), the composer psr-4 root, the two `occ`
command prefixes and the IAppConfig namespace all move together, because
they are the same identifier as far as Nextcloud is concerned.
A rename IS a data migration. Nextcloud has no in-place app-id upgrade —
the new id is simply a different app, so every store keyed by app id goes
unreachable the moment `<id>` changes. Two repair steps carry them across,
registered FIRST under both <install> and <post-migration>:
- MigrateAppConfigKeys copies every oc_appconfig row. This app keeps
almost all its state there and every reader carries a default, so a
lost value silently reverts rather than erroring: trusted_sources is
the external-install allowlist, pin.{appId} is every version pin, and
policy.{appId} plus auto_update.* decide what the nightly sweep is
allowed to move. It enumerates getKeys() rather than a fixed list
because most keys are per-managed-app and therefore unbounded.
- MigrateUserPreferences copies oc_preferences by walking
callForSeenUsers() + getUserKeys(). Today's code writes no user value,
so this is close to a no-op — but getUsersForUserValue() would have
migrated nothing while reporting success, and the cost of the walk is
one install-time pass against an unbounded silent failure.
Both put every READ inside the try, not just the write. These run under
<install>, so a step that throws does not merely fail an upgrade: the app
never enables and every route goes with it.
FROZEN, with the reason written at each site:
- app_versions_pats / app_versions_audit table names. Table names are
not keyed by app id, so the rename never reached them and every PAT
and audit row is already where the new code looks. Renaming them would
need a real copy-and-verify migration for zero visible benefit, and a
shipped migration cannot be edited after the fact anyway.
- oc_activity / oc_notifications rows under the old id — Nextcloud owns
those tables and offers no supported re-key.
Left alone deliberately: getAppVersions() is English ("the versions of an
app"), not this app's id. The User-Agent moves to Nextcloud-Versioniq —
it is this app announcing itself, no counterparty stores or matches it.
The l10n domain (t('app_versions', …) -> t('versioniq', …)), every OCS
path under /apps/app_versions/api/, the SPA mount id shared by
templates/index.php and main.ts, the in-page skip-link anchor, the
package.json name that decides the built asset filename, and the display
name in every user-facing string.
l10n/nl.json moves with them: the Dutch entries are keyed on the English
SOURCE string, so leaving "App Versions" there after the source strings
became "Versioniq" would have silently unhooked five translations without
failing anything.
localStorage got the same treatment as oc_appconfig, for the same reason.
The three admin UI toggles (safe mode, debug mode, dry run) are persisted
per browser, and no server-side repair step can reach into a browser — so
after the rename `getItem('versioniq_safe_mode')` returns null and each
toggle silently reverts to its shipped default. An admin who deliberately
turned safe mode OFF would find it back ON with nothing to explain why.
Reads now fall back to the old key while writes only go to the new one,
and the old entries are left in place so a rollback still finds them.
npm run lint: 0 errors (39 pre-existing jsdoc warnings, untouched).
npm test: 11 files, 58 tests, all passing.
…rsioniq Regenerates openapi.json (route prefix follows the app id), repoints the release/quality app-name inputs at the new id, and refreshes the stale psalm baseline snippet that still quoted the old display name. Freezes the project-board app-name inputs on the old spelling: they are matched against a GitHub Project single-select option name, where a miss is a silent no-op.
Renames the remaining OCA\AppVersions namespace imports, the app-id literals in OCS URLs / occ config keys / notification queries, and the UI text assertions that must match the renamed source strings. Leaves oc_app_versions_pats and oc_app_versions_audit on the old prefix: table names are not keyed by app id and the rename does not touch them.
… to versioniq Renames the product name, occ prefixes, API paths and repo URLs across docs/ and the active openspec specs, and repoints leftover Codeberg links at GitHub. Rewrites MigrateUserPreferences against OCP\Config\IUserConfig: the IConfig user-value API it was written on is deprecated and left 7 psalm errors. The new version preserves each key's laziness, because getKeys() returns lazy and non-lazy keys alike while getValueString() reads only the mode it is asked for — copying everything as non-lazy would read a lazy value as empty and silently drop it. FROZEN, with reasons in-file: the docs subdomain (the new host does not resolve), the project-board app-name inputs, the oc_app_versions_* table names, and the conduction.nl product page.
Contributor
Quality Report — ConductionNL/versioniq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| composer | ✅ | ✅ 29/29 | |||
| npm | ✅ | ✅ 282/282 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 16:13 UTC
Download the full PDF report from the workflow artifacts.
gate-16: the two occ commands' configure() and orphanedMigrationsSummary() counted as changed (command names and the l10n domain moved with the app id) and carried no @SPEC. Both surfaces are genuinely specified, so they get real tags rather than excludes. gate-59: 'app_versions' reads as a config gate that is read and never written. Here the never-write IS the safety property — it is the previous app's id, and writing back under it would make the migration destructive. Suppressed with a reason; idempotency comes from the already-present check, not from closing the gate.
Contributor
Quality Report — ConductionNL/versioniq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| composer | ✅ | ✅ 29/29 | |||
| npm | ✅ | ✅ 282/282 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-22 16:31 UTC
Download the full PDF report from the workflow artifacts.
…nt the tag gate-16 reads @SPEC from the docblock immediately above the method, so moving the tag to a following line comment hid it from the gate. Put it back in the JSDoc and declare 'spec' to jsdoc/check-tag-names, so the linter no longer warns on the tag CI requires — the warning is what pushed the tag out of the docblock in the first place. Also clears 8 pre-existing check-tag-names warnings on the .vue files that already carried @SPEC (39 -> 31 warnings, still 0 errors).
Contributor
Quality Report — ConductionNL/versioniq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| composer | ✅ | ✅ 29/29 | |||
| npm | ✅ | ✅ 282/282 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-22 16:44 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 27, 2026
`runJob()` read `if (id) { … }`, so a missing `oc_jobs` row made the helper a
silent no-op. The job never ran, the test carried on, and the assertion that
followed failed with whatever the job should have produced. On
jobs.spec.ts:169 that surfaces as "drift recorded on the pin" — which reads
as a product bug in PinDriftHandler when it may simply mean PinReconcileJob
was never executed. Those need different fixes, so they must not wear the
same words.
The sibling test is worse off: `the reconcile job records no drift while the
installed version matches the pin` asserts an ABSENCE, so a job that never
runs makes it PASS. A silent no-op cannot fail that test at all.
The row can genuinely go missing. This app has moved its job classes twice --
`lib/Cron` into `lib/BackgroundJob` (#231) and the app_versions -> versioniq
rename (#187) -- and each move orphans the rows registered under the old
class string. RemoveRetiredCronJobs clears `OCA\Versioniq\Cron\*`; nothing
clears an `OCA\AppVersions\…` leftover.
So this also asserts the match is UNIQUE. `LIKE '%…%' LIMIT 1` with no ORDER
BY picks an arbitrary row, and an orphan sitting beside the live job could be
executed instead -- silently, because executing a job whose class no longer
exists does nothing observable.
This does not by itself fix the failing drift test: it makes that test say
which of the two things went wrong. Diagnosing the drift failure needs a live
instance, and the message it prints today points at the wrong layer.
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
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.
Finishes the fleet rename of this app from
app_versionsto Versioniq.What moved
<id>)app_versionsversioniqOCA\AppVersionsOCA\Versioniq/apps/app_versions/api/…/apps/versioniq/api/…occcommandsocc app_versions:versions/:installocc versioniq:versions/:installt('app_versions', …)t('versioniq', …)nextcloud/app_versions,app_versionsnextcloud/versioniq,versioniqapp-name(quality + release)app_versionsversioniqopenapi.jsonwas regenerated (composer openapi) rather than hand-edited, so the route prefix follows the app id from the source of truth.The data migration
Nextcloud has no in-place app-id upgrade: the new id is simply a different app, so every store keyed by app id becomes unreachable. Because every reader supplies a default, nothing errors — settings just silently revert. This app stores PATs and pins, so that matters.
Two repair steps carry the rows across, registered in
appinfo/info.xmlunder both<install>and<post-migration>, first in each block (anything writing fresh defaults must run after the copy, or the never-overwrite guard sees the fresh value and discards the admin's stored one):MigrateAppConfigKeys—oc_appconfig, enumerated viaIAppConfig::getKeys().MigrateUserPreferences—oc_preferences, enumerated by user viaIUserManager::callForSeenUsers()+IUserConfig::getKeys().Both are idempotent and non-destructive: a value is copied only when nothing is stored under the new id, and the old rows are never deleted, so a rollback still finds them.
MigrateUserPreferenceswas rewritten in this PR againstOCP\Config\IUserConfig— theIConfiguser-value API it was written on is deprecated and left 7 psalm errors. The rewrite also preserves each key's laziness:getKeys()returns lazy and non-lazy keys alike, butgetValueString()reads only the mode it is asked for, so copying everything as non-lazy would have read a lazy value as empty and dropped it silently — the exact failure this step exists to prevent.Enumeration is deliberately by user rather than by value: a value search needs the value up front and is exhaustive only for a closed value set, so against an open-valued key it migrates nothing while reporting success.
FROZEN — left on the old name, deliberately
Each of these is verified and commented in-file.
oc_app_versions_pats/oc_app_versions_audit. Table names are not keyed by app id, so the rename does not reach them. Renaming them would be a separate destructive migration for no gain. Constants inlib/Db/PatMapper.php/AuditEntryMapper.phpand all five migrations stay as-is.app-versions.conduction.nl. Verified 2026-08-22: the old host answers 200,versioniq.conduction.nldoes not resolve. Frozen indocs/static/CNAME,docs/docusaurus.config.js(url),.github/workflows/documentation.yml(cname:), and the absolute links indocs/static/llms.txt. Moves only when the new subdomain exists in DNS.conduction.nl/apps/app-versions. Old URL 200,…/versioniq404.app-nameinputs inopenspec-sync.ymlandissue-triage.yml. The shared workflow matches this against the name of an option on the org project board's "App" single-select field (appField.options.find(o => o.name === appName)); a miss is a silent no-op — the issue still lands on the board, the field is just left unset. Note the value is already neither app id (the old id used an underscore) — it is the old repo name. Needs the board option renamed in GitHub, in the same change.app-versions-docs(.forgejo/workflows/documentation.yml) and the matchingdocs/package.jsonname — deployment identity of the frozen docs site.OLD_APP_ID = 'app_versions'constants in both repair steps, and the old localStorage keys insrc/App.vue— these are the migration sources; they are supposed to say the old name.occ versioniq:install openconnector 1.2.0indocs/cli.md. Cross-app ids are duck-typed runtime lookups that fail silently when wrong; they move in a coordinated pass, not here. This app is about other apps' versions, so it contains many of these.getAppVersions()/appVersions()/operationId: api-app-versions— domain vocabulary ("the versions of an app"), not the app id. The openapi generator itself re-emitsapi-app-versionsfrom the method name.Consequence worth flagging
app-nameinrelease.ymlhad to move, because the packaged folder must equal<id>or the tarball will not install. That means releases now publish under a new App Store id; the old listing does not follow automatically.Verification
composer check:strict— ALL CHECKS PASSED (lint, php-cs-fixer, psalm 0 errors)composer test:unit-only— 565 tests, 1172 assertions, OKnpm run lint— 0 errors (39 pre-existing JSDoc warnings)npm run test— 11 files, 58 tests passedFixed along the way: a stale
psalm-baseline.xmlsnippet still quoting the old display name (which would have surfaced the baselined error as new), the rootpackage-lock.jsonname left out of step withpackage.json, and php-cs-fixer drift in 4 files.