Fix Actions pull_request.paths being triggered incorrectly by rebase (#36045)

Partially fix #34710 

The bug described in #34710 can be divided into two parts: `push.paths`
and `pull_request.paths`. This PR fixes the issue related to
`pull_request.paths`. The root cause is that the check for whether the
workflow can be triggered happens **before** updating the PR’s merge
base. This causes the file-change detection to use the old merge base.
Therefore, we need to update the merge base first and then check whether
the workflow can be triggered.
This commit is contained in:
Zettat123
2025-11-28 12:33:52 -07:00
committed by GitHub
parent a36951aef6
commit f4e38e6367
3 changed files with 73 additions and 13 deletions

View File

@@ -427,10 +427,16 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
for _, pr := range headBranchPRs { for _, pr := range headBranchPRs {
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() { if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID) changed, newMergeBase, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
if err != nil { if err != nil {
log.Error("checkIfPRContentChanged: %v", err) log.Error("checkIfPRContentChanged: %v", err)
} }
if newMergeBase != "" && pr.MergeBase != newMergeBase {
pr.MergeBase = newMergeBase
if _, err := pr.UpdateColsIfNotMerged(ctx, "merge_base"); err != nil {
log.Error("Update merge base for %-v: %v", pr, err)
}
}
if changed { if changed {
// Mark old reviews as stale if diff to mergebase has changed // Mark old reviews as stale if diff to mergebase has changed
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil { if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
@@ -496,30 +502,30 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
// checkIfPRContentChanged checks if diff to target branch has changed by push // checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, mergeBase string, err error) {
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) // FIXME: why it still needs to create a temp repo, since the alongside calls like GetDiverging doesn't do so anymore prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) // FIXME: why it still needs to create a temp repo, since the alongside calls like GetDiverging doesn't do so anymore
if err != nil { if err != nil {
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
return false, err return false, "", err
} }
defer cancel() defer cancel()
tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
if err != nil { if err != nil {
return false, fmt.Errorf("OpenRepository: %w", err) return false, "", fmt.Errorf("OpenRepository: %w", err)
} }
defer tmpRepo.Close() defer tmpRepo.Close()
// Find the merge-base // Find the merge-base
_, base, err := tmpRepo.GetMergeBase("", "base", "tracking") mergeBase, _, err = tmpRepo.GetMergeBase("", "base", "tracking")
if err != nil { if err != nil {
return false, fmt.Errorf("GetMergeBase: %w", err) return false, "", fmt.Errorf("GetMergeBase: %w", err)
} }
cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base) cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, mergeBase)
stdoutReader, stdoutWriter, err := os.Pipe() stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil { if err != nil {
return false, fmt.Errorf("unable to open pipe for to run diff: %w", err) return false, mergeBase, fmt.Errorf("unable to open pipe for to run diff: %w", err)
} }
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
@@ -535,19 +541,19 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest,
}). }).
Run(ctx); err != nil { Run(ctx); err != nil {
if err == util.ErrNotEmpty { if err == util.ErrNotEmpty {
return true, nil return true, mergeBase, nil
} }
err = gitcmd.ConcatenateError(err, stderr.String()) err = gitcmd.ConcatenateError(err, stderr.String())
log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v", log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
newCommitID, oldCommitID, base, newCommitID, oldCommitID, mergeBase,
pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch, pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
err) err)
return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err) return false, mergeBase, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, mergeBase, err)
} }
return false, nil return false, mergeBase, nil
} }
// PushToBaseRepo pushes commits from branches of head repository to // PushToBaseRepo pushes commits from branches of head repository to

View File

@@ -333,7 +333,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if headCommitID == commitID { if headCommitID == commitID {
stale = false stale = false
} else { } else {
stale, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID) stale, _, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release" release_service "code.gitea.io/gitea/services/release"
@@ -1595,3 +1596,56 @@ jobs:
assert.NotNil(t, run) assert.NotNil(t, run)
}) })
} }
func TestPullRequestWithPathsRebase(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repoName := "actions-pr-paths-rebase"
apiRepo := createActionsTestRepo(t, token, repoName, false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
apiCtx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository)
runner := newMockRunner()
runner.registerAsRepoRunner(t, "user2", repoName, "mock-runner", []string{"ubuntu-latest"}, false)
// init files and dirs
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir1/dir1.txt", "1")
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir2/dir2.txt", "2")
wfFileContent := `name: ci
on:
pull_request:
paths:
- 'dir1/**'
jobs:
ci-job:
runs-on: ubuntu-latest
steps:
- run: echo 'ci'
`
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", ".gitea/workflows/ci.yml", wfFileContent)
// create a PR to modify "dir1/dir1.txt", the workflow will be triggered
testEditFileToNewBranch(t, session, "user2", repoName, repo.DefaultBranch, "update-dir1", "dir1/dir1.txt", "11")
_, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir1")(t)
assert.NoError(t, err)
pr1Task := runner.fetchTask(t)
_, _, pr1Run := getTaskAndJobAndRunByTaskID(t, pr1Task.Id)
assert.Equal(t, webhook_module.HookEventPullRequest, pr1Run.Event)
// create a PR to modify "dir2/dir2.txt" then update main branch and rebase, the workflow will not be triggered
testEditFileToNewBranch(t, session, "user2", repoName, repo.DefaultBranch, "update-dir2", "dir2/dir2.txt", "22")
apiPull, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir2")(t)
runner.fetchNoTask(t)
assert.NoError(t, err)
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11") // change the file in "dir1"
req := NewRequestWithValues(t, "POST",
fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index), // update by rebase
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
session.MakeRequest(t, req, http.StatusSeeOther)
runner.fetchNoTask(t)
})
}