mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-26 14:43:39 +09:00
feat(actions): update actionslib, support self:, misc fixes (#39358)
Updates actionslib to https://gitea.com/gitea/actionslib/releases/tag/v1.2.1, moves workflow parsing into it and aligns behaviour with GitHub. 1. `uses:` supports `self:` (Gitea-only feature) and `$/` paths. 1. `strategy`, `matrix`, `max-parallel` and `fail-fast` accept expressions, including over `needs`. A job whose `name`, `runs-on` or `continue-on-error` reads `needs` is resolved once they finish. 1. A job `if:` may only read `github`, `needs`, `vars` and `inputs` and is decided before the matrix, as on github.com. 1. Matrix `fail-fast` cancels the other combinations, and `always()` jobs keep running when a run is cancelled. 1. Invalid workflow files, including a malformed `on:` and unknown or cyclic `needs`, show up on push as failed runs with the error. 1. A job whose `if:` or `concurrency:` fails to evaluate is skipped or failed with the error, instead of staying blocked. 1. Reusable workflows: a missing and an unreadable repository fail alike, public callers cannot use private workflows, nested jobs cannot exceed the caller's token permissions. 1. Runner labels match case-insensitively, and `runs-on` accepts an array from an expression. Runner PR: https://gitea.com/gitea/runner/pulls/1247 Docs PR: https://gitea.com/gitea/docs/pulls/553 Fixes: https://github.com/go-gitea/gitea/issues/38990 Fixes: https://github.com/go-gitea/gitea/issues/39382 Fixes: https://github.com/go-gitea/gitea/issues/32364 Fixes: https://github.com/go-gitea/gitea/issues/36077 Fixes: https://github.com/go-gitea/gitea/issues/23277 Fixes: https://github.com/go-gitea/gitea/issues/29020 Co-authored-by: Claude (Opus 5) <noreply@anthropic.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
+127
-181
@@ -14,18 +14,19 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// SingleWorkflow is a workflow with single job and single matrix
|
||||
type SingleWorkflow struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawOn yaml.Node `yaml:"on,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
RawJobs yaml.Node `yaml:"jobs,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
RunName string `yaml:"run-name,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
RawOn yaml.Node `yaml:"on,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
RawJobs yaml.Node `yaml:"jobs,omitempty"`
|
||||
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||
RunName string `yaml:"run-name,omitempty"`
|
||||
}
|
||||
|
||||
func (w *SingleWorkflow) Job() (string, *Job) {
|
||||
@@ -93,7 +94,9 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(w); err != nil {
|
||||
payload := *w
|
||||
payload.RunName = "" // already interpolated into the run title, a runner would parse it as a template again
|
||||
if err := enc.Encode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
@@ -103,24 +106,24 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
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 yaml.Node `yaml:"services,omitempty"`
|
||||
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||
Defaults yaml.Node `yaml:"defaults,omitempty"`
|
||||
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
With yaml.Node `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.
|
||||
@@ -171,8 +174,18 @@ func (j *Job) EraseNeeds() *Job {
|
||||
return j
|
||||
}
|
||||
|
||||
// RunsOn returns the labels Gitea matches runners against, unescaped like DisplayName.
|
||||
func (j *Job) RunsOn() []string {
|
||||
return (&model.Job{RawRunsOn: j.RawRunsOn}).RunsOn()
|
||||
runsOn := model.RunsOnFromNode(j.RawRunsOn)
|
||||
for i, label := range runsOn {
|
||||
runsOn[i] = unescapeExpressions(label)
|
||||
}
|
||||
return runsOn
|
||||
}
|
||||
|
||||
// DisplayName is the name Gitea stores, without the escaping the payload keeps for runners.
|
||||
func (j *Job) DisplayName() string {
|
||||
return util.EllipsisDisplayString(unescapeExpressions(j.Name), 255)
|
||||
}
|
||||
|
||||
// BlockSafeString works around https://github.com/yaml/go-yaml/issues/399, quoting a value whose
|
||||
@@ -187,17 +200,17 @@ func (s BlockSafeString) MarshalYAML() (any, error) {
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name BlockSafeString `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run BlockSafeString `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"`
|
||||
ID string `yaml:"id,omitempty"`
|
||||
If yaml.Node `yaml:"if,omitempty"`
|
||||
Name BlockSafeString `yaml:"name,omitempty"`
|
||||
Uses string `yaml:"uses,omitempty"`
|
||||
Run BlockSafeString `yaml:"run,omitempty"`
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
Shell string `yaml:"shell,omitempty"`
|
||||
Env yaml.Node `yaml:"env,omitempty"`
|
||||
With yaml.Node `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
|
||||
@@ -225,20 +238,45 @@ func (s *Step) String() string {
|
||||
}).String()
|
||||
}
|
||||
|
||||
type ContainerSpec struct {
|
||||
Image string `yaml:"image,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty"`
|
||||
Ports []string `yaml:"ports,omitempty"`
|
||||
Volumes []string `yaml:"volumes,omitempty"`
|
||||
Options string `yaml:"options,omitempty"`
|
||||
Credentials map[string]string `yaml:"credentials,omitempty"`
|
||||
Cmd []string `yaml:"cmd,omitempty"`
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
FailFastString string `yaml:"fail-fast,omitempty"`
|
||||
MaxParallelString string `yaml:"max-parallel,omitempty"`
|
||||
RawMatrix yaml.Node `yaml:"matrix,omitempty"`
|
||||
JobIndex int `yaml:"job-index,omitempty"` // set by buildMatrixCombos, read back from its payload
|
||||
JobTotal int `yaml:"job-total,omitempty"`
|
||||
RawExpression yaml.Node `yaml:"-"` // a whole-value `strategy: ${{ }}`, see Strategy.resolve
|
||||
}
|
||||
|
||||
type rawStrategy Strategy
|
||||
|
||||
func (s *Strategy) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
*s = Strategy{RawExpression: *node}
|
||||
return nil
|
||||
}
|
||||
return node.Decode((*rawStrategy)(s))
|
||||
}
|
||||
|
||||
func (s Strategy) MarshalYAML() (any, error) {
|
||||
if s.RawExpression.Kind != 0 {
|
||||
return &s.RawExpression, nil
|
||||
}
|
||||
return rawStrategy(s), nil
|
||||
}
|
||||
|
||||
func (s Strategy) actStrategy() *model.Strategy {
|
||||
return &model.Strategy{FailFastString: s.FailFastString, MaxParallelString: s.MaxParallelString, RawMatrix: s.RawMatrix}
|
||||
}
|
||||
|
||||
// context is the strategy context, combination 0 of 1 without a matrix as on GitHub, and without job-index for an unexpanded matrix.
|
||||
func (s *Strategy) context() map[string]any {
|
||||
switch {
|
||||
case s == nil:
|
||||
return exprparser.StrategyContext(nil, 0, 0)
|
||||
case s.RawMatrix.Kind == 0 && s.RawExpression.Kind == 0:
|
||||
return exprparser.StrategyContext(s.actStrategy(), 0, 1)
|
||||
}
|
||||
return exprparser.StrategyContext(s.actStrategy(), s.JobIndex, s.JobTotal)
|
||||
}
|
||||
|
||||
type Defaults struct {
|
||||
@@ -250,20 +288,10 @@ type RunDefaults struct {
|
||||
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||
}
|
||||
|
||||
type WorkflowDispatchInput struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Required bool `yaml:"required"`
|
||||
Default string `yaml:"default"`
|
||||
Type string `yaml:"type"`
|
||||
Options []string `yaml:"options"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Name string
|
||||
acts map[string][]string
|
||||
schedules []map[string]string
|
||||
inputs []WorkflowDispatchInput
|
||||
}
|
||||
|
||||
func (evt *Event) IsSchedule() bool {
|
||||
@@ -278,10 +306,6 @@ func (evt *Event) Schedules() []map[string]string {
|
||||
return evt.schedules
|
||||
}
|
||||
|
||||
func (evt *Event) Inputs() []WorkflowDispatchInput {
|
||||
return evt.inputs
|
||||
}
|
||||
|
||||
func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) {
|
||||
w, err := ReadWorkflow(content)
|
||||
if err != nil {
|
||||
@@ -290,26 +314,30 @@ func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) {
|
||||
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) {
|
||||
actJob := &model.Job{}
|
||||
// newJobEvaluator evaluates against a stored job's contexts, with its single matrix combination.
|
||||
func newJobEvaluator(jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (expreval.Evaluator, error) {
|
||||
var strategy *Strategy
|
||||
var matrix map[string]any
|
||||
if job != nil {
|
||||
actJob.Strategy = &model.Strategy{
|
||||
FailFastString: job.Strategy.FailFastString,
|
||||
MaxParallelString: job.Strategy.MaxParallelString,
|
||||
RawMatrix: job.Strategy.RawMatrix,
|
||||
strategy = &job.Strategy
|
||||
rawMatrix := model.CloneYamlNode(job.Strategy.RawMatrix)
|
||||
replaceScalars(&rawMatrix, unescapeExpressions)
|
||||
matrixes, err := (&model.Job{Strategy: &model.Strategy{RawMatrix: rawMatrix}}).GetMatrixes()
|
||||
if err != nil {
|
||||
return expreval.Evaluator{}, err
|
||||
}
|
||||
if len(matrixes[0]) > 0 {
|
||||
matrix = matrixes[0]
|
||||
}
|
||||
}
|
||||
return expreval.New(NewInterpeter(jobID, strategy, matrix, model.GithubContextFromMap(gitCtx), results, vars, inputs).Evaluate), nil
|
||||
}
|
||||
|
||||
matrix := make(map[string]any)
|
||||
matrixes, err := matrixesOf(actJob)
|
||||
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) {
|
||||
evaluator, err := newJobEvaluator(jobID, job, gitCtx, results, vars, inputs)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if len(matrixes) > 0 {
|
||||
matrix = matrixes[0]
|
||||
}
|
||||
|
||||
evaluator := expreval.New(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs).Evaluate)
|
||||
var node yaml.Node
|
||||
if err := node.Encode(rc); err != nil {
|
||||
return "", false, fmt.Errorf("failed to encode concurrency: %w", err)
|
||||
@@ -327,40 +355,6 @@ func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCt
|
||||
return evaluated.Group, util.ParseYamlBool(evaluated.CancelInProgress), nil
|
||||
}
|
||||
|
||||
func toGitContext(input map[string]any) *model.GithubContext {
|
||||
gitContext := &model.GithubContext{
|
||||
EventPath: asString(input["event_path"]),
|
||||
Workflow: asString(input["workflow"]),
|
||||
RunID: asString(input["run_id"]),
|
||||
RunNumber: asString(input["run_number"]),
|
||||
Actor: asString(input["actor"]),
|
||||
Repository: asString(input["repository"]),
|
||||
EventName: asString(input["event_name"]),
|
||||
Sha: asString(input["sha"]),
|
||||
Ref: asString(input["ref"]),
|
||||
RefName: asString(input["ref_name"]),
|
||||
RefType: asString(input["ref_type"]),
|
||||
HeadRef: asString(input["head_ref"]),
|
||||
BaseRef: asString(input["base_ref"]),
|
||||
Token: asString(input["token"]),
|
||||
Workspace: asString(input["workspace"]),
|
||||
Action: asString(input["action"]),
|
||||
ActionPath: asString(input["action_path"]),
|
||||
ActionRef: asString(input["action_ref"]),
|
||||
ActionRepository: asString(input["action_repository"]),
|
||||
Job: asString(input["job"]),
|
||||
RepositoryOwner: asString(input["repository_owner"]),
|
||||
RetentionDays: asString(input["retention_days"]),
|
||||
}
|
||||
|
||||
event, ok := input["event"].(map[string]any)
|
||||
if ok {
|
||||
gitContext.Event = event
|
||||
}
|
||||
|
||||
return gitContext
|
||||
}
|
||||
|
||||
// workflowCallEvent is only fired by another workflow's `uses:`, so it is excluded from trigger detection.
|
||||
const workflowCallEvent = "workflow_call"
|
||||
|
||||
@@ -372,6 +366,9 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rawOn.ShortTag() != "!!str" || val == "" {
|
||||
return nil, fmt.Errorf("invalid event %q", val)
|
||||
}
|
||||
if val == workflowCallEvent {
|
||||
return []*Event{}, nil
|
||||
}
|
||||
@@ -421,18 +418,24 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
}
|
||||
schedules := make([]map[string]string, len(t))
|
||||
if k == "schedule" {
|
||||
if len(t) == 0 {
|
||||
return nil, errors.New("schedule must contain at least one cron entry")
|
||||
}
|
||||
for i, tt := range t {
|
||||
vv, ok := tt.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown on type(schedule): %#v", v)
|
||||
return nil, errors.New("unknown on type(schedule)")
|
||||
}
|
||||
schedules[i] = make(map[string]string, len(vv))
|
||||
for k, vvv := range vv {
|
||||
var ok bool
|
||||
if schedules[i][k], ok = vvv.(string); !ok {
|
||||
return nil, fmt.Errorf("unknown on type(schedule): %#v", v)
|
||||
return nil, errors.New("unknown on type(schedule)")
|
||||
}
|
||||
}
|
||||
if _, err := cron.ParseStandard(schedules[i]["cron"]); err != nil {
|
||||
return nil, fmt.Errorf("invalid cron %q: %w", schedules[i]["cron"], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,14 +447,14 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
schedules: schedules,
|
||||
})
|
||||
case yaml.MappingNode:
|
||||
// Keep combined include and ignore filters for existing Gitea workflows, although GitHub rejects them.
|
||||
acts := make(map[string][]string, len(v.Content)/2)
|
||||
var inputs []WorkflowDispatchInput
|
||||
expectedKey := true
|
||||
var act string
|
||||
for _, content := range v.Content {
|
||||
if expectedKey {
|
||||
if content.Kind != yaml.ScalarNode {
|
||||
return nil, fmt.Errorf("key type not string: %#v", content)
|
||||
return nil, errors.New("key type not string")
|
||||
}
|
||||
act = ""
|
||||
err := content.Decode(&act)
|
||||
@@ -476,48 +479,23 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
acts[act] = []string{t}
|
||||
case yaml.MappingNode:
|
||||
if k != "workflow_dispatch" || act != "inputs" {
|
||||
return nil, fmt.Errorf("map should only for workflow_dispatch but %s: %#v", act, content)
|
||||
return nil, fmt.Errorf("map should only for workflow_dispatch but %s", act)
|
||||
}
|
||||
|
||||
var key string
|
||||
for i, vv := range content.Content {
|
||||
if i%2 == 0 {
|
||||
if vv.Kind != yaml.ScalarNode {
|
||||
return nil, fmt.Errorf("key type not string: %#v", vv)
|
||||
}
|
||||
key = ""
|
||||
if err := vv.Decode(&key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if vv.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("key type not map(%s): %#v", key, vv)
|
||||
}
|
||||
|
||||
input := WorkflowDispatchInput{}
|
||||
if err := vv.Decode(&input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
input.Name = key
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
if err := content.Decode(new(map[string]model.WorkflowDispatchInput)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown on type: %#v", content)
|
||||
return nil, fmt.Errorf("unknown on type for %s", act)
|
||||
}
|
||||
}
|
||||
expectedKey = !expectedKey
|
||||
}
|
||||
if len(inputs) == 0 {
|
||||
inputs = nil
|
||||
}
|
||||
if len(acts) == 0 {
|
||||
acts = nil
|
||||
}
|
||||
res = append(res, &Event{
|
||||
Name: k,
|
||||
acts: acts,
|
||||
inputs: inputs,
|
||||
Name: k,
|
||||
acts: acts,
|
||||
})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown on type: %v", v.Kind)
|
||||
@@ -529,35 +507,12 @@ func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// EvaluateJobIfExpression evaluates a job's `if:`.
|
||||
func EvaluateJobIfExpression(jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any, matrixDeferred bool) (bool, error) {
|
||||
actJob := &model.Job{
|
||||
Strategy: &model.Strategy{
|
||||
FailFastString: job.Strategy.FailFastString,
|
||||
MaxParallelString: job.Strategy.MaxParallelString,
|
||||
RawMatrix: job.Strategy.RawMatrix,
|
||||
},
|
||||
// EvaluateJobIfExpression evaluates a job's `if:`, which github.com decides before the matrix, so without the matrix and strategy contexts.
|
||||
func EvaluateJobIfExpression(jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (bool, error) {
|
||||
if unavailable := unavailableContext(IfExpression(job.If.Value), jobConditionContexts); unavailable != "" { // only a job stored before its conditions were validated
|
||||
return false, fmt.Errorf("job %s: Unrecognized named-value: '%s', update the workflow and trigger a new run", jobID, unavailable)
|
||||
}
|
||||
// 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.
|
||||
//
|
||||
// A deferred-matrix placeholder is the exception: its combinations do not exist yet, and reading the
|
||||
// raw matrix here would either fail outright (an `include` that is still a scalar expression) or bind
|
||||
// `matrix.*` to the expression's own source text. Leaving it nil is safe: the caller checks
|
||||
// ExpressionReadsMatrix first, so an `if:` that reads `matrix.*` is deferred to the post-expansion pass.
|
||||
var matrix map[string]any
|
||||
if !matrixDeferred {
|
||||
matrixes, err := matrixesOf(actJob)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(matrixes) > 0 && len(matrixes[0]) > 0 {
|
||||
matrix = matrixes[0]
|
||||
}
|
||||
}
|
||||
evaluator := expreval.New(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs).Evaluate)
|
||||
evaluator := expreval.New(NewInterpeter(jobID, nil, nil, model.GithubContextFromMap(gitCtx), results, vars, inputs).Evaluate)
|
||||
return evaluator.EvalBool(job.If.Value, exprparser.DefaultStatusCheckSuccess)
|
||||
}
|
||||
|
||||
@@ -593,12 +548,3 @@ func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) {
|
||||
|
||||
return scalars, datas, nil
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
} else if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user