From 2615c84d7b56b41e99e56019240ed23870e33c0d Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Mon, 3 Aug 2026 20:55:16 +0200 Subject: [PATCH 1/2] fix(quality): the new Frontend Build gate fails on any repo with >40 build outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `head` closes the pipe as soon as it has its 40 lines, `tr` dies of SIGPIPE, and `set -o pipefail` three lines above turns that into a FAILED JOB — for a log-preview line with no bearing on whether the build worked. tr '\0' '\n' < /tmp/frontend-build-files.z | head -40 It only fires when the producer is still writing as head leaves, so it is invisible on small builds and certain on large ones: openregister build writes <40 files -> Frontend Build passed nextcloud-vue dist is thousands -> failed EVERY run since the gate landed (835918f5, today 16:40) Reproduced outside CI. 200k NUL-separated entries: $ bash -c "set -euo pipefail; tr '\0' '\n' < big.z | head -40 >/dev/null; \ echo REACHED-AFTER" exit=141 # 128+13 = SIGPIPE, and REACHED-AFTER never prints $ bash -c "set -euo pipefail; tr '\0' '\n' < big.z | sed -n '1,40p' >/dev/null; \ echo REACHED-AFTER" REACHED-AFTER exit=0 `sed` reads its input to the end, so there is nothing to break, and it still prints exactly 40 lines. The "…" trailer now says how many files were elided instead of implying there might be none. SAME BUG, WORSE FAILURE MODE, same file — the Quality Report step: COVERED=$(grep -oP 'coveredstatements="\K[0-9]+' clover.xml | head -1 || echo "0") That step also sets pipefail, and here `|| echo "0"` does not rescue the value: it APPENDS "0" to what head already printed, so the arithmetic below receives "1234\n0". Measured on a 60k-file clover.xml: old: COVERED=[0 0] new: COVERED=[0] Reporting a wrong coverage number is worse than failing, because nobody re-verifies a number that rendered. Switched to `grep -oPm1`, which stops grep itself and removes the pipe rather than working around it. No other `| head` under pipefail remains in this workflow. --- .github/workflows/quality.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 27f35a1..4a45bd4 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -637,8 +637,22 @@ jobs: fi tar --null -czf /tmp/frontend-build-output.tar.gz -T /tmp/frontend-build-files.z echo "uploaded=true" >> "$GITHUB_OUTPUT" - tr '\0' '\n' < /tmp/frontend-build-files.z | head -40 - echo "…" + # `sed -n '1,40p'`, NOT `head -40`. `head` closes the pipe as soon as + # it has its 40 lines, `tr` then dies of SIGPIPE, and `set -o pipefail` + # (three lines up) turns that into a FAILED JOB — for a log-preview + # line that has no bearing on whether the build worked. + # + # It only fires when the producer is still writing when head leaves, + # i.e. on repos with many build outputs. openregister's build writes + # fewer than 40 files and passed; nextcloud-vue's dist is thousands of + # files and failed every run. Reproduced: 200k NUL-separated entries + # through `tr | head -40` under pipefail exits 141 and the step never + # reaches the next line. `sed` reads its input to the end, so there is + # nothing to break. + tr '\0' '\n' < /tmp/frontend-build-files.z | sed -n '1,40p' + if [ "$COUNT" -gt 40 ]; then + echo "… and $((COUNT - 40)) more" + fi - name: Upload build output if: steps.package.outputs.uploaded == 'true' @@ -2858,8 +2872,15 @@ jobs: # Coverage (if available) if [ -f "coverage/clover.xml" ]; then - COVERED=$(grep -oP 'coveredstatements="\K[0-9]+' coverage/clover.xml | head -1 || echo "0") - TOTAL_STMTS=$(grep -oP 'statements="\K[0-9]+' coverage/clover.xml | head -1 || echo "0") + # `grep -m1` rather than `grep | head -1`: this step also runs under + # `set -o pipefail`, so head closing the pipe on a clover.xml with + # many matches kills grep with SIGPIPE. The `|| echo "0"` then does + # not rescue the value — it APPENDS "0" to whatever head already + # printed, and the arithmetic below gets "1234\n0". Reporting a wrong + # coverage number is worse than failing, because nobody re-checks a + # number that rendered. -m1 stops grep itself, so there is no pipe. + COVERED=$(grep -oPm1 'coveredstatements="\K[0-9]+' coverage/clover.xml || echo "0") + TOTAL_STMTS=$(grep -oPm1 'statements="\K[0-9]+' coverage/clover.xml || echo "0") if [ "$TOTAL_STMTS" -gt 0 ]; then COVERAGE=$(echo "scale=1; $COVERED * 100 / $TOTAL_STMTS" | bc) echo "**Coverage:** ${COVERAGE}% ($COVERED/$TOTAL_STMTS statements)" >> "$REPORT" From e5c432f35a7aa31607864877451a4c1fc563f90e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 3 Aug 2026 20:56:25 +0200 Subject: [PATCH 2/2] fix(gate-4): audit the LOCK, and stop reporting "could not audit" as "CVEs" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the gates' first real CI run, on openbuild#104: gate-4 reported `composer-audit: FAIL — CVEs or advisories`. There were no CVEs. Auditing the same lock reports "No security vulnerability advisories found." `composer audit` with no vendor/ present does not audit the lock, and what it does instead depends on the composer version. BOTH behaviours are wrong: composer >= 2.8 "No installed packages found. Please run composer install ... or pass --locked", exit 1 — so the gate announced a security finding for a run that found nothing and audited nothing. A configuration error wearing a CVE's clothes. composer 2.7.x "No packages - skipping audit", exit 0 — a SILENT FAIL-OPEN. The gate PASSED having audited nothing at all. The second is the more dangerous one, and it is the reason this could not be left as "just make CI run composer install first": on any runner with an older composer, gate-4 has been passing without auditing anything, and that is indistinguishable from a clean audit in the output. Now: - audit `--locked` whenever a composer.lock exists. The lock is the right object anyway — it is what CI installs and what pins the transitive tree — and it needs no vendor/, so the gate no longer depends on whether some earlier step happened to run `composer install`. - an exit 0 whose output says "no packages" is treated as a FAILURE, not a pass. Auditing nothing is never a clean audit. - "could not run" is reported in those words and explicitly NOT as a CVE finding, so the two are distinguishable at a glance. Positive control, three directions, in a clean php:8.3-cli container with composer 2.10.2 and no vendor/: openbuild's real lock -> [gate-4] PASS ("No security vulnerability advisories found.") lock with guzzlehttp/guzzle -> [gate-4] FAIL — CVEs or advisories, and the 6.5.0 pinned in log names the package and the advisories ("Found 16 security vulnerability advisories affecting 2 packages", GHSA-h95v-h523-3mw8, ...) no lock and no vendor -> [gate-4] FAIL — "audit COULD NOT RUN ... NOT a CVE finding" Worth noting for the second case: composer 2.10 refuses to resolve a vulnerable package at all by default (policy.advisories.block), so the control needed that policy disabled to even produce the lock. The first attempt at it silently produced no lock — and the new code correctly reported "could not run" rather than inventing a finding, which is itself the behaviour under test. --- hydra-gates/scripts/run-hydra-gates.sh | 46 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/hydra-gates/scripts/run-hydra-gates.sh b/hydra-gates/scripts/run-hydra-gates.sh index 999b1ae..a952322 100755 --- a/hydra-gates/scripts/run-hydra-gates.sh +++ b/hydra-gates/scripts/run-hydra-gates.sh @@ -376,6 +376,27 @@ fi # --------------------------------------------------------------------------- # Gate 4: Composer audit +# +# Audits the LOCK FILE, not the installed tree, whenever a composer.lock +# exists. That is deliberate and it is a fix, not a convenience: +# +# `composer audit` with no vendor/ present does NOT audit the lock. What it +# does depends on the composer version, and BOTH behaviours are wrong: +# +# composer >= 2.8 : "No installed packages found. Please run composer install +# ... or pass --locked", exit 1. The gate then reported +# "CVEs or advisories" for a run that found no CVEs and +# audited nothing — a configuration error wearing a +# security finding's clothes. +# composer 2.7.x : "No packages - skipping audit", exit 0 — a SILENT +# FAIL-OPEN. The gate passed having audited nothing at all. +# +# Measured 2026-08-03 on openbuild in CI (composer 2.10.2, no vendor/): gate-4 +# FAILED, and `--locked` on the same lock reported no advisories whatsoever. +# +# The lock is also the right object to audit: it is what CI installs and what +# pins the transitive tree. `--locked` needs no vendor/, so this gate no longer +# depends on whether some earlier step happened to run `composer install`. # --------------------------------------------------------------------------- if [ -f composer.json ] && command -v composer >/dev/null 2>&1; then _run_audit=1 @@ -383,10 +404,29 @@ if [ -f composer.json ] && command -v composer >/dev/null 2>&1; then _in_scope "composer.json" || _in_scope "composer.lock" || _run_audit=0 fi if [ "${_run_audit}" = "1" ]; then - if composer audit --format=plain >/tmp/hydra-gate-composer-audit.log 2>&1; then - _pass 4 "composer-audit" + _ca_log=/tmp/hydra-gate-composer-audit.log + if [ -f composer.lock ]; then + _ca_mode="--locked" + else + # No lock to audit. Auditing the installed tree is the only option, + # and it is only meaningful if there IS one. + _ca_mode="" + fi + composer audit ${_ca_mode} --format=plain >"${_ca_log}" 2>&1 + _ca_rc=$? + if [ "${_ca_rc}" -eq 0 ]; then + # Distinguish "audited, clean" from "audited nothing, called it + # clean". The second is the 2.7.x fail-open above, and it must never + # be counted as a pass. + if grep -qiE "no packages|no installed packages" "${_ca_log}"; then + _fail 4 "composer-audit" "audited NOTHING (composer found no packages) — this is not a clean audit; see ${_ca_log}" + else + _pass 4 "composer-audit" + fi + elif grep -qiE "no installed packages found|please run \"?composer install" "${_ca_log}"; then + _fail 4 "composer-audit" "audit COULD NOT RUN (no installed packages and no lock to audit) — NOT a CVE finding; see ${_ca_log}" else - _fail 4 "composer-audit" "CVEs or advisories — see /tmp/hydra-gate-composer-audit.log" + _fail 4 "composer-audit" "CVEs or advisories — see ${_ca_log}" fi fi fi