44"""Guard the checked-in credential boundary; GitHub environment rules are external."""
55
66import os
7+ import re
78import subprocess
89from pathlib import Path
910
11+ import pytest
1012import yaml
1113
1214REPO_ROOT = Path (__file__ ).resolve ().parents [2 ]
@@ -42,8 +44,7 @@ def test_shared_nix_setup_only_reads_the_cache():
4244 assert "cachix-auth-token" not in setup .get ("inputs" , {})
4345
4446
45- def test_cache_writer_uses_only_the_main_push_revision ():
46- writer = load_yaml (WRITER )
47+ def check_cache_writer (writer : dict ) -> None :
4748 assert writer ["on" ] == {"push" : {"branches" : ["main" ]}}
4849 assert writer ["permissions" ] == {"contents" : "read" }
4950 assert set (writer ["jobs" ]) == {"populate" }
@@ -59,25 +60,71 @@ def test_cache_writer_uses_only_the_main_push_revision():
5960 {"system" : "aarch64-linux" , "runner" : "ubuntu-24.04-arm" },
6061 {"system" : "aarch64-darwin" , "runner" : "macos-15-xlarge" },
6162 ]
62- # No PR artifacts, local actions or caller-supplied source enter this job.
63- actions = {
64- step [ "uses" ]. split ( "@" )[ 0 ]: step for step in job ["steps" ] if "uses" in step
65- }
66- assert set ( actions ) == {
63+ # Review any new step in this credential-bearing job. Preserve order and
64+ # duplicates: a later main checkout must not hide an earlier candidate build.
65+ steps = job ["steps" ]
66+ assert [ step . get ( "uses" , "run" ). split ( "@" )[ 0 ] for step in steps ] == [
67+ "run" ,
6768 "actions/checkout" ,
6869 "cachix/install-nix-action" ,
6970 "cachix/cachix-action" ,
70- }
71- assert actions ["actions/checkout" ]["with" ] == {
71+ "run" ,
72+ ]
73+ credential , checkout , install_nix , cache , build = steps
74+ assert "run" in credential and "run" in build
75+ for step in (checkout , install_nix , cache ):
76+ assert re .fullmatch (r"[0-9a-f]{40}" , step ["uses" ].partition ("@" )[2 ])
77+ assert checkout ["with" ] == {
7278 "persist-credentials" : "false" ,
7379 "ref" : "${{ github.sha }}" ,
7480 }
75- assert actions [ "cachix/cachix-action" ] ["with" ] == {
81+ assert cache ["with" ] == {
7682 "name" : "openshell" ,
7783 "authToken" : "${{ secrets.CACHIX_WRITE_TOKEN }}" ,
7884 }
7985
8086
87+ def test_cache_writer_uses_only_the_main_push_revision ():
88+ check_cache_writer (load_yaml (WRITER ))
89+
90+
91+ def test_cache_writer_rejects_candidate_checkout_followed_by_main_restore ():
92+ writer = load_yaml (WRITER )
93+ steps = writer ["jobs" ]["populate" ]["steps" ]
94+ checkout = steps [1 ]
95+ candidate_checkout = {
96+ ** checkout ,
97+ "with" : {** checkout ["with" ], "ref" : "refs/pull/123/head" },
98+ }
99+ steps .insert (- 1 , candidate_checkout )
100+ steps .append (checkout )
101+
102+ with pytest .raises (AssertionError ):
103+ check_cache_writer (writer )
104+
105+
106+ @pytest .mark .parametrize (
107+ ("source" , "destination" ),
108+ [(1 , 4 ), (4 , 1 )],
109+ ids = ["checkout-after-build" , "build-before-checkout" ],
110+ )
111+ def test_cache_writer_rejects_reordered_steps (source : int , destination : int ):
112+ writer = load_yaml (WRITER )
113+ steps = writer ["jobs" ]["populate" ]["steps" ]
114+ steps .insert (destination , steps .pop (source ))
115+
116+ with pytest .raises (AssertionError ):
117+ check_cache_writer (writer )
118+
119+
120+ def test_cache_writer_rejects_mutable_action_refs ():
121+ writer = load_yaml (WRITER )
122+ writer ["jobs" ]["populate" ]["steps" ][1 ]["uses" ] = "actions/checkout@main"
123+
124+ with pytest .raises (AssertionError ):
125+ check_cache_writer (writer )
126+
127+
81128def test_cache_writer_requires_its_environment_credential ():
82129 step = load_yaml (WRITER )["jobs" ]["populate" ]["steps" ][0 ]
83130 assert step ["env" ] == {"CACHIX_WRITE_TOKEN" : "${{ secrets.CACHIX_WRITE_TOKEN }}" }
0 commit comments