diff --git a/act/common/git/git.go b/act/common/git/git.go index 69d1438b..1f7e6294 100644 --- a/act/common/git/git.go +++ b/act/common/git/git.go @@ -9,11 +9,13 @@ import ( "errors" "fmt" "io" + nethttp "net/http" "os" "path" "regexp" "strings" "sync" + "time" "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/internal/pkg/lock" @@ -22,6 +24,8 @@ import ( "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/client" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/mattn/go-isatty" log "github.com/sirupsen/logrus" @@ -38,6 +42,20 @@ var ( ErrShortRef = errors.New("short SHA references are not supported") ) +func init() { + httpClient := newHTTPClient(&nethttp.HTTP2Config{SendPingTimeout: 30 * time.Second}) + client.InstallProtocol("http", httpClient) + client.InstallProtocol("https", httpClient) +} + +// newHTTPClient pings idle HTTP/2 connections to drop stalled ones. +func newHTTPClient(h2 *nethttp.HTTP2Config) transport.Transport { + defaultTransport, _ := nethttp.DefaultTransport.(*nethttp.Transport) + httpTransport := defaultTransport.Clone() + httpTransport.HTTP2 = h2 + return http.NewClient(&nethttp.Client{Transport: httpTransport}) +} + // AcquireCloneLock returns an unlock function after locking the per-directory mutex for dir. // Only concurrent operations targeting the same directory are serialized; clones into different directories run in parallel. // Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too, diff --git a/act/common/git/git_test.go b/act/common/git/git_test.go index 17cc7bb6..4355c3a4 100644 --- a/act/common/git/git_test.go +++ b/act/common/git/git_test.go @@ -6,7 +6,9 @@ package git import ( "context" + "crypto/tls" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -756,3 +758,25 @@ func TestNewGitCloneExecutorFetchHonoursContext(t *testing.T) { t.Fatal("fetch ignored context cancellation") } } + +func TestNewGitCloneExecutorFailsOnSilentHTTP2Connection(t *testing.T) { + server := httptest.NewUnstartedServer(nil) + server.EnableHTTP2 = true + server.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ + "h2": func(_ *http.Server, conn *tls.Conn, _ http.Handler) { _, _ = io.Copy(io.Discard, conn) }, + } + server.StartTLS() + t.Cleanup(server.Close) + + previous := gogitclient.Protocols["https"] + gogitclient.InstallProtocol("https", newHTTPClient(&http.HTTP2Config{SendPingTimeout: 50 * time.Millisecond, PingTimeout: 50 * time.Millisecond})) + t.Cleanup(func() { gogitclient.InstallProtocol("https", previous) }) + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + err := NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: server.URL + "/action", Ref: "main", Dir: filepath.Join(t.TempDir(), "action"), InsecureSkipTLS: true, + })(ctx) + require.Error(t, err) + require.NoError(t, ctx.Err()) +}