Skip to content

Commit cefee04

Browse files
jayvdbclaude
andcommitted
Add free-disk-space-windows action; narrow CI to test.yaml-windows to verify
A new local composite action at .github/actions/free-disk-space-windows removes safely-unused preinstalled software (Android SDK, old Python / JDK / .NET SDK versions, misc DB SDKs) from the GHA Windows runner's C:\ before docker work. Counterpart to jlumbroso/free-disk-space on Linux, which has no Windows path. All five categories are individual inputs defaulting to true so callers can opt out per category. Steps run under Git Bash with `/c/...` paths (no PowerShell) so they satisfy the new gha_action shell-policy below. Wired into both docker-windows.yaml (before the existing docker-prune cleanup) and test.yaml (after the Linux jlumbroso step, gated on `if: runner.os == 'Windows'`). New conftest policy at config/conftest/policy/gha_action/gha_action.rego: composite-action `run:` steps must use shell `bash --noprofile --norc -euo pipefail {0}` exactly (matching the workflow `defaults.run.shell` invariant in the existing gha rule). All three composite actions (install-mise, install-mise-tools, free-disk-space-windows) migrated to the explicit form. The conftest-check-yaml task gains a third pass to enforce the rule across `.github/actions/*/action.yaml`. Audit of other workflow-only tooling and extension to actions: - action-validator: now validates both - zizmor: now scans both; the github-env false positive on install-mise's deterministic $GITHUB_PATH append is suppressed via inline `# zizmor: ignore[github-env]` (config-file ignore doesn't work for composite actions per zizmor docs) - ast-grep gha-no-folded-strip: extended to actions; gained a `description:` carve-out (action.yaml description fields ARE long folded paragraphs by design) - Other gha-* ast-grep rules stay workflow-only (job/strategy/ concurrency are workflow shape; not relevant to actions) CI temporarily narrowed: check.yaml, dependencies.yaml, docker-linux.yaml, docker-windows.yaml all parked with `if: false`; test.yaml PR matrix narrowed to windows-latest only. Drop the TEMPORARY blocks (4 file-pairs total) and restore test.yaml's 4-OS PR matrix once the new action is confirmed to free disk space as expected on a real CI run. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 465ed5f commit cefee04

11 files changed

Lines changed: 219 additions & 35 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
name: Free disk space
3+
description: >-
4+
Remove safely-unused preinstalled software from the GHA Windows runner's
5+
`C:\` to give downstream `docker build` (which stages HCS layer scratch
6+
on `C:\Windows\SystemTemp`) more headroom. Counterpart to
7+
jlumbroso/free-disk-space on Linux, which doesn't have a Windows path.
8+
Logs disk-free before + after so the reclaimed space is visible.
9+
10+
Caller is responsible for the `if: runner.os == 'Windows'` guard --
11+
every step here uses Git Bash with `C:\` mounted at `/c`; running this
12+
on a non-Windows runner will silently no-op (no /c mount) at best.
13+
14+
inputs:
15+
android:
16+
description: "Remove Android SDK trees (~12 GB)."
17+
required: false
18+
default: "true"
19+
hostedtoolcache-old-pythons:
20+
description: >-
21+
Remove Python 3.9/3.10/3.11/3.12 versions from `hostedtoolcache`
22+
(~2 GB). The repo uses mise's pinned 3.13.
23+
required: false
24+
default: "true"
25+
hostedtoolcache-old-jdks:
26+
description: >-
27+
Remove JDK 8/11/17/21 versions from `hostedtoolcache\Java`
28+
(~1 GB). The repo uses mise's pinned Java 26.
29+
required: false
30+
default: "true"
31+
old-dotnet-sdks:
32+
description: >-
33+
Remove .NET SDK 6.0.x and 7.0.x trees (~1-2 GB). The repo
34+
installs current dotnet via mise.
35+
required: false
36+
default: "true"
37+
misc-sdks:
38+
description: >-
39+
Remove unused preinstalled SDKs (Heroku, MongoDB, PostgreSQL,
40+
Strawberry Perl) -- ~1 GB combined.
41+
required: false
42+
default: "true"
43+
44+
runs:
45+
using: composite
46+
steps:
47+
- name: before
48+
shell: bash --noprofile --norc -euo pipefail {0}
49+
run: df -h /c
50+
51+
- name: Android SDK
52+
if: inputs.android == 'true'
53+
shell: bash --noprofile --norc -euo pipefail {0}
54+
run: |
55+
for p in "/c/Android" "/c/Program Files (x86)/Android"; do
56+
[ -e "$p" ] && { echo "removing $p"; rm -rf -- "$p"; } || true
57+
done
58+
59+
- name: hostedtoolcache old Pythons
60+
if: inputs.hostedtoolcache-old-pythons == 'true'
61+
shell: bash --noprofile --norc -euo pipefail {0}
62+
run: |
63+
shopt -s nullglob
64+
for p in /c/hostedtoolcache/Python/3.{9,10,11,12}*; do
65+
echo "removing $p"
66+
rm -rf -- "$p"
67+
done
68+
69+
- name: hostedtoolcache old JDKs
70+
if: inputs.hostedtoolcache-old-jdks == 'true'
71+
shell: bash --noprofile --norc -euo pipefail {0}
72+
run: |
73+
shopt -s nullglob
74+
for p in /c/hostedtoolcache/Java_Adopt_jdk/{8,11,17,21}.*; do
75+
echo "removing $p"
76+
rm -rf -- "$p"
77+
done
78+
79+
- name: old .NET SDKs
80+
if: inputs.old-dotnet-sdks == 'true'
81+
shell: bash --noprofile --norc -euo pipefail {0}
82+
run: |
83+
shopt -s nullglob
84+
for p in "/c/Program Files/dotnet/sdk/"{6,7}.0.*; do
85+
echo "removing $p"
86+
rm -rf -- "$p"
87+
done
88+
89+
- name: misc SDKs
90+
if: inputs.misc-sdks == 'true'
91+
shell: bash --noprofile --norc -euo pipefail {0}
92+
run: |
93+
pf="/c/Program Files"
94+
pf86="/c/Program Files (x86)"
95+
for p in "$pf/MongoDB" "$pf/PostgreSQL" "$pf86/Heroku" "/c/Strawberry"; do
96+
[ -e "$p" ] && { echo "removing $p"; rm -rf -- "$p"; } || true
97+
done
98+
99+
- name: after
100+
shell: bash --noprofile --norc -euo pipefail {0}
101+
run: df -h /c

.github/actions/install-mise-tools/action.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ runs:
3434
extra-tools: ${{ inputs.extra-tools }}
3535

3636
- name: Show MISE_ENV
37-
shell: bash
37+
shell: bash --noprofile --norc -euo pipefail {0}
3838
run: echo "MISE_ENV=$MISE_ENV"
3939

4040
# Optional npm backend, installed before the main `mise install`.
@@ -45,13 +45,13 @@ runs:
4545
- name: Install aube (optional npm backend, allowed to fail)
4646
if: contains(env.MISE_ENV, 'js')
4747
continue-on-error: true
48-
shell: bash
48+
shell: bash --noprofile --norc -euo pipefail {0}
4949
env:
5050
GITHUB_TOKEN: ${{ inputs.github-token }}
5151
run: mise run setup-aube
5252

5353
- name: Install mise tools
54-
shell: bash
54+
shell: bash --noprofile --norc -euo pipefail {0}
5555
env:
5656
GITHUB_TOKEN: ${{ inputs.github-token }}
5757
run: |
@@ -67,7 +67,7 @@ runs:
6767
# that's the step that creates the shims.
6868
- name: Remove self-recursive mise.cmd shim on Windows
6969
if: runner.os == 'Windows'
70-
shell: bash
70+
shell: bash --noprofile --norc -euo pipefail {0}
7171
run: |
7272
shims="$LOCALAPPDATA/mise/shims"
7373
rm -fv "$shims/mise.cmd" "$shims/mise"

.github/actions/install-mise/action.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@ runs:
3636
# this problem (each .cmd shim invokes `mise exec --` plainly via cmd).
3737
- name: Add mise shims dir to PATH on Windows
3838
if: runner.os == 'Windows'
39-
shell: bash
40-
run: echo "$LOCALAPPDATA\mise\shims" >> "$GITHUB_PATH"
39+
shell: bash --noprofile --norc -euo pipefail {0}
40+
# The appended value is a hardcoded mise install-dir constant under
41+
# our control -- no user input flows in -- so the github-env audit's
42+
# arbitrary-code-execution shape doesn't apply.
43+
run: echo "$LOCALAPPDATA\mise\shims" >> "$GITHUB_PATH" # zizmor: ignore[github-env]
4144

4245
# Marks the checked-out .mise/config*.toml as trusted so subsequent mise
4346
# invocations don't error out with "Config files are not trusted". The
4447
# caller must run actions/checkout BEFORE this composite action so the
4548
# config files are on disk.
4649
- name: Trust mise config
47-
shell: bash
50+
shell: bash --noprofile --norc -euo pipefail {0}
4851
run: mise trust

.github/workflows/check.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ env:
2323

2424
jobs:
2525
check:
26+
# TEMPORARY: `if: false` parks the job so PR runner capacity goes to the
27+
# test.yaml windows-only matrix while we verify the new
28+
# free-disk-space-windows action. Drop this once the action is confirmed.
29+
if: false
2630
runs-on: ubuntu-latest
2731
timeout-minutes: 25
2832
steps:

.github/workflows/dependencies.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ env:
2929

3030
jobs:
3131
dependencies:
32+
# TEMPORARY: `if: false` parks the job so PR runner capacity goes to the
33+
# test.yaml windows-only matrix while we verify the new
34+
# free-disk-space-windows action. Drop this once the action is confirmed.
35+
if: false
3236
# `ubuntu-latest`, not `ubuntu-slim`: although every step in this workflow
3337
# is metadata/network-bound (no Rust compile), `taiki-e/install-action`
3438
# itself assumes a standard runner-image FHS (e.g. ~/.cargo/bin) and

.github/workflows/docker-linux.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ env:
2424

2525
jobs:
2626
build:
27+
# TEMPORARY: `if: false` parks the job so PR runner capacity goes to the
28+
# test.yaml windows-only matrix while we verify the new
29+
# free-disk-space-windows action. Drop this once the action is confirmed.
30+
if: false
2731
runs-on: ubuntu-latest
2832
timeout-minutes: 120
2933
strategy:

.github/workflows/docker-windows.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ name: docker-windows
44
"on":
55
pull_request:
66
paths:
7+
- .github/actions/free-disk-space-windows/**
78
- .github/workflows/docker-windows.yaml
89
- Dockerfile.nanoserver
910
- Dockerfile.windows
@@ -25,6 +26,10 @@ env:
2526

2627
jobs:
2728
build:
29+
# TEMPORARY: `if: false` parks the job so PR runner capacity goes to the
30+
# test.yaml windows-only matrix while we verify the new
31+
# free-disk-space-windows action. Drop this once the action is confirmed.
32+
if: false
2833
runs-on: ${{ matrix.runner }}
2934
strategy:
3035
fail-fast: false
@@ -74,6 +79,15 @@ jobs:
7479
sc query docker | grep -q RUNNING || net start docker
7580
docker version
7681
82+
# Reclaim runner C:\ before any docker work. Windows `docker build`
83+
# stages HCS layer scratch under `C:\Windows\SystemTemp`, so every GB
84+
# we free here is a GB that doesn't go to disk-pressure failures
85+
# inside the build. Preinstalled trees we never touch (Android, old
86+
# Python / JDK versions, old .NET SDKs, misc DB SDKs) come out first;
87+
# the docker-prune step below clears any layer-cache leftovers.
88+
- name: Free disk space on Windows runner
89+
uses: ./.github/actions/free-disk-space-windows
90+
7791
# Free runner disk before the build. The Server Core lane has been hitting
7892
# "There is not enough space on the disk" during the conda-clang-tools
7993
# install inside `mise install`. `docker system prune -af` clears any

.github/workflows/test.yaml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,16 @@ jobs:
8686
|| github.event.inputs.os == 'windows-latest' && '[{"os":"windows-latest","timeout":60}]'
8787
|| github.event.inputs.os == 'windows-11-arm' && '[{"os":"windows-11-arm","timeout":60}]'
8888
)
89-
|| format('[{0},{1},{2},{3}]',
90-
'{"os":"ubuntu-latest","timeout":40}',
91-
'{"os":"ubuntu-24.04-arm","timeout":40}',
92-
'{"os":"macos-latest","timeout":45}',
93-
'{"os":"windows-latest","timeout":60}')
89+
|| '[{"os":"windows-latest","timeout":60}]'
9490
) }}
91+
# TEMPORARY: PR matrix narrowed to windows-latest only while we verify
92+
# the new free-disk-space-windows action. Restore the canonical 4-OS
93+
# PR matrix once the action is confirmed:
94+
# || format('[{0},{1},{2},{3}]',
95+
# '{"os":"ubuntu-latest","timeout":40}',
96+
# '{"os":"ubuntu-24.04-arm","timeout":40}',
97+
# '{"os":"macos-latest","timeout":45}',
98+
# '{"os":"windows-latest","timeout":60}')
9599
steps:
96100
- name: Checkout
97101
uses: actions/checkout@v4
@@ -103,6 +107,10 @@ jobs:
103107
if: runner.os == 'Linux'
104108
uses: jlumbroso/free-disk-space@v1.3.1
105109

110+
- name: Free disk space on Windows runner
111+
if: runner.os == 'Windows'
112+
uses: ./.github/actions/free-disk-space-windows
113+
106114
- name: Install Mesa Vulkan drivers (lavapipe)
107115
if: runner.os == 'Linux'
108116
run: |
@@ -128,7 +136,10 @@ jobs:
128136
echo "=== Git Bash perspective ==="
129137
echo " HOME=[$HOME] USERPROFILE=[$USERPROFILE]"
130138
echo "=== cmd.exe perspective ==="
131-
cmd.exe /c "echo HOME=[%HOME%] USERPROFILE=[%USERPROFILE%]"
139+
# `cmd //c` (double slash) -- the single-slash form is rewritten by
140+
# MSYS path-translation as a Unix path, dropping cmd into interactive
141+
# mode and skipping the echo.
142+
cmd //c "echo HOME=[%HOME%] USERPROFILE=[%USERPROFILE%]"
132143
133144
# mise's Tera renderer expects $HOME to be set when evaluating env.HOME
134145
# in [vars] (config.toml has e.g. `conda_openssl = "{{ env.HOME }}/.local

.mise/config.toml

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -543,14 +543,14 @@ depends = ["fix:*"]
543543
description = "Apply all lint-fix passes: fix:rust + any loaded guest fix:<lang>"
544544

545545
[tasks.action-validator]
546-
description = "Validate GitHub Actions workflow YAML"
547-
run = "action-validator .github/workflows/*.yaml"
546+
description = "Validate GitHub Actions workflow + composite-action YAML"
547+
run = "action-validator .github/workflows/*.yaml .github/actions/*/action.yaml"
548548

549549
[tasks.zizmor-check]
550-
description = "Audit GitHub Actions workflows for security issues (zizmor)"
550+
description = "Audit GitHub Actions workflows + composite actions for security issues (zizmor)"
551551
# --offline so it needs no GH token and stays deterministic; config/zizmor.yaml
552552
# turns off the hash-pin policy (we pin actions by tag, not commit).
553-
run = "zizmor --offline --no-progress -c config/zizmor.yaml .github/workflows"
553+
run = "zizmor --offline --no-progress -c config/zizmor.yaml .github/workflows .github/actions"
554554

555555
[tasks.ls-lint-check]
556556
description = "Lint file and directory naming conventions (ls-lint)"
@@ -647,19 +647,21 @@ git ls-files '*Cargo.toml' '.mise/config*.toml' '*pyproject.toml' Cargo.lock con
647647
shell = "bash -euo pipefail -c"
648648

649649
[tasks.conftest-check-yaml]
650-
description = "Run conftest OPA/Rego policies over the GitHub Actions workflow YAML"
651-
# Two passes: per-file rules (gha, no_trailing_backslash) for shape/content
652-
# checks that don't need a file path; --combine rules (gha_combined) for
653-
# cross-cutting invariants where the rule has to compare each workflow's
654-
# parsed body against its own filename (the `paths:` self-reference check).
655-
# Conftest's per-file mode has no `input.path`, so `gha_combined` lives in
656-
# its own pass.
650+
description = "Run conftest OPA/Rego policies over the GitHub Actions workflow + action YAML"
651+
# Three passes: per-file rules (gha, no_trailing_backslash) over workflow
652+
# YAML; --combine rules (gha_combined) over workflow YAML for cross-cutting
653+
# invariants where the rule has to compare each workflow's parsed body
654+
# against its own filename (the `paths:` self-reference check; conftest's
655+
# per-file mode has no `input.path`); and gha_action over the composite
656+
# actions under .github/actions/*/action.yaml (different schema, so its
657+
# own namespace).
657658
run = """
658659
ns="--namespace gha --namespace no_trailing_backslash"
659660
# $ns expands to repeated `--namespace <n>` pairs; word-splitting is intentional.
660661
# shellcheck disable=SC2086
661662
conftest test --parser yaml $ns -p config/conftest/policy .github/workflows
662663
conftest test --parser yaml --combine --namespace gha_combined -p config/conftest/policy .github/workflows
664+
conftest test --parser yaml --namespace gha_action -p config/conftest/policy .github/actions/*/action.yaml
663665
"""
664666
shell = "bash -euo pipefail -c"
665667

config/ast-grep/rules/gha-no-folded-strip.yaml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,28 @@ message: |
1111
- a literal block `run: |` (which preserves newlines verbatim).
1212
files:
1313
- .github/workflows/*.yaml
14+
- .github/actions/*/action.yaml
1415
# tree-sitter parses every block scalar (folded `>`, literal `|`, both with
1516
# their chomping variants `+` / `-`) as a single `block_scalar` node whose
1617
# leading characters carry the header. Regex-match only the `>-` form so `>`,
1718
# `|`, and `|-` continue to pass; `^>-` anchors at the start of the node text
1819
# so we don't accidentally match a `>-` that appears inside the scalar body.
1920
#
20-
# Carve-out: `shell: >-` is fine. The rule's whitespace concern is about
21-
# `run:` bodies losing newlines; `shell:` values are command-line strings
22-
# that NEED whitespace folding (a multi-line `${{ ... && ... || ... }}`
23-
# ternary must fold to a single command line). Match the parent
24-
# block_mapping_pair's text starting with `shell:` to skip just that key.
21+
# Carve-outs: `shell: >-` is fine because shell values are command-line
22+
# strings that NEED whitespace folding (a multi-line `${{ ... && ... ||
23+
# ... }}` ternary must fold to one command line). `description: >-` is
24+
# fine because action.yaml documentation fields ARE long folded
25+
# paragraphs by design -- newline loss is the wanted behavior, not the
26+
# feared one.
2527
rule:
26-
kind: block_scalar
27-
regex: "^>-"
28-
not:
29-
inside:
30-
pattern: "shell: $VAL"
31-
stopBy: end
28+
all:
29+
- kind: block_scalar
30+
- regex: "^>-"
31+
- not:
32+
any:
33+
- inside:
34+
pattern: "shell: $VAL"
35+
stopBy: end
36+
- inside:
37+
pattern: "description: $VAL"
38+
stopBy: end

0 commit comments

Comments
 (0)