summaryrefslogtreecommitdiffstats
path: root/scripts/gerrit/cherry-pick_automation/notifier.js
blob: 39a61146891826ca649116d7ba4c0607cb09e1d5 (plain)
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

exports.id = "notifier";

const path = require('path');
const onExit = require("node-cleanup");
let autoload = require("auto-load")
let fs = require('fs');
const toBool = require("to-bool");

const Logger = require("./logger");
const logger = new Logger();
exports.logger = logger;
logger.log("Logger started...");
const Server = require("./server");
const server = new Server(logger);
exports.server = server;
const RequestProcessor = require("./requestProcessor");
const requestProcessor = new RequestProcessor(logger);
exports.requestProcessor = requestProcessor;
server.requestProcessor = requestProcessor;
const RetryProcessor = require("./retryProcessor");
const retryProcessor = new RetryProcessor(logger, requestProcessor);
exports.retryProcessor = retryProcessor;
requestProcessor.retryProcessor = retryProcessor;
const SingleRequestManager = require("./singleRequestManager");
const singleRequestManager = new SingleRequestManager(logger, retryProcessor, requestProcessor);
const RelationChainManager = require("./relationChainManager");
const relationChainManager = new RelationChainManager(logger, retryProcessor, requestProcessor);
const StartupStateRecovery = require("./startupStateRecovery");
const startupStateRecovery = new StartupStateRecovery(logger, requestProcessor);
const postgreSQLClient = require("./postgreSQLClient");

exports.registerCustomListener = registerCustomListener;

function envOrConfig(ID, configFile) {
  if (process.env[ID]) {
    return process.env[ID];
  } else if (configFile && fs.existsSync(configFile)) {
    const config = require(configFile);
    return config[ID];
  }
}

// release resources here before node exits
onExit(function (exitCode, signal) {
  if (signal) {
    logger.log("Cleaning up...");
    postgreSQLClient.end(() => {
      logger.log("Exiting");
      process.exit(0);
    });
    onExit.uninstall(); // don't call cleanup handler again
    return false;
  }
});

// Create user bots which can tie into the rest of the system.
// Bots should accept this instance of Notifier as the only
// constructor parameter.
// Bot configuration should set an environment variable of the same
// name as the bot, upper cased and suffixed with _ENABLED.
// If this variable is set in neither the environment or in the
// config file, the plugin will not be loaded.
if (!fs.existsSync('plugin_bots'))
  fs.mkdirSync('plugin_bots');
let plugin_bots = autoload('plugin_bots');
let initialized_bots = {};
Object.keys(plugin_bots).forEach((bot) => {
  if (
    toBool(envOrConfig(`${bot.toUpperCase()}_ENABLED`, path.resolve("plugin_bots", bot, "config.json")))
  ) {
    initialized_bots[bot] = new plugin_bots[bot][bot](this);
    logger.log(`plugin "${bot}" loaded`);
  } else {
    logger.log(`${bot} is disabled in config. Skipping...`);
  }
});

// Plugin bots can use this function to register a custom listener to
// route events from one module to itself. For example, to set up
// a listener for server to emit an event "integrationFail"
function registerCustomListener(source, event, destination) {
  source.on(event, function () {
    destination(...arguments)
  });
}

// Notifier handles all event requests from worker modules.
// A worker module should avoid calling a function from itself or another
// module directly where possible. Instead, it should always send an
// event to Notifier, which will route the event. The exception to this
// rule is where a synchronous operation with callback is required. In
// this case, the function performing the direct call should be responsible
// for emitting a signal to Notifier when its operation is complete.

// Emitted when the HTTP Listener is up and running and we're actively listening
// for incoming POST events to /gerrit-events
server.on("serverStarted", (info) => {
  if (info)
    console.log(info);
});

// Emitted by server when a new incoming request is received by the listener.
server.on("newRequest", (reqBody) => {
  server.receiveEvent(reqBody);
});

// Emitted by the server when the incoming request has been written to the database.
server.on("newRequestStored", (uuid) => {
  requestProcessor.processMerge(uuid);
});

// Emitted by startupStateRecovery if an in-process item stored had no
// cherry-picks created yet.
startupStateRecovery.on("recoverFromStart", (uuid) => {
  requestProcessor.processMerge(uuid);
});

// Emitted when an incoming change has been found to have a pick-to header with
// at least one branch. Before continuing, determine if the change is part of
// a relation chain and process it accordingly.
requestProcessor.on("determineProcessingPath", (parentJSON, branches) => {
  requestProcessor.determineProcessingPath(parentJSON, branches);
});

