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:
silverwind
2026-09-25 00:06:42 +02:00
committed by GitHub
parent 64f31d9b70
commit f757631a47
59 changed files with 1509 additions and 2223 deletions
+41 -56
View File
@@ -4,10 +4,15 @@
package actions
import (
"cmp"
"context"
"fmt"
"maps"
"slices"
"strconv"
"gitea.dev/actionslib/pkg/expreval"
"gitea.dev/actionslib/pkg/exprparser"
"gitea.dev/actionslib/pkg/model"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
@@ -96,6 +101,11 @@ func GenerateGiteaContext(ctx context.Context, run *actions_model.ActionRun, att
"workflow": run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository.
"workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action.
"actor_id": strconv.FormatInt(run.TriggerUserID, 10),
"repository_id": strconv.FormatInt(run.RepoID, 10),
"repository_owner_id": strconv.FormatInt(run.Repo.OwnerID, 10),
"workflow_sha": run.WorkflowCommitSHA,
// additional contexts
"gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(),
}
@@ -164,9 +174,9 @@ type TaskNeed struct {
// FindTaskNeeds finds the `needs` for the task by the task's job.
// Lookup is scoped to the same ParentJobID.
func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*TaskNeed, error) {
func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*TaskNeed, map[string][]*actions_model.ActionRunJob, error) {
if len(job.Needs) == 0 {
return nil, nil //nolint:nilnil // return nil when the job has no needs
return nil, nil, nil
}
needs := container.SetOf(job.Needs...)
@@ -178,7 +188,7 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st
jobs, err := db.Find[actions_model.ActionRunJob](ctx, findOpts)
if err != nil {
return nil, fmt.Errorf("FindRunJobs: %w", err)
return nil, nil, fmt.Errorf("FindRunJobs: %w", err)
}
jobIDJobs := make(map[string][]*actions_model.ActionRunJob)
@@ -199,9 +209,10 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st
if !needs.Contains(jobID) {
continue
}
sortJobsByCompletion(jobsWithSameID)
var jobOutputs map[string]string
for _, candidate := range jobsWithSameID {
if !candidate.Status.IsDone() {
if !candidate.Status.IsDone() || candidate.IsReusableCaller && candidate.Status != actions_model.StatusSuccess {
continue
}
var outputs map[string]string
@@ -212,7 +223,7 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st
outputs, err = loadJobTaskOutputs(ctx, candidate)
}
if err != nil {
return nil, err
return nil, nil, err
}
if len(jobOutputs) == 0 {
jobOutputs = outputs
@@ -225,7 +236,7 @@ func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[st
Result: actions_model.AggregateJobStatus(jobsWithSameID),
}
}
return ret, nil
return ret, jobIDJobs, nil
}
// computeReusableCallerOutputs returns the workflow_call outputs of a reusable caller by recursing into its child subtree.
@@ -240,7 +251,7 @@ func computeReusableCallerOutputs(ctx context.Context, caller *actions_model.Act
if err := caller.LoadRun(ctx); err != nil {
return nil, err
}
wcSpec, err := jobparser.ParseWorkflowCallSpec(caller.ReusableWorkflowContent)
wcSpec, err := jobparser.ParseWorkflowCallConfig(caller.ReusableWorkflowContent)
if err != nil {
return nil, err
}
@@ -249,7 +260,8 @@ func computeReusableCallerOutputs(ctx context.Context, caller *actions_model.Act
}
// Per-job outputs over the children of this caller.
jobOutputs := make(jobparser.JobOutputs, len(directChildren))
sortJobsByCompletion(directChildren)
jobOutputs := make(map[string]*model.WorkflowCallResult, len(directChildren))
for _, child := range directChildren {
var outs map[string]string
switch {
@@ -262,10 +274,9 @@ func computeReusableCallerOutputs(ctx context.Context, caller *actions_model.Act
return nil, err
}
if existing, ok := jobOutputs[child.JobID]; ok {
jobOutputs[child.JobID] = mergeTwoOutputs(outs, existing)
} else {
jobOutputs[child.JobID] = outs
outs = mergeTwoOutputs(outs, existing.Outputs)
}
jobOutputs[child.JobID] = &model.WorkflowCallResult{Outputs: outs}
}
// build contexts for evaluating outputs
@@ -288,7 +299,19 @@ func computeReusableCallerOutputs(ctx context.Context, caller *actions_model.Act
}
}
return jobparser.EvaluateWorkflowCallOutputs(wcSpec, gitCtx.ToGitHubContext(), vars, inputs, jobOutputs)
// See `on.workflow_call.outputs.<output_id>.value` in https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability
return expreval.New(exprparser.NewInterpeter(&exprparser.EvaluationEnvironment{
Github: gitCtx.ToGitHubContext(),
Jobs: &jobOutputs,
Vars: vars,
Inputs: inputs,
}, exprparser.Config{}).Evaluate).EvaluateWorkflowCallOutputs(wcSpec)
}
func sortJobsByCompletion(jobs []*actions_model.ActionRunJob) {
slices.SortFunc(jobs, func(left, right *actions_model.ActionRunJob) int {
return cmp.Or(cmp.Compare(left.Stopped, right.Stopped), cmp.Compare(left.ID, right.ID))
})
}
// loadJobTaskOutputs returns the task-output map of `job`.
@@ -312,56 +335,18 @@ func loadJobTaskOutputs(ctx context.Context, job *actions_model.ActionRunJob) (m
// Values with the same output name may be overridden. The user should ensure the output names are unique.
// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job
func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
ret := make(map[string]string, len(o1))
ret := make(map[string]string, len(o1)+len(o2))
maps.Copy(ret, o2)
for k1, v1 := range o1 {
if len(v1) > 0 {
if len(v1) > 0 || ret[k1] == "" {
ret[k1] = v1
} else {
ret[k1] = o2[k1]
}
}
return ret
}
func contextMapValueOrDefault[T any](m map[string]any, key string, defaultValue T) T {
if value, ok := m[key]; ok {
if v, ok := value.(T); ok {
return v
}
}
return defaultValue
}
func (g *GiteaContext) ToGitHubContext() *model.GithubContext {
return &model.GithubContext{
Event: contextMapValueOrDefault(*g, "event", map[string]any(nil)),
EventPath: contextMapValueOrDefault(*g, "event_path", ""),
Workflow: contextMapValueOrDefault(*g, "workflow", ""),
RunID: contextMapValueOrDefault(*g, "run_id", ""),
RunNumber: contextMapValueOrDefault(*g, "run_number", ""),
Actor: contextMapValueOrDefault(*g, "actor", ""),
Repository: contextMapValueOrDefault(*g, "repository", ""),
EventName: contextMapValueOrDefault(*g, "event_name", ""),
Sha: contextMapValueOrDefault(*g, "sha", ""),
Ref: contextMapValueOrDefault(*g, "ref", ""),
RefName: contextMapValueOrDefault(*g, "ref_name", ""),
RefType: contextMapValueOrDefault(*g, "ref_type", ""),
HeadRef: contextMapValueOrDefault(*g, "head_ref", ""),
BaseRef: contextMapValueOrDefault(*g, "base_ref", ""),
Token: "", // deliberately omitted for security
Workspace: contextMapValueOrDefault(*g, "workspace", ""),
Action: contextMapValueOrDefault(*g, "action", ""),
ActionPath: contextMapValueOrDefault(*g, "action_path", ""),
ActionRef: contextMapValueOrDefault(*g, "action_ref", ""),
ActionRepository: contextMapValueOrDefault(*g, "action_repository", ""),
Job: contextMapValueOrDefault(*g, "job", ""),
JobName: "", // not present in GiteaContext
RepositoryOwner: contextMapValueOrDefault(*g, "repository_owner", ""),
RetentionDays: contextMapValueOrDefault(*g, "retention_days", ""),
RunnerPerflog: "", // not present in GiteaContext
RunnerTrackingID: "", // not present in GiteaContext
ServerURL: contextMapValueOrDefault(*g, "server_url", ""),
APIURL: contextMapValueOrDefault(*g, "api_url", ""),
GraphQLURL: contextMapValueOrDefault(*g, "graphql_url", ""),
}
githubCtx := model.GithubContextFromMap(*g)
githubCtx.Token = "" // deliberately omitted for security
return githubCtx
}