Skip to content

Commit e2488aa

Browse files
intel352claude
andcommitted
fix: wire pipeline context into GitHub step Execute methods for dynamic field resolution
All three step Execute methods were discarding triggerData, stepOutputs, and current, making it impossible to use pipeline-dynamic values (e.g. commit SHA from a trigger, run_id from a prior step) in config fields. Added resolve.go with a resolveField() helper that replaces {{.field}}, {{.steps.<name>.<field>}}, and {{.current.<field>}} references using the pipeline context maps passed to Execute. Updated actionTriggerStep (owner, repo, workflow, ref, inputs), actionStatusStep (owner, repo, run_id), and createCheckStep (owner, repo, sha) to resolve fields at execution time. The actionStatusStep also stores run_id as a raw string when a template reference is present at parse time, deferring integer conversion to Execute. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a370fab commit e2488aa

4 files changed

Lines changed: 168 additions & 36 deletions

File tree

internal/resolve.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// resolveField performs basic template resolution on value, replacing
9+
// {{.field}} references with values looked up from triggerData, stepOutputs,
10+
// and current (in that priority order).
11+
//
12+
// Supported reference forms:
13+
//
14+
// {{.field}} — look up "field" in triggerData
15+
// {{.steps.stepName.field}} — look up stepOutputs["stepName"]["field"]
16+
// {{.current.field}} — look up "field" in current
17+
//
18+
// If the placeholder cannot be resolved the original placeholder text is left
19+
// in place so misconfiguration is visible rather than silently swallowed.
20+
func resolveField(value string, triggerData map[string]any, stepOutputs map[string]map[string]any, current map[string]any) string {
21+
if !strings.Contains(value, "{{") {
22+
return value
23+
}
24+
25+
result := value
26+
// Iterate until no more replacements can be made (handles multiple refs).
27+
for strings.Contains(result, "{{") {
28+
start := strings.Index(result, "{{")
29+
end := strings.Index(result, "}}")
30+
if end < start {
31+
break
32+
}
33+
placeholder := result[start : end+2]
34+
inner := strings.TrimSpace(result[start+2 : end])
35+
36+
resolved, ok := lookupRef(inner, triggerData, stepOutputs, current)
37+
if ok {
38+
result = strings.Replace(result, placeholder, fmt.Sprintf("%v", resolved), 1)
39+
} else {
40+
// Leave the unresolvable placeholder and stop to avoid an infinite loop.
41+
break
42+
}
43+
}
44+
return result
45+
}
46+
47+
// lookupRef resolves a single template reference (the content between {{ and }}).
48+
func lookupRef(ref string, triggerData map[string]any, stepOutputs map[string]map[string]any, current map[string]any) (any, bool) {
49+
// Strip leading dot.
50+
ref = strings.TrimPrefix(ref, ".")
51+
52+
parts := strings.SplitN(ref, ".", 3)
53+
54+
switch parts[0] {
55+
case "steps":
56+
// {{.steps.<stepName>.<field>}}
57+
if len(parts) < 3 {
58+
return nil, false
59+
}
60+
stepName, field := parts[1], parts[2]
61+
if stepOutputs == nil {
62+
return nil, false
63+
}
64+
outputs, ok := stepOutputs[stepName]
65+
if !ok {
66+
return nil, false
67+
}
68+
v, ok := outputs[field]
69+
return v, ok
70+
71+
case "current":
72+
// {{.current.<field>}}
73+
if len(parts) < 2 {
74+
return nil, false
75+
}
76+
field := strings.Join(parts[1:], ".")
77+
if current == nil {
78+
return nil, false
79+
}
80+
v, ok := current[field]
81+
return v, ok
82+
83+
default:
84+
// {{.field}} — look up directly in triggerData.
85+
field := strings.Join(parts, ".")
86+
if triggerData == nil {
87+
return nil, false
88+
}
89+
v, ok := triggerData[field]
90+
return v, ok
91+
}
92+
}

internal/step_action_status.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"strconv"
8+
"strings"
89
"time"
910

