feat(hydra-gates): gate 94 — retired-git-host-metadata - #546
Merged
Conversation
ConductionNL moved onto Codeberg 2026-05-29 and moved back off 2026-07-23. The git layer completed — verified 2026-08-22, zero codeberg remotes fleet-wide. The shipped metadata did not. 23 apps still declare <bugs>, <website>, <repository> and <screenshot> URLs pointing at codeberg.org/Conduction/<app> in appinfo/info.xml. That file is published to the Nextcloud app store, so this is user-visible: <bugs> sends a bug reporter to a tracker nobody reads, and <screenshot> 404s in the listing. Gate 94 is full-tree, deliberately NOT diff-scoped — the same reasoning gates 84 and 93 give, but sharper here. The drift this gate exists to catch is already in the tree, put there by a migration that touched every repo at once. A diff-scoped version would report nothing on the ~99% of PRs that never open appinfo/info.xml, i.e. it would be blind to 100% of the debt it was written for. Scope is shipped metadata only: appinfo/info.xml (root, since an NC app ships exactly one) plus package.json / composer.json, and within those only the fields carrying a URL a user follows. Docs, changelogs, specs, learnings and archived openspec changes are out of scope — they record what was true when written, and a gate noisy enough to be switched off catches nothing. custom_apps/ and friends are skipped: a dev environment drops checkouts of other apps inside a repo, and walking them made the gate report openregister as having 69 findings, 63 of which belonged to seven other apps. A gate that blames a repo for a neighbour's metadata teaches people to ignore it. Verified: procest → FAIL (9), nextcloud-vue → PASS, hydra → SKIP(na).
This was referenced Aug 21, 2026
rubenvdlinde
added a commit
that referenced
this pull request
Aug 22, 2026
Gate 94 landed in #546 without a fixture bundle, and main's "Package invariants" job is red because of it: the coverage ratchet in test_gate_acceptance_matrix.sh is a hard failure for a gate declared in the runner with neither a bundle nor an UNCOVERED.md row — deliberately, so a gate cannot land untested in silence. This is that bundle. The two arms differ ONLY in the host the shipped URLs point at, and planted/ reproduces the fleet's actual 2026-08-22 state: <bugs>, <website>, <repository> and <screenshot> all still on codeberg.org a month after the last codeberg remote was removed. Two shapes are pinned on purpose: - Both arms carry a prose mention of "codeberg" in <description>. Gate 94 is scoped to URL-bearing FIELDS, never to the file's prose — a scanner that grepped the whole file would fail the clean/ arm on that line, and a gate that flagged every historical mention would be noisy enough to switch off. - Both arms carry an identical <screenshot> PATH, so a checker keyed on the path rather than the host would misgrade the pair. The subject substring is `codeberg.org`, not a field name: the finding is "this URL points at a retired host" and the URL is what a human needs to see. A gate reporting only a count would satisfy a nonzero-exit check for free while naming nothing. Verified locally: acceptance matrix 181 passed / 0 failed, gate 94 now in the covered set (67 of 74 gates fixtured). Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
pushed a commit
to ConductionNL/openregister
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, added in ConductionNL/.github#546) flags six shipped URLs here: website, bugs, repository and three screenshots, all still pointing at codeberg.org. GitHub is the only host; Codeberg is retired. The screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and all three were checked rather than assumed — HTTP 200 each. A green gate with dead image URLs would be worse than the finding. Not caused by this branch's lock bump: gate-94 landed at 02:18 UTC and this app's last development run was 01:51 UTC, so the PR runs are simply the first to be measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/filinq
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags nine shipped URLs here — website, bugs, repository, three screenshots and three markdown links in the description. Two things had to change together, and only one of them is the host: codeberg.org/Conduction/DOCUDESK -> github.com/ConductionNL/FILINQ The Codeberg URLs still carry the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/docudesk — which resolves today purely because GitHub redirects a renamed repo, and stops resolving the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and all three were fetched rather than assumed — HTTP 200 each. Not caused by this branch's lock bump: gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it.
rubenvdlinde
pushed a commit
to ConductionNL/dossiq
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/procest -> github.com/ConductionNL/dossiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/procest, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/integriq
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/openconnector -> github.com/ConductionNL/integriq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/openconnector, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/launchpad
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/launchpad -> github.com/ConductionNL/launchpad The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/launchpad, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/zaakafhandelapp
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/zaakafhandelapp -> github.com/ConductionNL/zaakafhandelapp The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/zaakafhandelapp, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/stackiq
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/softwarecatalog -> github.com/ConductionNL/stackiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/softwarecatalog, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
pushed a commit
to ConductionNL/thematiq
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/nldesign -> github.com/ConductionNL/thematiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/nldesign, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible.
rubenvdlinde
added a commit
to ConductionNL/dossiq
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/procest -> github.com/ConductionNL/dossiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/procest, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/filinq
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(info.xml): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags nine shipped URLs here — website, bugs, repository, three screenshots and three markdown links in the description. Two things had to change together, and only one of them is the host: codeberg.org/Conduction/DOCUDESK -> github.com/ConductionNL/FILINQ The Codeberg URLs still carry the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/docudesk — which resolves today purely because GitHub redirects a renamed repo, and stops resolving the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and all three were fetched rather than assumed — HTTP 200 each. Not caused by this branch's lock bump: gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/integriq
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/openconnector -> github.com/ConductionNL/integriq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/openconnector, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/launchpad
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/launchpad -> github.com/ConductionNL/launchpad The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/launchpad, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/openregister
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(info.xml): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, added in ConductionNL/.github#546) flags six shipped URLs here: website, bugs, repository and three screenshots, all still pointing at codeberg.org. GitHub is the only host; Codeberg is retired. The screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and all three were checked rather than assumed — HTTP 200 each. A green gate with dead image URLs would be worse than the finding. Not caused by this branch's lock bump: gate-94 landed at 02:18 UTC and this app's last development run was 01:51 UTC, so the PR runs are simply the first to be measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/stackiq
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(psalm): stub OpenRegister's contract, which v1.9.0 stopped autoloading hydra-gates v1.9.0 removed `OCA\OpenRegister\Contract\` from its runtime psr-4 autoload (ConductionNL/.github#531). The removal was correct — that prefix is longer than openregister's own, so a vendored copy in ANY app defined the contract for the whole process — but it was verified against PHPUnit only. Psalm never runs the test bootstrap; it resolves types through the composer autoload map. So the guarded require added to this app's bootstrap does nothing for it, and 213 UndefinedClass errors appeared for a class the app typehints but does not own. This is the same situation as the decidesk event stubs already in this file: a sibling Nextcloud app supplies the type at runtime, so it is absent from the analysis path and gets declared here. A stub teaches the analyser the shape without putting the class back into the runtime autoloader, which is what caused the original defect. Measured in this checkout on the real v1.9.0: 213 errors before, 0 after — "No errors found!", psalm exit 0. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/softwarecatalog -> github.com/ConductionNL/stackiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/softwarecatalog, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/thematiq
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/nldesign -> github.com/ConductionNL/thematiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/nldesign, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/zaakafhandelapp
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/zaakafhandelapp -> github.com/ConductionNL/zaakafhandelapp The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/zaakafhandelapp, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
pushed a commit
to ConductionNL/nextcloud-app-template
that referenced
this pull request
Aug 22, 2026
… host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags seven shipped URLs here: the three <documentation> links, website, discussion, bugs, repository and the app-store screenshot. GitHub is the only host. This repo keeps its name, so unlike the renamed apps only the host changes. The screenshot moves to raw.githubusercontent.com rather than a github.com/raw redirect, and was fetched rather than assumed — HTTP 200. Worth fixing here above all: this is the template every new app is scaffolded from, so the Codeberg URLs were being copied forward into each new app's metadata.
rubenvdlinde
added a commit
to ConductionNL/nextcloud-app-template
that referenced
this pull request
Aug 22, 2026
* chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(info.xml): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags seven shipped URLs here: the three <documentation> links, website, discussion, bugs, repository and the app-store screenshot. GitHub is the only host. This repo keeps its name, so unlike the renamed apps only the host changes. The screenshot moves to raw.githubusercontent.com rather than a github.com/raw redirect, and was fetched rather than assumed — HTTP 200. Worth fixing here above all: this is the template every new app is scaffolded from, so the Codeberg URLs were being copied forward into each new app's metadata. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> Co-authored-by: Conduction <dev@conduction.nl>
rubenvdlinde
added a commit
to ConductionNL/dossiq
that referenced
this pull request
Aug 22, 2026
…ow the moves GATE 94 CAUGHT THIS REPO. The gate added in ConductionNL/.github#546 failed on this PR with 9 shipped URLs pointing at Codeberg — procest's own appinfo/info.xml. Worth recording why the fleet sweep missed it: that sweep read each repo's DEFAULT branch, and procest's default is `main`, which had already been repointed. `development` had not. So the app store metadata was correct on one branch and stale on the branch everything is built from — exactly the split the gate exists to find, and it found it on the change that introduced it. Repointed the same way the fleet sweep does: repo name resolved from the API (procest -> ConductionNL/dossiq, renamed 2026-08-21) and every asset URL probed until one returns 200, rather than substituting the domain and hoping. `main`'s version was NOT copied — it carries a different, single-screenshot set. E2E, three scenarios following surfaces this change moved: - Doorlooptijd's heading is the dashboard page's title now ("Processing time"), not the retired component's <h2>Processing Time Analytics</h2>. The old wording survives as the subtitle, which the same spec already asserts. - The two Settings scenarios are retargeted from the retired in-app /apps/procest/settings at /settings/admin/procest. One of them carried FIXME(#719) saying the in-app page rendered only section chrome and that "the same components do render on /settings/admin/procest" — retiring that page resolves the FIXME rather than working around it, so the test is un-fixme'd. gate-94 verified locally: 0 failures across 5 shipped metadata files.
rubenvdlinde
added a commit
to ConductionNL/dossiq
that referenced
this pull request
Aug 22, 2026
…st side) (#1328) * refactor(ai): delegate human-oversight decisions to hermiq (E1, procest side) Hermiq owns AI oversight; procest owns the moment a handler accepts, rejects or corrects a suggestion. This is procest's half of E1 — it lands AFTER hermiq #514, which added the advisory Approval variant and the event contract this dispatches. WHAT MOVES, AND WHAT DELIBERATELY DOES NOT. procest's audit log holds two kinds of entry: `action: suggestion`, which records that the model RAN, and `userAction: accepted|rejected|modified`, which records that a human JUDGED it. Only the second is EU AI Act Art. 14 evidence. Sending the first would put rows in hermiq's oversight log that nobody decided, so suggestion entries stay in procest's own log. VOCABULARY IS TRANSLATED, NOT FLATTENED. procest says `modified`; hermiq says `overridden`. Getting that mapping backwards would file every correction as a rejection, which is the one distinction an oversight audit most needs. THE LOCAL WRITE STAYS. Delegation is additive, not a replacement: on an instance without hermiq the local copy is the only one, and dropping it would make the existence of an oversight trail depend on which apps happen to be installed. `recordUserAction()` now also returns `delegated`, so the caller can tell "recorded centrally" from "local only" — those look identical otherwise. DELEGATION NEVER FAILS THE HANDLER. Unlike ContractDecisionDelegationService, which fails closed because it GATES work, this records something that already happened: the handler has acted and the case has moved on. Refusing after the fact would turn an audit outage into a functional one. MIGRATION RUNS THROUGH THE SAME PATH. MigrateAiOversightToHermiq replays existing decisions through the identical event, so old evidence travels exactly as new evidence does and hermiq never reads procest's register. Idempotent (stable externalRef per source entry) and non-fatal — an upgrade must not fail because an audit projection could not complete, and the local copies remain to replay from. Retires `/settings/ai-oversight` and `/settings/ai-oversight/:id`, replacing the menu entry with a deeplink to `/apps/hermiq/ai-oversight` (hermiq is history-routed like procest, unlike OpenRegister which needs the `#/` form — verified, not assumed). Also records C2 and D1–D4 as DEFERRED in tasks.md: the fleet is mid-way through consolidating the flow engine into OpenRegister (ADR-065), and re-expressing automatic actions or decidesk's approval routes against a moving target would have to be redone. Tests: AiOversightDelegationServiceTest 10/10 covering each vocabulary mapping, the suggestion-only refusal, document-vs-case subject resolution, the no-subject refusal, map-value rendering, replay idempotency and the hermiq-absent path. Adds tests/Stubs/Hermiq so the by-name contract is actually exercised. Full suite 2313 tests green. psalm 0 errors. check:manifest Ajv PASS (50 pages). l10n and prettier clean. Live-verified: /apps/procest/settings/ai-oversight no longer resolves; exactly one nav entry, pointing at /apps/hermiq/ai-oversight, which returns 200. * fix(quality): close the three Hydra gate failures — and a real regression they exposed CI ran gate-16, gate-26 and gate-60 against #1323. All three were genuine, and gate-26 was pointing at something worse than a missing baseline. gate-26 REVEALED FOUR DELETIONS THAT SILENTLY CAME BACK. It flagged DoorlooptijdDashboard.vue and TenantOnboardingDashboard.vue as "new page components" — files this change had DELETED. They were deleted in A3 and B3, and then re-added by the F1 commit: `git add -A src/` re-staged them because they were still sitting on disk. ProcessMiningDashboard.vue and TermijnDashboard.vue came back the same way. All four were dead — nothing imports them, the manifest routes to their widget replacements — so the app was correct and the repository was not. Deleted again, and this is exactly the kind of thing that survives every green check except a gate that asks "what is new here". gate-16: 66 changed methods without @SPEC, now 0. The widget components, the mixins and the three stores each carried only a file-level tag; every computed, action, getter and lifecycle hook now points at the capability spec. Four members had no docblock at all and got one. gate-60: `ShieldAccountOutline` on the AVG link is not registered in src/icons.js, so it rendered as NO icon rather than a fallback. Replaced with `ShieldLockOutline`, which is both registered and ADR-077's `privacy` concept — the AVG processing register is privacy, not administration. The first attempt at that fix was WRONG and the gate caught it: it also changed the admin link from `CogOutline` to `ShieldAccountOutline`, reading ADR-077's "admin" row. Running the checker locally showed ADR-077 Tier A requires `CogOutline` for the `settings` concept, and that the icon swapped in was itself unregistered. Reverted; the lesson is that the ADR table has two rows that look applicable and only the checker knows which one governs a given label. Also removes an orphaned comment fragment the C1 visual-spec edit left behind. All three gates verified locally against the real checkers from ConductionNL/.github: spec-coverage count=0, visual-coverage PASS (7 new pages, all with a visual proof), icon-vocabulary 0 failures. eslint 0 errors, prettier clean, l10n en/nl in sync, check:manifest Ajv PASS, phpunit 2313 green. * fix(ai): the oversight replay read the wrong key — it would have migrated nothing phpstan caught a silent no-op before it ever ran. MigrateAiOversightToHermiq read `$batch['results']`, but AiAuditLog::list() returns `{entries, total, limit, offset}`. The key never existed, so `?? []` handed back an empty array on every page, the loop broke on the first iteration, and the step would have reported "AI oversight replay: no audit entries to consider" on an instance full of them — while the upgrade went green and the evidence stayed behind. This is the exact failure shape this change set has been finding all along: a default-valued read turning missing data into confident wrong behaviour. Fixed to `$batch['entries']`, and read WITHOUT a `??` fallback so a future shape change fails loudly instead of quietly returning nothing. MigrateAiOversightToHermiqTest pins it: an entries-bearing log must report "1 decision(s) sent", not "no audit entries". Also closes the other CI findings on this PR: - phpcs: missing @PARAM for the new AiAuditService dependency, three internal calls needing named arguments, and a missing @SPEC on getName(). - phpmd: run() was doing paging AND per-entry handling AND reporting (cyclomatic 11, NPath 219, an else clause). Split into replayPage() and report(); the gate was right that two concerns were tangled. phpcs 0 errors across lib/, phpmd clean, phpstan [OK] No errors, phpunit 2327 tests green. * fix: gate-94 catches procest's own info.xml, and three e2e specs follow the moves GATE 94 CAUGHT THIS REPO. The gate added in ConductionNL/.github#546 failed on this PR with 9 shipped URLs pointing at Codeberg — procest's own appinfo/info.xml. Worth recording why the fleet sweep missed it: that sweep read each repo's DEFAULT branch, and procest's default is `main`, which had already been repointed. `development` had not. So the app store metadata was correct on one branch and stale on the branch everything is built from — exactly the split the gate exists to find, and it found it on the change that introduced it. Repointed the same way the fleet sweep does: repo name resolved from the API (procest -> ConductionNL/dossiq, renamed 2026-08-21) and every asset URL probed until one returns 200, rather than substituting the domain and hoping. `main`'s version was NOT copied — it carries a different, single-screenshot set. E2E, three scenarios following surfaces this change moved: - Doorlooptijd's heading is the dashboard page's title now ("Processing time"), not the retired component's <h2>Processing Time Analytics</h2>. The old wording survives as the subtitle, which the same spec already asserts. - The two Settings scenarios are retargeted from the retired in-app /apps/procest/settings at /settings/admin/procest. One of them carried FIXME(#719) saying the in-app page rendered only section chrome and that "the same components do render on /settings/admin/procest" — retiring that page resolves the FIXME rather than working around it, so the test is un-fixme'd. gate-94 verified locally: 0 failures across 5 shipped metadata files. * fix(dashboard): the straplines were declared under a key nothing reads CnDashboardPage's prop is `description`, not `subtitle`. The manifest schema accepted `subtitle`, Ajv passed, and all three converted dashboards rendered NO strapline — the components they replaced each drew one, so the conversion silently dropped it. A declaration that validates and does nothing. The e2e suite is what caught it: the Doorlooptijd scenario asserts its heading AND its strapline, the heading fix landed, and the strapline assertion then failed on text that was never rendered. Worth noting the shape — an assertion that survived a rewrite is what found a regression the rewrite introduced. Verified live: `.cn-dashboard-page__description` now reads "SLA adherence and processing time analysis". Also fixes the Settings scenario's strict-mode violation. Retargeting it at /settings/admin/procest worked — and that surface renders EVERY section, four of which have their own labelled save ("Save mandate matrix settings", "Save consultation settings", …). The retired in-app page rendered only chrome, so a loose "Save" matched exactly one button there. Now matched with `exact: true`, which is the Configuration section's own control. check:manifest Ajv PASS, prettier clean. * fix(dashboard): restore the back-to-Dashboard control the conversion dropped Third and last thing the Doorlooptijd e2e scenario caught. The component this replaced drew an explicit back-arrow to the Dashboard; the dashboard page type draws no such control, so converting the page silently removed it. The spec asserted it, which is how it surfaced — the affordance is restored as a declarative `config.headerActions` navigate action rather than deleting the assertion. The icon took two tries and the gate was right both times. `ArrowLeft` mirrors what the old component drew, but it is not registered in src/icons.js — it would have rendered as NO icon, not a fallback — and ADR-077 Tier A requires `ViewDashboardOutline` for the `dashboard` concept anyway. The action's DESTINATION names it, not the direction of travel. Live-verified: heading "Processing time", description "SLA adherence and processing time analysis", and one "Dashboard" button — the three things the scenario asserts, in one page load. icon-vocabulary 0 failures, check:manifest Ajv PASS. * docs(openspec): resume plan for C2/D now the flow engine is consolidated The flow-engine consolidation has landed, so C2 and D1-D4 are no longer blocked. PLAN-RESUME.md carries the state, the step order and the traps, written to survive a context compaction rather than living in a conversation. The thing worth reading before starting C2: `automaticAction` is NOT a standalone object. `caseType.workflowSteps` embeds references to it in three places (`automaticActions`, `config.autoActions`, `config.escalationRule`), so retiring the two pages does not retire the concept — every case type in the field points at these by id, and a migration has to rewrite those references or keep them resolvable. That has to be established before the spec, not after. D1 keeps one gate the flow engine did not open: it removes the two besluitvorming routes that the active consume-decidesk-besluitvorming-leaf change deliberately keeps alive, so it lands after that one, not beside it. Also records the merge order for the open stack — #1328 into #1323, then #1323 into development, because #1323 alone still fails gate-16 and gate-60 and those fixes live on #1328.
rubenvdlinde
added a commit
to ConductionNL/thematiq
that referenced
this pull request
Aug 26, 2026
* chore(quality): migrate to PHPStan 2 (and hydra-gates 1.8.2) (#378) Half the fleet moved to PHPStan 2 earlier; this app did not. Bringing it into line, which for nldesign is only the constraint: phpstan/phpstan ^1.10 -> ^2.0 conduction/hydra-gates ^1.0 -> ^1.8.2 Zero source changes were needed. PHPStan 2 finds no errors here at all, which is worth stating explicitly because it is the exception — the nine apps migrated in the earlier pass needed ~231 fixes between them, mostly `alreadyNarrowedType`, `identical.alwaysTrue` and `arrayValues.list` findings that only PHPStan 2 reports. hydra-gates moves with it deliberately, not incidentally: the two are coupled in every already-migrated app (^2.0 alongside ^1.8.2), and leaving the gates package behind is what produced the phpcs-4 confusion elsewhere today. Verified locally ---------------- phpstan exit 0 — No errors phpcs exit 0 psalm exit 0 phpmd exit 0 PHPUnit cannot run in a standalone clone here — it dies at `Class "OC\Mail\EMailTemplate" not found` (lib/Mail/NLDesignEMailTemplate.php:48) because the suite needs the Nextcloud server classes. Confirmed pre-existing rather than introduced: the identical failure occurs with the composer change stashed and the old constraints reinstalled. CI runs the suite inside a real Nextcloud tree, so it is measured there. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * chore(deps): refresh the shared Conduction locks (#376) * chore(deps): refresh the shared Conduction locks hydra-gates v1.8.1 -> v1.8.2 nc-vue 2.9.2 -> 2.9.2 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * chore(deps): nc-vue 2.9.2 -> 2.10.1 2.9.2/2.10.0 carry a CnDashboardPage regression: an `object-table` dashboard widget canonicalised to `table` and rendered the wrong component (ConductionNL/nextcloud-vue#722). 2.10.1 is the fix. Lock-only; added 0, removed 0, dev-flag changes 0. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * fix(ci): give dispatched runs their own concurrency lane (#382) The group suffixed only pushes, leaving workflow_dispatch sharing quality-development with the Sync-to-Beta PR (whose head_ref IS development and which re-triggers on every merge). A dispatched run was therefore cancelled by it — measured on shillinq: dispatch 32487948678 cancelled by pull_request run 32490160836. This blocks the fleet gate-drift sweep (.github#523), which dispatches per app with --ref development because schedule: cannot choose a branch. Under the old group those runs report neither pass nor fail. Adopts hermiq's form verbatim, already live there. * chore(deps): refresh the shared Conduction locks (#384) * chore(deps): refresh the shared Conduction locks hydra-gates v1.8.2 -> v1.9.0 nc-vue 2.10.1 -> 2.11.1 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. * fix(metadata): point shipped URLs at GitHub, not the retired Codeberg host gate-94 (retired-git-host-metadata, ConductionNL/.github#546) flags the shipped URLs in this app's metadata — website, bugs, repository and screenshots — as pointing at codeberg.org. GitHub is the only host. Two things changed together, and only one of them is the host: codeberg.org/Conduction/nldesign -> github.com/ConductionNL/thematiq The Codeberg URLs still carried the app's PRE-RENAME name. Swapping only the host would have produced github.com/ConductionNL/nldesign, which resolves today purely because GitHub redirects a renamed repo — and stops the moment anyone creates a repo at the old path. Screenshots move to raw.githubusercontent.com rather than a github.com/raw redirect, and every one was fetched rather than assumed: HTTP 200 each. A green gate with dead image URLs would be worse than the finding it silenced. Not caused by this branch's lock bump — gate-94 landed at 02:18 UTC, after this app's last development run, so the PR runs are simply the first measured against it. Pre-existing debt, newly visible. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * feat: rename the app id from nldesign to thematiq, freezing the design system (#386) * wip(rename): app id nldesign -> thematiq in the manifest, composer and package names The design-system names (--nldesign-* tokens, nldesign-* classes, the nldesign design-system id, nldesignsystem.nl) are NOT part of this rename. * wip(thematiq): checkpoint in-flight rename work before session limit * feat: rename the app id from nldesign to thematiq, freezing the design system This app IMPLEMENTS the NL Design System, so most occurrences of "nldesign" in it are the external standard, not this app's id. Only the id moved. MOVED: <id>, <namespace> and OCA\NLDesign -> OCA\Thematiq across 111 files, display name, composer/package names (both lockfiles refreshed), the l10n domain, /apps/nldesign routes, getAppPath('nldesign'), APP_ID, occ prefix, and the two test artifacts named after the app. FROZEN -- the NL Design System is owned by the Dutch government and its names are matched BY STRING against stylesheets in every other app in the fleet and in @conduction/nextcloud-vue. Renaming one here does not rename it there: the selector simply stops matching and the theme silently falls back to default styling, with nothing in any log. - the nldesign-* CSS custom properties and class/DOM-id prefixes (nldesign-color-primary, nldesign-font-family, nldesign-theme, ...) - the nldesign design-system id in design-systems.json, nldesignsystem.nl, and the nl-design-system/themes upstream - class names that NAME the standard: NLDesignEMailTemplate, NlDesignThemeShareableConfigType - openspec/changes/archive/** and every @SPEC capability path Also fixed: appinfo/info.xml did not parse. The explanatory comment contained , and a double hyphen is illegal inside an XML comment, so the file was malformed and the app could not have installed. Rephrased. Two docblocks were damaged by an over-eager rewrap of over-long @return lines and are restored; the rewrap now splits type from description on brace depth, so a type containing spaces stays whole. Local: phpcs 0, psalm clean, l10n OK, prettier 0, no deletions against development. phpstan reports 11 OCA\Theming class.notFound errors -- Nextcloud's own Theming app, absent from a bare clone and present in CI's container; identical on development. * fix: repoint the phpstan baseline and the CI app-name Two path/message-keyed references the sweep did not reach. phpstan-baseline.neon matches by ERROR MESSAGE, and those messages embed the namespace. After OCA\NLDesign -> OCA\Thematiq the six baselined entries stopped matching, so long-baselined OCA\Theming class.notFound errors resurfaced as new ones. Same shape as an eslint-suppressions or phpmd baseline keyed by file path. .github/workflows/{code-quality,issue-triage}.yml still declared 'app-name: nldesign', so CI would check the app out under the OLD id while info.xml declares the new one. documentation.yml's 'cname: nldesign.conduction.nl' stays: that host is live and thematiq.conduction.nl is not. sync-tokens.yml's chore/sync-nldesign-tokens branch stays too — it syncs NL Design System tokens, so it names the standard. phpstan: OK. * fix(tests): imagePath takes the APP id, getDesignSystem takes the STANDARD EmailThemingServiceTest mocked imagePath() with 'nldesign'. That argument is the APP id -- production passes Application::APP_ID -- so after the rename the mock matched nothing and the service returned null for the logo URL. CssInjectionServiceTest's getDesignSystem('nldesign') is deliberately NOT touched: that argument is the design-system id, an external standard, and it does not move. The same word, one line apart, with opposite verdicts -- decided by which system answers to the name. * fix(e2e): app-id-derived URLs and initial-state ids follow the rename Three admin-settings scenarios read Nextcloud's initial-state inputs, whose DOM id is 'initial-state-<APP ID>-<key>'. The app id moved, so the locator matched nothing. Three per-app-theming scenarios counted stylesheets whose href contains '/nldesign/'. CssInjectionService calls Util::addStyle(application: Application::APP_ID, ...), so the served path is now /apps/thematiq/css/... and the count was always zero. Both are app-id-DERIVED strings, so both move. The nldesign-* DOM ids and class prefixes in the same files are NOT touched -- those are the design system's, and they are what the markup still emits. * fix(e2e): the same app-id-derived stylesheet path in three more specs dark-mode and dark-mode-scope filter stylesheet hrefs on '/nldesign/' exactly as app-theming did. Same cause, same fix -- the served path is /apps/thematiq/ now. Caught by grepping for the pattern rather than waiting for the next CI round. * fix(newman): the postman PATH array is what Postman actually requests Newman failed with 'expected status 200 but got 404' on every request. The collection's raw URLs had been rewritten to /apps/thematiq, but all 46 "path" ARRAYS still carried the "nldesign" segment -- and Postman builds the request from the array, not from raw. So the diff showed the new route while the runner asked for the old one. Exactly the discrepancy already recorded for dossiq's collection (raw said /dwangsom/, path said penaltyPayment). Rewritten inside the path arrays only, so nothing else in the collection is touched. Note for the record: the same run's 'info.xml lint' red is INFRASTRUCTURE, not this app -- the job's own error says libxml2-utils could not be installed after 3 attempts because the Ubuntu mirror was unreachable. * fix: templates/ and js/ were never swept — the settings UI loaded nothing Every settings E2E failed, including specs this rename never touched (custom-css-overrides, custom-token-set-upload). One cause: templates/settings/admin.php script('nldesign', 'admin'); style('nldesign', 'admin'); The FIRST argument to Nextcloud's script()/style() is the APP ID. After the rename the page asked for /apps/nldesign/js/admin.js, which no longer exists, so the settings page rendered with no JavaScript at all and every interactive assertion failed. js/ was never swept either: 123 t('nldesign', …) calls and 39 /apps/nldesign/... fetch URLs — the whole settings UI's l10n domain and its API endpoints. Both directories were simply absent from my sweep's file list, which covered lib/ src/ tests/ appinfo/ docs/ .github/. This app keeps its hand-written JavaScript in js/ (tracked, not build output) and its markup in templates/, so the sweep missed the entire front end. The info.xml comment I wrote for this app names 'script()/style()/loadState() channels' as app-id-derived. Naming the rule is not the same as applying it to every directory. FROZEN and untouched, as before: the nldesign-* DOM ids and class prefixes in the same template (the design system's, and what the markup still emits), and a CSS comment recording a historical 404 URL. Verified: decidiq's js/ is gitignored build output, and buildiq/stackiq have no templates/ or js/ occurrences — this miss is thematiq-specific. * fix: Nextcloud route names are <appid>.<controller>.<method> CssInjectionService and FontService resolved URLs with linkToRoute('nldesign.font.css') and linkToRoute('nldesign.font.serve'). A Nextcloud route name is prefixed by the APP ID, so after the rename those routes are registered as thematiq.font.* and the old names resolve to nothing. That is why the dark-mode-scope specs failed their own precondition -- 'a tokens/dark/* stylesheet must be loaded, otherwise these assertions cannot distinguish scoped correctly from never loaded'. The stylesheet link was never emitted, so there was nothing to assert against. That precondition is well written: it refused to pass silently on an absent stylesheet, which is exactly how this surfaced as a real failure instead of three green no-op tests. Swept for other <appid>.<x>.<y> route names: these two were the only ones. * fix(appinfo): order repair-steps children as info.xsd requires `quality / info.xml lint` failed with: appinfo/info.xml:168: element post-migration: Schemas validity error : Element 'post-migration': This element is not expected. Expected is ( uninstall ). The message points at post-migration, but nothing is wrong with that element — the fault is that it comes second. info.xsd declares repair-steps as an <xs:sequence> of pre-migration, post-migration, live-migration, install, uninstall. A sequence is ordered, so once the parser has consumed <install> the only child that may still appear is <uninstall>, and a perfectly valid <post-migration> arriving there is rejected for its position alone. Both blocks are kept exactly as they were, and deliberately so: the comment above them explains that <install> is the only hook a renamed app's first enable fires, because to Nextcloud `thematiq` is a new app rather than an upgrade of `nldesign`, so post-migration alone would never run on the instances that most need the migration. Dropping the duplicate to satisfy the schema would have undone that. Only the order changes, and a note now records why it cannot drift back. Verified against the real schema rather than by reasoning about it: validating with DOMDocument::schemaValidate against apps.nextcloud.com/schema/apps/info.xsd reports VALID for this file, and the pre-fix version INVALID with CI's exact message — so the local check discriminates and is not just agreeing with me. * fix(ci): the playwright seed wrote token_set to the old app namespace Five dark-mode-scope specs and one dark-mode spec failed their own precondition: precondition: a tokens/dark/* stylesheet must be loaded, otherwise these assertions cannot distinguish "scoped correctly" from "never loaded" code-quality.yml seeds the active token set with php occ config:app:set nldesign token_set --value rijkshuisstijl and `occ config:app:set` takes the APP ID. After the rename that wrote into a namespace nothing reads, so token_set kept its default of 'nextcloud' — and css/tokens/dark/nextcloud.css does not exist, so injectDarkVariantStyle() correctly emitted nothing and the dark stylesheet was never loaded. css/tokens/dark/rijkshuisstijl.css does exist, which is why the seeded value matters. This is the same silent-default shape as the app-config migration itself: the write succeeded, the read found nothing, and the default took over without an error anywhere. That precondition is well written — it refused to pass over an absent stylesheet, which is the only reason this surfaced as six real failures rather than six green no-ops. * fix(ci): four more workflows still declared app-name: nldesign release.yml (three jobs), openspec-sync.yml and the comment prose in code-quality.yml still named the old app id. app-name drives where the reusable workflow checks the app out and what it packages, so release.yml would have built the release artefact under an id the app no longer declares. Found by sweeping every workflow for old-id ARGUMENTS after the token_set fix, rather than waiting for each one to fail in its own way — release.yml only runs on a tag, so this would not have surfaced until a release. Left alone: documentation.yml's live cname, the chore/sync-nldesign-tokens branch name (it syncs NL Design System tokens, so it names the standard), and every nl-design-system / nldesignsystem.nl reference. * fix: OC.filePath()/OC.linkTo() take the APP ID as their first argument Five icon-asset specs failed once the dark-mode ones were fixed. The suite derives its icon and logo bases from OC.filePath('nldesign', 'img', 'icons') and that first argument is the APP ID, so it resolved a web root under an app that no longer exists and every asset 404'd. Swept the same call family and found two more, one of them PRODUCTION code: js/admin.js:650,659 — OC.linkTo('nldesign', proposed.logo) and its _dark sibling, which build the logo preview URLs in the admin settings UI. Those would have resolved under the old app root for real users, not just in tests. tests/e2e/workflows/_helpers.ts:125 — the same OC.filePath helper. Another app-id-derived channel to add to the list alongside script()/style(), linkToRoute(), initial-state ids, and occ config:app:set: anything whose first argument names the app resolves against the app root, and a rename moves it. * fix: getAppPath(appId: ...) — named arguments hid two more app-id sites ThemingService resolved the app root twice with $this->appManager->getAppPath(appId: 'nldesign') which points at a directory that no longer exists, so every path derived from it — the logo and token assets the preview banner and contrast audit read — resolved to nothing. These survived every earlier sweep because my patterns matched POSITIONAL calls (getAppPath('nldesign')) and these use NAMED arguments (appId:). Same for Util::addStyle(application: ...) elsewhere in this app, which happened to already use APP_ID. Worth remembering: in a codebase that uses named arguments, a grep for "func('appid'" misses the very calls most likely to be correct style. Also moved a logger context key ['app' => 'nldesign'] to the new id. FROZEN and deliberately untouched: the OCS capability namespace $capabilities['nldesign'], which external clients read and which render-context-injection.spec.ts asserts by that exact name, and the #nldesign-* DOM ids the templates still emit. phpcs 0, psalm clean. * fix: the app's own id in PROTECTED_IDS, and getAppVersion() AppThemingService::PROTECTED_IDS is the list of apps that must NEVER be excluded from theming, and it still named 'nldesign'. With the app's own id absent from its own protection list, this app became excludable from its own theming — and the theme-preview banner is only injected on a THEMED page, which is why 'the banner appears on every themed page' and 'discarding from the banner restores the active set' both timed out waiting for #nldesign-preview-banner that was never created. (The DOM id itself is correct and frozen: source and spec agree on it.) Also moved getAppVersion('nldesign') in ComplianceReportService — the app id, so it reported no version at all. Both unit tests that pin the protected list were updated with the code, so the fixture states what the service actually protects. Left frozen, deliberately: the OCS capability namespace $capabilities['nldesign'] (external clients read it, and render-context-injection.spec.ts asserts that exact key) and every nldesign-* DOM id and CSS custom property. phpcs 0, psalm clean. * fix: loadState() takes the APP ID — the preview banner read null The three remaining specs failed because #nldesign-preview-banner was never created. The DOM id was correct all along (source and spec agree, and it is a frozen design-system prefix); the banner script simply returned early. js/preview-banner.js:27 read its state with OCP.InitialState.loadState('nldesign', 'preview', null) while ThemePreviewBannerService::inject() provides it through IInitialState under the APP ID, which is now thematiq. So the read found nothing, took the null default, and rendered no banner — the same silent-default shape as the appconfig migration: the write succeeded, the read missed, and the fallback looked like 'no preview active'. js/admin.js:84 had the same call for every admin settings value. That is the third app-id channel in this one file family, after script()/style() in the template and OC.linkTo() for the logo URLs. js/ was never in my original sweep, and each pass has found one more thing in it — noting that here so the next reader treats a hand-written js/ directory as first-class source, not build output. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * fix(docs): point the repository CTA at GitHub, not Codeberg (#389) The landing page shipped a button sending visitors to codeberg.org. GitHub is the only host we publish to, so the link opened a repository we no longer read. Now points at https://github.com/ConductionNL/thematiq. * feat(docs): move the docs host nldesign.conduction.nl -> thematiq.conduction.nl (#388) * feat(docs): move the docs host to the new name, and stop linking to Codeberg The `<new>.conduction.nl` docs hosts did not exist until 2026-08-23, which is why every documentationUrl, CNAME and docusaurus `url:` in this repo was deliberately frozen on the old host. They exist now: each new hostname was attached as a SECOND custom domain on the SAME `<old>-docs` worker, so both hosts answer and nothing goes dark in either direction. `docs-hosts` lists BOTH hosts on purpose. wrangler reconciles a worker's triggers against the config it is handed, so a hostname omitted there is REMOVED from the worker — which would take that site down. (Observed the same day: a deploy without `workers_dev` in the file silently disabled the workers.dev URL, 404 where it had been 200, while reporting success.) Codeberg links in docs/ now point at GitHub. GitHub is the only host, including for issues, and the navbar on the published site was offering Codeberg. The docs will only actually change once ConductionNL/.github#555 lands: the reusable documentation workflow writes gh-pages and never touches the worker that serves the host, so every fleet docs site has been serving a May/June build while reporting success on each run. * docs: point the remaining repo links at the renamed repository The merge conflict this branch just resolved was one GitHub link development had already moved from ConductionNL/nldesign to ConductionNL/thematiq. Sweeping for the rest found four more the branch had left behind: the docusaurus editUrl and navbar href, the icon-documentation link, and llms.txt's source-code entry. They are not broken today — GitHub redirects a renamed repository — but a redirect is a grace period, not a contract, and llms.txt in particular exists to tell a machine where the source lives. Leaving them also puts the docs in the state the conflict was about: the same fact spelled two ways in one site, which is what made the conflict rather than a clean merge. Verified ConductionNL/thematiq resolves (default branch `main`) before pointing anything at it, rather than assuming the rename landed. Config still parses. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * chore(deps): refresh the shared Conduction locks (#393) hydra-gates v1.9.0 -> v1.9.0 nc-vue 2.11.1 -> 2.15.0 Lock-only: both packages are already declared with caret ranges that permit these versions, so nothing about what this app ACCEPTS changes - only what it currently resolves to. Opened by the weekly fleet shared-dependency bump, because a lock nobody re-resolves is a pin nobody chose. Merging is gated by this repository's own suite, deliberately: taking hydra-gates v1.8.1 added patchObject() to a published interface, which is a load-time fatal for any concrete double that implements it without the method. CI is the only thing that can tell a safe bump from that. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(repair): move OpenRegister schemas from nldesign onto the thematiq application id (#395) OpenRegister resolves a REGISTER by slug alone, but a SCHEMA by the PAIR (application, slug) via SchemaMapper::findByApplicationAndSlug(). An import passes appId: Application::APP_ID = 'thematiq', while any schema written under the old id still carries application = 'nldesign'. The pair matches nothing, and ImportHandler's not-found branch is not an error path — it is the create-a-new-one path, so the import builds a second, EMPTY schema set while every stored object stays bound to the old rows. Nothing errors; the collections render empty. This app ships no register of its own — its theming lives in Nextcloud's IConfig — and nldesign did not appear in the live fleet measurement, so on a clean instance the step reports 'nothing to do' and writes nothing. It is registered as the guard for an instance that DOES carry such rows, where the failure is silent rather than loud. The step refuses rather than merges where a slug already has a twin under the new application id, distinguishes a FAILED READ from an EMPTY RESULT, never deletes a schema and never throws — it runs under <install>, where an escaping exception aborts the install. * fix: build the three repair steps info.xml declared but never shipped (#397) appinfo/info.xml declared OCA\Thematiq\Repair\MigrateAppConfigKeys, MigrateUserPreferences and MigrateStoredClassNames in both <post-migration> and <install>. There was no lib/Repair/ directory at all. OC_App::executeRepairSteps() catches "Repair step is unknown" per step and logs it, so the install never aborted — it silently skipped all three. None of them has ever run. These are exactly the migrations the nldesign -> thematiq rename needs. Nextcloud namespaces oc_appconfig and oc_preferences by app id and there is no in-place app-id upgrade, so the rename does not move the stored rows, it makes every stored value unreachable. Every reader supplies a default, so nothing errors: a themed government instance silently reverts to stock. MigrateStoredClassNames was verified to be real before being written. The app stores exactly one class name — mail_template_class in config.php — and the stale value fails twice: Mailer::createEMailTemplate() guards it with class_exists() and falls back to the stock template, and getState() classifies the app's own pre-rename value as `foreign`, so enable() refuses it with HTTP 409 and the admin is locked out of the fix. Other candidates were ruled out: oc_jobs self-heals (JobList removes a row whose class is gone), there is no register JSON or seeded descriptor, and HealthController's two FQCNs name OpenRegister classes that must not move with this rename. Also fixed, same defect class: - EmailThemingService::OCC_ENABLE_COMMAND still named OCA\NLDesign\..., and it is rendered into the admin panel as the copy-paste command for a read-only config.php. An admin who ran it set a class that does not exist. Two spec scenarios stated the same stale FQCN. - tests/bootstrap.php re-pointed PSR-4 and stripped the classMap for OCA\NLDesign\, a namespace with no classes since the rename, so its worktree/symlink protection was a silent no-op for the only namespace it covers. loadApp('nldesign') was stale for the same reason. - The info.xml ordering comment justified step order with a dark_variants_generated key that does not exist; GenerateDarkVariantsRepairStep writes no config at all. Order is still required, as a standing invariant. All three steps are idempotent, never overwrite a value already present under the new id, never delete the old rows, and never throw — they run under <install>, where an escaping exception aborts the install entirely. 35 new tests, all passing. Pinned specifically: reserved appconfig keys are skipped (copying `enabled` as STRING permanently breaks app:enable), preferences are enumerated by user rather than by value (three users holding three different values under one key all move), and a foreign mail template is never clobbered. * fix(docs): publish from development, to the worker that actually serves this site (#398) Two silent failures. 1. The workflow triggered on `documentation`, a branch that exists but nobody updates. That is why PR #392 exists at all — a hand-run "sync development into documentation" is a symptom of the trigger being wrong, not a fix for it. Pointing the trigger at `development` removes the need for that sync entirely. 2. `worker-name` was never passed, so the callee derived it from `cname` — `thematiq-docs`, a worker that does not exist. Both custom domains live on `nldesign-docs`. Deploying the derived name CREATES a second worker while both domains keep routing to the original: every deploy green, reaching nobody, with the live-site verification added in ConductionNL/.github#555 as the only thing that would ever have noticed. Unlike the rest of the fleet this site's live title already matches its source, because docs/docusaurus.config.js still says `title: 'NLDesign'` — the docs content has not been renamed yet. That is a separate change; this one only makes publishing work. * docs: rename the app to Thematiq, keep the NL Design System as-is (#400) The app formerly called NLDesign is now Thematiq. The NL Design System it implements is an external Dutch government standard and does not move — renaming it would invent a "Thematiq Design System" that does not exist. Renamed (the APP): - site title, landing page/hero title, package name, GitHub projectName - page headings and prose across intro, getting-started, features, reference and llms.txt - app-id channels verified against Application::APP_ID = 'thematiq': occ config:app:set / app:enable, appconfig keys (hide_slogan, show_menu_labels, disabled_apps, marianne_enabled, custom_token_sets), imagePath('thematiq', ...), custom_apps/thematiq paths, and the never-excludable id list (PROTECTED_IDS) - og:image renamed on disk together with its reference Deliberately left (the STANDARD, verified frozen): - the tagline "NL Design System tokens for Nextcloud" - --nldesign-* custom properties, .nldesign-icon classes, data-nldesign-theme-scope, css/systems/nldesign/ - the "nldesign" design-system id in design-systems.json - capabilities.nldesign.* — still spelled nldesign in lib/Capabilities.php - the "NL Design System Theme" admin heading, still hardcoded in templates/settings/admin.php (out of scope) - appId="nldesign" / app="nldesign" — cross-package lookups into @conduction/docusaurus-preset, which ships no thematiq key docs build green; all 10 validate-ai-baseline checks pass. * The government checklist named the wrong source host (#402) docs/GOVERNMENT-FEATURES.md is a Programma van Eisen checklist handed to procuring organisations. Its open-source rows named Codeberg as the canonical source host and its community row pointed at Codeberg Issues. Codeberg is an unmaintained mirror; GitHub is the only host, including for issues. The document was making a false claim to a customer. This was not a stale link that a sweep would catch. A live openspec requirement REQUIRED the Codeberg reference, and ClaimAccuracyTest enforced it in both directions — asserting the document contains "Codeberg" and does NOT match /GitHub/i. Correcting the document alone would have turned the suite red; the claim was pinned by a test. So all three move together, and the assertions are INVERTED rather than deleted. This test class exists to stop the app claiming things that are not true. Naming a host the organisation no longer publishes to is exactly that class of defect, so the invariant should keep its teeth pointing the other way rather than be dropped. The requirement also still named the pre-rename repo `codeberg.org/Conduction/nldesign`; it now names `github.com/ConductionNL/thematiq`. Verified: ClaimAccuracyTest 12 tests / 384 assertions pass, php -l clean. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * fix: repoint stale Codeberg links to GitHub (#396) GitHub is the only host for this org. Codeberg was a mirror, is no longer used (including for issue tracking), and these URLs were dead or wrong. Many links also carried the PRE-RENAME repo name: this app is now `thematiq` (appinfo/info.xml already declares `<id>thematiq</id>`), so `codeberg.org/Conduction/nldesign` was wrong on BOTH host and repo name. Changes: - 49 PHP `@link` headers: codeberg.org/Conduction/nldesign -> github.com/ConductionNL/thematiq (45 under lib/, 4 under tests/) - README.md: `git clone` URL repointed; the following `cd nldesign` is updated to `cd thematiq` because the clone now creates that directory - docs/static/llms.txt: the line labelled `GitHub:` pointed at codeberg.org/Conduction — URL fixed, label kept - img/ICONS.md + scripts/build-icons.js (the generator that emits it): nextcloud-vue `/src/branch/main/` deep link -> `/blob/main/` - src/manifest.json: `$schema` `/raw/branch/main/` -> `/raw/main/` Issue-link decision (deliberate): Codeberg issue numbers do NOT correspond to GitHub issue numbers. Rewriting only the host would have silently pointed at a real but UNRELATED GitHub issue, which is worse than a dead link. So the two issue references were de-linked, keeping the fact as plain text: - phpstan-baseline.neon: issue 98 - css/systems/nldesign/defaults.css: issue 127 Deliberately left untouched: - .forgejo/workflows/tests.yml (6 refs) — Forgejo CI config, needs a human decision, not a link rewrite - docs/GOVERNMENT-FEATURES.md, tests/Unit/ClaimAccuracyTest.php and openspec/specs/claim-accuracy/spec.md — a live spec and a PHPUnit test actively REQUIRE "Codeberg" and FORBID "GitHub" in that doc. Inverting that is a spec change, out of scope for a link repoint. - tests/schemas/app-manifest-v2.schema.json — `codeberg` is a legitimate forge enum value, not a stale link - openspec/changes/archive/** — historical records Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> * fix(docs): pass secrets to the reusable documentation workflow (#406) * fix(docs): pass secrets to the reusable documentation workflow A called workflow receives no secrets from its caller unless they are passed explicitly or inherited. Without `secrets: inherit` the callee sees an empty `secrets.CF_API_TOKEN`, its "Publish to the Cloudflare Worker" step skips itself on its own guard, and the run finishes green having written only gh-pages — which nothing serves. The live docs site never changes and no check goes red to say so. Measured on planninq run 32715324775: all three jobs green, GitHub Pages deploy success, Worker publish skipped, warn step reporting the Worker was not updated. * fix(docs): map the Cloudflare secrets explicitly instead of inheriting all `secrets: inherit` handed the reusable documentation workflow every secret this repo holds — the Nextcloud signing cert and key, the appstore token, the deploy keys — for the sake of two Cloudflare values. It also would not have worked. The org secrets are CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID and `inherit` passes secrets under their original names, while the callee reads CF_API_TOKEN / CF_ACCOUNT_ID — so the publish step would still have skipped itself and the run would still have gone green over an unchanged live site. Maps the two names explicitly instead, so nothing else crosses the boundary. Depends on ConductionNL/.github#568, which declares both as optional secrets on the callee: an explicit mapping only compiles for names the callee declares. * fix(cache): rejoin the WCAG cache the rename split in two (#408) TokenSetService's own comment said it best: "Deliberately the same ICache prefix `nldesign_wcag_level` Capabilities uses, so the public catalogue and the active-theme capability share one cache entry per set id." The app-id rename moved Capabilities to `thematiq_wcag_level` and left TokenSetService on `nldesign_wcag_level`. The sharing that comment promises stopped happening, and the WCAG level is now computed twice for the same set id. NOTHING FAILED, which is why it survived. Two prefixes are two perfectly valid caches, and a permanent miss on a cache you own is indistinguishable from a cold one — you get the right answer, computed again, every time. The only symptom is work nobody measured, plus the chance that two surfaces answering the same question disagree inside one TTL window. All four references now read `thematiq_wcag_level`, including the two docblocks in ShippedTokenSetAuditService that documented the old prefix as the contract. And the pairing is now ENFORCED rather than asserted in prose. The new test builds both classes against a recording ICacheFactory and compares the prefixes they request WITHOUT naming the expected value, so a future rename that moves both together still passes and only a rename that moves one fails. A second test checks the shared prefix carries the current app id — kept separate because two classes agreeing on a STALE prefix is a different defect from two disagreeing, and one test covering both would not say which had happened. Verified the test catches the real thing: reverting the prefix reproduces the bug and both assertions fail with the message naming the fix. 347 Service tests pass. The 2 CssInjectionServiceTest failures are pre-existing on development, confirmed against a clean tree. * chore(ci): remove dead Forgejo/Codeberg CI configuration (#412) GitHub is the only host this organisation publishes to. No local checkout has a Codeberg git remote, so nothing is pushed there and no workflow under .forgejo/ has ever run for this repository. This repository held no issue templates under `.forgejo/` — only workflows. This repository had no `.forgejo` release workflow. Its release path, `.github/workflows/release.yml`, is untouched. .github/workflows/ is untouched — that is the live CI. Any CODEBERG_TOKEN reference lived only inside the deleted files and goes with them. Removes 2 file(s) under .forgejo/. * fix(docs): map the Cloudflare secrets from the names that actually exist (#411) The mapping read `secrets.CLOUDFLARE_API_TOKEN` / `secrets.CLOUDFLARE_ACCOUNT_ID`, which are not secrets anywhere in this org. Mapping from a non-existent secret is not an error - it yields an empty string - so the callee's "Publish to the Cloudflare Worker" step skipped itself on its own guard and the run stayed green while the live docs site kept its pre-rename build. The real org secrets are CF_API_TOKEN / CF_ACCOUNT_ID, the same names the callee declares and the same ones ConductionNL/.github deploy-docs.yml reads directly. Only the mapping values change; the keys stay. The comment justifying the explicit mapping claimed the names differ on each side. They do not, and that claim is what produced the bug. Replaced with the reason that still holds: `secrets: inherit` would hand the callee every secret this repo holds for the sake of two Cloudflare values. * ci(quality): give every event its own concurrency lane, not just main and development (#415) The event suffix applied only when ref_name was main or development, so on every other branch push and pull_request computed the SAME group and cancel-in-progress made them kill each other — contradicting the comment above it, which already says push, dispatch and schedule each get their own lane. Reachable since the push allow-list widened on 2026-08-14 to include feat/**, fix/**, perf/**, refactor/** and chore/**: those branches now get both a push run and a pull_request run for one commit. quality / Quality Report is a needs:-gated aggregator that reports FAILURE when its dependencies are cancelled, so this shows up as a red gate on a PR that was never evaluated, and re-running collides the same way. Already applied to openregister, integriq and pipelinq, where cancellations went to zero. See ConductionNL/.github#563. --------- Co-authored-by: Ruben van der Linde <rubenvdlinde@gmail.com> Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ruben van der Linde <ruben@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.
Why
ConductionNL moved onto Codeberg on 2026-05-29 and moved back off on 2026-07-23. The git layer completed — verified 2026-08-22, zero codeberg remotes fleet-wide. The shipped metadata did not.
23 apps still declare Codeberg URLs in
appinfo/info.xml, which is published to the Nextcloud app store:<bugs><screenshot><repository>/<website>This is not cosmetic drift — it is the part of a host migration that is invisible from the git layer, which is exactly why it survived a month past the migration.
Why a gate, and why full-tree
Why a gate and not just a sweep: it needs both. A sweep fixes today's 23; the gate is what stops the 24th. The fleet has done two host migrations in three months — the specific host won't recur, but the shape will.
Why NOT diff-scoped — the reasoning gates 84 and 93 give, but sharper here. The drift this gate catches is already in the tree, put there by a migration that touched every repo at once. A diff-scoped version reports nothing on the ~99% of PRs that never open
appinfo/info.xml— it would be blind to 100% of the debt it was written for.Scope: shipped metadata, not prose
Reads only files whose contents reach a user or a package registry:
appinfo/info.xml— root only (an NC app ships exactly one; a nested one belongs to a different app)package.json/composer.json— any depth (workspaces, per-package design tokens are legitimate)and within those, only the fields carrying a followable URL (
bugs,website,repository,documentation,screenshot,homepage,support).Deliberately out of scope: docs, changelogs, specs, learnings, archived openspec changes. Those record what was true when written; rewriting them makes the history untrue. A gate noisy enough to be switched off catches nothing.
custom_apps/and friends are skipped. A dev environment drops checkouts of other apps inside a repo. Walking them made the first draft report openregister as having 69 findings — 63 of which belonged to seven other apps. A gate that blames a repo for a neighbour's metadata teaches people to ignore it.Verification
procestinfo.xml+package.jsonopenregistercustom_appsfixdecidesknextcloud-vuehydrainfo.xml/package.json/composer.jsonChecker exit codes:
0clean,1findings,4not applicable.bash -nclean; rc-to-verdict mapping exercised in isolation (PASS / FAIL / SKIP(na) all reproduced).How this reaches the fleet
Per the delegator contract, each app's
Code QualitycallsConductionNL/.github/.github/workflows/quality.yml@mainand resolves the gate package at run time — so merging this reaches every app with no commit in any app.fleet-drift-sweep.ymlthen surfaces which apps fail it.Note on the sweep: it currently exits non-zero on every run — but that is the sweep working. Its 2026-08-21 run dispatched to all 21 apps, collected verdicts, and failed on
not green on development: pipelinq shillinq. Those are real app failures, not sweep breakage. Issue #523 ("the sweep has never run") is closed and its premise no longer holds.