Go · 1851 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package concurrency
4
5 import (
6 "fmt"
7 "strings"
8
9 "github.com/tenseleyFlow/shithub/internal/actions/expr"
10 )
11
12 // EvalContext is the limited trigger-time expression context for a
13 // concurrency.group value. It deliberately excludes secrets.
14 type EvalContext struct {
15 EventPayload map[string]any
16 HeadSHA string
17 HeadRef string
18 }
19
20 // ResolveGroup evaluates `${{ ... }}` fragments in a concurrency group value.
21 // Literal text outside expressions is preserved.
22 func ResolveGroup(raw string, in EvalContext) (string, error) {
23 ctx := expr.Context{
24 Shithub: expr.ShithubContext{
25 Event: in.EventPayload,
26 SHA: in.HeadSHA,
27 Ref: in.HeadRef,
28 },
29 Untrusted: expr.DefaultUntrusted(),
30 }
31 var out strings.Builder
32 if err := walkExpressions(raw, func(literal, body string) error {
33 if body == "" {
34 out.WriteString(literal)
35 return nil
36 }
37 out.WriteString(literal)
38 v, err := eval(body, &ctx)
39 if err != nil {
40 return err
41 }
42 out.WriteString(v.String())
43 return nil
44 }); err != nil {
45 return "", fmt.Errorf("actions concurrency: resolve group: %w", err)
46 }
47 return validateGroup(out.String())
48 }
49
50 func eval(body string, ctx *expr.Context) (expr.Value, error) {
51 toks, err := expr.Lex(strings.TrimSpace(body))
52 if err != nil {
53 return expr.Value{}, err
54 }
55 ast, err := expr.Parse(toks)
56 if err != nil {
57 return expr.Value{}, err
58 }
59 return expr.Eval(ast, ctx)
60 }
61
62 func walkExpressions(raw string, fn func(literal, body string) error) error {
63 for {
64 start := strings.Index(raw, "${{")
65 if start < 0 {
66 return fn(raw, "")
67 }
68 end := strings.Index(raw[start+3:], "}}")
69 if end < 0 {
70 return fmt.Errorf("render expression: missing closing }}")
71 }
72 end += start + 3
73 if err := fn(raw[:start], raw[start+3:end]); err != nil {
74 return err
75 }
76 raw = raw[end+2:]
77 }
78 }
79