1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "strings"
7 "testing"
8)
9
10const (
11 cacheTestURL = "http://127.0.0.1:9089/"
12 cacheTestToken = "eyJqIjoiMSJ9.c2lnbmF0dXJl"
13)
14
15// cacheTestContainerURL is the relay on the docker bridge, for the steps that
16// run in a container.
17const cacheTestContainerURL = "http://172.17.0.1:9089/"
18
19func withCache(c *Config) {
20 c.CacheURL, c.CacheToken, c.ContainerCacheURL = cacheTestURL, cacheTestToken, cacheTestContainerURL
21}
22
23const envAction = `name: e
24description: d
25runs:
26 using: node20
27 main: main.sh
28`
29
30const envScript = "echo \"mode=[${ACTIONS_CACHE_MODE-unset}] url=[$ACTIONS_CACHE_URL] rurl=[$ACTIONS_RUNTIME_URL] token=[$ACTIONS_RUNTIME_TOKEN] v2=[${ACTIONS_CACHE_SERVICE_V2-unset}] results=[${ACTIONS_RESULTS_URL-unset}]\"\n"
31
32// NodeScriptActionHandler.cs:55-72: a node action gets the runtime variables. The
33// URL is the base the toolkit appends "_apis/artifactcache/…" to (it ends with a
34// slash), ACTIONS_CACHE_SERVICE_V2 is never set, and the token is masked in the
35// log.
36func TestNodeActionGetsCacheEnv(t *testing.T) {
37 rec, res := actionRun(t, files{".github/actions/e/action.yml": envAction, ".github/actions/e/main.sh": envScript},
38 " - uses: ./.github/actions/e\n", withCache)
39 if res.Conclusion != ConclusionSuccess {
40 t.Fatalf("%+v\n%s", res, logOf(rec.steps[1]))
41 }
42 got := logOf(rec.step(t, "Run ./.github/actions/e"))
43 want := "mode=[unset] url=[" + cacheTestURL + "] rurl=[" + cacheTestURL + "] token=[***] v2=[unset] results=[unset]"
44 if !strings.Contains(got, want) {
45 t.Errorf("want %q in\n%s", want, got)
46 }
47 if strings.Contains(got, cacheTestToken) {
48 t.Errorf("the token is in the log:\n%s", got)
49 }
50}
51
52// A run: step is not an action handler: it gets none of it (the official runner
53// sets these for actions only), and a job without a cache service sets nothing.
54func TestCacheEnvOnlyForActions(t *testing.T) {
55 rec, res := actionRun(t, files{}, " - run: echo \"[$ACTIONS_CACHE_URL][$ACTIONS_RUNTIME_TOKEN]\"\n", withCache)
56 if res.Conclusion != ConclusionSuccess {
57 t.Fatalf("%+v", res)
58 }
59 if got := logOf(rec.step(t, "Run echo \"[$ACTIONS_CACHE_URL][$ACTIONS_RUNTIME_TOKEN]\"")); !strings.HasSuffix(strings.TrimRight(got, "\n"), "\n[][]") {
60 t.Errorf("run step env:\n%s", got)
61 }
62 rec, res = actionRun(t, files{".github/actions/e/action.yml": envAction, ".github/actions/e/main.sh": envScript},
63 " - uses: ./.github/actions/e\n", nil)
64 if res.Conclusion != ConclusionSuccess {
65 t.Fatalf("%+v", res)
66 }
67 if got := logOf(rec.step(t, "Run ./.github/actions/e")); !strings.Contains(got, "mode=[unset] url=[] rurl=[] token=[]") {
68 t.Errorf("no cache service, no variables:\n%s", got)
69 }
70}
71
72// ContainerActionHandler.cs:219-236: a docker action gets them too, passed by
73// name to "docker run" like the other variables.
74func TestDockerActionGetsCacheEnv(t *testing.T) {
75 d, lf := fakeDocker(t, 0)
76 rec, res := actionRun(t, files{}, " - uses: docker://alpine:3.20\n", func(c *Config) {
77 c.Actions.DockerPath = d
78 withCache(c)
79 })
80 if res.Conclusion != ConclusionSuccess {
81 t.Fatalf("%+v %v", res, rec.names())
82 }
83 cs := calls(t, lf)
84 if len(cs) != 2 {
85 t.Fatalf("calls %v", cs)
86 }
87 args := "\n" + strings.Join(cs[1], "\n") + "\n"
88 for _, n := range []string{"ACTIONS_CACHE_URL", "ACTIONS_RUNTIME_URL", "ACTIONS_RUNTIME_TOKEN"} {
89 if !strings.Contains(args, "\n-e\n"+n+"\n") {
90 t.Errorf("docker run lacks -e %s:\n%s", n, args)
91 }
92 }
93 if strings.Contains(args, "ACTIONS_CACHE_SERVICE_V2") || strings.Contains(args, cacheTestToken) {
94 t.Errorf("service v2 or the token value on the command line:\n%s", args)
95 }
96 if run := logOf(rec.steps[2]); strings.Contains(run, cacheTestToken) {
97 t.Errorf("the token is in the log:\n%s", run)
98 }
99}
100
101// ACTIONS_CACHE_MODE (NodeScriptActionHandler.cs:71-84): set from the cache
102// mode of the job, for node and docker actions, next to the URL only.
103func TestCacheModeEnv(t *testing.T) {
104 for _, mode := range []string{"write", "read"} {
105 rec, res := actionRun(t, files{".github/actions/e/action.yml": envAction, ".github/actions/e/main.sh": envScript},
106 " - uses: ./.github/actions/e\n", func(c *Config) { withCache(c); c.CacheMode = mode })
107 if res.Conclusion != ConclusionSuccess {
108 t.Fatalf("%+v", res)
109 }
110 if got := logOf(rec.step(t, "Run ./.github/actions/e")); !strings.Contains(got, "mode=["+mode+"] url=[") {
111 t.Errorf("mode %s:\n%s", mode, got)
112 }
113 }
114 // A mode without a cache service sets nothing.
115 rec, res := actionRun(t, files{".github/actions/e/action.yml": envAction, ".github/actions/e/main.sh": envScript},
116 " - uses: ./.github/actions/e\n", func(c *Config) { c.CacheMode = "write" })
117 if res.Conclusion != ConclusionSuccess {
118 t.Fatalf("%+v", res)
119 }
120 if got := logOf(rec.step(t, "Run ./.github/actions/e")); !strings.Contains(got, "mode=[unset]") {
121 t.Errorf("no cache service:\n%s", got)
122 }
123}
124
125func TestDockerActionGetsCacheMode(t *testing.T) {
126 d, lf := fakeDocker(t, 0)
127 _, res := actionRun(t, files{}, " - uses: docker://alpine:3.20\n", func(c *Config) {
128 c.Actions.DockerPath = d
129 withCache(c)
130 c.CacheMode = "read"
131 })
132 if res.Conclusion != ConclusionSuccess {
133 t.Fatalf("%+v", res)
134 }
135 cs := calls(t, lf)
136 if len(cs) != 2 || !strings.Contains("\n"+strings.Join(cs[1], "\n")+"\n", "\n-e\nACTIONS_CACHE_MODE\n") {
137 t.Errorf("docker run lacks -e ACTIONS_CACHE_MODE: %v", cs)
138 }
139}