import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
import org.sonar.api.ce.posttask.Project;
import org.sonar.api.ce.posttask.QualityGate;
+import org.sonar.api.ce.posttask.ScannerContext;
import org.sonar.api.utils.text.JsonWriter;
import static java.util.Objects.requireNonNull;
+import static org.sonar.core.config.WebhookProperties.ANALYSIS_PROPERTY_PREFIX;
@Immutable
public class WebhookPayload {
}
writeProject(analysis, writer, analysis.getProject());
writeQualityGate(writer, analysis.getQualityGate());
+ writeAnalysisProperties(writer, analysis.getScannerContext());
writer.endObject().close();
return new WebhookPayload(analysis.getProject().getKey(), string.toString());
}
+ private static void writeAnalysisProperties(JsonWriter writer, ScannerContext scannerContext) {
+ writer.name("properties");
+ writer.beginObject();
+ scannerContext.getProperties().entrySet()
+ .stream()
+ .filter(prop -> prop.getKey().startsWith(ANALYSIS_PROPERTY_PREFIX))
+ .forEach(prop -> writer.prop(prop.getKey(), prop.getValue()));
+ writer.endObject();
+ }
+
private static void writeTask(JsonWriter writer, CeTask ceTask) {
writer.prop("taskId", ceTask.getId());
writer.prop("status", ceTask.getStatus().toString());
*/
package org.sonar.server.computation.task.projectanalysis.webhook;
+import com.google.common.collect.ImmutableMap;
import java.util.Date;
+import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.Test;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.ce.posttask.ScannerContext;
+import static java.util.Collections.emptyMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newConditionBuilder;
.setErrorThreshold("70.0")
.build(QualityGate.EvaluationStatus.WARN, "74.0"))
.build();
- PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate, emptyMap());
WebhookPayload payload = WebhookPayload.from(analysis);
assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/success.json"));
}
- @Test
- public void create_payload_for_failed_analysis() {
- CeTask ceTask = newCeTaskBuilder().setStatus(CeTask.Status.FAILED).setId("#1").build();
- PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(ceTask, null);
-
- WebhookPayload payload = WebhookPayload.from(analysis);
-
- assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
- assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/failed.json"));
- }
-
@Test
public void create_payload_with_gate_conditions_without_value() {
CeTask task = newCeTaskBuilder()
.setErrorThreshold("70.0")
.buildNoValue())
.build();
- PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate, emptyMap());
WebhookPayload payload = WebhookPayload.from(analysis);
assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/gate_condition_without_value.json"));
}
- private static PostProjectAnalysisTask.ProjectAnalysis newAnalysis(CeTask task, @Nullable QualityGate gate) {
+ @Test
+ public void create_payload_with_analysis_properties() {
+ CeTask task = newCeTaskBuilder()
+ .setStatus(CeTask.Status.SUCCESS)
+ .setId("#1")
+ .build();
+ QualityGate gate = newQualityGateBuilder()
+ .setId("G1")
+ .setName("Gate One")
+ .setStatus(QualityGate.Status.WARN)
+ .build();
+ Map<String, String> scannerProperties = ImmutableMap.of(
+ "sonar.analysis.revision", "ab45d24",
+ "sonar.analysis.buildNumber", "B123",
+ "not.prefixed.with.sonar.analysis", "should be ignored",
+ "foo", "should be ignored too"
+ );
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate, scannerProperties);
+
+ WebhookPayload payload = WebhookPayload.from(analysis);
+ assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/with_analysis_properties.json"));
+ assertThat(payload.toJson())
+ .doesNotContain("not.prefixed.with.sonar.analysis")
+ .doesNotContain("foo")
+ .doesNotContain("should be ignored");
+ }
+
+ @Test
+ public void create_payload_for_failed_analysis() {
+ CeTask ceTask = newCeTaskBuilder().setStatus(CeTask.Status.FAILED).setId("#1").build();
+ PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(ceTask, null, emptyMap());
+
+ WebhookPayload payload = WebhookPayload.from(analysis);
+
+ assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+ assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/failed.json"));
+ }
+
+ private static PostProjectAnalysisTask.ProjectAnalysis newAnalysis(CeTask task, @Nullable QualityGate gate,
+ Map<String, String> scannerProperties) {
return new PostProjectAnalysisTask.ProjectAnalysis() {
@Override
public CeTask getCeTask() {
@Override
public ScannerContext getScannerContext() {
- return newScannerContextBuilder().build();
+ return newScannerContextBuilder()
+ .addProperties(scannerProperties)
+ .build();
}
};
}
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder;
import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.DUMB_PROJECT;
public class WebhookPostTaskTest {
.setKey("P1")
.setName("Project One")
.build())
+ .withScannerContext(newScannerContextBuilder().build())
.execute();
}
}
"project": {
"key": "P1",
"name": "Project One"
+ },
+ "properties": {
}
}
"warningThreshold": "75.0"
}
]
+ },
+ "properties": {
}
}
--- /dev/null
+{
+ "taskId": "#1",
+ "status": "SUCCESS",
+ "analysedAt": "2017-07-14T04:40:00+0200",
+ "project": {
+ "key": "P1",
+ "name": "Project One"
+ },
+ "qualityGate": {
+ "name": "Gate One",
+ "status": "WARN",
+ "conditions": [
+ ]
+ },
+ "properties": {
+ "sonar.analysis.revision": "ab45d24",
+ "sonar.analysis.buildNumber": "B123"
+ }
+}
public static final String PROJECT_KEY = "sonar.webhooks.project";
public static final String NAME_FIELD = "name";
public static final String URL_FIELD = "url";
+
+ /**
+ * Prefix of the properties to be automatically exported from scanner to payload
+ */
+ public static final String ANALYSIS_PROPERTY_PREFIX = "sonar.analysis.";
+
private static final String CATEGORY = "webhooks";
private static final String DESCRIPTION = "Webhooks are used to notify external services when a project analysis is done. " +
"A HTTP POST request including a JSON payload is sent to each of the provided URLs. " +
/**
* Add a property to the scanner context. This context is available
* in Compute Engine when processing the report.
+ * <br/>
+ * The properties starting with {@code "sonar.analysis."} are included to the
+ * payload of webhooks.
*
+ * @throws IllegalArgumentException if key or value parameter is null
* @see org.sonar.api.ce.posttask.PostProjectAnalysisTask.ProjectAnalysis#getScannerContext()
* @since 6.1
*/
// prevents instantiation outside PostProjectAnalysisTaskTester
}
+ public ScannerContextBuilder addProperties(Map<String, String> map) {
+ properties.putAll(map);
+ return this;
+ }
+
public ScannerContext build() {
- return new ScannerContext() {
- @Override
- public Map<String, String> getProperties() {
- return properties;
- }
- };
+ return () -> properties;
}
}
}
package org.sonar.scanner.report;
import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
import java.util.Map;
+import javax.annotation.Nonnull;
+import org.sonar.api.config.Settings;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReportWriter;
import org.sonar.scanner.repository.ContextPropertiesCache;
import static com.google.common.collect.FluentIterable.from;
+import static org.sonar.core.config.WebhookProperties.ANALYSIS_PROPERTY_PREFIX;
public class ContextPropertiesPublisher implements ReportPublisherStep {
+
private final ContextPropertiesCache cache;
+ private final Settings settings;
- public ContextPropertiesPublisher(ContextPropertiesCache cache) {
+ public ContextPropertiesPublisher(ContextPropertiesCache cache, Settings settings) {
this.cache = cache;
+ this.settings = settings;
}
@Override
public void publish(ScannerReportWriter writer) {
- Iterable<ScannerReport.ContextProperty> it = from(cache.getAll().entrySet()).transform(new MapEntryToContextPropertyFunction());
- writer.writeContextProperties(it);
+ MapEntryToContextPropertyFunction transformer = new MapEntryToContextPropertyFunction();
+
+ // properties defined programmatically by plugins
+ Iterable<ScannerReport.ContextProperty> fromCache = from(cache.getAll().entrySet())
+ .transform(transformer);
+
+ // properties that are automatically included to report so that
+ // they can be included to webhook payloads
+ Iterable<ScannerReport.ContextProperty> fromSettings = from(settings.getProperties().entrySet())
+ .filter(e -> e.getKey().startsWith(ANALYSIS_PROPERTY_PREFIX))
+ .transform(transformer);
+
+ writer.writeContextProperties(Iterables.concat(fromCache, fromSettings));
}
private static final class MapEntryToContextPropertyFunction implements Function<Map.Entry<String, String>, ScannerReport.ContextProperty> {
private final ScannerReport.ContextProperty.Builder builder = ScannerReport.ContextProperty.newBuilder();
@Override
- public ScannerReport.ContextProperty apply(Map.Entry<String, String> input) {
+ public ScannerReport.ContextProperty apply(@Nonnull Map.Entry<String, String> input) {
return builder.clear().setKey(input.getKey()).setValue(input.getValue()).build();
}
}
*/
package org.sonar.scanner.report;
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import java.util.Map;
-import javax.annotation.Nonnull;
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.List;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
-import org.sonar.scanner.repository.ContextPropertiesCache;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.config.Settings;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.repository.ContextPropertiesCache;
+import static java.util.Collections.emptyList;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@Rule
public ExpectedException expectedException = ExpectedException.none();
- ContextPropertiesCache cache = new ContextPropertiesCache();
- ContextPropertiesPublisher underTest = new ContextPropertiesPublisher(cache);
+ private ScannerReportWriter writer = mock(ScannerReportWriter.class);
+ private ContextPropertiesCache cache = new ContextPropertiesCache();
+ private Settings settings = new MapSettings();
+ private ContextPropertiesPublisher underTest = new ContextPropertiesPublisher(cache, settings);
@Test
public void publish_writes_properties_to_report() {
cache.put("foo1", "bar1");
cache.put("foo2", "bar2");
- ScannerReportWriter writer = mock(ScannerReportWriter.class);
underTest.publish(writer);
- verify(writer).writeContextProperties(argThat(new TypeSafeMatcher<Iterable<ScannerReport.ContextProperty>>() {
- @Override
- protected boolean matchesSafely(Iterable<ScannerReport.ContextProperty> props) {
- Map<String, ScannerReport.ContextProperty> map = Maps.uniqueIndex(props, ContextPropertyToKey.INSTANCE);
- return map.size() == 2 &&
- map.get("foo1").getValue().equals("bar1") &&
- map.get("foo2").getValue().equals("bar2");
- }
-
- @Override
- public void describeTo(Description description) {
- }
- }));
+ List<ScannerReport.ContextProperty> expected = Arrays.asList(
+ newContextProperty("foo1", "bar1"),
+ newContextProperty("foo2", "bar2"));
+ expectWritten(expected);
}
@Test
public void publish_writes_no_properties_to_report() {
- ScannerReportWriter writer = mock(ScannerReportWriter.class);
underTest.publish(writer);
+ expectWritten(emptyList());
+ }
+
+ @Test
+ public void publish_settings_prefixed_with_sonar_analysis_for_webhooks() {
+ settings.setProperty("foo", "should not be exported");
+ settings.setProperty("sonar.analysis.revision", "ab45b3");
+ settings.setProperty("sonar.analysis.build.number", "B123");
+
+ underTest.publish(writer);
+
+ List<ScannerReport.ContextProperty> expected = Arrays.asList(
+ newContextProperty("sonar.analysis.revision", "ab45b3"),
+ newContextProperty("sonar.analysis.build.number", "B123"));
+ expectWritten(expected);
+ }
+
+ private void expectWritten(List<ScannerReport.ContextProperty> expected) {
verify(writer).writeContextProperties(argThat(new TypeSafeMatcher<Iterable<ScannerReport.ContextProperty>>() {
@Override
protected boolean matchesSafely(Iterable<ScannerReport.ContextProperty> props) {
- return Iterables.isEmpty(props);
+ List<ScannerReport.ContextProperty> copy = Lists.newArrayList(props);
+ copy.removeAll(expected);
+ return copy.isEmpty();
}
@Override
}));
}
- private enum ContextPropertyToKey implements Function<ScannerReport.ContextProperty, String> {
- INSTANCE;
- @Override
- public String apply(@Nonnull ScannerReport.ContextProperty input) {
- return input.getKey();
- }
+ private static ScannerReport.ContextProperty newContextProperty(String key, String value) {
+ return ScannerReport.ContextProperty.newBuilder()
+ .setKey(key)
+ .setValue(value)
+ .build();
}
}