Skip to content

Commit 475d748

Browse files
committed
feat(ci): do not build if already cached
1 parent db1e5e4 commit 475d748

File tree

2 files changed

+150
-7
lines changed

2 files changed

+150
-7
lines changed

.github/workflows/nix-build.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@ jobs:
3333
name: Generate Nix Matrix
3434
run: |
3535
set -Eeu
36-
echo matrix="$(nix eval --json '.#githubActions.matrix')" >> "$GITHUB_OUTPUT"
36+
echo matrix="$(python scripts/github-matrix.py)" >> "$GITHUB_OUTPUT"
3737
3838
build-run-image:
3939
name: ${{ matrix.name }} (${{ matrix.system }})
4040
needs: nix-matrix
41-
runs-on:
42-
group: ${{ contains(matrix.os, 'blacksmith-32vcpu-ubuntu-2404') && '' || 'self-hosted-runners-nix' }}
43-
labels:
44-
- ${{ matrix.os }}
41+
runs-on: ${{ matrix.runs_on.group && matrix.runs_on || matrix.runs_on.labels }}
4542
strategy:
4643
fail-fast: false
4744
matrix: ${{fromJSON(needs.nix-matrix.outputs.matrix)}}
@@ -73,8 +70,14 @@ jobs:
7370
aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}
7471
aws_session_token = ${AWS_SESSION_TOKEN}
7572
EOF
76-
- name: nix-fast-build
77-
run: nix build -L
73+
- name: nix build
74+
run: |
75+
if ${{ matrix.already_cached == 'true' }}; then
76+
echo "${{ matrix.attr }} already cached, skipping build"
77+
exit 0
78+
fi
79+
nix build -L .#${{ matrix.attr }}
80+
7881
run-tests:
7982
needs: build-run-image
8083
if: ${{ success() }}

scripts/github-matrix.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import os
5+
import subprocess
6+
import sys
7+
from typing import Any, Dict, Generator, List, Literal, NotRequired, Optional, Set, TypedDict
8+
9+
10+
class NixEvalJobsOutput(TypedDict):
11+
"""Raw output from nix-eval-jobs command."""
12+
13+
attr: str
14+
attrPath: List[str]
15+
cacheStatus: Literal["notBuilt", "cached", "local"]
16+
drvPath: str
17+
isCached: bool
18+
name: str
19+
system: str
20+
neededBuilds: NotRequired[List[Any]]
21+
neededSubstitutes: NotRequired[List[Any]]
22+
outputs: NotRequired[Dict[str, str]]
23+
24+
25+
class RunsOnConfig(TypedDict):
26+
"""GitHub Actions runs-on configuration."""
27+
28+
group: NotRequired[str]
29+
labels: List[str]
30+
31+
32+
class GitHubActionPackage(TypedDict):
33+
"""Processed package for GitHub Actions matrix."""
34+
35+
attr: str
36+
name: str
37+
system: str
38+
already_cached: bool
39+
runs_on: RunsOnConfig
40+
41+
42+
BUILD_RUNNER_MAP: Dict[str, RunsOnConfig] = {
43+
"aarch64-linux": {
44+
"group": "self-hosted-runners-nix",
45+
"labels": ["aarch64-linux"],
46+
},
47+
"aarch64-darwin": {
48+
"group": "self-hosted-runners-nix",
49+
"labels": ["aarch64-darwin"],
50+
},
51+
"x86_64-linux": {
52+
"labels": ["blacksmith-32vcpu-ubuntu-2404"],
53+
},
54+
}
55+
56+
57+
def get_worker_count() -> int:
58+
"""Get optimal worker count based on CPU cores."""
59+
try:
60+
return max(1, int(os.cpu_count() / 2))
61+
except (OSError, AttributeError):
62+
print(
63+
"Warning: Unable to get CPU count, using default max_workers=1",
64+
file=sys.stderr,
65+
)
66+
return 1
67+
68+
69+
def build_nix_eval_command(max_workers: int) -> List[str]:
70+
"""Build the nix-eval-jobs command with appropriate flags."""
71+
return [
72+
"nix-eval-jobs",
73+
"--flake",
74+
".#checks",
75+
"--check-cache-status",
76+
"--force-recurse",
77+
"--quiet",
78+
"--workers",
79+
str(max_workers),
80+
]
81+
82+
83+
def parse_nix_eval_line(
84+
line: str, drv_paths: Set[str]
85+
) -> Optional[GitHubActionPackage]:
86+
"""Parse a single line of nix-eval-jobs output"""
87+
if not line.strip():
88+
return None
89+
90+
try:
91+
data: NixEvalJobsOutput = json.loads(line)
92+
if data["drvPath"] in drv_paths:
93+
return None
94+
drv_paths.add(data["drvPath"])
95+
96+
runs_on_config = BUILD_RUNNER_MAP[data["system"]]
97+
98+
return {
99+
"attr": "checks." + data["attr"],
100+
"name": data["name"],
101+
"system": data["system"],
102+
"already_cached": data.get("cacheStatus") != "notBuilt",
103+
"runs_on": runs_on_config,
104+
}
105+
except json.JSONDecodeError:
106+
print(f"Skipping invalid JSON line: {line}", file=sys.stderr)
107+
return None
108+
109+
110+
def run_nix_eval_jobs(cmd: List[str]) -> Generator[GitHubActionPackage, None, None]:
111+
"""Run nix-eval-jobs and yield parsed package data."""
112+
print(f"Running command: {' '.join(cmd)}", file=sys.stderr)
113+
114+
with subprocess.Popen(
115+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
116+
) as process:
117+
drv_paths = set()
118+
119+
for line in process.stdout:
120+
package = parse_nix_eval_line(line, drv_paths)
121+
if package:
122+
yield package
123+
124+
if process.returncode and process.returncode != 0:
125+
print("Error: Evaluation failed", file=sys.stderr)
126+
sys.stderr.write(process.stderr.read())
127+
sys.exit(process.returncode)
128+
129+
130+
def main() -> None:
131+
max_workers = get_worker_count()
132+
cmd = build_nix_eval_command(max_workers)
133+
134+
gh_action_packages = list(run_nix_eval_jobs(cmd))
135+
gh_output = {"include": gh_action_packages}
136+
print(json.dumps(gh_output))
137+
138+
139+
if __name__ == "__main__":
140+
main()

0 commit comments

Comments
 (0)