1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package bundledactions
import (
"embed"
"fmt"
"io/fs"
"path"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
//go:embed all:workflows all:tangy
var assets embed.FS
type Input struct {
Default string `yaml:"default"`
}
type Action struct {
Uses string
Path string
Using string
Main string
Inputs map[string]Input
Source []byte
YAML []byte
}
type actionDef struct {
Runs struct {
Using string `yaml:"using"`
Main string `yaml:"main"`
} `yaml:"runs"`
Inputs map[string]Input `yaml:"inputs"`
}
func Lookup(uses string) (*Action, error) {
value := strings.TrimSpace(uses)
if value == "" {
return nil, fmt.Errorf("action uses is empty")
}
if strings.HasPrefix(value, "./") || strings.HasPrefix(value, "/") {
return nil, fmt.Errorf("net-only sandbox refuses local action %q", value)
}
parts := strings.SplitN(value, "@", 2)
if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" {
return nil, fmt.Errorf("unsupported uses format (expected org/repo@version): %q", value)
}
orgRepo := strings.TrimSpace(parts[0])
if !strings.HasPrefix(orgRepo, "tangy/") {
return nil, fmt.Errorf("action %q is not bundled; remote action downloads are disabled", orgRepo)
}
clean := path.Clean(orgRepo)
if clean != orgRepo || strings.Contains(clean, "..") || !fs.ValidPath(clean) {
return nil, fmt.Errorf("invalid bundled action path %q", orgRepo)
}
dir := clean
yamlPath := path.Join(dir, "action.yml")
yamlBytes, err := assets.ReadFile(yamlPath)
if err != nil {
yamlPath = path.Join(dir, "action.yaml")
yamlBytes, err = assets.ReadFile(yamlPath)
if err != nil {
return nil, fmt.Errorf("bundled action %q not found", orgRepo)
}
}
var def actionDef
if err := yaml.Unmarshal(yamlBytes, &def); err != nil {
return nil, fmt.Errorf("parse %s: %w", yamlPath, err)
}
main := strings.TrimSpace(def.Runs.Main)
if main == "" {
return nil, fmt.Errorf("bundled action %q has no main", orgRepo)
}
if !fs.ValidPath(main) || strings.Contains(main, "..") {
return nil, fmt.Errorf("bundled action %q has invalid main %q", orgRepo, main)
}
source, err := assets.ReadFile(path.Join(dir, main))
if err != nil {
return nil, fmt.Errorf("bundled action %q main %q not found", orgRepo, main)
}
return &Action{
Uses: value,
Path: "bundled-actions/" + orgRepo,
Using: strings.TrimSpace(def.Runs.Using),
Main: main,
Inputs: def.Inputs,
Source: source,
YAML: yamlBytes,
}, nil
}
func Workflows() map[string]string {
entries, err := fs.ReadDir(assets, "workflows")
if err != nil {
return map[string]string{}
}
out := make(map[string]string)
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || !strings.HasSuffix(name, ".yml") {
continue
}
data, err := assets.ReadFile(path.Join("workflows", name))
if err != nil {
continue
}
out[strings.TrimSuffix(name, ".yml")] = string(data)
}
return out
}
func Workflow(name string) ([]byte, bool) {
clean := strings.TrimSpace(name)
if clean == "" || strings.Contains(clean, "/") || strings.Contains(clean, "..") {
return nil, false
}
data, err := assets.ReadFile(path.Join("workflows", clean+".yml"))
if err != nil {
return nil, false
}
return data, true
}
func Names() []string {
all := Workflows()
out := make([]string, 0, len(all))
for name := range all {
out = append(out, name)
}
sort.Strings(out)
return out
}