v0.28.0 fca9155

ci_secrets_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase_test
4
5import (
6	"strings"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10
11	"bnema.dev/ermite/internal/usecase"
12)
13
14// Which secrets a job refers to: a name by dot or bracket, anything else is all.
15func TestSecretsUsedBy(t *testing.T) {
16	t.Parallel()
17	steps := func(run string) string {
18		return "on: push\njobs:\n  a:\n    runs-on: x\n    steps:\n      - run: " + run + "\n"
19	}
20	cases := []struct {
21		name string
22		yaml string
23		want usecase.SecretUse
24	}{
25		{"none", steps("echo hi"), usecase.SecretUse{}},
26		{"dot", steps("echo ${{ secrets.Token }}"), usecase.SecretUse{Names: []string{"TOKEN"}}},
27		{"bracket", steps(`echo "${{ secrets['my_Token'] }}"`), usecase.SecretUse{Names: []string{"MY_TOKEN"}}},
28		{"bracket with double quotes is not an expression string", steps(`echo '${{ secrets["x"] }}'`), usecase.SecretUse{All: true}},
29		{"several, sorted, deduplicated", steps("echo ${{ secrets.B }} ${{ secrets.a }} ${{ secrets.B }}"), usecase.SecretUse{Names: []string{"A", "B"}}},
30		{"in a condition", steps("echo\n        if: ${{ secrets.FLAG != '' }}"), usecase.SecretUse{Names: []string{"FLAG"}}},
31		{"in a function", steps("echo ${{ format('{0}', secrets.X) }}"), usecase.SecretUse{Names: []string{"X"}}},
32		{"the context name is case-insensitive", steps("echo ${{ SECRETS.x }}"), usecase.SecretUse{Names: []string{"X"}}},
33		{"bare context", steps("echo ${{ secrets }}"), usecase.SecretUse{All: true}},
34		{"toJSON(secrets)", steps("echo ${{ toJSON(secrets) }}"), usecase.SecretUse{All: true}},
35		{"computed index", steps("echo ${{ secrets[matrix.name] }}"), usecase.SecretUse{All: true}},
36		{"star", steps("echo ${{ secrets.* }}"), usecase.SecretUse{All: true}},
37		{"a named secret next to a bare use", steps("echo ${{ secrets.A }} ${{ join(secrets) }}"), usecase.SecretUse{All: true, Names: []string{"A"}}},
38		{"a string literal that says secrets is not a use", steps("echo ${{ 'secrets.NOT' }}"), usecase.SecretUse{}},
39		{"text outside the braces is not a use", steps("echo secrets.NOT"), usecase.SecretUse{}},
40		{"an expression with a quoted brace", steps("echo ${{ format('}}', secrets.Q) }}"), usecase.SecretUse{Names: []string{"Q"}}},
41		{"does not parse but mentions secrets", steps("echo ${{ secrets.A == }}"), usecase.SecretUse{All: true}},
42		{"does not parse and does not mention them", steps("echo ${{ == }}"), usecase.SecretUse{}},
43		{"not YAML", "on: [", usecase.SecretUse{All: true}},
44		{"not a mapping", "- a\n- b\n", usecase.SecretUse{All: true}},
45		{"empty", "", usecase.SecretUse{All: true}},
46	}
47	for _, tc := range cases {
48		assert.Equal(t, tc.want, usecase.SecretsUsedBy(tc.yaml, "a"), tc.name)
49	}
50}
51
52func TestSecretsUsedByCoversTheWholeSubtreeAndWorkflowEnvOnly(t *testing.T) {
53	t.Parallel()
54	src := `on: push
55env:
56  WF: ${{ secrets.WORKFLOW_LEVEL }}
57defaults:
58  run:
59    shell: bash
60jobs:
61  a:
62    runs-on: ${{ secrets.IN_RUNS_ON }}
63    name: ${{ secrets.IN_NAME }}
64    env:
65      E: ${{ secrets.JOB_ENV }}
66    container:
67      image: x
68      credentials:
69        password: ${{ secrets.REGISTRY }}
70    services:
71      db:
72        image: y
73        env:
74          P: ${{ secrets.SERVICE }}
75    outputs:
76      o: ${{ secrets.OUTPUT }}
77    strategy:
78      matrix:
79        k: [ "${{ secrets.MATRIX }}" ]
80    steps:
81      - uses: some/action@v1
82        with:
83          token: ${{ secrets.WITH_INPUT }}
84      - run: echo
85        env: { K: "${{ secrets.FLOW_STYLE }}" }
86  b:
87    runs-on: x
88    steps:
89      - run: echo ${{ secrets.OTHER_JOB }}
90`
91	got := usecase.SecretsUsedBy(src, "a")
92	assert.False(t, got.All)
93	assert.Equal(t, []string{
94		"FLOW_STYLE", "IN_NAME", "IN_RUNS_ON", "JOB_ENV", "MATRIX", "OUTPUT", "REGISTRY", "SERVICE", "WITH_INPUT", "WORKFLOW_LEVEL",
95	}, got.Names, "every expression of the job and of the workflow env; not another job's")
96	assert.Equal(t, []string{"OTHER_JOB", "WORKFLOW_LEVEL"}, usecase.SecretsUsedBy(src, "b").Names)
97	assert.Equal(t, []string{"WORKFLOW_LEVEL"}, usecase.SecretsUsedBy(src, "missing").Names)
98}
99
100// An anchor defined in another job (or reached through a chain of aliases) is
101// followed: the job that aliases it refers to its secrets.
102func TestSecretsUsedByFollowsYAMLAliases(t *testing.T) {
103	t.Parallel()
104	src := `on: push
105jobs:
106  b:
107    runs-on: x
108    env: &shared
109      K: ${{ secrets.FROM_OTHER_JOB }}
110    steps:
111      - run: echo
112  a:
113    runs-on: x
114    env: *shared
115    steps:
116      - run: echo
117        env: &chain
118          J: ${{ secrets.CHAIN_ROOT }}
119      - run: echo
120        env: *chain
121  c:
122    runs-on: x
123    steps:
124      - run: echo
125        with: { list: [ *shared, *chain ] }
126`
127	assert.Equal(t, []string{"FROM_OTHER_JOB"}, usecase.SecretsUsedBy(src, "b").Names)
128	assert.Equal(t, []string{"CHAIN_ROOT", "FROM_OTHER_JOB"}, usecase.SecretsUsedBy(src, "a").Names, "anchor from another job, and an anchor used twice")
129	assert.Equal(t, []string{"CHAIN_ROOT", "FROM_OTHER_JOB"}, usecase.SecretsUsedBy(src, "c").Names, "aliases inside a flow sequence")
130}
131
132// Deeply nested YAML is read as "all secrets" rather than followed forever.
133func TestSecretsUsedByDeepNestingIsAll(t *testing.T) {
134	t.Parallel()
135	var b strings.Builder
136	b.WriteString("on: push\njobs:\n  a:\n    runs-on: x\n    steps:\n      - run: echo\n        env: ")
137	b.WriteString(strings.Repeat("[", 400) + strings.Repeat("]", 400) + "\n")
138	assert.True(t, usecase.SecretsUsedBy(b.String(), "a").All)
139}
140
141// The analysis is made once per run and job, kept apart from the run it
142// belongs to, and dropped with it. It holds names only.
143func TestSecretUseIsCachedPerRunAndJob(t *testing.T) {
144	src := "on: push\njobs:\n  a:\n    runs-on: x\n    steps:\n      - run: echo ${{ secrets.A }}\n  b:\n    runs-on: x\n    steps:\n      - run: echo ${{ secrets.B }}\n"
145	c := usecase.NewSecretUseCache(4)
146	before := usecase.SecretWalks()
147	for range 5 {
148		assert.Equal(t, []string{"A"}, c.Use(1, src, "a").Names)
149	}
150	assert.EqualValues(t, 1, usecase.SecretWalks()-before, "five legs, one walk")
151	assert.Equal(t, []string{"B"}, c.Use(1, src, "b").Names, "another job of the run has its own")
152	assert.Equal(t, []string{"A"}, c.Use(2, src, "a").Names, "another run too")
153	assert.EqualValues(t, 3, usecase.SecretWalks()-before)
154	c.Drop(1)
155	c.Use(1, src, "a")
156	assert.EqualValues(t, 4, usecase.SecretWalks()-before, "dropped with the run")
157}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close