Skip to content

ci(quality): move hydra-gates-ref to v1.5.0 — restore the gates - #287

Closed
rubenvdlinde wants to merge 1 commit into
developmentfrom
ci/hydra-gates-v150
Closed

ci(quality): move hydra-gates-ref to v1.5.0 — restore the gates#287
rubenvdlinde wants to merge 1 commit into
developmentfrom
ci/hydra-gates-v150

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

quality.yml is consumed @main and executes three scripts by path inside the pinned hydra-gates package:

scripts/lib/check_spec_anchors.py
scripts/lib/check_form_labels.py
scripts/lib/check_license_triangle.py

Verified by recursive tree listing per ref:

ref scripts present
v1.0.1 · v1.2.0 · v1.3.0 · v1.4.0 0 of 3
v1.5.0 · main 3 of 3

So every consumer below v1.5.0 fails with exit 1, and the workflow says so itself:

::error::hydra-gates-ref does not contain: ...
::error::This workflow floats on @main and executes those paths by name inside the PINNED package.

This is not a code-quality finding about this repository. It is the floating-caller / pinned-callee split — the third occurrence today, after the require-full-coverage default change reaching old runners and #168 executing axe-run.cjs by path.

What v1.5.0 also brings

Measured across the same 22 fleet trees, each version running its own code (no CI output read, so a split fleet cannot skew it):

gate v1.3.0 v1.5.0
46 spec-anchor-existence 2,228 918
40 form-label-association 1,211 517
9 semantic-auth 45 11

Every relaxed predicate was mutation-checked both ways — always-true and always-false must each fail — so the reduction is precision, not blindness.

It also fixes the SCOPE WAS EMPTY reporting bug (an unanchored grep -q "0 changed file(s)", which 10 changed file(s) also matched), the hardcoded /tmp/hydra-gate-*.log collision that produced cross-repo false findings, and a gate-28 NUL-byte hole that could yield a false green.

Scope

Exactly one line. No other change.

⚠️ Known follow-up, tracked separately: on a push to development the gate diffs against origin/development — itself — so scope is 0 files and v1.5.0 exits 99. A scoping fix (against github.event.before) is in progress. That is a true statement replacing a vacuous pass, and strictly better than failing because a script is missing.

…onger satisfies the floating workflow

`quality.yml` is consumed `@main` and executes three scripts BY PATH inside the
PINNED hydra-gates package:

    scripts/lib/check_spec_anchors.py
    scripts/lib/check_form_labels.py
    scripts/lib/check_license_triangle.py

Verified by recursive tree listing per ref: v1.0.1, v1.2.0, v1.3.0 and v1.4.0
each contain 0 of 3; only v1.5.0 and main contain all three. So every consumer
below v1.5.0 fails with exit 1, and the workflow says so itself:

    ::error::hydra-gates-ref does not contain: ...
    ::error::This workflow floats on @main and executes those paths by name
             inside the PINNED package.

This is NOT a code-quality finding about this repository — it is the
floating-caller / pinned-callee split, third occurrence today.

v1.5.0 also carries large genuine gate improvements, measured across the same
22 fleet trees with each version running its own code:

    gate-46 spec-anchor-existence   2,228 -> 918
    gate-40 form-label-association  1,211 -> 517
    gate-9  semantic-auth              45 -> 11

Every relaxed predicate in it was mutation-checked both ways: always-true and
always-false must each fail.

Diff is exactly one line.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/scholiq @ 78972f7

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
composer ✅ 100/100
npm ✅ 750/750
PHPUnit
Newman ⏭️
Playwright ⏭️
Hydra gates

Quality workflow — 2026-08-06 07:29 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Superseded by #285, which removes hydra-gates-ref entirely instead of moving it to v1.5.0.

The product owner's directive is to stop pinning the package per repo so every repo always runs the newest gates. Moving the pin to v1.5.0 would leave the same silent expiry date in place — the shared workflow now declares the default (main) and its comment states callers should not set this input at all.

