mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-23 15:25:03 +01:00
Replace expressions engine (#133)
This commit is contained in:
30
internal/model/anchors.go
Normal file
30
internal/model/anchors.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Assumes there is no cycle ensured via test TestVerifyCycleIsInvalid
|
||||
func resolveAliases(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.AliasNode:
|
||||
aliasTarget := node.Alias
|
||||
if aliasTarget == nil {
|
||||
return errors.New("unresolved alias node")
|
||||
}
|
||||
*node = *aliasTarget
|
||||
if err := resolveAliases(node); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case yaml.DocumentNode, yaml.MappingNode, yaml.SequenceNode:
|
||||
for _, child := range node.Content {
|
||||
if err := resolveAliases(child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
242
internal/model/strategy_utils.go
Normal file
242
internal/model/strategy_utils.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TraceWriter is an interface for logging trace information.
|
||||
// Implementations can write to console, file, or any other sink.
|
||||
type TraceWriter interface {
|
||||
Info(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// StrategyResult holds the result of expanding a strategy.
|
||||
// FlatMatrix contains the expanded matrix entries.
|
||||
// IncludeMatrix contains entries that were added via include.
|
||||
// FailFast indicates whether the job should fail fast.
|
||||
// MaxParallel is the maximum parallelism allowed.
|
||||
// MatrixKeys is the set of keys present in the matrix.
|
||||
type StrategyResult struct {
|
||||
FlatMatrix []map[string]yaml.Node
|
||||
IncludeMatrix []map[string]yaml.Node
|
||||
FailFast bool
|
||||
MaxParallel *float64
|
||||
MatrixKeys map[string]struct{}
|
||||
}
|
||||
|
||||
type strategyContext struct {
|
||||
jobTraceWriter TraceWriter
|
||||
failFast bool
|
||||
maxParallel float64
|
||||
matrix map[string][]yaml.Node
|
||||
|
||||
flatMatrix []map[string]yaml.Node
|
||||
includeMatrix []map[string]yaml.Node
|
||||
|
||||
include []yaml.Node
|
||||
exclude []yaml.Node
|
||||
}
|
||||
|
||||
func (strategyContext *strategyContext) handleInclude() error {
|
||||
// Handle include logic
|
||||
if len(strategyContext.include) > 0 {
|
||||
for _, incNode := range strategyContext.include {
|
||||
if incNode.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("include entry is not a mapping node")
|
||||
}
|
||||
incMap := make(map[string]yaml.Node)
|
||||
for i := 0; i < len(incNode.Content); i += 2 {
|
||||
keyNode := incNode.Content[i]
|
||||
valNode := incNode.Content[i+1]
|
||||
if keyNode.Kind != yaml.ScalarNode {
|
||||
return fmt.Errorf("include key is not scalar")
|
||||
}
|
||||
incMap[keyNode.Value] = *valNode
|
||||
}
|
||||
matched := false
|
||||
for _, row := range strategyContext.flatMatrix {
|
||||
match := true
|
||||
for k, v := range incMap {
|
||||
if rv, ok := row[k]; ok && !nodesEqual(rv, v) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
matched = true
|
||||
// Add missing keys
|
||||
strategyContext.jobTraceWriter.Info("Add missing keys %v", incMap)
|
||||
for k, v := range incMap {
|
||||
if _, ok := row[k]; !ok {
|
||||
row[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
if strategyContext.jobTraceWriter != nil {
|
||||
strategyContext.jobTraceWriter.Info("Append include entry %v", incMap)
|
||||
}
|
||||
strategyContext.includeMatrix = append(strategyContext.includeMatrix, incMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (strategyContext *strategyContext) handleExclude() error {
|
||||
// Handle exclude logic
|
||||
if len(strategyContext.exclude) > 0 {
|
||||
for _, exNode := range strategyContext.exclude {
|
||||
// exNode is expected to be a mapping node
|
||||
if exNode.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("exclude entry is not a mapping node")
|
||||
}
|
||||
// Convert mapping to map[string]yaml.Node
|
||||
exMap := make(map[string]yaml.Node)
|
||||
for i := 0; i < len(exNode.Content); i += 2 {
|
||||
keyNode := exNode.Content[i]
|
||||
valNode := exNode.Content[i+1]
|
||||
if keyNode.Kind != yaml.ScalarNode {
|
||||
return fmt.Errorf("exclude key is not scalar")
|
||||
}
|
||||
exMap[keyNode.Value] = *valNode
|
||||
}
|
||||
// Remove matching rows
|
||||
filtered := []map[string]yaml.Node{}
|
||||
for _, row := range strategyContext.flatMatrix {
|
||||
match := true
|
||||
for k, v := range exMap {
|
||||
if rv, ok := row[k]; !ok || !nodesEqual(rv, v) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
filtered = append(filtered, row)
|
||||
} else if strategyContext.jobTraceWriter != nil {
|
||||
strategyContext.jobTraceWriter.Info("Removing %v from matrix due to exclude entry %v", row, exMap)
|
||||
}
|
||||
}
|
||||
strategyContext.flatMatrix = filtered
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpandStrategy expands the given strategy into a flat matrix and include matrix.
|
||||
// It mimics the behavior of the C# StrategyUtils. The strategy parameter is expected
|
||||
// to be populated from a YAML mapping that follows the GitHub Actions strategy schema.
|
||||
func ExpandStrategy(strategy *Strategy, jobTraceWriter TraceWriter) (*StrategyResult, error) {
|
||||
if strategy == nil {
|
||||
return &StrategyResult{FlatMatrix: []map[string]yaml.Node{{}}, IncludeMatrix: []map[string]yaml.Node{}, FailFast: true}, nil
|
||||
}
|
||||
|
||||
// Initialize defaults
|
||||
strategyContext := &strategyContext{
|
||||
jobTraceWriter: jobTraceWriter,
|
||||
failFast: strategy.FailFast,
|
||||
maxParallel: strategy.MaxParallel,
|
||||
matrix: strategy.Matrix,
|
||||
flatMatrix: []map[string]yaml.Node{{}},
|
||||
}
|
||||
// Process matrix entries
|
||||
for key, values := range strategyContext.matrix {
|
||||
switch key {
|
||||
case "include":
|
||||
strategyContext.include = values
|
||||
case "exclude":
|
||||
strategyContext.exclude = values
|
||||
default:
|
||||
// Other keys are treated as matrix dimensions
|
||||
// Expand each existing row with the new key/value pairs
|
||||
next := []map[string]yaml.Node{}
|
||||
for _, row := range strategyContext.flatMatrix {
|
||||
for _, val := range values {
|
||||
newRow := make(map[string]yaml.Node)
|
||||
for k, v := range row {
|
||||
newRow[k] = v
|
||||
}
|
||||
newRow[key] = val
|
||||
next = append(next, newRow)
|
||||
}
|
||||
}
|
||||
strategyContext.flatMatrix = next
|
||||
}
|
||||
}
|
||||
|
||||
if err := strategyContext.handleExclude(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(strategyContext.flatMatrix) == 0 {
|
||||
if jobTraceWriter != nil {
|
||||
jobTraceWriter.Info("Matrix is empty, adding an empty entry")
|
||||
}
|
||||
strategyContext.flatMatrix = []map[string]yaml.Node{{}}
|
||||
}
|
||||
|
||||
// Enforce job matrix limit of github
|
||||
if len(strategyContext.flatMatrix) > 256 {
|
||||
if jobTraceWriter != nil {
|
||||
jobTraceWriter.Info("Failure: Matrix contains more than 256 entries after exclude")
|
||||
}
|
||||
return nil, errors.New("matrix contains more than 256 entries")
|
||||
}
|
||||
|
||||
// Build matrix keys set
|
||||
matrixKeys := make(map[string]struct{})
|
||||
if len(strategyContext.flatMatrix) > 0 {
|
||||
for k := range strategyContext.flatMatrix[0] {
|
||||
matrixKeys[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if err := strategyContext.handleInclude(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StrategyResult{
|
||||
FlatMatrix: strategyContext.flatMatrix,
|
||||
IncludeMatrix: strategyContext.includeMatrix,
|
||||
FailFast: strategyContext.failFast,
|
||||
MaxParallel: &strategyContext.maxParallel,
|
||||
MatrixKeys: matrixKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// nodesEqual compares two yaml.Node values for equality.
|
||||
func nodesEqual(a, b yaml.Node) bool {
|
||||
return DeepEquals(a, b, true)
|
||||
}
|
||||
|
||||
// GetDefaultDisplaySuffix returns a string like "(foo, bar, baz)".
|
||||
// Empty items are ignored. If all items are empty the result is "".
|
||||
func GetDefaultDisplaySuffix(items []string) string {
|
||||
var b strings.Builder // efficient string concatenation
|
||||
|
||||
first := true // true until we write the first non‑empty item
|
||||
|
||||
for _, mk := range items {
|
||||
if mk == "" { // Go has no null string, so we only need to check for empty
|
||||
continue
|
||||
}
|
||||
if first {
|
||||
b.WriteString("(")
|
||||
first = false
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(mk)
|
||||
}
|
||||
|
||||
if !first { // we wrote at least one item
|
||||
b.WriteString(")")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
68
internal/model/strategy_utils_test.go
Normal file
68
internal/model/strategy_utils_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type EmptyTraceWriter struct {
|
||||
}
|
||||
|
||||
func (e *EmptyTraceWriter) Info(_ string, _ ...interface{}) {
|
||||
}
|
||||
|
||||
func TestStrategy(t *testing.T) {
|
||||
table := []struct {
|
||||
content string
|
||||
flatmatrix int
|
||||
includematrix int
|
||||
}{
|
||||
{`
|
||||
matrix:
|
||||
label:
|
||||
- a
|
||||
- b
|
||||
fields:
|
||||
- a
|
||||
- b
|
||||
`, 4, 0},
|
||||
{`
|
||||
matrix:
|
||||
label:
|
||||
- a
|
||||
- b
|
||||
include:
|
||||
- label: a
|
||||
x: self`, 2, 0,
|
||||
},
|
||||
{`
|
||||
matrix:
|
||||
label:
|
||||
- a
|
||||
- b
|
||||
include:
|
||||
- label: c
|
||||
x: self`, 2, 1,
|
||||
},
|
||||
{`
|
||||
matrix:
|
||||
label:
|
||||
- a
|
||||
- b
|
||||
exclude:
|
||||
- label: a`, 1, 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range table {
|
||||
var strategy Strategy
|
||||
err := yaml.Unmarshal([]byte(tc.content), &strategy)
|
||||
require.NoError(t, err)
|
||||
res, err := ExpandStrategy(&strategy, &EmptyTraceWriter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.FlatMatrix, tc.flatmatrix)
|
||||
require.Len(t, res.IncludeMatrix, tc.includematrix)
|
||||
}
|
||||
}
|
||||
148
internal/model/token_utils.go
Normal file
148
internal/model/token_utils.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
v2 "github.com/actions-oss/act-cli/internal/eval/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// DeepEquals compares two yaml.Node values recursively.
|
||||
// It supports scalar, mapping and sequence nodes and allows
|
||||
// an optional partial match for mappings and sequences.
|
||||
func DeepEquals(a, b yaml.Node, partialMatch bool) bool {
|
||||
// Scalar comparison
|
||||
if a.Kind == yaml.ScalarNode && b.Kind == yaml.ScalarNode {
|
||||
return scalarEquals(a, b)
|
||||
}
|
||||
|
||||
// Mapping comparison
|
||||
if a.Kind == yaml.MappingNode && b.Kind == yaml.MappingNode {
|
||||
return deepMapEquals(a, b, partialMatch)
|
||||
}
|
||||
|
||||
// Sequence comparison
|
||||
if a.Kind == yaml.SequenceNode && b.Kind == yaml.SequenceNode {
|
||||
return deepSequenceEquals(a, b, partialMatch)
|
||||
}
|
||||
|
||||
// Different kinds are not equal
|
||||
return false
|
||||
}
|
||||
|
||||
func scalarEquals(a, b yaml.Node) bool {
|
||||
var left, right any
|
||||
return a.Decode(&left) == nil && b.Decode(&right) == nil && v2.CreateIntermediateResult(v2.NewEvaluationContext(), left).AbstractEqual(v2.CreateIntermediateResult(v2.NewEvaluationContext(), right))
|
||||
}
|
||||
|
||||
func deepMapEquals(a, b yaml.Node, partialMatch bool) bool {
|
||||
mapA := make(map[string]yaml.Node)
|
||||
for i := 0; i < len(a.Content); i += 2 {
|
||||
keyNode := a.Content[i]
|
||||
valNode := a.Content[i+1]
|
||||
if keyNode.Kind != yaml.ScalarNode {
|
||||
return false
|
||||
}
|
||||
mapA[strings.ToLower(keyNode.Value)] = *valNode
|
||||
}
|
||||
mapB := make(map[string]yaml.Node)
|
||||
for i := 0; i < len(b.Content); i += 2 {
|
||||
keyNode := b.Content[i]
|
||||
valNode := b.Content[i+1]
|
||||
if keyNode.Kind != yaml.ScalarNode {
|
||||
return false
|
||||
}
|
||||
mapB[strings.ToLower(keyNode.Value)] = *valNode
|
||||
}
|
||||
if partialMatch {
|
||||
if len(mapA) < len(mapB) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if len(mapA) != len(mapB) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for k, vB := range mapB {
|
||||
vA, ok := mapA[k]
|
||||
if !ok || !DeepEquals(vA, vB, partialMatch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func deepSequenceEquals(a, b yaml.Node, partialMatch bool) bool {
|
||||
if partialMatch {
|
||||
if len(a.Content) < len(b.Content) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if len(a.Content) != len(b.Content) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
limit := len(b.Content)
|
||||
if !partialMatch {
|
||||
limit = len(a.Content)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
if !DeepEquals(*a.Content[i], *b.Content[i], partialMatch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// traverse walks a YAML node recursively.
|
||||
func traverse(node *yaml.Node, omitKeys bool, result *[]*yaml.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
|
||||
*result = append(*result, node)
|
||||
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
if omitKeys {
|
||||
// node.Content: key0, val0, key1, val1, …
|
||||
for i := 1; i < len(node.Content); i += 2 { // only the values
|
||||
traverse(node.Content[i], omitKeys, result)
|
||||
}
|
||||
} else {
|
||||
for _, child := range node.Content {
|
||||
traverse(child, omitKeys, result)
|
||||
}
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
// For all other node kinds (Scalar, Sequence, Alias, etc.)
|
||||
for _, child := range node.Content {
|
||||
traverse(child, omitKeys, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDisplayStrings implements the LINQ expression:
|
||||
//
|
||||
// from displayitem in keys.SelectMany(key => item[key].Traverse(true))
|
||||
// where !(displayitem is SequenceToken || displayitem is MappingToken)
|
||||
// select displayitem.ToString()
|
||||
func GetDisplayStrings(keys []string, item map[string]*yaml.Node) []string {
|
||||
var res []string
|
||||
|
||||
for _, k := range keys {
|
||||
if node, ok := item[k]; ok {
|
||||
var all []*yaml.Node
|
||||
traverse(node, true, &all) // include the parent node itself
|
||||
|
||||
for _, n := range all {
|
||||
// Keep only scalars – everything else is dropped
|
||||
if n.Kind == yaml.ScalarNode {
|
||||
res = append(res, n.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
277
internal/model/workflow_state.go
Normal file
277
internal/model/workflow_state.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package model
|
||||
|
||||
import "gopkg.in/yaml.v3"
|
||||
|
||||
type JobStatus int
|
||||
|
||||
const (
|
||||
JobStatusPending JobStatus = iota
|
||||
JobStatusDependenciesReady
|
||||
JobStatusBlocked
|
||||
JobStatusCompleted
|
||||
)
|
||||
|
||||
type JobState struct {
|
||||
JobID string // Workflow path to job, incl matrix and parent jobids
|
||||
Result string // Actions Job Result
|
||||
Outputs map[string]string // Returned Outputs
|
||||
State JobStatus
|
||||
Strategy []MatrixJobState
|
||||
}
|
||||
|
||||
type MatrixJobState struct {
|
||||
Matrix map[string]any
|
||||
Name string
|
||||
Result string
|
||||
Outputs map[string]string // Returned Outputs
|
||||
State JobStatus
|
||||
}
|
||||
|
||||
type WorkflowStatus int
|
||||
|
||||
const (
|
||||
WorkflowStatusPending WorkflowStatus = iota
|
||||
WorkflowStatusDependenciesReady
|
||||
WorkflowStatusBlocked
|
||||
WorkflowStatusCompleted
|
||||
)
|
||||
|
||||
type WorkflowState struct {
|
||||
Name string
|
||||
RunName string
|
||||
Jobs JobState
|
||||
StateWorkflowStatus WorkflowStatus
|
||||
}
|
||||
|
||||
type Workflow struct {
|
||||
On *On `yaml:"on,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
RunName yaml.Node `yaml:"run-name,omitempty"`
|
||||
Permissions *Permissions `yaml:"permissions,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
Defaults yaml.Node `yaml:"defaults,omitempty"`
|
||||
Concurrency yaml.Node `yaml:"concurrency,omitempty"` // Two layouts
|
||||
Jobs map[string]Job `yaml:"jobs,omitempty"`
|
||||
}
|
||||
|
||||
type On struct {
|
||||
Data map[string]yaml.Node `yaml:"-"`
|
||||
WorkflowDispatch *WorkflowDispatch `yaml:"workflow_dispatch,omitempty"`
|
||||
WorkflowCall *WorkflowCall `yaml:"workflow_call,omitempty"`
|
||||
Schedule []Cron `yaml:"schedule,omitempty"`
|
||||
}
|
||||
|
||||
type Cron struct {
|
||||
Cron string `yaml:"cron,omitempty"`
|
||||
}
|
||||
|
||||
func (a *On) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Data = map[string]yaml.Node{}
|
||||
a.Data[s] = yaml.Node{}
|
||||
case yaml.SequenceNode:
|
||||
var s []string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Data = map[string]yaml.Node{}
|
||||
for _, v := range s {
|
||||
a.Data[v] = yaml.Node{}
|
||||
}
|
||||
default:
|
||||
if err := node.Decode(&a.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
type OnObj On
|
||||
if err := node.Decode((*OnObj)(a)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *On) MarshalYAML() (interface{}, error) {
|
||||
return a.Data, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ yaml.Unmarshaler = &On{}
|
||||
_ yaml.Marshaler = &On{}
|
||||
_ yaml.Unmarshaler = &Concurrency{}
|
||||
_ yaml.Unmarshaler = &RunsOn{}
|
||||
_ yaml.Unmarshaler = &ImplicitStringArray{}
|
||||
_ yaml.Unmarshaler = &Environment{}
|
||||
)
|
||||
|
||||
type WorkflowDispatch struct {
|
||||
Inputs map[string]Input `yaml:"inputs,omitempty"`
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Default string `yaml:"default,omitempty"`
|
||||
Required bool `yaml:"required,omitempty"`
|
||||
}
|
||||
|
||||
type WorkflowCall struct {
|
||||
Inputs map[string]Input `yaml:"inputs,omitempty"`
|
||||
Secrets map[string]Secret `yaml:"secrets,omitempty"`
|
||||
Outputs map[string]Output `yaml:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
type Secret struct {
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Required bool `yaml:"required,omitempty"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Value yaml.Node `yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
Needs ImplicitStringArray `yaml:"needs,omitempty"`
|
||||
Permissions *Permissions `yaml:"permissions,omitempty"`
|
||||
Strategy yaml.Node `yaml:"strategy,omitempty"`
|
||||
Name yaml.Node `yaml:"name,omitempty"`
|
||||
Concurrency yaml.Node `yaml:"concurrency,omitempty"`
|
||||
// Reusable Workflow
|
||||
Uses yaml.Node `yaml:"uses,omitempty"`
|
||||
With yaml.Node `yaml:"with,omitempty"`
|
||||
Secrets yaml.Node `yaml:"secrets,omitempty"`
|
||||
// Runner Job
|
||||
RunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||
Defaults yaml.Node `yaml:"defaults,omitempty"`
|
||||
TimeoutMinutes yaml.Node `yaml:"timeout-minutes,omitempty"`
|
||||
Container yaml.Node `yaml:"container,omitempty"`
|
||||
Services yaml.Node `yaml:"services,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
Steps []yaml.Node `yaml:"steps,omitempty"`
|
||||
Outputs yaml.Node `yaml:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
type ImplicitStringArray []string
|
||||
|
||||
func (a *ImplicitStringArray) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = []string{s}
|
||||
return nil
|
||||
}
|
||||
return node.Decode((*[]string)(a))
|
||||
}
|
||||
|
||||
type Permissions map[string]string
|
||||
|
||||
func (p *Permissions) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
var perm string
|
||||
switch s {
|
||||
case "read-all":
|
||||
perm = "read"
|
||||
case "write-all":
|
||||
perm = "write"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
(*p)["actions"] = perm
|
||||
(*p)["attestations"] = perm
|
||||
(*p)["contents"] = perm
|
||||
(*p)["checks"] = perm
|
||||
(*p)["deployments"] = perm
|
||||
(*p)["discussions"] = perm
|
||||
(*p)["id-token"] = perm
|
||||
(*p)["issues"] = perm
|
||||
(*p)["models"] = perm
|
||||
(*p)["packages"] = perm
|
||||
(*p)["pages"] = perm
|
||||
(*p)["pull-requests"] = perm
|
||||
(*p)["repository-projects"] = perm
|
||||
(*p)["security-events"] = perm
|
||||
(*p)["statuses"] = perm
|
||||
return nil
|
||||
}
|
||||
return node.Decode((*map[string]string)(p))
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
Matrix map[string][]yaml.Node `yaml:"matrix"`
|
||||
MaxParallel float64 `yaml:"max-parallel"`
|
||||
FailFast bool `yaml:"fail-fast"`
|
||||
}
|
||||
|
||||
type Concurrency struct {
|
||||
Group string `yaml:"group"`
|
||||
CancelInProgress bool `yaml:"cancel-in-progress"`
|
||||
}
|
||||
|
||||
func (c *Concurrency) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Group = s
|
||||
return nil
|
||||
}
|
||||
type ConcurrencyObj Concurrency
|
||||
return node.Decode((*ConcurrencyObj)(c))
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
Name string `yaml:"name"`
|
||||
URL yaml.Node `yaml:"url"`
|
||||
}
|
||||
|
||||
func (e *Environment) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Name = s
|
||||
return nil
|
||||
}
|
||||
type EnvironmentObj Environment
|
||||
return node.Decode((*EnvironmentObj)(e))
|
||||
}
|
||||
|
||||
type RunsOn struct {
|
||||
Labels []string `yaml:"labels"`
|
||||
Group string `yaml:"group,omitempty"`
|
||||
}
|
||||
|
||||
func (a *RunsOn) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
var s string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Labels = []string{s}
|
||||
return nil
|
||||
}
|
||||
if node.Kind == yaml.SequenceNode {
|
||||
var s []string
|
||||
if err := node.Decode(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
a.Labels = s
|
||||
return nil
|
||||
}
|
||||
type RunsOnObj RunsOn
|
||||
return node.Decode((*RunsOnObj)(a))
|
||||
}
|
||||
141
internal/model/workflow_state_test.go
Normal file
141
internal/model/workflow_state_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
v2 "github.com/actions-oss/act-cli/internal/eval/v2"
|
||||
"github.com/actions-oss/act-cli/internal/templateeval"
|
||||
"github.com/actions-oss/act-cli/pkg/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestParseWorkflow(t *testing.T) {
|
||||
ee := &templateeval.ExpressionEvaluator{
|
||||
EvaluationContext: v2.EvaluationContext{
|
||||
Variables: v2.CaseInsensitiveObject[any]{},
|
||||
Functions: v2.GetFunctions(),
|
||||
},
|
||||
}
|
||||
var node yaml.Node
|
||||
err := yaml.Unmarshal([]byte(`
|
||||
on: push
|
||||
run-name: ${{ fromjson('{}') }}
|
||||
jobs:
|
||||
_:
|
||||
name: ${{ github.ref_name }}
|
||||
steps:
|
||||
- run: echo Hello World
|
||||
env:
|
||||
TAG: ${{ env.global }}
|
||||
`), &node)
|
||||
require.NoError(t, err)
|
||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
||||
Definition: "workflow-root",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ee.RestrictEval = true
|
||||
ee.EvaluationContext.Variables = v2.CaseInsensitiveObject[any]{
|
||||
"github": v2.CaseInsensitiveObject[any]{
|
||||
"ref_name": "self",
|
||||
},
|
||||
"vars": v2.CaseInsensitiveObject[any]{},
|
||||
"inputs": v2.CaseInsensitiveObject[any]{},
|
||||
}
|
||||
|
||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
||||
Definition: "workflow-root",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
})
|
||||
require.Error(t, err)
|
||||
var myw Workflow
|
||||
require.NoError(t, node.Decode(&myw))
|
||||
}
|
||||
|
||||
func TestParseWorkflowCall(t *testing.T) {
|
||||
ee := &templateeval.ExpressionEvaluator{
|
||||
EvaluationContext: v2.EvaluationContext{
|
||||
Variables: v2.CaseInsensitiveObject[any]{},
|
||||
Functions: v2.GetFunctions(),
|
||||
},
|
||||
}
|
||||
var node yaml.Node
|
||||
// jobs.test.outputs.test
|
||||
err := yaml.Unmarshal([]byte(`
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
test:
|
||||
value: ${{ jobs.test.outputs.test }} # tojson(vars.raw)
|
||||
run-name: ${{ github.ref_name }}
|
||||
jobs:
|
||||
_:
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ github.ref_name }}
|
||||
steps:
|
||||
- run: echo Hello World
|
||||
env:
|
||||
TAG: ${{ env.global }}
|
||||
`), &node)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resolveAliases(node.Content[0]))
|
||||
require.NoError(t, (&schema.Node{
|
||||
Definition: "workflow-root",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
}).UnmarshalYAML(node.Content[0]))
|
||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
||||
Definition: "workflow-root",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var raw any
|
||||
err = node.Content[0].Decode(&raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ee.RestrictEval = true
|
||||
ee.EvaluationContext.Variables = v2.CaseInsensitiveObject[any]{
|
||||
"github": v2.CaseInsensitiveObject[any]{
|
||||
"ref_name": "self",
|
||||
},
|
||||
"vars": v2.CaseInsensitiveObject[any]{
|
||||
"raw": raw,
|
||||
},
|
||||
"inputs": v2.CaseInsensitiveObject[any]{},
|
||||
"jobs": v2.CaseInsensitiveObject[any]{
|
||||
"test": v2.CaseInsensitiveObject[any]{
|
||||
"outputs": v2.CaseInsensitiveObject[any]{
|
||||
"test": "Hello World",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = ee.EvaluateYamlNode(context.Background(), node.Content[0], &schema.Node{
|
||||
RestrictEval: true,
|
||||
Definition: "workflow-root",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var myw Workflow
|
||||
require.NoError(t, node.Decode(&myw))
|
||||
workflowCall := myw.On.WorkflowCall
|
||||
if workflowCall != nil {
|
||||
for _, out := range workflowCall.Outputs {
|
||||
err = ee.EvaluateYamlNode(context.Background(), &out.Value, &schema.Node{
|
||||
RestrictEval: true,
|
||||
Definition: "workflow-output-context",
|
||||
Schema: schema.GetWorkflowSchema(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Hello World", out.Value.Value)
|
||||
}
|
||||
}
|
||||
out, err := yaml.Marshal(&myw)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, out)
|
||||
}
|
||||
Reference in New Issue
Block a user