Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,90 @@ def split_diagnostic_logd(logd_path: Path, chunk_size: int = DIAGNOSTIC_CHUNK_SI
return chunks


def find_stale_diagnostic_artifacts(
diagnostic_dir: Path,
current_commit_id: str,
) -> list[Path]:
"""Return diagnostic artifacts that do NOT belong to the current commit.

Scans ``diagnostic_dir`` for files matching the standard naming patterns
(``build-XXXXXXXX.logd``, ``build-XXXXXXXX-partNNN.logd``,
``build-XXXXXXXX.json``) and returns those whose embedded commit ID
differs from *current_commit_id*.

This is a read-only operation -- nothing is deleted.
"""
if not diagnostic_dir.exists():
return []

stale: list[Path] = []
for entry in sorted(diagnostic_dir.iterdir()):
if not entry.is_file():
continue
name = entry.name
# Match build-XXXXXXXX.logd, build-XXXXXXXX-partNNN.logd, build-XXXXXXXX.json
if name.startswith("build-") and (
name.endswith(".logd") or name.endswith(".json")
):
# Extract the 8-char commit id after "build-"
# For "build-ec966bc5.logd" -> "ec966bc5"
# For "build-ec966bc5-part001.logd" -> "ec966bc5"
# For "build-ec966bc5.json" -> "ec966bc5"
suffix = name[len("build-"):]
commit_part = suffix.split("-")[0] # handles both with and without -part
if len(commit_part) == 8 and all(c in "0123456789abcdef" for c in commit_part):
if commit_part != current_commit_id:
stale.append(entry)
return stale


def run_diagnostic_cleanup(
diagnostic_dir: Path,
current_commit_id: str,
force: bool = False,
) -> int:
"""List (dry-run) or delete (force) stale diagnostic artifacts.

Returns the number of stale artifacts found (or deleted when *force* is
True). Current-commit artifacts are never removed.
"""
stale = find_stale_diagnostic_artifacts(diagnostic_dir, current_commit_id)

if not stale:
print(f" {color('✓', Colors.GREEN)} No stale diagnostic artifacts found.")
return 0

mode_label = "DELETING" if force else "WOULD DELETE (dry-run)"
mode_color = Colors.RED if force else Colors.YELLOW
print(f"\n {color(mode_label, mode_color)} {len(stale)} stale diagnostic artifact(s):")

total_bytes = 0
for artifact in stale:
try:
size = artifact.stat().st_size
total_bytes += size
size_str = f"{size / 1024:.1f} KiB" if size < 1024 * 1024 else f"{size / (1024 * 1024):.1f} MiB"
except OSError:
size_str = "unknown size"
rel = artifact.relative_to(ROOT) if artifact.is_relative_to(ROOT) else artifact
print(f" {color('▸', mode_color)} {rel} ({size_str})")

total_str = f"{total_bytes / 1024:.1f} KiB" if total_bytes < 1024 * 1024 else f"{total_bytes / (1024 * 1024):.1f} MiB"
print(f"\n Total space that {'would be freed' if not force else 'freed'}: {total_str}")

if not force:
print(f" {color('Run with --force-cleanup to actually delete these artifacts.', Colors.GRAY)}")
else:
for artifact in stale:
try:
artifact.unlink()
except OSError as e:
print(f" {color('✗', Colors.RED)} Failed to delete {artifact.name}: {e}")
print(f" {color('✓', Colors.GREEN)} Stale diagnostic artifacts deleted.")

return len(stale)


@dataclass
class Module:
name: str
Expand Down Expand Up @@ -827,6 +911,10 @@ def main():

Diagnostic bundle:
python3 build.py

Stale diagnostic cleanup:
python3 build.py --diagnostic-cleanup Dry-run: list stale artifacts
python3 build.py --diagnostic-cleanup --force-cleanup Actually delete stale artifacts
""",
)
parser.add_argument(
Expand All @@ -850,6 +938,16 @@ def main():
"--list", action="store_true",
help="List available modules and exit",
)
parser.add_argument(
"--diagnostic-cleanup", action="store_true",
help="List stale diagnostic artifacts (dry-run by default; "
"current commit artifacts are never removed)",
)
parser.add_argument(
"--force-cleanup", action="store_true",
help="When used with --diagnostic-cleanup, actually delete stale "
"artifacts instead of just listing them",
)

args = parser.parse_args()

Expand All @@ -865,6 +963,17 @@ def main():
print(f" build: {' '.join(m.build_cmd)}")
return 0

if args.diagnostic_cleanup:
commit_id = current_commit_id()
print(f" {color('Stale diagnostic cleanup', Colors.BOLD)}")
print(f" Current commit: {commit_id}")
if args.force_cleanup:
print(f" {color('Mode: FORCE (artifacts will be deleted)', Colors.RED)}")
else:
print(f" {color('Mode: DRY-RUN (no files will be modified)', Colors.YELLOW)}")
count = run_diagnostic_cleanup(DIAGNOSTIC_DIR, commit_id, force=args.force_cleanup)
return 0

print(f" {color('Checking prerequisites...', Colors.GRAY)}")
missing = check_prerequisites()
if missing:
Expand Down
92 changes: 92 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,98 @@ FROM pg_stat_activity
WHERE state = 'idle' AND age > interval '1 hour';
```

## Diagnostic Artifact Cleanup

Over time, the `diagnostic/` directory accumulates build artifacts (`.logd` and
`.json` files) from previous commits. Stale artifacts from old or interrupted
builds can make the directory noisy and hard to audit. The build system
provides a dedicated cleanup mode that safely identifies and optionally removes
these stale files.

### Overview

Each build generates diagnostic artifacts named after the current Git commit:

- `diagnostic/build-XXXXXXXX.logd` -- encrypted diagnostic log bundle
- `diagnostic/build-XXXXXXXX-partNNN.logd` -- chunked parts of oversized logs
- `diagnostic/build-XXXXXXXX.json` -- diagnostic metadata

Artifacts whose commit ID does not match the current HEAD are considered
**stale**. The current commit's artifacts are always preserved.

### Dry-Run Mode (Default)

To see which stale artifacts would be removed without actually deleting
anything:

```bash
python3 build.py --diagnostic-cleanup
```

This is safe for CI pipelines and local inspection. It prints a list of stale
files with their sizes and the total space that would be freed, but does not
modify any files.

### Force Mode (Actual Deletion)

To actually delete the stale artifacts:

```bash
python3 build.py --diagnostic-cleanup --force-cleanup
```

This removes all stale diagnostic files while preserving the current commit's
artifacts. Use this when you need to reclaim disk space or clean up before a
release.

### Safety Guarantees

- **Dry-run is the default**: running `--diagnostic-cleanup` alone never
modifies or deletes any files.
- **Current commit artifacts are always preserved**: the cleanup logic
explicitly excludes any file whose embedded commit ID matches the current
HEAD, including chunked `.logd` parts.
- **Only standard diagnostic files are targeted**: files that do not match the
`build-XXXXXXXX.logd`, `build-XXXXXXXX-partNNN.logd`, or
`build-XXXXXXXX.json` naming pattern are ignored.

### Running Tests

The cleanup logic is tested independently using temporary directories:

```bash
python3 -m pytest tools/test_stale_cleanup.py -v
```

Or with unittest directly:

```bash
python3 tools/test_stale_cleanup.py
```

### Example Session

```
$ python3 build.py --diagnostic-cleanup

Tent of Trials: building
Working directory: /home/user/TentOfTrials

Stale diagnostic cleanup
Current commit: ec966bc5
Mode: DRY-RUN (no files will be modified)

WOULD DELETE (dry-run) 12 stale diagnostic artifact(s):
▸ diagnostic/build-0f9ce9bb.json (0.5 KiB)
▸ diagnostic/build-0f9ce9bb.logd (12.3 KiB)
▸ diagnostic/build-11b17016.json (0.5 KiB)
▸ diagnostic/build-11b17016-part001.logd (40960.0 KiB)
...

Total space that would be freed: 245.8 MiB
Run with --force-cleanup to actually delete these artifacts.
```

## Capacity Planning

### Resource Utilization
Expand Down
Loading