Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package digger-cli for openSUSE:Factory checked in at 2026-10-01 16:48:08 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/digger-cli (Old) and /work/SRC/openSUSE:Factory/.digger-cli.new.1253 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "digger-cli" Thu Oct 1 16:48:08 2026 rev:56 rq:1381776 version:0.6.152 Changes: -------- --- /work/SRC/openSUSE:Factory/digger-cli/digger-cli.changes 2026-09-21 12:08:55.518450865 +0200 +++ /work/SRC/openSUSE:Factory/.digger-cli.new.1253/digger-cli.changes 2026-10-01 16:49:37.184511891 +0200 @@ -1,0 +2,8 @@ +Thu Oct 01 05:14:37 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.6.152: + * fix: honor git_timeout from digger.yml before cloning (#2724) + * fix: adjust GitHub check run char limit considering the MD + terraform text wrapper (#2615) + +------------------------------------------------------------------- Old: ---- digger-cli-0.6.151.obscpio New: ---- digger-cli-0.6.152.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ digger-cli.spec ++++++ --- /var/tmp/diff_new_pack.8BjW3e/_old 2026-10-01 16:49:38.524568009 +0200 +++ /var/tmp/diff_new_pack.8BjW3e/_new 2026-10-01 16:49:38.526568093 +0200 @@ -19,7 +19,7 @@ %define executable_name digger Name: digger-cli -Version: 0.6.151 +Version: 0.6.152 Release: 0 Summary: CLI for the digger open source IaC orchestration tool License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.8BjW3e/_old 2026-10-01 16:49:38.595570983 +0200 +++ /var/tmp/diff_new_pack.8BjW3e/_new 2026-10-01 16:49:38.601571234 +0200 @@ -6,8 +6,8 @@ <param name="exclude">go.mod</param> <param name="exclude">go.work</param> <param name="exclude">go.work.sum</param> - <param name="revision">refs/tags/v0.6.151</param> - <param name="match-tag">v0.6.151</param> + <param name="revision">refs/tags/v0.6.152</param> + <param name="match-tag">v0.6.152</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.8BjW3e/_old 2026-10-01 16:49:38.648573203 +0200 +++ /var/tmp/diff_new_pack.8BjW3e/_new 2026-10-01 16:49:38.652573370 +0200 @@ -5,6 +5,6 @@ <param name="url">https://github.com/johanneskastl/digger</param> <param name="changesrevision">8fe377068e53e2050ff4c745388d8428d2b13bb0</param></service><service name="tar_scm"> <param name="url">https://github.com/diggerhq/digger.git</param> - <param name="changesrevision">85cd59db7caa3d8bf6cefe303a62c12812b0bc48</param></service></servicedata> + <param name="changesrevision">9ac4e2237b7b8afa455f422b92737bfe106d495c</param></service></servicedata> (No newline at EOF) ++++++ digger-cli-0.6.151.obscpio -> digger-cli-0.6.152.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/controllers/cache.go new/digger-cli-0.6.152/backend/controllers/cache.go --- old/digger-cli-0.6.151/backend/controllers/cache.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/controllers/cache.go 2026-09-30 18:56:09.000000000 +0200 @@ -69,7 +69,7 @@ cloneUrl := fmt.Sprintf("https://%v/%v", utils.GetGithubHostname(), repo.RepoFullName) branch := request.Branch - _, token, err := utils.GetGithubService(d.GithubClientProvider, installationId, repoFullName, repoOwner, repoName) + ghService, token, err := utils.GetGithubService(d.GithubClientProvider, installationId, repoFullName, repoOwner, repoName) if err != nil { slog.Error("Could not get GitHub service", "error", err, "repoFullName", repoFullName, "orgId", orgId) c.String(http.StatusInternalServerError, fmt.Sprintf("could not get github service %v %v", repoFullName, orgId)) @@ -83,7 +83,7 @@ // update the cache here, do it async for immediate response go func(ctx context.Context) { defer logging.InheritRequestLogger(ctx)() - err = git_utils.CloneGitRepoAndDoAction(cloneUrl, branch, "", *token, "", func(dir string) error { + err = git_utils.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, "", *token, "", ghService.ReadRepositoryFile, func(dir string) error { diggerYmlBytes, err := os.ReadFile(path.Join(dir, "digger.yml")) diggerYmlStr = string(diggerYmlBytes) config, _, _, newAtlantisConfig, err = dg_configuration.LoadDiggerConfig(dir, true, nil, nil) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/controllers/github_helpers.go new/digger-cli-0.6.152/backend/controllers/github_helpers.go --- old/digger-cli-0.6.151/backend/controllers/github_helpers.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/controllers/github_helpers.go 2026-09-30 18:56:09.000000000 +0200 @@ -863,7 +863,7 @@ var diggerYmlStr string var dependencyGraph graph.Graph[string, digger_config.Project] - err = git_utils.CloneGitRepoAndDoAction(cloneUrl, branch, commitSha, *token, "", func(dir string) error { + err = git_utils.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, commitSha, *token, "", ghService.ReadRepositoryFile, func(dir string) error { slog.Debug("Reading Digger config from cloned repository", "directory", dir) diggerYmlStr, err = digger_config.ReadDiggerYmlFileContents(dir) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/controllers/projects_helpers.go new/digger-cli-0.6.152/backend/controllers/projects_helpers.go --- old/digger-cli-0.6.151/backend/controllers/projects_helpers.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/controllers/projects_helpers.go 2026-09-30 18:56:09.000000000 +0200 @@ -512,9 +512,12 @@ // Character limit check - GitHub check run text field has a 65535 character limit const maxCheckRunTextLength = 65535 cutOffMsg := "\n[Character limit exceeded, output truncated]" - if utf8.RuneCountInString(job.TerraformOutput) > maxCheckRunTextLength { + wrapper := "```terraform\n" + "```\n" + maxOutputLength := maxCheckRunTextLength - utf8.RuneCountInString(wrapper) + + if utf8.RuneCountInString(job.TerraformOutput) > maxOutputLength { runes := []rune(job.TerraformOutput) - truncateAt := maxCheckRunTextLength - utf8.RuneCountInString(cutOffMsg) + truncateAt := maxOutputLength - utf8.RuneCountInString(cutOffMsg) job.TerraformOutput = string(runes[:truncateAt]) + cutOffMsg } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/controllers/projects_test.go new/digger-cli-0.6.152/backend/controllers/projects_test.go --- old/digger-cli-0.6.151/backend/controllers/projects_test.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/controllers/projects_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -128,7 +128,7 @@ }, { name: "at limit - no truncation", - inputLength: 65535, + inputLength: 65535 - 17, // account for wrapper expectTruncate: false, }, { @@ -140,25 +140,29 @@ const maxCheckRunTextLength = 65535 cutOffMsg := "\n[Character limit exceeded, output truncated]" + wrapper := "```terraform\n" + "```\n" + maxOutputLength := maxCheckRunTextLength - utf8.RuneCountInString(wrapper) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { input := strings.Repeat("a", tt.inputLength) result := input - if utf8.RuneCountInString(result) > maxCheckRunTextLength { + if utf8.RuneCountInString(result) > maxOutputLength { runes := []rune(result) - truncateAt := maxCheckRunTextLength - utf8.RuneCountInString(cutOffMsg) + truncateAt := maxOutputLength - utf8.RuneCountInString(cutOffMsg) result = string(runes[:truncateAt]) + cutOffMsg } + text := "```terraform\n" + result + "```\n" + if tt.expectTruncate { - assert.Equal(t, maxCheckRunTextLength, utf8.RuneCountInString(result), + assert.Equal(t, maxCheckRunTextLength, utf8.RuneCountInString(text), "truncated output should be exactly 65535 characters") assert.True(t, strings.HasSuffix(result, cutOffMsg), "truncated output should end with cutoff message") } else { - assert.Equal(t, tt.inputLength, utf8.RuneCountInString(result), + assert.LessOrEqual(t, utf8.RuneCountInString(text), maxCheckRunTextLength, "non-truncated output should maintain original length") assert.False(t, strings.HasSuffix(result, cutOffMsg), "non-truncated output should not have cutoff message") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/utils/bitbucket.go new/digger-cli-0.6.152/backend/utils/bitbucket.go --- old/digger-cli-0.6.151/backend/utils/bitbucket.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/utils/bitbucket.go 2026-09-30 18:56:09.000000000 +0200 @@ -95,7 +95,7 @@ "changedFilesCount", len(changedFiles), ) - err = git_utils.CloneGitRepoAndDoAction(cloneUrl, branch, "", token, "x-token-auth", func(dir string) error { + err = git_utils.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, "", token, "x-token-auth", service.ReadRepositoryFile, func(dir string) error { diggerYmlPath := path.Join(dir, "digger.yml") diggerYmlBytes, err := os.ReadFile(diggerYmlPath) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/backend/utils/gitlab.go new/digger-cli-0.6.152/backend/utils/gitlab.go --- old/digger-cli-0.6.151/backend/utils/gitlab.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/backend/utils/gitlab.go 2026-09-30 18:56:09.000000000 +0200 @@ -114,7 +114,7 @@ "changedFilesCount", len(changedFiles), ) - err = git_utils.CloneGitRepoAndDoAction(cloneUrl, branch, "", token, "", func(dir string) error { + err = git_utils.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, "", token, "", service.ReadRepositoryFile, func(dir string) error { diggerYmlPath := path.Join(dir, "digger.yml") diggerYmlBytes, err := os.ReadFile(diggerYmlPath) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/background/projects-refresh-service/projects_refesh_main.go new/digger-cli-0.6.152/background/projects-refresh-service/projects_refesh_main.go --- old/digger-cli-0.6.151/background/projects-refresh-service/projects_refesh_main.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/background/projects-refresh-service/projects_refesh_main.go 2026-09-30 18:56:09.000000000 +0200 @@ -3,6 +3,7 @@ import ( "fmt" "github.com/diggerhq/digger/backend/models" + "github.com/diggerhq/digger/libs/ci/github" dg_configuration "github.com/diggerhq/digger/libs/digger_config" utils3 "github.com/diggerhq/digger/libs/git_utils" "log/slog" @@ -45,7 +46,12 @@ models.ConnectDatabase() slog.Info("refreshing projects from repo", "repoFullName", repoFullName) - err := utils3.CloneGitRepoAndDoAction(cloneUrl, branch, "", token, "", func(dir string) error { + ghService, err := github.NewServiceForCloneURL(cloneUrl, token) + if err != nil { + slog.Error("failed to create GitHub client", "error", err) + os.Exit(1) + } + err = utils3.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, "", token, "", ghService.ReadRepositoryFile, func(dir string) error { config, _, err := dg_configuration.LoadDiggerConfigYaml(dir, true, nil, nil) if err != nil { slog.Error("failed to load digger.yml: %v", "error", err) @@ -61,7 +67,7 @@ return nil }) if err != nil { - slog.Error("error while cloning repo: %v", err) + slog.Error("error while cloning repo", "error", err) os.Exit(1) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/docs/ce/reference/digger.yml.mdx new/digger-cli-0.6.152/docs/ce/reference/digger.yml.mdx --- old/digger-cli-0.6.151/docs/ce/reference/digger.yml.mdx 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/docs/ce/reference/digger.yml.mdx 2026-09-30 18:56:09.000000000 +0200 @@ -216,7 +216,7 @@ </ParamField> <ParamField path="git_timeout" type="integer" default="30"> - Timeout in seconds for each git command (clone, checkout) Digger runs when it clones a repository. Increase this for large repositories that take longer than 30 seconds to clone. + Timeout in seconds for each git command (clone, checkout) Digger runs when loading repository configuration. Increase this for large repositories, for example `git_timeout: 120`. Digger reads this setting through the Git provider API before cloning, using the requested branch or commit. It checks `digger.yml` first, then `digger.yaml`. An omitted, zero, or negative value uses the 30-second default. The initial API lookup has its own 30-second timeout. </ParamField> <ParamField path="projects" type="array"> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/drift/utils/github.go new/digger-cli-0.6.152/drift/utils/github.go --- old/digger-cli-0.6.151/drift/utils/github.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/drift/utils/github.go 2026-09-30 18:56:09.000000000 +0200 @@ -107,7 +107,7 @@ var changedFiles []string = nil - err = utils2.CloneGitRepoAndDoAction(cloneUrl, branch, "", *token, "", func(dir string) error { + err = utils2.CloneGitRepoAndDoActionWithConfig(cloneUrl, branch, "", *token, "", ghService.ReadRepositoryFile, func(dir string) error { diggerYmlBytes, err := os.ReadFile(path.Join(dir, "digger.yml")) diggerYmlStr = string(diggerYmlBytes) config, _, dependencyGraph, _, err = dg_configuration.LoadDiggerConfig(dir, true, changedFiles, nil) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/bitbucket/repository_file.go new/digger-cli-0.6.152/libs/ci/bitbucket/repository_file.go --- old/digger-cli-0.6.151/libs/ci/bitbucket/repository_file.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/bitbucket/repository_file.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,34 @@ +package bitbucket + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" +) + +func (b BitbucketAPI) ReadRepositoryFile(ctx context.Context, path, ref string) ([]byte, error) { + if ref == "" { + ref = "HEAD" + } + endpoint := fmt.Sprintf("%s/repositories/%s/%s/src/%s/%s", bitbucketBaseURL, url.PathEscape(b.RepoWorkspace), url.PathEscape(b.RepoName), url.PathEscape(ref), url.PathEscape(path)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+b.AuthToken) + response, err := b.HttpClient.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%s: %w", path, os.ErrNotExist) + } + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("read %s: Bitbucket returned HTTP %d", path, response.StatusCode) + } + return io.ReadAll(response.Body) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/bitbucket/repository_file_test.go new/digger-cli-0.6.152/libs/ci/bitbucket/repository_file_test.go --- old/digger-cli-0.6.151/libs/ci/bitbucket/repository_file_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/bitbucket/repository_file_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,45 @@ +package bitbucket + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" +) + +type repositoryFileTransport func(*http.Request) (*http.Response, error) + +func (f repositoryFileTransport) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} + +func TestReadRepositoryFile(t *testing.T) { + for _, status := range []int{http.StatusOK, http.StatusNotFound, http.StatusForbidden} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + svc := BitbucketAPI{ + RepoWorkspace: "owner", RepoName: "repo", AuthToken: "test-token", + HttpClient: http.Client{Transport: repositoryFileTransport(func(r *http.Request) (*http.Response, error) { + if r.URL.EscapedPath() != "/2.0/repositories/owner/repo/src/feature%2Ftimeout/digger.yml" { + t.Errorf("unexpected request: %s", r.URL) + } + if r.Header.Get("Authorization") != "Bearer test-token" { + t.Error("missing authentication") + } + return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader("git_timeout: 120\n")), Header: make(http.Header)}, nil + })}, + } + data, err := svc.ReadRepositoryFile(context.Background(), "digger.yml", "feature/timeout") + if status == http.StatusOK { + if err != nil || string(data) != "git_timeout: 120\n" { + t.Fatalf("got %q, %v", data, err) + } + } else if err == nil || errors.Is(err, os.ErrNotExist) != (status == http.StatusNotFound) { + t.Fatalf("unexpected error for HTTP %d: %v", status, err) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/github/repository_file.go new/digger-cli-0.6.152/libs/ci/github/repository_file.go --- old/digger-cli-0.6.151/libs/ci/github/repository_file.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/github/repository_file.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,48 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/google/go-github/v61/github" +) + +// NewServiceForCloneURL creates a client for workers that only have a clone URL and token. +func NewServiceForCloneURL(cloneURL, token string) (*GithubService, error) { + u, err := url.Parse(cloneURL) + if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") { + return nil, fmt.Errorf("expected an HTTP(S) GitHub clone URL") + } + parts := strings.Split(strings.Trim(strings.TrimSuffix(u.Path, ".git"), "/"), "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("expected owner/repository in GitHub clone URL") + } + client := github.NewClient(nil).WithAuthToken(token) + if u.Host != "github.com" { + baseURL := u.Scheme + "://" + u.Host + "/" + client, err = client.WithEnterpriseURLs(baseURL, baseURL) + if err != nil { + return nil, err + } + } + return &GithubService{Client: client, Owner: parts[0], RepoName: parts[1]}, nil +} + +func (svc GithubService) ReadRepositoryFile(ctx context.Context, path, ref string) ([]byte, error) { + file, _, response, err := svc.Client.Repositories.GetContents(ctx, svc.Owner, svc.RepoName, path, &github.RepositoryContentGetOptions{Ref: ref}) + if response != nil && response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%s: %w", path, os.ErrNotExist) + } + if err != nil { + return nil, err + } + if file == nil { + return nil, fmt.Errorf("%s is not a file", path) + } + contents, err := file.GetContent() + return []byte(contents), err +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/github/repository_file_test.go new/digger-cli-0.6.152/libs/ci/github/repository_file_test.go --- old/digger-cli-0.6.151/libs/ci/github/repository_file_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/github/repository_file_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,68 @@ +package github + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestReadRepositoryFile(t *testing.T) { + for _, status := range []int{http.StatusOK, http.StatusNotFound, http.StatusForbidden} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v3/repos/owner/repo/contents/digger.yml" || r.URL.Query().Get("ref") != "feature/timeout" { + t.Errorf("unexpected request: %s", r.URL) + } + if r.Header.Get("Authorization") != "Bearer test-token" { + t.Error("missing authentication") + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if status == http.StatusOK { + fmt.Fprint(w, `{"type":"file","encoding":"base64","content":"Z2l0X3RpbWVvdXQ6IDEyMAo="}`) + } else { + fmt.Fprint(w, `{"message":"unavailable"}`) + } + })) + defer server.Close() + svc, err := NewServiceForCloneURL(server.URL+"/owner/repo.git", "test-token") + if err != nil { + t.Fatal(err) + } + data, err := svc.ReadRepositoryFile(context.Background(), "digger.yml", "feature/timeout") + switch status { + case http.StatusOK: + if err != nil || string(data) != "git_timeout: 120\n" { + t.Fatalf("got %q, %v", data, err) + } + case http.StatusNotFound: + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected missing file, got %v", err) + } + case http.StatusForbidden: + if err == nil || errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected API failure, got %v", err) + } + } + }) + } +} + +func TestNewServiceForCloneURL(t *testing.T) { + svc, err := NewServiceForCloneURL("https://github.com/owner/repo.git", "test-token") + if err != nil { + t.Fatal(err) + } + if svc.Client.BaseURL.String() != "https://api.github.com/" || svc.Owner != "owner" || svc.RepoName != "repo" { + t.Fatalf("unexpected service: %+v", svc) + } + for _, cloneURL := range []string{"", "file:///tmp/repo", "https://github.com/owner", "https://github.com/owner/repo/extra"} { + if _, err := NewServiceForCloneURL(cloneURL, "test-token"); err == nil { + t.Errorf("accepted invalid clone URL: %s", cloneURL) + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/gitlab/repository_file.go new/digger-cli-0.6.152/libs/ci/gitlab/repository_file.go --- old/digger-cli-0.6.151/libs/ci/gitlab/repository_file.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/gitlab/repository_file.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,18 @@ +package gitlab + +import ( + "context" + "fmt" + "net/http" + "os" + + gitlab "github.com/xanzy/go-gitlab" +) + +func (svc GitLabService) ReadRepositoryFile(ctx context.Context, path, ref string) ([]byte, error) { + contents, response, err := svc.Client.RepositoryFiles.GetRawFile(*svc.Context.ProjectId, path, &gitlab.GetRawFileOptions{Ref: &ref}, gitlab.WithContext(ctx)) + if response != nil && response.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%s: %w", path, os.ErrNotExist) + } + return contents, err +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/ci/gitlab/repository_file_test.go new/digger-cli-0.6.152/libs/ci/gitlab/repository_file_test.go --- old/digger-cli-0.6.151/libs/ci/gitlab/repository_file_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/ci/gitlab/repository_file_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,45 @@ +package gitlab + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + gitlab "github.com/xanzy/go-gitlab" +) + +func TestReadRepositoryFile(t *testing.T) { + for _, status := range []int{http.StatusOK, http.StatusNotFound, http.StatusForbidden} { + t.Run(fmt.Sprint(status), func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v4/projects/42/repository/files/digger.yml/raw" || r.URL.Query().Get("ref") != "feature/timeout" { + t.Errorf("unexpected request: %s", r.URL) + } + if r.Header.Get("PRIVATE-TOKEN") != "test-token" { + t.Error("missing authentication") + } + w.WriteHeader(status) + fmt.Fprint(w, "git_timeout: 120\n") + })) + defer server.Close() + client, err := gitlab.NewClient("test-token", gitlab.WithBaseURL(server.URL)) + if err != nil { + t.Fatal(err) + } + projectID := 42 + svc := GitLabService{Client: client, Context: &GitLabContext{ProjectId: &projectID}} + data, err := svc.ReadRepositoryFile(context.Background(), "digger.yml", "feature/timeout") + if status == http.StatusOK { + if err != nil || string(data) != "git_timeout: 120\n" { + t.Fatalf("got %q, %v", data, err) + } + } else if err == nil || errors.Is(err, os.ErrNotExist) != (status == http.StatusNotFound) { + t.Fatalf("unexpected error for HTTP %d: %v", status, err) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/git_utils/clone_utils.go new/digger-cli-0.6.152/libs/git_utils/clone_utils.go --- old/digger-cli-0.6.151/libs/git_utils/clone_utils.go 2026-09-13 11:13:52.000000000 +0200 +++ new/digger-cli-0.6.152/libs/git_utils/clone_utils.go 2026-09-30 18:56:09.000000000 +0200 @@ -29,25 +29,34 @@ return CloneGitRepoAndDoActionWithTimeout(repoUrl, branch, commitHash, token, tokenUsername, defaultGitTimeout, action) } -// CloneGitRepoAndDoActionWithTimeout is CloneGitRepoAndDoAction with a per-git-command timeout (digger.yml git_timeout, in seconds) +// CloneGitRepoAndDoActionWithTimeout overrides the per-command timeout. +// A non-positive timeout uses the default of 30 seconds. func CloneGitRepoAndDoActionWithTimeout(repoUrl string, branch string, commitHash string, token string, tokenUsername string, timeout time.Duration, action action) error { dir, err := createTempDir() if err != nil { slog.Error("Failed to create temporary directory", "error", err) return err } + defer func() { + slog.Debug("Removing cloned directory", "directory", dir) + if err := os.RemoveAll(dir); err != nil { + slog.Warn("Failed to remove directory", "directory", dir, "error", err) + } + }() + + git := NewGitShellWithTokenAuth(dir, token, tokenUsername) + if timeout > 0 { + git.timeout = timeout + } slog.Debug("Cloning git repository", "repoUrl", repoUrl, "branch", branch, "commitHash", commitHash, "directory", dir, + "timeout", git.timeout, ) - git := NewGitShellWithTokenAuth(dir, token, tokenUsername) - if timeout > 0 { - git.timeout = timeout - } err = git.Clone(repoUrl, branch) if err != nil { slog.Error("Failed to clone repository", @@ -69,14 +78,6 @@ } } - defer func() { - slog.Debug("Removing cloned directory", "directory", dir) - ferr := os.RemoveAll(dir) - if ferr != nil { - slog.Warn("Failed to remove directory", "directory", dir, "error", ferr) - } - }() - err = action(dir) if err != nil { slog.Error("Error performing action on repository", "directory", dir, "error", err) @@ -176,8 +177,11 @@ err := cmd.Run() if err != nil { + if ctx.Err() != nil { + return "", fmt.Errorf("git command timed out after %s: %w: %s", g.timeout, ctx.Err(), stderr.String()) + } if stderr.Len() > 0 { - return "", fmt.Errorf("git command failed: %v: %s", err, stderr.String()) + return "", fmt.Errorf("git command failed: %w: %s", err, stderr.String()) } return "", err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/git_utils/clone_utils_test.go new/digger-cli-0.6.152/libs/git_utils/clone_utils_test.go --- old/digger-cli-0.6.151/libs/git_utils/clone_utils_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/git_utils/clone_utils_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,107 @@ +package git_utils + +import ( + "context" + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func installFakeGit(t *testing.T, script string) { + t.Helper() + binDir := t.TempDir() + if err := os.WriteFile(filepath.Join(binDir, "git"), []byte("#!/bin/sh\n"+script), 0700); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) +} + +func TestCloneUsesConfigTimeoutForEachCommand(t *testing.T) { + // Each command fits within two seconds, but together they exceed it. + installFakeGit(t, "echo \"$1\" >> \"$GIT_TEST_COMMANDS\"\nexec sleep 1.1\n") + commandsPath := filepath.Join(t.TempDir(), "commands") + t.Setenv("GIT_TEST_COMMANDS", commandsPath) + config := []byte("git_timeout: 2\n") + readFile := func(ctx context.Context, path, ref string) ([]byte, error) { + if path != "digger.yml" || ref != "commit" { + t.Fatalf("unexpected config lookup: %s at %s", path, ref) + } + if _, err := os.Stat(commandsPath); !os.IsNotExist(err) { + t.Fatal("Git started before reading configuration") + } + return config, nil + } + var clonedDir string + err := CloneGitRepoAndDoActionWithConfig("https://example.com/repo", "main", "commit", "", "", readFile, func(dir string) error { + clonedDir = dir + return nil + }) + if err != nil { + t.Fatal(err) + } + if clonedDir == "" { + t.Fatal("repository action was not called") + } + if _, err := os.Stat(clonedDir); !os.IsNotExist(err) { + t.Fatalf("clone directory was not removed: %v", err) + } + commands, err := os.ReadFile(commandsPath) + if err != nil { + t.Fatal(err) + } + if string(commands) != "clone\ncheckout\n" { + t.Fatalf("unexpected commands: %q", commands) + } + + // The same clone must fail with a shorter YAML timeout. + if err := os.Remove(commandsPath); err != nil { + t.Fatal(err) + } + config = []byte("git_timeout: 1\n") + err = CloneGitRepoAndDoActionWithConfig("https://example.com/repo", "main", "commit", "", "", readFile, func(string) error { + t.Fatal("action called after timeout") + return nil + }) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("expected timeout, got %v", err) + } +} + +func TestExplicitCloneTimeoutAndCleanup(t *testing.T) { + installFakeGit(t, "exec sleep 10\n") + cloneRoot := t.TempDir() + t.Setenv("TMPDIR", cloneRoot) + err := CloneGitRepoAndDoActionWithTimeout("https://example.com/repo", "main", "", "", "", 20*time.Millisecond, func(string) error { + t.Fatal("action called after timeout") + return nil + }) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("expected timeout, got %v", err) + } + if !strings.Contains(err.Error(), "timed out after 20ms") { + t.Fatalf("timeout duration missing: %v", err) + } + entries, err := os.ReadDir(cloneRoot) + if err != nil { + t.Fatal(err) + } + if len(entries) != 0 { + t.Fatalf("failed clone left temporary files: %v", entries) + } +} + +func TestGitCommandFailureIsNotTimeout(t *testing.T) { + installFakeGit(t, "echo 'clone failed' >&2\nexit 1\n") + _, err := NewGitShell(t.TempDir(), nil).runCommand("clone") + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) || errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("expected process failure, got %v", err) + } + if !strings.Contains(err.Error(), "clone failed") { + t.Fatalf("command stderr missing: %v", err) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/git_utils/config_timeout.go new/digger-cli-0.6.152/libs/git_utils/config_timeout.go --- old/digger-cli-0.6.151/libs/git_utils/config_timeout.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/git_utils/config_timeout.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,57 @@ +package git_utils + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// RepositoryFileReader reads a file at a branch or commit through the provider API. +// Missing files must return an error wrapping os.ErrNotExist. +type RepositoryFileReader func(ctx context.Context, path, ref string) ([]byte, error) + +// CloneGitRepoAndDoActionWithConfig reads git_timeout before starting the clone. +func CloneGitRepoAndDoActionWithConfig(repoURL, branch, commitHash, token, tokenUsername string, readFile RepositoryFileReader, action action) error { + ref := branch + if commitHash != "" { + ref = commitHash + } + timeout, err := readGitTimeout(readFile, ref) + if err != nil { + return err + } + return CloneGitRepoAndDoActionWithTimeout(repoURL, branch, commitHash, token, tokenUsername, timeout, action) +} + +func readGitTimeout(readFile RepositoryFileReader, ref string) (time.Duration, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultGitTimeout) + defer cancel() + for _, path := range []string{"digger.yml", "digger.yaml"} { + contents, err := readFile(ctx, path, ref) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return 0, fmt.Errorf("read %s before cloning: %w", path, err) + } + // Project generation needs a checkout; only decode the timeout here. + var config struct { + GitTimeout int64 `yaml:"git_timeout"` + } + if err := yaml.Unmarshal(contents, &config); err != nil { + return 0, fmt.Errorf("parse %s before cloning: %w", path, err) + } + if config.GitTimeout <= 0 { + return defaultGitTimeout, nil + } + if config.GitTimeout > int64((1<<63-1)/time.Second) { + return 0, fmt.Errorf("git_timeout in %s is too large", path) + } + return time.Duration(config.GitTimeout) * time.Second, nil + } + return defaultGitTimeout, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.151/libs/git_utils/config_timeout_test.go new/digger-cli-0.6.152/libs/git_utils/config_timeout_test.go --- old/digger-cli-0.6.151/libs/git_utils/config_timeout_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/digger-cli-0.6.152/libs/git_utils/config_timeout_test.go 2026-09-30 18:56:09.000000000 +0200 @@ -0,0 +1,77 @@ +package git_utils + +import ( + "context" + "errors" + "fmt" + "os" + "reflect" + "testing" + "time" +) + +func TestReadGitTimeout(t *testing.T) { + for _, test := range []struct { + name string + files map[string]string + want time.Duration + paths []string + wantErr bool + }{ + {"configured", map[string]string{"digger.yml": "git_timeout: 120\nprojects: []"}, 120 * time.Second, []string{"digger.yml"}, false}, + {"yaml fallback", map[string]string{"digger.yaml": "git_timeout: 90"}, 90 * time.Second, []string{"digger.yml", "digger.yaml"}, false}, + {"yml precedence", map[string]string{"digger.yml": "git_timeout: 60", "digger.yaml": "git_timeout: 90"}, time.Minute, []string{"digger.yml"}, false}, + {"missing files", nil, 30 * time.Second, []string{"digger.yml", "digger.yaml"}, false}, + {"omitted", map[string]string{"digger.yml": "projects: []"}, 30 * time.Second, []string{"digger.yml"}, false}, + {"zero", map[string]string{"digger.yml": "git_timeout: 0"}, 30 * time.Second, []string{"digger.yml"}, false}, + {"negative", map[string]string{"digger.yml": "git_timeout: -1"}, 30 * time.Second, []string{"digger.yml"}, false}, + {"invalid type", map[string]string{"digger.yml": "git_timeout: slow"}, 0, []string{"digger.yml"}, true}, + {"overflow", map[string]string{"digger.yml": "git_timeout: 9223372037"}, 0, []string{"digger.yml"}, true}, + {"malformed", map[string]string{"digger.yml": "git_timeout: ["}, 0, []string{"digger.yml"}, true}, + } { + t.Run(test.name, func(t *testing.T) { + var paths []string + got, err := readGitTimeout(func(ctx context.Context, path, ref string) ([]byte, error) { + paths = append(paths, path) + if ref != "feature/timeout" { + t.Fatalf("unexpected ref: %s", ref) + } + if _, ok := ctx.Deadline(); !ok { + t.Fatal("API lookup has no deadline") + } + if data, ok := test.files[path]; ok { + return []byte(data), nil + } + return nil, fmt.Errorf("missing %s: %w", path, os.ErrNotExist) + }, "feature/timeout") + if (err != nil) != test.wantErr || got != test.want { + t.Fatalf("got %s, %v; want %s, error=%v", got, err, test.want, test.wantErr) + } + if !reflect.DeepEqual(paths, test.paths) { + t.Fatalf("read %v; want %v", paths, test.paths) + } + }) + } +} + +func TestConfigLookupFailurePreventsClone(t *testing.T) { + commandsPath := t.TempDir() + "/commands" + t.Setenv("GIT_TEST_COMMANDS", commandsPath) + installFakeGit(t, "echo clone > \"$GIT_TEST_COMMANDS\"\n") + apiErr := errors.New("API unavailable") + err := CloneGitRepoAndDoActionWithConfig("https://example.com/repo", "main", "", "", "", func(ctx context.Context, path, ref string) ([]byte, error) { + if ref != "main" { + t.Fatalf("unexpected ref: %s", ref) + } + return nil, apiErr + }, func(string) error { + t.Fatal("action called after configuration lookup failed") + return nil + }) + if !errors.Is(err, apiErr) { + t.Fatalf("expected API error, got %v", err) + } + if _, err := os.Stat(commandsPath); !os.IsNotExist(err) { + t.Fatal("clone started after configuration lookup failed") + } +} ++++++ digger-cli.obsinfo ++++++ --- /var/tmp/diff_new_pack.8BjW3e/_old 2026-10-01 16:49:40.346644314 +0200 +++ /var/tmp/diff_new_pack.8BjW3e/_new 2026-10-01 16:49:40.351644524 +0200 @@ -1,5 +1,5 @@ name: digger-cli -version: 0.6.151 -mtime: 1789290832 -commit: 85cd59db7caa3d8bf6cefe303a62c12812b0bc48 +version: 0.6.152 +mtime: 1790787369 +commit: 9ac4e2237b7b8afa455f422b92737bfe106d495c ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/digger-cli/vendor.tar.gz /work/SRC/openSUSE:Factory/.digger-cli.new.1253/vendor.tar.gz differ: char 32, line 2
