detached 8111cdb

test(executor): pre and post of repository and docker actions, token not in the environment

commit
8111cdbb001915ad9c6d16ba92a08df9c72a8d91
author
brice · · 2026-10-13 16:41 UTC
parent
0ee31e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +75 -10
M internal/executor/actions_secrets.go +8 -10
@@ -2,21 +2,19 @@
22
33 package executor
44
5-import (
6- "encoding/base64"
7- "sync"
8-)
5+import "encoding/base64"
96
10-// extraSecrets are the values the job registered in the masker after it
11-// started: the archive tokens of the actions (ActionManager.cs:1130,1650).
12-// Phase G's add-mask feeds the same masker; whoever merges second keeps both.
13-var extraSecretsMu sync.Mutex
7+// extraSecrets (job.extraSecrets) are the values registered in the masker after
8+// the job started: the archive tokens of the actions (ActionManager.cs:1130,
9+// 1650). Integration point with the add-mask command of phase G, which feeds
10+// the same masker: it should append to j.extraSecrets under j.mu and rebuild
11+// the masker like addSecrets does.
1412
1513 // addSecrets registers values with the masker, and the base64 of
1614 // "x-access-token:<value>" for tokens (ActionManager.CreateAuthHeader).
1715 func (j *job) addSecrets(values ...string) {
18- extraSecretsMu.Lock()
19- defer extraSecretsMu.Unlock()
16+ j.mu.Lock()
17+ defer j.mu.Unlock()
2018 j.extraSecrets = append(j.extraSecrets, values...)
2119 all := append(j.state.Secrets(), j.extraSecrets...)
2220 j.masker.Store(newMasker(all...))
@@ -2,21 +2,19 @@
2 2
3 package executor3 package executor
4 4
5-import (5+import "encoding/base64"
6- "encoding/base64"
7- "sync"
8-)
9 6
10-// extraSecrets are the values the job registered in the masker after it7+// extraSecrets (job.extraSecrets) are the values registered in the masker after
11-// started: the archive tokens of the actions (ActionManager.cs:1130,1650).8+// the job started: the archive tokens of the actions (ActionManager.cs:1130,
12-// Phase G's add-mask feeds the same masker; whoever merges second keeps both.9+// 1650). Integration point with the add-mask command of phase G, which feeds
13-var extraSecretsMu sync.Mutex10+// the same masker: it should append to j.extraSecrets under j.mu and rebuild
11+// the masker like addSecrets does.
14 12
15 // addSecrets registers values with the masker, and the base64 of13 // addSecrets registers values with the masker, and the base64 of
16 // "x-access-token:<value>" for tokens (ActionManager.CreateAuthHeader).14 // "x-access-token:<value>" for tokens (ActionManager.CreateAuthHeader).
17 func (j *job) addSecrets(values ...string) {15 func (j *job) addSecrets(values ...string) {
18- extraSecretsMu.Lock()16+ j.mu.Lock()
19- defer extraSecretsMu.Unlock()17+ defer j.mu.Unlock()
20 j.extraSecrets = append(j.extraSecrets, values...)18 j.extraSecrets = append(j.extraSecrets, values...)
21 all := append(j.state.Secrets(), j.extraSecrets...)19 all := append(j.state.Secrets(), j.extraSecrets...)
22 j.masker.Store(newMasker(all...))20 j.masker.Store(newMasker(all...))
M internal/executor/actions_test.go +67 -0
@@ -590,3 +590,70 @@ func TestMissingManifestInDownloadedAction(t *testing.T) {
590590 t.Fatalf("%+v\n%s", res, logOf(rec.steps[0]))
591591 }
592592 }
593+
594+func TestRepositoryActionPrePostAndState(t *testing.T) {
595+ act := "name: pp\nruns:\n using: node20\n pre: pre.sh\n main: main.sh\n post: post.sh\n pre-if: always()\n post-if: success()\n"
596+ sc := "echo \"[$P] state=[$STATE_k]\"\necho \"::save-state name=k::from-$P\"\n"
597+ forge := newForge(t, map[string][]byte{"/tarball/o/pp/v1": tarOf(t, "o-pp-x", files{
598+ "action.yml": act, "pre.sh": "P=pre\n" + sc, "main.sh": "P=main\n" + sc, "post.sh": "P=post\n" + sc})})
599+ rec, res := actionRun(t, files{}, " - run: echo first\n - uses: o/pp@v1\n - run: echo last\n", func(c *Config) { c.Actions.Resolver = forge.resolver("") })
600+ if res.Conclusion != ConclusionSuccess {
601+ t.Fatalf("%+v %v", res, rec.names())
602+ }
603+ want := "Set up job|Pre Run o/pp@v1|Run echo first|Run o/pp@v1|Run echo last|Post Run o/pp@v1|Complete job"
604+ if got := strings.Join(rec.names(), "|"); got != want {
605+ t.Fatalf("%s", got)
606+ }
607+ if got := logOf(rec.steps[1]); !strings.Contains(got, "[pre] state=[]") || !strings.Contains(got, "##[group]Run o/pp@v1") {
608+ t.Errorf("pre:\n%s", got)
609+ }
610+ if got := logOf(rec.steps[3]); !strings.Contains(got, "[main] state=[from-pre]") {
611+ t.Errorf("main sees the state of pre:\n%s", got)
612+ }
613+ if got := logOf(rec.steps[5]); !strings.Contains(got, "Post job cleanup.") || !strings.Contains(got, "[post] state=[from-main]") {
614+ t.Errorf("post:\n%s", got)
615+ }
616+}
617+
618+func TestDockerPrePostEntrypoints(t *testing.T) {
619+ forge := newForge(t, map[string][]byte{"/tarball/o/dk/v1": tarOf(t, "o-dk-x", files{
620+ "action.yml": "name: dk\nruns:\n using: docker\n image: docker://alpine:3\n pre-entrypoint: /pre.sh\n entrypoint: /main.sh\n post-entrypoint: /post.sh\n"})})
621+ d, lf := fakeDocker(t, 0)
622+ rec, res := actionRun(t, files{}, " - uses: o/dk@v1\n", func(c *Config) {
623+ c.Actions.DockerPath = d
624+ c.Actions.Resolver = forge.resolver("")
625+ })
626+ if res.Conclusion != ConclusionSuccess {
627+ t.Fatalf("%+v %v", res, rec.names())
628+ }
629+ want := "Set up job|Pull alpine:3|Pre Run o/dk@v1|Run o/dk@v1|Post Run o/dk@v1|Complete job"
630+ if got := strings.Join(rec.names(), "|"); got != want {
631+ t.Fatalf("%s", got)
632+ }
633+ var eps []string
634+ for _, c := range calls(t, lf) {
635+ if c[0] != "run" {
636+ continue
637+ }
638+ for i, a := range c {
639+ if a == "--entrypoint" {
640+ eps = append(eps, c[i+1])
641+ }
642+ }
643+ }
644+ if strings.Join(eps, ",") != "/pre.sh,/main.sh,/post.sh" {
645+ t.Errorf("entrypoints %v", eps)
646+ }
647+}
648+
649+func TestRepositoryActionInputsAreMaskedAndTokenNotInEnv(t *testing.T) {
650+ forge := newForge(t, map[string][]byte{"/tarball/o/t/v1": tarOf(t, "o-t-x", files{
651+ "action.yml": nodeAction, "main.sh": "env | grep -c archive-secret || true\n"})})
652+ rec, res := actionRun(t, files{}, " - uses: o/t@v1\n", func(c *Config) { c.Actions.Resolver = forge.resolver("archive-secret") })
653+ if res.Conclusion != ConclusionSuccess {
654+ t.Fatalf("%+v", res)
655+ }
656+ if got := logOf(rec.steps[1]); !strings.HasSuffix(got, "\n0") {
657+ t.Errorf("the archive token is not in the environment of actions:\n%s", got)
658+ }
659+}
@@ -590,3 +590,70 @@ func TestMissingManifestInDownloadedAction(t *testing.T) {
590 t.Fatalf("%+v\n%s", res, logOf(rec.steps[0]))590 t.Fatalf("%+v\n%s", res, logOf(rec.steps[0]))
591 }591 }
592 }592 }
593+
594+func TestRepositoryActionPrePostAndState(t *testing.T) {
595+ act := "name: pp\nruns:\n using: node20\n pre: pre.sh\n main: main.sh\n post: post.sh\n pre-if: always()\n post-if: success()\n"
596+ sc := "echo \"[$P] state=[$STATE_k]\"\necho \"::save-state name=k::from-$P\"\n"
597+ forge := newForge(t, map[string][]byte{"/tarball/o/pp/v1": tarOf(t, "o-pp-x", files{
598+ "action.yml": act, "pre.sh": "P=pre\n" + sc, "main.sh": "P=main\n" + sc, "post.sh": "P=post\n" + sc})})
599+ rec, res := actionRun(t, files{}, " - run: echo first\n - uses: o/pp@v1\n - run: echo last\n", func(c *Config) { c.Actions.Resolver = forge.resolver("") })
600+ if res.Conclusion != ConclusionSuccess {
601+ t.Fatalf("%+v %v", res, rec.names())
602+ }
603+ want := "Set up job|Pre Run o/pp@v1|Run echo first|Run o/pp@v1|Run echo last|Post Run o/pp@v1|Complete job"
604+ if got := strings.Join(rec.names(), "|"); got != want {
605+ t.Fatalf("%s", got)
606+ }
607+ if got := logOf(rec.steps[1]); !strings.Contains(got, "[pre] state=[]") || !strings.Contains(got, "##[group]Run o/pp@v1") {
608+ t.Errorf("pre:\n%s", got)
609+ }
610+ if got := logOf(rec.steps[3]); !strings.Contains(got, "[main] state=[from-pre]") {
611+ t.Errorf("main sees the state of pre:\n%s", got)
612+ }
613+ if got := logOf(rec.steps[5]); !strings.Contains(got, "Post job cleanup.") || !strings.Contains(got, "[post] state=[from-main]") {
614+ t.Errorf("post:\n%s", got)
615+ }
616+}
617+
618+func TestDockerPrePostEntrypoints(t *testing.T) {
619+ forge := newForge(t, map[string][]byte{"/tarball/o/dk/v1": tarOf(t, "o-dk-x", files{
620+ "action.yml": "name: dk\nruns:\n using: docker\n image: docker://alpine:3\n pre-entrypoint: /pre.sh\n entrypoint: /main.sh\n post-entrypoint: /post.sh\n"})})
621+ d, lf := fakeDocker(t, 0)
622+ rec, res := actionRun(t, files{}, " - uses: o/dk@v1\n", func(c *Config) {
623+ c.Actions.DockerPath = d
624+ c.Actions.Resolver = forge.resolver("")
625+ })
626+ if res.Conclusion != ConclusionSuccess {
627+ t.Fatalf("%+v %v", res, rec.names())
628+ }
629+ want := "Set up job|Pull alpine:3|Pre Run o/dk@v1|Run o/dk@v1|Post Run o/dk@v1|Complete job"
630+ if got := strings.Join(rec.names(), "|"); got != want {
631+ t.Fatalf("%s", got)
632+ }
633+ var eps []string
634+ for _, c := range calls(t, lf) {
635+ if c[0] != "run" {
636+ continue
637+ }
638+ for i, a := range c {
639+ if a == "--entrypoint" {
640+ eps = append(eps, c[i+1])
641+ }
642+ }
643+ }
644+ if strings.Join(eps, ",") != "/pre.sh,/main.sh,/post.sh" {
645+ t.Errorf("entrypoints %v", eps)
646+ }
647+}
648+
649+func TestRepositoryActionInputsAreMaskedAndTokenNotInEnv(t *testing.T) {
650+ forge := newForge(t, map[string][]byte{"/tarball/o/t/v1": tarOf(t, "o-t-x", files{
651+ "action.yml": nodeAction, "main.sh": "env | grep -c archive-secret || true\n"})})
652+ rec, res := actionRun(t, files{}, " - uses: o/t@v1\n", func(c *Config) { c.Actions.Resolver = forge.resolver("archive-secret") })
653+ if res.Conclusion != ConclusionSuccess {
654+ t.Fatalf("%+v", res)
655+ }
656+ if got := logOf(rec.steps[1]); !strings.HasSuffix(got, "\n0") {
657+ t.Errorf("the archive token is not in the environment of actions:\n%s", got)
658+ }
659+}

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 4

Tags 11

↑↓ moveenter openesc close