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:
Nicolas
2026-04-18 22:21:21 +02:00
committed by GitHub
parent af31b9d433
commit f247d7d4e5
4 changed files with 150 additions and 7 deletions

View File

@@ -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

View 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)
})
}

View File

@@ -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
}
}

View File

@@ -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