diff --git a/.github/scripts/apt-retry.sh b/.github/scripts/apt-retry.sh new file mode 100644 index 0000000..e972a1a --- /dev/null +++ b/.github/scripts/apt-retry.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "usage: apt-retry.sh [--sudo] " >&2 + exit 2 +} + +use_sudo=false +if [[ "${1:-}" == "--sudo" ]]; then + use_sudo=true + shift +fi +(( $# > 0 )) || usage + +attempts="${RIVET_APT_RETRY_ATTEMPTS:-2}" +timeout_seconds="${RIVET_APT_TIMEOUT_SECONDS:-180}" +delay_seconds="${RIVET_APT_RETRY_DELAY_SECONDS:-10}" +apt_get="${RIVET_APT_GET:-apt-get}" +timeout_command="${RIVET_TIMEOUT_COMMAND:-timeout}" +if [[ -x /usr/bin/timeout && -z "${RIVET_TIMEOUT_COMMAND:-}" ]]; then + timeout_command=/usr/bin/timeout +fi + +for value_name in attempts timeout_seconds delay_seconds; do + value="${!value_name}" + if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then + echo "apt-retry: $value_name must be a positive integer" >&2 + exit 2 + fi +done + +apt_command=( + env DEBIAN_FRONTEND=noninteractive + "$apt_get" + -o Acquire::Retries=3 + -o DPkg::Lock::Timeout=60 + "$@" +) +if [[ "$use_sudo" == true ]]; then + apt_command=(sudo -n "${apt_command[@]}") +fi + +operation="$1" +for (( attempt = 1; attempt <= attempts; attempt++ )); do + echo "apt-retry: $operation attempt $attempt/$attempts (timeout ${timeout_seconds}s)" + if "$timeout_command" --foreground --signal=TERM --kill-after=30s \ + "${timeout_seconds}s" "${apt_command[@]}"; then + exit 0 + else + status=$? + fi + if (( attempt == attempts )); then + echo "apt-retry: $operation failed after $attempts attempts" >&2 + exit "$status" + fi + sleep "$(( delay_seconds * attempt ))" +done diff --git a/.github/scripts/test-apt-retry.sh b/.github/scripts/test-apt-retry.sh new file mode 100644 index 0000000..c10530d --- /dev/null +++ b/.github/scripts/test-apt-retry.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +fake_apt="$work/fake-apt-get" +cat >"$fake_apt" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +count=0 +if [[ -f "$FAKE_APT_STATE" ]]; then + count="$(cat "$FAKE_APT_STATE")" +fi +count="$(( count + 1 ))" +printf '%s' "$count" >"$FAKE_APT_STATE" +printf '%s\n' "$*" >"$FAKE_APT_ARGUMENTS" +case "$FAKE_APT_MODE" in + retry) + (( count >= 3 )) + ;; + timeout) + sleep 5 + ;; + *) + exit 64 + ;; +esac +SH +chmod +x "$fake_apt" + +export RIVET_APT_GET="$fake_apt" +export RIVET_APT_RETRY_DELAY_SECONDS=1 +export FAKE_APT_STATE="$work/count" +export FAKE_APT_ARGUMENTS="$work/arguments" + +export FAKE_APT_MODE=retry +export RIVET_APT_RETRY_ATTEMPTS=3 +export RIVET_APT_TIMEOUT_SECONDS=5 +bash "$script_dir/apt-retry.sh" install --yes example-package +[[ "$(cat "$FAKE_APT_STATE")" == 3 ]] +grep -Fq -- 'install --yes example-package' "$FAKE_APT_ARGUMENTS" +grep -Fq -- 'Acquire::Retries=3' "$FAKE_APT_ARGUMENTS" +grep -Fq -- 'DPkg::Lock::Timeout=60' "$FAKE_APT_ARGUMENTS" + +export FAKE_APT_MODE=timeout +export RIVET_APT_RETRY_ATTEMPTS=1 +export RIVET_APT_TIMEOUT_SECONDS=1 +if bash "$script_dir/apt-retry.sh" update; then + echo "apt-retry test expected the bounded command to fail" >&2 + exit 1 +fi diff --git a/.github/workflows/architectures.yml b/.github/workflows/architectures.yml index 5658e99..fe64105 100644 --- a/.github/workflows/architectures.yml +++ b/.github/workflows/architectures.yml @@ -172,8 +172,9 @@ jobs: raco pkg install --auto --no-docs --name rivet --link "$PWD" - name: Install GTK4 development files and rpm tooling run: | - sudo apt-get update - sudo apt-get install --yes libgtk-4-dev rpm desktop-file-utils + bash .github/scripts/apt-retry.sh --sudo update + bash .github/scripts/apt-retry.sh --sudo install --yes \ + libgtk-4-dev rpm desktop-file-utils - name: Compile full schema matrix, package, and verify a GTK4 app shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c4e239..5a67881 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: - name: Test release tag provenance validator shell: bash run: bash .github/scripts/test-release-tag.sh + - name: Test bounded apt retry wrapper + shell: bash + run: bash .github/scripts/test-apt-retry.sh - name: Set up Racket CS uses: Bogdanp/setup-racket@2466913449df77df2bad149d1f2fc4e1ea4795dd # v1.15 with: diff --git a/.github/workflows/linux-installers.yml b/.github/workflows/linux-installers.yml index 027b612..59dbff5 100644 --- a/.github/workflows/linux-installers.yml +++ b/.github/workflows/linux-installers.yml @@ -24,8 +24,9 @@ jobs: shell: bash run: | set -euo pipefail - sudo apt-get update - sudo apt-get install --yes desktop-file-utils libgtk-4-dev libsecret-1-dev rpm + bash .github/scripts/apt-retry.sh --sudo update + bash .github/scripts/apt-retry.sh --sudo install --yes \ + desktop-file-utils libgtk-4-dev libsecret-1-dev rpm raco pkg install --auto --no-docs --name rivet --link "$PWD" - name: Build release artifacts shell: bash @@ -119,14 +120,15 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: linux-native-installers path: ${{ runner.temp }}/installers - name: Install headless Wayland compositor run: | - sudo apt-get update - sudo apt-get install --yes weston + bash .github/scripts/apt-retry.sh --sudo update + bash .github/scripts/apt-retry.sh --sudo install --yes weston - name: Launch AppImage outside the build tree shell: bash env: diff --git a/.github/workflows/native-ui.yml b/.github/workflows/native-ui.yml index 50ebcab..db4963b 100644 --- a/.github/workflows/native-ui.yml +++ b/.github/workflows/native-ui.yml @@ -24,8 +24,8 @@ jobs: shell: bash run: | set -euo pipefail - sudo apt-get update - sudo apt-get install --yes \ + bash .github/scripts/apt-retry.sh --sudo update + bash .github/scripts/apt-retry.sh --sudo install --yes \ at-spi2-core dbus-x11 libgtk-4-dev libsecret-1-dev \ python3-pyatspi weston raco pkg install --auto --no-docs --name rivet --link "$PWD" diff --git a/.github/workflows/roundtrip.yml b/.github/workflows/roundtrip.yml index b5cea21..af7fc1d 100644 --- a/.github/workflows/roundtrip.yml +++ b/.github/workflows/roundtrip.yml @@ -24,8 +24,9 @@ jobs: run: raco pkg install --auto --no-docs --name rivet --link "$PWD" - name: Install Linux development files run: | - sudo apt-get update - sudo apt-get install --yes desktop-file-utils libglib2.0-dev libsecret-1-dev libgtk-4-dev weston + bash .github/scripts/apt-retry.sh --sudo update + bash .github/scripts/apt-retry.sh --sudo install --yes \ + desktop-file-utils libglib2.0-dev libsecret-1-dev libgtk-4-dev weston - name: Build backend and integration runner shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cd544..35fdd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Bound and retry Ubuntu runner package-manager operations so a stalled mirror + cannot consume an entire desktop validation job without a useful failure. - Removed the misleading `rivet-type` runtime descriptors from the pre-1.0 public surface. Schema type identifiers are declaration grammar, while Keel is the canonical runtime-inspection model. diff --git a/docs/ci.md b/docs/ci.md index cec8edc..e4be42e 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -94,6 +94,14 @@ Wayland gate: `DISPLAY` is unset, so the application cannot silently pass by using X11 or XWayland. Products supporting Linux should keep an equivalent Wayland launch in their release matrix; see [Linux display policy](linux-display.md). +Rivet's own Ubuntu jobs run provisioning `apt-get` commands through +`.github/scripts/apt-retry.sh`. Each attempt is non-interactive, applies APT's +network and lock retries, and has a three-minute wall-clock limit; one fresh +attempt is allowed before the job fails. Product CI should use the same bounded +principle so a stalled package mirror cannot consume the whole native-build +budget. Package installation lifecycle tests themselves stay unwrapped because +their raw transaction outcome is the behavior under test. + The separate `Native UI End-to-End` workflow packages the maintained Taskboard and drives its real WinUI 3, SwiftUI/AppKit, and GTK4 accessibility surfaces. It covers activation, an RPC mutation, an Event, State rendering, and orderly diff --git a/docs/ci.zh-CN.md b/docs/ci.zh-CN.md index 05bc3d4..9ed4cdb 100644 --- a/docs/ci.zh-CN.md +++ b/docs/ci.zh-CN.md @@ -101,6 +101,12 @@ AT-SPI 检查真实原生界面。门禁覆盖窗口激活、一次 RPC 变更 State 渲染与有序关闭;失败时保留有界的无障碍快照、交互轨迹、截图和日志。 应用如何复用这套模式,见[原生 UI 端到端测试](native-ui-testing.md)。 +Rivet 自己的 Ubuntu 任务会通过 `.github/scripts/apt-retry.sh` 执行环境准备阶段的 +`apt-get`。每次尝试都使用非交互模式、APT 网络与锁重试,并设有三分钟墙钟时间 +上限;失败后只允许再换一次新尝试。产品 CI 也应采用同样的有界原则,避免软件源 +卡住后耗尽整个原生构建预算。安装器生命周期测试仍直接执行原始包管理器事务, +因为事务结果本身就是这些测试要验证的行为。 + ## 发布门禁 推送标签前,应要求上述所有原生宿主任务和产品自己的 Racket 测试通过。