rubenvdlinde added a commit that referenced this pull request Aug 6, 2026
…ppHost (gate-64 / ADR-040) (#288)

* fix(apphost): register OpenRegister's autoloader before referencing AppHost

Nextcloud registers apps in sorted order: OC_App::getEnabledApps() does
sort($apps) and Coordinator::registerApps() walks that list calling
OC_App::registerAutoloading($appId, $path) and then $app->register() for one
app at a time, so every app registers before the PSR-4 prefix of every
alphabetically-later app exists.

`scholiq` sorts after `openregister`, so OCA\OpenRegister\ happens to be
autoloadable here today — by alphabet, not by design. Scholiq depends on that
accident more sharply than most: its Bootstrap::register() call is UNGUARDED,
so the moment the ordering stops holding the resulting \Error aborts the WHOLE
of Application::register(). Coordinator catches it, logs an 'emergency' and
continues, leaving Scholiq enabled and serving with ServiceOverrideRegistrar
and EventListenerWiring silently never run.

Fix: register OpenRegister's prefix ourselves first. registerAutoloading()
touches only the autoloader and is idempotent, so on the current ordering this
costs nothing. IAppManager::loadApp() is deliberately NOT used: it marks
OpenRegister loaded and calls Coordinator::bootApp(), booting it before its own
register() has run.

Caught by hydra gate-64 (apphost-autoload-prelude), ADR-040. Unblocks the
gate-64 failure on the hydra-gates v1.5.0 bump PR (#287).

* fix(apphost): make the prelude branch-free and declare OC_App to psalm

Two CI findings on the prelude, both real:

1. psalm UndefinedClass on \OC_App. It is Nextcloud's server-private legacy
   bootstrap class, absent from nextcloud/ocp, and there is no OCP interface
   for registering another app's autoloader. Declared as a suppressed
   referencedClass in psalm.xml, the same way doriath declares it.

2. The coverage ratchet. `return true` after the call plus `return false` in
   the catch gave the method a branch that NO environment can exercise both
   sides of — whichever runs, the other is dead in that run — so the class
   could never reach full line coverage. No caller ever consumed the return
   value either: what callers depend on is the class_exists() guard that
   follows the call. The method is now void with a single statement in the
   try and a comment-only catch, so every executable line runs in every
   environment.

The tests now assert the two things that are actually observable: that control
returns to the caller at all (a Throwable escaping would fail the test, and in
production would abort the whole register()), and that a second call does not
stack another autoloader.

phpmd StaticAccess on the new composition-root call is documented on the
calling method rather than baselined.

* docs(spec): state the prelude invariant as prose, not as excluded scenarios

The two scenarios added for the ADR-040 prelude each carried an '@e2e exclude'.
An exclusion is not evidence, and neither behaviour is reachable from a browser
or an HTTP client: both live in the app-registration phase, which completes
before the first request is dispatched, and the absent-OpenRegister path cannot
be set up on an instance that needs OpenRegister to serve the app at all.

Stated in the requirement prose instead, naming the unit test that does assert
them (tests/Unit/AppInfo/OpenRegisterAutoloaderTest.php). No scenario is
declared, so none is excluded.

* test: cover the prelude's degraded path, which no instance could reach

The coverage ratchet was right and the code was wrong. Clover for scholiq shows
it exactly: line 100 (the registerAutoloading call) count=2, line 101 (the
catch) count=0. The catch was never entered — because every instance this suite
runs on HAS OpenRegister installed, so getAppPath() never throws. The
never-rethrow branch, which is the entire reason this class exists, had never
once been executed by a test.

register() now takes an optional app id. Production callers pass nothing and get
'openregister'; the new test passes an id that cannot resolve, so
getAppPath() throws and the catch runs. The literal stays AT the
registerAutoloading call site rather than becoming a signature default, so it
remains visible to a reader and to hydra gate-64, which reads that call's
arguments.

The new test asserts something real rather than merely not throwing: a prelude
whose app cannot be resolved must leave spl_autoload_functions() untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant