77
88_REPO_ROOT = Path (__file__ ).resolve ().parents [3 ]
99_WORKFLOW_DIR = _REPO_ROOT / ".github" / "workflows"
10- _CACHE_KEY_CONSUMERS = (
11- "build-haos-test-image.yml" ,
12- "haos-e2e-tests.yml" ,
13- "haos-e2e-embedded-tests.yml" ,
14- "haos-e2e-inaddon-tests.yml" ,
15- "haos-e2e-stdio-tests.yml" ,
16- )
10+ _CACHE_KEY_CONSUMER_FLOOR = 5
11+ _CACHE_KEY_OUTPUT_MARKER = "cache-key=haos-image-"
12+ _HAOS_IMAGE_CACHE_PATH = "/tmp/haos-test-image.qcow2"
13+ _CACHE_ACTIONS = {
14+ "actions/cache" ,
15+ "actions/cache/restore" ,
16+ "actions/cache/save" ,
17+ }
1718_CACHE_KEY_COMMAND = """hash=$(git ls-tree -r HEAD \\
1819 tests/haos_image_build \\
1920 tests/initial_test_state \\
@@ -28,30 +29,106 @@ def _workflow(path: Path) -> dict[str, Any]:
2829 return yaml .safe_load (path .read_text (encoding = "utf-8" ))
2930
3031
31- def _cache_key_command (path : Path ) -> str :
32- workflow = _workflow (path )
33- steps = [
32+ def _job_steps (job : dict [str , Any ]) -> list [dict [str , Any ]]:
33+ return [step for step in job .get ("steps" , []) if isinstance (step , dict )]
34+
35+
36+ def _cache_key_steps (job : dict [str , Any ]) -> list [dict [str , Any ]]:
37+ return [
3438 step
35- for job in workflow ["jobs" ].values ()
36- for step in job .get ("steps" , [])
37- if "image cache key" in str (step .get ("name" , "" )).lower ()
39+ for step in _job_steps (job )
40+ if _CACHE_KEY_OUTPUT_MARKER in str (step .get ("run" , "" ))
41+ ]
42+
43+
44+ def _uses_haos_image_cache (job : dict [str , Any ]) -> bool :
45+ return any (
46+ str (step .get ("uses" , "" )).partition ("@" )[0 ] in _CACHE_ACTIONS
47+ and _HAOS_IMAGE_CACHE_PATH
48+ in str (step .get ("with" , {}).get ("path" , "" )).splitlines ()
49+ for step in _job_steps (job )
50+ )
51+
52+
53+ def _cache_key_consumers (
54+ workflow_dir : Path = _WORKFLOW_DIR ,
55+ ) -> list [tuple [Path , str ]]:
56+ workflow_paths = sorted ((* workflow_dir .glob ("*.yml" ), * workflow_dir .glob ("*.yaml" )))
57+ consumers = [
58+ (path , str (job_id ))
59+ for path in workflow_paths
60+ for job_id , job in _workflow (path )["jobs" ].items ()
61+ if isinstance (job , dict ) and _uses_haos_image_cache (job )
3862 ]
39- assert len (steps ) == 1 , f"{ path .name } must have one image cache-key step"
63+ assert len (consumers ) >= _CACHE_KEY_CONSUMER_FLOOR , (
64+ "expected at least "
65+ f"{ _CACHE_KEY_CONSUMER_FLOOR } HAOS image cache-key consumers, found "
66+ f"{ [(path .name , job_id ) for path , job_id in consumers ]} "
67+ )
68+ return consumers
69+
70+
71+ def _cache_key_command (path : Path , job_id : str ) -> str :
72+ consumer = f"{ path .name } :{ job_id } "
73+ job = _workflow (path )["jobs" ][job_id ]
74+ assert isinstance (job , dict ), f"{ consumer } must be a job mapping"
75+ steps = _cache_key_steps (job )
76+ assert len (steps ) == 1 , f"{ consumer } must have one image cache-key step"
4077 script = str (steps [0 ]["run" ])
4178 start_marker = "hash=$(git ls-tree -r HEAD"
4279 end_marker = 'echo "cache-key=haos-image-$hash" >> "$GITHUB_OUTPUT"\n '
4380 assert script .count (start_marker ) == 1 , (
44- f"{ path . name } must have one cache-key command"
81+ f"{ consumer } must have one cache-key command"
4582 )
4683 assert script .count (end_marker ) == 1 , (
47- f"{ path . name } must emit one HAOS image cache key"
84+ f"{ consumer } must emit one HAOS image cache key"
4885 )
4986 start = script .index (start_marker )
5087 end = script .index (end_marker , start ) + len (end_marker )
5188 return script [start :end ]
5289
5390
5491def test_haos_image_cache_key_command_matches_every_consumer () -> None :
55- for filename in _CACHE_KEY_CONSUMERS :
56- path = _WORKFLOW_DIR / filename
57- assert _cache_key_command (path ) == _CACHE_KEY_COMMAND , filename
92+ for path , job_id in _cache_key_consumers ():
93+ consumer = f"{ path .name } :{ job_id } "
94+ assert _cache_key_command (path , job_id ) == _CACHE_KEY_COMMAND , consumer
95+
96+
97+ def test_cache_key_consumer_discovery_is_marker_independent (tmp_path : Path ) -> None :
98+ workflow = """jobs:
99+ lane:
100+ steps:
101+ - uses: actions/cache/restore@pinned
102+ with:
103+ path: /tmp/haos-test-image.qcow2
104+ key: shared
105+ """
106+ expected = []
107+ for index in range (_CACHE_KEY_CONSUMER_FLOOR ):
108+ path = tmp_path / f"lane-{ index } .yaml"
109+ path .write_text (workflow , encoding = "utf-8" )
110+ expected .append ((path , "lane" ))
111+
112+ assert _cache_key_consumers (tmp_path ) == expected
113+
114+
115+ def test_cache_key_consumer_discovery_tracks_jobs_individually (
116+ tmp_path : Path ,
117+ ) -> None :
118+ path = tmp_path / "multi-lane.yaml"
119+ jobs = {
120+ f"lane-{ index } " : {
121+ "steps" : [
122+ {
123+ "uses" : "actions/cache/restore@pinned" ,
124+ "with" : {"path" : _HAOS_IMAGE_CACHE_PATH , "key" : "shared" },
125+ }
126+ ]
127+ }
128+ for index in range (_CACHE_KEY_CONSUMER_FLOOR )
129+ }
130+ path .write_text (yaml .safe_dump ({"jobs" : jobs }), encoding = "utf-8" )
131+
132+ assert _cache_key_consumers (tmp_path ) == [
133+ (path , f"lane-{ index } " ) for index in range (_CACHE_KEY_CONSUMER_FLOOR )
134+ ]
0 commit comments