summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Smith <daniel.smith@qt.io>2024-03-04 12:10:23 +0100
committerDaniel Smith <daniel.smith@qt.io>2024-03-20 13:37:21 +0100
commit2b7563fc37446adddb61f3be66d504f1dc057243 (patch)
tree62df39392e808250ee0c84522a8fdf6f21269107
parentf3ae55255751a841c15ad77615e66c53cfd28f0e (diff)
Extend cherry-pick bot to support using submit in select repos
Some repos are in submit-only mode and do not use the CI. This patch adds support for using the submit command instead, based on configuration in the config.json or environment variables. This config is only made available per-repo, and cannot be used to make a specific branch submit-only. Using configuration over deductive or fallback logic is intended to be more explicit to reduce security concerns of unreviewed code being hard-submitted in unapproved repos. Task-number: QTQAINFRA-6157 Change-Id: If2d0e538369c470e0753d311e183ea736981b63c Reviewed-by: Daniel Smith <daniel.smith@qt.io>
-rw-r--r--scripts/gerrit/cherry-pick_automation/README.md10
-rw-r--r--scripts/gerrit/cherry-pick_automation/config.json1
-rw-r--r--scripts/gerrit/cherry-pick_automation/gerritRESTTools.js41
-rw-r--r--scripts/gerrit/cherry-pick_automation/notifier.js5
-rw-r--r--scripts/gerrit/cherry-pick_automation/relationChainManager.js15
-rw-r--r--scripts/gerrit/cherry-pick_automation/requestProcessor.js97
-rw-r--r--scripts/gerrit/cherry-pick_automation/singleRequestManager.js17
-rw-r--r--scripts/gerrit/cherry-pick_automation/toolbox.js21
8 files changed, 194 insertions, 13 deletions
diff --git a/scripts/gerrit/cherry-pick_automation/README.md b/scripts/gerrit/cherry-pick_automation/README.md
index eaffb207..4aa04c30 100644
--- a/scripts/gerrit/cherry-pick_automation/README.md
+++ b/scripts/gerrit/cherry-pick_automation/README.md
@@ -79,13 +79,15 @@ Configuration set via config.json OR environment variables
gerrit's REST API. `Default: ''`
7. `GERRIT_PASS` Basic Authentication password gerrit REST API user.
`Default: ''`
-8. **Optional** `SES_ACCESS_KEY_ID`: The access key ID for connecting with an Amazon SES
+8. `SUBMIT_MODE_REPOS` Repos which exclusively use Submit instead of stage.
+ If set via environment variable, use a comma-separated string format.
+9. **Optional** `SES_ACCESS_KEY_ID`: The access key ID for connecting with an Amazon SES
email client. `Default: ''`
-9. **Optional** `SES_SECRET_ACCESS_KEY`: The access key secret for connecting with an Amazon
+10. **Optional** `SES_SECRET_ACCESS_KEY`: The access key secret for connecting with an Amazon
SES email client. `Default: ''`
-10. **Optional** `ADMIN_EMAIL` Email address where critical debug messages are sent.
+11. **Optional** `ADMIN_EMAIL` Email address where critical debug messages are sent.
`Default: ''`
-11. **Optional** `EMAIL_SENDER` Email address from which critical debug messages are sent.
+12. **Optional** `EMAIL_SENDER` Email address from which critical debug messages are sent.
Note the formatting of the default sender.
`Default: '"Cherry-Pick Bot" <cherrypickbot@qt.io>'`
diff --git a/scripts/gerrit/cherry-pick_automation/config.json b/scripts/gerrit/cherry-pick_automation/config.json
index 0d6b336b..5162cd37 100644
--- a/scripts/gerrit/cherry-pick_automation/config.json
+++ b/scripts/gerrit/cherry-pick_automation/config.json
@@ -6,6 +6,7 @@
"GERRIT_PORT": 443,
"GERRIT_USER": "",
"GERRIT_PASS": "",
+ "SUBMIT_MODE_REPOS": [],
"ADMIN_EMAIL": "",
"EMAIL_SENDER": "\"Cherry-Pick Bot\" <cherrypickbot@qt.io>",
"SES_ACCESS_KEY_ID": "",
diff --git a/scripts/gerrit/cherry-pick_automation/gerritRESTTools.js b/scripts/gerrit/cherry-pick_automation/gerritRESTTools.js
index a28c8d6a..7dfbc74f 100644
--- a/scripts/gerrit/cherry-pick_automation/gerritRESTTools.js
+++ b/scripts/gerrit/cherry-pick_automation/gerritRESTTools.js
@@ -255,6 +255,47 @@ function stageCherryPick(parentUuid, cherryPickJSON, customAuth, callback) {
}, 5000);
}
+// Submit a conflict-free change directly to the target branch.
+exports.submitCherryPick = submitCherryPick;
+function submitCherryPick(parentUuid, cherryPickJSON, customAuth, callback) {
+ let url =`${
+ gerritBaseURL("changes")}/${cherryPickJSON.id}/revisions/current/submit`;
+
+ logger.log(`POST request to: ${url}`, "debug", parentUuid);
+
+ setTimeout(function () {
+ axios({ method: "post", url: url, data: {}, auth: customAuth || gerritAuth })
+ .then(function (response) {
+ logger.log(`Successfully submitted "${cherryPickJSON.id}"`, "info", parentUuid);
+ callback(true, undefined);
+ })
+ .catch(function (error) {
+ if (error.response) {
+ // The request was made and the server responded with a status code
+ // that falls out of the range of 2xx
+
+ // Call this a permanent failure for staging. Ask the owner to handle it.
+ logger.log(
+ `An error occurred in POST to "${url}". Error ${error.response.status}: ${
+ error.response.data}`,
+ "error", parentUuid
+ );
+ callback(false, { status: error.response.status, data: error.response.data });
+ } else if (error.request) {
+ // The request was made but no response was received. Retry it later.
+ callback(false, "retry");
+ } else {
+ // Something happened in setting up the request that triggered an Error
+ logger.log(
+ `Error in HTTP request while trying to submit. Error: ${safeJsonStringify(error)}`,
+ "error", parentUuid
+ );
+ callback(false, error.message);
+ }
+ });
+ }, 5000);
+}
+
// Post a comment to the change on the latest revision.
exports.postGerritComment = postGerritComment;
function postGerritComment(
diff --git a/scripts/gerrit/cherry-pick_automation/notifier.js b/scripts/gerrit/cherry-pick_automation/notifier.js
index c0583875..f716ef3f 100644
--- a/scripts/gerrit/cherry-pick_automation/notifier.js
+++ b/scripts/gerrit/cherry-pick_automation/notifier.js
@@ -194,6 +194,11 @@ requestProcessor.on("cherrypickReadyForStage", (parentJSON, cherryPickJSON, resp
requestProcessor.stageCherryPick(parentJSON, cherryPickJSON, responseSignal);
});
+// Emitted when a cherry pick is approved and ready for automatic submission.
+requestProcessor.on("cherrypickReadyForSubmit", (parentJSON, cherryPickJSON, responseSignal) => {
+ requestProcessor.submitCherryPick(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
diff --git a/scripts/gerrit/cherry-pick_automation/relationChainManager.js b/scripts/gerrit/cherry-pick_automation/relationChainManager.js
index 3efe3166..14eaf8a2 100644
--- a/scripts/gerrit/cherry-pick_automation/relationChainManager.js
+++ b/scripts/gerrit/cherry-pick_automation/relationChainManager.js
@@ -476,10 +476,17 @@ class relationChainManager {
handleCherrypickReadyForStage(originalRequestJSON, cherryPickJSON, parentChangeID, parentStatus) {
// The status of the cherry-pick's parent ok. Stage the new cherry-pick.
let _this = this;
- _this.requestProcessor.emit(
- "cherrypickReadyForStage", originalRequestJSON, cherryPickJSON,
- "relationChain_stagingDone"
- );
+ if (toolbox.repoUsesStaging(originalRequestJSON.uuid, cherryPickJSON)) {
+ _this.requestProcessor.emit(
+ "cherrypickReadyForStage", originalRequestJSON, cherryPickJSON,
+ "relationChain_stagingDone"
+ );
+ } else {
+ _this.requestProcessor.emit(
+ "cherrypickReadyForSubmit", originalRequestJSON, cherryPickJSON,
+ "relationChain_stagingDone"
+ );
+ }
}
handleCherrypickWaitForParent(originalRequestJSON, cherryPickJSON, parentChangeID, parentStatus) {
diff --git a/scripts/gerrit/cherry-pick_automation/requestProcessor.js b/scripts/gerrit/cherry-pick_automation/requestProcessor.js
index 12ab828e..b23dba76 100644
--- a/scripts/gerrit/cherry-pick_automation/requestProcessor.js
+++ b/scripts/gerrit/cherry-pick_automation/requestProcessor.js
@@ -1349,6 +1349,103 @@ class requestProcessor extends EventEmitter {
);
}
+ // Attempt to submit the cherry-pick directly to the target branch.
+ submitCherryPick(parentJSON, cherryPickJSON, responseSignal) {
+ let _this = this;
+ _this.logger.log(`Starting submit for ${cherryPickJSON.id}`, "verbose", parentJSON.uuid);
+ toolbox.addToCherryPickStateUpdateQueue(
+ parentJSON.uuid,
+ {
+ branch: cherryPickJSON.branch, statusDetail: "stagingStarted",
+ args: [parentJSON, cherryPickJSON, responseSignal]
+ },
+ "cherrypickReadyForSubmit"
+ );
+ gerritTools.submitCherryPick(
+ parentJSON.uuid, cherryPickJSON, parentJSON.customGerritAuth,
+ function (success, data) {
+ if (success) {
+ _this.logger.log(`submitd ${cherryPickJSON.id}`, "info", parentJSON.uuid);
+ toolbox.addToCherryPickStateUpdateQueue(
+ parentJSON.uuid, { branch: cherryPickJSON.branch, statusDetail: "submitted", args: [] },
+ "done_submitted",
+ function () {
+ toolbox.decrementPickCountRemaining(parentJSON.uuid);
+ }
+ );
+ _this.emit(responseSignal, true, parentJSON, cherryPickJSON);
+ } else if (data == "retry") {
+ // Do nothing. This callback function will be called again on retry.
+ _this.logger.log(`Failed to submit cherry pick ${
+ cherryPickJSON.id} due to a network issue. Retrying in a bit.`);
+ _this.retryProcessor.addRetryJob(
+ parentJSON.uuid, "cherrypickReadyForSubmit",
+ [parentJSON, cherryPickJSON, responseSignal]
+ );
+
+ toolbox.addToCherryPickStateUpdateQueue(
+ parentJSON.uuid,
+ { branch: cherryPickJSON.branch, statusDetail: "submitFailedRetryWait" },
+ "cherrypickReadyForSubmit"
+ );
+ } else {
+ _this.logger.log(
+ `Failed to submit ${cherryPickJSON.id}. Reason: ${safeJsonStringify(data)}`,
+ "error", parentJSON.uuid
+ );
+ gerritTools.locateDefaultAttentionUser(parentJSON.uuid, cherryPickJSON,
+ parentJSON.patchSet.uploader.email, function(user) {
+ if (user && user == "copyReviewers") {
+ gerritTools.copyChangeReviewers(parentJSON.uuid, parentJSON.fullChangeID,
+ cherryPickJSON.id);
+ } else {
+ gerritTools.setChangeReviewers(parentJSON.uuid, cherryPickJSON.id,
+ [user], undefined, function() {
+ gerritTools.addToAttentionSet(
+ parentJSON.uuid, cherryPickJSON, user, "Relevant user",
+ parentJSON.customGerritAuth,
+ function (success, data) {
+ if (!success) {
+ _this.logger.log(
+ `Failed to add "${user}" to the`
+ + ` attention set of ${cherryPickJSON.id}\n`
+ + `Reason: ${safeJsonStringify(data)}`,
+ "error", parentJSON.uuid
+ );
+ }
+ }
+ );
+ });
+ }
+ }
+ );
+ _this.gerritCommentHandler(
+ parentJSON.uuid, cherryPickJSON.id,
+ undefined,
+ `INFO: The cherry-pick bot failed to automatically submit this change. Please try to submit it manually.\n\nContact gerrit administration if you continue to experience issues.\n\nReason: ${
+ data
+ ? safeJsonStringify(data, undefined, 4)
+ : "Unknown error. Please contact the gerrit admins at gerrit-admin@qt-project.org"
+ }`,
+ "OWNER"
+ );
+ toolbox.addToCherryPickStateUpdateQueue(
+ parentJSON.uuid,
+ {
+ branch: cherryPickJSON.branch, statusDetail: data.data || data.message,
+ statusCode: data.status || "", args: []
+ },
+ "submitFailed",
+ function () {
+ toolbox.decrementPickCountRemaining(parentJSON.uuid);
+ }
+ );
+ _this.emit(responseSignal, false, parentJSON, cherryPickJSON);
+ }
+ }
+ );
+ }
+
// Set up a a post-comment action and retry it until it goes through.
// this function should never be relied upon to succeed, as posting
// comments will be handled in an async "it's done when it's done"
diff --git a/scripts/gerrit/cherry-pick_automation/singleRequestManager.js b/scripts/gerrit/cherry-pick_automation/singleRequestManager.js
index 300e18d4..f9bfeea4 100644
--- a/scripts/gerrit/cherry-pick_automation/singleRequestManager.js
+++ b/scripts/gerrit/cherry-pick_automation/singleRequestManager.js
@@ -6,7 +6,7 @@ exports.id = "singleRequestManager";
const safeJsonStringify = require("safe-json-stringify");
-const { findPickToBranches } = require("./toolbox");
+const { findPickToBranches, repoUsesStaging } = require("./toolbox");
const gerritTools = require("./gerritRESTTools");
// The singleRequestManager processes incoming changes linerally.
@@ -145,10 +145,17 @@ class singleRequestManager {
handleCherrypickReadyForStage(parentJSON, cherryPickJSON) {
let _this = this;
- _this.requestProcessor.emit(
- "cherrypickReadyForStage", parentJSON, cherryPickJSON,
- "singleRequest_stagingDone"
- );
+ if (repoUsesStaging(parentJSON.uuid, cherryPickJSON)) {
+ _this.requestProcessor.emit(
+ "cherrypickReadyForStage", parentJSON, cherryPickJSON,
+ "singleRequest_stagingDone"
+ );
+ } else {
+ _this.requestProcessor.emit(
+ "cherrypickReadyForSubmit", parentJSON, cherryPickJSON,
+ "singleRequest_integrationDone"
+ );
+ }
}
handleStagingDone(success, data) {
diff --git a/scripts/gerrit/cherry-pick_automation/toolbox.js b/scripts/gerrit/cherry-pick_automation/toolbox.js
index 7d17e51d..8b30f92c 100644
--- a/scripts/gerrit/cherry-pick_automation/toolbox.js
+++ b/scripts/gerrit/cherry-pick_automation/toolbox.js
@@ -10,6 +10,12 @@ const postgreSQLClient = require("./postgreSQLClient");
const { queryBranchesRe, checkBranchAndAccess, queryChange } = require("./gerritRESTTools");
const Logger = require("./logger");
const logger = new Logger();
+const config = require("./config.json");
+
+// Set default values with the config file, but prefer environment variable.
+function envOrConfig(ID) {
+ return process.env[ID] || config[ID];
+}
let dbSubStatusUpdateQueue = [];
let dbSubStateUpdateLockout = false;
@@ -383,6 +389,21 @@ function sortBranches(branches) {
});
}
+exports.repoUsesStaging = repoUsesStaging;
+function repoUsesStaging(uuid, cherryPickJSON) {
+ let submitModeRepos = envOrConfig("SUBMIT_MODE_REPOS");
+ // Convert the comma-separated list of repos to an array if it is not already.
+ if (submitModeRepos && typeof submitModeRepos === "string") {
+ submitModeRepos = submitModeRepos.split(",");
+ }
+ if (submitModeRepos && submitModeRepos.includes(cherryPickJSON.project)) {
+ logger.log(
+ `Repo ${cherryPickJSON.project} is in SUBMIT_MODE.`, "verbose", uuid);
+ return false;
+ }
+ return true;
+}
+
// Take a gerrit Change object and mock a change-merged event.
// Use the original merge event as the template for the mocked event.
exports.mockChangeMergedFromOther = mockChangeMergedFromOther;