detached 9845c39

fix(executor): insert the github keys added by the runner at their sorted place

commit
9845c398f5f916bc280ef322622006911d77dbdb
author
brice · · 2026-10-12 23:10 UTC
parent
e1538eb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +51 -4
M internal/executor/contexts.go +33 -4
@@ -77,6 +77,35 @@ func setKey(d *expr.Dictionary, key string, v expr.Value) *expr.Dictionary {
7777 return out
7878 }
7979
80+// setKeySorted is setKey for the github context: a key that is not there yet
81+// goes before the first key that sorts after it (bytewise; "token" stays first)
82+// instead of at the end, so that the keys the runner adds (workspace,
83+// secret_source, the file commands...) sit where the ermite-conformance
84+// recordings list them. Note that the recordings are normalised with sorted
85+// keys, so they show the sorted order, not necessarily the insertion order of
86+// GitHub's runner (SetGitHubContext appends). Keys that exist are replaced in
87+// place.
88+func setKeySorted(d *expr.Dictionary, key string, v expr.Value) *expr.Dictionary {
89+ for _, p := range d.Pairs() {
90+ if strings.EqualFold(p.Key, key) {
91+ return setKey(d, key, v)
92+ }
93+ }
94+ out := expr.NewDictionary()
95+ done := false
96+ for _, p := range d.Pairs() {
97+ if !done && p.Key > key && !strings.EqualFold(p.Key, "token") {
98+ out.Add(key, v)
99+ done = true
100+ }
101+ out.Add(p.Key, p.Value)
102+ }
103+ if !done {
104+ out.Add(key, v)
105+ }
106+ return out
107+}
108+
80109 func str(d *expr.Dictionary, key string) string {
81110 if v, ok := d.Get(key); ok {
82111 if s, ok := v.(expr.String); ok {
@@ -222,7 +251,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
222251 }
223252 }
224253 if str(gh, "job") == "" && jobKey != "" {
225- gh = setKey(gh, "job", expr.String(jobKey))
254+ gh = setKeySorted(gh, "job", expr.String(jobKey))
226255 }
227256 repo := repoName(str(gh, "repository"))
228257 if repo == "" {
@@ -233,7 +262,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
233262 }
234263 home := "/home/runner"
235264 s.layout = NewLayout(home, repo, "")
236- s.github = setKey(gh, "workspace", expr.String(s.layout.Workspace))
265+ s.github = setKeySorted(gh, "workspace", expr.String(s.layout.Workspace))
237266 return s, s.layout, s.initRest(root, matrixJSON, r)
238267 }
239268
@@ -241,7 +270,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
241270 // must be called before the first step.
242271 func (s *State) WithLayout(l Layout) {
243272 s.layout = l
244- s.github = setKey(s.github, "workspace", expr.String(l.Workspace))
273+ s.github = setKeySorted(s.github, "workspace", expr.String(l.Workspace))
245274 s.runner = setKey(s.runner, "temp", expr.String(l.Temp))
246275 s.runner = setKey(s.runner, "tool_cache", expr.String(l.ToolCache))
247276 s.runner = setKey(s.runner, "workspace", expr.String(l.RunnerWorkspace))
@@ -315,7 +344,7 @@ func (s *State) initRest(root *expr.Dictionary, matrixJSON string, r RunnerInfo)
315344 func (s *State) Layout() Layout { return s.layout }
316345
317346 // SetGitHub sets a key of the github context (SetGitHubContext).
318-func (s *State) SetGitHub(k, v string) { s.github = setKey(s.github, k, expr.String(v)) }
347+func (s *State) SetGitHub(k, v string) { s.github = setKeySorted(s.github, k, expr.String(v)) }
319348
320349 // GitHub returns a string key of the github context ("" when absent).
321350 func (s *State) GitHub(k string) string { return str(s.github, k) }
@@ -77,6 +77,35 @@ func setKey(d *expr.Dictionary, key string, v expr.Value) *expr.Dictionary {
77 return out77 return out
78 }78 }
79 79
80+// setKeySorted is setKey for the github context: a key that is not there yet
81+// goes before the first key that sorts after it (bytewise; "token" stays first)
82+// instead of at the end, so that the keys the runner adds (workspace,
83+// secret_source, the file commands...) sit where the ermite-conformance
84+// recordings list them. Note that the recordings are normalised with sorted
85+// keys, so they show the sorted order, not necessarily the insertion order of
86+// GitHub's runner (SetGitHubContext appends). Keys that exist are replaced in
87+// place.
88+func setKeySorted(d *expr.Dictionary, key string, v expr.Value) *expr.Dictionary {
89+ for _, p := range d.Pairs() {
90+ if strings.EqualFold(p.Key, key) {
91+ return setKey(d, key, v)
92+ }
93+ }
94+ out := expr.NewDictionary()
95+ done := false
96+ for _, p := range d.Pairs() {
97+ if !done && p.Key > key && !strings.EqualFold(p.Key, "token") {
98+ out.Add(key, v)
99+ done = true
100+ }
101+ out.Add(p.Key, p.Value)
102+ }
103+ if !done {
104+ out.Add(key, v)
105+ }
106+ return out
107+}
108+
80 func str(d *expr.Dictionary, key string) string {109 func str(d *expr.Dictionary, key string) string {
81 if v, ok := d.Get(key); ok {110 if v, ok := d.Get(key); ok {
82 if s, ok := v.(expr.String); ok {111 if s, ok := v.(expr.String); ok {
@@ -222,7 +251,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
222 }251 }
223 }252 }
224 if str(gh, "job") == "" && jobKey != "" {253 if str(gh, "job") == "" && jobKey != "" {
225- gh = setKey(gh, "job", expr.String(jobKey))254+ gh = setKeySorted(gh, "job", expr.String(jobKey))
226 }255 }
227 repo := repoName(str(gh, "repository"))256 repo := repoName(str(gh, "repository"))
228 if repo == "" {257 if repo == "" {
@@ -233,7 +262,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
233 }262 }
234 home := "/home/runner"263 home := "/home/runner"
235 s.layout = NewLayout(home, repo, "")264 s.layout = NewLayout(home, repo, "")
236- s.github = setKey(gh, "workspace", expr.String(s.layout.Workspace))265+ s.github = setKeySorted(gh, "workspace", expr.String(s.layout.Workspace))
237 return s, s.layout, s.initRest(root, matrixJSON, r)266 return s, s.layout, s.initRest(root, matrixJSON, r)
238 }267 }
239 268
@@ -241,7 +270,7 @@ func NewState(contextsJSON, matrixJSON, jobKey string, r RunnerInfo, repoFallbac
241 // must be called before the first step.270 // must be called before the first step.
242 func (s *State) WithLayout(l Layout) {271 func (s *State) WithLayout(l Layout) {
243 s.layout = l272 s.layout = l
244- s.github = setKey(s.github, "workspace", expr.String(l.Workspace))273+ s.github = setKeySorted(s.github, "workspace", expr.String(l.Workspace))
245 s.runner = setKey(s.runner, "temp", expr.String(l.Temp))274 s.runner = setKey(s.runner, "temp", expr.String(l.Temp))
246 s.runner = setKey(s.runner, "tool_cache", expr.String(l.ToolCache))275 s.runner = setKey(s.runner, "tool_cache", expr.String(l.ToolCache))
247 s.runner = setKey(s.runner, "workspace", expr.String(l.RunnerWorkspace))276 s.runner = setKey(s.runner, "workspace", expr.String(l.RunnerWorkspace))
@@ -315,7 +344,7 @@ func (s *State) initRest(root *expr.Dictionary, matrixJSON string, r RunnerInfo)
315 func (s *State) Layout() Layout { return s.layout }344 func (s *State) Layout() Layout { return s.layout }
316 345
317 // SetGitHub sets a key of the github context (SetGitHubContext).346 // SetGitHub sets a key of the github context (SetGitHubContext).
318-func (s *State) SetGitHub(k, v string) { s.github = setKey(s.github, k, expr.String(v)) }347+func (s *State) SetGitHub(k, v string) { s.github = setKeySorted(s.github, k, expr.String(v)) }
319 348
320 // GitHub returns a string key of the github context ("" when absent).349 // GitHub returns a string key of the github context ("" when absent).
321 func (s *State) GitHub(k string) string { return str(s.github, k) }350 func (s *State) GitHub(k string) string { return str(s.github, k) }
M internal/executor/contexts_test.go +18 -0
@@ -251,3 +251,21 @@ func TestWithLayout(t *testing.T) {
251251 }
252252 }
253253 }
254+
255+func TestGitHubKeysAddedByTheRunnerKeepTheSortedOrder(t *testing.T) {
256+ // A server that lists its keys sorted, as the recordings do: the keys the
257+ // runner adds land at their sorted place, replaced keys stay, token is first.
258+ s, _, err := NewState(`{"github":{"actor":"a","event_name":"push","job":"j","repository":"o/r","sha":"s","zzz":"1"}}`, "", "j", RunnerInfo{}, "")
259+ if err != nil {
260+ t.Fatal(err)
261+ }
262+ for _, k := range []string{"secret_source", "artifacts", "action", "event_path"} {
263+ s.SetGitHub(k, "x")
264+ }
265+ s.SetGitHub("sha", "t")
266+ got := strings.Join(s.Values(nil)["github"].(*expr.Dictionary).Keys(), ",")
267+ want := "token,action,actor,artifacts,event_name,event_path,job,repository,secret_source,sha,workspace,zzz"
268+ if got != want {
269+ t.Errorf("keys\n got %s\nwant %s", got, want)
270+ }
271+}
@@ -251,3 +251,21 @@ func TestWithLayout(t *testing.T) {
251 }251 }
252 }252 }
253 }253 }
254+
255+func TestGitHubKeysAddedByTheRunnerKeepTheSortedOrder(t *testing.T) {
256+ // A server that lists its keys sorted, as the recordings do: the keys the
257+ // runner adds land at their sorted place, replaced keys stay, token is first.
258+ s, _, err := NewState(`{"github":{"actor":"a","event_name":"push","job":"j","repository":"o/r","sha":"s","zzz":"1"}}`, "", "j", RunnerInfo{}, "")
259+ if err != nil {
260+ t.Fatal(err)
261+ }
262+ for _, k := range []string{"secret_source", "artifacts", "action", "event_path"} {
263+ s.SetGitHub(k, "x")
264+ }
265+ s.SetGitHub("sha", "t")
266+ got := strings.Join(s.Values(nil)["github"].(*expr.Dictionary).Keys(), ",")
267+ want := "token,action,actor,artifacts,event_name,event_path,job,repository,secret_source,sha,workspace,zzz"
268+ if got != want {
269+ t.Errorf("keys\n got %s\nwant %s", got, want)
270+ }
271+}

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