diff --git a/act/common/git/git.go b/act/common/git/git.go index 16120c70..28b17655 100644 --- a/act/common/git/git.go +++ b/act/common/git/git.go @@ -265,8 +265,23 @@ type NewGitCloneExecutorInput struct { func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) { r, err := git.PlainOpen(input.Dir) if err == nil { - // Reuse existing clone - return r, true, nil + // Verify the cached clone still points to the resolved URL before reusing it. + remote, err := r.Remote("origin") + if err == nil && len(remote.Config().URLs) > 0 && remote.Config().URLs[0] == input.URL { + // Reuse existing clone + return r, true, nil + } + + if err != nil { + logger.Debugf("Removing cached clone at %s because origin cannot be read: %v", input.Dir, err) + } else if len(remote.Config().URLs) == 0 { + logger.Debugf("Removing cached clone at %s because origin has no URL", input.Dir) + } else { + logger.Debugf("Removing cached clone at %s because origin URL changed from %s to %s", input.Dir, remote.Config().URLs[0], input.URL) + } + if err := os.RemoveAll(input.Dir); err != nil { + return nil, false, fmt.Errorf("remove cached clone %s: %w", input.Dir, err) + } } var progressWriter io.Writer diff --git a/act/common/git/git_test.go b/act/common/git/git_test.go index 1a3398ef..ed40e0c5 100644 --- a/act/common/git/git_test.go +++ b/act/common/git/git_test.go @@ -235,6 +235,51 @@ func TestGitCloneExecutor(t *testing.T) { } } +func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) { + createRemote := func(message string) string { + remoteDir := t.TempDir() + require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir)) + + workDir := t.TempDir() + require.NoError(t, gitCmd("clone", remoteDir, workDir)) + require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main")) + require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", message)) + require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main")) + + return remoteDir + } + + oldRemoteDir := createRemote("old-action") + newRemoteDir := createRemote("new-action") + cacheDir := t.TempDir() + + require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: oldRemoteDir, + Ref: "main", + Dir: cacheDir, + })(t.Context())) + + markerPath := filepath.Join(cacheDir, "stale-marker") + require.NoError(t, os.WriteFile(markerPath, []byte("stale"), 0o644)) + + require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: newRemoteDir, + Ref: "main", + Dir: cacheDir, + })(t.Context())) + + originURL, err := findGitRemoteURL(t.Context(), cacheDir, "origin") + require.NoError(t, err) + assert.Equal(t, newRemoteDir, originURL) + + out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output() + require.NoError(t, err) + assert.Equal(t, "new-action", strings.TrimSpace(string(out))) + + _, err = os.Stat(markerPath) + require.True(t, os.IsNotExist(err), "stale cached directory should be removed before recloning") +} + func TestGitCloneExecutorNonFastForwardRef(t *testing.T) { // Simulate the scenario where a remote ref (e.g. a GitHub PR head ref) changes // non-fast-forward between two fetches. Before the fix, the fetch used Force=false,