mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 06:20:24 +00:00
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>
This commit is contained in:
parent
d8a5ee81fb
commit
9b2f7c557b
11 changed files with 654 additions and 33 deletions
|
|
@ -208,6 +208,30 @@ func (run *ActionRun) SetDefaultConcurrencyGroup() {
|
|||
))
|
||||
}
|
||||
|
||||
func (run *ActionRun) FindOuterWorkflowCall(ctx context.Context, innerCall *ActionRunJob) (*ActionRunJob, error) {
|
||||
allJobs, err := GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failure to get run jobs: %w", err)
|
||||
}
|
||||
if innerCall.workflowPayloadDecoded == nil || innerCall.workflowPayloadDecoded.Metadata.WorkflowCallParent == "" {
|
||||
return nil, errors.New("invalid state for FindOuterWorkflowCall")
|
||||
}
|
||||
parent := innerCall.workflowPayloadDecoded.Metadata.WorkflowCallParent
|
||||
for _, job := range allJobs {
|
||||
if job.ID == innerCall.ID {
|
||||
continue
|
||||
}
|
||||
swf, err := job.DecodeWorkflowPayload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if swf.Metadata.WorkflowCallID == parent {
|
||||
return job, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no workflow call with ID %s found in run %d", parent, run.ID)
|
||||
}
|
||||
|
||||
func actionsCountOpenCacheKey(repoID int64) string {
|
||||
return fmt.Sprintf("Actions:CountOpenActionRuns:%d", repoID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -272,3 +272,58 @@ jobs:
|
|||
// Expect job with an incomplete runs-on to be StatusBlocked:
|
||||
assert.Equal(t, StatusBlocked, job.Status)
|
||||
}
|
||||
|
||||
func TestActionRun_FindOuterWorkflowCall(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
pullRequestPosterID := int64(4)
|
||||
repoID := int64(10)
|
||||
pullRequestID := int64(2)
|
||||
run := &ActionRun{
|
||||
RepoID: repoID,
|
||||
PullRequestID: pullRequestID,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
}
|
||||
|
||||
workflowRaw := []byte(`
|
||||
jobs:
|
||||
outer-job:
|
||||
uses: ./.forgejo/workflows/reusable.yml
|
||||
`)
|
||||
workflows, err := jobparser.Parse(workflowRaw, false,
|
||||
jobparser.WithJobOutputs(map[string]map[string]string{}),
|
||||
jobparser.ExpandLocalReusableWorkflows(func(job *jobparser.Job, path string) ([]byte, error) {
|
||||
return []byte(`
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
inner-job-1:
|
||||
runs-on: debian
|
||||
steps: []
|
||||
inner-job-2:
|
||||
runs-on: debian
|
||||
steps: []
|
||||
`), nil
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, InsertRun(t.Context(), run, workflows))
|
||||
|
||||
jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: run.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 3)
|
||||
|
||||
for _, j := range jobs {
|
||||
t.Run(j.Name, func(t *testing.T) {
|
||||
_, err := j.DecodeWorkflowPayload()
|
||||
require.NoError(t, err)
|
||||
outer, err := run.FindOuterWorkflowCall(t.Context(), j)
|
||||
if j.Name == "outer-job" {
|
||||
require.ErrorContains(t, err, "invalid state for FindOuterWorkflowCall")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, outer)
|
||||
assert.Equal(t, "outer-job", outer.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
actions_module "forgejo.org/modules/actions"
|
||||
"forgejo.org/modules/keying"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
|
@ -120,28 +118,17 @@ func (s *Secret) SetSecret(data string) {
|
|||
s.Data = keying.ActionSecret.Encrypt([]byte(data), keying.ColumnAndID("data", s.ID))
|
||||
}
|
||||
|
||||
func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) {
|
||||
func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]string, error) {
|
||||
secrets := map[string]string{}
|
||||
|
||||
secrets["GITHUB_TOKEN"] = task.Token
|
||||
secrets["GITEA_TOKEN"] = task.Token
|
||||
secrets["FORGEJO_TOKEN"] = task.Token
|
||||
|
||||
if task.Job.Run.IsForkPullRequest && task.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 secrets, nil
|
||||
}
|
||||
|
||||
ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
|
||||
ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: ownerID})
|
||||
if err != nil {
|
||||
log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
|
||||
log.Error("find secrets of owner %v: %v", ownerID, err)
|
||||
return nil, err
|
||||
}
|
||||
repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID})
|
||||
repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: repoID})
|
||||
if err != nil {
|
||||
log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
|
||||
log.Error("find secrets of repo %v: %v", repoID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ package secret
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/actions"
|
||||
"forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/keying"
|
||||
"forgejo.org/modules/util"
|
||||
|
|
@ -85,17 +83,8 @@ func TestInsertEncryptedSecret(t *testing.T) {
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("Get secrets", func(t *testing.T) {
|
||||
secrets, err := GetSecretsOfTask(t.Context(), &actions.ActionTask{
|
||||
Job: &actions.ActionRunJob{
|
||||
Run: &actions.ActionRun{
|
||||
RepoID: 1,
|
||||
Repo: &repo.Repository{
|
||||
OwnerID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
t.Run("FetchActionSecrets", func(t *testing.T) {
|
||||
secrets, err := FetchActionSecrets(t.Context(), 2, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "some owner secret", secrets["OWNER_SECRET"])
|
||||
assert.Equal(t, "some repository secret", secrets["REPO_SECRET"])
|
||||
|
|
|
|||
94
services/actions/TestGetSecretsOfJob/action_run.yml
Normal file
94
services/actions/TestGetSecretsOfJob/action_run.yml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Supporting data for Case 600
|
||||
-
|
||||
id: 900
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 4
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: false
|
||||
|
||||
# Supporting data for Case 601
|
||||
-
|
||||
id: 901
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 5
|
||||
trigger_event: "pull_request_target"
|
||||
is_fork_pull_request: false
|
||||
|
||||
# Supporting data for Case 602
|
||||
-
|
||||
id: 902
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 6
|
||||
trigger_event: "pull_request_target"
|
||||
is_fork_pull_request: true
|
||||
|
||||
# Supporting data for Case 603
|
||||
-
|
||||
id: 903
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 7
|
||||
trigger_event: "pull_request"
|
||||
is_fork_pull_request: false
|
||||
|
||||
# Supporting data for Case 604
|
||||
-
|
||||
id: 904
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 8
|
||||
trigger_event: "pull_request"
|
||||
is_fork_pull_request: true
|
||||
|
||||
# Supporting data for Case 605
|
||||
-
|
||||
id: 905
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 9
|
||||
trigger_event: "pull_request"
|
||||
is_fork_pull_request: false
|
||||
|
||||
# Supporting data for Case 607
|
||||
-
|
||||
id: 906
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
workflow_id: "running.yaml"
|
||||
index: 10
|
||||
trigger_event: "pull_request"
|
||||
is_fork_pull_request: false
|
||||
|
||||
# Supporting data for Case 610
|
||||
-
|
||||
id: 907
|
||||
title: "running"
|
||||
owner_id: 2
|
||||
repo_id: 63
|
||||
ref: "refs/heads/main"
|
||||
workflow_id: "running.yaml"
|
||||
index: 11
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: false
|
||||
event_payload: |
|
||||
{
|
||||
"inputs": {
|
||||
"some_wd_input": "some_wd_input_value"
|
||||
}
|
||||
}
|
||||
192
services/actions/TestGetSecretsOfJob/action_run_job.yml
Normal file
192
services/actions/TestGetSecretsOfJob/action_run_job.yml
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Case 600 -- on:push workflow with some secrets
|
||||
-
|
||||
id: 600
|
||||
run_id: 900
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
|
||||
# Case 601 -- on: pull_request_target workflow, local PR (not fork)
|
||||
-
|
||||
id: 601
|
||||
run_id: 901
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request_target:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
|
||||
# Case 602 -- on: pull_request_target workflow, fork PR
|
||||
-
|
||||
id: 602
|
||||
run_id: 902
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request_target:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
|
||||
# Case 603 -- on: pull_request workflow, local PR (not fork)
|
||||
-
|
||||
id: 603
|
||||
run_id: 903
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
|
||||
# Case 604 -- on: pull_request workflow, fork PR
|
||||
-
|
||||
id: 604
|
||||
run_id: 904
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
|
||||
# Case 605 -- workflow call inner job, inherit secrets, 606 is the outer job
|
||||
-
|
||||
id: 605
|
||||
run_id: 905
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request_target:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
__metadata:
|
||||
workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
-
|
||||
id: 606
|
||||
run_id: 905
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request_target:
|
||||
jobs:
|
||||
invoke-reusable:
|
||||
uses: ./.forgejo/workflows/produce.yml
|
||||
secrets: inherit
|
||||
__metadata:
|
||||
workflow_call_id: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
|
||||
# Case 607 -- workflow call two layer inner job, inherit secrets, 607->608->609
|
||||
-
|
||||
id: 607
|
||||
run_id: 906
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
__metadata:
|
||||
workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
-
|
||||
id: 608
|
||||
run_id: 906
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
jobs:
|
||||
invoke-reusable:
|
||||
uses: ./.forgejo/workflows/produce-specific.yml
|
||||
secrets: inherit
|
||||
__metadata:
|
||||
workflow_call_id: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
workflow_call_parent: 1976193ec4c48a92ba58816b34116272f5b3a612b91494956e5b53ee70b8714f
|
||||
-
|
||||
id: 609
|
||||
run_id: 906
|
||||
workflow_payload: |
|
||||
"on":
|
||||
pull_request:
|
||||
jobs:
|
||||
invoke-reusable:
|
||||
uses: ./.forgejo/workflows/produce.yml
|
||||
secrets:
|
||||
secret_1: ${{ secrets.secret_1 }} -- but are you sure?
|
||||
__metadata:
|
||||
workflow_call_id: 1976193ec4c48a92ba58816b34116272f5b3a612b91494956e5b53ee70b8714f
|
||||
|
||||
# Case 610 -- workflow call specifically defined secrets, 611 is the outer job, 612 is another job in the same workflow
|
||||
-
|
||||
id: 610
|
||||
run_id: 907
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: docker
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
__metadata:
|
||||
workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
-
|
||||
id: 611
|
||||
run_id: 907
|
||||
needs: '["provide-outputs"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
invoke-reusable:
|
||||
uses: ./.forgejo/workflows/produce-specific.yml
|
||||
secrets:
|
||||
forgejo: context forgejo = ${{ forgejo.ref }}
|
||||
inputs: context inputs = ${{ inputs.some_wd_input }}
|
||||
matrix: context matrix = ${{ matrix.some-dimension }}
|
||||
needs: context needs = ${{ needs.provide-outputs.outputs.some-output }}
|
||||
secrets: context secrets = ${{ secrets.secret_1 }}
|
||||
strategy: context strategy = ${{ strategy.fail-fast }}
|
||||
vars: context vars = ${{ vars.repo_var }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
some-dimension:
|
||||
- some-dimension-value
|
||||
__metadata:
|
||||
workflow_call_id: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed
|
||||
-
|
||||
id: 612
|
||||
run_id: 907
|
||||
job_id: provide-outputs
|
||||
status: 1 # success
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
provide-outputs:
|
||||
steps: []
|
||||
task_id: 100
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Supporting data for Case 610
|
||||
-
|
||||
id: 100
|
||||
task_id: 100
|
||||
output_key: some-output
|
||||
output_value: 'abcdefghijklmnopqrstuvwxyz'
|
||||
22
services/actions/TestGetSecretsOfJob/action_variable.yml
Normal file
22
services/actions/TestGetSecretsOfJob/action_variable.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Case w/ action_run_job.id = 601
|
||||
-
|
||||
id: 1001
|
||||
name: REPO_VAR
|
||||
owner_id: 0
|
||||
repo_id: 63
|
||||
data: "this is a repo variable"
|
||||
created_unix: 1737000000
|
||||
-
|
||||
id: 1002
|
||||
name: ORG_VAR
|
||||
owner_id: 2
|
||||
repo_id: 0
|
||||
data: "this is an org variable"
|
||||
created_unix: 1737000000
|
||||
-
|
||||
id: 1003
|
||||
name: GLOBAL_VAR
|
||||
owner_id: 0
|
||||
repo_id: 0
|
||||
data: "this is a global variable"
|
||||
created_unix: 1737000000
|
||||
145
services/actions/secret.go
Normal file
145
services/actions/secret.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// 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
|
||||
}
|
||||
108
services/actions/secret_test.go
Normal file
108
services/actions/secret_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
secret_model "forgejo.org/models/secret"
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetSecretsOfJob(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
runJobID int64
|
||||
secrets map[string]string
|
||||
}{
|
||||
{
|
||||
name: "push run",
|
||||
runJobID: 600,
|
||||
secrets: map[string]string{
|
||||
"SECRET_1": "the sky is blue",
|
||||
"SECRET_2": "the ocean is also blue",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on: pull_request_target workflow, local PR (not fork)",
|
||||
runJobID: 601,
|
||||
secrets: map[string]string{
|
||||
"SECRET_1": "the sky is blue",
|
||||
"SECRET_2": "the ocean is also blue",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on: pull_request_target workflow, fork PR",
|
||||
runJobID: 602,
|
||||
secrets: map[string]string{
|
||||
"SECRET_1": "the sky is blue",
|
||||
"SECRET_2": "the ocean is also blue",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on: pull_request workflow, local PR (not fork)",
|
||||
runJobID: 603,
|
||||
secrets: map[string]string{
|
||||
"SECRET_1": "the sky is blue",
|
||||
"SECRET_2": "the ocean is also blue",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on: pull_request workflow, fork PR",
|
||||
runJobID: 604,
|
||||
secrets: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "workflow call inner job, inherit secrets",
|
||||
runJobID: 605,
|
||||
secrets: map[string]string{
|
||||
"SECRET_1": "the sky is blue",
|
||||
"SECRET_2": "the ocean is also blue",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workflow call two layer inner job, inherit secrets",
|
||||
runJobID: 607,
|
||||
secrets: map[string]string{
|
||||
// Even though we're 'inherit' in this case, we're inheriting from the parent call which is a subset
|
||||
// (and modification) of the secrets -- so shouldn't see SECRET_2.
|
||||
"SECRET_1": "the sky is blue -- but are you sure?",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workflow call inner job, defined secrets",
|
||||
runJobID: 610,
|
||||
secrets: map[string]string{
|
||||
"FORGEJO": "context forgejo = refs/heads/main",
|
||||
"INPUTS": "context inputs = some_wd_input_value",
|
||||
"MATRIX": "context matrix = some-dimension-value",
|
||||
"NEEDS": "context needs = abcdefghijklmnopqrstuvwxyz",
|
||||
"SECRETS": "context secrets = the sky is blue",
|
||||
"STRATEGY": "context strategy = false",
|
||||
"VARS": "context vars = this is a repo variable",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestGetSecretsOfJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Due to encryption, more maintainable to do this rather than create secrets in fixture data
|
||||
_, err := secret_model.InsertEncryptedSecret(t.Context(), 2, 0, "secret_1", "the sky is blue")
|
||||
require.NoError(t, err)
|
||||
_, err = secret_model.InsertEncryptedSecret(t.Context(), 0, 63, "secret_2", "the ocean is also blue")
|
||||
require.NoError(t, err)
|
||||
|
||||
runJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: tt.runJobID})
|
||||
actualSecrets, err := getSecretsOfJob(t.Context(), runJob)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.secrets, actualSecrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
secret_model "forgejo.org/models/secret"
|
||||
"forgejo.org/modules/timeutil"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
|
|
@ -39,7 +38,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
|
|||
}
|
||||
job = t.Job
|
||||
|
||||
secrets, err := secret_model.GetSecretsOfTask(ctx, t)
|
||||
secrets, err := getSecretsOfTask(ctx, t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetSecretsOfTask: %w", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue