- conformance/lib/run_l1_offline.py: checks run in a thread pool (--jobs / CONF_OFFLINE_JOBS,
default min(8, usable CPUs)); results consumed in submission order, so log lines, JUnit,
step summary and the 0/1/3 aggregation are unchanged. Exports a smaller MUTATION_JOBS to
the children when parallel, so nested pools do not oversubscribe.
- keycloak-{passkey,client-secret}-reconciler/mutations.py: anchors checked first (serial),
then the --selftest subprocesses run in a pool (MUTATION_JOBS, default min(8, CPUs));
verdicts reported in MUTATIONS order. Output byte-identical to the serial loop.
- ci/lint.sh: yamllint over LINT_JOBS contiguous slices concurrently, per-slice output
buffered and printed in order; stage rc = highest slice rc.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
4.7 KiB
Bash
Executable File
97 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ci/lint.sh — Stage 1: yaml/lint. Runs in CI without a cluster (live now).
|
|
# Lints all YAML in the repo against ci/yamllint.yaml.
|
|
set -euo pipefail
|
|
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck disable=SC1091
|
|
source "${CI_DIR}/lib.sh"
|
|
|
|
log "Stage: yaml/lint"
|
|
require_tool yamllint "Install: pip install 'yamllint==${YAMLLINT_VERSION}'"
|
|
|
|
# Lint tracked + untracked (not yet committed) YAML; skip .git and artifact dirs.
|
|
cd "$REPO_ROOT"
|
|
mapfile -t files < <(
|
|
{ git ls-files '*.yaml' '*.yml' 2>/dev/null
|
|
git ls-files --others --exclude-standard '*.yaml' '*.yml' 2>/dev/null
|
|
} | sort -u
|
|
)
|
|
# Fallback for a non-git checkout.
|
|
if [ "${#files[@]}" -eq 0 ]; then
|
|
mapfile -t files < <(find . -path ./.git -prune -o \( -name '*.yaml' -o -name '*.yml' \) -print)
|
|
fi
|
|
if [ "${#files[@]}" -eq 0 ]; then
|
|
ok "no YAML files to lint"
|
|
exit 0
|
|
fi
|
|
|
|
# yamllint is single-threaded, so the file list is split into LINT_JOBS CONTIGUOUS slices of the
|
|
# sorted list, one yamllint per slice, run concurrently. Each slice's output goes to its own file
|
|
# and the files are printed in slice order afterwards: the log is the same text, in the same
|
|
# file order, as one serial `yamllint "${files[@]}"`, and a finding is never interleaved with
|
|
# another process's output (yamllint's standard format puts the FILE NAME on its own line above
|
|
# that file's `line:col` findings, so interleaving would misattribute them). The stage's exit
|
|
# code is the highest any slice returned (yamllint: 1 = error, 2 = warning under strict), so a
|
|
# single failing slice fails the stage exactly as the serial call did. LINT_JOBS=1 is serial.
|
|
lint_jobs="${LINT_JOBS:-$(n="$(nproc 2>/dev/null || echo 1)"; [ "$n" -gt 8 ] && n=8; echo "$n")}"
|
|
case "$lint_jobs" in ''|*[!0-9]*|0) err "LINT_JOBS must be a positive integer, got '${lint_jobs}'"; exit 2 ;; esac
|
|
[ "$lint_jobs" -gt "${#files[@]}" ] && lint_jobs="${#files[@]}"
|
|
lint_tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$lint_tmp"' EXIT
|
|
per=$(( (${#files[@]} + lint_jobs - 1) / lint_jobs ))
|
|
pids=()
|
|
for (( i = 0; i < lint_jobs; i++ )); do
|
|
slice=("${files[@]:$(( i * per )):${per}}")
|
|
[ "${#slice[@]}" -gt 0 ] || break
|
|
( rc=0
|
|
yamllint -c "${CI_DIR}/yamllint.yaml" -f "${YAMLLINT_FORMAT:-standard}" "${slice[@]}" \
|
|
>"${lint_tmp}/${i}.out" 2>&1 || rc=$?
|
|
echo "$rc" >"${lint_tmp}/${i}.rc" ) &
|
|
pids+=("$!")
|
|
done
|
|
wait "${pids[@]}" || true
|
|
lint_rc=0
|
|
for (( i = 0; i < ${#pids[@]}; i++ )); do
|
|
cat "${lint_tmp}/${i}.out"
|
|
# A slice that left no rc file died before yamllint returned: that is a failure, not a pass.
|
|
slice_rc="$(cat "${lint_tmp}/${i}.rc" 2>/dev/null || true)"
|
|
case "$slice_rc" in ''|*[!0-9]*) slice_rc=1 ;; esac
|
|
[ "$slice_rc" -gt "$lint_rc" ] && lint_rc="$slice_rc"
|
|
done
|
|
if [ "$lint_rc" -ne 0 ]; then
|
|
err "yaml/lint failed (yamllint exit ${lint_rc}; ${#files[@]} files in ${#pids[@]} parallel slice(s))"
|
|
exit "$lint_rc"
|
|
fi
|
|
ok "yaml/lint passed (${#files[@]} files, ${#pids[@]} parallel slice(s))"
|
|
|
|
# A workflow's `configurationFile:` must name a file that exists. Renovate's did not after #79
|
|
# renamed renovate.json -> renovate.json5, and every scheduled run failed with ENOENT for 11 days
|
|
# while the gate stayed green. Valid YAML, so yamllint cannot see it.
|
|
missing=0
|
|
while IFS=: read -r wf _ value; do
|
|
value="${value#*configurationFile:}"
|
|
value="$(echo "$value" | sed -E 's/#.*//; s/^[[:space:]"'\'']+//; s/[[:space:]"'\'']+$//')"
|
|
if [ -n "$value" ] && [ ! -e "$value" ]; then
|
|
err "${wf}: configurationFile '${value}' does not exist in the repo"
|
|
missing=1
|
|
fi
|
|
done < <(grep -nE '^[[:space:]]*configurationFile:' .gitea/workflows/*.y*ml 2>/dev/null || true)
|
|
[ "$missing" -eq 0 ] || exit 1
|
|
ok "workflow configurationFile references resolve"
|
|
|
|
# A workflow must not use an action that needs a Docker daemon: jobs on this runner get NONE, by
|
|
# decision (decisions/2026-10-29-image-pipeline.md; act_runner maps docker_host to `-`). Such an
|
|
# action is valid YAML and passes every other stage, then dies at runtime with
|
|
# "failed to connect to the docker API at unix:///var/run/docker.sock". renovatebot/github-action
|
|
# did exactly that on every Renovate run until 2026-10-30 (run 343). Run the tool's image as the
|
|
# job `container:` instead (see .gitea/workflows/renovate.yaml). Container (`docker://`) actions
|
|
# and Docker's own daemon-bound actions are rejected for the same reason. Comments are ignored.
|
|
daemon_uses='uses:[[:space:]]*["'\'']?(docker://|renovatebot/github-action|docker/(build-push|setup-buildx|setup-qemu|login)-action)'
|
|
bad=0
|
|
while IFS= read -r hit; do
|
|
err "${hit%%:*}: needs a Docker daemon, which jobs here do not get: $(echo "${hit#*:*:}" | sed -E 's/^[[:space:]]+//')"
|
|
bad=1
|
|
done < <(grep -HnE "^[^#]*${daemon_uses}" .gitea/workflows/*.y*ml 2>/dev/null || true)
|
|
[ "$bad" -eq 0 ] || exit 1
|
|
ok "no workflow uses a Docker-daemon-bound action"
|