gitforge/services/actions/secret.go
Mathieu Fenniak 6b3359f016 feat: evaluate action job's if on the server-side when possible (#13030)
Fixes #12937.

The intent of this change is to allow Forgejo to evaluate `if` without having to send jobs to a runner.  When you send a job to a runner just for it to return "skipped!", it takes up 1 runner capacity for a `fetch_interval` period, which can be avoided if Forgejo can evaluate the `if` condition itself.

### Tests for Go changes

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13030
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
2026-06-16 00:02:57 +02:00

133 lines
4.6 KiB
Go

// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package actions
import (
"context"
"errors"
"fmt"
actions_model "forgejo.org/models/actions"
secret_model "forgejo.org/models/secret"
actions_module "forgejo.org/modules/actions"
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
)
func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
secrets, err := getSecretsOfJob(ctx, task.Job)
secrets["GITHUB_TOKEN"] = task.Token
secrets["GITEA_TOKEN"] = task.Token
secrets["FORGEJO_TOKEN"] = task.Token
return secrets, err
}
func getSecretsOfJob(ctx context.Context, job *actions_model.ActionRunJob) (map[string]string, error) {
isInnerWorkflowCall, err := job.IsWorkflowCallInnerJob()
if err != nil {
return nil, err
}
err = job.LoadRun(ctx)
if err != nil {
return nil, fmt.Errorf("failure to load job run: %w", err)
}
if isInnerWorkflowCall {
return getSecretsOfInnerWorkflowCall(ctx, job)
}
if job.Run.IsForkPullRequest && job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
// ignore secrets for fork pull request, except GITHUB_TOKEN, GITEA_TOKEN and FORGEJO_TOKEN which are automatically generated.
// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
return map[string]string{}, nil
}
err = job.Run.LoadRepo(ctx)
if err != nil {
return nil, err
}
jobSecrets, err := secret_model.FetchActionSecrets(ctx, job.Run.Repo.OwnerID, job.Run.RepoID)
if err != nil {
// Don't return error details, just in case they contain confidential details and error reaches a user;
// FetchActionSecrets logs all errors to the server log.
return nil, errors.New("failure to fetch secrets")
}
return jobSecrets, nil
}
func getSecretsOfInnerWorkflowCall(ctx context.Context, job *actions_model.ActionRunJob) (map[string]string, error) {
// Workflow calls can have two different behaviours -- they can either have `secrets: inherit` in which case we get
// the secrets of the caller and pass them in, or, they can have `secrets: { ... }` with key-values that need to be
// evaluated in the context of the parent (that is, `${{ secret.example_secret }}` would reference `example_secret`
// from the caller's secrets).
//
// In either case, we need the caller job's secrets, and we need the caller job's workflow definition to find out
// how they wanted secrets defined for this workflow call.
outerWorkflowCall, err := job.Run.FindOuterWorkflowCall(ctx, job)
if err != nil {
return nil, fmt.Errorf("failure to find outer workflow call: %w", err)
}
outerSecrets, err := getSecretsOfJob(ctx, outerWorkflowCall)
if err != nil {
return nil, err
}
outerWorkflowPayload, err := outerWorkflowCall.DecodeWorkflowPayload()
if err != nil {
return nil, err
}
_, outerJob := outerWorkflowPayload.Job()
if outerJob.InheritSecrets() {
return outerSecrets, nil
}
// Gather all the data that is needed to perform an expression evaluation of the parent job's secrets context:
err = outerWorkflowCall.LoadRun(ctx)
if err != nil {
return nil, fmt.Errorf("failure to load job's run: %w", err)
}
err = outerWorkflowCall.Run.LoadRepo(ctx)
if err != nil {
return nil, fmt.Errorf("failure to load run's repo: %w", err)
}
githubContext := generateGiteaContextForRun(outerWorkflowCall.Run)
taskNeeds, err := FindTaskNeeds(ctx, outerWorkflowCall)
if err != nil {
return nil, fmt.Errorf("failure evaluating 'needs' for job: %w", err)
}
needs := make([]string, 0, len(taskNeeds))
jobResults := make(map[string]string, len(taskNeeds))
jobOutputs := make(map[string]map[string]string, len(taskNeeds))
for jobID, n := range taskNeeds {
needs = append(needs, jobID)
jobResults[jobID] = n.Result.String()
jobOutputs[jobID] = n.Outputs
}
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
if err != nil {
return nil, fmt.Errorf("failure evaluating 'vars' for run: %w", err)
}
var inputs map[string]any
if outerWorkflowCall.Run.TriggerEvent == actions_module.GithubEventWorkflowDispatch {
inputs = getRunInputs(outerWorkflowCall.Run)
}
jobSecrets := jobparser.EvaluateWorkflowCallSecrets(&jobparser.EvaluateWorkflowCallSecretsArgs{
CallerWorkflow: outerWorkflowPayload,
CallerSecrets: outerSecrets,
GitCtx: githubContext,
Vars: vars,
Needs: needs,
JobResults: jobResults,
JobOutputs: jobOutputs,
JobInputs: inputs,
})
return jobSecrets, nil
}