forgejo/services/actions/secret.go
Mathieu Fenniak 9b2f7c557b
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
feat: support jobs.<job_id>.secrets with reusable workflow expansion (#10627)
Follow-up to #10525; adds support for `jobs.<job_id>.secrets` to expanded reusable workflows (when no `runs-on` is specified in a job that `uses: ...` another workflow).

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).
- **end-to-end testing**: [prepared, PR n](https://code.forgejo.org/forgejo/end-to-end/pulls/1351)

### Documentation

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

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10627
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2025-12-30 17:33:21 +01:00

145 lines
5.1 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"
"forgejo.org/modules/json"
"forgejo.org/modules/structs"
"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 {
// workflow_dispatch inputs are stored in the event payload
var dispatchPayload *structs.WorkflowDispatchPayload
err := json.Unmarshal([]byte(outerWorkflowCall.Run.EventPayload), &dispatchPayload)
if err != nil {
return nil, fmt.Errorf("failure reading workflow dispatch payload: %w", err)
}
// transition from map[string]string to map[string]any...
inputs = make(map[string]any, len(dispatchPayload.Inputs))
for k, v := range dispatchPayload.Inputs {
inputs[k] = v
}
}
jobSecrets := jobparser.EvaluateWorkflowCallSecrets(&jobparser.EvaluateWorkflowCallSecretsArgs{
CallerWorkflow: outerWorkflowPayload,
CallerSecrets: outerSecrets,
GitCtx: githubContext,
Vars: vars,
Needs: needs,
JobResults: jobResults,
JobOutputs: jobOutputs,
JobInputs: inputs,
})
return jobSecrets, nil
}