blob: 6fe088295cd03f9dcbee6a092a6d2e93157bdf90 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package trigger determines if and how Gerrit CL is triggered.
package trigger
import (
"time"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/gerrit/botdata"
"go.chromium.org/luci/cv/internal/run"
)
// CQLabel is full Gerrit label name used for triggering LUCI CV, previously
// known as CQ.
const CQLabelName = "Commit-Queue"
var voteToMode = map[int32]run.Mode{
1: run.DryRun,
2: run.FullRun,
}
var modeToVote = map[run.Mode]int32{
run.DryRun: 1,
run.FullRun: 2,
}
// Find computes the latest trigger based on CQ+1 and CQ+2 votes.
//
// CQ+2 a.k.a. Full Run takes priority of CQ+1 a.k.a Dry Run,
// even if CQ+1 vote is newer. Among several equal votes, the earliest is
// selected as the *triggering* CQ vote.
//
// If the applicable ConfigGroup specifies additional modes AND user who voted
// on the *triggering* CQ vote also voted on the additional label, then the
// additional mode is selected instead of standard Dry/Full Run.
//
// Returns nil if CL is not triggered.
func Find(ci *gerritpb.ChangeInfo, cg *cfgpb.ConfigGroup) *run.Trigger {
if ci.GetStatus() != gerritpb.ChangeStatus_NEW {
return nil
}
li := ci.GetLabels()[CQLabelName]
if li == nil {
return nil
}
curRevision := ci.GetCurrentRevision()
// Check if there was a previous attempt that got canceled by means of a
// comment. Normally, CQDaemon would remove appropriate label, but in case
// of ACLs misconfiguration preventing CQDaemon from removing votes on
// behalf of users, CQDaemon will abort attempt by posting a special comment.
var prevAttemptTs time.Time
for _, msg := range ci.GetMessages() {
if bd, ok := botdata.Parse(msg); ok {
if bd.Action == botdata.Cancel && bd.Revision == curRevision && bd.TriggeredAt.After(prevAttemptTs) {
prevAttemptTs = bd.TriggeredAt
}
}
}
largest := int32(0)
var earliest time.Time
var ret *run.Trigger
for _, ai := range li.GetAll() {
val := ai.GetValue()
switch {
case val <= 0:
continue
case val < largest:
continue
case val >= 2:
// Clamp down vote value for compatibility with CQDaemon.
val = 2
}
switch t := ai.GetDate().AsTime(); {
case !prevAttemptTs.Before(t):
continue
case val > largest:
largest = val
earliest = t
case earliest.Before(t):
continue
case earliest.After(t):
earliest = t
default:
// Equal timestamps shouldn't really ever happen.
continue
}
ret = &run.Trigger{
Mode: string(voteToMode[val]),
GerritAccountId: ai.GetUser().GetAccountId(),
Time: ai.GetDate(),
Email: ai.GetUser().GetEmail(),
}
}
if ret == nil {
return nil
}
for _, mode := range cg.GetAdditionalModes() {
if applyAdditionalMode(ci, mode, ret) {
// first one wins
break
}
}
// Gerrit may copy CQ vote(s) to next patchset in some project configurations.
// In such cases, CQ vote timestamp will be *before* new patchset creation,
// and yet *this* CQ attempt started only when new patchset was created.
// So, attempt start is the latest of timestamps (CQ vote, this patchset
// creation).
revisionTs := ci.Revisions[curRevision].GetCreated()
if ret.GetTime().AsTime().Before(revisionTs.AsTime()) {
ret.Time = revisionTs
}
return ret
}
func applyAdditionalMode(ci *gerritpb.ChangeInfo, mode *cfgpb.Mode, res *run.Trigger) bool {
labelVotes := ci.GetLabels()[mode.GetTriggeringLabel()]
switch {
case labelVotes == nil:
return false
case mode.GetCqLabelValue() != modeToVote[run.Mode(res.GetMode())]:
return false
}
matchesVote := func(vote *gerritpb.ApprovalInfo) bool {
switch {
case vote.GetValue() != mode.GetTriggeringValue():
return false
case vote.GetUser().GetAccountId() != res.GetGerritAccountId():
return false
case !vote.GetDate().AsTime().Equal(res.GetTime().AsTime()):
return false
default:
return true
}
}
for _, vote := range labelVotes.GetAll() {
if matchesVote(vote) {
res.AdditionalLabel = mode.GetTriggeringLabel()
res.Mode = mode.GetName()
return true
}
}
return false
}