This commit is contained in:
@@ -7,8 +7,11 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -31,6 +34,11 @@ func (w *SingleWorkflow) Job() (string, *Job) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// WorkflowDispatchConfig returns the `on: workflow_dispatch` declaration, nil if there is none.
|
||||
func (w *SingleWorkflow) WorkflowDispatchConfig() *model.WorkflowDispatch {
|
||||
return (&model.Workflow{RawOn: w.RawOn}).WorkflowDispatchConfig()
|
||||
}
|
||||
|
||||
func (w *SingleWorkflow) jobs() ([]string, []*Job, error) {
|
||||
ids, jobs, err := parseMappingNode[*Job](&w.RawJobs)
|
||||
if err != nil {
|
||||
@@ -74,27 +82,56 @@ func (w *SingleWorkflow) SetJob(id string, job *Job) error {
|
||||
}
|
||||
|
||||
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
return yaml.Marshal(w)
|
||||
// Encode with the same indentation SetJob uses (2). yaml.Marshal's default
|
||||
// indentation (4) makes the encoder emit multi-line block scalars (e.g. a
|
||||
// `run:` step that begins with blank lines) with a wrong explicit indentation
|
||||
// indicator (`run: |4`) that then fails to re-parse, which silently strands
|
||||
// the job during concurrency evaluation. Keeping both encoders at indent 2
|
||||
// makes the serialized single workflow round-trip.
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Steps []*Step `yaml:"steps,omitempty"`
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
With map[string]any `yaml:"with,omitempty"`
|
||||
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
|
||||
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Steps []*Step `yaml:"steps,omitempty"`
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"`
|
||||
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
With map[string]any `yaml:"with,omitempty"`
|
||||
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
|
||||
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// GetContinueOnError decodes the continue-on-error field to a bool.
|
||||
// The field may be a literal bool or an already-evaluated expression node.
|
||||
func (j *Job) GetContinueOnError() bool {
|
||||
if j.RawContinueOnError.Kind == 0 {
|
||||
return false
|
||||
}
|
||||
var v bool
|
||||
if err := j.RawContinueOnError.Decode(&v); err != nil {
|
||||
return false
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (j *Job) Clone() *Job {
|
||||
@@ -102,23 +139,24 @@ func (j *Job) Clone() *Job {
|
||||
return nil
|
||||
}
|
||||
return &Job{
|
||||
Name: j.Name,
|
||||
RawNeeds: j.RawNeeds,
|
||||
RawRunsOn: j.RawRunsOn,
|
||||
Env: j.Env,
|
||||
If: j.If,
|
||||
Steps: j.Steps,
|
||||
TimeoutMinutes: j.TimeoutMinutes,
|
||||
Services: j.Services,
|
||||
Strategy: j.Strategy,
|
||||
RawContainer: j.RawContainer,
|
||||
Defaults: j.Defaults,
|
||||
Outputs: j.Outputs,
|
||||
Uses: j.Uses,
|
||||
With: j.With,
|
||||
RawSecrets: j.RawSecrets,
|
||||
RawConcurrency: j.RawConcurrency,
|
||||
RawPermissions: j.RawPermissions,
|
||||
Name: j.Name,
|
||||
RawNeeds: j.RawNeeds,
|
||||
RawRunsOn: j.RawRunsOn,
|
||||
Env: j.Env,
|
||||
If: j.If,
|
||||
Steps: j.Steps,
|
||||
TimeoutMinutes: j.TimeoutMinutes,
|
||||
RawContinueOnError: j.RawContinueOnError,
|
||||
Services: j.Services,
|
||||
Strategy: j.Strategy,
|
||||
RawContainer: j.RawContainer,
|
||||
Defaults: j.Defaults,
|
||||
Outputs: j.Outputs,
|
||||
Uses: j.Uses,
|
||||
With: j.With,
|
||||
RawSecrets: j.RawSecrets,
|
||||
RawConcurrency: j.RawConcurrency,
|
||||
RawPermissions: j.RawPermissions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,17 +174,29 @@ func (j *Job) RunsOn() []string {
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run string `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With map[string]string `yaml:"with,omitempty"`
|
||||
ContinueOnError bool `yaml:"continue-on-error,omitempty"`
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run string `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With map[string]string `yaml:"with,omitempty"`
|
||||
RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` // raw: the runner evaluates it with the steps context
|
||||
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML canonicalizes booleans like continue-on-error
|
||||
func (s *Step) UnmarshalYAML(node *yaml.Node) error {
|
||||
type rawStep Step
|
||||
if err := node.Decode((*rawStep)(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw := &s.RawContinueOnError; raw.Tag == "!!bool" {
|
||||
raw.Value = strings.ToLower(raw.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String gets the name of step
|
||||
@@ -220,9 +270,11 @@ func (evt *Event) Inputs() []WorkflowDispatchInput {
|
||||
}
|
||||
|
||||
func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) {
|
||||
w := new(model.Workflow)
|
||||
err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w)
|
||||
return w.RawConcurrency, err
|
||||
w, err := ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.RawConcurrency, nil
|
||||
}
|
||||
|
||||
func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (string, bool, error) {
|
||||
@@ -261,7 +313,7 @@ func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCt
|
||||
if evaluated.RawExpression != "" {
|
||||
return evaluated.RawExpression, false, nil
|
||||
}
|
||||
return evaluated.Group, evaluated.CancelInProgress == "true", nil
|
||||
return evaluated.Group, util.ParseYamlBool(evaluated.CancelInProgress), nil
|
||||
}
|
||||
|
||||
func toGitContext(input map[string]any) *model.GithubContext {
|
||||
@@ -298,6 +350,9 @@ func toGitContext(input map[string]any) *model.GithubContext {
|
||||
return gitContext
|
||||
}
|
||||
|
||||
// workflowCallEvent is only fired by another workflow's `uses:`, so it is excluded from trigger detection.
|
||||
const workflowCallEvent = "workflow_call"
|
||||
|
||||
func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
switch rawOn.Kind {
|
||||
case yaml.ScalarNode:
|
||||
@@ -306,6 +361,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val == workflowCallEvent {
|
||||
return []*Event{}, nil
|
||||
}
|
||||
return []*Event{
|
||||
{Name: val},
|
||||
}, nil
|
||||
@@ -319,6 +377,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
for _, v := range val {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t == workflowCallEvent {
|
||||
continue
|
||||
}
|
||||
res = append(res, &Event{Name: t})
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid type %T", t)
|
||||
@@ -332,6 +393,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
}
|
||||
res := make([]*Event, 0, len(events))
|
||||
for i, k := range events {
|
||||
if k == workflowCallEvent {
|
||||
continue
|
||||
}
|
||||
v := triggers[i]
|
||||
switch v.Kind {
|
||||
case yaml.ScalarNode:
|
||||
@@ -454,6 +518,30 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func EvaluateJobIfExpression(jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (bool, error) {
|
||||
actJob := &model.Job{
|
||||
Strategy: &model.Strategy{
|
||||
FailFastString: job.Strategy.FailFastString,
|
||||
MaxParallelString: job.Strategy.MaxParallelString,
|
||||
RawMatrix: job.Strategy.RawMatrix,
|
||||
},
|
||||
}
|
||||
// Each per-matrix job carries its single matrix combination in RawMatrix so resolve it and pass it in;
|
||||
// otherwise `matrix.*` references in `if:` evaluate to null.
|
||||
// GetMatrixes always returns at least one element (an empty map for a job without a matrix),
|
||||
// so only a non-empty combination should populate `matrix.*`, leaving it nil otherwise.
|
||||
var matrix map[string]any
|
||||
matrixes, err := actJob.GetMatrixes()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(matrixes) > 0 && len(matrixes[0]) > 0 {
|
||||
matrix = matrixes[0]
|
||||
}
|
||||
evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs))
|
||||
return evaluator.evaluateCondition(job.If.Value)
|
||||
}
|
||||
|
||||
// parseMappingNode parse a mapping node and preserve order.
|
||||
func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
|
||||
Reference in New Issue
Block a user