Release: merge development into beta - #79
Open
github-actions[bot] wants to merge 495 commits into
Open
Conversation
…ct-table column fallback' (#166) from chore/ncvue-beta209 into development
…hemas, all manifest-mutating tools refused' (#167) from wip/mcp-adoption into development
The per-app icon endpoint took ~2s, and the builder requests it twice per page load — the slowest thing on the cowboy dashboard by far. IconService fetched the Application, serialized it to an array, threw the entity away, and handed a bare UUID string to OpenRegister's FileService::getFile(). getFile() accepts an ObjectEntity OR a string — but given a STRING it calls objectMapper->find($uuid), and a bare UUID carries no register/schema, so OR searches every magic table to find which one owns the object. On this instance that is ~1,960 tables. We already hold the entity, so keep it and pass it through. Only an ObjectEntity is passed (findAll() can also yield plain arrays, which getFile() would reject with a TypeError); anything else still falls back to the UUID string — correct, just slow. Net: ~2.0-2.8s -> 0.87s per icon (baseline request overhead on a loaded box), live-verified, still serving the app's own icon. Fixes a test-fake drift while here: the FileService stub declared `getFile(string $object)` — narrower than the real `ObjectEntity|string|null` — so passing the entity, the whole point of this fix, threw only in tests while prod worked. A stub that narrows the real signature hides the bug it should catch.
…le, not a bare UUID (2s -> 0.87s)' (#168) from fix/icon-entity-scan into development
…I; MCP vs API vs RAG assessment Adds openbuild/test-scenarios/ with the first scenario: exercise every OpenBuild action-menu capability (add/alter schema, create/design pages, overrides, import, publish, versions, export, permissions, template, settings) by instructing the AI companion, and assess how the agent perceives page state — whether MCP tools suffice or it needs the OR API and/or RAG. Depends on a tool-capable chat provider (anthropic-agent-provider or a tool-capable Ollama model).
…hannel next to MCP; hybrid apps out of MCP scope Per direction: OpenRegister already supports RAG, so for virtual apps RAG is available alongside MCP by design (not a gap to close) — the agent uses RAG to read the viewed item's field values, which the cnAiContext snapshot carries identity for but not content. Hybrid apps (large JSON core manifest) are explicitly out of the OpenBuild MCP scope for now.
… coverage + MCP/API/RAG assessment' (#169) from test/agent-driven-scenarios into development
Also removes the dead widget() registry helper (pre-existing no-unused-vars lint error, zero callers).
…beta.212' (#170) from chore/ncvue-beta212 into development
…r' (#171) from chore/ncvue-beta213-lockstep into development
Fixed ignored change-requests from PR https://codeberg.org/Conduction/openbuild/pulls/96 Works through the change requests that were raised on #96 but never applied before it merged. Each was re-verified against current code before acting (several claims had gone stale or were inaccurate). Blockers - ApplicationDeletionService — 153 lines of destructive code, no tests: FIXED. Added tests/Unit/Service/ApplicationDeletionServiceTest.php (preserve vs purge, empty-batch, zero-progress, cap-exhaustion). - ApplicationDeletionService purge cap hit silently: FIXED. Log a warning when MAX_PURGE_ROUNDS is exhausted so the downstream register-orphan is diagnosable. Cap kept at 100k (raising it risks request timeout; async deletion is the real large-register fix) — deferred by decision. - AppDeleteDialogSlot / DeleteAppDialog untested: FIXED. Added vitest specs; coverage 85.18% -> 86.07%, ratchet green. - DeleteAppDialog missing i18n strings: MOSTLY STALE. The 4 flagged strings were already in l10n/en.json (added after the review); removed the genuinely-dead "Permanently delete...and data" key. Also extracted 3 unrelated ExportDialog strings that were actually reddening the gate. Concerns - ApplicationPublishControllerTest deleteData forwarding unverified: FIXED. Old mock constrained only 2 of 3 args; now asserts the 3rd positionally (later reworked to drive via the request param, below). - deleteData query-param footgun: HARDENED. The "?deleteData=false -> true" claim is FALSE on NC35 (the dispatcher maps 'false'->false), but the general stringy-bool risk is real, so destroy() now reads the raw param safe-by-default: only '1'/'true' purge, anything else preserves. - einddatum type narrowed to "string": FIXED. Restored ["string","null"] (it was intentionally the one nullable date field; narrowing would fail existing rows holding null on next save). No migration needed. - SeedApplicationTemplates idempotency froze templates on upgrade: FIXED. Implemented version-based upsert (isSeeded rows only, version_compare, admin-created templates never clobbered); added an `updated` count. - builderDesigner route regex maintenance trap: FIXED via guard. Instead of a breaking URL refactor or a (quarantined) Playwright test, added a RoutesTest that derives the designer routes from src/manifest.json and fails in CI if the regex drifts from them. Minor - SeedApplicationTemplates return-on-DoesNotExist skips remaining slugs: DOCUMENTED, no behaviour change. The suggested `continue` would regress the deferred contract; the return is correct because every slug targets one hardcoded schema. Added a comment stating the invariant + when to revisit. - Global v-tooltip directive registration hard to discover: FIXED (docs). Pointer comments in src/main.js and src/registerDirectives.js. - "-dark" slug reservation undocumented: FIXED. Enforced for APP slugs only (schema pattern + SlugValidator::validateAppSlug) and documented; version slugs are deliberately unaffected. Added tests.
…: false` stub) The resolve block sets `fallback.path` to `path-browserify` (needed by the @nextcloud/dialogs FilePicker chunk), but a later block re-assigned `resolve.fallback` with `path: false`, clobbering it. Its comment claimed the FilePicker code path never runs — no longer true: nextcloud-vue's CnFilesWidgetForm opens the folder picker via `pickNodes()`, which calls `path.join`. With `path` stubbed to an empty module, `path.join` was undefined and the picker threw "path.join is not a function" at runtime. Remove the conflicting `path: false` override so `path` stays polyfilled by path-browserify. Production build verified.
…o GitHub Brings all Codeberg development (page-designer #174 fix, AI-companion, node_modules strip, and the full 07-xx feature work) together with GitHub's unique commits (nc-vue beta bumps, agent-driven test scenarios). Conflicts in the 4 page-designer/ builder files resolved to Codeberg's validated versions (1178 vitest tests green). History is flattened at this import: Codeberg's commit history carried a 169 MB webpack-cache blob (docs/node_modules/.cache) that exceeds GitHub's 100 MB file limit, so the full ancestry could not be pushed. Granular history remains on the Codeberg mirror; docs/node_modules is now gitignored.
Picks up the CnOpenBuildEditButton glyph fix (cube → OpenBuild stacked-layers). Consumers on the 24h min-release-age hold must install with --min-release-age=0 until the hold elapses. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
H2: disable redirect following in RemoteTemplateStoreService::fetch (allow_redirects => false) so a public host cannot redirect to a private/metadata address with the registry Bearer token attached. DoS (business-rules engine): - FeelParser: max source length, max token count (bounds AST nodes), and a recursion-depth guard across parseOr/parseNot/parsePrimary. - ExpressionEvaluator: evaluation recursion-depth backstop. - RuleEngineService: call-rule-set re-entry guard (cycle + depth), fail-closed before any log write or side effect; maskPii depth cap. - RulesController::evaluate: reject oversized payloads (413) before logging. Covers openspec change harden-xss-dos-csrf groups 0-1. Unit tests added for every guard. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
createFromTemplate provisions a per-app OpenRegister register (admin-only in OR, issue #157) — the same fan-out ApplicationCreationController::wizard performs. Mirror the wizard's guard: add #[UserRateLimit(10/3600)] and an admin gate so a non-admin gets a clear 403 instead of an opaque provisioning failure, and the endpoint cannot be used as an unthrottled register/schema-sprawl amplifier (DoS #4). Adds ApplicationsControllerTest (401 anonymous, 403 non-admin, rate-limit attribute present). Covers harden-xss-dos-csrf group 2. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
Remove the unjustified NoCSRF opt-outs from state-changing endpoints: - SettingsController::create / ::load (#[NoCSRFRequired] dropped) — create is the supply-chain lever (a forged request could repoint registry_url / registry_token); the SPA already sends the NC request token so nothing legitimate breaks. - PreferencesController::setPreference (@NoCSRFRequired docblock dropped). The read-only settings#index and preferences#getPreference GETs keep their no-CSRF posture (a browser navigation cannot carry a request token). Tests assert the writes are CSRF-protected and the read GETs still opt out. Covers harden-xss-dos-csrf group 3. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
Add dompurify as a direct dependency and route the two client-side v-html sinks through it: - DocumentTemplateAttachmentDialog::onPreview sanitizes the Docudesk preview (full HTML profile) before binding — a template authored by one user renders in another's session, so it is a cross-user XSS sink. - iconCatalogues::resolveAppIcon sanitizes the author-supplied <svg> branch (SVG profile) before preview and before persistence. Vitest specs assert script/event-handler payloads are neutralized and benign markup/SVG preserved. Covers harden-xss-dos-csrf group 4. (M7 — bumping the transitive dompurify past the advisory line — remains a separate finding.) Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
Wrap-up for harden-xss-dos-csrf (group 5): - Register the change in the five capability specs (OpenSpec changes link; flip openbuild-template-catalogue + app-icon-management to in-progress). - Move the createFromTemplate gate/rate-limit tests into the existing CreateFromTemplateTest (the standalone file I added collided with the repo's ApplicationsControllerTest class — removed); make authenticateAs grant admin by default so the existing clone-flow tests exercise the admin-gated path, add a non-admin 403 case + rate-limit reflection check. - Fix Step4Review.spec assertions to match DOMPurify-normalized SVG output (assert preserved path data / fallback equality, not byte-exact). - Reconcile phpmd.baseline.xml for the renamed methods (evaluate→evaluateNode, parsePrimary→parsePrimaryInner) + ExpressionEvaluator class complexity. Full suite green: PHP 607 unit + phpcs/psalm/phpstan; vitest 1184; eslint clean. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
PHPCS flagged the $depth parameter added in the DoS-hardening change as missing its @param line. Doc-only fix; no behaviour change. Assisted-by: ClaudeCode:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Robert Zondervan <robert@conduction.nl>
Market deepdive round 2 (research logged to spectr register 2444): - public-forms-runtime: anonymous tokenized form submission, spam guard, URL prefill, per-record edit links (Forms#358/#805/#624, Baserow 2.2) - automation-approval-steps: human-in-the-loop approval action compiling to OpenRegister approval chains (consume, not rebuild) - component-blocks: reusable manifest blocks across pages and apps (Appsmith#1911, Budibase#18726/7, ToolJet#4850) - app-theming: per-app theme tokens with WCAG contrast guardrails (Appsmith#3095, largest single market ask) - agent-workspace: persistent named agents over existing MCP tools with transparent run log (2026 table stakes) - automation-document-action: generateDocument action consuming docudesk via owner-impersonated call to the pinned route (Forms#103) All changes openspec validate clean, kind: code, tasks within cap.
spec: market-deepdive round 2 — six ff changes (public forms, approvals, blocks, theming, agents, documents)
Enhances core application stability and safety features
…flicts) # Conflicts: # openspec/specs/docudesk-document-templates/spec.md # openspec/specs/openbuild-template-catalogue/spec.md
fix(security): harden SSRF, rules-engine DoS, CSRF, and XSS surfaces
Adds OpenBuild's first #[PublicPage] surface: a ShareToken model (OR-backed, openbuild register) scoping anonymous access to exactly one Application + one page, with submit/read/edit modes, honeypot spam guard, AnonRateLimit, prefill-from-URL, and per-record edit links. Public rendering/submission resolve solely through the token, never session/organisation auth; writes go through PublicSubmissionService acting as the Application owner, never the OR client-facing objects API or a visitor identity. - ShareToken schema (register.d/50-public-forms-runtime.json, ADR-037 fragment) - ShareTokenService (issue/revoke/resolve/list) + PublicSubmissionService (submit) - PublicFormController (#[PublicPage] render/submit) + ShareTokenController (authenticated CRUD) - Public bootstrap entry (src/public-form.js, new webpack/template/route) + ShareTokenDialog.vue - FormPageEditor.vue "Public access" block; wired from PageDesigner toolbar + app Actions menu - 27 new PHPUnit tests (ShareTokenService, PublicSubmissionService) + 13 new vitest tests Archives change public-forms-runtime; syncs public-form-access (new) and openbuild-runtime (added requirement) canonical specs.
…d coverage (#77) Groundwork for the role-scoped permission suites (versionRouting 9.2, schema-access-scopes-rbac). Adds grantAppRoles(), which writes an Application's `permissions` block through OpenRegister's object API — there is no openbuild permissions endpoint — carrying the WHOLE record forward, because OR saves are PUT-semantic and omitted properties are dropped rather than left alone. It works, and is not enough. Measured on a live instance: - the grant lands and reads back as {owners:[user:admin], editors:[group:rbac-editors], viewers:[group:rbac-viewers]}; - rbac-viewer is in rbac-viewers and rbac-editor in rbac-editors per OCS, and both groups exist; - PermissionResolver::matchesCaller() classifies `group:` principals and intersects them against the caller's groups, so the grammar is right; - yet GET /api/applications returns 200 with an EMPTY list for BOTH users. Something below openbuild's permission layer filters the object out, most likely OpenRegister-level object visibility — a separate grant from the manifest permissions block. Filed as #76. Committed as groundwork rather than held back: it encodes the verified permission shape and the PUT-semantics trap, so whoever picks up #76 starts from measured facts instead of re-deriving them. The role-scoped scenarios stay skipped until a member can actually see the app. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
A UUID is OpenRegister METADATA, not an object property, so
findAll(filters: ['uuid' => ...]) matches nothing — hermiq's SkillService
documents exactly this rule in a comment I failed to apply. Every declared
connector silently failed to resolve.
Caught by a dry-run publish, not by tests: the first real artefact was 4 files
with connectors {declared: 0} despite 25 declared bindings. Now 46 files with
declared 42 / missing 0, verified against a live instance.
Adds a counter to the descriptor. 'declared 0' was indistinguishable
from an app that declared nothing, which is precisely how this hid; 'declared 42,
missing 4' is diagnosable. The counter also surfaces a legitimate case: the
schema is admin-read-gated, so a non-admin publish drops sources — the
exporter correctly exports only what the caller may read, and now says so.
Warnings upgraded from debug to warning for the same reason.
fix(app-repo): resolve connectors with find(), not a uuid filter
… for two months (#74) * ci: point reusable-workflow calls at ConductionNL, not the non-existent Conduction org All 8 reusable-workflow calls referenced `Conduction/.github`. That GitHub org does not exist (the 2026-06-01 rename went the wrong way; `GET /orgs/Conduction` is a 404). Actions cannot resolve the ref, so every run produced ZERO jobs and failed instantly. No ESLint, PHPCS, PHPMD, PHPStan, Psalm, licence scan, security scan, SBOM or PHPUnit has run on this repo since. The tell: in `gh run list --json name`, an unresolved run's `name` is the raw path (`.github/workflows/code-quality.yml`) rather than the workflow's declared `name:` (`Code Quality`). Also fixes a 9th dead reference a `uses:`-only grep misses: the `additional-apps` input cloned `https://github.com/Conduction/openregister.git`, which would have 404'd inside the PHPUnit / Newman / Playwright legs even after the `uses:` fix. `.forgejo/workflows/` is deliberately untouched — on Codeberg the org really is `Conduction`, so those refs are correct. * fix(deps): resync package-lock.json so `npm ci` resolves again Every npm-based quality job (ESLint, Stylelint, License (npm), Security (npm)) died before running a single check: npm error `npm ci` can only install packages when your package.json and npm error package-lock.json ... are in sync. npm error Missing: pinia@4.0.2 from lock file npm error Missing: vite@8.2.0 from lock file ... + the whole esbuild/rolldown/lightningcss platform-binary fan-out Cause: `@nextcloud/vue` carries a nested `vue-router@5.2.0` whose `peerDependenciesMeta`-optional peers are `pinia: ^3.0.4 || ^4.0.2` and `vite: ^7.3.0 || ^8.0.0`. Those versions have since been published, so npm's ideal tree now includes them while the committed lockfile predates them. The lockfile was simply never regenerated — nobody noticed, because the workflow that would have caught it produced zero jobs. Regenerated with the npm major this repo pins (`engines.npm: ^10.0.0`) on Node 20. npm@11 must NOT be used here: it reports this lockfile as "up to date" and produces one that npm@10 `ci` then rejects. The diff is additive and does not touch the app's own dependency graph: - 0 packages removed - 0 nested version changes - 1 version change: @napi-rs/wasm-runtime 1.1.6 -> 1.2.2 (a wasm shim) - ~60 additions, all platform binaries pulled in by the optional peers above Vue, pinia (top-level 2.3.1), @nextcloud/vue and @conduction/nextcloud-vue are all unchanged, so the produced bundle — and the e2e runs verified against it — are unaffected. Fixed by resyncing rather than by `--legacy-peer-deps` or an `overrides` block: suppressing the resolution would leave the lockfile lying about the tree that `npm ci` actually builds. * fix(quality): clear everything the revived Code Quality gates surfaced With the workflow refs and lockfile fixed, all 23 jobs actually run. This clears what they found. Stylelint (40 errors, was blocked behind `npm ci`) `rule-empty-line-before` across ThemePickerDialog, WorkflowAttachmentDialog and app.css. Applied the repo's own `stylelint-fix`. PHPCS (14 errors) Alignment, control-structure spacing and long-condition closing comments. Applied the repo's own `phpcbf`, then hand-fixed the two it cannot: an uncapitalised inline comment and a missing docblock on `AppRepoParser::parseCompanionSchemas()`. PHPStan — and the reason nobody saw these `composer phpstan`, `psalm` and `phpmd` all ended in `|| echo '<tool> not installed, skipping...'`, so they exited 0 no matter what. The CI job for PHPStan reported SUCCESS on the previous run while the log contained `[ERROR] Found 2 errors` and two `##[error]` annotations — a gate that runs, finds real problems, and reports green. Dropped the swallowing `|| echo` from phpstan and psalm. Both are now genuinely clean: - AutomationCompilerService::apply() had a stray early `return $provenance;` that orphaned the block below it. The dead half is where the load-bearing "DO NOT tidy this back into `'ruleSetSlug' => $ruleSetSlug`" comment lives — precisely the warning a future reader needed, stranded in unreachable code. Behaviour was unaffected (both halves were identical); removed the duplicate and kept the documented one. - SetupController::runAction() applied `?? 0` to `$result['updated']`, which the seeder's return shape guarantees is always a non-null int. Psalm (2 errors) `GenericHealthController` / `GenericMetricsController` are bound as string class names in lazy closures and resolved at request time. psalm.xml already carries a documented per-class allow-list for exactly these dynamically loaded OpenRegister classes; these two were simply missing from it. Added them there rather than suppressing at the call site. Features Check `docs/features.json` predated `openspec/features.overlay.json`, which the extractor treats as the authoritative curated list. Regenerated. NOT fixed — stated plainly rather than hidden `composer phpmd` keeps its `|| echo` fallback. Removing it exposes 15 violations that the committed `phpmd.baseline.xml` does not cover (TooManyMethods, ExcessiveClassComplexity, 3x CyclomaticComplexity, ExcessiveMethodLength, ElseExpression, ...). Those need real refactoring of GitHubAppSyncService / TemplateSeedService / IconService and should not ride along in a CI-restoration PR. I did NOT regenerate the baseline to bury them. The fallback message now says so out loud instead of claiming PHPMD is "not installed". This is the top follow-up. * fix(deps): pin @conduction/nextcloud-vue 2.1.0-vue3.13 to get off proprietary vue3-apexcharts The revived licence gate caught a real exposure: `vue3-apexcharts` was MIT up to and including 1.8.0, and at 1.9.0 (2025-10-13) moved to a proprietary dual-license — free only under $2M USD annual revenue, forbidding "sublicensing under different terms" (which is what redistributing inside an EUPL-1.2 app is) and requiring a paid OEM licence for "No-code dashboards", "Embedded BI tools" and "White-labeled apps or SDKs". Not the same as the `apexcharts` entry in .license-overrides.json: that one is a genuine license-checker misread of an MIT package, and the core apexcharts@4.7.0 is still MIT. Only the Vue 3 wrapper changed licence. The identical `Custom: <image-url>` symptom made them look alike. The dependency is transitive through @conduction/nextcloud-vue, and the fix already existed upstream: 2.1.0-vue3.7 -> vue3-apexcharts ~1.10.0 (proprietary) 2.1.0-vue3.13 -> vue3-apexcharts ~1.8.0 (MIT) This repo declared `^2.1.0-vue3.7`. A caret does not move an already-resolved prerelease in a lockfile, so the pin held us on the proprietary line even though a fixed release had shipped. Now pinned EXACTLY, without a caret — a caret floats prereleases, and that is how this drifted in the first place. Verified from the lockfile rather than the manifest: - node_modules/vue3-apexcharts: 1.10.0 -> 1.8.0 - exactly one copy in the tree; zero 1.9.0+ entries at any nesting depth - installed package.json now declares `"license": "MIT"` (was `"see LICENSE in LICENSE"`), which is what license-checker reads No override was added. .license-overrides.json is unchanged and contains no vue3-apexcharts entry — the gate passes because the dependency changed. Lockfile churn is exactly two entries: the nc-vue bump and the apexcharts downgrade. 0 additions, 0 removals, 0 nested version changes. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…file set (#80) * fix(app-repo): fetch the v2 channels — the parser was being fed a v1 file set fetchRepoFiles() only ever fetched the descriptor, manifest and schemas/. The serializer emits data-registers/, connectors/, automations/ and skills/, and the parser knows how to read them — but nothing fetched them, so a v2 repository installed as if it carried nothing but a manifest, and reported success. Caught by round-tripping the real published artefacts, not by tests: buildiq-spectr fetched 2 files -> 0 data-registers, 0 connectors (the repository holds 46 blobs) buildiq-hydra fetched 2 files -> 0 skills (the repository holds 748 blobs, 94 skills) After the fix: buildiq-spectr 45 files -> dataRegisters 1, connector kinds 4 buildiq-hydra 746 files -> dataRegisters 1, skills 94 Uses ONE recursive tree call rather than a contents walk per directory: a v2 artefact can carry ~750 blobs and per-directory listing would multiply round trips before a single file is read. Bounded at 2048 with truncation LOGGED — an install that quietly drops half an app is the exact failure this format exists to prevent. * fix(app-repo): declare missingCount in the collectConnectors() return shape phpstan level 5 rejects reading $connectors['missingCount'] at line 185 because the annotation never declared it — although both the real return and the $empty early-return have always contained it. The docblock was the thing that was wrong. Pre-existing on development, not introduced here: this PR touches only GitHubCatalogService. It surfaced because openbuild has no vendor/ checked out, so the local gate suite never runs phpstan and CI is the first place it fires. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
) Every openbuild schema declared "authorization": { "create": ["admin"], "update": ["admin"], "delete": ["admin"] } — non-empty, and with no `read` key. OpenRegister treats that as fail-closed: `MagicRbacHandler::buildRbacConditionsSql()` bypasses filtering only for an EMPTY block; a populated block with no `read` rule falls through to the owner condition alone, so every non-admin caller saw zero rows. Not a bug in OR — deliberate, and commented as such at MagicRbacHandler:1031. That is the whole of #76. An owner could grant a colleague editor or viewer on an app and they still saw an empty list, because OR filtered the objects out one layer below openbuild's own permission check. Adds `"read": ["authenticated"]` to all 15 schemas — 6 in the monolith and 9 across the register.d fragments, which were missed by the first pass and would have left business rules, automations, component blocks and the agent workspace owner-only. `authenticated` requires $userId !== null (MagicRbacHandler:414), so anonymous callers are NOT granted. This is intentionally the coarse layer: appinfo/routes.php already documents that OR's schema read rule is a group ACL, not a row filter, and that the per-app `permissions` block is enforced by /api/applications. Both layers verified live. Measured on the disposable instance after a FORCED re-import: caller OR object API openbuild /api/applications admin 21 21 rbac-editor 0 -> 21 1 (granted editor on pw-verchain) rbac-viewer 0 -> 21 1 (granted viewer on pw-verchain) rbac-outsider 0 -> 21 0 (no grant) anonymous 0 401 Diagnosis trail: ConductionNL/openregister#2252. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…then dropped (#85) * feat(app-repo): apply the v2 channels on install — they were parsed, then dropped Six steps carry an app between instances: serialize -> bind -> push -> fetch -> parse -> apply. Five were built. The sixth never was, so installing a published v2 app produced an app holding its manifest and NOTHING that makes it run, and reported success. Verified against the code with a positive control before writing any of this: $template['manifest'] / ['version'] (control) 7 hits $template['connectors'] 0 $template['automations'] 0 $template['skills'] 0 $template['dataRegisters'] 3, all in the export/zip path Both entry points confirmed by reading them: pull() persisted manifest + companion schemas only; installFromTemplateArray() read exactly slug + manifest. This is the fourth time in this programme that one half of a round trip was extended and the other left behind. Publish looked perfect every time, because publish is the half that kept getting extended. Three rules shape the implementation: NEVER OVERWRITE. Connectors are shared infrastructure - one source can serve several apps - so a colliding uuid is skipped and reported. Enforced with saveObject(failIfExists: true) so the guarantee lives in the call rather than in a preceding existence check that could drift or race. NEVER CLAIM ATOMICITY. OpenRegister has no cross-object transaction, so one failing item must not cost the caller the rest. ChannelApplyReport enforces created + skipped + failed === declared and THROWS when it does not hold, so a dropped item is arithmetically impossible to hide. The 64-skill silent cap this programme already shipped is what that identity exists to prevent. NEVER DROP SILENTLY. Every channel is bounded; truncation is logged AND counted. Two defects found while writing the tests, both of which would have shipped: 1. Collision was detected by MESSAGE TEXT. A plain PHP 'Unknown named parameter $failIfExists' error therefore reported itself as a benign 'already exists' - a wiring bug wearing the costume of an expected outcome, and the reason three tests were briefly green for the wrong reason. Now caught BY TYPE (ObjectExistsException), and the stale test stub that hid it is fixed. 2. The credential lookup called findAll(filters:, register:, schema:), which is not the real signature - findAll takes a $config array. credentialExists() swallows a failed lookup and returns true (an inconclusive lookup must never manufacture an absence claim), so needsCredentials would have been silently empty forever. Register/schema confirmed against the live instance (credential-broker / brokeredcredential) with a positive control, not assumed. Skills delegate to hermiq's SkillBundleInstaller by repo coordinates rather than being reimplemented, so frontmatter byte-fidelity and the ADR-068 aux-file rules keep living in exactly one place. openconnector and hermiq stay OPTIONAL - OpenBuild declares only openregister - and degrade with a machine-readable reason while every other channel still applies. The collision test is mutation-checked: flipping failIfExists to false turns it red. phpstan is run explicitly, because openbuild ships no vendor/ and the local 40-gate suite silently SKIPS phpstan - green there never meant phpstan passed. * fix(gates): @SPEC on the new report methods, repoint archived anchors, declare persistApplication throw gate-16 wanted @SPEC on every new public method. gate-46 and gate-49 were PRE-EXISTING on development (36 identical dangling anchors; persistApplication untouched) and surfaced only because this change touches the file — fixed here per the repo rule rather than left for later. The 36 anchors pointed at retrofit-2026-05-24-annotate-openbuild, which has since been ARCHIVED; repointed to the archive path. persistApplication already wraps its save — the unguarded call is normaliseObject() AFTER it, so the contract is now declared rather than the behaviour quietly changed. * fix(gates): document persistApplication exception contract for gate-49 The save is wrapped in catch(Throwable), which covers OpenRegister's ValidationException and DoesNotExistException — the gate's heuristic matches on named exceptions and cannot see that a Throwable catch subsumes them. Documenting the actual contract is what the gate is for, so it is now stated rather than the behaviour changed to suit the checker. * fix(app-repo): read channels where the parser ACTUALLY puts them, plus psalm/phpcs THE BUG THIS COMMIT FIXES WOULD HAVE MADE THE WHOLE CHANGE A NO-OP. AppRepoParser nests the v2 channels under `$payload['channels']`. The applier read them from the top level, so every channel resolved to [] and reported `declared: 0` — an install that does nothing and returns success, which is precisely the failure this change exists to end. Found by reading the parser while preparing the live probe, NOT by the unit tests: I had written the test fixtures in the same wrong shape, so tests and implementation agreed with each other while both disagreed with the real producer. The durable fix is the new test, not the one-line change: testAppliesTheChannelShapeTheParserActuallyProduces drives the REAL AppRepoParser over a v2 file map and feeds its output straight into the applier, so the two shapes cannot drift apart again without a red suite. Mutation-checked — reverting channelOf() to the top-level read turns it red. Lesson worth keeping: a hand-written fixture that mirrors the implementation's assumption cannot detect a shape mismatch with the real producer. It only ever tests that the code agrees with itself. Also: adoptCounts() now takes truncated as the BOOL it is (hermiq knows truncation happened, not how many items it missed) and absorbs any shortfall between our declared count and the source's outcomes as a NAMED skip, so the balance identity holds and the cause is stated rather than the difference silently disagreeing. psalm needed ObjectExistsException on the cross-app suppression list; two inline comments needed capitals. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…n the RBAC suite (#84) * fix(rbac): grant authenticated read on every openbuild schema (#76) Every openbuild schema declared "authorization": { "create": ["admin"], "update": ["admin"], "delete": ["admin"] } — non-empty, and with no `read` key. OpenRegister treats that as fail-closed: `MagicRbacHandler::buildRbacConditionsSql()` bypasses filtering only for an EMPTY block; a populated block with no `read` rule falls through to the owner condition alone, so every non-admin caller saw zero rows. Not a bug in OR — deliberate, and commented as such at MagicRbacHandler:1031. That is the whole of #76. An owner could grant a colleague editor or viewer on an app and they still saw an empty list, because OR filtered the objects out one layer below openbuild's own permission check. Adds `"read": ["authenticated"]` to all 15 schemas — 6 in the monolith and 9 across the register.d fragments, which were missed by the first pass and would have left business rules, automations, component blocks and the agent workspace owner-only. `authenticated` requires $userId !== null (MagicRbacHandler:414), so anonymous callers are NOT granted. This is intentionally the coarse layer: appinfo/routes.php already documents that OR's schema read rule is a group ACL, not a row filter, and that the per-app `permissions` block is enforced by /api/applications. Both layers verified live. Measured on the disposable instance after a FORCED re-import: caller OR object API openbuild /api/applications admin 21 21 rbac-editor 0 -> 21 1 (granted editor on pw-verchain) rbac-viewer 0 -> 21 1 (granted viewer on pw-verchain) rbac-outsider 0 -> 21 0 (no grant) anonymous 0 401 Diagnosis trail: ConductionNL/openregister#2252. * test(e2e): un-skip versionRouting 9.2 and record the measured blockers on the RBAC suite 9.2 — ENABLED, four scenarios replacing one that asserted almost nothing. Two things unblocked it. globalSetup now provisions the rbac-* fixture users and mints one storageState each, so no spec form-logs-in (four consecutive logins is exactly what trips Nextcloud's brute-force throttle) — `loginAs` is gone from this file. And openbuild#76: until every schema carried a `read` rule, non-admins saw ZERO objects, so "viewer gets 404" passed for the wrong reason and no 200 assertion was reachable at all. The old body could not fail meaningfully. It located the schema list with `.ob-schema-list` / `[data-testid="schema-list"]`, neither of which exists in src/, so "must NOT be visible" held on any page including a correct one; and it downgraded a missing not-found UI to a console.warn. Now asserted: - viewer + staging -> 404 with the body pinned EXACTLY, plus a regex check that the envelope names no authorisation reason - non-member + staging -> byte-identical to the viewer's, and identical again to an unknown version slug (that indistinguishability IS REQ-OBVR-003) - editor + staging -> 200 with a manifest. The positive control: without it a broken fixture, a missing chain or a blanket denial all look like a pass - viewer UI -> `.openbuild-schema-list` (the REAL selector) absent, no stack trace Setup grants the roles via grantAppRoles(), which had been merged with no caller — so the editor control now actually exercises an editor. schema-access-scopes-rbac — STAYS SKIPPED, with honest blockers. Its three recorded blockers are all resolved (fixture users exist; the version chain is seeded; the feature and its copy are real — the warning is an NcNoteCard sibling of .openbuild-access-editor, and `.note-stub` never existed). A fourth, found by driving it rather than reading it, is not: the schema designer is unreachable for a non-admin. rbac-editor lands on the first-time setup wizard with `.openbuild-schema-list` count 0, because /api/setup/status is admin-only and useSetupStatus read its 403 as "nothing done". Fixed upstream in ConductionNL/nextcloud-vue#574; unblocks on a published bump. The comment also records a defect found while measuring, filed as #83: availableGroups feeds the dropdown `group:`-prefixed values while authorLockedOut compares bare gids, so the lock-out warning fires for members too — exactly the REQ-OBDSA-004 scenario. Noted so this suite is not simply un-skipped and declared green once the wizard blocker lifts. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…, default off) (#82) * fix(events): resolve the schema slug listeners compare against (gated) ProductionVersionGuardListener and AutomationCleanupListener each carried a private extractSchemaSlug() that probed for ObjectEntity::getSchemaSlug() — a method that does not exist — and then fell back to `@self.schema`, which is the schema's numeric id. Both then compared that id with `!==` against a slug literal ('application', 'automation'), so the comparison was always true and neither handler body has ever executed. No exception, no log line. Replaces both helpers with one shared ObjectSchemaSlugResolver that resolves the id to a slug via SchemaMapper::find() (request-cached, and memoised here including misses so a hot write path does not become an N+1). The resolver matches the REGISTER as well as the schema. A schema slug is not unique instance-wide: this instance carries two distinct schemas with the slug `automation` (ids 71 and 5103), so matching on the schema slug alone would fire OpenBuild's handlers for another app's objects. Mirrors the register+schema pair pattern already shipped in petstore and planix. GATED, DEFAULT OFF — `openbuild.listener_slug_contract`. Correcting the comparison is not behaviour-neutral. ProductionVersionGuardListener is a FAIL-CLOSED validation guard: it fails OPEN today, so mismatched production versions are never blocked, and waking it starts REJECTING writes that currently succeed. AutomationCleanupListener starts DELETING compiled artifacts on automation delete. Neither path has ever run, so neither has been exercised against real data. The flag lets the fix ship and be reviewed without switching all of that on in one deploy, mirroring openregister#2248's approach to the sibling ObjectTransitionedEvent defect. Not addressed here: DocumentGenerationListener and AutomationApprovalTriggerListener have the same id-vs-slug defect via schemaOf(), but they feed the id into automation trigger matching rather than a literal comparison, so they need a separate change. * test(events): repair the constructor break and pin the default-off gate Both listeners gained two constructor params (the slug resolver and the opt-in contract) but neither test was updated — 7 ArgumentCountError errors, so CI was red on all six PHP/NC combinations. The old tests were also not a control. They fed `'@self' => ['schema' => 'automation']` — a SLUG — where MagicMapper writes a numeric ID, which is the exact reason these listeners never ran in production. Passing the fixture the production shape would not produce is how a dead listener tests green. The fixtures now carry ids and the resolver decides, which is what the shipped code does. Added a default-off test per listener asserting that nothing happens at all — not even the slug lookup. That is the merge-safety assertion: the production version guard is fail-closed and currently fails open, so waking it starts rejecting writes that succeed today. 739 tests, 0 failures (was 737 with 7 errors). --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…ns can use the app (#87) Picks up ConductionNL/nextcloud-vue#576: a 401/403 from /api/setup/status now empties the unmet-steps lists, so a non-admin is no longer shown the first-time-setup wizard. Why this bump matters more than the version delta suggests: after #81 gave non-admins OpenRegister-level read on the schemas, an editor still could not use OpenBuild. They landed on "Welcome to OpenBuild / Set up this app" with .openbuild-schema-list count 0 — the setup endpoints are admin-only, they answer 403, and useSetupStatus read that as "setup unfinished". Also corrects a drift found on the way: node_modules held 2.1.0-vue3.7 while package.json and the lockfile both said 2.1.0-vue3.13. Nobody had run an install since that bump, so local builds were linking a library three versions behind the pin. This commit was built from a clean `npm ci`. New: tests/e2e/non-admin-access.spec.ts, a regression suite for the OUTCOME rather than any one layer. Two consecutive fixes for this looked green while the app stayed broken for the user — openbuild#76's grant, then a nc-vue fix that short-circuited `completed`, which CnAppRoot never reads. The only assertion that would have caught both is "a non-admin sees the app", so that is what these three tests assert: - an editor reaches the schema designer AND no setup wizard (both halves — asserting the wizard's absence alone passes on a blank page) - an editor sees the app they were granted - an outsider sees none — the control proving openbuild's row-level filter still runs on top of OR's coarse `authenticated` read grant Verified live against the disposable instance: 3 passed. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…he list pagination (#86) (#88) * test(e2e): scope wizard buttons to the dialog — "Next" also matches the list pagination All 8 createApplicationWizard scenarios failed, reproducibly, with the wizard's Next button "visible, enabled and stable" and every click swallowed: <div class="dialog__actions"> from <div ... data-testid-modal="cn-wizard-dialog"> subtree intercepts pointer events That reads as a broken dialog. It is not. `getByRole('button', { name: /^next$/i }) .first()` was never finding the wizard's Next at all. The applications list BEHIND the modal renders a pagination control whose button is also labelled "Next" (`.cn-pagination__nav`). It comes first in DOM order, so `.first()` took it. Measured at the moment of failure: Next #1 y=1318 secondary .cn-pagination__nav <- picked, off-screen Next #2 y= 616 PRIMARY .dialog__actions [cn-wizard-dialog] Viewport is 720px tall. Playwright judged the pagination button visible and enabled (it has a box and is not display:none), scrolled to it, and the modal overlay then intercepted the click — so the error named the DIALOG while the target was a page element underneath it. It only started failing once the seeded fixture apps grew past one page and the pagination appeared, which is why it looked like a wizard regression. It is not version-related either: reproduced identically on @conduction/nextcloud-vue 2.1.0-vue3.7 and 2.1.0-vue3.15. Every wizard action button is now looked up inside `[data-testid-modal="cn-wizard-dialog"]` via a `wizard(page)` helper — Next, Create and Back. "Add app" stays page-scoped; it genuinely lives on the page. 8/8 pass. Closes #86. * fix(schema-designer): offer bare group ids — a `group:`-prefixed scope granted nobody Application permission buckets carry `user:<uid>`, `group:<gid>` or a bare gid (useRole.js). `availableGroups` filtered out only the `user:` form and passed `group:rbac-editors` straight through to the Access editor's dropdown — so the value an admin picked was also the value written into the schema's read rule. OpenRegister matches read rules against getUserGroupIds(), which returns BARE gids. A prefixed rule therefore matched nobody: any group scope configured through this UI silently denied everyone, including the person who set it. Measured on a live instance against the same schema, restoring between runs: read: ["authenticated"] -> a member of rbac-editors saw 22 objects read: ["group:rbac-editors"] -> the same member saw 0 read: ["rbac-editors"] -> the same member saw 22 The same mismatch made `authorLockedOut` fire for members, because it compares the selected values against getCurrentUserGroups() (also bare) — so the REQ-OBDSA-004 "a member editor sees NO warning" scenario could never pass. That is the symptom this was filed under; the dead scope underneath it is worse. availableGroups now strips the prefix, leaves an already-bare gid alone, and dedupes when a group appears in both forms. Every pre-existing test in SchemaDesigner.access.spec.js seeded permissions with bare gids, which is why 13 green tests sat over a scope that granted nothing in production. Added 4 covering the prefixed form, including the member-sees-no-warning case. 17/17 pass. Closes #83. * test(e2e): replace two guessed blocker notes with measured ones Neither suite changes behaviour; both had blocker comments that were partly wrong, and a wrong blocker is worse than none — it sends the next person to fix something that is already fixed, or to retarget a selector when the fixture is what is missing. schema-access-scopes-rbac — two of the five blockers are now gone: - the designer is reachable for non-admins (nc-vue 2.1.0-vue3.15; the earlier #575 attempt was inert, guarded now by non-admin-access.spec.ts); - prefixed gids are normalised (#83). What actually blocks it is a DRAFT-VERSION SCHEMA. Measured: the version chain carries exactly one schema, `pw-verchain-production-hello-message`, and no `pw-verchain-staging-*`. Every remaining scenario needs a non-production copy — 004/007 because production is owner-only read-only by design, 006 because it compares the two copies. versionChain.ts creates VERSIONS, not their schemas. version-rollback — the file said retargeting two selectors would be enough. It would not. Driven live against the full pw-verchain chain with the tab mounted: .ob-versions-tab 1 (the tab IS there) .version-history 1 (the component renders) .version-history__empty 1 (and renders EMPTY) .version-history__row 0 .version-history__btn--danger 0 VersionHistory lists PUBLISH SNAPSHOTS, not ApplicationVersions. The old note had the dependency backwards — rows come from publishing, not from having versions — so a three-version app still shows zero. What it needs is a fixture that publishes at least twice, which is new work, not a retarget. Also recorded there, since reading cannot reveal it: the detail route takes the UUID not the slug, and the sidebar tabs render WITHOUT clicking .app-sidebar__toggle — a toggle click times out at 5s because they are already open, so the original "open the sidebar first" step actively hangs. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…n a field the response lacks (#89) VersionHistory fetches `/apps/openbuild/api/applications/{slug}/versions`, then filtered the result with raw.filter(r => r.applicationUuid === this.applicationUuid) as "IDOR defence-in-depth". That endpoint does not return `applicationUuid`. Measured: GET /api/applications/pw-verchain/versions -> 3 rows, each { name, slug, manifest, manifestDelta, baseRef, register, semver, status } — no applicationUuid on any of them ApplicationVersionsTab passes BOTH app-slug and application-uuid, so the filter removed every row and the "Version history" tab rendered `.version-history__empty` for every application, always. Filtering a server-scoped response against a field that response does not carry is not defence in depth, it is an unconditional deny. The filter now applies only to the unscoped `/applicationversions?applicationUuid=` endpoint, where the field does exist and the check is meaningful. Verified live before the instance was lost: 3 rows rendered, empty-state gone. This also corrects a claim I committed earlier in version-rollback.spec.ts — that VersionHistory "lists publish SNAPSHOTS, not versions, so it needs a fixture that publishes twice". That was wrong. It lists exactly the versions versionChain.ts already creates; they were being filtered out. versionRouting 9.2 — the viewer-UI assertion was wrong twice over: 1. It PASSED for the wrong reason. Before the setup-wizard fix a non-admin never reached the builder at all, so "no schema list" held because nothing rendered for anyone. 2. It cannot distinguish the roles. Measured side by side, the viewer (DENIED staging) and the editor (ALLOWED staging) render an IDENTICAL surface: `.openbuild-schema-list` count 1, reading "No schemas yet". No data leaks — the list is empty for both — so this is a UX gap (the builder renders no version-not-found state), not a security one. The assertion now checks what it can actually detect: that no schema of the forbidden version is NAMED. The gate itself is covered by the three request-level tests, which is where it is enforced. 6/6 pass. version-rollback.spec.ts — rewritten against the verified contract (ApplicationVersionsTab.onRollback: manifest copied over, version relabelled `<version>-rollback-<hex>`, status forced to draft), and left SKIPPED because it has never been executed: the disposable instance was destroyed by a disk-full event before it could run once. Enabling it is deleting one `.skip` — but do that with a run, not on the strength of the comment. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…, not the contract (#91) Ran it against the shared dev instance. It never reached its assertions, and the reason is not the rollback contract: aside.app-sidebar width 0 (closed) section.app-sidebar__tab[role=tabpanel] display:none button[aria-label="Open sidebar"] present AND visible -> clicking it TIMES OUT on actionability, at 1280x720 AND at 1920x1080 The sidebar will not open, so every tab-scoped assertion is unreachable — even though the tab's content is mounted underneath it. That is a UI defect to chase on its own, not something a selector change fixes. The product fix this spec depends on IS confirmed on both instances: `?tab=history` deep-links the tab and `.version-history__row` count is 3, where the panel previously rendered empty for every app. The data is right; only the chrome is unreachable. Three dead ends recorded so the next attempt does not repeat them: - getByText('Version history') resolves the tab button's LABEL SPAN, which is display:none once the tab strip collapses to icons — waiting on its visibility waits forever (18 resolutions, all "hidden"). FIVE nodes carry that exact text; the deepest is the panel's own <h3>. - getByRole('tab', …) finds nothing — these are not ARIA tabs. - [aria-label*="sidebar" i] matches "Close sidebar" FIRST; the control is labelled exactly "Open sidebar". And the one route that does work: /applications/{uuid}?tab=history mounts the tab content directly. Also carried over from the same run, both measured rather than guessed: - a 150s budget for this describe, from GET /api/applications taking ~6.9s on the shared box against ~0.3s on a disposable one (28 apps, 200+ schemas); - the sidebar's open/closed state is per-user UI state and differs between instances, so a fixed "open it first" or "click it directly" step is wrong either way. Stays skipped: its assertions remain unexecuted, and shipping an unexecuted spec as coverage is the failure mode this file already documents three times. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…unaccounted for" (#90) * fix(app-repo): count an idempotent skill re-install as applied, not "unaccounted for" A regression I introduced with hermiq's idempotent installer, caught by reading the seam rather than by any failing test. The applier read only `installed` from hermiq's response. Once hermiq stopped duplicating skills, a re-install of a bundle already present reports installed: 0 updated: 0 unchanged: 94 so the applier accounted for 0 of 94 declared items and absorbed the shortfall as "not-accounted-for-by-source" — a loud failure banner on a perfectly good run, and precisely the kind of false alarm that trains people to ignore the report. "Present as intended" is installed + updated + unchanged. The source's own breakdown is now carried through unflattened in `sourceCounts`, so a first install and a no-op re-run stay distinguishable instead of collapsing into one total. Mutation-checked: reverting to `created: $installed` turns the new test red (0 vs 2). The skills channel moved into its own SkillChannelDelegate — talking to another app across an optional-dependency boundary is a different responsibility from applying OpenRegister channels, and phpmd flagged the applier at 51 > 50 when the mapping was added inline. A real split, not a suppression; the applier's tests compose the REAL delegate so the degradation and count assertions keep biting. 754 tests OK, phpstan 0, phpcs 0, phpmd 0. * style: capitalise an inline comment (phpcs) --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…rontend jobs with it (#92) Six jobs have been red on development: Vue Quality (eslint), Vue Quality (stylelint), License (npm), Security (npm), SBOM, and Quality Report. They share one cause, and it is not any of the tools they are named after. Every one of those jobs runs `npm ci` first, and `npm ci` refused to install: npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync. npm error Missing: pinia@4.0.2 from lock file npm error Missing: vite@8.2.0 from lock file npm error Missing: esbuild@0.28.1 from lock file So no linter ever ran. The jobs were not reporting lint failures — they were reporting that the install step died before reaching a linter, which reads identically on the dashboard. The missing entries are TRANSITIVE: nothing declares pinia@4 or vite@8, and the declared ranges are ^2.1.7 and ^5.4.0. They arrived with the @conduction/nextcloud-vue 2.1.0-vue3.15 bump (#87), whose lockfile regeneration did not capture the new transitive graph. This is invisible locally: an existing node_modules already contains the packages, so `npm run lint` passes on a developer machine and `npm ls` reports the tree resolves. Only `npm ci` — which installs from the LOCKFILE rather than the tree — can see it. Reproduced under CI's node 20 rather than the local node 22. The diff is purely additive and verified as such: declared dependency versions changed: 0 packages added: 63 packages removed: 0 Verified under node:20 after the fix: npm ci OK (previously EUSAGE) npm run lint clean npm stylelint clean npm audit --audit-level=critical --omit=dev exit 0 (10 vulns: 5 low, 4 moderate, 1 high — all below the critical gate) The one licence outside my first hand-written allowlist replica, domain-browser 5.7.0 (Artistic-2.0, via node-polyfill-webpack-plugin), is on the shared workflow's real allowlist — my replica was incomplete, not the licence. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
… the schema does not have (#94) `onRollback` did: obPatchApp({ manifest, version: `${v}-rollback-${hex}`, status: 'draft' }) `obPatchApp` PUTs the whole Application object at /api/objects/openbuild/application/{uuid}. The `application` schema declares 15 properties and NEITHER `manifest` NOR `version` is one of them, so OpenRegister dropped both. Only `status` survived — and since a rolled-back app is normally already a draft, the entire feature was a visible no-op. Measured live, with a control that changes a KNOWN property so "the write landed" is not assumed: PUT description: 'CONTROL_MARKER_XYZ789' -> persisted (PUT landed) version: 'PROBE-rollback-deadbeef' -> None (DROPPED) manifest: {probeMarker: ...} -> None (DROPPED) ApplicationManifestTab already hit this exact trap and moved to PUT /api/applications/{slug}/manifest, leaving a comment saying so. The versions tab was never updated. It now uses that same route; the endpoint round-trip is verified independently ({"status":"ok","target":"version"}). The `<version>-rollback-<hex>` label is deliberately NOT reinstated: there is no `version` property to hold it, and inventing one is a schema change, not a bug fix. `shortHex()` goes with it — the label was its only caller. WHY THE SPEC COULD NOT SEE THIS, AND WHY IT WAS SKIPPED FOR SO LONG The blocker note this file carried was wrong, and it was mine. It said the sidebar "refuses to open — a UI defect to chase on its own" and that `getByRole('tab', …)` "finds nothing — these are not ARIA tabs". What was actually on screen was a MODAL. CnAppRoot offers the setup wizard whenever every REQUIRED step is met but at least one OPTIONAL step is not (`optionalSetupGating`, REQ-SETUP-NV-012) and opens it as a full `modal-mask`. OpenBuild trips it permanently: its `store` step carries NO `required` key — the word "optional" lives only in the title string — so `optionalUnmet` is never empty. Users dismiss it once via localStorage; every Playwright context is FRESH, so it reopened in every test. That mask sat over the sidebar toggle (hence "visible but not actionable") and marked the background aria-hidden (hence the missing tab role). One cause, both symptoms. `document.elementsFromPoint(<centre of the toggle>)` named it in one probe, after three wrong theories. New `suppressSetupWizard()` helper, applied to the four specs that were exposed: this one, applicationDetailOverview, non-admin-access, versionRouting. THE SPEC NOW HAS TO BE ABLE TO FAIL Two defects in my own test, both caught before shipping: - Every seeded snapshot carries the same empty manifest, so "restored the snapshot" and "did nothing" were byte-identical and the assertion could never fail. It now plants a distinct manifest FIRST and asserts it is gone. - The target was read as "first non-production row in the API response", which is a DIFFERENT row than the first Roll back button on screen — so the spec failed against a WORKING fix. It now reads the semver from the DOM row that owns the button it clicks. It also compares menu/pages only: the GET returns an EFFECTIVE manifest with `runtime` injected and `name` added, so a whole-document match is impossible. And it gets its OWN fixture (`pw-rollback`). It mutates the active manifest twice per run, and versionRouting.spec.ts drives `pw-verchain` and reads /manifest?_version= off it. Three-way verified on the current spec: pre-fix FAILS (the planted manifest survives the rollback), fixed PASSES 2/2. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…ed a field the schema does not have (#95) * fix(rollback): rolling back restored NOTHING — it wrote to two fields the schema does not have `onRollback` did: obPatchApp({ manifest, version: `${v}-rollback-${hex}`, status: 'draft' }) `obPatchApp` PUTs the whole Application object at /api/objects/openbuild/application/{uuid}. The `application` schema declares 15 properties and NEITHER `manifest` NOR `version` is one of them, so OpenRegister dropped both. Only `status` survived — and since a rolled-back app is normally already a draft, the entire feature was a visible no-op. Measured live, with a control that changes a KNOWN property so "the write landed" is not assumed: PUT description: 'CONTROL_MARKER_XYZ789' -> persisted (PUT landed) version: 'PROBE-rollback-deadbeef' -> None (DROPPED) manifest: {probeMarker: ...} -> None (DROPPED) ApplicationManifestTab already hit this exact trap and moved to PUT /api/applications/{slug}/manifest, leaving a comment saying so. The versions tab was never updated. It now uses that same route; the endpoint round-trip is verified independently ({"status":"ok","target":"version"}). The `<version>-rollback-<hex>` label is deliberately NOT reinstated: there is no `version` property to hold it, and inventing one is a schema change, not a bug fix. `shortHex()` goes with it — the label was its only caller. WHY THE SPEC COULD NOT SEE THIS, AND WHY IT WAS SKIPPED FOR SO LONG The blocker note this file carried was wrong, and it was mine. It said the sidebar "refuses to open — a UI defect to chase on its own" and that `getByRole('tab', …)` "finds nothing — these are not ARIA tabs". What was actually on screen was a MODAL. CnAppRoot offers the setup wizard whenever every REQUIRED step is met but at least one OPTIONAL step is not (`optionalSetupGating`, REQ-SETUP-NV-012) and opens it as a full `modal-mask`. OpenBuild trips it permanently: its `store` step carries NO `required` key — the word "optional" lives only in the title string — so `optionalUnmet` is never empty. Users dismiss it once via localStorage; every Playwright context is FRESH, so it reopened in every test. That mask sat over the sidebar toggle (hence "visible but not actionable") and marked the background aria-hidden (hence the missing tab role). One cause, both symptoms. `document.elementsFromPoint(<centre of the toggle>)` named it in one probe, after three wrong theories. New `suppressSetupWizard()` helper, applied to the four specs that were exposed: this one, applicationDetailOverview, non-admin-access, versionRouting. THE SPEC NOW HAS TO BE ABLE TO FAIL Two defects in my own test, both caught before shipping: - Every seeded snapshot carries the same empty manifest, so "restored the snapshot" and "did nothing" were byte-identical and the assertion could never fail. It now plants a distinct manifest FIRST and asserts it is gone. - The target was read as "first non-production row in the API response", which is a DIFFERENT row than the first Roll back button on screen — so the spec failed against a WORKING fix. It now reads the semver from the DOM row that owns the button it clicks. It also compares menu/pages only: the GET returns an EFFECTIVE manifest with `runtime` injected and `name` added, so a whole-document match is impossible. And it gets its OWN fixture (`pw-rollback`). It mutates the active manifest twice per run, and versionRouting.spec.ts drives `pw-verchain` and reads /manifest?_version= off it. Three-way verified on the current spec: pre-fix FAILS (the planted manifest survives the rollback), fixed PASSES 2/2. * fix(exports): the Exports tab was empty for every app — the filter used a field the schema does not have ExportJobsList fetched its rows with '?filter[applicationSlug]=' + this.applicationSlug which is wrong twice over, so the list was empty for every application, always: 1. `export-job` declares 18 properties and `applicationSlug` is NOT one of them — it is `applicationUuid`. Nothing ever wrote a slug onto these objects, so no stored row could carry one. 2. The `filter[...]` bracket syntax is not what the endpoint reads. Measured against the same 5 stored jobs: ?applicationUuid=<uuid> -> 1 (correct) ?filter[applicationUuid]=<u> -> 0 ?_filter[applicationUuid]=<u> -> 5 (ignored entirely) Same failure as the rollback fix in #94 and the VersionHistory fix before it: querying a field the schema does not declare. The code names it, so it looks real; only the schema says otherwise. `applicationSlug` is still correct for the SUBMIT endpoint (/api/applications/{slug}/exports), so both props are kept and ExportJobsTab now passes the uuid alongside it. Rows were also keyed `:key="job.uuid"`. `uuid` is not a property either, so EVERY key was undefined — identical keys let Vue reuse the wrong <tr> as statuses change under the 2s poll. Keyed on the OR object id instead. globalSetup: "already exists" is not the same as "usable" Role accounts were provisioned with POST /cloud/users and OCS 102 ("already exists") was accepted as the state we want. On a long-lived instance the account may PREDATE this harness and carry a different password: creation is skipped, every later login fails, and the specs run that role UNAUTHENTICATED — which reads as a product change rather than a fixture gap. Measured on the shared dev box: rbac-owner/editor/viewer authenticated 200, rbac-outsider 401, and versionRouting's "non-member must receive 404" failed with 401. globalSetup now PROVES the credentials work and repairs them when they do not. Live: "rbac-outsider existed with a different password — reset 200, now usable". NOT fixed here, and it blocks the ZIP round-trip spec: a submitted export never leaves `queued`. Driven by hand — submit 202, the queue entry is consumed, `occ background-job:execute` reports "Job executed!" — and the job object still reads status=queued with no errorMessage. OR ships Service\Lifecycle\TransitionEngine and no "TransitionEngine unavailable" warning is logged, so the transition runs and simply does not advance the job. That is a product blocker underneath export-zip.spec.ts, and a DIFFERENT one than the reason recorded in that file. --------- Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
Aligns this app on the current nc-vue release and pins it EXACTLY — no
caret. A caret on a prerelease is how the fleet previously drifted onto a
proprietary `vue3-apexcharts`, so the range operator is removed rather
than merely retargeted.
Verified from the LOCKFILE (not package.json):
- `@conduction/nextcloud-vue` resolves to exactly `2.1.0-vue3.16`,
a single instance, no nested duplicate.
- `vue3-apexcharts` resolves to `1.8.0` — below the 1.9.0 boundary at
which that package became proprietary and stopped permitting
sublicensing, which our EUPL-1.2 apps require. nc-vue itself pins it
as a direct `~1.8.0` dependency, so the tilde cannot reach 1.9.0.
No licence override was added: core `apexcharts` is MIT and reports
an identical `Custom: <url>` symptom, so an override would mask the
real signal.
Install sequence: `rm -rf package-lock.json node_modules`, then
`npm install` (npm 11), `npm install --package-lock-only` (npm 10),
`npm ci` (npm 10).
Opt-ins evaluated (applied only where the app actually has the pattern):
- `@nextcloud/initial-state` `overrides` entry — NOT PRESENT in this
app, so nothing to drop. vue3.16 relaxes that peer to
`^2.2.0 || ^3.0.0`; this app already declared `^2.2.0`, which
satisfied the old peer too.
- local `vue/no-multiple-template-root: 'off'` — NOT PRESENT in this
app; the shared preset now disables it.
- e2e base-URL resolver — this app's resolver already refuses to
default to `localhost:8080`, so switching to nc-vue's shared one
would be churn without behaviour change. Left alone.
Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…e 3 lint gate
Both unit failures were tests still asserting behaviour that a later, correct
production fix had deliberately replaced. Neither could be noticed, because
no app's JS unit suite has ever run in CI — the shared `quality.yml`
`frontend-checks` input defaults to `"[]"` and the job is guarded on
`!= '[]'`, so the matrix always resolved empty and reported "skipping".
Baseline on the unmodified base at load ~3 was 2 failures, not 1:
SaveAsTemplateAction > openSaveAsTemplate gathers schemas + templates …
views/ExportJobsList > filters by the applicationSlug prop
1. SaveAsTemplateAction — the mock was call-ORDER dependent.
The spec queued one `axiosMock.get.mockResolvedValueOnce(...)` for the
templates read. `openSaveAsTemplate()` now issues TWO GETs: it resolves
the manifest from `/api/applications/{slug}/manifest` FIRST, then reads
the templates. The one-shot response was therefore consumed by the
manifest call, and the templates read fell through to the generic
`{ data: application }` default — which has no `results` array, so
`existingTemplates` came back `[]`.
Production is right. The manifest GET was added because an Application
record carries neither `manifest` nor `currentVersion`, so the old
`obApp.manifest` read always fell through to `{}` and saving ANY app as
a template was impossible. The mock is now routed by URL, which removes
the ordering dependency and lets both responses be asserted; the
manifest endpoint is now asserted explicitly too.
2. ExportJobsList — the spec pinned the exact bug that was fixed.
It asserted `filter[applicationSlug]=my-app`. Commit c1d22f4 (#95)
replaced that with a plain `?applicationUuid=`, for two independently
sufficient reasons measured against real stored jobs: `export-job`
declares no `applicationSlug` property at all, and the `filter[...]`
bracket syntax is not what the endpoint reads. Production is right; the
expectation is corrected, and both ruled-out shapes are now pinned
negatively so neither can come back.
No assertion was weakened, skipped, or given a longer timeout.
LINT — adopt `@conduction/nextcloud-vue/eslint`
`eslint --print-config` on this Vue 3 app showed ZERO `vue/no-deprecated-*`
rules armed and `vue/no-multiple-template-root` armed at `[2]`. Spreading
`conductionVue3Fixes` last arms 21 deprecation rules and disarms the three
inverted Vue-2 rules (the two hand-rolled local disables are now redundant
and were removed).
Positive control, because "0 deprecation findings" is meaningless without
one: injecting a `beforeDestroy()` hook into PageDesigner.vue errors with
the shared preset and is COMPLETELY SILENT on the base config — the same
configuration that let four `beforeDestroy` hooks survive openconnector's
Vue 3 migration as silent memory leaks. Sentinel reverted.
The preset surfaced 9 findings, all fixed (backlog is now zero, not
deferred): 5 `vue/v-on-event-hyphenation` + 4 `comma-dangle`.
The hyphenation fixes were applied by hand, never via `--fix`, and only
after proving they are semantics-preserving. `@vue/compiler-sfc` compiles
BOTH `@update:dataSource` and `@update:data-source` to the identical
handler key `"onUpdate:dataSource"` (Vue 3 camelizes a static v-on
argument). The removed config comment asserted the opposite — that the
hyphenated form is "silently DEAD" — and that claim is false. The four
edited components each have their own passing specs.
Verified by deliberate break (both reverted):
- templates response -> SENTINEL: reds SaveAsTemplateAction
- applicationUuid prop -> SENTINEL: reds ExportJobsList
Positive control for the 0/0 lint baseline: an injected `var` + unused
binding reports 2 errors, so the clean baseline is real and not a
misconfigured lint run.
After: 140 files / 1364 tests pass, twice, at load ~3-4.
`npm run lint`: 0 errors, 0 warnings.
…eset fix(tests,lint): two stale specs pinned removed contracts; arm the Vue 3 lint gate
…ing violations (#99) Removes the '|| echo' from composer phpmd and regenerates phpmd.baseline.xml as a ratchet.
test:all (and test:unit where present) ended in '|| echo Tests require Nextcloud environment, skipping...', so phpunit's exit status was discarded unconditionally and check:strict could never fail on a test failure. This repo's suite runs standalone, so the message was untrue here. Positive control, PHP 8.3.32 container, vendor/ freshly installed, adding one deliberately failing test under tests/: old composer.json + failing test -> test:all never named, swallowed new composer.json + failing test -> check:strict fails NAMING test:all new composer.json, test removed -> test:all passes Also passes --no-coverage: this repo's phpunit.xml requests coverage, and without a driver PHPUnit warns and exits non-zero, which would have made the newly-live gate red for an environment reason rather than a code one. shillinq and decidesk already do this. Tooling only - no findings fixed here.
…paced code (#102) Every Conduction repo enables rulesets/design.xml/DevelopmentCodeFragment, and it has never reported anything in any of them. The cause is a config gap, not a phpmd bug: PDepend resolves an unqualified call inside a namespaced file to the current-namespace-qualified image, so `var_dump($x)` written inside `namespace OCA\MyApp\Service;` reaches the rule as `OCA\MyApp\Service\var_dump` and never matches the `unwanted-functions` list. All of our production PHP is namespaced, so with the default the rule is dead. The rule's own `ignore-namespaces` property is the switch. This mirrors the configuration already merged in openregister (ConductionNL/openregister#2286). Proof, phpmd 2.15.0 / PHP 8.3.32, against this repo's own phpmd.xml: namespaced probe class calling var_dump() -> exit 2, DevelopmentCodeFragment same class with the call removed -> exit 0, no finding Before the change the identical namespaced probe exited 0. Blast radius on this repo: measured 0 new findings over the scanned path on the base branch, with a per-run positive control (dropping the namespaced probe into the same extracted tree does produce exit 2, so the zero is a true zero). Nothing is baselined or suppressed here.
…-> 2026-08-01) (#103) The lockfile pinned roave/security-advisories to a commit from 2026-03-20, so the metapackage's conflict rules — and therefore the protection against installing known-vulnerable dependency versions — were frozen at that date. Refreshed to the 2026-08-01 tip. composer audit --locked: clean before and after.
Jobs without timeout-minutes fall back to GitHub's 360-minute default, so a hung runner burns six hours of Actions minutes before it is reaped. Bounds are derived from observed run durations and left deliberately loose: a timeout that fires under normal contention is worse than no timeout, because it turns a slow run into a phantom defect. Jobs that only call a reusable workflow (job-level `uses:`) are untouched -- they inherit their bound from the called workflow.
…he app-local proxy (ADR-080) StoreController now extends AppHost's GenericStoreControllerBase and inherits search() plus the SSRF-guarded, redirect-refusing, token-private fetch. RemoteTemplateStoreService (331 lines) and its test file are deleted; the behaviour and the SSRF controls live in OpenRegister's GenericStoreServiceTest. Install stays here, and only install — cloning a template into a local virtual app is OpenBuild-specific and differently authorized from the connector-adapter and agent-template installs in other apps. Three things the tests caught that review would not have: 1. The suite could not load StoreController at all — `extends` is resolved by the AUTOLOADER, not the container, and the OR AppHost classes are stubbed here rather than autoloaded. Added stubs for GenericStoreService, StoreDescriptor and GenericStoreControllerBase. This is the same mechanism by which a missing sibling app 500s EVERY route in production, which is why ADR-080 restricts subclassing to apps declaring openregister a hard <app> dependency — this app qualifies (8 controllers already type-hint OR classes). 2. The search test's `getParam` mock returned one value for ANY key, so `kind` silently received the query string. Made it key-aware; the action reads `q` and `kind` separately. 3. tests/stubs/openregister-stubs.php defines a NO-OP SecurityService, because the real guard does DNS lookups that fail for .test fixture hostnames. Any "SSRF negative control" written against that stub passes regardless of what the guard does. Corrected the docblock to say so, and to point at the OpenRegister suite where the real guard is exercised. The new store stub is deliberately non-behaving for the same reason. 743/743 unit tests, 7/7 on the store controller.
StoreController is a plain Controller injecting GenericStoreService, with its
own ~30-line search() action, rather than subclassing a cross-app base. The
inheritance broke phpstan ('extends unknown class', which it refuses to let you
ignore), psalm, and the unit suite's class loading — one stub entry now covers
the injected type-hint instead.
phpstan OK, psalm no errors, 743/743 tests.
…settings refactor(store): consume OpenRegister's GenericStoreService, delete the app-local proxy (ADR-080)
…110) Bumps @conduction/nextcloud-vue 2.1.0-vue3.16 -> 3.0.0-vue3.2, pinned exactly. The major bump is narrow. Diffing the packed tarballs, 3.0.0-vue3.2 removes exactly three symbols against every 2.1.0-vue3.x baseline (.13/.15/.16/.17/.19): CnFlowCanvas, CnEditFlowsModal and CnFlowCanvasModal — the old flow-authoring surface, which moved into OpenRegister's one flow store. From .16 it also ADDS CnFlowDetail, CnFlowEditModal and CnObjectAccessTab, and exports useFlowStore from the barrel. Nothing else in src/ changed: the only other edited file is CnOpenBuildEditButton, which internally swapped the two removed dialogs for CnFlowEditModal. No OpenBuild source imported any of the three removed symbols, so the bump needs no code change here. Verified in the built bundle rather than by assertion: after the rebuild neither removed symbol appears in js/, while CnFlowEditModal does — the positive control for that grep. Separately, four peers nc-vue has declared since at least 2.1.0-vue3.13 were never declared by this app and were resolving only by luck of hoisting: @vueuse/core, axe-core, dexie and marked. Declared them at the versions OpenRegister and OpenConnector already use. Verified from the lockfile, not package.json: nextcloud-vue is exactly 3.0.0-vue3.2, and vue3-apexcharts resolves to 1.8.0 — below the 1.9.0 that turned proprietary and cannot be sublicensed under EUPL-1.2. @nextcloud/l10n resolves to 3.4.1, so this app is not exposed to the v2 boot-killer where nc-vue's pre-bundled dialogs call the v3-only getGettextBuilder().detectLanguage(). Gates, all after a full rm -rf of node_modules and package-lock.json: npm install (npm 11) then npm ci (npm 10) both exit 0 on a 780 KB / 1717-package lockfile, eslint exit 0, 1364 unit tests in 140 files passing, and webpack exit 0 with only the pre-existing asset-size warnings. The build was run with USE_LOCAL_LIB=false so it measured the published package: this repo sits next to a nextcloud-vue checkout, and webpack.config.js aliases the sibling by default, which would have measured the wrong code entirely. Co-authored-by: Ruben van der Linde <juan.claude@conduction.nl>
…104) * build(quality): run the Hydra gates as part of composer check:strict openbuild is the pilot for conduction/hydra-gates. The gates have until now only run inside hydra's own containers, which means an agent finishing a task here could report done without any of the 61 mechanical gates having looked at its diff. Wiring them into check:strict makes "gates pass" part of the definition of done in the same command everything else already runs. The gates are diff-scoped per ADR-020, so this does NOT import openbuild's inherited debt. A full-repo run of this tree fails 16 gates today (img-alt, button-name, table-headers, spec-anchor-existence and friends); scoped to a PR's own diff, only what that PR touched is enforced. `composer gates:full` is available for the audit view, and is deliberately not what check:strict runs. check:strict keeps its 0/1 contract, but the gate exit code is preserved separately and reported, because it carries the failure COUNT and flows route on it. 99 is distinguished from a gate failure in the summary: it means the gates could not run at all, which is a configuration error and not a clean tree. The dependency is declared against a `path` repository at ../hydra, which is the fleet's checkout layout. That is the part of this change that is provisional — see the PR description; how hydra-gates should be DISTRIBUTED (private Packagist, a Satis mirror, or a split public repo) is an open decision, and a path repository is the shape that is verifiable today without one. * build(quality): lock conduction/hydra-gates at dev stability A path repository derives its version from the checked-out branch, so the default minimum-stability of 'stable' rejects a '*' constraint outright: 'found conduction/hydra-gates[dev-main, ...] but it does not match your minimum-stability'. '@dev' is the constraint that resolves, and it keeps working once the package is eventually tagged. * build(quality): resolve hydra-gates from the public package, not a path repo The `path` repository at `../hydra` is what blocked this PR. It works on a developer machine and fails in CI with `Source path "../hydra" is not found`, because the fleet's sibling-checkout layout does not exist in a CI job and hydra is private, so no runner can fetch it. conduction/hydra-gates now lives in ConductionNL/.github, which is PUBLIC and already owns the shared workflows (ConductionNL/.github#131). Pointing at it needs no credentials in this repo or on any runner. - repositories: `path ../hydra` -> `vcs https://github.com/ConductionNL/.github.git` with `"no-api": true`, so composer clones over git instead of the GitHub API and a rate-limited unauthenticated runner cannot become a failed install. - require-dev: `@dev` -> `^1.0`, resolving to the v1.0.0 tag. That also drops the dev-stability requirement the previous commit needed, because a path repository derives its version from the checked-out branch and a tag does not. - composer.lock pins commit fdad2546f2ac68aa64be5fecef898784c1847538. Nothing else changes. `composer gates`, `composer gates:full` and the gate handling inside `check:strict` are untouched, including the part that captures the gate exit code separately: it carries the FAILURE COUNT, and 99 ("could not run at all") is still reported distinctly from a gate failure so a configuration error can never read as a clean tree. Also merged origin/development in. The branch predated the fleet-wide timeout-minutes work, so it was carrying a silent revert of the bounds on exporter-e2e.yml and pull-request-lint-check.yaml. * ci(quality): actually run the gates in CI, not just in a local composer script Wiring the gates into `composer check:strict` was a local-only change. Nothing in this repository's CI invokes check:strict — no workflow does, verified by grepping .github/workflows — so this PR would have merged green with the 61 gates never having executed on any diff. A gate that runs only on a developer's machine is not a gate, and a green that never ran it is the failure mode the gates exist to catch. The shared quality workflow now ships a `hydra-gates` job (ConductionNL/.github#131), opt-in per repo and defaulting to off. openbuild is the pilot, so it opts in. `hydra-gates-ref` is pinned to the v1.0.0 tag rather than tracking `main`, so a change to the gates cannot move this repo's verdict without a commit here to move the pin. The job resolves the diff base from the PR's real target branch, checks out the app with fetch-depth: 0 (a shallow clone has no base to diff against), and reports exit 99 — "could not run at all" — distinctly from "N gates failed". * build(quality): bump hydra-gates to v1.0.1 for the gate-4 audit fix The first real CI run of the gates on this PR failed on gate-4: `composer-audit: FAIL — CVEs or advisories`. There are no CVEs — auditing this repo's lock properly reports "No security vulnerability advisories found." `composer audit` with no vendor/ present does not audit the lock. On the runner's composer it errors and exits non-zero, which gate-4 reported as a security finding. On composer 2.7.x the same situation prints "No packages - skipping audit" and exits 0 — a silent fail-open that passes having audited nothing. Fixed in the package (ConductionNL/.github#135, released as v1.0.1): gate-4 audits `--locked`, treats an exit 0 that audited no packages as a failure, and reports "could not run" in those words rather than as a CVE finding. Both the workflow pin and composer.lock move to v1.0.1 so the two paths into the gates cannot disagree about which version this repo is on. --------- Co-authored-by: Ruben van der Linde <juan.claude@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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.