feat: shallow clone action repositories (#1053)

## Summary

When a workflow references a remote action (e.g. `uses: actions/checkout@v4`) the runner clones that repository during job setup.
Previously this was always a full clone(every branch and the complete history) even though only a single ref is needed.

This PR makes the runner shallow-clone the requested ref by default (`--depth=1`, single branch), falling back to a full clone when a shallow clone fails.

Notes:
- Existing on-disk caches are reused as-is; there is no forced re-clone on upgrade.

## Changes

- A new `runner.action_shallow_clone` option (default `true`) lets operators opt back into full clones.
- `cloneAtDepth`: attempt a shallow clone; fall back to a full clone when shallow clone fails.
- Keep a shallow cache cheap on update: fetch the single requested ref at depth 1 and skip `pull`.

---------

Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1053
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
Zettat123
2026-06-28 20:12:21 +00:00
committed by Nicolas
parent 8f72c60afa
commit 99bc50d538
8 changed files with 178 additions and 3 deletions

View File

@@ -257,6 +257,10 @@ type NewGitCloneExecutorInput struct {
Token string
OfflineMode bool
// Depth limits the clone/fetch to the given number of commits from the tip of the requested ref.
// 0 for full clone.
Depth int
// For Gitea
InsecureSkipTLS bool
}
@@ -309,7 +313,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
}
}
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
r, err = cloneAtDepth(ctx, input, cloneOptions, logger)
if err != nil {
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
return nil, false, err
@@ -364,6 +368,16 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
pullOptions.InsecureSkipTLS = true
}
// Action clones only ever need the tip commit, so keep a shallow cache cheap on update at depth 1 regardless of its original depth
// Turning action_shallow_clone off does not convert an existing shallow cache; evict it for a full clone.
shallow := isShallow(r)
if shallow {
fetchOptions.Depth = 1
if spec, ok := shallowFetchRefSpec(r, input.Ref); ok {
fetchOptions.RefSpecs = []config.RefSpec{spec}
}
}
if !isOfflineMode {
err = r.Fetch(&fetchOptions)
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
@@ -431,11 +445,13 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
reusedMsg := ""
if !isOfflineMode {
switch {
case !isOfflineMode && !shallow:
// In shallow mode the depth-limited fetch above already advanced the ref.
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
logger.Debugf("Unable to pull %s: %v", refName, err)
}
} else if reused {
case isOfflineMode && reused:
reusedMsg = " (reused in offline mode)"
}
@@ -468,3 +484,53 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return nil
}
}
// cloneAtDepth clones input.URL into input.Dir using opts.
// With input.Depth > 0 it first tries a shallow, single-branch clone of input.Ref, falling back when error.
func cloneAtDepth(ctx context.Context, input NewGitCloneExecutorInput, opts git.CloneOptions, logger log.FieldLogger) (*git.Repository, error) {
if input.Depth > 0 {
for _, refName := range []plumbing.ReferenceName{
plumbing.NewBranchReferenceName(input.Ref),
plumbing.NewTagReferenceName(input.Ref),
} {
shallowOpts := opts
shallowOpts.Depth = input.Depth
shallowOpts.SingleBranch = true
shallowOpts.ReferenceName = refName
shallowOpts.Tags = git.NoTags
r, err := git.PlainCloneContext(ctx, input.Dir, false, &shallowOpts)
if err == nil {
return r, nil
}
logger.Debugf("Shallow clone of %s as %s failed: %v", input.URL, refName, err)
if rmErr := os.RemoveAll(input.Dir); rmErr != nil {
return nil, fmt.Errorf("remove partial clone %s: %w", input.Dir, rmErr)
}
}
logger.Debugf("Falling back to a full clone of %s for ref %q", input.URL, input.Ref)
}
return git.PlainCloneContext(ctx, input.Dir, false, &opts)
}
// isShallow reports whether the local repository was cloned with a limited depth.
func isShallow(r *git.Repository) bool {
shallows, err := r.Storer.Shallow()
return err == nil && len(shallows) > 0
}
// shallowFetchRefSpec returns the single refspec that updates only input.Ref, keeping a shallow clone from re-downloading every branch's history.
// ok is false when the ref is not present locally as a tag or remote-tracking branch, in which case the broad default refspec is used.
func shallowFetchRefSpec(r *git.Repository, ref string) (config.RefSpec, bool) {
tagRef := plumbing.NewTagReferenceName(ref)
if _, err := r.Reference(tagRef, false); err == nil {
return config.RefSpec(fmt.Sprintf("+%s:%s", tagRef, tagRef)), true
}
remoteRef := plumbing.NewRemoteReferenceName("origin", ref)
if _, err := r.Reference(remoteRef, false); err == nil {
branchRef := plumbing.NewBranchReferenceName(ref)
return config.RefSpec(fmt.Sprintf("+%s:%s", branchRef, remoteRef)), true
}
return "", false
}