Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MS Teams #112

Merged
merged 7 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ gocd.slack {
- `proxy.port` - Proxy Port
- `proxy.type` - `socks` or `http` are the only accepted values.

### Teams Configuration

To send notifications to Microsoft Teams instead of Slack you need to configure the `listener` setting as shown in the example below.
The other difference is that the channel setting is not used,
instead with Teams you create an incoming webhook for each channel you want to send messages to.

```hocon
gocd.slack {
# Tell the plugin you are using Microsoft Teams instead of Slack.
listener = "in.ashwanthkumar.gocd.teams.TeamsPipelineListener"

# Determines the Team and Channel to send notifications to unless overridden by a pipeline rule.
webhookUrl = "https://xxx.webhook.office.com/webhookb2/xxx/IncomingWebhook/xxx/xxx"

# The channel setting is not used, only the webhookUrl.

pipelines = [{
# The channel setting is ignored.

# Optionally override the default webhook to send notifications to a different channel.
webhookUrl = "https://example.com"

# The rest of the configuration functions the same.
},
}
```

## Pipeline Rules
By default the plugin pushes a note about all failed stages across all pipelines to Slack. You have fine grain control over this operation.
```hocon
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>in.ashwanthkumar</groupId>
<artifactId>gocd-slack-notifier</artifactId>
<version>2.0.2</version>
<version>2.1.0-beta</version>
<packaging>jar</packaging>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,18 @@ public List<MaterialRevision> fetchChanges(Rules rules)
server.getPipelineInstance(pipeline.name, Integer.parseInt(pipeline.counter));
return pipelineInstance.rootChanges(server);
}

public Stage pickCurrentStage(Stage[] stages) {
for (Stage stage : stages) {
if (getStageName().equals(stage.name)) {
return stage;
}
}
throw new IllegalArgumentException("The list of stages from the pipeline ("
+ getPipelineName()
+ ") doesn't have the active stage ("
+ getStageName()
+ ") for which we got the notification.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.util.List;

abstract public class PipelineListener {
private Logger LOG = Logger.getLoggerFor(PipelineListener.class);
private static final Logger LOG = Logger.getLoggerFor(PipelineListener.class);
protected Rules rules;

public PipelineListener(Rules rules) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

public class SlackPipelineListener extends PipelineListener {
public static final int DEFAULT_MAX_CHANGES_PER_MATERIAL_IN_SLACK = 5;
private Logger LOG = Logger.getLoggerFor(SlackPipelineListener.class);
private static final Logger LOG = Logger.getLoggerFor(SlackPipelineListener.class);

private final Slack slack;

Expand Down Expand Up @@ -79,7 +79,7 @@ public void onCancelled(PipelineRule rule, GoNotificationMessage message) throws
}

private SlackAttachment slackAttachment(PipelineRule rule, GoNotificationMessage message, PipelineStatus pipelineStatus) throws URISyntaxException {
String title = String.format("Stage [%s] %s %s", message.fullyQualifiedJobName(), verbFor(pipelineStatus), pipelineStatus).replaceAll("\\s+", " ");
String title = String.format("Stage [%s] %s %s", message.fullyQualifiedJobName(), pipelineStatus.verb(), pipelineStatus).replaceAll("\\s+", " ");
SlackAttachment buildAttachment = new SlackAttachment("")
.fallback(title)
.title(title, message.goServerUrl(rules.getGoServerHost()));
Expand All @@ -88,7 +88,7 @@ private SlackAttachment slackAttachment(PipelineRule rule, GoNotificationMessage
// Describe the build.
try {
Pipeline details = message.fetchDetails(rules);
Stage stage = pickCurrentStage(details.stages, message);
Stage stage = message.pickCurrentStage(details.stages);
buildAttachment.addField(new SlackAttachment.Field("Triggered by", stage.approvedBy, true));
if (details.buildCause.triggerForced) {
buildAttachment.addField(new SlackAttachment.Field("Reason", "Manual Trigger", true));
Expand Down Expand Up @@ -180,32 +180,6 @@ private List<String> createConsoleLogLinks(String host, Pipeline pipeline, Stage
return consoleLinks;
}

private Stage pickCurrentStage(Stage[] stages, GoNotificationMessage message) {
for (Stage stage : stages) {
if (message.getStageName().equals(stage.name)) {
return stage;
}
}

throw new IllegalArgumentException("The list of stages from the pipeline (" + message.getPipelineName() + ") doesn't have the active stage (" + message.getStageName() + ") for which we got the notification.");
}

private String verbFor(PipelineStatus pipelineStatus) {
switch (pipelineStatus) {
case BROKEN:
case FIXED:
case BUILDING:
return "is";
case FAILED:
case PASSED:
return "has";
case CANCELLED:
return "was";
default:
return "";
}
}

private void updateSlackChannel(String slackChannel) {
LOG.debug(String.format("Updating target slack channel to %s", slackChannel));
// by default post it to where ever the hook is configured to do so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ public void handle(PipelineListener listener, PipelineRule rule, GoNotificationM
}
};

public String verb() {
switch (this) {
case BROKEN:
case FIXED:
case BUILDING:
return "is";
case FAILED:
case PASSED:
return "has";
case CANCELLED:
return "was";
default:
return "";
}
}

public boolean matches(String state) {
return this == ALL || this == PipelineStatus.valueOf(state.toUpperCase());
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/in/ashwanthkumar/gocd/teams/CardHttpContent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package in.ashwanthkumar.gocd.teams;

import com.google.api.client.http.AbstractHttpContent;
import com.google.api.client.json.Json;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

/**
* Serialize a {@link TeamsCard} for the Google HTTP Client.
*/
public class CardHttpContent extends AbstractHttpContent {
private final TeamsCard card;

protected CardHttpContent(TeamsCard card) {
super(Json.MEDIA_TYPE);
this.card = card;
}

@Override
public void writeTo(OutputStream out) throws IOException {
try (var osw = new OutputStreamWriter(out)) {
osw.write(card.toString());
}
}
}
89 changes: 89 additions & 0 deletions src/main/java/in/ashwanthkumar/gocd/teams/MessageCardSchema.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package in.ashwanthkumar.gocd.teams;

import com.google.gson.annotations.SerializedName;
import in.ashwanthkumar.gocd.slack.ruleset.PipelineStatus;

import java.util.ArrayList;
import java.util.List;

/**
* These objects create the MessageCard JSON sent to Teams using {@link com.google.gson.Gson}.
* More details:
* https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference
*/
public class MessageCardSchema {
@SerializedName("@type")
String type = "MessageCard";
String themeColor = Color.NONE.getHexCode();
String title = "";
/**
* Not sure what this does, but a summary or text field is required.
*/
String summary = "GoCD build update";
List<FactSection> sections = new ArrayList<>();
List<Object> potentialAction = new ArrayList<>();

public enum Color {
NONE(""),
RED("990000"),
GREEN("009900");

private final String hexCode;

Color(String hexCode) {
this.hexCode = hexCode;
}

public static Color findColor(PipelineStatus status) {
switch (status) {
case PASSED:
case FIXED:
return Color.GREEN;
case FAILED:
case BROKEN:
return Color.RED;
default:
return Color.NONE;
}
}

public String getHexCode() {
return this.hexCode;
}
}

public static class Fact {
String name = "";
String value = "";

public Fact(String name, String value) {
this.name = name;
this.value = value;
}
}

public static class FactSection {
List<Fact> facts = new ArrayList<>();
}

public static class OpenUriAction {
@SerializedName("@type")
String type = "OpenUri";
String name = "";
List<Target> targets = new ArrayList<>();

public OpenUriAction(String name, String uri) {
this.name = name;
this.targets.add(new MessageCardSchema.Target(uri));
}
}

public static class Target {
String os = "default";
String uri = "";

public Target(String uri) {
this.uri = uri;
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/in/ashwanthkumar/gocd/teams/TeamsCard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package in.ashwanthkumar.gocd.teams;

import com.google.gson.Gson;

/**
* Populate the values of a Message Card for Teams.
*/
public class TeamsCard {
private final MessageCardSchema.FactSection factSection = new MessageCardSchema.FactSection();
private final MessageCardSchema schema = new MessageCardSchema();

public TeamsCard() {
this.schema.sections.add(this.factSection);
}

public void setTitle(String title) {
this.schema.title = title;
}

public void addFact(String name, String value) {
this.factSection.facts.add(new MessageCardSchema.Fact(name, value));
}

@Override
public String toString() {
return new Gson().toJson(schema);
}

public void setColor(MessageCardSchema.Color color) {
this.schema.themeColor = color.getHexCode();
}

public void addLinkAction(String name, String uri) {
this.schema.potentialAction.add(new MessageCardSchema.OpenUriAction(name, uri));
}
}
Loading