fix(gitops): incorrect workflow status for git-based helm edge stack [BE-12978] (#2678)

This commit is contained in:
Oscar Zhou
2026-05-19 09:07:35 +12:00
committed by GitHub
parent ca5f695459
commit c89f34770f
3 changed files with 82 additions and 34 deletions
+12 -5
View File
@@ -68,25 +68,32 @@ func FetchWorkflows(
items := make([]Workflow, 0, len(entries))
for _, s := range entries {
source, artifact := computePhases(ctx, gitService, s.GitConfig)
gitEntries := []GitEntries{
{Name: s.GitConfig.ConfigFilePath, IsFile: true},
}
for _, additionalPath := range s.AdditionalFiles {
gitEntries = append(gitEntries, GitEntries{Name: additionalPath, IsFile: true})
}
source, artifact := computePhases(ctx, gitService, s.GitConfig, gitEntries)
items = append(items, MapStackToWorkflow(s, s.GitConfig, source, artifact))
}
return items, nil
}
func computePhases(ctx context.Context, gitSvc portainer.GitService, cfg *gittypes.RepoConfig) (source, artifact WorkflowPhaseStatus) {
func computePhases(ctx context.Context, gitSvc portainer.GitService, cfg *gittypes.RepoConfig, gitEntries []GitEntries) (source, artifact WorkflowPhaseStatus) {
if gitSvc == nil || cfg == nil {
return WorkflowPhaseStatus{Status: StatusUnknown}, WorkflowPhaseStatus{Status: StatusUnknown}
}
username, password := gitCredentials(cfg)
return ComputeGitPhases(ctx, cfg.ReferenceName, cfg.ConfigFilePath,
return ComputeGitPhases(ctx, cfg.ReferenceName, gitEntries,
func(ctx context.Context) ([]string, error) {
return gitSvc.ListRefs(ctx, cfg.URL, username, password, false, cfg.TLSSkipVerify)
},
func(ctx context.Context, exts []string) ([]string, error) {
return gitSvc.ListFiles(ctx, cfg.URL, cfg.ReferenceName, username, password, false, false, exts, cfg.TLSSkipVerify)
func(ctx context.Context, exts []string, dirOnly bool) ([]string, error) {
return gitSvc.ListFiles(ctx, cfg.URL, cfg.ReferenceName, username, password, dirOnly, false, exts, cfg.TLSSkipVerify)
},
)
}
+55 -14
View File
@@ -11,11 +11,17 @@ import (
type ListRefsFunc func(ctx context.Context) ([]string, error)
// ListFilesFunc lists files in a repository branch filtered by extension.
type ListFilesFunc func(ctx context.Context, exts []string) ([]string, error)
type ListFilesFunc func(ctx context.Context, exts []string, dirOnly bool) ([]string, error)
// GitEntries represents a git entry which can be either a file or a directory.
type GitEntries struct {
Name string
IsFile bool
}
// ComputeGitPhases checks source (ref reachability) and artifact (config file presence).
// If source fails, artifact is returned as unknown without making a network call.
func ComputeGitPhases(ctx context.Context, referenceName, configFilePath string, listRefs ListRefsFunc, listFiles ListFilesFunc) (source, artifact WorkflowPhaseStatus) {
func ComputeGitPhases(ctx context.Context, referenceName string, configFilePath []GitEntries, listRefs ListRefsFunc, listFiles ListFilesFunc) (source, artifact WorkflowPhaseStatus) {
source = computeSourcePhase(ctx, referenceName, listRefs)
if source.Status == StatusError {
return source, WorkflowPhaseStatus{Status: StatusUnknown}
@@ -37,23 +43,58 @@ func computeSourcePhase(ctx context.Context, referenceName string, listRefs List
return WorkflowPhaseStatus{Status: StatusHealthy}
}
func computeArtifactPhase(ctx context.Context, configFilePath string, listFiles ListFilesFunc) WorkflowPhaseStatus {
if configFilePath == "" {
func computeArtifactPhase(ctx context.Context, gitEntries []GitEntries, listFiles ListFilesFunc) WorkflowPhaseStatus {
if len(gitEntries) == 0 {
return WorkflowPhaseStatus{Status: StatusError, Error: "no config file path specified"}
}
ext := path.Ext(configFilePath)
var exts []string
if len(ext) > 0 {
ext = ext[1:]
exts = []string{ext}
var (
exts []string
fileEntries []string
dirEntries []string
)
for _, gitEntry := range gitEntries {
if gitEntry.IsFile {
ext := path.Ext(gitEntry.Name)
if len(ext) > 0 {
ext = ext[1:]
exts = append(exts, ext)
}
fileEntries = append(fileEntries, gitEntry.Name)
continue
}
dirEntries = append(dirEntries, gitEntry.Name)
}
files, err := listFiles(ctx, exts)
if err != nil {
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
// Check file entries
if len(fileEntries) > 0 {
files, err := listFiles(ctx, exts, false)
if err != nil {
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
}
for _, fileEntry := range fileEntries {
if !slices.Contains(files, fileEntry) {
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("file %q not found", fileEntry)}
}
}
}
if !slices.Contains(files, configFilePath) {
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("file %q not found", configFilePath)}
// Check directory entries
if len(dirEntries) > 0 {
dirs, err := listFiles(ctx, nil, true)
if err != nil {
return WorkflowPhaseStatus{Status: StatusError, Error: err.Error()}
}
for _, dirEntry := range dirEntries {
if !slices.Contains(dirs, dirEntry) {
return WorkflowPhaseStatus{Status: StatusError, Error: fmt.Sprintf("directory %q not found", dirEntry)}
}
}
}
return WorkflowPhaseStatus{Status: StatusHealthy}
}
+15 -15
View File
@@ -14,20 +14,20 @@ func TestComputeGitPhases(t *testing.T) {
okRefs := func(_ context.Context) ([]string, error) {
return []string{"refs/heads/main"}, nil
}
okFiles := func(_ context.Context, _ []string) ([]string, error) {
okFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
return []string{"docker-compose.yml"}, nil
}
errRefs := func(_ context.Context) ([]string, error) {
return nil, errors.New("connection refused")
}
errFiles := func(_ context.Context, _ []string) ([]string, error) {
errFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
return nil, errors.New("connection refused")
}
cases := []struct {
name string
referenceName string
configFilePath string
configFilePath []GitEntries
listRefs ListRefsFunc
listFiles ListFilesFunc
expectedSource Status
@@ -36,7 +36,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "listRefs errors → source error, artifact unknown",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: errRefs,
listFiles: okFiles,
expectedSource: StatusError,
@@ -45,7 +45,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "ref not in list → source error, artifact unknown",
referenceName: "refs/heads/missing",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: func(_ context.Context) ([]string, error) {
return []string{"refs/heads/main"}, nil
},
@@ -56,7 +56,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "empty configFilePath → artifact error",
referenceName: "refs/heads/main",
configFilePath: "",
configFilePath: []GitEntries{},
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
@@ -65,7 +65,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "listFiles errors → artifact error",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: okRefs,
listFiles: errFiles,
expectedSource: StatusHealthy,
@@ -74,9 +74,9 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "file not in list → artifact error",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: okRefs,
listFiles: func(_ context.Context, _ []string) ([]string, error) {
listFiles: func(_ context.Context, _ []string, _ bool) ([]string, error) {
return []string{"other.yml"}, nil
},
expectedSource: StatusHealthy,
@@ -85,7 +85,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "both healthy",
referenceName: "refs/heads/main",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
@@ -94,7 +94,7 @@ func TestComputeGitPhases(t *testing.T) {
{
name: "empty referenceName → source healthy (default HEAD)",
referenceName: "",
configFilePath: "docker-compose.yml",
configFilePath: []GitEntries{{Name: "docker-compose.yml", IsFile: true}},
listRefs: okRefs,
listFiles: okFiles,
expectedSource: StatusHealthy,
@@ -132,9 +132,9 @@ func TestComputeArtifactPhase_ExtensionFilter(t *testing.T) {
ComputeGitPhases(
t.Context(),
"",
tc.configPath,
[]GitEntries{{Name: tc.configPath, IsFile: true}},
func(_ context.Context) ([]string, error) { return nil, nil },
func(_ context.Context, exts []string) ([]string, error) {
func(_ context.Context, exts []string, dirOnly bool) ([]string, error) {
capturedExts = exts
return []string{tc.configPath}, nil
},
@@ -151,12 +151,12 @@ func TestComputeGitPhases_ArtifactNotCalledOnSourceError(t *testing.T) {
listRefs := func(_ context.Context) ([]string, error) {
return nil, errors.New("repo unreachable")
}
listFiles := func(_ context.Context, _ []string) ([]string, error) {
listFiles := func(_ context.Context, _ []string, _ bool) ([]string, error) {
listFilesCalled = true
return nil, nil
}
ComputeGitPhases(t.Context(), "refs/heads/main", "docker-compose.yml", listRefs, listFiles)
ComputeGitPhases(t.Context(), "refs/heads/main", []GitEntries{{Name: "docker-compose.yml", IsFile: true}}, listRefs, listFiles)
assert.False(t, listFilesCalled, "listFiles must not be called when source fails")
}