Files
Gitea/modules/actions/scoped_workflows.go
T
bircni 8161479fde fix(actions): resolve pull_request_target reusable workflows at the base commit (#38886)
For a `pull_request_target` (PRT) run, Gitea loads the top-level
workflow from the trusted base branch, but any local reusable workflow
it calls (`uses: ./...`) was read from the PR **head** commit, which the
fork author controls.

## Fix

**Record the source commit where the content is read.**
`DetectedWorkflow` now carries a `SourceCommitSHA` filled in next to
`Content`, so the PRT detection pass at the base commit records the base
SHA automatically.

**Defense in depth.** `loadReusableWorkflowSource` pins the PR base
commit for a PRT run's local `uses: ./...` rather than trusting the
stored SHA. This also covers runs recorded before this change, whose
rows still hold the head SHA and would otherwise resolve from the fork
on rerun.

Existing run rows are not migrated.

---------

Co-authored-by: Zettat <zettat123@gmail.com>
2026-08-12 19:03:44 +02:00

94 lines
3.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
webhook_module "gitea.dev/modules/webhook"
)
// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit.
func ListScopedWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) {
return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.ScopedWorkflowDirs)
}
// ParsedScopedWorkflow is one scoped workflow's source-side parse result
type ParsedScopedWorkflow struct {
EntryName string
DisplayName string // the workflow `name:` or base file name
Content []byte // raw content of the workflow file
Events []*jobparser.Event // decoded `on:` events
}
// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS).
func ParseScopedWorkflows(ctx context.Context, gitRepo *git.Repository, sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) {
_, entries, err := ListScopedWorkflows(ctx, gitRepo, sourceCommit)
if err != nil {
return nil, err
}
parsed := make([]*ParsedScopedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(ctx, gitRepo, entry)
if err != nil {
return nil, err
}
// one workflow may have multiple events
events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid scoped workflow %q: %v", entry.Name(), err)
continue
}
parsed = append(parsed, &ParsedScopedWorkflow{
EntryName: entry.Name(),
DisplayName: WorkflowDisplayName(entry.Name(), content),
Content: content,
Events: events,
})
}
return parsed, nil
}
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
func MatchScopedWorkflows(
ctx context.Context,
parsed []*ParsedScopedWorkflow,
sourceCommitSHA string,
consumerGitRepo *git.Repository,
consumerCommit *git.Commit,
inputEvent webhook_module.HookEventType,
payload api.Payloader,
) (matched, filtered []*DetectedWorkflow) {
for _, p := range parsed {
for _, evt := range p.Events {
if evt.IsSchedule() {
// schedule is a non-target for scoped workflows
continue
}
dwf := &DetectedWorkflow{
EntryName: p.EntryName,
TriggerEvent: evt,
Content: p.Content,
SourceCommitSHA: sourceCommitSHA,
}
switch detectWorkflowMatch(ctx, consumerGitRepo, consumerCommit, inputEvent, payload, evt) {
case detectMatched:
matched = append(matched, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
}
}
return matched, filtered
}