From 20cb1eb453b923ac1c47c6887c7b5eea7a996550 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 1 Oct 2026 11:20:55 -0700 Subject: [PATCH 1/4] CI: retry apt-get and time-limit install steps Every workflow that installs packages now passes Acquire::Retries=3 to apt-get, and each install step gets its own timeout, sized to fit inside its job's limit, so a stalled mirror fails fast under a step name that says what went wrong. - cppcheck runs apt-get update before installing, and installs with -y - sftp-test and sshd-test move their inline installs into their own steps - test-fatfs job timeout goes from 4 to 10 minutes to hold its installs --- .github/workflows/code-coverage.yml | 5 +++-- .github/workflows/cppcheck.yml | 5 ++++- .github/workflows/multi-compiler.yml | 5 +++-- .github/workflows/network-contention-test.yml | 3 ++- .github/workflows/paramiko-sftp-test.yml | 5 +++-- .github/workflows/sftp-test.yml | 9 ++++++--- .github/workflows/sshd-test.yml | 9 +++++++-- .github/workflows/test-fatfs.yml | 12 +++++++----- .github/workflows/tpm-ssh.yml | 7 ++++--- .github/workflows/windows-cert-store-test.yml | 5 +++-- .github/workflows/x509-interop.yml | 10 ++++++---- 11 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 627aedde5..ad3289447 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -56,9 +56,10 @@ jobs: # clang 18 is the min: -fcoverage-mcdc does not exist before it. - name: Install clang and LLVM coverage tools + timeout-minutes: 5 run: | - sudo apt-get update - sudo apt-get install -y clang-18 llvm-18 libclang-rt-18-dev + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y clang-18 llvm-18 libclang-rt-18-dev - name: Download wolfSSL uses: actions/download-artifact@v8 diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml index 7d03d256b..d9a43c3b9 100644 --- a/.github/workflows/cppcheck.yml +++ b/.github/workflows/cppcheck.yml @@ -16,7 +16,10 @@ jobs: - name: Install cppcheck if: always() - run: sudo apt-get install cppcheck + timeout-minutes: 5 + run: | + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y cppcheck - name: Run CppCheck id: cpp_check_run diff --git a/.github/workflows/multi-compiler.yml b/.github/workflows/multi-compiler.yml index 1ae8d4b85..290cce46e 100644 --- a/.github/workflows/multi-compiler.yml +++ b/.github/workflows/multi-compiler.yml @@ -65,9 +65,10 @@ jobs: steps: - name: Install compiler + timeout-minutes: 5 run: | - sudo apt-get update - sudo apt-get install -y ${{ matrix.cc }} + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y ${{ matrix.cc }} - name: Checkout wolfSSH uses: actions/checkout@v6 diff --git a/.github/workflows/network-contention-test.yml b/.github/workflows/network-contention-test.yml index cdde41de8..40508138a 100644 --- a/.github/workflows/network-contention-test.yml +++ b/.github/workflows/network-contention-test.yml @@ -108,7 +108,8 @@ jobs: run: ./scripts/sftp.test - name: Install expect for extended tests - run: sudo apt-get update && sudo apt-get install -y expect + timeout-minutes: 2 + run: sudo apt-get -o Acquire::Retries=3 update && sudo apt-get -o Acquire::Retries=3 install -y expect - name: Create large test files working-directory: ./wolfssh/ diff --git a/.github/workflows/paramiko-sftp-test.yml b/.github/workflows/paramiko-sftp-test.yml index c7d4041c5..8198583c3 100644 --- a/.github/workflows/paramiko-sftp-test.yml +++ b/.github/workflows/paramiko-sftp-test.yml @@ -67,9 +67,10 @@ jobs: run: make - name: Install dependencies + timeout-minutes: 5 run: | - sudo apt-get update - sudo apt-get install -y python3-pip openssh-client + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y python3-pip openssh-client python3 -m pip install paramiko - name: Create test directories diff --git a/.github/workflows/sftp-test.yml b/.github/workflows/sftp-test.yml index 780a6e492..c6aaaafb7 100644 --- a/.github/workflows/sftp-test.yml +++ b/.github/workflows/sftp-test.yml @@ -106,6 +106,12 @@ jobs: echo "Echoserver started with PID: $!" sleep 2 # Give the server time to start + - name: Install expect + timeout-minutes: 2 + run: | + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y expect + - name: Run SFTP test working-directory: ./wolfssh/ run: | @@ -124,9 +130,6 @@ jobs: EOF chmod +x /tmp/sftp_test.exp - # Install expect - sudo apt-get update && sudo apt-get install -y expect - # Run the expect script /tmp/sftp_test.exp diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml index 0469ce559..e007dd490 100644 --- a/.github/workflows/sshd-test.yml +++ b/.github/workflows/sshd-test.yml @@ -125,12 +125,17 @@ jobs: sudo ./run_all_sshd_tests.sh # could use optimization with caching + - name: Install valgrind + if: matrix.extra_flags == '' + timeout-minutes: 2 + run: | + sudo apt-get -o Acquire::Retries=3 -y update + sudo apt-get -o Acquire::Retries=3 -y install valgrind + - name: Test memory after close down working-directory: ./wolfssh/ if: matrix.extra_flags == '' run: | - sudo apt-get -y update - sudo apt-get -y install valgrind touch sshd_config.txt ./configure --enable-all LDFLAGS="-L${{ github.workspace }}/build-dir/lib" CPPFLAGS="-I${{ github.workspace }}/build-dir/include -DWOLFSSH_NO_FPKI -DWOLFSSH_NO_SFTP_TIMEOUT -DWOLFSSH_MAX_SFTP_RW=4000000 -DMAX_PATH_SZ=120" --enable-static --disable-shared && make # wolfSSHd loads the host key through the secure gate; the daemon runs diff --git a/.github/workflows/test-fatfs.yml b/.github/workflows/test-fatfs.yml index e2b729940..05945345b 100644 --- a/.github/workflows/test-fatfs.yml +++ b/.github/workflows/test-fatfs.yml @@ -30,7 +30,7 @@ jobs: name: Test wolfSSH with FatFS needs: get-fatfs runs-on: ubuntu-latest - timeout-minutes: 4 + timeout-minutes: 10 steps: - name: Checkout code uses: actions/checkout@v6 @@ -43,9 +43,10 @@ jobs: fail-on-cache-miss: true - name: Install dependencies + timeout-minutes: 3 run: | - sudo apt-get update - sudo apt-get install -y build-essential autoconf automake libtool pkg-config openssh-server dosfstools + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y build-essential autoconf automake libtool pkg-config openssh-server dosfstools - name: Build wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 @@ -89,9 +90,10 @@ jobs: chmod 600 ~/.ssh/config - name: Install expect + timeout-minutes: 2 run: | - sudo apt-get update - sudo apt-get install -y expect + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y expect - name: Run wolfsftp client to get file run: | diff --git a/.github/workflows/tpm-ssh.yml b/.github/workflows/tpm-ssh.yml index e13485b77..377762dae 100644 --- a/.github/workflows/tpm-ssh.yml +++ b/.github/workflows/tpm-ssh.yml @@ -40,10 +40,11 @@ jobs: path: wolftpm - name: Install Dependencies + timeout-minutes: 10 run: | - sudo apt-get update - sudo apt-get install -y libtool automake autoconf - sudo apt-get install -y build-essential git autoconf-archive \ + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y libtool automake autoconf + sudo apt-get -o Acquire::Retries=3 install -y build-essential git autoconf-archive \ libcmocka-dev libssl-dev uthash-dev libglib2.0-dev \ tpm2-tools openssh-client sshpass diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index 08a805ccc..2c02134fd 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -298,9 +298,10 @@ jobs: - uses: actions/checkout@v4 - name: Install mingw toolchain and autotools + timeout-minutes: 5 run: | - sudo apt-get update - sudo apt-get install -y gcc-mingw-w64-x86-64 autoconf automake libtool + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y gcc-mingw-w64-x86-64 autoconf automake libtool - name: Generate configure run: ./autogen.sh diff --git a/.github/workflows/x509-interop.yml b/.github/workflows/x509-interop.yml index 742d31b5e..4bdc5e205 100644 --- a/.github/workflows/x509-interop.yml +++ b/.github/workflows/x509-interop.yml @@ -64,10 +64,11 @@ jobs: lookup-only: true - name: Install build dependencies + timeout-minutes: 5 if: steps.cache-pkixssh.outputs.cache-hit != 'true' run: | - sudo apt-get -y update - sudo apt-get -y install libssl-dev zlib1g-dev + sudo apt-get -o Acquire::Retries=3 -y update + sudo apt-get -o Acquire::Retries=3 -y install libssl-dev zlib1g-dev - name: Download, build, and install PKIX-SSH if: steps.cache-pkixssh.outputs.cache-hit != 'true' @@ -108,9 +109,10 @@ jobs: fail-on-cache-miss: true - name: Install test dependencies + timeout-minutes: 2 run: | - sudo apt-get -y update - sudo apt-get -y install netcat-traditional + sudo apt-get -o Acquire::Retries=3 -y update + sudo apt-get -o Acquire::Retries=3 -y install netcat-traditional - uses: actions/checkout@v6 with: From 612237d80ffcd16b1699cf888ed553493fbf27eb Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 1 Oct 2026 11:47:54 -0700 Subject: [PATCH 2/4] CI: install apt packages from ghcr .deb bundles Port wolfSSL's install-apt-deps action and ci-deps-image workflow. A daily job on master downloads each package list's .debs and publishes them as ghcr.io/wolfssl/wolfssh-ci-debs; the workflows install from that bundle offline and fall back to the mirror with bounded retries. - two bundles: ubuntu-24.04-tests and ubuntu-24.04-compilers - every apt-get install step in the workflows now uses the action - the package must be made public once after the first build; until then every job takes the mirror fallback - sshd-test job timeout goes from 10 to 15 minutes to fit the install --- .github/actions/install-apt-deps/action.yml | 181 ++++++++++++++++++ .../packages-ubuntu-24.04-compilers.txt | 15 ++ .../ci-deps/packages-ubuntu-24.04-tests.txt | 24 +++ .github/scripts/download-deb-closure.sh | 42 ++++ .github/workflows/ci-deps-image.yml | 86 +++++++++ .github/workflows/code-coverage.yml | 9 +- .github/workflows/cppcheck.yml | 8 +- .github/workflows/multi-compiler.yml | 13 +- .github/workflows/network-contention-test.yml | 6 +- .github/workflows/paramiko-sftp-test.yml | 12 +- .github/workflows/sftp-test.yml | 8 +- .github/workflows/sshd-test.yml | 11 +- .github/workflows/test-fatfs.yml | 14 +- .github/workflows/tpm-ssh.yml | 15 +- .github/workflows/windows-cert-store-test.yml | 9 +- .github/workflows/x509-interop.yml | 25 ++- 16 files changed, 416 insertions(+), 62 deletions(-) create mode 100644 .github/actions/install-apt-deps/action.yml create mode 100644 .github/ci-deps/packages-ubuntu-24.04-compilers.txt create mode 100644 .github/ci-deps/packages-ubuntu-24.04-tests.txt create mode 100755 .github/scripts/download-deb-closure.sh create mode 100644 .github/workflows/ci-deps-image.yml diff --git a/.github/actions/install-apt-deps/action.yml b/.github/actions/install-apt-deps/action.yml new file mode 100644 index 000000000..fe21b61fe --- /dev/null +++ b/.github/actions/install-apt-deps/action.yml @@ -0,0 +1,181 @@ +name: 'Install apt dependencies' +description: 'Install apt packages from a prebuilt ghcr .deb bundle, falling back to the apt mirror' +# Ported from wolfSSL's .github/actions/install-apt-deps. +inputs: + packages: + description: 'Space-separated list of apt packages to install' + required: true + ghcr-debs-tag: + description: > + Tag of a .deb bundle published to ghcr.io/wolfssl/wolfssh-ci-debs by + the ci-deps-image workflow (e.g. "ubuntu-24.04-tests"). The packages + are installed from that bundle with no network access. Any failure - + bundle missing, not public, or not covering every requested package - + falls back to the apt mirror, so it is always safe to set. + required: false + default: '' + retries: + description: 'Attempts at the apt mirror fallback' + required: false + default: '2' + budget-seconds: + description: > + Wall-clock for the whole mirror fallback, split across the attempts as + per-command deadlines so a wedged mirror is reported here instead of + the job being cancelled around it. It overshoots by retry-delay plus + 10s of kill grace, and the per-command floors make values below + retries*80 inert. This plus pull-timeout has to fit the caller's + timeout-minutes. + required: false + default: '180' + pull-timeout: + description: 'Deadline in seconds for the ghcr bundle pull' + required: false + default: '60' + retry-delay: + description: 'Initial delay between retries (seconds, doubles each attempt)' + required: false + default: '5' + no-install-recommends: + description: 'Pass --no-install-recommends to apt-get install' + required: false + default: 'false' +runs: + using: 'composite' + steps: + # Resolve against ONLY the bundle's own index, through a private + # sources.list and lists dir. The runner's lists are frozen into its image + # and lag the archive the bundle was built from, so resolving against them + # asks for versions the bundle does not carry. The system lists are left + # untouched for the fallback below. + - name: Install from the ghcr .deb bundle (offline) + id: ghcr + if: inputs.ghcr-debs-tag != '' + shell: bash + run: | + set -u + IMG="ghcr.io/wolfssl/wolfssh-ci-debs:${{ inputs.ghcr-debs-tag }}" + + fallback() { + echo "::warning::$IMG: $1; falling back to the apt mirror" + echo "apt fallback: \`$IMG\`: $1" \ + >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + exit 0 + } + + # A bundle holds .debs for one Ubuntu release. + WANT=$(printf '%s' "${{ inputs.ghcr-debs-tag }}" \ + | sed -n 's/^\(ubuntu-[0-9]*\.[0-9]*\).*/\1/p') + HAVE="" + if [ -r /etc/os-release ]; then + HAVE=$( . /etc/os-release + printf '%s-%s' "${ID:-?}" "${VERSION_ID:-?}" ) + fi + if [ -n "$WANT" ] && [ -n "$HAVE" ] && [ "$WANT" != "$HAVE" ]; then + fallback "holds .debs for $WANT, but this job runs on $HAVE" + fi + command -v docker >/dev/null 2>&1 || fallback "no docker CLI" + + BUNDLE="$RUNNER_TEMP/ghcr-debs" + APTD="$RUNNER_TEMP/ghcr-apt" + rm -rf "$BUNDLE" "$APTD" + mkdir -p "$BUNDLE" "$APTD/etc" "$APTD/lists/partial" + + # The image must be PUBLIC so an anonymous pull works from fork PRs. + timeout -k 10 ${{ inputs.pull-timeout }} docker pull -q "$IMG" \ + >/dev/null 2>&1 || fallback "pull failed or timed out" + cid=$(docker create "$IMG" 2>/dev/null) || fallback "cannot open the image" + docker cp "$cid:/debs/." "$BUNDLE/" >/dev/null \ + || fallback "unpacking failed" + docker rm "$cid" >/dev/null 2>&1 || true + ls "$BUNDLE"/*.deb >/dev/null 2>&1 || fallback "no .debs inside" + + if [ -f "$BUNDLE/bundle-info" ]; then + echo "Bundle: $(tr '\n' ' ' < "$BUNDLE/bundle-info")" + echo "Runner: image ${ImageOS:-?} ${ImageVersion:-?}" + fi + [ -s "$BUNDLE/Packages" ] || fallback "no Packages index" + + printf 'deb [trusted=yes] file:%s ./\n' "$BUNDLE" > "$APTD/etc/sources.list" + # The bundle is the only repository apt can see. The unroutable proxy + # is a tripwire: a non-file: URI fails at once instead of hanging on + # the mirror. + APT_LOCAL=(-o Dir::Etc::SourceList="$APTD/etc/sources.list" + -o Dir::Etc::SourceParts=/dev/null + -o Dir::State::Lists="$APTD/lists" + -o Acquire::Languages=none + -o Acquire::Retries=0 + -o APT::Sandbox::User=root + -o Acquire::http::Proxy=http://127.0.0.1:9 + -o Acquire::https::Proxy=http://127.0.0.1:9) + + sudo apt-get "${APT_LOCAL[@]}" update >/dev/null 2>&1 \ + || fallback "apt could not read the bundle index" + + NO_REC="" + if [ "${{ inputs.no-install-recommends }}" = "true" ]; then + NO_REC="--no-install-recommends" + fi + if sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_LOCAL[@]}" \ + install -y $NO_REC ${{ inputs.packages }}; then + echo "satisfied=true" >> "$GITHUB_OUTPUT" + echo "Installed offline from $IMG" + else + fallback "does not cover ${{ inputs.packages }}" + fi + + - name: Install packages + if: steps.ghcr.outputs.satisfied != 'true' + shell: bash + run: | + RETRIES=${{ inputs.retries }} + DELAY=${{ inputs.retry-delay }} + BUDGET=${{ inputs.budget-seconds }} + NO_REC="" + if [ "${{ inputs.no-install-recommends }}" = "true" ]; then + NO_REC="--no-install-recommends" + fi + + # A wedged mirror hangs apt rather than failing it. apt drops a + # stalled connection after 30s and retries it, `timeout` kills an + # apt-get that wedged anyway, and the loop re-runs, re-reading + # apt-mirrors.txt so a retry can land on another mirror. apt resumes + # from archives/partial/, so a killed transfer is not restarted. + + # No wolfSSH job installs from the runner's Google/Microsoft repos, and + # a bad index on either fails apt-get update. Drop them. + grep -rlE 'dl\.google\.com|packages\.microsoft\.com' \ + /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -vf || true + APT_OPTS=(-o Acquire::Retries=3 -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30) + + DEADLINE=$(($(date +%s) + BUDGET)) + + # sudo resets the environment, so DEBIAN_FRONTEND rides along on each + # privileged command. + for i in $(seq 1 $RETRIES); do + # Split what is left over the attempts still to come. update gets + # half an attempt, capped at the 90s apt's own retries need. + PER=$(( (DEADLINE - $(date +%s)) / (RETRIES - i + 1) )) + UPD=$((PER / 2)) + [ "$UPD" -le 90 ] || UPD=90 + [ "$UPD" -ge 20 ] || UPD=20 + INS=$((PER - UPD)) + [ "$INS" -ge 40 ] || INS=40 + # A previous attempt killed mid-unpack leaves dpkg needing this. + sudo dpkg --configure -a >/dev/null 2>&1 || true + if sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 $UPD \ + apt-get "${APT_OPTS[@]}" update -q && \ + sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 $INS \ + apt-get "${APT_OPTS[@]}" install -y \ + $NO_REC ${{ inputs.packages }}; then + exit 0 + fi + if [ "$i" -eq "$RETRIES" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; then + echo "::error::apt-get failed after $i attempt(s) in ${BUDGET}s" + exit 1 + fi + echo "::warning::apt-get failed (attempt $i/$RETRIES), retrying in ${DELAY}s..." + sleep $DELAY + DELAY=$((DELAY * 2)) + done diff --git a/.github/ci-deps/packages-ubuntu-24.04-compilers.txt b/.github/ci-deps/packages-ubuntu-24.04-compilers.txt new file mode 100644 index 000000000..50a3faaca --- /dev/null +++ b/.github/ci-deps/packages-ubuntu-24.04-compilers.txt @@ -0,0 +1,15 @@ +# Compilers for multi-compiler, code-coverage and the mingw cross build, +# bundled by ci-deps-image.yml. Kept apart from -tests for its size. +autoconf +automake +clang-14 +clang-15 +clang-17 +clang-18 +gcc-11 +gcc-12 +gcc-13 +gcc-mingw-w64-x86-64 +libclang-rt-18-dev +libtool +llvm-18 diff --git a/.github/ci-deps/packages-ubuntu-24.04-tests.txt b/.github/ci-deps/packages-ubuntu-24.04-tests.txt new file mode 100644 index 000000000..71d03a06c --- /dev/null +++ b/.github/ci-deps/packages-ubuntu-24.04-tests.txt @@ -0,0 +1,24 @@ +# Packages for the wolfSSH test workflows, bundled by ci-deps-image.yml. +# A step's whole package list must be here or it falls back to the mirror. +autoconf +autoconf-archive +automake +build-essential +cppcheck +dosfstools +expect +git +libcmocka-dev +libglib2.0-dev +libssl-dev +libtool +netcat-traditional +openssh-client +openssh-server +pkg-config +python3-pip +sshpass +tpm2-tools +uthash-dev +valgrind +zlib1g-dev diff --git a/.github/scripts/download-deb-closure.sh b/.github/scripts/download-deb-closure.sh new file mode 100755 index 000000000..14bb1f785 --- /dev/null +++ b/.github/scripts/download-deb-closure.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Download the .deb closure for one package list into a directory, for +# .github/workflows/ci-deps-image.yml to publish as a bundle. Ported from +# wolfSSL's .github/scripts/download-deb-closure.sh. +# +# Runs as root on the runner. apt downloads only what is not already +# installed, so the closure carries nothing the runner image preinstalls and +# is only installable on that same runner image. +# +# usage: download-deb-closure.sh +set -uo pipefail + +LIST=${1:?package list} +DEST=${2:?destination directory} + +mapfile -t PKGS < <(grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$LIST") +echo "Packages (${#PKGS[@]}): ${PKGS[*]}" +export DEBIAN_FRONTEND=noninteractive +mkdir -p "$DEST" && rm -f "$DEST"/*.deb +apt-get clean +# No wolfSSH job installs from the runner's Google/Microsoft apt repos, and a +# bad index on either fails apt-get update. Drop them. +grep -rlE 'dl\.google\.com|packages\.microsoft\.com' \ + /etc/apt/sources.list.d/ 2>/dev/null | xargs -r rm -vf || true +# apt drops a stalled connection after 30s and retries it, `timeout` kills a +# wedged apt-get, then retry() re-runs it. +APT_OPTS=(-o Acquire::Retries=3 -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30) +retry() { local i; for i in 1 2 3 4 5; do "$@" && return 0; sleep $((2**i)); done; "$@"; } +retry timeout -k 10 120 apt-get "${APT_OPTS[@]}" update -q +# One closure per package, so one unbundleable package cannot abort the rest; +# install-apt-deps falls back to apt for anything missing. +skipped=0 +for pkg in "${PKGS[@]}"; do + retry timeout -k 10 300 apt-get "${APT_OPTS[@]}" install -y --download-only "$pkg" \ + || { echo "::warning::could not download $pkg"; skipped=$((skipped+1)); } +done +cp /var/cache/apt/archives/*.deb "$DEST/" 2>/dev/null || true +# The index and image steps run unprivileged. +chown --reference="$DEST" "$DEST"/*.deb 2>/dev/null || true +echo "Bundled $(ls "$DEST"/*.deb 2>/dev/null | wc -l) .deb files ($(du -sh "$DEST" | cut -f1)); ${skipped} skipped" +test -n "$(ls "$DEST"/*.deb 2>/dev/null)" # fail if nothing was bundled diff --git a/.github/workflows/ci-deps-image.yml b/.github/workflows/ci-deps-image.yml new file mode 100644 index 000000000..70701e0de --- /dev/null +++ b/.github/workflows/ci-deps-image.yml @@ -0,0 +1,86 @@ +name: CI deps image + +# Builds the apt .deb bundles that .github/actions/install-apt-deps installs +# offline (input ghcr-debs-tag), so PR jobs stay off the apt mirror. Each +# bundle holds the .debs for a package list in .github/ci-deps/ - every +# package plus the dependencies not already on the runner image - and its own +# apt index, published to ghcr.io/wolfssl/wolfssh-ci-debs:. Ported from +# wolfSSL's ci-deps-image workflow. +# +# ONE-TIME SETUP: after the first successful run, make the package +# `wolfssh-ci-debs` PUBLIC (org > Packages > Package settings > Change +# visibility). Until then install-apt-deps falls back to apt. + +on: + schedule: + - cron: '0 2 * * *' + push: + branches: [ master ] + paths: + - '.github/ci-deps/**' + - '.github/scripts/download-deb-closure.sh' + - '.github/workflows/ci-deps-image.yml' + workflow_dispatch: + +concurrency: + group: ci-deps-image-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +jobs: + build: + name: build ${{ matrix.tag }} + if: github.repository_owner == 'wolfssl' + strategy: + fail-fast: false + matrix: + include: + # The .debs must be downloaded on the Ubuntu version that consumes + # them. -compilers is its own tag so the other jobs do not pull it. + - runner: ubuntu-24.04 + tag: ubuntu-24.04-tests + - runner: ubuntu-24.04 + tag: ubuntu-24.04-compilers + runs-on: ${{ matrix.runner }} + # Backstop only; the download script kills and retries stalled apt calls. + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + + - name: Resolve and download the .deb closure + run: | + sudo bash .github/scripts/download-deb-closure.sh \ + ".github/ci-deps/packages-${{ matrix.tag }}.txt" debs + + - name: Index the bundle + run: | + set -euo pipefail + # The consumer resolves against this index, not the runner's lists. + # dpkg-scanpackages is from dpkg-dev, preinstalled on the runners. + ( cd debs && dpkg-scanpackages --multiversion . /dev/null > Packages ) + gzip -9kf debs/Packages + printf 'tag=%s\nrunner=%s %s\nbuilt=%s\ndebs=%s\n' \ + "${{ matrix.tag }}" "${ImageOS:-?}" "${ImageVersion:-?}" \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(ls debs/*.deb | wc -l)" \ + > debs/bundle-info + cat debs/bundle-info + + - name: Build bundle image + run: | + printf 'FROM busybox\nCOPY debs /debs\nLABEL org.opencontainers.image.source=https://github.com/wolfSSL/wolfssh\n' \ + > Dockerfile.debs + docker build -f Dockerfile.debs -t bundle . + + - name: Log in to ghcr + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Push to ghcr + run: | + set -euo pipefail + IMG="ghcr.io/wolfssl/wolfssh-ci-debs:${{ matrix.tag }}" + docker tag bundle "$IMG" + docker push "$IMG" + echo "Pushed $IMG" diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index ad3289447..d950dcc40 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -56,10 +56,11 @@ jobs: # clang 18 is the min: -fcoverage-mcdc does not exist before it. - name: Install clang and LLVM coverage tools - timeout-minutes: 5 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y clang-18 llvm-18 libclang-rt-18-dev + uses: ./.github/actions/install-apt-deps + with: + packages: clang-18 llvm-18 libclang-rt-18-dev + ghcr-debs-tag: ubuntu-24.04-compilers + budget-seconds: 300 - name: Download wolfSSL uses: actions/download-artifact@v8 diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml index d9a43c3b9..c728a6b67 100644 --- a/.github/workflows/cppcheck.yml +++ b/.github/workflows/cppcheck.yml @@ -16,10 +16,10 @@ jobs: - name: Install cppcheck if: always() - timeout-minutes: 5 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y cppcheck + uses: ./.github/actions/install-apt-deps + with: + packages: cppcheck + ghcr-debs-tag: ubuntu-24.04-tests - name: Run CppCheck id: cpp_check_run diff --git a/.github/workflows/multi-compiler.yml b/.github/workflows/multi-compiler.yml index 290cce46e..e92c60b02 100644 --- a/.github/workflows/multi-compiler.yml +++ b/.github/workflows/multi-compiler.yml @@ -64,15 +64,16 @@ jobs: cxx: clang++-17 steps: - - name: Install compiler - timeout-minutes: 5 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y ${{ matrix.cc }} - - name: Checkout wolfSSH uses: actions/checkout@v6 + - name: Install compiler + uses: ./.github/actions/install-apt-deps + with: + packages: ${{ matrix.cc }} + ghcr-debs-tag: ubuntu-24.04-compilers + budget-seconds: 300 + - name: Download wolfSSL uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/network-contention-test.yml b/.github/workflows/network-contention-test.yml index 40508138a..8f518f057 100644 --- a/.github/workflows/network-contention-test.yml +++ b/.github/workflows/network-contention-test.yml @@ -108,8 +108,10 @@ jobs: run: ./scripts/sftp.test - name: Install expect for extended tests - timeout-minutes: 2 - run: sudo apt-get -o Acquire::Retries=3 update && sudo apt-get -o Acquire::Retries=3 install -y expect + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: expect + ghcr-debs-tag: ubuntu-24.04-tests - name: Create large test files working-directory: ./wolfssh/ diff --git a/.github/workflows/paramiko-sftp-test.yml b/.github/workflows/paramiko-sftp-test.yml index 8198583c3..734dce9a8 100644 --- a/.github/workflows/paramiko-sftp-test.yml +++ b/.github/workflows/paramiko-sftp-test.yml @@ -67,11 +67,13 @@ jobs: run: make - name: Install dependencies - timeout-minutes: 5 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y python3-pip openssh-client - python3 -m pip install paramiko + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: python3-pip openssh-client + ghcr-debs-tag: ubuntu-24.04-tests + + - name: Install paramiko + run: python3 -m pip install paramiko - name: Create test directories run: | diff --git a/.github/workflows/sftp-test.yml b/.github/workflows/sftp-test.yml index c6aaaafb7..14ccfdd2f 100644 --- a/.github/workflows/sftp-test.yml +++ b/.github/workflows/sftp-test.yml @@ -107,10 +107,10 @@ jobs: sleep 2 # Give the server time to start - name: Install expect - timeout-minutes: 2 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y expect + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: expect + ghcr-debs-tag: ubuntu-24.04-tests - name: Run SFTP test working-directory: ./wolfssh/ diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml index e007dd490..23db8e51f 100644 --- a/.github/workflows/sshd-test.yml +++ b/.github/workflows/sshd-test.yml @@ -92,7 +92,7 @@ jobs: extra_flags: ' -DWOLFSSH_SMALL_STACK' name: Build and test the wolfsshd and wolfssh apps runs-on: ${{ matrix.os }} - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checking cache for wolfssl uses: actions/cache@v5 @@ -124,13 +124,12 @@ jobs: git log -3 sudo ./run_all_sshd_tests.sh - # could use optimization with caching - name: Install valgrind if: matrix.extra_flags == '' - timeout-minutes: 2 - run: | - sudo apt-get -o Acquire::Retries=3 -y update - sudo apt-get -o Acquire::Retries=3 -y install valgrind + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: valgrind + ghcr-debs-tag: ubuntu-24.04-tests - name: Test memory after close down working-directory: ./wolfssh/ diff --git a/.github/workflows/test-fatfs.yml b/.github/workflows/test-fatfs.yml index 05945345b..ed94afcd8 100644 --- a/.github/workflows/test-fatfs.yml +++ b/.github/workflows/test-fatfs.yml @@ -43,10 +43,10 @@ jobs: fail-on-cache-miss: true - name: Install dependencies - timeout-minutes: 3 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y build-essential autoconf automake libtool pkg-config openssh-server dosfstools + uses: ./.github/actions/install-apt-deps + with: + packages: build-essential autoconf automake libtool pkg-config openssh-server dosfstools expect + ghcr-debs-tag: ubuntu-24.04-tests - name: Build wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 @@ -89,12 +89,6 @@ jobs: echo " UserKnownHostsFile=/dev/null" >> ~/.ssh/config chmod 600 ~/.ssh/config - - name: Install expect - timeout-minutes: 2 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y expect - - name: Run wolfsftp client to get file run: | # Export the library path diff --git a/.github/workflows/tpm-ssh.yml b/.github/workflows/tpm-ssh.yml index 377762dae..3c4c29fdd 100644 --- a/.github/workflows/tpm-ssh.yml +++ b/.github/workflows/tpm-ssh.yml @@ -40,13 +40,14 @@ jobs: path: wolftpm - name: Install Dependencies - timeout-minutes: 10 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y libtool automake autoconf - sudo apt-get -o Acquire::Retries=3 install -y build-essential git autoconf-archive \ - libcmocka-dev libssl-dev uthash-dev libglib2.0-dev \ - tpm2-tools openssh-client sshpass + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: >- + libtool automake autoconf build-essential git autoconf-archive + libcmocka-dev libssl-dev uthash-dev libglib2.0-dev + tpm2-tools openssh-client sshpass + ghcr-debs-tag: ubuntu-24.04-tests + budget-seconds: 300 - name: Build wolfSSL run: | diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index 2c02134fd..af7ed7d99 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -298,10 +298,11 @@ jobs: - uses: actions/checkout@v4 - name: Install mingw toolchain and autotools - timeout-minutes: 5 - run: | - sudo apt-get -o Acquire::Retries=3 update - sudo apt-get -o Acquire::Retries=3 install -y gcc-mingw-w64-x86-64 autoconf automake libtool + uses: ./.github/actions/install-apt-deps + with: + packages: gcc-mingw-w64-x86-64 autoconf automake libtool + ghcr-debs-tag: ubuntu-24.04-compilers + budget-seconds: 300 - name: Generate configure run: ./autogen.sh diff --git a/.github/workflows/x509-interop.yml b/.github/workflows/x509-interop.yml index 4bdc5e205..5128a0f74 100644 --- a/.github/workflows/x509-interop.yml +++ b/.github/workflows/x509-interop.yml @@ -63,12 +63,17 @@ jobs: key: wolfssh-x509-interop-pkixssh-${{ env.PKIXSSH_VERSION }}-ubuntu-latest lookup-only: true + - uses: actions/checkout@v6 + if: steps.cache-pkixssh.outputs.cache-hit != 'true' + with: + sparse-checkout: .github/actions + - name: Install build dependencies - timeout-minutes: 5 if: steps.cache-pkixssh.outputs.cache-hit != 'true' - run: | - sudo apt-get -o Acquire::Retries=3 -y update - sudo apt-get -o Acquire::Retries=3 -y install libssl-dev zlib1g-dev + uses: ./.github/actions/install-apt-deps + with: + packages: libssl-dev zlib1g-dev + ghcr-debs-tag: ubuntu-24.04-tests - name: Download, build, and install PKIX-SSH if: steps.cache-pkixssh.outputs.cache-hit != 'true' @@ -108,16 +113,16 @@ jobs: key: wolfssh-x509-interop-pkixssh-${{ env.PKIXSSH_VERSION }}-ubuntu-latest fail-on-cache-miss: true - - name: Install test dependencies - timeout-minutes: 2 - run: | - sudo apt-get -o Acquire::Retries=3 -y update - sudo apt-get -o Acquire::Retries=3 -y install netcat-traditional - - uses: actions/checkout@v6 with: path: wolfssh/ + - name: Install test dependencies + uses: ./wolfssh/.github/actions/install-apt-deps + with: + packages: netcat-traditional + ghcr-debs-tag: ubuntu-24.04-tests + - name: autogen working-directory: ./wolfssh/ run: ./autogen.sh From 6af7c1a515bc595c17bc016632b247b884da285f Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 1 Oct 2026 11:59:02 -0700 Subject: [PATCH 3/4] CI: create the bundle directory before sudo download-deb-closure.sh runs as root, so a debs directory it creates is root-owned and the unprivileged index step cannot write Packages into it. Create the directory as the runner user first; the script then chowns the .debs to match. --- .github/workflows/ci-deps-image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-deps-image.yml b/.github/workflows/ci-deps-image.yml index 70701e0de..4c253e7d8 100644 --- a/.github/workflows/ci-deps-image.yml +++ b/.github/workflows/ci-deps-image.yml @@ -52,6 +52,8 @@ jobs: - name: Resolve and download the .deb closure run: | + # Created unprivileged so the later steps can write the index. + mkdir -p debs sudo bash .github/scripts/download-deb-closure.sh \ ".github/ci-deps/packages-${{ matrix.tag }}.txt" debs From d50f6ddfae5a48c308769ac92a17f2eba816a6f5 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 1 Oct 2026 12:46:17 -0700 Subject: [PATCH 4/4] CI: install valgrind without its recommends The sshd-test valgrind install pulled 27 MB with gdb and libc6-dbg, which a slow mirror could not deliver inside the 180s fallback budget. Skip the recommends and give the fallback 300s, which still fits the 15-minute job. --- .github/workflows/sshd-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/sshd-test.yml b/.github/workflows/sshd-test.yml index 23db8e51f..b4b68ad2d 100644 --- a/.github/workflows/sshd-test.yml +++ b/.github/workflows/sshd-test.yml @@ -130,6 +130,9 @@ jobs: with: packages: valgrind ghcr-debs-tag: ubuntu-24.04-tests + # The recommends (gdb, libc6-dbg) nearly double the download. + no-install-recommends: true + budget-seconds: 300 - name: Test memory after close down working-directory: ./wolfssh/