detached 5f78aaa

feat(executor): inner steps inherit the timeout of the step that uses the action

commit
5f78aaa9e2c66f185392cb5554f5d9f92f7b004c
author
brice · · 2026-10-14 03:27 UTC
parent
3ee1c67
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +25 -1
M internal/executor/step.go +5 -0
@@ -6,6 +6,7 @@ import (
66 "context"
77 "errors"
88 "strings"
9+ "time"
910
1011 "bnema.dev/ermite-actions/action"
1112 "bnema.dev/ermite-actions/expr"
@@ -50,6 +51,10 @@ type stepRun struct {
5051 scope stepScope
5152 name string
5253 timedOut timedOutFlag
54+ // deadline is when the timeout-minutes of the step, or of the step around it,
55+ // ends: zero for none. An inner step of a composite action inherits what is
56+ // left of it (ExecutionContext.CreateEmbeddedChild, :498).
57+ deadline time.Time
5358 }
5459
5560 // depth is the number of composite actions around the step.
@@ -6,6 +6,7 @@ import (
6 "context"6 "context"
7 "errors"7 "errors"
8 "strings"8 "strings"
9+ "time"
9 10
10 "bnema.dev/ermite-actions/action"11 "bnema.dev/ermite-actions/action"
11 "bnema.dev/ermite-actions/expr"12 "bnema.dev/ermite-actions/expr"
@@ -50,6 +51,10 @@ type stepRun struct {
50 scope stepScope51 scope stepScope
51 name string52 name string
52 timedOut timedOutFlag53 timedOut timedOutFlag
54+ // deadline is when the timeout-minutes of the step, or of the step around it,
55+ // ends: zero for none. An inner step of a composite action inherits what is
56+ // left of it (ExecutionContext.CreateEmbeddedChild, :498).
57+ deadline time.Time
53 }58 }
54 59
55 // depth is the number of composite actions around the step.60 // depth is the number of composite actions around the step.
M internal/executor/step_run.go +20 -1
@@ -5,6 +5,7 @@ package executor
55 import (
66 "context"
77 "sync"
8+ "time"
89
910 "bnema.dev/ermite-actions/action"
1011 "bnema.dev/ermite-actions/expr"
@@ -303,6 +304,16 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult {
303304 stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }()
304305 if q.setup == nil {
305306 stopTimeout, minutes = j.armStepTimeout(s, closeCancel)
307+ if minutes > 0 {
308+ s.deadline = j.now().Add(time.Duration(minutes)*time.Minute + j.cfg.StepTimeoutSkew)
309+ }
310+ if p := s.parent; p != nil && !p.deadline.IsZero() {
311+ // The inner step has the time that is left to the step around it, and
312+ // no timeout of its own (the schema refuses one).
313+ s.deadline = p.deadline
314+ t := time.AfterFunc(time.Until(p.deadline), func() { s.timedOut.Store(true); closeCancel() })
315+ stopTimeout = func() { t.Stop() }
316+ }
306317 }
307318 if q.setup != nil {
308319 r = q.setup(s, stepCancel)
@@ -312,7 +323,15 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult {
312323 stopTimeout()
313324 close(stopWatch)
314325 watch.Wait()
315- r = j.stepTimedOut(s, r, minutes)
326+ if minutes > 0 {
327+ r = j.stepTimedOut(s, r, minutes)
328+ }
329+ if s.parent != nil && r == resCanceled && s.timedOut.Load() && !j.cancelRequested() {
330+ // CompositeActionHandler.RunStepAsync (:497-506): an inner step stopped by
331+ // the timeout it inherited fails with a message that names no step.
332+ j.errorf(num, "The action has timed out.")
333+ r = resFailed
334+ }
316335
317336 // Merge the result of the commands (StepsRunner.cs:347-351).
318337 if s.cmdResult != nil {
@@ -5,6 +5,7 @@ package executor
5 import (5 import (
6 "context"6 "context"
7 "sync"7 "sync"
8+ "time"
8 9
9 "bnema.dev/ermite-actions/action"10 "bnema.dev/ermite-actions/action"
10 "bnema.dev/ermite-actions/expr"11 "bnema.dev/ermite-actions/expr"
@@ -303,6 +304,16 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult {
303 stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }()304 stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }()
304 if q.setup == nil {305 if q.setup == nil {
305 stopTimeout, minutes = j.armStepTimeout(s, closeCancel)306 stopTimeout, minutes = j.armStepTimeout(s, closeCancel)
307+ if minutes > 0 {
308+ s.deadline = j.now().Add(time.Duration(minutes)*time.Minute + j.cfg.StepTimeoutSkew)
309+ }
310+ if p := s.parent; p != nil && !p.deadline.IsZero() {
311+ // The inner step has the time that is left to the step around it, and
312+ // no timeout of its own (the schema refuses one).
313+ s.deadline = p.deadline
314+ t := time.AfterFunc(time.Until(p.deadline), func() { s.timedOut.Store(true); closeCancel() })
315+ stopTimeout = func() { t.Stop() }
316+ }
306 }317 }
307 if q.setup != nil {318 if q.setup != nil {
308 r = q.setup(s, stepCancel)319 r = q.setup(s, stepCancel)
@@ -312,7 +323,15 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult {
312 stopTimeout()323 stopTimeout()
313 close(stopWatch)324 close(stopWatch)
314 watch.Wait()325 watch.Wait()
315- r = j.stepTimedOut(s, r, minutes)326+ if minutes > 0 {
327+ r = j.stepTimedOut(s, r, minutes)
328+ }
329+ if s.parent != nil && r == resCanceled && s.timedOut.Load() && !j.cancelRequested() {
330+ // CompositeActionHandler.RunStepAsync (:497-506): an inner step stopped by
331+ // the timeout it inherited fails with a message that names no step.
332+ j.errorf(num, "The action has timed out.")
333+ r = resFailed
334+ }
316 335
317 // Merge the result of the commands (StepsRunner.cs:347-351).336 // Merge the result of the commands (StepsRunner.cs:347-351).
318 if s.cmdResult != nil {337 if s.cmdResult != nil {

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