ci(quality): gate the code this repo actually runs — and it was hiding two real bugs - #23
Closed
rubenvdlinde wants to merge 1 commit into
Closed
ci(quality): gate the code this repo actually runs — and it was hiding two real bugs#23rubenvdlinde wants to merge 1 commit into
rubenvdlinde wants to merge 1 commit into
Conversation
…g two real bugs
code-quality.yml existed here, but it ran only the shared pipeline's PHP legs
— which are aimed at phpcs-custom-sniffs/. That is correct for what they are:
this is a Python ExApp sidecar and there is no lib/. What it left ungated is
the application. ex_app/lib/main.py is 623 lines — the largest of the four
sidecars — and nothing in this repo has ever looked at it.
The file's own comment called this a "PHP-only ExApp". It is a Python ExApp;
the only PHP in the tree is a custom sniff. Corrected.
TWO REAL DEFECTS, not style
1. Popen.stdout, and a closure over a global that gets nulled.
start_opentalk() spawned the controller and started a daemon thread whose
body was `for line in OPENTALK_PROCESS.stdout`. It read the GLOBAL, and
stop_opentalk() sets that global to None — so a stop, or a
stop-then-restart, would have raised AttributeError inside the log thread
on its next iteration. Popen.stdout is also Optional and was
unguarded. The thread now binds the process and the stream locally and
returns if the stream is absent.
Surfaced by mypy: `Item "None" of "IO[Any] | None" has no attribute
"__iter__"`.
2. Dead OIDC computation. _serve_index_html() computed
`oidc_authority = f"{keycloak_browser_url}/realms/{KEYCLOAK_REALM}"` and
never used it — residue from an earlier oidc-client-ts/sessionStorage
approach that the injected bootstrap script no longer takes (it writes
plain localStorage keys). The authority reaches the frontend via
_build_config_js(), which is what window.config consumes. Removed, and
the stale docstring corrected to describe what the function does.
Surfaced by ruff: `F841 Local variable oidc_authority is assigned to but
never used`.
Also narrowed selectors' `key.fileobj` (typed `int | HasFileno`) with a real
isinstance check rather than an assert or a cast: a raw fd reaching
.recv()/.close() would raise AttributeError inside the Keycloak proxy loop.
ADDED
python-checks — ruff (lint + format) and mypy over ex_app/, matching the
pattern already used by n8n-nextcloud and keycloak-nextcloud.
pyproject.toml carries the config; requirements-dev.txt pins ruff==0.16.1
and mypy==2.3.0 exactly, because a floating linter changes a repo's verdict
with no commit in that repo to explain it. requirements.txt is installed in
CI too, so mypy resolves nc_py_api/httpx for real rather than falling back
to ignore_missing_imports and checking less than it appears to — that is
what made defect 1 visible at all.
MEASURED BEFORE AND AFTER, identically conditioned
ruff check ex_app/ BEFORE: 5 findings (I001, SIM108, SIM105, RUF010,
F841)
AFTER: All checks passed
ruff format --check BEFORE: 1 file would be reformatted
AFTER: 1 file already formatted
mypy ex_app/ BEFORE: 5 errors (4x union-attr on selectors
fileobj, 1x union-attr on Popen.stdout)
AFTER: 0 errors, 1 source file checked
make check-strict AFTER: exit 0
The BEFORE numbers were re-measured against the committed HEAD after the fix,
so they are a positive control: both jobs demonstrably fail on the code as it
stood, and mypy exited 1.
WHY HYDRA GATES WAS SKIPPING — and it was not the Playwright dependency
`enable-hydra-gates` defaults to false in the shared workflow and this file
never passed it. The job's guard is
`if: inputs.enable-hydra-gates && !cancelled()`, so it was the FIRST term that
deleted the job. `!cancelled()` was already doing its job correctly.
Measured with a --full scan of the whole tree before switching it on:
29 of 63 gates reported, 0 failures. Switched on here.
One of those 29 was not real. gate-28 (license-triangle) called `_pass`
unconditionally after a comparison that only runs when `[ -d lib ]`, so it
reported PASS having opened zero files. Fixed separately in
ConductionNL/.github; with that fix it reports NOT APPLICABLE here and the
count drops to 28. A gate that inspected nothing now says so.
hydra-gates-ref is deliberately left at `main` rather than pinned. A pin is a
silent expiry date — the fleet pinned v1.0.1 across 22 repos, the pin predated
the fixes to 16 gates, and all 16 were dead for as long as the pin stood.
NO TEST SUITE, AND NO PRETENDING OTHERWISE
This repo has no automated tests: no pytest config, no test_*.py, no
phpunit.xml, no tests/. composer.json's check:strict already says so at length
and asks that no test script be re-added until a real suite exists. That is
respected — nothing here scaffolds a suite. Both defects above were found by
static analysis; neither is covered by a regression test, and that gap is real.
The Makefile's `test` target IS renamed to `run`, because it never tested
anything: it is an interactive `docker run -it` that boots the container,
asserts nothing, and cannot run in CI at all (-it needs a TTY). There is now no
`test` target, so `make test` fails loudly instead of exiting 0 having proved
nothing. check-strict prints what its green does and does not cover.
Contributor
Author
|
Companion gate-28 fix: ConductionNL/.github#172 — |
Contributor
Author
|
Closing: out of scope for now — the ExApp sidecar wrappers are not part of the 16 Nextcloud apps this sweep covers. Findings are recorded in the agent report; the branch |
Quality Report — ConductionNL/opentalk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ✅ | ✅ 69/69 | |||
| npm | ✅ | ✅ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 21:06 UTC
Download the full PDF report from the workflow artifacts.
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.
The gap
code-quality.ymlexisted here, but it ran only the shared pipeline's PHP legs — which are aimed atphpcs-custom-sniffs/. That is correct for what they are: this is a Python ExApp sidecar and there is nolib/. Forcing PHP tooling at a Python codebase would be the wrong fix.What it left ungated is the application.
ex_app/lib/main.pyis 623 lines — the largest of the four sidecars — and nothing in this repo has ever looked at it.(The file's own comment called this a "PHP-only ExApp". It is a Python ExApp; the only PHP in the tree is a custom sniff. Corrected.)
Two real defects, not style
1.
Popen.stdout, and a closure over a global that gets nulled.start_opentalk()spawned the controller and started a daemon thread whose body wasfor line in OPENTALK_PROCESS.stdout. It read the global — andstop_opentalk()sets that global toNone. A stop, or a stop-then-restart, would have raisedAttributeErrorinside the log thread on its next iteration.Popen.stdoutis alsoOptionaland was unguarded. Surfaced by mypy:The thread now binds the process and the stream locally and returns if the stream is absent.
2. Dead OIDC computation.
_serve_index_html()computedoidc_authority = f"{keycloak_browser_url}/realms/{KEYCLOAK_REALM}"and never used it — residue from an earlier oidc-client-ts/sessionStorageapproach the injected bootstrap script no longer takes (it writes plainlocalStoragekeys). The authority reaches the frontend via_build_config_js(), which is whatwindow.configconsumes. Removed, and the stale docstring corrected to describe what the function actually does. Surfaced by ruffF841.Also narrowed selectors'
key.fileobj(typedint | HasFileno) with a realisinstancecheck rather than anassertor acast— a raw fd reaching.recv()/.close()would raiseAttributeErrorinside the Keycloak proxy loop.Measured before and after, identically conditioned
ruff check ex_app/ruff format --check ex_app/mypy ex_app/union-attron selectorsfileobj, 1×union-attronPopen.stdoutmake check-stricthydra-gates --base origin/development5 changed file(s)in scope, 27 of 63 reported, 0 failuresThe before column was re-measured against the committed HEAD after the fix, so it is a positive control: both jobs demonstrably fail on the code as it stood.
requirements.txtis installed in CI alongsiderequirements-dev.txton purpose — so mypy resolvesnc_py_api/httpxfor real rather than falling back toignore_missing_importsand checking less than it appears to. That is what made defect 1 visible at all.Why Hydra Gates was skipping — and it was not the Playwright dependency
enable-hydra-gatesdefaults tofalseinConductionNL/.github/.github/workflows/quality.yml, and this file never passed it. The job's guard is:It was the first term that deleted the job.
!cancelled()— the part with the long comment aboutneeds:implyingsuccess()— was already doing its job correctly.Measured with a
--fullscan of the whole tree before switching it on: 29 of 63 gates reported, 0 failures. Switched on here.hydra-gates-refis deliberately left atmainrather than pinned. A pin is a silent expiry date: the fleet pinnedv1.0.1across 22 repos, the pin predated the fixes to 16 gates, and all 16 were dead for as long as the pin stood.gate-28(license-triangle) called_pass 28unconditionally after a comparison that only runs insideif [ -n "${_composer_lic}" ] && [ -d lib ]. This repo has nolib/, so it printed[gate-28] license-triangle: PASShaving opened zero files — and the coverage accounting counted it as a gate that reported a result.Fixed in ConductionNL/.github#(companion PR); with that fix it reports
NOT APPLICABLEwith a stated reason and the count drops 29 → 27 diff-scoped. Until that companion PR merges, this PR's CI will still show gate-28 as a green PASS. That green is the vacuous one — do not read it as coverage.No test suite, and no pretending otherwise
This repo has no automated tests: no pytest config, no
test_*.py, nophpunit.xml, notests/.composer.json'scheck:strictalready says so and asks that no test script be re-added until a real suite exists. That is respected — nothing here scaffolds a suite. Both defects above were found by static analysis; neither is covered by a regression test, and that gap is real.The Makefile's
testtarget is renamed torun, because it never tested anything: an interactivedocker run -itthat boots the container, asserts nothing, and cannot run in CI at all (-itneeds a TTY). There is now notesttarget, somake testfails loudly instead of exiting 0 having proved nothing.