forked from rhysd/actionlint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaction_metadata.go
145 lines (126 loc) · 4.56 KB
/
action_metadata.go
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
136
137
138
139
140
141
142
143
144
145
package actionlint
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
//go:generate go run ./scripts/generate-popular-actions -s remote -f go ./popular_actions.go
// ActionMetadataInputRequired represents if the action input is required to be set or not.
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs
type ActionMetadataInputRequired bool
// UnmarshalYAML implements yaml.Unmarshaler.
func (required *ActionMetadataInputRequired) UnmarshalYAML(n *yaml.Node) error {
// Name this local type for better error message on unmarshaling
type actionInputMetadata struct {
Required bool `yaml:"required"`
Default *string `yaml:"default"`
}
var input actionInputMetadata
if err := n.Decode(&input); err != nil {
return err
}
*required = ActionMetadataInputRequired(input.Required && input.Default == nil)
return nil
}
// ActionMetadata represents structure of action.yaml.
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
type ActionMetadata struct {
// Name is "name" field of action.yaml
Name string `yaml:"name" json:"name"`
// Inputs is "inputs" field of action.yaml
Inputs map[string]ActionMetadataInputRequired `yaml:"inputs" json:"inputs"`
// Outputs is "outputs" field of action.yaml. Key is name of output. Description is omitted
// since actionlint does not use it.
Outputs map[string]struct{} `yaml:"outputs" json:"outputs"`
// SkipInputs is flag to specify behavior of inputs check. When it is true, inputs for this
// action will not be checked.
SkipInputs bool `json:"skip_inputs"`
// SkipOutputs is flag to specify a bit loose typing to outputs object. If it is set to
// true, the outputs object accepts any properties along with strictly typed props.
SkipOutputs bool `json:"skip_outputs"`
}
// LocalActionsCache is cache for local actions' metadata. It avoids repeating to find/read/parse
// local action's metadata file (action.yml).
type LocalActionsCache struct {
mu sync.RWMutex
proj *Project // might be nil
cache map[string]*ActionMetadata
dbg io.Writer
}
// NewLocalActionsCache creates new LocalActionsCache instance.
func NewLocalActionsCache(proj *Project, dbg io.Writer) *LocalActionsCache {
return &LocalActionsCache{
proj: proj,
cache: map[string]*ActionMetadata{},
dbg: dbg,
}
}
func (c *LocalActionsCache) debug(format string, args ...interface{}) {
if c.dbg == nil {
return
}
format = "[LocalActionsCache] " + format + "\n"
fmt.Fprintf(c.dbg, format, args...)
}
func (c *LocalActionsCache) readCache(key string) (*ActionMetadata, bool) {
c.mu.RLock()
m, ok := c.cache[key]
c.mu.RUnlock()
return m, ok
}
func (c *LocalActionsCache) writeCache(key string, val *ActionMetadata) {
c.mu.Lock()
c.cache[key] = val
c.mu.Unlock()
}
// FindMetadata finds metadata for given spec. The spec should indicate for local action hence it
// should start with "./". The first return value can be nil even if error did not occur.
// LocalActionCache caches that the action was not found. At first search, it returns an error that
// the action was not found. But at the second search, it does not return an error even if the result
// is nil. This behavior prevents repeating to report the same error from multiple places.
// Calling this method is thread-safe.
func (c *LocalActionsCache) FindMetadata(spec string) (*ActionMetadata, error) {
if c.proj == nil || !strings.HasPrefix(spec, "./") {
return nil, nil
}
if m, ok := c.readCache(spec); ok {
c.debug("Cache hit for %s: %v", spec, m)
return m, nil
}
dir := filepath.Join(c.proj.RootDir(), filepath.FromSlash(spec))
b, err := readLocalActionMetadataFile(dir)
if err != nil {
c.writeCache(spec, nil) // Remember action was not found
return nil, err
}
var meta ActionMetadata
if err := yaml.Unmarshal(b, &meta); err != nil {
c.writeCache(spec, nil) // Remember action was invalid
msg := strings.ReplaceAll(err.Error(), "\n", " ")
return nil, fmt.Errorf("action.yml in %q is invalid: %s", dir, msg)
}
c.debug("New metadata parsed from action %s: %v", dir, &meta)
c.writeCache(spec, &meta)
return &meta, nil
}
func readLocalActionMetadataFile(dir string) ([]byte, error) {
for _, p := range []string{
filepath.Join(dir, "action.yaml"),
filepath.Join(dir, "action.yml"),
} {
if b, err := ioutil.ReadFile(p); err == nil {
return b, nil
}
}
if wd, err := os.Getwd(); err == nil {
if p, err := filepath.Rel(wd, dir); err == nil {
dir = p
}
}
return nil, fmt.Errorf("neither action.yaml nor action.yml is found in directory \"%s\"", dir)
}