// Copyright (C) 2024 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 = "submodule_update_watcher"; const axios = require("axios"); const express = require("express"); const bodyParser = require('body-parser'); const gerritTools = require("./gerritRESTTools"); const { logger } = require("./logger"); const config = require("./config.json"); const safeJsonStringify = require("safe-json-stringify"); function envOrConfig(ID) { return process.env[ID] || config[ID]; } const gerritAuth = { username: envOrConfig("SUBMODULE_UPDATE_GERRIT_USER"), password: envOrConfig("SUBMODULE_UPDATE_GERRIT_PASS") }; const jenkinsAuth = { username: envOrConfig("SUBMODULE_UPDATE_JENKINS_USER"), password: envOrConfig("SUBMODULE_UPDATE_JENKINS_TOKEN") }; const jenkinsURL = envOrConfig("SUBMODULE_UPDATE_JENKINS_URL"); if (!gerritAuth.username || !gerritAuth.password || !jenkinsAuth.username) { logger.error("Missing credentials for submodule update watcher. Exiting."); logger.error(jenkinsURL ? `Jenkins URL set to ${jenkinsURL}` : "No Jenkins URL set."); process.exit(1); } function handleIntegrationFailed(req) { logger.info( `Received dependency update failure notification for ${req.change.project}` ); gerritTools.queryChange(req.fullChangeID, undefined, gerritAuth, function (success, data) { if (success && envOrConfig("SUBMODULE_UPDATE_FAILED_ATTENTION_USER")) { gerritTools.addToAttentionSet( req, envOrConfig("SUBMODULE_UPDATE_FAILED_ATTENTION_USER"), undefined, gerritAuth, function () { gerritTools.postGerritComment(req.fullChangeID, undefined, `${req.change.project} dependency update integration failed.`, undefined, gerritAuth ); } ); } else { logger.info("No default user configured to add to the attention set" + " for submodule updates.", undefined,); } // Run the bot again, which will either stage it or give up. } ); sendBuildRequest(req); } function handleIntegrationPassed(req) { if (envOrConfig("SUBMODULE_UPDATE_TEAMS_URL") && req.change.commitMessage.match(/Update (submodule refs|submodules) on/) ) { axios.post(envOrConfig("SUBMODULE_UPDATE_TEAMS_URL"), { "Text": `Successfully updated ${req.change.project} on **${req.change.branch}** submodule` + ` set in [https://codereview.qt-project.org/#/q/${req.change.id},n,z]` + `(https://codereview.qt-project.org/#/q/${req.change.id},n,z)` }).catch(function (error) { logger.info(`Unable to send teams message... ${safeJsonStringify(error)}`); }); } sendBuildRequest(req) } function handleJenkinsError(req, res, error, action) { logger.info(`Unable to ${action} submodule update job for ${req.branch || req.change.branch}.`); if (error.response) logger.info(`Jenkins Error: ${error.response.status}: ${error.response.data}`); if (res === undefined) return; // Only button presses from Teams carry a res object to respond to. if (error.response) { res.status(500).send(`Bad response from Jenkins: ${error.response.status}` + ` - ${error.response.data}
Contact the gerrit admins at` + " gerrit-admin@qt.project.org"); logger.info(`Jenkins responded with: ${error.response.status} - ${error.response.data}`); } else if (error.request) { res.status(504).send("Timeout when attempting to contact Jenkins. Contact the gerrit" + " admins at gerrit-admin@qt.project.org"); logger.info(`Jenkins timed out! URL: ${jenkinsURL}`, "SUBMODULE"); } else { res.status(500).send(`Unknown error attempting to ${action} submodule update job in` + " Jenkins. Contact the gerrit admins at gerrit-admin@qt.project.org"); logger.info(`Unknown error attempting to ${action} submodule updates ${error}`, "SUBMODULE"); } } function sendBuildRequest(req, res) { // Need to make the branch compatible with jenkins project naming rules. let branch = (req.branch || req.change.branch).replace("/", "-"); if (jenkinsURL) { logger.info(`Running new submodule update job on ${branch}`); let url = `${jenkinsURL}/job/qt_submodule_update_${branch}/buildWithParameters` axios.post(url, undefined, { auth: jenkinsAuth } ).catch(function (error) { handleJenkinsError(req, res, error, "trigger new"); }); } else { logger.info("Unable to run new submodule update job. No URL set!"); } } function pause_updates(req, res) { if (jenkinsURL) { logger.info(`Pausing submodule updates for ${req.branch}`); axios.post( `${jenkinsURL}/job/qt_submodule_update_${req.branch}/disable`, undefined, { auth: jenkinsAuth } ).then(function (response) { res.status(200).send(`Submodule update job for ${req.branch} disabled.`); axios.post(envOrConfig("SUBMODULE_UPDATE_TEAMS_URL"), { "Text": `INFO: Paused submodule update automation on '**${req.branch}**'` }).catch(function (error) { logger.info(`Unable to send teams message... ${safeJsonStringify(error)}`); }); }).catch(function (error) { handleJenkinsError(req, res, error, "disable"); }) } else { logger.info(`Unable to disable submodule update job for ${req.branch}. Jenkins` + " URL not set!"); res.status(500).send("No destination URL for Jenkins set. Contact the Gerrit Admins."); } } function resume_updates(req, res) { if (jenkinsURL) { logger.info(`Resuming submodule updates for ${req.branch}`); axios.post( `${jenkinsURL}/job/qt_submodule_update_${req.branch}/enable`, undefined, { auth: jenkinsAuth } ).then(function (response) { res.status(200).send(`Submodule update job for ${req.branch} enabled and restarted.`); sendBuildRequest(req, res); axios.post(envOrConfig("SUBMODULE_UPDATE_TEAMS_URL"), { "Text": `INFO: Resumed submodule update automation on '**${req.branch}**'` }).catch(function (error) { logger.info(`Unable to send teams message... ${safeJsonStringify(error)}`); }); }).catch(function (error) { handleJenkinsError(req, res, error, "resume"); }); } else { logger.info(`Unable to resume submodule update job for ${req.branch}.` + " Jenkins URL not set!"); res.status(500).send("No destination URL for Jenkins set. Contact the Gerrit Admins."); } } function reset_updates(req, res) { if (jenkinsURL) { // Temporary block of this button in Teams since it has been abused. // Will be replaced with a better button or deprecated. res.status(401).send("Unauthorized. Contact jani.heikkinen@qt.io to perform a reset."); return; // logger.info(`Resetting submodule update round on ${req.branch}`); // axios.post( // `${jenkinsURL}/job/qt_submodule_update_${req.branch}/buildWithParameters?RESET=true`, // undefined, { auth: jenkinsAuth } // ).then(function (response) { // res.status(200).send(`Submodule update job for ${req.branch} reset.`); // axios.post(envOrConfig("SUBMODULE_UPDATE_TEAMS_URL"), { // "Text": `INFO: Reset submodule update round on '**${req.branch}**'` // }).catch(function (error) { // logger.info("Unable to send teams message..."); // }); // // Then kick off a new round immediately // axios.post( // `${jenkinsURL}/job/qt_submodule_update_${req.branch}/buildWithParameters`, // undefined, { auth: jenkinsAuth } // ).catch(function (error) { // logger.info(`Unable to start new submodule update job for ${req.branch}.` // + `\n${error.response ? error.response.status : error}`); // }) // }).catch(function (error) { // _handleJenkinsError(req, res, error, "reset"); // }) } else { logger.info(`Unable to reset submodule update job for ${req.branch}.` + " Jenkins URL not set!") res.status(500).send("No destination URL for Jenkins set. Contact the Gerrit Admins."); } } function retry_updates(req, res) { if (jenkinsURL) { logger.info(`Running RETRY submodule update job on ${req.branch}`); axios.post( `${jenkinsURL}/job/qt_submodule_update_${req.branch}` + `/buildWithParameters?RETRY_FAILED_MODULES=true`, undefined, { auth: jenkinsAuth } ).then(function (response) { res.status(200).send(`Submodule update job for ${req.branch} started.`); axios.post(envOrConfig("SUBMODULE_UPDATE_TEAMS_URL"), { "Text": `INFO: Retrying failed submodules on '**${req.branch}**'` }).catch(function (error) { logger.info(`Unable to send teams message... ${safeJsonStringify(error)}`); }); }).catch(function (error) { handleJenkinsError(req, res, error, "retry"); }) } else { logger.info(`Unable to start submodule update job for ${req.branch}.` + " Jenkins URL not set!") res.status(500).send("No destination URL for Jenkins set. Contact the Gerrit Admins.") } } // Setup server to listen for events function runWebserver() { const app = express(); const port = envOrConfig("WEBHOOK_PORT") || 8093; app.use(bodyParser.json()); app.get('/status', (req, res) => { res.status(200).send('OK'); }); app.post('/', async (req, res) => { const event = req.body; event.fullChangeID = `${event.change.project}~${event.change.branch}~${event.change.id}`; if (event.type === 'change-integration-fail') { if (event.change.commitMessage.match( /Update (submodule|submodules|dependencies) (refs )?on/)) { handleIntegrationFailed(event); } } else if (event.type === 'change-integration-pass') { if (event.change.commitMessage.match( /Update (submodule|submodules|dependencies) (refs )?on/)) { handleIntegrationPassed(event); } } else { res.status(400).send('Event type not supported'); return; } res.status(200); }); app.post("/pause-submodule-updates", express.json(), (req, res) => { req = req.body; pause_updates(req, res); }) app.post("/resume-submodule-updates", express.json(), (req, res) => { req = req.body; resume_updates(req, res); }) app.post("/reset-submodule-updates", express.json(), (req, res) => { req = req.body; reset_updates(req, res); }) app.post("/retry-submodule-updates", express.json(), (req, res) => { req = req.body; retry_updates(req, res); }) app.listen(port, () => { logger.info(`Listening for webhooks on port ${port}`); }); } runWebserver();