Skip to content

refactor(rename): app_versions -> versioniq, with the app-id data migration - #187

Merged
rubenvdlinde merged 8 commits into
developmentfrom
feat/rename-app-versions-to-versioniq
Aug 22, 2026
Merged

refactor(rename): app_versions -> versioniq, with the app-id data migration#187
rubenvdlinde merged 8 commits into
developmentfrom
feat/rename-app-versions-to-versioniq

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Finishes the fleet rename of this app from app_versions to Versioniq.

What moved

Thing Before After
Nextcloud app id (<id>) app_versions versioniq
PHP namespace OCA\AppVersions OCA\Versioniq
Display name App Versions Versioniq
OCS routes /apps/app_versions/api/… /apps/versioniq/api/…
occ commands occ app_versions:versions / :install occ versioniq:versions / :install
l10n domain t('app_versions', …) t('versioniq', …)
composer / npm package nextcloud/app_versions, app_versions nextcloud/versioniq, versioniq
CI app-name (quality + release) app_versions versioniq

openapi.json was 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.xml under 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):

  • MigrateAppConfigKeysoc_appconfig, enumerated via IAppConfig::getKeys().
  • MigrateUserPreferencesoc_preferences, enumerated by user via IUserManager::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.

MigrateUserPreferences was rewritten in this PR against OCP\Config\IUserConfig — the IConfig user-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, but getValueString() 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.

  1. DB tables 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 in lib/Db/PatMapper.php / AuditEntryMapper.php and all five migrations stay as-is.
  2. Docs subdomain app-versions.conduction.nl. Verified 2026-08-22: the old host answers 200, versioniq.conduction.nl does not resolve. Frozen in docs/static/CNAME, docs/docusaurus.config.js (url), .github/workflows/documentation.yml (cname:), and the absolute links in docs/static/llms.txt. Moves only when the new subdomain exists in DNS.
  3. Product page conduction.nl/apps/app-versions. Old URL 200, …/versioniq 404.
  4. Project-board app-name inputs in openspec-sync.yml and issue-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.
  5. Cloudflare Pages project app-versions-docs (.forgejo/workflows/documentation.yml) and the matching docs/package.json name — deployment identity of the frozen docs site.
  6. OLD_APP_ID = 'app_versions' constants in both repair steps, and the old localStorage keys in src/App.vue — these are the migration sources; they are supposed to say the old name.
  7. Other apps' ids appearing as data — e.g. occ versioniq:install openconnector 1.2.0 in docs/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.
  8. getAppVersions() / appVersions() / operationId: api-app-versions — domain vocabulary ("the versions of an app"), not the app id. The openapi generator itself re-emits api-app-versions from the method name.

Consequence worth flagging

app-name in release.yml had 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:strictALL CHECKS PASSED (lint, php-cs-fixer, psalm 0 errors)
  • composer test:unit-only565 tests, 1172 assertions, OK
  • npm run lint0 errors (39 pre-existing JSDoc warnings)
  • npm run test11 files, 58 tests passed

Fixed along the way: a stale psalm-baseline.xml snippet still quoting the old display name (which would have surfaced the baselined error as new), the root package-lock.json name left out of step with package.json, and php-cs-fixer drift in 4 files.

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

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/versioniq @ d8cc2af

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

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/versioniq @ 720cb54

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

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/versioniq @ 1c9aadc

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
rubenvdlinde merged commit 7169f55 into development Aug 22, 2026
40 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/rename-app-versions-to-versioniq branch August 22, 2026 17:31
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>
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.

1 participant