1011
sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk"
@@ -33,6 +34,7 @@ type actionStatusConfig struct {
3334
Owner string `yaml:"owner"`
3435
Repo string `yaml:"repo"`
3536
RunID int64 `yaml:"run_id"`
37+
RunIDRaw string // raw string value for dynamic {{.field}} resolution
3638
Token string `yaml:"token"`
3739
Wait bool `yaml:"wait"`
3840
PollInterval time.Duration `yaml:"poll_interval"`
@@ -70,6 +72,8 @@ func parseActionStatusConfig(raw map[string]any) (actionStatusConfig, error) {
7072
}
7173

7274
// run_id can be provided as int, int64, float64, or string.
75+
// When the string contains a template reference (e.g. {{.steps.trigger.run_id}})
76+
// the literal value is stored in RunIDRaw and resolved at Execute time.
7377
switch v := raw["run_id"].(type) {
7478
case int:
7579
cfg.RunID = int64(v)
@@ -79,14 +83,18 @@ func parseActionStatusConfig(raw map[string]any) (actionStatusConfig, error) {
7983
cfg.RunID = int64(v)
8084
case string:
8185
if v != "" {
82-
n, err := strconv.ParseInt(v, 10, 64)
83-
if err != nil {
84-
return cfg, fmt.Errorf("config.run_id is not a valid integer: %w", err)
86+
if strings.Contains(v, "{{") {
87+
cfg.RunIDRaw = v
88+
} else {
89+
n, err := strconv.ParseInt(v, 10, 64)
90+
if err != nil {
91+
return cfg, fmt.Errorf("config.run_id is not a valid integer: %w", err)
92+
}
93+
cfg.RunID = n
8594
}
86-
cfg.RunID = n
8795
}
8896
}
89-
if cfg.RunID == 0 {
97+
if cfg.RunID == 0 && cfg.RunIDRaw == "" {
9098
return cfg, fmt.Errorf("config.run_id is required")
9199
}
92100

@@ -118,27 +126,47 @@ func parseActionStatusConfig(raw map[string]any) (actionStatusConfig, error) {
118126
}
119127

120128
// Execute checks the status of the configured workflow run.
129+
// triggerData, stepOutputs, and current are used to resolve dynamic field
130+
// references (e.g. {{.steps.trigger.run_id}}) in owner, repo, and run_id.
121131
// When wait=true it polls until the run completes or the timeout elapses.
122132
func (s *actionStatusStep) Execute(
123133
ctx context.Context,
124-
_ map[string]any,
125-
_ map[string]map[string]any,
126-
_ map[string]any,
134+
triggerData map[string]any,
135+
stepOutputs map[string]map[string]any,
136+
current map[string]any,
127137
_ map[string]any,
128138
) (*sdk.StepResult, error) {
129139
token := s.config.Token
130140
if token == "" {
131141
return errorResult("GITHUB_TOKEN is not configured"), nil
132142
}
133143

144+
// Resolve dynamic owner / repo.
145+
owner := resolveField(s.config.Owner, triggerData, stepOutputs, current)
146+
repo := resolveField(s.config.Repo, triggerData, stepOutputs, current)
147+
148+
// Resolve run_id — may be a static int or a dynamic template reference.
149+
runID := s.config.RunID
150+
if s.config.RunIDRaw != "" {
151+
resolved := resolveField(s.config.RunIDRaw, triggerData, stepOutputs, current)
152+
n, err := strconv.ParseInt(resolved, 10, 64)
153+
if err != nil {
154+
return errorResult(fmt.Sprintf("run_id resolved to non-integer value %q: %v", resolved, err)), nil
155+
}
156+
runID = n
157+
}
158+
if runID == 0 {
159+
return errorResult("run_id resolved to zero — check pipeline context"), nil
160+
}
161+
134162
if !s.config.Wait {
135-
return s.fetchStatus(ctx, token)
163+
return s.fetchStatusDynamic(ctx, owner, repo, runID, token)
136164
}
137165

138166
// Poll with timeout.
139167
deadline := time.Now().Add(s.config.Timeout)
140168
for {
141-
result, err := s.fetchStatus(ctx, token)
169+
result, err := s.fetchStatusDynamic(ctx, owner, repo, runID, token)
142170
if err != nil {
143171
return nil, err
144172
}
@@ -149,7 +177,7 @@ func (s *actionStatusStep) Execute(
149177
}
150178

151179
if time.Now().After(deadline) {
152-
return errorResult(fmt.Sprintf("timeout waiting for workflow run %d after %s", s.config.RunID, s.config.Timeout)), nil
180+
return errorResult(fmt.Sprintf("timeout waiting for workflow run %d after %s", runID, s.config.Timeout)), nil
153181
}
154182

155183
select {
@@ -160,9 +188,10 @@ func (s *actionStatusStep) Execute(
160188
}
161189
}
162190

163-
// fetchStatus retrieves the current state of the workflow run from the GitHub API.
164-
func (s *actionStatusStep) fetchStatus(ctx context.Context, token string) (*sdk.StepResult, error) {
165-
run, err := s.ghClient.GetWorkflowRun(ctx, s.config.Owner, s.config.Repo, s.config.RunID, token)
191+
// fetchStatusDynamic retrieves the current state of a workflow run from the
192+
// GitHub API using caller-supplied (already-resolved) owner, repo, and runID.
193+
func (s *actionStatusStep) fetchStatusDynamic(ctx context.Context, owner, repo string, runID int64, token string) (*sdk.StepResult, error) {
194+
run, err := s.ghClient.GetWorkflowRun(ctx, owner, repo, runID, token)
166195
if err != nil {
167196
return errorResult(fmt.Sprintf("failed to get workflow run: %v", err)), nil
168197
}

internal/step_action_trigger.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,39 +92,43 @@ func parseActionTriggerConfig(raw map[string]any) (actionTriggerConfig, error) {
9292
}
9393

9494
// Execute triggers the configured GitHub Actions workflow.
95-
// It returns the trigger confirmation and stops on error.
95+
// triggerData, stepOutputs, and current are used to resolve dynamic field
96+
// references (e.g. {{.owner}}, {{.steps.prev.ref}}) in the config values.
9697
func (s *actionTriggerStep) Execute(
9798
ctx context.Context,
98-
_ map[string]any,
99-
_ map[string]map[string]any,
100-
_ map[string]any,
99+
triggerData map[string]any,
100+
stepOutputs map[string]map[string]any,
101+
current map[string]any,
101102
_ map[string]any,
102103
) (*sdk.StepResult, error) {
103104
token := s.config.Token
104105
if token == "" {
105106
return errorResult("GITHUB_TOKEN is not configured"), nil
106107
}
107108

108-
err := s.ghClient.TriggerWorkflow(
109-
ctx,
110-
s.config.Owner,
111-
s.config.Repo,
112-
s.config.Workflow,
113-
s.config.Ref,
114-
s.config.Inputs,
115-
token,
116-
)
109+
owner := resolveField(s.config.Owner, triggerData, stepOutputs, current)
110+
repo := resolveField(s.config.Repo, triggerData, stepOutputs, current)
111+
workflow := resolveField(s.config.Workflow, triggerData, stepOutputs, current)
112+
ref := resolveField(s.config.Ref, triggerData, stepOutputs, current)
113+
114+
// Resolve template references in each input value.
115+
inputs := make(map[string]string, len(s.config.Inputs))
116+
for k, v := range s.config.Inputs {
117+
inputs[k] = resolveField(v, triggerData, stepOutputs, current)
118+
}
119+
120+
err := s.ghClient.TriggerWorkflow(ctx, owner, repo, workflow, ref, inputs, token)
117121
if err != nil {
118122
return errorResult(fmt.Sprintf("failed to trigger workflow: %v", err)), nil
119123
}
120124

121125
return &sdk.StepResult{
122126
Output: map[string]any{
123127
"triggered": true,
124-
"owner": s.config.Owner,
125-
"repo": s.config.Repo,
126-
"workflow": s.config.Workflow,
127-
"ref": s.config.Ref,
128+
"owner": owner,
129+
"repo": repo,
130+
"workflow": workflow,
131+
"ref": ref,
128132
},
129133
}, nil
130134
}

internal/step_create_check.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func parseCreateCheckConfig(raw map[string]any) (createCheckConfig, error) {
9090
}
9191

9292
cfg.SHA, _ = raw["sha"].(string)
93+
// sha may be a dynamic template reference (e.g. {{.commit}}) resolved at Execute time.
9394
if cfg.SHA == "" {
9495
return cfg, fmt.Errorf("config.sha is required")
9596
}
@@ -125,21 +126,27 @@ func parseCreateCheckConfig(raw map[string]any) (createCheckConfig, error) {
125126
}
126127

127128
// Execute creates the GitHub Check Run.
129+
// triggerData, stepOutputs, and current are used to resolve dynamic field
130+
// references (e.g. {{.commit}}, {{.steps.prev.sha}}) in owner, repo, and sha.
128131
func (s *createCheckStep) Execute(
129132
ctx context.Context,
130-
_ map[string]any,
131-
_ map[string]map[string]any,
132-
_ map[string]any,
133+
triggerData map[string]any,
134+
stepOutputs map[string]map[string]any,
135+
current map[string]any,
133136
_ map[string]any,
134137
) (*sdk.StepResult, error) {
135138
token := s.config.Token
136139
if token == "" {
137140
return errorResult("GITHUB_TOKEN is not configured"), nil
138141
}
139142

143+
owner := resolveField(s.config.Owner, triggerData, stepOutputs, current)
144+
repo := resolveField(s.config.Repo, triggerData, stepOutputs, current)
145+
sha := resolveField(s.config.SHA, triggerData, stepOutputs, current)
146+
140147
req := &CreateCheckRunRequest{
141148
Name: s.config.Name,
142-
HeadSHA: s.config.SHA,
149+
HeadSHA: sha,
143150
Status: s.config.Status,
144151
Conclusion: s.config.Conclusion,
145152
}
@@ -151,7 +158,7 @@ func (s *createCheckStep) Execute(
151158
}
152159
}
153160

154-
check, err := s.ghClient.CreateCheckRun(ctx, s.config.Owner, s.config.Repo, req, token)
161+
check, err := s.ghClient.CreateCheckRun(ctx, owner, repo, req, token)
155162
if err != nil {
156163
return errorResult(fmt.Sprintf("failed to create check run: %v", err)), nil
157164
}

0 commit comments

Comments
 (0)