// Emitted when a merged change is part of a relation chain.
requestProcessor.on("processAsSingleChange", (parentJSON, branches) => {
  singleRequestManager.start(parentJSON, branches);
});

// Emitted when a merged change is part of a relation chain.
requestProcessor.on("processAsRelatedChange", (parentJSON, branches) => {
  relationChainManager.start(parentJSON, branches);
});

// Emitted when a change-merged event found pick-to branches in the commit message.
requestProcessor.on("validateBranch", (parentJSON, branch, responseSignal) => {
  requestProcessor.validateBranch(parentJSON, branch, responseSignal);
});

// Emitted when a branch needs to be checked against private lts branches.
requestProcessor.on("checkLtsTarget", (currentJSON, branch, newParentRev, responseSignal) => {
  requestProcessor.checkLtsTarget(currentJSON, branch, newParentRev, responseSignal);
});

// Emitted when a cherry pick is dependent on another cherry pick.
requestProcessor.on("verifyParentPickExists", (parentJSON, branch, responseSignal, errorSignal, isRetry) => {
  requestProcessor.verifyParentPickExists(parentJSON, branch, responseSignal, errorSignal, isRetry);
});

// Emitted when a cherry-pick's parent is not a suitable target on the pick-to branch.
requestProcessor.on("locateNearestParent", (currentJSON, next, branch, responseSignal) =>
  requestProcessor.locateNearestParent(currentJSON, next, branch, responseSignal));

// Emitted when a branch has been validated against the merge's project in codereview.
requestProcessor.on(
  "validBranchReadyForPick",
  (parentJSON, branch, newParentRev, responseSignal) => {
    requestProcessor.doCherryPick(parentJSON, branch, newParentRev, responseSignal);
  }
);

// Emitted when a new cherry pick has been generated on codereview.
requestProcessor.on("newCherryPick", (parentJSON, cherryPickJSON, responseSignal) => {
  requestProcessor.processNewCherryPick(parentJSON, cherryPickJSON, responseSignal);
});

// Emitted when a cherry pick has been validated and has no conflicts.
requestProcessor.on("cherryPickDone", (parentJSON, cherryPickJSON, responseSignal) => {
  requestProcessor.autoApproveCherryPick(parentJSON, cherryPickJSON, responseSignal);
});

// Emitted when a cherry pick needs to stage against a specific parent change.
requestProcessor.on(
  "stageEligibilityCheck",
  (originalRequestJSON, cherryPickJSON, responseSignal, errorSignal) => {
    requestProcessor.stagingReadyCheck(
      originalRequestJSON, cherryPickJSON,
      responseSignal, errorSignal
    );
  }
);

// Emitted when a cherry pick is approved and ready for automatic staging.
requestProcessor.on("cherrypickReadyForStage", (parentJSON, cherryPickJSON, responseSignal) => {
  requestProcessor.stageCherryPick(parentJSON, cherryPickJSON, responseSignal);
});

// Emitted when a comment is requested to be posted to codereview.
// requestProcessor.gerritCommentHandler handles failure cases and posts
// this event again for retry. This design is such that the gerritCommentHandler
// or this event can be fired without caring about the result.
requestProcessor.on(
  "postGerritComment",
  (parentUuid, fullChangeID, revision, message, notifyScope, customGerritAuth) => {
    requestProcessor.gerritCommentHandler(
      parentUuid, fullChangeID, revision,
      message, notifyScope, customGerritAuth
    );
  }
);

// Emitted when a job fails to complete for a non-fatal reason such as network
// disruption. The job is then stored in the database and rescheduled.
requestProcessor.on("addRetryJob", (originalUuid, action, args) => {
  retryProcessor.addRetryJob(originalUuid, action, args);
});

// Emitted when a retry job should be processed again.
retryProcessor.on("processRetry", (uuid) => {
  logger.log(`Retrying retry job: ${uuid}`, "warn");
  retryProcessor.processRetry(uuid, function (success, data) {
    // This callback should only be called if the database threw
    // an error. This should not happen, so just log the failure.
    logger.log(
      `A database error occurred when trying to process a retry for ${uuid}: ${data}`,
      "warn"
    );
  });
});

// Restore any open event listeners, followed by any items that
// weren't complete before the last app shutdown.
startupStateRecovery.restoreActionListeners();
startupStateRecovery.on("RestoreListenersDone", () => {
  logger.log("Finished restoring listeners from the database.");
  startupStateRecovery.restoreProcessingItems();
});

startupStateRecovery.on("restoreProcessingDone", () => {
  logger.log("Finished restoring in-process items from the database.");
});

// Start the server and begin listening for incoming webhooks.w
server.startListening();