mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-26 23:47:03 +00:00
Enhance GetActionWorkflow to support fallback references (#37189)
If a workflow is not in default branch the hooks could not be detected Fixes #37169 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
@@ -5,6 +5,7 @@ package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
@@ -805,7 +807,10 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref))
|
||||
if err != nil && errors.Is(err, util.ErrNotExist) {
|
||||
convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("GetActionWorkflow: %v", err)
|
||||
return
|
||||
|
||||
109
services/convert/action_test.go
Normal file
109
services/convert/action_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// buildWorkflowTestRepo creates a temporary git repository for testing GetActionWorkflow.
|
||||
// The default branch "main" has no workflow files; "feature" and "release-v1" each add their own workflow file.
|
||||
func buildWorkflowTestRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
ctx := t.Context()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, _, err := gitcmd.NewCommand("init").WithDir(tmpDir).RunStdString(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
readme := "readme"
|
||||
featureWF := "on: [push]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo test\n"
|
||||
releaseWF := "on: [push]\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - run: echo release\n"
|
||||
|
||||
// Build a git fast-import stream:
|
||||
// :4 = initial commit on main (README.md only)
|
||||
// :5 = feature branch commit (adds feature workflow)
|
||||
// :6 = release commit from :4 (adds release workflow, tagged release-v1, not on main)
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "blob\nmark :1\ndata %d\n%s\n", len(readme), readme)
|
||||
fmt.Fprintf(&sb, "blob\nmark :2\ndata %d\n%s\n", len(featureWF), featureWF)
|
||||
fmt.Fprintf(&sb, "blob\nmark :3\ndata %d\n%s\n", len(releaseWF), releaseWF)
|
||||
fmt.Fprintf(&sb, "commit refs/heads/main\nmark :4\nauthor Test <test@gitea.com> 1000000000 +0000\ncommitter Test <test@gitea.com> 1000000000 +0000\ndata 14\ninitial commit\nM 100644 :1 README.md\n\n")
|
||||
fmt.Fprintf(&sb, "commit refs/heads/feature\nmark :5\nauthor Test <test@gitea.com> 1000000001 +0000\ncommitter Test <test@gitea.com> 1000000001 +0000\ndata 12\nadd workflow\nfrom :4\nM 100644 :2 .gitea/workflows/my-workflow.yml\n\n")
|
||||
fmt.Fprintf(&sb, "reset refs/pull/42/merge\nfrom :5\n\n")
|
||||
fmt.Fprintf(&sb, "commit refs/heads/main\nmark :6\nauthor Test <test@gitea.com> 1000000002 +0000\ncommitter Test <test@gitea.com> 1000000002 +0000\ndata 16\nrelease workflow\nfrom :4\nM 100644 :3 .gitea/workflows/my-workflow.yml\n\n")
|
||||
fmt.Fprintf(&sb, "reset refs/tags/release-v1\nfrom :6\n\n")
|
||||
fmt.Fprintf(&sb, "reset refs/heads/main\nfrom :4\n\n")
|
||||
fmt.Fprintf(&sb, "done\n")
|
||||
|
||||
_, _, err = gitcmd.NewCommand("fast-import").WithDir(tmpDir).WithStdinBytes([]byte(sb.String())).RunStdString(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
func TestGetActionWorkflow_FallbackRef(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
repoDir := buildWorkflowTestRepo(t)
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repoDir)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
repo := &repo_model.Repository{
|
||||
DefaultBranch: "main",
|
||||
OwnerName: "test-owner",
|
||||
Name: "test-repo",
|
||||
Units: []*repo_model.RepoUnit{
|
||||
{
|
||||
Type: unit.TypeActions,
|
||||
Config: &repo_model.ActionsConfig{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("returns error when workflow only on non-default branch", func(t *testing.T) {
|
||||
_, err := GetActionWorkflow(ctx, gitRepo, repo, "my-workflow.yml")
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
})
|
||||
|
||||
t.Run("returns workflow when found via ref", func(t *testing.T) {
|
||||
wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/heads/feature"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-workflow.yml", wf.ID)
|
||||
})
|
||||
|
||||
t.Run("returns workflow when found via pull ref", func(t *testing.T) {
|
||||
wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/pull/42/merge"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-workflow.yml", wf.ID)
|
||||
assert.Contains(t, wf.HTMLURL, "/src/commit/")
|
||||
})
|
||||
|
||||
t.Run("returns workflow with tag link when found via tag ref", func(t *testing.T) {
|
||||
wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/tags/release-v1"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-workflow.yml", wf.ID)
|
||||
assert.Contains(t, wf.HTMLURL, "/src/tag/release-v1/")
|
||||
})
|
||||
|
||||
t.Run("returns error when workflow missing from ref", func(t *testing.T) {
|
||||
_, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "nonexistent.yml", git.RefName("refs/heads/feature"))
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
})
|
||||
}
|
||||
@@ -387,12 +387,15 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branchName, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
|
||||
func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
|
||||
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
|
||||
cfg := cfgUnit.ActionsConfig()
|
||||
|
||||
workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name()))
|
||||
workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(branchName), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
|
||||
workflowRepoURL := fmt.Sprintf("%s/src/commit/%s/%s/%s", repo.HTMLURL(ctx), commit.ID.String(), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
|
||||
if refWebLinkPath := refName.RefWebLinkPath(); refWebLinkPath != "" {
|
||||
workflowRepoURL = fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(ctx), refWebLinkPath, util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
|
||||
}
|
||||
badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch))
|
||||
|
||||
// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
|
||||
@@ -457,7 +460,7 @@ func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *rep
|
||||
|
||||
workflows := make([]*api.ActionWorkflow, len(entries))
|
||||
for i, entry := range entries {
|
||||
workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, repo.DefaultBranch, folder, entry)
|
||||
workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), folder, entry)
|
||||
}
|
||||
|
||||
return workflows, nil
|
||||
@@ -469,14 +472,35 @@ func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folder, entries, err := actions.ListWorkflows(defaultBranchCommit)
|
||||
return getActionWorkflowFromCommit(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), workflowID)
|
||||
}
|
||||
|
||||
func GetActionWorkflowByRef(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string, ref git.RefName) (*api.ActionWorkflow, error) {
|
||||
if ref == "" {
|
||||
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
|
||||
}
|
||||
|
||||
refCommitID, err := gitrepo.GetRefCommitID(ref.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refCommit, err := gitrepo.GetCommit(refCommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getActionWorkflowFromCommit(ctx, repo, refCommit, ref, workflowID)
|
||||
}
|
||||
|
||||
func getActionWorkflowFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, workflowID string) (*api.ActionWorkflow, error) {
|
||||
folder, entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == workflowID {
|
||||
return getActionWorkflowEntry(ctx, repo, defaultBranchCommit, repo.DefaultBranch, folder, entry), nil
|
||||
return getActionWorkflowEntry(ctx, repo, commit, refName, folder, entry), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
@@ -1032,7 +1034,10 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref))
|
||||
if err != nil && errors.Is(err, util.ErrNotExist) {
|
||||
convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("GetActionWorkflow: %v", err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user