Skip to content

Commit 840005b

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

File tree

2 files changed

+161
-11
lines changed

2 files changed

+161
-11
lines changed

.github/workflows/nix-build.yml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ permissions:
1414
contents: write
1515
packages: write
1616

17-
concurrency:
18-
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
19-
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
20-
2117
jobs:
2218
nix-matrix:
2319
runs-on:
@@ -33,15 +29,12 @@ jobs:
3329
name: Generate Nix Matrix
3430
run: |
3531
set -Eeu
36-
echo matrix="$(nix eval --json '.#githubActions.matrix')" >> "$GITHUB_OUTPUT"
32+
echo matrix="$(python scripts/github-matrix.py)" >> "$GITHUB_OUTPUT"
3733
3834
build-run-image:
3935
name: ${{ matrix.name }} (${{ matrix.system }})
4036
needs: nix-matrix
41-
runs-on:
42-
group: ${{ contains(matrix.os, 'blacksmith-32vcpu-ubuntu-2404') && '' || 'self-hosted-runners-nix' }}
43-
labels:
44-
- ${{ matrix.os }}
37+
runs-on: ${{ matrix.runs_on.group && matrix.runs_on || matrix.runs_on.labels }}
4538
strategy:
4639
fail-fast: false
4740
matrix: ${{fromJSON(needs.nix-matrix.outputs.matrix)}}
@@ -73,8 +66,14 @@ jobs:
7366
aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}
7467
aws_session_token = ${AWS_SESSION_TOKEN}
7568
EOF
76-
- name: nix-fast-build
77-
run: nix build -L
69+
- name: nix build
70+
run: |
71+
if ${{ matrix.already_cached }}; then
72+
echo "${{ matrix.attr }} already cached, skipping build"
73+
exit 0
74+
fi
75+
nix build -L .#${{ matrix.attr }}
76+
7877
run-tests:
7978
needs: build-run-image
8079
if: ${{ success() }}

scripts/github-matrix.py

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

0 commit comments

Comments
 (0)