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) {