Skip to content

Commit b7462ab

Browse files
authored
Merge pull request #58 from link-foundation/issue-57-3b7cfd0944e2
fix: handle du exit code for missing paths in sandbox script (Issue #57)
2 parents d80121c + 41daed0 commit b7462ab

File tree

4 files changed

+2476
-2
lines changed

4 files changed

+2476
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
bump: patch
3+
---
4+
5+
Fix CI failure caused by `du` exit code regression introduced in Issue #55 fix: only pass paths that exist to `du -sb` to avoid killing the script under `set -euo pipefail` when Homebrew or Rust installation dirs are absent.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Case Study: CI/CD Failed — `du` Exit Code Regression from Issue #55 Fix (Issue #57)
2+
3+
## Summary
4+
5+
The CI/CD "Measure Disk Space and Update README" workflow failed with **exit code 1** on the merge of PR #56 (the Issue #55 fix). The script `scripts/measure-disk-space.sh` exited early inside the sandbox user sub-script when measuring Homebrew. The fix for Issue #55 introduced a regression: `du -sb` is called on paths that may not exist, and with `set -euo pipefail` active, a non-zero `du` exit code kills the script before it can record 0 MB and continue.
6+
7+
## Issue Reference
8+
9+
- **Issue**: [#57 — We have CI/CD failed](https://github.com/link-foundation/sandbox/issues/57)
10+
- **Failed CI run**: https://github.com/link-foundation/sandbox/actions/runs/22347524656/job/64665886012
11+
- **Log file**: `docs/case-studies/issue-57/ci-job-64665886012.log`
12+
- **Triggered by**: Merge of PR #56 (commit `f274bfab` — "1.3.10: Fix language runtime size measurements")
13+
14+
## Timeline of Events
15+
16+
| Time (UTC) | Event |
17+
|---|---|
18+
| 2026-02-22T01:47:26Z | **Last successful run** (PR #52, commit `03a9d8da`) — Homebrew failed silently, recorded 0 MB |
19+
| 2026-02-24T10:48:21Z | PR #56 merged (`f274bfab`) — introduces the Issue #55 fix (du-based measurement for Homebrew/Rust) |
20+
| 2026-02-24T10:48:24Z | CI job starts |
21+
| 2026-02-24T10:58:42Z | Opam + Rocq/Coq recorded (1307.03 MB) |
22+
| 2026-02-24T10:58:42Z | Homebrew installer runs — exits with "Insufficient permissions" |
23+
| 2026-02-24T10:58:44Z | **Script exits with code 1** — all subsequent steps skipped |
24+
25+
## Failure Evidence
26+
27+
From `ci-job-64665886012.log` (lines 2207–2218):
28+
29+
```
30+
==> Running in non-interactive mode because `$NONINTERACTIVE` is set.
31+
/usr/bin/ldd: line 41: printf: write error: Broken pipe
32+
/usr/bin/ldd: line 43: printf: write error: Broken pipe
33+
==> Checking for `sudo` access (which may request your password)...
34+
Insufficient permissions to install Homebrew to "/home/linuxbrew/.linuxbrew" (the default prefix).
35+
36+
Alternative (unsupported) installation methods are available at:
37+
https://docs.brew.sh/Installation#alternative-installs
38+
39+
Please note this will require most formula to build from source, a buggy, slow and energy-inefficient experience.
40+
We will close any issues without response for these unsupported configurations.
41+
42+
##[error]Process completed with exit code 1.
43+
```
44+
45+
---
46+
47+
## Root Cause Analysis
48+
49+
### Root Cause 1: `du` returns exit code 1 for non-existent paths; `set -euo pipefail` kills the script
50+
51+
**Location:** `scripts/measure-disk-space.sh` (sandbox sub-script, Homebrew section, around line 588)
52+
53+
**The buggy code** (introduced by the Issue #55 fix in commit `3471bf8`):
54+
55+
```bash
56+
if install_homebrew; then
57+
cleanup_for_measurement
58+
brew_bytes=$(du -sb /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew" 2>/dev/null | awk '{sum+=$1} END{print sum+0}')
59+
...
60+
```
61+
62+
**What happens step by step:**
63+
64+
1. `install_homebrew()` is called. Inside it:
65+
```bash
66+
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || true
67+
```
68+
The Homebrew installer checks `sudo` access and write permissions. The `sandbox` user has no NOPASSWD sudo, so `sudo -n -l mkdir` fails. Even though `/home/linuxbrew/.linuxbrew` is owned by sandbox (created by the outer script), the Homebrew installer exits with code 1. The `|| true` suppresses this, so `install_homebrew()` returns **0** (success).
69+
70+
2. Because `install_homebrew` returns 0, the `if install_homebrew; then` branch is taken.
71+
72+
3. Inside the branch, `du -sb /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew"` is run:
73+
- `/home/linuxbrew/.linuxbrew` exists (created by the outer script, but empty — brew install failed)
74+
- `"$HOME/.linuxbrew"` does NOT exist (sandbox user HOME = `/home/sandbox`, no `.linuxbrew` there)
75+
- `du` exits with **code 1** because one argument doesn't exist
76+
- The `2>/dev/null` suppresses the stderr error message, but NOT the exit code
77+
- With `set -euo pipefail` active in the sandbox sub-script, the non-zero exit from `du` kills the entire script
78+
79+
4. The sandbox sub-script exits with code 1, which propagates to the outer `sudo -i -u sandbox bash /tmp/sandbox-measure.sh` call, which propagates to the CI step.
80+
81+
**Reproducer:**
82+
```bash
83+
bash -c 'set -euo pipefail; result=$(du -sb /tmp/nonexistent_dir 2>/dev/null | awk "{sum+=\$1} END{print sum+0}"); echo "$result"'
84+
# → exits with code 1 (not 0)
85+
```
86+
87+
### Root Cause 2: Homebrew installer permission check vs. sandbox user
88+
89+
The Homebrew installer (as of Jan 2026) aborts when ALL these conditions are true:
90+
- `HOMEBREW_PREFIX` (`/home/linuxbrew/.linuxbrew`) is not writable, OR
91+
- `/home/linuxbrew` is not writable, OR
92+
- `/home` is not writable, AND
93+
- `have_sudo_access()` returns false
94+
95+
With `NONINTERACTIVE=1`, `have_sudo_access()` runs `sudo -n -l mkdir`. The sandbox user was added to the `sudo` group via `usermod -aG sudo sandbox`, but GitHub Actions runners only grant passwordless sudo to the `runner` user. The sandbox user has no NOPASSWD entry, so `sudo -n` fails.
96+
97+
The outer script creates `/home/linuxbrew/.linuxbrew` and `chown -R sandbox:sandbox /home/linuxbrew` (as root), which should make the directory writable by sandbox. However, this issue represents a brittle dependency: the permission pre-setup in the outer script must work correctly for every run. If something changes (runner image update, timing issue), Homebrew will fail.
98+
99+
**Note**: In the previous run (commit `03a9d8da`), Homebrew also failed with the same "Insufficient permissions" error, but `measure_install "Homebrew" "Package Manager" install_homebrew` correctly handled the failure by recording 0 MB and continuing.
100+
101+
---
102+
103+
## Sequence of Events (Root Cause 1 in detail)
104+
105+
```
106+
outer script (root) runs
107+
→ creates /home/linuxbrew/.linuxbrew, chowns to sandbox
108+
→ writes /tmp/sandbox-measure.sh
109+
→ runs: sudo -i -u sandbox bash /tmp/sandbox-measure.sh /tmp/disk-space-*.json
110+
111+
sandbox-measure.sh (runs as sandbox user, set -euo pipefail)
112+
→ measures Bun, deno, nvm, pyenv, Go, Rust, SDKMAN, Kotlin, Lean, Opam
113+
→ cleanup_for_measurement
114+
→ install_homebrew() called:
115+
NONINTERACTIVE=1 /bin/bash -c "$(brew install.sh)" || true
116+
→ brew installer: "Insufficient permissions" → exits 1
117+
→ || true → install_homebrew returns 0
118+
→ [if branch taken because install_homebrew returned 0]
119+
→ brew_bytes=$(du -sb /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew" 2>/dev/null | awk ...)
120+
→ /home/linuxbrew/.linuxbrew exists (empty)
121+
→ $HOME/.linuxbrew does NOT exist
122+
→ du exits with code 1
123+
→ 2>/dev/null silences the error message
124+
→ EXIT CODE 1 propagates through command substitution $()
125+
→ set -euo pipefail kills the script HERE
126+
← sandbox-measure.sh exits with code 1
127+
128+
outer script exits with code 1
129+
CI step fails
130+
All subsequent steps (Update README, Validate, Commit, Push) are SKIPPED
131+
```
132+
133+
---
134+
135+
## Comparison: Before vs. After Issue #55 Fix
136+
137+
### Before (commit `03a9d8da`, successful run 2026-02-22):
138+
139+
```bash
140+
measure_install "Homebrew" "Package Manager" install_homebrew
141+
```
142+
143+
The `measure_install` wrapper calls `install_homebrew` inside an `if "$@"; then / else` block:
144+
```bash
145+
if "$@"; then
146+
...
147+
else
148+
log_warning "Installation of $name failed"
149+
add_measurement "$name" "$category" 0 0
150+
fi
151+
```
152+
153+
Since `install_homebrew` returns 0 (due to `|| true`), the success branch runs, calculates `df`-based size (which was inaccurate but at least didn't crash), and continues.
154+
155+
### After (commit `3471bf8`, failing run 2026-02-24):
156+
157+
```bash
158+
if install_homebrew; then
159+
brew_bytes=$(du -sb /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew" 2>/dev/null | awk ...)
160+
...
161+
```
162+
163+
`install_homebrew` returns 0 (same as before), but now `du` is called on paths that may not exist, and its exit code is not handled. Result: script dies.
164+
165+
---
166+
167+
## Fix
168+
169+
The fix ensures `du` does not cause script failure when directories don't exist. There are two approaches:
170+
171+
### Option A: Only pass paths that exist to `du`
172+
173+
```bash
174+
cleanup_for_measurement
175+
if install_homebrew; then
176+
cleanup_for_measurement
177+
brew_paths=()
178+
[[ -d /home/linuxbrew/.linuxbrew ]] && brew_paths+=(/home/linuxbrew/.linuxbrew)
179+
[[ -d "$HOME/.linuxbrew" ]] && brew_paths+=("$HOME/.linuxbrew")
180+
if [[ ${#brew_paths[@]} -gt 0 ]]; then
181+
brew_bytes=$(du -sb "${brew_paths[@]}" 2>/dev/null | awk '{sum+=$1} END{print sum+0}')
182+
else
183+
brew_bytes=0
184+
fi
185+
brew_mb=$(awk "BEGIN {printf \"%.2f\", $brew_bytes / 1000000}")
186+
add_measurement "Homebrew" "Package Manager" "$brew_bytes" "$brew_mb"
187+
else
188+
log_warning "Installation of Homebrew failed"
189+
add_measurement "Homebrew" "Package Manager" 0 0
190+
fi
191+
```
192+
193+
### Option B: Use `|| true` to suppress `du` exit code (simpler)
194+
195+
```bash
196+
brew_bytes=$(du -sb /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew" 2>/dev/null | awk '{sum+=$1} END{print sum+0}') || brew_bytes=0
197+
```
198+
199+
**Option A** is more correct because it avoids measuring partial data silently; **Option B** is a minimal change. The same fix applies to the Rust measurement, though Rust paths (`$HOME/.rustup` and `$HOME/.cargo`) are typically both present after a successful install.
200+
201+
The same pattern in the outer script (lines 509–517 for Rust) should also be reviewed, though it doesn't currently fail because Rust installation succeeds and both directories exist.
202+
203+
---
204+
205+
## Related Issues
206+
207+
- **Issue #55**: Incorrect language runtime size measurements (introduced the regression)
208+
- **Issue #46**: Relative path resolution for sandbox user JSON file
209+
- **Issue #49**: sed-based JSON manipulation failures
210+
211+
## External Resources
212+
213+
- [Homebrew install.sh permission check](https://github.com/Homebrew/install/blob/main/install.sh) — the `elif ! [[ -w "${HOMEBREW_PREFIX}" ]] && ... && ! have_sudo_access` condition
214+
- [Homebrew Discussion #5929](https://github.com/orgs/Homebrew/discussions/5929) — "Insufficient permissions to install Homebrew to /home/linuxbrew/.linuxbrew"
215+
- [Homebrew Discussion #4212](https://github.com/orgs/Homebrew/discussions/4212) — sudo-less installation discussion
216+
- [Homebrew Issue #714](https://github.com/Homebrew/install/issues/714) — NONINTERACTIVE mode suppresses sudo password prompt

0 commit comments

Comments
 (0)