diff --git a/tcbot-integration-tests/src/integrationTest/java/org/apache/ignite/tcbot/integration/BotLoginTriggerQueueFlowTest.java b/tcbot-integration-tests/src/integrationTest/java/org/apache/ignite/tcbot/integration/BotLoginTriggerQueueFlowTest.java index e7770b4e2..1992b8349 100644 --- a/tcbot-integration-tests/src/integrationTest/java/org/apache/ignite/tcbot/integration/BotLoginTriggerQueueFlowTest.java +++ b/tcbot-integration-tests/src/integrationTest/java/org/apache/ignite/tcbot/integration/BotLoginTriggerQueueFlowTest.java @@ -374,6 +374,48 @@ public void suiteRetriggerAppearsInPrResults() throws Exception { waitForPrResultsSuiteBuild(token, "pull/12006/head", secondSuiteId); } + /** */ + @Test + public void suiteRetriggerFallsBackWhenTeamCityRejectsBuildComments() throws Exception { + String token = env.login(); + String branch = "pull/12999/head"; + String suiteId = "IgniteTests24Java17_ComputeGrid"; + + IntegrationTestEnvironment.HttpResponse configured = request("POST", env.teamcityUrl + + "/__test__/teamcity/reject-build-comments", null, "application/json", + "{\"enabled\":true}"); + + assertEquals(configured.body, 200, configured.status); + assertTrue(configured.body, configured.body.contains("\"rejectBuildComments\": true")); + + try { + IntegrationTestEnvironment.HttpResponse trigger = request("GET", env.botUrl + + "/rest/build/triggerBuildsAsync" + + "?srvCode=apache" + + "&branchName=" + enc(branch) + + "&parentSuiteId=" + enc("IgniteTests24Java17_RunAll") + + "&suiteIdList=" + enc(suiteId) + + "&top=false" + + "&observe=false" + + "&cleanRebuild=false" + + "&processId=700000046", + "Token " + token, null, null); + + assertEquals(trigger.body, 200, trigger.status); + assertTrue(trigger.body, trigger.body.contains("Trigger process started")); + + String status = waitForProcess(700000046L, token); + + assertTrue(status, status.contains("Tests started.")); + assertTrue("Suite retrigger should create a build after retrying without TeamCity comment", + waitForTriggeredBuild(branch, suiteId) > 0); + } + finally { + request("POST", env.teamcityUrl + "/__test__/teamcity/reject-build-comments", null, "application/json", + "{\"enabled\":false}"); + } + } + /** */ @Test public void currentPageRestModelForMasterIsNotEmpty() throws Exception { diff --git a/tcbot-integration-tests/src/integrationTest/python/teamcity_emulator.py b/tcbot-integration-tests/src/integrationTest/python/teamcity_emulator.py index 95aaad19f..758f77e89 100644 --- a/tcbot-integration-tests/src/integrationTest/python/teamcity_emulator.py +++ b/tcbot-integration-tests/src/integrationTest/python/teamcity_emulator.py @@ -227,6 +227,9 @@ def do_POST(self): if parsed.path == "/__test__/teamcity/forbid-build-details": return self.forbid_build_details() + if parsed.path == "/__test__/teamcity/reject-build-comments": + return self.reject_build_comments() + if parsed.path == "/__test__/teamcity/reset": return self.reset() @@ -555,6 +558,12 @@ def trigger_build(self): return self.json(401, {"message": "Authentication required"}) payload = self.read_trigger_request() + + if self.server.reject_build_comments and payload.get("hasComment"): + return self.xml(403, '' + 'You do not have "Comment build" permission in project with ' + 'internal id: project17') + self.server.next_build += 1 build_id = str(self.server.next_build) branch = payload.get("branchName", "pull/12001/head") @@ -634,11 +643,21 @@ def complete_build(self): return self.json(200, build_json(build_id, build, self.server.server_port)) + def reject_build_comments(self): + payload = self.read_json() + self.server.reject_build_comments = payload.get("enabled", True) + + return self.json(200, { + "status": "configured", + "rejectBuildComments": self.server.reject_build_comments + }) + def reset(self): self.server.next_build = 900000 self.server.builds = initial_builds() self.server.forbidden_build_details = set() self.server.build_details_requests = {} + self.server.reject_build_comments = False return self.json(200, {"status": "reset", "service": "teamcity"}) @@ -678,7 +697,8 @@ def read_trigger_request(self): return { "buildTypeId": build_type.group(1) if build_type else RUN_ALL, - "branchName": branch.group(1) if branch else "pull/12001/head" + "branchName": branch.group(1) if branch else "pull/12001/head", + "hasComment": "\n"; String comments = " " + - Strings.nullToEmpty(freeTextComments) + ", " + - "Build triggered from Ignite TC Bot" + - " [cleanSources=" + cleanRebuild + ", cleanRebuild=" + cleanRebuild + ", top=" + queueAtTop + "]" + + XmlUtil.xmlEscapeText(buildComment(freeTextComments, cleanRebuild, queueAtTop)) + "\n"; Map props = new HashMap<>(); @@ -213,10 +211,65 @@ private File logsDir() { props.put(ITeamcity.TCBOT_TRIGGER_TIME, System.currentTimeMillis()); // + StringBuilder buildWithComment = triggerBuildRequest(branchName, buildTypeId, triggeringOptions, comments, + props); + + String url = host() + "app/rest/buildQueue"; + + try { + return sendTriggerBuildRequest(url, buildWithComment.toString(), buildTypeId, branchName, cleanRebuild, + queueAtTop, props); + } + catch (IllegalStateException e) { + if (!isMissingBuildCommentPermission(e)) + throw e; + + logger.warn("TeamCity rejected build comment while triggering buildTypeId={}, branchName={}; " + + "retrying build queue request without comment.", buildTypeId, branchName); + + StringBuilder buildWithoutComment = triggerBuildRequest(branchName, buildTypeId, triggeringOptions, null, + props); + + try { + return sendTriggerBuildRequest(url, buildWithoutComment.toString(), buildTypeId, branchName, + cleanRebuild, queueAtTop, props); + } + catch (IOException retryErr) { + throw new UncheckedIOException(retryErr); + } + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * @param freeTextComments Additional comment text. + * @param cleanRebuild Clean rebuild flag. + * @param queueAtTop Queue-at-top flag. + */ + private String buildComment(String freeTextComments, boolean cleanRebuild, boolean queueAtTop) { + return Strings.nullToEmpty(freeTextComments) + ", " + + "Build triggered from Ignite TC Bot" + + " [cleanSources=" + cleanRebuild + ", cleanRebuild=" + cleanRebuild + ", top=" + queueAtTop + "]"; + } + + /** + * @param branchName Branch name. + * @param buildTypeId Build type id. + * @param triggeringOptions Triggering options XML. + * @param comments Optional comment XML. + * @param props Build properties. + */ + private StringBuilder triggerBuildRequest(String branchName, String buildTypeId, String triggeringOptions, + @Nullable String comments, Map props) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append(" \n"); - sb.append(comments); + + if (comments != null) + sb.append(comments); + sb.append(triggeringOptions); sb.append(" \n"); @@ -228,29 +281,45 @@ private File logsDir() { sb.append(" \n"); sb.append(""); - String url = host() + "app/rest/buildQueue"; - - try { - logger.info("Triggering build: buildTypeId={}, branchName={}, cleanRebuild={}, queueAtTop={}, buildParms={}", - buildTypeId, branchName, cleanRebuild, queueAtTop, props); + return sb; + } - String body = sb.toString(); + /** + * @param url Build queue URL. + * @param body Request body. + * @param buildTypeId Build type id. + * @param branchName Branch name. + * @param cleanRebuild Clean rebuild flag. + * @param queueAtTop Queue-at-top flag. + * @param props Build properties. + */ + private Build sendTriggerBuildRequest(String url, String body, String buildTypeId, String branchName, + boolean cleanRebuild, boolean queueAtTop, Map props) throws IOException { + logger.info("Triggering build: buildTypeId={}, branchName={}, cleanRebuild={}, queueAtTop={}, buildParms={}", + buildTypeId, branchName, cleanRebuild, queueAtTop, props); - if (logger.isDebugEnabled()) - logger.debug("(TRIGGER REQUEST):\n" + body); + if (logger.isDebugEnabled()) + logger.debug("(TRIGGER REQUEST):\n" + body); - try (StringReader reader = new StringReader(HttpUtil.sendPostAsString(basicAuthTok, url, body))) { - return XmlUtil.load(Build.class, reader); - } - catch (JAXBException e) { - throw ExceptionUtil.propagateException(e); - } + try (StringReader reader = new StringReader(HttpUtil.sendPostAsString(basicAuthTok, url, body))) { + return XmlUtil.load(Build.class, reader); } - catch (IOException e) { - throw new UncheckedIOException(e); + catch (JAXBException e) { + throw ExceptionUtil.propagateException(e); } } + /** + * @param e TeamCity error. + */ + private boolean isMissingBuildCommentPermission(IllegalStateException e) { + String msg = e.getMessage(); + + return msg != null + && msg.contains("Invalid Response Code : 403") + && msg.contains("\"Comment build\" permission"); + } + /** {@inheritDoc} */ @AutoProfiling @Override public ProblemOccurrences getProblems(int buildId) {