Compare commits
1 Commits
bugfix/tim
...
ci-experim
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
cfb1bc2fe9 |
@@ -30,31 +30,167 @@ jobs:
|
||||
git fetch origin ${{ gitea.ref }}
|
||||
git checkout FETCH_HEAD
|
||||
working-directory: ./autogits
|
||||
- name: Prepare binaries
|
||||
- name: Prepare binaries (baseline)
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images
|
||||
- name: Prepare images (baseline)
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
- name: Make sure the pod is down (1)
|
||||
run: make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Start images
|
||||
- name: Start images (baseline)
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests
|
||||
run: make pytest
|
||||
- name: Run tests 30 times (baseline)
|
||||
run: |
|
||||
pass=0
|
||||
fail=0
|
||||
for i in $(seq 1 30); do
|
||||
echo "Iteration $i/30..."
|
||||
if podman exec -t tester pytest -v tests/workflow_pr_review_test.py::test_005_any_maintainer_approval_sufficient; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done
|
||||
echo "Summary (baseline): $pass passes, $fail failures"
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down
|
||||
- name: Make sure the pod is down (2)
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Cherry-pick 1
|
||||
run: |
|
||||
git config user.email "bot@example.com"
|
||||
git config user.name "Bot"
|
||||
git cherry-pick 572e33111bd72518f33ec4f7c93a7222282f43999afafac948e1e3da5c3453a0
|
||||
working-directory: ./autogits
|
||||
- name: Prepare binaries (after CP1)
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images (after CP1)
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down (3)
|
||||
run: make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Start images (after CP1)
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests 30 times (after CP1)
|
||||
run: |
|
||||
pass=0
|
||||
fail=0
|
||||
for i in $(seq 1 30); do
|
||||
echo "Iteration $i/30..."
|
||||
if podman exec -t tester pytest -v tests/workflow_pr_review_test.py::test_005_any_maintainer_approval_sufficient; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done
|
||||
echo "Summary (after CP1): $pass passes, $fail failures"
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down (4)
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Cherry-pick 2
|
||||
run: |
|
||||
git cherry-pick 8fa732e67518769c9a962e6d12c2e70b38f7bc06e26332fb007ac666fa5e38c1
|
||||
working-directory: ./autogits
|
||||
- name: Prepare binaries (after CP2)
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images (after CP2)
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down (5)
|
||||
run: make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Start images (after CP2)
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests 30 times (after CP2)
|
||||
run: |
|
||||
pass=0
|
||||
fail=0
|
||||
for i in $(seq 1 30); do
|
||||
echo "Iteration $i/30..."
|
||||
if podman exec -t tester pytest -v tests/workflow_pr_review_test.py::test_005_any_maintainer_approval_sufficient; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done
|
||||
echo "Summary (after CP2): $pass passes, $fail failures"
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down (6)
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Discard and Cherry-pick 3
|
||||
run: |
|
||||
git reset --hard FETCH_HEAD
|
||||
git cherry-pick 58f410befce4da40f3ebc27e21ac81a55b6425dd4214e08eb59359d54322a29d
|
||||
working-directory: ./autogits
|
||||
- name: Prepare binaries (after CP3)
|
||||
run: make build
|
||||
working-directory: ./autogits
|
||||
- name: Prepare images (after CP3)
|
||||
run: |
|
||||
make build
|
||||
podman rmi $(podman images -f "dangling=true" -q)
|
||||
working-directory: ./autogits/integration
|
||||
- name: Make sure the pod is down (7)
|
||||
run: make down
|
||||
working-directory: ./autogits/integration
|
||||
- name: Start images (after CP3)
|
||||
run: |
|
||||
make up
|
||||
make wait_healthy
|
||||
podman ps
|
||||
sleep 5
|
||||
working-directory: ./autogits/integration
|
||||
- name: Run tests 30 times (after CP3)
|
||||
run: |
|
||||
pass=0
|
||||
fail=0
|
||||
for i in $(seq 1 30); do
|
||||
echo "Iteration $i/30..."
|
||||
if podman exec -t tester pytest -v tests/workflow_pr_review_test.py::test_005_any_maintainer_approval_sufficient; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done
|
||||
echo "Summary (after CP3): $pass passes, $fail failures"
|
||||
working-directory: ./autogits/integration
|
||||
- name: Final cleanup
|
||||
if: always()
|
||||
run: |
|
||||
podman ps
|
||||
make down
|
||||
working-directory: ./autogits/integration
|
||||
|
||||
|
||||
@@ -768,10 +768,6 @@ func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ..
|
||||
return nil, fmt.Errorf("Cannot create pull request reviews: %w", err)
|
||||
}
|
||||
|
||||
// Invalidate the timeline cache so the next GetTimeline call reflects
|
||||
// the newly created review_requested entry.
|
||||
gitea.ResetTimelineCache(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
||||
|
||||
return review.GetPayload(), nil
|
||||
}
|
||||
|
||||
@@ -780,13 +776,6 @@ func (gitea *GiteaTransport) UnrequestReview(org, repo string, id int64, reviwer
|
||||
repository.NewRepoDeletePullReviewRequestsParams().WithOwner(org).WithRepo(repo).WithIndex(id).WithBody(&models.PullReviewRequestOptions{
|
||||
Reviewers: reviwers,
|
||||
}), gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err == nil {
|
||||
// Invalidate the timeline cache so the next GetTimeline call reflects
|
||||
// the newly created review_request_removed entry.
|
||||
gitea.ResetTimelineCache(org, repo, id)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -872,31 +861,24 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
|
||||
prID := fmt.Sprintf("%s/%s!%d", org, repo, idx)
|
||||
giteaTimelineCacheMutex.RLock()
|
||||
TimelineCache, IsCached := giteaTimelineCache[prID]
|
||||
if IsCached && TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
return TimelineCache.data, nil
|
||||
var LastCachedTime strfmt.DateTime
|
||||
if IsCached {
|
||||
l := len(TimelineCache.data)
|
||||
if l > 0 {
|
||||
LastCachedTime = TimelineCache.data[0].Updated
|
||||
}
|
||||
|
||||
// cache data for 5 seconds
|
||||
if TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
}
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
|
||||
giteaTimelineCacheMutex.Lock()
|
||||
defer giteaTimelineCacheMutex.Unlock()
|
||||
|
||||
// Re-read after acquiring the write lock: another goroutine may have
|
||||
// already refreshed the cache while we were waiting.
|
||||
TimelineCache, IsCached = giteaTimelineCache[prID]
|
||||
if IsCached && TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
|
||||
// Find the highest Updated timestamp across all cached items so the
|
||||
// incremental fetch picks up both new entries and modified ones.
|
||||
var LastCachedTime strfmt.DateTime
|
||||
for _, d := range TimelineCache.data {
|
||||
if time.Time(d.Updated).Compare(time.Time(LastCachedTime)) > 0 {
|
||||
LastCachedTime = d.Updated
|
||||
}
|
||||
}
|
||||
|
||||
for resCount > 0 {
|
||||
opts := issue.NewIssueGetCommentsAndTimelineParams().WithOwner(org).WithRepo(repo).WithIndex(idx).WithPage(&page)
|
||||
if !LastCachedTime.IsZero() {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opentracing/opentracing-go/log"
|
||||
@@ -629,29 +628,9 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// prLocks serialises concurrent processing of the same PR.
|
||||
// Both the RabbitMQ event loop and the consistency-checker goroutine call
|
||||
// ProcesPullRequest; without this lock they can race on reviewer add/remove.
|
||||
// Key format: "org/repo#num"
|
||||
var prLocks sync.Map // map[string]chan struct{}
|
||||
|
||||
func prLockKey(pr *models.PullRequest) string {
|
||||
return fmt.Sprintf("%s/%s#%d", pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
||||
}
|
||||
|
||||
func acquirePRLock(key string) chan struct{} {
|
||||
v, _ := prLocks.LoadOrStore(key, make(chan struct{}, 1))
|
||||
ch := v.(chan struct{})
|
||||
ch <- struct{}{}
|
||||
return ch
|
||||
}
|
||||
|
||||
func releasePRLock(ch chan struct{}) {
|
||||
<-ch
|
||||
}
|
||||
|
||||
type RequestProcessor struct {
|
||||
configuredRepos map[string][]*common.AutogitConfig
|
||||
recursive int
|
||||
}
|
||||
|
||||
func (w *RequestProcessor) Process(pr *models.PullRequest) error {
|
||||
@@ -668,9 +647,6 @@ func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
lock := acquirePRLock(prLockKey(pr))
|
||||
defer releasePRLock(lock)
|
||||
|
||||
PRProcessor, err := AllocatePRProcessor(pr, configs)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -687,23 +663,17 @@ func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
|
||||
common.LogInfo("panic cought --- recovered")
|
||||
common.LogError(string(debug.Stack()))
|
||||
}
|
||||
w.recursive--
|
||||
}()
|
||||
|
||||
w.recursive++
|
||||
if w.recursive > 3 {
|
||||
common.LogError("Recursion limit reached... something is wrong with this PR?")
|
||||
return nil
|
||||
}
|
||||
|
||||
var pr *models.PullRequest
|
||||
if req, ok := request.Data.(*common.PullRequestWebhookEvent); ok {
|
||||
// Skip pull_request_sync events triggered by the bot's own pushes to
|
||||
// prjgit branches. Those would re-run AssignReviewers immediately
|
||||
// after the bot itself just set them, producing spurious add/remove
|
||||
// cycles. Human-triggered syncs have a different sender and are still
|
||||
// processed normally.
|
||||
if request.Type == common.RequestType_PRSync && CurrentUser != nil &&
|
||||
req.Sender.Username == CurrentUser.UserName {
|
||||
common.LogDebug("Skipping self-triggered pull_request_sync from", req.Sender.Username,
|
||||
"on", req.Pull_Request.Base.Repo.Owner.Username+"/"+req.Pull_Request.Base.Repo.Name,
|
||||
"#", req.Pull_Request.Number)
|
||||
return nil
|
||||
}
|
||||
|
||||
pr, err = Gitea.GetPullRequest(req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
|
||||
if err != nil {
|
||||
common.LogError("Cannot find PR for issue:", req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
|
||||
@@ -740,16 +710,8 @@ func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
|
||||
common.LogError("*** Cannot find config for org:", pr.Base.Repo.Owner.UserName)
|
||||
}
|
||||
if err = ProcesPullRequest(pr, configs); err == updatePrjGitError_requeue {
|
||||
// Retry after a delay in a background goroutine so the event loop is
|
||||
// not blocked while we wait. The per-PR lock inside ProcesPullRequest
|
||||
// ensures no other processing races with the retry.
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
if err := ProcesPullRequest(pr, configs); err != nil {
|
||||
common.LogError("requeue retry failed:", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
time.Sleep(time.Second * 5)
|
||||
return w.ProcessFunc(request)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -989,6 +989,18 @@ func TestProcessFunc(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Recursion limit", func(t *testing.T) {
|
||||
reqProc.recursive = 3
|
||||
err := reqProc.ProcessFunc(&common.Request{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error on recursion limit, got %v", err)
|
||||
}
|
||||
if reqProc.recursive != 3 {
|
||||
t.Errorf("Expected recursive to be 3, got %d", reqProc.recursive)
|
||||
}
|
||||
reqProc.recursive = 0 // Reset
|
||||
})
|
||||
|
||||
t.Run("Invalid data format", func(t *testing.T) {
|
||||
err := reqProc.ProcessFunc(&common.Request{Data: nil})
|
||||
if err == nil || !strings.Contains(err.Error(), "Invalid data format") {
|
||||
|
||||
Reference in New Issue
Block a user