about summary refs log tree commit diff
path: root/submitqueue/runner.go
blob: 6e4a54a71bb9e157083058a0dba0e2269e579942 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package submitqueue

import (
	"fmt"
	"sync"

	"github.com/apex/log"

	"github.com/tweag/gerrit-queue/gerrit"
)

// Runner is a struct existing across the lifetime of a single run of the submit queue
// it contains a mutex to avoid being run multiple times.
// In fact, it even cancels runs while another one is still in progress.
// It contains a Gerrit object facilitating access, a log object, the configured submit queue tag
// and a `wipSerie` (only populated if waiting for a rebase)
type Runner struct {
	mut              sync.Mutex
	currentlyRunning bool
	wipSerie         *gerrit.Serie
	logger           *log.Logger
	gerrit           *gerrit.Client
}

// NewRunner creates a new Runner struct
func NewRunner(logger *log.Logger, gerrit *gerrit.Client) *Runner {
	return &Runner{
		logger: logger,
		gerrit: gerrit,
	}
}

// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase
// for this, it needs to:
//  * have the "Autosubmit" label set to +1
//  * have gerrit's 'submittable' field set to true
// it doesn't check if the series is rebased on HEAD
func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool {
	for _, c := range s.ChangeSets {
		if c.Submittable != true || !c.IsAutosubmit() {
			return false
		}
	}
	return true
}

// IsCurrentlyRunning returns true if the runner is currently running
func (r *Runner) IsCurrentlyRunning() bool {
	return r.currentlyRunning
}

// GetWIPSerie returns the current wipSerie, if any, nil otherwiese
// Acquires a lock, so check with IsCurrentlyRunning first
func (r *Runner) GetWIPSerie() *gerrit.Serie {
	r.mut.Lock()
	defer func() {
		r.mut.Unlock()
	}()
	return r.wipSerie
}

// Trigger gets triggered periodically
func (r *Runner) Trigger(fetchOnly bool) error {
	// TODO: If CI fails, remove the auto-submit labels => rules.pl
	// Only one trigger can run at the same time
	r.mut.Lock()
	if r.currentlyRunning {
		return fmt.Errorf("Already running, skipping")
	}
	r.currentlyRunning = true
	r.mut.Unlock()
	defer func() {
		r.mut.Lock()
		r.currentlyRunning = false
		r.mut.Unlock()
	}()

	// Prepare the work by creating a local cache of gerrit state
	err := r.gerrit.Refresh()
	if err != nil {
		return err
	}

	// early return if we only want to fetch
	if fetchOnly {
		return nil
	}

	if r.wipSerie != nil {
		// refresh wipSerie with how it looks like in gerrit now
		wipSerie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
			// the new wipSerie needs to have the same number of changesets
			if len(r.wipSerie.ChangeSets) != len(s.ChangeSets) {
				return false
			}
			// … and the same ChangeIDs.
			for idx, c := range s.ChangeSets {
				if r.wipSerie.ChangeSets[idx].ChangeID != c.ChangeID {
					return false
				}
			}
			return true
		})
		if wipSerie == nil {
			r.logger.WithField("wipSerie", r.wipSerie).Warn("wipSerie has disappeared")
			r.wipSerie = nil
		} else {
			r.wipSerie = wipSerie
		}
	}

	for {
		// initialize logger
		r.logger.Info("Running")
		if r.wipSerie != nil {
			// if we have a wipSerie
			l := r.logger.WithField("wipSerie", r.wipSerie)
			l.Info("Checking wipSerie")

			// discard wipSerie not rebased on HEAD
			// we rebase them at the end of the loop, so this means master advanced without going through the submit queue
			if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) {
				l.Warnf("HEAD has moved to {} while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD())
				r.wipSerie = nil
				continue
			}

			// we now need to check CI feedback:
			// wipSerie might have failed CI in the meantime
			for _, c := range r.wipSerie.ChangeSets {
				if c.Verified < 0 {
					l.WithField("failingChangeset", c).Warnf("wipSerie failed CI in the meantime, discarding.")
					r.wipSerie = nil
					continue
				}
			}

			// it might still be waiting for CI
			for _, c := range r.wipSerie.ChangeSets {
				if c.Verified == 0 {
					l.WithField("pendingChangeset", c).Warnf("still waiting for CI feedback in wipSerie, going back to sleep.")
					// break the loop, take a look at it at the next trigger.
					return nil
				}
			}

			// it might be autosubmittable
			if r.isAutoSubmittable(r.wipSerie) {
				l.Infof("submitting wipSerie")
				// if the WIP changeset is ready (auto submittable and rebased on HEAD), submit
				for _, changeset := range r.wipSerie.ChangeSets {
					_, err := r.gerrit.SubmitChangeset(changeset)
					if err != nil {
						l.WithField("changeset", changeset).Error("error submitting changeset")
						r.wipSerie = nil
						return err
					}
				}
				r.wipSerie = nil
			} else {
				// should never be reached?!
				log.Warnf("reached branch we should never reach")
			}
		}

		r.logger.Info("Looking for series ready to submit")
		// Find serie, that:
		//  * has the auto-submit label
		//  * has +2 review
		//  * has +1 CI
		//  * is rebased on master
		serie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
			return r.isAutoSubmittable(s) && s.ChangeSets[0].ParentCommitIDs[0] == r.gerrit.GetHEAD()
		})
		if serie != nil {
			r.logger.WithField("serie", serie).Info("Found serie to submit without necessary rebase")
			r.wipSerie = serie
			continue
		}

		// Find serie, that:
		//  * has the auto-submit label
		//  * has +2 review
		//  * has +1 CI
		//  * is NOT rebased on master
		serie = r.gerrit.FindSerie(r.isAutoSubmittable)
		if serie == nil {
			r.logger.Info("no more submittable series found, going back to sleep.")
			break
		}

		l := r.logger.WithField("serie", serie)
		l.Info("found serie, which needs a rebase")
		// TODO: move into Client.RebaseSeries function
		head := r.gerrit.GetHEAD()
		for _, changeset := range serie.ChangeSets {
			changeset, err := r.gerrit.RebaseChangeset(changeset, head)
			if err != nil {
				l.Error(err.Error())
				return err
			}
			head = changeset.CommitID
		}
		// we don't need to care about updating the rebased changesets or getting the updated HEAD,
		// as we'll refetch it on the beginning of the next trigger anyways
		r.wipSerie = serie
		break
	}

	r.logger.Info("Run complete")
	return nil
}