diff --git a/.github/actions/measure-after/action.yml b/.github/actions/measure-after/action.yml new file mode 100644 index 0000000..f469546 --- /dev/null +++ b/.github/actions/measure-after/action.yml @@ -0,0 +1,84 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Measure space after cleanup +description: Ends timer, measures disk space, computes metrics, and uploads CSV artifact +# This action runs after cleanup actions. For easimon jobs which remount the workspace, +# the action must be inlined directly in the workflow (see easimon job). +# Accepts start_ts as input to compute duration without relying on environment variables +# that may be lost during workspace operations. +inputs: + image: + description: Runner image name + required: true + option: + description: Cleanup option name + required: true + intensity: + description: Intensity level (fast/standard/max) + required: true + repeat: + description: Repeat number + required: true + before_root_avail: + description: Available bytes on root before cleanup + required: true + before_ws_avail: + description: Available bytes on workspace before cleanup + required: true + start_ts: + description: Start timestamp in seconds from measure-before + required: true +runs: + using: composite + steps: + - name: Measure space after + id: after + shell: bash + run: | + ROOT_AVAIL=$(df --output=avail -B1 / | tail -1) + WS_PATH=${GITHUB_WORKSPACE:-$PWD} + WS_AVAIL=$(df --output=avail -B1 "$WS_PATH" | tail -1) + echo "root_avail=$ROOT_AVAIL" >> "$GITHUB_OUTPUT" + echo "ws_path=$WS_PATH" >> "$GITHUB_OUTPUT" + echo "ws_avail=$WS_AVAIL" >> "$GITHUB_OUTPUT" + + - name: Compute metrics CSV + shell: bash + run: | + END_TS=$(date +%s) + START_TS="${{ inputs.start_ts }}" + DURATION=$((END_TS - START_TS)) + FREED_ROOT=$(( ${{ steps.after.outputs.root_avail }} - ${{ inputs.before_root_avail }} )) + FREED_WS=$(( ${{ steps.after.outputs.ws_avail }} - ${{ inputs.before_ws_avail }} )) + + # Validate that cleanup freed at least 100 Bytes on either filesystem + MIN_THRESHOLD=100 + if [[ $FREED_WS -lt $MIN_THRESHOLD && $FREED_ROOT -lt $MIN_THRESHOLD ]]; then + echo "❌ Error: Cleanup freed less than 100 Bytes (freed_ws=$(echo "scale=2; $FREED_WS / 1073741824" | bc) GiB, freed_root=$(echo "scale=2; $FREED_ROOT / 1073741824" | bc) GiB)" + echo "This indicates the cleanup configuration is not working properly." + exit 1 + fi + + mkdir -p metrics + OUT=metrics/metrics.csv + if [[ ! -f "$OUT" ]]; then + echo "image,option,intensity,repeat,before_root,after_root,freed_root,before_ws,after_ws,freed_ws,duration_seconds" > "$OUT" + fi + echo "${{ inputs.image }},${{ inputs.option }},${{ inputs.intensity }},${{ inputs.repeat }},${{ inputs.before_root_avail }},${{ steps.after.outputs.root_avail }},$FREED_ROOT,${{ inputs.before_ws_avail }},${{ steps.after.outputs.ws_avail }},$FREED_WS,$DURATION" >> "$OUT" + + - name: Upload metrics artifact + uses: actions/upload-artifact@v4 + with: + name: metrics-${{ inputs.image }}-${{ inputs.option }}-${{ inputs.intensity }}-${{ inputs.repeat }} + path: metrics/metrics.csv diff --git a/.github/actions/measure-before/action.yml b/.github/actions/measure-before/action.yml new file mode 100644 index 0000000..8ec3f5f --- /dev/null +++ b/.github/actions/measure-before/action.yml @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Measure space before cleanup +description: Prepares metrics directory, measures disk space, and starts timer +# This action captures the baseline disk state and starts the timer. +# Outputs are used by measure-after to compute freed space and duration. +# start_ts must be passed through step outputs (not env vars) to survive workspace remounts. +outputs: + root_avail: + description: Available bytes on root filesystem + value: ${{ steps.before.outputs.root_avail }} + ws_path: + description: Workspace path + value: ${{ steps.before.outputs.ws_path }} + ws_avail: + description: Available bytes on workspace filesystem + value: ${{ steps.before.outputs.ws_avail }} + start_ts: + description: Start timestamp in seconds + value: ${{ steps.timer.outputs.start_ts }} +runs: + using: composite + steps: + - name: Prepare metrics dir + shell: bash + run: mkdir -p metrics + + - name: Measure space before + id: before + shell: bash + run: | + ROOT_AVAIL=$(df --output=avail -B1 / | tail -1) + WS_PATH=${GITHUB_WORKSPACE:-$PWD} + WS_AVAIL=$(df --output=avail -B1 "$WS_PATH" | tail -1) + echo "root_avail=$ROOT_AVAIL" >> "$GITHUB_OUTPUT" + echo "ws_path=$WS_PATH" >> "$GITHUB_OUTPUT" + echo "ws_avail=$WS_AVAIL" >> "$GITHUB_OUTPUT" + + - name: Start timer + id: timer + shell: bash + run: echo "start_ts=$(date +%s)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml new file mode 100644 index 0000000..8dd96fc --- /dev/null +++ b/.github/workflows/PR.yml @@ -0,0 +1,43 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Note: "PR" is what will appear on the website as the workflow name, so keep it short. +name: PR + +on: + # Run on Pull Requests + pull_request: + types: [opened, reopened, synchronize] + + # Run once PR is accepted + # Hint: runs on a temp branch with PR and main branch pre-merged. + merge_group: + types: [checks_requested] + + # Run on manual trigger (for testing/debugging) + workflow_dispatch: + +# Minimal permissions by default; individual jobs can elevate as needed. +permissions: + contents: read + +jobs: + test_job: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Test action (default config) + uses: ./ diff --git a/.github/workflows/benchmark-disk-space.yml b/.github/workflows/benchmark-disk-space.yml new file mode 100644 index 0000000..20a2630 --- /dev/null +++ b/.github/workflows/benchmark-disk-space.yml @@ -0,0 +1,522 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Benchmark Disk Space Options + +on: + workflow_dispatch: + inputs: + repeats: + description: "Number of repetitions per combination (1-5)" + required: false + default: "3" + type: choice + options: + - "1" + - "2" + - "3" + - "4" + - "5" + +# The repeats input parameter dynamically controls the repeat array size. +# Using logical operators (&& and ||) to map input values to repeat arrays: +# '1' => [1] +# '2' => [1, 2] +# '3' => [1, 2, 3] (default) +# '4' => [1, 2, 3, 4] +# '5' => [1, 2, 3, 4, 5] +# Note: Arithmetic operators (+, -) aren't supported in GitHub Actions expressions, +# so conditional chaining is used instead. + +jobs: + manual: + name: Manual | ${{ matrix.image }} | ${{ matrix.tool }} | rep ${{ matrix.repeat }} + runs-on: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + image: [ubuntu-22.04, ubuntu-24.04] + tool: [swift, chromium, aws-cli, haskell, miniconda, dotnet, android, gradle, julia, aws-sam-cli, powershell] + repeat: ${{ fromJSON(inputs.repeats == '1' && '[1]' || inputs.repeats == '2' && '[1, 2]' || inputs.repeats == '3' && '[1, 2, 3]' || inputs.repeats == '4' && '[1, 2, 3, 4]' || '[1, 2, 3, 4, 5]') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Measure before + id: before + uses: ./.github/actions/measure-before + + - name: Manual cleanup (${{ matrix.tool }}) + shell: bash + # Removes individual tools from GitHub runners, one per matrix run + # Tools sorted by efficiency (GiB/sec) from fastest to slowest + # See alternatives.md for full classification + run: | + set -ex + TOOL="${{ matrix.tool }}" + echo "Removing $TOOL..." + + case "$TOOL" in + swift) + which swift + sudo rm -rf /usr/share/swift || true + ;; + chromium) + which chromium-browser || which chromium + sudo rm -rf /usr/local/share/chromium || true + ;; + aws-cli) + which aws + sudo rm -rf /usr/local/aws-cli || true + ;; + haskell) + which ghc || which cabal + sudo rm -rf /usr/local/.ghcup || true + sudo rm -rf /opt/ghc || true + ;; + miniconda) + which conda + sudo rm -rf /usr/share/miniconda || true + ;; + dotnet) + which dotnet + sudo rm -rf /usr/share/dotnet || true + ;; + android) + ls /usr/local -al + ls /usr/local/lib -al + sudo rm -rf /usr/local/lib/android || true + ;; + gradle) + which gradle + sudo rm -rf /usr/bin/gradle || true + ;; + julia) + which julia + sudo rm -rf /usr/bin/julia || true + ;; + aws-sam-cli) + which sam + sudo rm -rf /usr/local/aws-sam-cli || true + ;; + powershell) + which pwsh + sudo rm -rf /usr/local/share/powershell || true + ;; + *) + echo "Unknown tool: $TOOL" + exit 1 + ;; + esac + echo "" + echo "Cleanup complete." + + - name: Measure after + uses: ./.github/actions/measure-after + with: + image: ${{ matrix.image }} + option: manual + intensity: ${{ matrix.tool }} + repeat: ${{ matrix.repeat }} + before_root_avail: ${{ steps.before.outputs.root_avail }} + before_ws_avail: ${{ steps.before.outputs.ws_avail }} + start_ts: ${{ steps.before.outputs.start_ts }} + + jlumbroso: + name: jlumbroso | ${{ matrix.image }} | ${{ matrix.intensity }} | rep ${{ matrix.repeat }} + runs-on: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + image: [ubuntu-22.04, ubuntu-24.04] + intensity: [default, minimal, light, standard, max] + repeat: ${{ fromJSON(inputs.repeats == '1' && '[1]' || inputs.repeats == '2' && '[1, 2]' || inputs.repeats == '3' && '[1, 2, 3]' || inputs.repeats == '4' && '[1, 2, 3, 4]' || '[1, 2, 3, 4, 5]') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Measure before + id: before + uses: ./.github/actions/measure-before + + - name: jlumbroso (default) + if: ${{ matrix.intensity == 'default' }} + uses: jlumbroso/free-disk-space@v1.3.1 + + - name: jlumbroso (minimal) + if: ${{ matrix.intensity == 'minimal' }} + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: false + dotnet: true + haskell: false + large-packages: false + docker-images: false + swap-storage: false + + - name: jlumbroso (light) + if: ${{ matrix.intensity == 'light' }} + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: false + large-packages: false + docker-images: false + swap-storage: false + + - name: jlumbroso (standard) + if: ${{ matrix.intensity == 'standard' }} + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: false + + - name: jlumbroso (max) + if: ${{ matrix.intensity == 'max' }} + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: false + swap-storage: true + + - name: Measure after + uses: ./.github/actions/measure-after + with: + image: ${{ matrix.image }} + option: jlumbroso + intensity: ${{ matrix.intensity }} + repeat: ${{ matrix.repeat }} + before_root_avail: ${{ steps.before.outputs.root_avail }} + before_ws_avail: ${{ steps.before.outputs.ws_avail }} + start_ts: ${{ steps.before.outputs.start_ts }} + + enderson: + name: enderson | ${{ matrix.image }} | ${{ matrix.intensity }} | rep ${{ matrix.repeat }} + runs-on: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + image: [ubuntu-22.04, ubuntu-24.04] + intensity: [minimal, light, standard, max] + repeat: ${{ fromJSON(inputs.repeats == '1' && '[1]' || inputs.repeats == '2' && '[1, 2]' || inputs.repeats == '3' && '[1, 2, 3]' || inputs.repeats == '4' && '[1, 2, 3, 4]' || '[1, 2, 3, 4, 5]') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Measure before + id: before + uses: ./.github/actions/measure-before + + - name: enderson (minimal) + if: ${{ matrix.intensity == 'minimal' }} + uses: endersonmenezes/free-disk-space@v3 + with: + remove_android: false + remove_dotnet: true + remove_haskell: false + remove_tool_cache: false + remove_swap: false + remove_packages_one_command: false + rm_cmd: rm + + - name: enderson (light) + if: ${{ matrix.intensity == 'light' }} + uses: endersonmenezes/free-disk-space@v3 + with: + remove_android: true + remove_dotnet: true + remove_haskell: false + remove_tool_cache: false + remove_swap: false + remove_packages_one_command: false + rm_cmd: rm + + - name: enderson (standard) + if: ${{ matrix.intensity == 'standard' }} + uses: endersonmenezes/free-disk-space@v3 + with: + remove_android: true + remove_dotnet: true + remove_haskell: true + remove_tool_cache: false + remove_swap: true + remove_packages_one_command: false + rm_cmd: rm + + - name: enderson (max) + if: ${{ matrix.intensity == 'max' }} + uses: endersonmenezes/free-disk-space@v3 + with: + remove_android: true + remove_dotnet: true + remove_haskell: true + remove_tool_cache: false + remove_swap: true + remove_packages_one_command: true + remove_packages: "azure-cli google-cloud-cli microsoft-edge-stable google-chrome-stable firefox postgresql* temurin-* *llvm* mysql* dotnet-sdk-*" + remove_folders: "/usr/share/swift /usr/share/miniconda /usr/share/az* /usr/local/share/chromium /usr/local/share/powershell /usr/local/julia /usr/local/aws-cli /usr/local/aws-sam-cli /usr/share/gradle" + rm_cmd: rm + + - name: Measure after + uses: ./.github/actions/measure-after + with: + image: ${{ matrix.image }} + option: enderson + intensity: ${{ matrix.intensity }} + repeat: ${{ matrix.repeat }} + before_root_avail: ${{ steps.before.outputs.root_avail }} + before_ws_avail: ${{ steps.before.outputs.ws_avail }} + start_ts: ${{ steps.before.outputs.start_ts }} + + easimon: + name: easimon | ${{ matrix.image }} | ${{ matrix.intensity }} | rep ${{ matrix.repeat }} + runs-on: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + image: [ubuntu-22.04, ubuntu-24.04] + intensity: [default, light, standard, max] + repeat: ${{ fromJSON(inputs.repeats == '1' && '[1]' || inputs.repeats == '2' && '[1, 2]' || inputs.repeats == '3' && '[1, 2, 3]' || inputs.repeats == '4' && '[1, 2, 3, 4]' || '[1, 2, 3, 4, 5]') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Measure before + id: before + uses: ./.github/actions/measure-before + + - name: easimon (default) + if: ${{ matrix.intensity == 'default' }} + uses: easimon/maximize-build-space@v10 + + - name: easimon (light) + if: ${{ matrix.intensity == 'light' }} + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: "1024" + temp-reserve-mb: "100" + swap-size-mb: "4096" + overprovision-lvm: false + remove-dotnet: false + remove-android: false + remove-haskell: false + remove-codeql: false + remove-docker-images: false + + - name: easimon (standard) + if: ${{ matrix.intensity == 'standard' }} + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: "1024" + temp-reserve-mb: "100" + swap-size-mb: "4096" + overprovision-lvm: false + remove-dotnet: true + remove-android: true + remove-haskell: false + remove-codeql: false + remove-docker-images: false + + - name: easimon (max) + if: ${{ matrix.intensity == 'max' }} + uses: easimon/maximize-build-space@v10 + with: + root-reserve-mb: "512" + temp-reserve-mb: "50" + swap-size-mb: "1024" + overprovision-lvm: true + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: false + + - name: Measure space after + id: after + shell: bash + run: | + set -euo pipefail + ROOT_AVAIL=$(df --output=avail -B1 / | tail -1) + WS_PATH=${GITHUB_WORKSPACE:-$PWD} + WS_AVAIL=$(df --output=avail -B1 "$WS_PATH" | tail -1) + echo "root_avail=$ROOT_AVAIL" >> "$GITHUB_OUTPUT" + echo "ws_path=$WS_PATH" >> "$GITHUB_OUTPUT" + echo "ws_avail=$WS_AVAIL" >> "$GITHUB_OUTPUT" + + - name: Compute metrics CSV + shell: bash + run: | + set -euo pipefail + END_TS=$(date +%s) + START_TS="${{ steps.before.outputs.start_ts }}" + DURATION=$((END_TS - START_TS)) + FREED_ROOT=$(( ${{ steps.after.outputs.root_avail }} - ${{ steps.before.outputs.root_avail }} )) + FREED_WS=$(( ${{ steps.after.outputs.ws_avail }} - ${{ steps.before.outputs.ws_avail }} )) + + # Validate that cleanup freed at least 0.1 GB (107374182 bytes) + MIN_THRESHOLD=107374182 + if [[ $FREED_WS -lt $MIN_THRESHOLD && $FREED_ROOT -lt $MIN_THRESHOLD ]]; then + echo "❌ Error: Cleanup freed less than 0.1 GB (freed_ws=$(echo "scale=2; $FREED_WS / 1073741824" | bc) GiB, freed_root=$(echo "scale=2; $FREED_ROOT / 1073741824" | bc) GiB)" + echo "This indicates the cleanup configuration is not working properly." + exit 1 + fi + + mkdir -p metrics + OUT=metrics/metrics.csv + if [[ ! -f "$OUT" ]]; then + echo "image,option,intensity,repeat,before_root,after_root,freed_root,before_ws,after_ws,freed_ws,duration_seconds" > "$OUT" + fi + echo "${{ matrix.image }},easimon,${{ matrix.intensity }},${{ matrix.repeat }},${{ steps.before.outputs.root_avail }},${{ steps.after.outputs.root_avail }},$FREED_ROOT,${{ steps.before.outputs.ws_avail }},${{ steps.after.outputs.ws_avail }},$FREED_WS,$DURATION" >> "$OUT" + + - name: Upload metrics artifact + uses: actions/upload-artifact@v4 + with: + name: metrics-${{ matrix.image }}-easimon-${{ matrix.intensity }}-${{ matrix.repeat }} + path: metrics/metrics.csv + + adityagarg: + name: adityagarg | ${{ matrix.image }} | ${{ matrix.intensity }} | rep ${{ matrix.repeat }} + runs-on: ${{ matrix.image }} + strategy: + fail-fast: false + matrix: + image: [ubuntu-22.04, ubuntu-24.04] + intensity: [minimal, light, standard, max] + repeat: ${{ fromJSON(inputs.repeats == '1' && '[1]' || inputs.repeats == '2' && '[1, 2]' || inputs.repeats == '3' && '[1, 2, 3]' || inputs.repeats == '4' && '[1, 2, 3, 4]' || '[1, 2, 3, 4, 5]') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Measure before + id: before + uses: ./.github/actions/measure-before + + - name: AdityaGarg8 (minimal) + if: ${{ matrix.intensity == 'minimal' }} + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-android: false + remove-haskell: false + remove-codeql: false + remove-docker-images: false + remove-large-packages: false + remove-cached-tools: false + remove-swapfile: false + verbose: false + + - name: AdityaGarg8 (light) + if: ${{ matrix.intensity == 'light' }} + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-android: true + remove-haskell: false + remove-codeql: false + remove-docker-images: false + remove-large-packages: true + remove-cached-tools: false + remove-swapfile: false + verbose: false + + - name: adityagarg (standard) + if: ${{ matrix.intensity == 'standard' }} + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: false + remove-large-packages: true + remove-cached-tools: false + remove-swapfile: false + verbose: false + + - name: adityagarg (max) + if: ${{ matrix.intensity == 'max' }} + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: false + remove-large-packages: true + remove-cached-tools: false + remove-swapfile: true + verbose: false + + - name: Measure after + uses: ./.github/actions/measure-after + with: + image: ${{ matrix.image }} + option: adityagarg + intensity: ${{ matrix.intensity }} + repeat: ${{ matrix.repeat }} + before_root_avail: ${{ steps.before.outputs.root_avail }} + before_ws_avail: ${{ steps.before.outputs.ws_avail }} + start_ts: ${{ steps.before.outputs.start_ts }} + + aggregate: + name: Aggregate results + runs-on: ubuntu-24.04 + needs: [manual, jlumbroso, enderson, easimon, adityagarg] + if: always() + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download all metrics + uses: actions/download-artifact@v4 + with: + path: all-metrics + + - name: Combine CSVs + id: combine + shell: bash + run: | + set -euo pipefail + OUT=combined.csv + echo "image,option,intensity,repeat,before_root,after_root,freed_root,before_ws,after_ws,freed_ws,duration_seconds" > "$OUT" + find all-metrics -name "metrics.csv" -print0 | xargs -0 -I{} sh -c 'tail -n +2 "{}"' >> "$OUT" + echo "combined_csv_path=$OUT" >> "$GITHUB_OUTPUT" + wc -l "$OUT" || true + + - name: Compute averages and summary + shell: bash + run: | + python3 scripts/aggregate.py combined.csv summary.md + { + echo '## Raw Combined Metrics (CSV)' + echo + echo 'Download the combined.csv artifact for details.' + } >> "$GITHUB_STEP_SUMMARY" + cat summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Upload combined CSV + uses: actions/upload-artifact@v4 + with: + name: combined-metrics + path: combined.csv diff --git a/.github/workflows/v1-test.yml b/.github/workflows/v1-test.yml new file mode 100644 index 0000000..e0f9f3b --- /dev/null +++ b/.github/workflows/v1-test.yml @@ -0,0 +1,33 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: v1-test + +on: + # Run on manual trigger (for testing/debugging) + workflow_dispatch: + +# Minimal permissions by default; individual jobs can elevate as needed. +permissions: + contents: read + +jobs: + test_job: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Test action (default config) + uses: eclipse-score/more-disk-space@v1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..d89a39c --- /dev/null +++ b/NOTICE @@ -0,0 +1,32 @@ +# Notices for Eclipse Safe Open Vehicle Core + +This content is produced and maintained by the Eclipse Safe Open Vehicle Core project. + + * Project home: https://projects.eclipse.org/projects/automotive.score + +## Trademarks + +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Apache License Version 2.0 which is available at +https://www.apache.org/licenses/LICENSE-2.0. + +SPDX-License-Identifier: Apache-2.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. \ No newline at end of file diff --git a/README.md b/README.md index ae615ce..a409bcf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ -# more-disk-space -GitHub Action to make more disk space available in Ubuntu based GitHub Actions runners + + +# More-Disk-Space + +**Trivial to use and fast disk cleanup** + +## Safety + +This action is **designed exclusively for GitHub-hosted Ubuntu runners**. If run on other environments (local machines, self-hosted runners, or non-Linux systems), it will: +- ⏭️ **Skip all deletions** (no-op mode) +- ✅ **Exit successfully** without errors + +This ensures safe testing and prevents accidental data loss. + +*⚠️ Before using any of these cleanup actions, understand what gets deleted ⚠️* + +## Quick Start + +**Default (recommended):** 7.6 GiB freed in ~4 seconds +```yaml +- uses: eclipse-score/more-disk-space@v1 + with: + level: 2 # Default +``` + +## Overview + +| Level | Freed Space | Duration | Efficiency | What Gets Deleted | +|------:|------------|----------|---------|-------------------| +| 1 | 3.7 GiB | ~1s | 3.7 GiB/sec | swift, chromium | +| 2 | 7.6 GiB | ~4s | 1.9 GiB/sec | +aws-cli, haskell | +| 3 | 12.3 GiB | ~16s | 0.8 GiB/sec | +miniconda, dotnet | +| 4 | 22.2 GiB | ~40s | 0.6 GiB/sec | +android | + + +## Why A Custom Action? + +* Existing cleanup actions are **4–5× slower**: + - **APT overhead**: Package manager queries, dependency checks, validation + - **Extra thoroughness**: Scanning for edge cases that more-disk-space simply ignores +* Trade-Offs: + - **Different models**: LVM expansion trades root space (see [alternatives](docs/alternatives.md)) +- Usability + * **what gets deleted**: Focused on known large items safe for S-CORE workflows + * **simple**: Sorted list by deletion speed, not a list of deletable items + +## Detailed Breakdown + +### Level 1 · Minimal (3.7 GiB in ~1s) + +**Removes:** +- Swift compiler (~1.5 GiB) +- Chromium browser (~0.4 GiB) + +**Impact:** Safe unless building iOS/macOS apps or testing web UIs locally. + +--- + +### Level 2 · Light — **DEFAULT** (7.6 GiB in ~4s) + +**Adds to Level 1:** +- AWS CLI v2 (~0.75 GiB) +- Haskell compiler & ghcup (~1.25 GiB) + +**Impact:** Safe unless using Haskell or AWS CLI directly (most workflows use SDKs or pre-cached tools). + +--- + +### Level 3 · Standard (12.3 GiB in ~16s) + +**Adds to Level 2:** +- Miniconda Python environment (~1.5 GiB) +- .NET runtime & SDKs (~1.5 GiB) + +**Impact:** Safe unless using Conda Python or building C#/F#/VB.NET projects. + +--- + +### Level 4 · Max (22.2 GiB in ~40s) + +**Adds to Level 3:** +- Android SDK (~5.5 GiB) — **slowest item, the bottleneck** + +**Impact:** Safe unless building Android/Flutter/React Native apps. + +--- + +## Alternative Options + +Why those 4 levels? + +Need more space or different trade-offs? + +See [docs/alternatives.md](docs/alternatives.md). \ No newline at end of file diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..be50fc5 --- /dev/null +++ b/action.yml @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: More Disk Space +description: Fast disk cleanup for GitHub's Ubuntu runners (22.04 & 24.04). Frees 3.7–22.2 GiB in 1–40 seconds. + +branding: + icon: trash-2 + color: blue + +inputs: + level: + description: | + Cleanup level (1-4): + - 1: 3.7 GiB in ~1s (swift, chromium) + - 2: 7.6 GiB in ~4s (+ aws-cli, haskell) [DEFAULT] + - 3: 12.3 GiB in ~16s (+ miniconda, dotnet) + - 4: 22.2 GiB in ~40s (+ android) + required: false + default: '2' + +runs: + using: node24 + main: 'index.js' + post: 'post.js' + post-if: 'always()' diff --git a/benchmark.md b/benchmark.md new file mode 100644 index 0000000..e58f34a --- /dev/null +++ b/benchmark.md @@ -0,0 +1,60 @@ + + +# More Disk Space – Benchmark Workflow + +This repository provides an on-demand GitHub Actions workflow to evaluate different strategies for freeing disk space on GitHub-hosted Ubuntu runners. + +## Architecture + +**Workflow**: `.github/workflows/benchmark-disk-space.yml` +- Triggers on `workflow_dispatch` with optional `repeats` parameter (1-5, default 3) +- 5 benchmark jobs (manual, jlumbroso, enderson, easimon, adityagarg) × 2 images × varying intensities × repeats +- Total: ~96-200 jobs depending on repeat count + +**Composite Actions**: +- `.github/actions/measure-before/action.yml`: Captures baseline disk state, outputs start timestamp +- `.github/actions/measure-after/action.yml`: Measures final state, computes freed space and duration (except easimon which inlines the logic to survive workspace remount) + +**Aggregation**: +- `scripts/aggregate.py`: Reads per-run CSVs, groups by (image, option, intensity), computes averages, generates markdown summary with Mermaid chart + +## How It Works + +1. **Before Cleanup**: Measure available bytes on `/` and workspace +2. **Action-Specific Cleanup**: Run one of the 5 cleanup approaches at selected intensity +3. **After Cleanup**: Measure again, compute freed bytes and duration +4. **Metrics**: Write CSV row with all measurements +5. **Aggregation**: Combine all CSVs, average by group, generate summary + +## Intensity Levels + +Each action supports 3-4 intensity levels: +- **minimal**: Smallest cleanup (tool-cache only for most) +- **light**: Moderate cleanup (add dotnet, android, etc.) +- **standard**: Aggressive (add haskell, swap, etc.) +- **max**: Full cleanup (includes packages, folders, swap, docker prune) + +## Run It On Demand + +Trigger the workflow manually from the Actions tab: +1. Open “Benchmark Disk Space Options”. +2. Click “Run workflow”. +3. The workflow will run a matrix over runners, options, intensities, and 3 repeats. + +## Outputs + + - Job Summary: A table with averages per image/option/intensity. + - Artifacts: + - Per-run CSV: `metrics.csv` with columns: image, option, intensity, repeat, before_root, after_root, freed_root, before_ws, after_ws, freed_ws, duration_seconds. + - Combined CSV: `combined.csv` aggregating all runs for downstream analysis. diff --git a/docs/alternatives.md b/docs/alternatives.md new file mode 100644 index 0000000..de6c2c5 --- /dev/null +++ b/docs/alternatives.md @@ -0,0 +1,302 @@ + + +# Alternatives + +[← Back to main README](../README.md) + +This document explains the design choices behind **more-disk-space**. + +--- + +## Alternative Actions Were Benchmarked + +5 different cleanup approaches across multiple intensity levels on actual GitHub +runners (ubuntu-22.04 & ubuntu-24.04) were benchmarked. For each of them a +several configurations were selected. + +See [benchmark workflow](../.github/workflows/benchmark-disk-space.yml) for details on the used parameters. + +Let's group them by disk space cleaned: + +### Minimal Cleanup (~4 GiB) + +| Action | Space Freed | Duration (range) | GiB/sec (range) | +| ------------------------------------------------------------------------------------------------------------ | ----------- | ---------------- | --------------- | +| **more-disk-space level 1** | 3.7 GiB | 1s | 3.7 | +| **more-disk-space level 2** | 7.6 GiB | 3-4s | 1.90-2.53 | +| [jlumbroso](https://github.com/marketplace/actions/free-disk-space-ubuntu) | 4.0 GiB | 6-8s | 0.50-0.66 | +| [enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners) | 4.0 GiB | 7-13s | 0.31-0.57 | +| [adityagarg](https://github.com/marketplace/actions/maximize-build-disk-space-only-remove-unwanted-software) | 4.0 GiB | 4-6s | 0.66-0.99 | + +**Result:** All tools achieve similar cleanup. more-disk-space is **4-7× +faster**. And even level 2 is faster, freeing double the space in just 3-4 +seconds. + +### Light Cleanup (~13 GiB) + +| Action | Space Freed | Duration (range) | GiB/sec (range) | +| ------------------------------------------------------------------------------------------------------------ | ----------- | ---------------- | --------------- | +| **more-disk-space level 3** | 12.3 GiB | 15-16s | 0.77-0.82 | +| [jlumbroso](https://github.com/marketplace/actions/free-disk-space-ubuntu) | 13.8 GiB | 30-50s | 0.28-0.46 | +| [enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners) | 13.8 GiB | 34-44s | 0.31-0.41 | +| [adityagarg](https://github.com/marketplace/actions/maximize-build-disk-space-only-remove-unwanted-software) | 19.5 GiB | 151-187s | 0.10-0.13 | + +**Result:** more-disk-space cleans slightly less than the others, but is **2-6× +faster** at this level. + +### Standard Cleanup (~20 GiB) + +| Action | Space Freed | Duration (range) | GiB/sec (range) | +| ------------------------------------------------------------------------------------------------------------ | ----------- | ---------------- | --------------- | +| **more-disk-space level 4** | 22.2 GiB | 30-49s | 0.45-0.74 | +| [jlumbroso](https://github.com/marketplace/actions/free-disk-space-ubuntu) | 22.4 GiB | 138-177s | 0.13-0.16 | +| [enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners) | 17.5 GiB | 38-46s | 0.38-0.46 | +| [enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners) (max) | 31.4 GiB | 158-190s | 0.17-0.20 | +| [adityagarg](https://github.com/marketplace/actions/maximize-build-disk-space-only-remove-unwanted-software) | 24.7 GiB | 142-203s | 0.12-0.17 | + +**Result:** Others may clean more (up to 31 GiB), but they are **3-5× slower** +than more-disk-space level 4. + +### Massive Workspace Expansion (75+ GiB) + +| Action | Space Freed | Duration (range) | GiB/sec (range) | +| ------- | ----------- | ---------------- | --------------- | +| easimon | 76.0 GiB | 34-38s | 2.00-2.24 | +| easimon | 81.0 GiB | 34-41s | 1.98-2.39 | + +**Result:** easimon achieves massive workspace sizes by consuming root space via +LVM. It is fast but leaves minimal root space (~1 GiB). + +### Summary: Why more-disk-space Wins on Speed + +**Direct `rm -rf` deletion:** +- No APT overhead (dependency checking, validation, index updates) +- No package scanning or filtering operations +- Hardcoded paths on known GitHub runner images + +**Result:** 4-5× faster than existing actions at equivalent cleanup levels. + +--- + +## Why 4 Levels? (Size/Speed Trade-off Analysis) + +Each level adds items with similar **deletion efficiency** (GiB/sec). This +creates natural breakpoints: + +### Level Progression + +| Level | Items Added | Total Space | Duration | New GiB/sec | Cumulative | +| ----- | ------------------ | ----------- | ---------- | --------------------- | -------------- | +| 1 | swift, chromium | 3.7 GiB | ~1s | 3.7 GiB/sec | ⚡ Ultra-fast | +| 2 | +aws-cli, haskell | +3.9 GiB | ~4s total | 1.3 GiB/sec per added | ⚡ Still fast | +| 3 | +miniconda, dotnet | +4.7 GiB | ~16s total | 0.4 GiB/sec per added | ⚠️ Slower items | +| 4 | +android | +9.9 GiB | ~40s total | 0.4 GiB/sec per added | 🐌 Bottleneck | + +### Decision Logic + +**Why not more levels?** +- Adding **Level 5-x**: The next level would be apt-base, but it only cleans 2-9 + GiB more but takes 140+ seconds longer. Not worth it. +- Each marginal GiB becomes increasingly expensive in time + +**Why not fewer levels?** +- **Level 1 alone**: 3.7 GiB is too modest for many workflows +- **Level 2 as default**: Sweet spot—7.6 GiB in ~4s catches 80% of space issues +- **Levels 3-4**: Options for workflows that need more aggressive cleanup + +**The breakpoint:** Android SDK at Level 4 is the final practical item. It's +slow (51 seconds for 5.5 GiB) but adds massive space. Beyond that, APT tools +become necessary. + +--- + +## Excluded: Extreme Runtime (4-5× Slower) + +APT-based tools sacrifice speed for comprehensiveness. Not worth the trade-off +for GitHub Actions. + +**Performance data:** +- **[adityagarg](https://github.com/marketplace/actions/maximize-build-disk-space-only-remove-unwanted-software) · standard**: 24.7 GiB in ~177s (0.14 GiB/sec) + - Only **2 GiB more** than more-disk-space Level 4 + - Takes **135+ seconds longer** (3.4× slower) +- **[enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners) · max**: 31.5 GiB in ~183s (0.17 GiB/sec) + - Only **9 GiB more** than more-disk-space Level 4 + - Takes **140+ seconds longer** (3.5× slower) +- **[jlumbroso](https://github.com/marketplace/actions/free-disk-space-ubuntu) · max**: ~22 GiB in ~165s (0.13 GiB/sec) + - Similar cleanup to more-disk-space Level 4 + - Takes **4× longer** (125+ seconds extra) + +**Why they're slow:** +1. **APT overhead**: `apt remove` checks dependencies, validates packages, + updates indices +2. **Package queries**: Scanning installed packages, filtering by patterns +3. **Cleanup operations**: `apt autoremove`, `apt clean`, index updates +4. **Safety checks**: More thorough validation before deletion + +**Time cost breakdown:** +- more-disk-space Level 4: 40 seconds = actual deletion +- APT tools: 177 seconds = ~137 seconds of APT overhead + deletion + +**When to consider:** +- You need **absolute maximum cleanup** (30+ GiB) **AND** time isn't critical +- Your build already takes forever, so adding some minutes is acceptable + +**Recommendation:** For most GitHub Actions workflows, the time penalty isn't +worth it. The marginal gains don't justify 135+ extra seconds. Use +more-disk-space Level 3-4 instead. + +--- + +## Excluded: Different Trade-off Model + +### easimon/maximize-build-space + +**The different approach:** Instead of deleting files, it expands workspace by +consuming root space via LVM. + +**Performance data:** +- **easimon 1**: 76 GiB workspace in ~36s (2.11 GiB/sec) +- **easimon 2**: 81 GiB workspace in ~41s (1.98 GiB/sec) + +**How it works:** Instead of just deleting files, it: +1. Removes some large packages (dotnet, android) +2. Creates an LVM volume by **consuming root partition space** +3. Remounts `/home/runner/work` (workspace) on the new LVM volume +4. Result: Huge workspace (75-80 GiB) but **minimal root space (~1 GiB)** + +**Trade-offs:** +- ✅ **Fast**: 2+ GiB/sec efficiency (faster than direct deletion!) +- ✅ **Massive workspace**: 75-80 GiB available for builds +- ⚠️ **Root space sacrifice**: Only ~1 GiB left on root partition +- ⚠️ **Risk**: Multi-step workflows that write to root may fail +- ⚠️ **Complexity**: Requires understanding LVM partitioning model + +**When to consider:** +- **Single-step builds** that need massive workspace: + - Compiling LLVM or kernel (10+ GiB artifacts) + - Building very large containers (30+ GiB layers) + - Machine learning model training (large datasets) +- You understand LVM and accept the root space trade-off +- Your workflow doesn't install APT packages or write to `/tmp` after cleanup + +**When NOT to use:** +- Multi-step workflows (checkout → build → test → deploy) +- Workflows that install packages mid-build (`apt install` needs root space) +- Workflows that write logs/artifacts to `/tmp` or other root paths +- You're not sure what LVM is or why it matters + +**Recommendation:** easimon is excellent for specific use cases (single-step +massive builds), but most workflows don't need 75 GiB workspace. Use +more-disk-space Level 3-4 for safe, predictable cleanup instead. + +--- + +## Why more-disk-space is Different (Design Philosophy) + +### Design Philosophy + +**Goal:** Maximum space/time efficiency for typical GitHub Actions workflows + +**Approach:** +1. **Benchmark-driven**: Measured individual package deletion times on actual + runners +2. **Efficiency-sorted**: Remove fast items first (swift: 6.5 GiB/sec) before + slow items (android: 0.1 GiB/sec) +3. **Direct deletion**: `sudo rm -rf` on known paths—no APT overhead +4. **Predictable**: Same paths on Ubuntu 22.04 and 24.04 + +**Result:** 5-10× faster than size-sorted or APT-based approaches + +**Sacrifices:** +- Less comprehensive: Only known large items safe for S-CORE workflows +- Hardcoded paths: Tied to specific GitHub runner images +- No package management: Ignores dependencies, may leave orphaned packages +- Limited customization: 4 fixed levels, not granular control +- No cleanup: directories are simply deleted, tools remain "installed" + + +## Risk Assessment: What to Delete + +Complete inventory of deletable items from GitHub runners, categorized by risk +level. + +### 🟢 Safe to Delete (Included in more-disk-space) +| Level | Path(s) | Size | Description | +| ----- | ------------------------------- | ----------- | ------------------------ | +| 1 | `/usr/share/swift` | 1-2 GiB | Swift compiler | +| 1 | `/usr/local/share/chromium` | 0.3-0.5 GiB | Chromium browser | +| 2 | `/usr/local/aws-cli` | 0.5-1 GiB | AWS CLI v2 | +| 2 | `/opt/ghc`, `/usr/local/.ghcup` | 1-1.5 GiB | Haskell compiler | +| 3 | `/usr/share/dotnet` | 1-2 GiB | .NET runtime & SDKs | +| 3 | `/usr/share/miniconda` | 1-2 GiB | Conda Python environment | +| 4 | `/usr/local/lib/android` | 5-6 GiB | Android SDK | + +**Also safe but not in more-disk-space** (due to APT overhead or marginal +gains, while increasing deletion time): + +| Path/Package | Size | Why Not Included | +| ---------------------------------------- | ---------------- | -------------------------------- | +| `/usr/share/gradle` | ~0 GiB | Small, APT overhead not worth it | +| `dotnet-sdk-*` (APT) | 0.5-1 GiB | APT overhead | +| `/usr/local/julia` | ~0 GiB | Uncommon, small | +| `temurin-*` (APT) | 0.5-1 GiB | APT overhead | +| `/usr/local/aws-sam-cli` | 0.3-0.5 GiB | Marginal gain | +| `/usr/local/share/powershell` | 0.2-0.3 GiB | Marginal gain | +| Browser packages (firefox, chrome, edge) | 0.5-1 GiB | APT overhead | +| `postgresql-*`, `mysql-*` (APT) | 0.3-0.5 GiB each | APT overhead | +| `google-cloud-cli` (APT) | 0.3-0.5 GiB | APT overhead | +| `*-llvm-*` (APT) | 0.5-2 GiB | APT overhead | + +### 🔴 Do NOT Delete (Critical Caches) + +These intentionally **NOT deleted** by more-disk-space. Removing them forces +re-downloads/rebuilds, adding 5-15 minutes to workflows. + +| Path/Package | Size | Impact if Deleted | +| ----------------------------- | ----------- | ----------------------------------------------------------------------------- | +| `/opt/hostedtoolcache` | 6-7 GiB | Node, Python, Ruby, Go, Java caches — Forces re-download of language runtimes | +| Docker layer cache | 3-5 GiB | Forces full image rebuilds (~5-15 min slowdown) | +| `/var/cache/apt` | 1-2 GiB | APT package cache — Forces re-download on `apt install` | +| `/usr/local/lib/node_modules` | 0.5-1 GiB | Global npm packages — Forces `npm install -g` | +| Pip/Python cache | 0.5-1 GiB | Python package cache — Forces PyPI re-downloads | +| Gem cache (Ruby) | 0.3-0.5 GiB | Forces `gem install` | +| Go module cache | 0.3-0.5 GiB | `/root/go/pkg/mod` — Forces re-download | +| Cargo cache (Rust) | 0.5-1 GiB | Forces crate re-downloads | +| Maven/Gradle caches | 0.5-1 GiB | Java dependency re-downloads | + +**Total potential savings:** 15-20 GiB +**Cost:** 5-15 minutes added to build time + +**Verdict:** Never worth it. More-disk-space preserves these intentionally. + +--- + +## Benchmark Methodology + +All performance data comes from +[benchmark-disk-space.yml](../.github/workflows/benchmark-disk-space.yml): + +- **Runners tested:** ubuntu-22.04, ubuntu-24.04 (GitHub-hosted free tier) +- **Actions benchmarked:** manual, [jlumbroso](https://github.com/marketplace/actions/free-disk-space-ubuntu), [enderson](https://github.com/marketplace/actions/free-disk-space-ubuntu-runners), easimon, [adityagarg](https://github.com/marketplace/actions/maximize-build-disk-space-only-remove-unwanted-software) +- **Measurements:** Wall-clock time, `df` space readings, individual package + deletion times +- **Repeats:** 5 runs per configuration for statistical validity +- **Validation:** All cleanup actions verified to free ≥0.1 GiB + +The revolutionary insight: **Android SDK takes 51 seconds to delete but swift +takes 0.23 seconds.** By deleting fast items first, we achieve 5-10× better +performance at small cleanup levels. + +[← Back to main README](../README.md) + diff --git a/index.js b/index.js new file mode 100644 index 0000000..60996a4 --- /dev/null +++ b/index.js @@ -0,0 +1,167 @@ +// Copyright (c) 2026 Contributors to the Eclipse Foundation + +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. + +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 + +// SPDX-License-Identifier: Apache-2.0 + +const { execSync, execFileSync } = require('child_process'); +const fs = require('fs'); + + +// Detect the runner environment from RUNNER_ENVIRONMENT variable +// Returns 'github-hosted' or 'self-hosted' (or 'unknown' if not set) +function runnerEnvironment() { + return process.env.RUNNER_ENVIRONMENT || 'unknown'; +} + +// Check if running on a GitHub-hosted runner (as opposed to self-hosted) +function isGithubHosted() { + return runnerEnvironment() === 'github-hosted'; +} + +// Get the operating system platform (returns 'linux', 'win32', 'darwin', etc.) +function platform() { + return process.platform || 'unknown'; +} + +// Check if running on Linux +function isLinux() { + return platform() === 'linux'; +} + +// Persist data to the post step via GITHUB_STATE file +// The post step (post.js) will read these values to report results +function persistForPostStep(stateObject) { + if (!process.env.GITHUB_STATE) throw new Error('GITHUB_STATE not set'); + + for (const [key, value] of Object.entries(stateObject)) { + fs.appendFileSync(process.env.GITHUB_STATE, `${key}=${value}\n`); + } +} + +// Recursively remove a directory using 'rm -rf' +// Continues silently if removal fails (e.g., permission denied) +function rmRf(path) { + try { + execFileSync('sudo', ['rm', '-rf', path], { stdio: 'inherit' }); + } catch (error) { + // Continue even if removal fails + console.error(`Warning: Failed to remove ${path}`); + } +} + +// Execute cleanup for the specified level +// Level determines which directories to remove (1-4 progressively more) +function performCleanup(levelNum) { + // Level 1: swift, chromium (fastest items, 4-6 GiB/sec) + if (levelNum >= 1) { + console.log('Removing swift...'); + rmRf('/usr/share/swift'); + console.log('Removing chromium...'); + rmRf('/usr/local/share/chromium'); + } + + // Level 2: + aws-cli, haskell (fast items, 0.5-0.6 GiB/sec) + if (levelNum >= 2) { + console.log('Removing aws-cli...'); + rmRf('/usr/local/aws-cli'); + console.log('Removing haskell...'); + rmRf('/usr/local/.ghcup'); + rmRf('/opt/ghc'); + } + + // Level 3: + miniconda, dotnet (medium items, 0.2 GiB/sec) + if (levelNum >= 3) { + console.log('Removing miniconda...'); + rmRf('/usr/share/miniconda'); + console.log('Removing dotnet...'); + rmRf('/usr/share/dotnet'); + } + + // Level 4: + android (bottleneck, 0.1 GiB/sec) + if (levelNum >= 4) { + console.log('Removing android...'); + rmRf('/usr/local/lib/android'); + } +} + +function getAvailableSpaceGiB() { + const out = execFileSync( + 'df', + ['--output=avail', '-B1G', '/'], + { encoding: 'utf8' } + ); + + return Number(out.trim().split('\n')[1]); +} + +function parseLevel() { + const level = process.env.INPUT_LEVEL || '2'; + + // Validate level + if (!/^[1-4]$/.test(level)) { + console.error(`❌ Error: Invalid level '${level}'. Must be 1, 2, 3, or 4.`); + process.exit(1); + } + + return parseInt(level); +} + +async function run() { + try { + const level = parseLevel(); + const githubHosted = isGithubHosted(); + const supportedPlatform = isLinux(); + + persistForPostStep({ level, githubHosted, supportedPlatform }); + + console.log(`🗑️ More Disk Space - Level ${level} cleanup`); + console.log(''); + + // Check if running on Linux + if (!supportedPlatform) { + console.log(`ℹ️ Unsupported platform: ${platform()}`); + console.log('⏭️ This action only runs on Linux'); + console.log(''); + return; + } + + + // Measure before + const before = getAvailableSpaceGiB(); + console.log(`Available space before: ${before} GiB`); + console.log(''); + + // Skip cleanup if not GitHub-hosted + if (githubHosted) { + console.log('✅ Running on GitHub-hosted runner'); + console.log(''); + + // Perform cleanup + performCleanup(level); + + // Measure after + const after = getAvailableSpaceGiB(); + const freed = after - before; + console.log(''); + console.log('✅ Cleanup complete!'); + console.log(`Available space after: ${after} GiB`); + console.log(`Space freed: ${freed} GiB`); + } + else { + console.log('ℹ️ Not running on GitHub-hosted runner - cleanup skipped'); + console.log(''); + } + + } catch (error) { + console.error(error.message); + process.exit(1); + } +} + +run(); diff --git a/post.js b/post.js new file mode 100644 index 0000000..8f7f30f --- /dev/null +++ b/post.js @@ -0,0 +1,104 @@ +// Copyright (c) 2026 Contributors to the Eclipse Foundation + +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. + +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 + +// SPDX-License-Identifier: Apache-2.0 + +const { execFileSync } = require('child_process'); + +// Get available disk space in GiB (gigabytes) +function getAvailableSpaceGiB() { + const out = execFileSync( + 'df', + ['--output=avail', '-B1G', '/'], + { encoding: 'utf8' } + ); + + return Number(out.trim().split('\n')[1]); +} + + +// Provide helpful suggestions based on available disk space +// Suggests increasing cleanup level if space is low +// Suggests reducing cleanup level if plenty of space remains +function reportSuggestions(availableGiB, level) { + // Suggest next level if running low + if (availableGiB < 5) { + if (level === 4) { + console.log('⚠️ Warning: Less than 5 GiB remaining and already at max level (4).'); + console.log(' Consider using alternative actions (see docs/alternatives.md)'); + } else { + console.log('⚠️ Warning: Less than 5 GiB remaining.'); + console.log(` Consider increasing to level ${level + 1} for your next run.`); + } + } else if (availableGiB < 10) { + console.log(`✅ Moderate buffer remaining (${availableGiB} GiB)`); + } else { + // 10+ GiB remaining - suggest lower level + console.log(`✅ Good buffer remaining (${availableGiB} GiB)`); + if (level === 1) { + console.log('💡 Tip: You have plenty of space remaining.'); + console.log(' You may not need this action for your workflow.'); + } else { + console.log('💡 Tip: You have plenty of space remaining.'); + console.log(` Consider reducing to level ${level - 1} to speed up your workflow.`); + } + } +} + + +function getState(name) { + return process.env[`STATE_${name}`]; +} + +function getStateBool(name) { + return getState(name) === 'true'; +} + +function getStateInt(name, fallback) { + const v = Number(getState(name)); + return Number.isFinite(v) ? v : fallback; +} + + +async function cleanup() { + try { + console.log(''); + console.log('📊 Final disk space report'); + console.log('=========================='); + + // Step 1: Read state information that was persisted by the main step (index.js) + const level = getStateInt('level', 2); + const githubHosted = getStateBool('githubHosted'); + const supportedPlatform = getStateBool('supportedPlatform'); + + // Step 2: If platform is unsupported, skip disk space reporting + if (!supportedPlatform) { + console.log(`⏭️ Unsupported platform; skipping disk space report`); + console.log(''); + return; + } + + // Step 3: Report current disk space + const availableGiB = getAvailableSpaceGiB(); + console.log(`Available space: ${availableGiB} GiB`); + console.log(''); + + // Step 4: Provide suggestions for future runs + if (githubHosted) { + reportSuggestions(availableGiB, level); + } + + } catch (error) { + console.error('Warning: Failed to report final disk space'); + console.error(error.message); + // Don't fail the job on cleanup errors + } +} + +cleanup(); diff --git a/scripts/aggregate.py b/scripts/aggregate.py new file mode 100644 index 0000000..1165d2c --- /dev/null +++ b/scripts/aggregate.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Aggregate benchmark metrics and generate summary with Mermaid chart.""" + +import csv +import sys +from collections import defaultdict + +if len(sys.argv) < 3: + print("Usage: aggregate.py ") + sys.exit(1) + +combined_path = sys.argv[1] +summary_path = sys.argv[2] + +groups: dict[tuple[str, str, str], dict[str, int]] = defaultdict( + lambda: { + "count": 0, + "sum_freed_root": 0, + "sum_freed_ws": 0, + "sum_dur": 0, + "sum_after_root": 0, + "sum_after_ws": 0, + } +) + +rows: list[dict[str, str]] = [] +with open(combined_path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + rows = list(reader) + +# Group results by (image, option, intensity) combination and sum metrics across repeats +for r in rows: + key = (r["image"], r["option"], r["intensity"]) + g = groups[key] + g["count"] += 1 + g["sum_freed_root"] += int(r["freed_root"]) + g["sum_freed_ws"] += int(r["freed_ws"]) + g["sum_dur"] += int(r["duration_seconds"]) + g["sum_after_root"] += int(r["after_root"]) + g["sum_after_ws"] += int(r["after_ws"]) + + +def fmt_gib(bytes_: int) -> str: + """Format bytes as GiB with 2 decimal places.""" + return f"{bytes_ / (1024**3):.2f} GiB" + + +lines: list[str] = [] +lines.append("## Disk Space Benchmark Summary\n") +lines.append("\n") +lines.append("### Understanding the Metrics\n") +lines.append("\n") +lines.append( + "- **Root (/)**: The system partition where the OS and most software is installed. This is typically ~84 GB on GitHub runners.\n" +) +lines.append( + "- **Workspace**: Your build directory (`$GITHUB_WORKSPACE`). On some actions (like easimon), this may be on a separate LVM volume.\n" +) +lines.append("- **Freed Space**: How much space was reclaimed by the cleanup action.\n") +lines.append( + "- **Available After**: Total free space remaining after cleanup. Higher is better for comparing runner images.\n" +) +lines.append( + "- **⚠️ easimon Note**: Shows negative root freed because it creates an LVM volume by consuming root space, then remounts workspace there. The workspace freed is what matters.\n" +) +lines.append("\n") +# Generate markdown table with averages. Integer division used for consistency with CSV values. +lines.append( + "Image | Option | Intensity | Freed (WS) | Freed (Root) | Avail After (WS) | Avail After (Root) | Duration | GiB/sec\n" +) +lines.append("--- | --- | --- | --- | --- | --- | --- | --- | ---\n") +# Compute averages for each (image, option, intensity) group +for (image, option, intensity), g in sorted(groups.items()): + c = g["count"] or 1 + avg_ws = g["sum_freed_ws"] // c + avg_root = g["sum_freed_root"] // c + avg_dur = g["sum_dur"] // c + avg_after_ws = g["sum_after_ws"] // c + avg_after_root = g["sum_after_root"] // c + # Compute GiB/sec using workspace freed (avoid division by zero) + gib_per_sec = (avg_ws / (1024**3)) / avg_dur if avg_dur > 0 else 0 + lines.append( + f"{image} | {option} | {intensity} | {fmt_gib(avg_ws)} | {fmt_gib(avg_root)} | {fmt_gib(avg_after_ws)} | {fmt_gib(avg_after_root)} | {avg_dur}s | {gib_per_sec:.3f}\n" + ) + +summary = "".join(lines) +print(summary)