Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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"
Expand Down
46 changes: 43 additions & 3 deletions hydra-gates/scripts/run-hydra-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -376,17 +376,57 @@ 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
if [ "${SCOPE_TO_DIFF}" = "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
Expand Down
Loading