From 7c6f1261d4bcba4ebc123a694b75c125dac76d88 Mon Sep 17 00:00:00 2001 From: Michael Hoang <194829+enzime@noreply.gitea.com> Date: Sun, 26 Apr 2026 11:08:23 +0000 Subject: [PATCH] fix: fetch when other refs get force-pushed (#846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due to `NewGitCloneExecutor` fetching all the refs (rather than `GoGitActionCache`), if any refs move in a non-fast-forward fashion, this causes the entire action update to fail. As GitHub has special refs like `pull/N/merge` which are guaranteed to move in a non-fast-forward fashion, this leads actions from GitHub usually failing to update. ``` ☁ git clone 'https://github.com/Mic92/update-flake-inputs-gitea' # ref=main cloning https://github.com/Mic92/update-flake-inputs-gitea to /var/lib/gitea-runner/nix0/.cache/act/9b0155f2957ac84c749f9ecc8afaec823af5ef2e67a104ac655623aee12ca5b2 Non-terminating error while running 'git clone': some refs were not updated ``` With the repo https://github.com/Mic92/update-flake-inputs-gitea, you can notice that it only has a `main` branch that moves in a fast-forward fashion and no tags that could've been force pushed. Fixes #726 --------- Co-authored-by: Michael Hoang Reviewed-on: https://gitea.com/gitea/runner/pulls/846 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Reviewed-by: Nicolas Co-authored-by: Michael Hoang <194829+enzime@noreply.gitea.com> Co-committed-by: Michael Hoang <194829+enzime@noreply.gitea.com> --- act/common/git/git.go | 1 + act/common/git/git_test.go | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/act/common/git/git.go b/act/common/git/git.go index f3ecd992..f59eddd1 100644 --- a/act/common/git/git.go +++ b/act/common/git/git.go @@ -277,6 +277,7 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) { fetchOptions.RefSpecs = []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"} + fetchOptions.Force = true pullOptions.Force = true if token != "" { diff --git a/act/common/git/git_test.go b/act/common/git/git_test.go index 25b8009d..ff9e4c5b 100644 --- a/act/common/git/git_test.go +++ b/act/common/git/git_test.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "syscall" "testing" @@ -220,6 +221,62 @@ func TestGitCloneExecutor(t *testing.T) { } } +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, + // causing go-git to return ErrForceNeeded and short-circuit the checkout. + + gitConfig() + + // Create a bare "remote" repo with an initial commit on main and a feature branch. + remoteDir := t.TempDir() + require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir)) + + // We need a working clone to push commits from. + 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", "initial")) + require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main")) + + // Create a feature branch (simulates refs/pull/N/head). + require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature")) + require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-1")) + require.NoError(t, gitCmd("-C", workDir, "push", "origin", "feature")) + + // First clone via the executor — should succeed and cache the repo. + cloneDir := t.TempDir() + clone := NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: remoteDir, + Ref: "main", + Dir: cloneDir, + }) + require.NoError(t, clone(context.Background())) + + // Now force-push the feature branch to a non-fast-forward commit (simulates + // a PR rebase). This makes refs/heads/feature non-fast-forward. + require.NoError(t, gitCmd("-C", workDir, "checkout", "main")) + require.NoError(t, gitCmd("-C", workDir, "branch", "-D", "feature")) + require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "feature")) + require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-rewritten")) + require.NoError(t, gitCmd("-C", workDir, "push", "--force", "origin", "feature")) + + // Also advance main so we can verify the clone picks up the new commit. + require.NoError(t, gitCmd("-C", workDir, "checkout", "main")) + require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "second")) + require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main")) + + // Second clone to the same directory — before the fix this returned ErrForceNeeded + // and left the working tree at the old commit. + err := clone(context.Background()) + require.NoError(t, err, "fetch with non-fast-forward refs must not fail when Force=true") + + // Verify the working tree was actually updated to the latest main commit. + out, err := exec.Command("git", "-C", cloneDir, "log", "--oneline", "-1", "--format=%s").Output() + require.NoError(t, err) + assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit") +} + func gitConfig() { if os.Getenv("GITHUB_ACTIONS") == "true" { var err error