|
| 1 | +name: 'Install apt dependencies' |
| 2 | +description: 'Install apt packages from a prebuilt ghcr .deb bundle, falling back to the apt mirror' |
| 3 | +# Ported from wolfSSL's .github/actions/install-apt-deps. |
| 4 | +inputs: |
| 5 | + packages: |
| 6 | + description: 'Space-separated list of apt packages to install' |
| 7 | + required: true |
| 8 | + ghcr-debs-tag: |
| 9 | + description: > |
| 10 | + Tag of a .deb bundle published to ghcr.io/wolfssl/wolfssh-ci-debs by |
| 11 | + the ci-deps-image workflow (e.g. "ubuntu-24.04-tests"). The packages |
| 12 | + are installed from that bundle with no network access. Any failure - |
| 13 | + bundle missing, not public, or not covering every requested package - |
| 14 | + falls back to the apt mirror, so it is always safe to set. |
| 15 | + required: false |
| 16 | + default: '' |
| 17 | + retries: |
| 18 | + description: 'Attempts at the apt mirror fallback' |
| 19 | + required: false |
| 20 | + default: '2' |
| 21 | + budget-seconds: |
| 22 | + description: > |
| 23 | + Wall-clock for the whole mirror fallback, split across the attempts as |
| 24 | + per-command deadlines so a wedged mirror is reported here instead of |
| 25 | + the job being cancelled around it. It overshoots by retry-delay plus |
| 26 | + 10s of kill grace, and the per-command floors make values below |
| 27 | + retries*80 inert. This plus pull-timeout has to fit the caller's |
| 28 | + timeout-minutes. |
| 29 | + required: false |
| 30 | + default: '180' |
| 31 | + pull-timeout: |
| 32 | + description: 'Deadline in seconds for the ghcr bundle pull' |
| 33 | + required: false |
| 34 | + default: '60' |
| 35 | + retry-delay: |
| 36 | + description: 'Initial delay between retries (seconds, doubles each attempt)' |
| 37 | + required: false |
| 38 | + default: '5' |
| 39 | + no-install-recommends: |
| 40 | + description: 'Pass --no-install-recommends to apt-get install' |
| 41 | + required: false |
| 42 | + default: 'false' |
| 43 | +runs: |
| 44 | + using: 'composite' |
| 45 | + steps: |
| 46 | + # Resolve against ONLY the bundle's own index, through a private |
| 47 | + # sources.list and lists dir. The runner's lists are frozen into its image |
| 48 | + # and lag the archive the bundle was built from, so resolving against them |
| 49 | + # asks for versions the bundle does not carry. The system lists are left |
| 50 | + # untouched for the fallback below. |
| 51 | + - name: Install from the ghcr .deb bundle (offline) |
| 52 | + id: ghcr |
| 53 | + if: inputs.ghcr-debs-tag != '' |
| 54 | + shell: bash |
| 55 | + run: | |
| 56 | + set -u |
| 57 | + IMG="ghcr.io/wolfssl/wolfssh-ci-debs:${{ inputs.ghcr-debs-tag }}" |
| 58 | +
|
| 59 | + fallback() { |
| 60 | + echo "::warning::$IMG: $1; falling back to the apt mirror" |
| 61 | + echo "apt fallback: \`$IMG\`: $1" \ |
| 62 | + >> "${GITHUB_STEP_SUMMARY:-/dev/null}" |
| 63 | + exit 0 |
| 64 | + } |
| 65 | +
|
| 66 | + # A bundle holds .debs for one Ubuntu release. |
| 67 | + WANT=$(printf '%s' "${{ inputs.ghcr-debs-tag }}" \ |
| 68 | + | sed -n 's/^\(ubuntu-[0-9]*\.[0-9]*\).*/\1/p') |
| 69 | + HAVE="" |
| 70 | + if [ -r /etc/os-release ]; then |
| 71 | + HAVE=$( . /etc/os-release |
| 72 | + printf '%s-%s' "${ID:-?}" "${VERSION_ID:-?}" ) |
| 73 | + fi |
| 74 | + if [ -n "$WANT" ] && [ -n "$HAVE" ] && [ "$WANT" != "$HAVE" ]; then |
| 75 | + fallback "holds .debs for $WANT, but this job runs on $HAVE" |
| 76 | + fi |
| 77 | + command -v docker >/dev/null 2>&1 || fallback "no docker CLI" |
| 78 | +
|
| 79 | + BUNDLE="$RUNNER_TEMP/ghcr-debs" |
| 80 | + APTD="$RUNNER_TEMP/ghcr-apt" |
| 81 | + rm -rf "$BUNDLE" "$APTD" |
| 82 | + mkdir -p "$BUNDLE" "$APTD/etc" "$APTD/lists/partial" |
| 83 | +
|
| 84 | + # The image must be PUBLIC so an anonymous pull works from fork PRs. |
| 85 | + timeout -k 10 ${{ inputs.pull-timeout }} docker pull -q "$IMG" \ |
| 86 | + >/dev/null 2>&1 || fallback "pull failed or timed out" |
| 87 | + cid=$(docker create "$IMG" 2>/dev/null) || fallback "cannot open the image" |
| 88 | + docker cp "$cid:/debs/." "$BUNDLE/" >/dev/null \ |
| 89 | + || fallback "unpacking failed" |
| 90 | + docker rm "$cid" >/dev/null 2>&1 || true |
| 91 | + ls "$BUNDLE"/*.deb >/dev/null 2>&1 || fallback "no .debs inside" |
| 92 | +
|
| 93 | + if [ -f "$BUNDLE/bundle-info" ]; then |
| 94 | + echo "Bundle: $(tr '\n' ' ' < "$BUNDLE/bundle-info")" |
| 95 | + echo "Runner: image ${ImageOS:-?} ${ImageVersion:-?}" |
| 96 | + fi |
| 97 | + [ -s "$BUNDLE/Packages" ] || fallback "no Packages index" |
| 98 | +
|
| 99 | + printf 'deb [trusted=yes] file:%s ./\n' "$BUNDLE" > "$APTD/etc/sources.list" |
| 100 | + # The bundle is the only repository apt can see. The unroutable proxy |
| 101 | + # is a tripwire: a non-file: URI fails at once instead of hanging on |
| 102 | + # the mirror. |
| 103 | + APT_LOCAL=(-o Dir::Etc::SourceList="$APTD/etc/sources.list" |
| 104 | + -o Dir::Etc::SourceParts=/dev/null |
| 105 | + -o Dir::State::Lists="$APTD/lists" |
| 106 | + -o Acquire::Languages=none |
| 107 | + -o Acquire::Retries=0 |
| 108 | + -o APT::Sandbox::User=root |
| 109 | + -o Acquire::http::Proxy=http://127.0.0.1:9 |
| 110 | + -o Acquire::https::Proxy=http://127.0.0.1:9) |
| 111 | +
|
| 112 | + sudo apt-get "${APT_LOCAL[@]}" update >/dev/null 2>&1 \ |
| 113 | + || fallback "apt could not read the bundle index" |
| 114 | +
|
| 115 | + NO_REC="" |
| 116 | + if [ "${{ inputs.no-install-recommends }}" = "true" ]; then |
| 117 | + NO_REC="--no-install-recommends" |
| 118 | + fi |
| 119 | + if sudo DEBIAN_FRONTEND=noninteractive apt-get "${APT_LOCAL[@]}" \ |
| 120 | + install -y $NO_REC ${{ inputs.packages }}; then |
| 121 | + echo "satisfied=true" >> "$GITHUB_OUTPUT" |
| 122 | + echo "Installed offline from $IMG" |
| 123 | + else |
| 124 | + fallback "does not cover ${{ inputs.packages }}" |
| 125 | + fi |
| 126 | +
|
| 127 | + - name: Install packages |
| 128 | + if: steps.ghcr.outputs.satisfied != 'true' |
| 129 | + shell: bash |
| 130 | + run: | |
| 131 | + RETRIES=${{ inputs.retries }} |
| 132 | + DELAY=${{ inputs.retry-delay }} |
| 133 | + BUDGET=${{ inputs.budget-seconds }} |
| 134 | + NO_REC="" |
| 135 | + if [ "${{ inputs.no-install-recommends }}" = "true" ]; then |
| 136 | + NO_REC="--no-install-recommends" |
| 137 | + fi |
| 138 | +
|
| 139 | + # A wedged mirror hangs apt rather than failing it. apt drops a |
| 140 | + # stalled connection after 30s and retries it, `timeout` kills an |
| 141 | + # apt-get that wedged anyway, and the loop re-runs, re-reading |
| 142 | + # apt-mirrors.txt so a retry can land on another mirror. apt resumes |
| 143 | + # from archives/partial/, so a killed transfer is not restarted. |
| 144 | +
|
| 145 | + # No wolfSSH job installs from the runner's Google/Microsoft repos, and |
| 146 | + # a bad index on either fails apt-get update. Drop them. |
| 147 | + grep -rlE 'dl\.google\.com|packages\.microsoft\.com' \ |
| 148 | + /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -vf || true |
| 149 | + APT_OPTS=(-o Acquire::Retries=3 -o Acquire::http::Timeout=30 |
| 150 | + -o Acquire::https::Timeout=30) |
| 151 | +
|
| 152 | + DEADLINE=$(($(date +%s) + BUDGET)) |
| 153 | +
|
| 154 | + # sudo resets the environment, so DEBIAN_FRONTEND rides along on each |
| 155 | + # privileged command. |
| 156 | + for i in $(seq 1 $RETRIES); do |
| 157 | + # Split what is left over the attempts still to come. update gets |
| 158 | + # half an attempt, capped at the 90s apt's own retries need. |
| 159 | + PER=$(( (DEADLINE - $(date +%s)) / (RETRIES - i + 1) )) |
| 160 | + UPD=$((PER / 2)) |
| 161 | + [ "$UPD" -le 90 ] || UPD=90 |
| 162 | + [ "$UPD" -ge 20 ] || UPD=20 |
| 163 | + INS=$((PER - UPD)) |
| 164 | + [ "$INS" -ge 40 ] || INS=40 |
| 165 | + # A previous attempt killed mid-unpack leaves dpkg needing this. |
| 166 | + sudo dpkg --configure -a >/dev/null 2>&1 || true |
| 167 | + if sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 $UPD \ |
| 168 | + apt-get "${APT_OPTS[@]}" update -q && \ |
| 169 | + sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 $INS \ |
| 170 | + apt-get "${APT_OPTS[@]}" install -y \ |
| 171 | + $NO_REC ${{ inputs.packages }}; then |
| 172 | + exit 0 |
| 173 | + fi |
| 174 | + if [ "$i" -eq "$RETRIES" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; then |
| 175 | + echo "::error::apt-get failed after $i attempt(s) in ${BUDGET}s" |
| 176 | + exit 1 |
| 177 | + fi |
| 178 | + echo "::warning::apt-get failed (attempt $i/$RETRIES), retrying in ${DELAY}s..." |
| 179 | + sleep $DELAY |
| 180 | + DELAY=$((DELAY * 2)) |
| 181 | + done |
0 commit comments