Skip to content

Commit

Permalink
Add Action to post message to slack
Browse files Browse the repository at this point in the history
The behavior of the action is similar to the existing GitHub actions,
and largely a cookie-cutter clone. (Some common code could be extracted
into a base class, but I resisted making unnecessary changes.)

Essentially, it accepts a Slack service token, a Slack channel ID, and
formatter, and posts the formatted message to the designated Slack
channel.

Creating a `slack-channel-message` action requires two "config"
parameters and one "secrets" parameter:

1 The "channel" conf value
2 The Horreum "formatter" name
3 The "token" secret

The Action will always post a "mrkdwn" Slack message using the markdown
document generated by the Horreum formatter template. It will post using
the name of the Slack app (e.g., "Horreum Server").

Note that there's some incompatibility between GitHub and Slack markdown
syntax, specifically for links. Existing Horreum markdown templates for
GitHub use the `[text](url)` syntax whereas Slack uses `<url|text>`.
This suggests that Horreum may need to extend the templating classes to
conditionalize link formatting based on the action context.

Resolves issue #838
  • Loading branch information
dbutenhof committed Apr 17, 2024
1 parent d98dae1 commit 47391c9
Show file tree
Hide file tree
Showing 34 changed files with 899 additions and 83 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ release.properties

# testing
/horreum-web/coverage
/horreum-integration-tests/dependency-reduced-pom.xml
/horreum-integration-tests/test.log

# production
/horreum-web/build
Expand All @@ -70,4 +72,4 @@ react-app-env.d.ts
#docs
docs/site/.hugo_build.lock
docs/site/public/
docs/site/resources/
docs/site/resources/
68 changes: 68 additions & 0 deletions docs/site/content/en/docs/Integrations/slack/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Slack
date: 2024-02-22
description: Using Actions to post updates to Slack
categories: [Integration]
weight: 1
---

Horreum can be configured to post messages to a Slack channel through the
`slack-channel-message` action. Actions can be triggered on various Horreum
lifecycle events: see [Configure Actions](../../Tasks/configure-actions/index.md)
for details.

## Configuration

Before you can configure a Slack action, either globally or for a specific
test, you'll need to create and install a "Slack application" as a bot allowed
to post messages to a specific Slack channel.

1. Create a Slack App. You can do this through the
[Slack development CLI](https://api.slack.com/automation/cli/install), or
by clicking "Create New App" on the web interface at
[Slack Apps](https://api.slack.com/apps):

![Create Slack App](slack_app_create.png)

2. Open the "OAuth & Permissions" tab and scroll down to the "Scopes" header.
Add your scopes by clicking on the "Add an OAuth Scope" button. The Horreum
Slack Action requires the `channels:join` and `chat:write` scopes:

![Set App Scopes](slack_app_scopes.png)

3. Once you've created the app, you'll need to install it in your Slack workspace.
Depending on the workspace, you may need to request approval from a workspace
administrator:

![Install Slack App](slack_app_install.png)

4. Once installation is complete, Slack will give you a "BotUser OAuth Token"; a
long string that should start with "xoxb-". This token identifies and authenticates
your app (in this case, the Horreum server) to Slack, and you'll need to provide
this value to Horreum as the `token` secret when configuring an Action.

![Find App OAuth Token](slack_app_token.png)

5. Now you need to give your app permission to post to your chosen Slack channel
by using your token to "join" the channel. In the Slack UI (either the web UI or
in the Slack app), you can right-click on the channel and choose "View channel
details". At the bottom of the sheet that opens you should see an alphanumeric
string labeled "Channel ID", with a "copy to clipboard" icon following the value.

![Find Slack Channel ID](slack_channel_id.png)

Navigate to the online [conversations.join Test API](https://api.slack.com/methods/conversations.join/test)
and enter the OAuth bot token and the Channel ID in the input boxes, and click "Test method".
You should get a success API response, and you (and Horreum) can now use your OAuth token to
post bot messages to the designated Slack channel ID.

![Join the Conversation](slack_conversation_join.png)

## Configuring a Horreum Slack Action

Using either the API or the UI, you can create a Slack Action globally or
for a specific Test. You will supply both the Slack channel ID and the Slack App
bot token you created earlier, along with specifying the particular event and
formatted template you want to post.

TBD
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 33 additions & 6 deletions docs/site/content/en/docs/Tasks/configure-actions/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Configure Actions
description:
description:
date: 2023-10-15
weight: 7
---
Expand All @@ -21,18 +21,15 @@ This allows remote systems or users to rely on automation that can reduce the ne
- Generic HTTP POST method request
- Github Issue Comment
- Create a new Github Issue
- Post a message to a Slack channel

As a user with the `admin` role you can see 'Administration' link in the navigation bar on the top; go there and in the [`Global Actions`](/docs/concepts/core-concepts#global-actions) tab hit the 'Add prefix' button:

{{% imgproc prefix Fit "1200x300" %}}
Define action prefix
{{% /imgproc %}}

When you save the modal you will see the prefix appearing in the table. Then in the lower half of the screen you can add global actions: whenever a new Test is created, a [`Run`](/docs/concepts/core-concepts#run) is uploaded or Change is emitted Horreum can issue a HTTP POST request to this URL, using the new JSON-encoded entity as a request body. You can also use a JSON path[^1] wrapped in `${...}`, e.g. `${$.data.foo}` in the URL to refer to the object sent.

{{% imgproc globalhook Fit "1200x300" %}}
Define action prefix
{{% /imgproc %}}
When you save the modal you will see the prefix appearing in the table. Then in the lower half of the screen you can add global actions: whenever a new Test is created, a [`Run`](/docs/concepts/core-concepts#run) is uploaded or Change is emitted Horreum can trigger an action.

Test owners can set [`Actions`](/docs/concepts/core-concepts#actions) for individual tests, too. Go to the Test configuration, 'Actions' tab and press the 'New Action' button. This works identically to the global actions.

Expand All @@ -43,3 +40,33 @@ Define test webhook
Even though non-admins (in case of global hooks) and non-owners of given test cannot see the URL it is advisable to not use any security sensitive tokens.

[^1]: In this case the JSON path is evaluated in the application, not in PostgreSQL, therefore you need to use the [Jayway JSON Path syntax](https://github.com/json-path/JsonPath) - this is a port of the original Javascript JSON Path implementation.

## HTTP Web Hook Action

Horreum can issue an HTTP POST request to a registered URL prefix, using the new JSON-encoded entity as a request body. You can also use a JSON path[^1] wrapped in `${...}`, e.g. `${$.data.foo}` in the URL to refer to the object sent.

{{% imgproc globalhook Fit "1200x300" %}}
Define action prefix
{{% /imgproc %}}

## GitHub Issue Create Action

Horreum can create a GitHub issue against a named user (or organization) repo on a "change/new" event type. Horreum creates a GitHub formatted markdown representing the event.

You supply the owner, repository, issue title, and a [GitHub token for authentication](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).

![Create Issue Dialog](githubcreateissue.png)

## GitHub Issue Comment Action

Horreum can add a comment to an existing GitHub issue on an "experiment_result/new" event, identifying the issue either by owner, repo, and issue number, or by a complete GitHub URI, and a [GitHub token for authentication](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).

![GitHub Issue Comment Dialog](githubcommentissue.png)

## Slack Channel Message Action

Horreum can post a comment to a Slack channel on a "test/new", "change/new", "experiment_result/new", or "test/new" event. Horreum creates a formatted markdown representing the event.

You supply the Slack channel ID, and a [Slack app OAuth token](../../Integrations/slack/index.md).

![Slack Message Dialog](slackmessage.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 74 additions & 6 deletions docs/site/content/en/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ paths:
description: query string to filter runs
schema:
type: string
example: $.*
- name: matchAll
in: query
description: match all Runs?
Expand Down Expand Up @@ -2388,9 +2389,17 @@ components:
config:
type: object
oneOf:
- $ref: '#/components/schemas/HttpAction'
- $ref: '#/components/schemas/GithubIssueCommentAction'
- $ref: '#/components/schemas/GithubIssueCreateAction'
- $ref: '#/components/schemas/GithubIssueCommentActionConfig'
- $ref: '#/components/schemas/GithubIssueCreateActionConfig'
- $ref: '#/components/schemas/HttpActionConfig'
- $ref: '#/components/schemas/SlackChannelMessageActionConfig'
discriminator:
propertyName: type
mapping:
github-issue-comment: '#/components/schemas/GithubIssueCommentActionConfig'
github-issue-create: '#/components/schemas/GithubIssueCreateActionConfig'
http: '#/components/schemas/HttpActionConfig'
slack-channel-message: '#/components/schemas/SlackChannelMessageActionConfig'
secrets:
type: object
allOf:
Expand Down Expand Up @@ -2418,6 +2427,14 @@ components:
type: string
type:
type: string
ActionType:
description: Type of Action
enum:
- HTTP
- GITHUB_ISSUE_COMMENT
- GITHUB_ISSUE_CREATE
- SLACK_MESSAGE
type: string
BetterOrWorse:
description: Result of running an Experiment
enum:
Expand Down Expand Up @@ -3079,34 +3096,69 @@ components:
type: object
allOf:
- $ref: '#/components/schemas/FixThresholdConfig'
GithubIssueCommentAction:
GithubIssueCommentActionConfig:
required:
- type
- issueUrl
- owner
- repo
- issue
- formatter
type: object
properties:
type:
description: Action type
type: string
issueUrl:
description: GitHub issue URL
type: string
owner:
description: GitHub repo owner
type: string
repo:
description: GitHub repo name
type: string
issue:
description: GitHub issue number
type: string
formatter:
description: Object markdown formatter
type: string
GithubIssueCreateAction:
GithubIssueCreateActionConfig:
required:
- type
- owner
- repo
- title
- formatter
type: object
properties:
type:
description: Action type
type: string
owner:
description: GitHub repo owner
type: string
repo:
description: GitHub repo name
type: string
title:
description: GitHub issue title
type: string
formatter:
description: Object markdown formatter
type: string
HttpAction:
HttpActionConfig:
required:
- type
- url
type: object
properties:
type:
description: Action type
type: string
url:
description: HTTP address
type: string
IndexedLabelValueMap:
type: object
Expand Down Expand Up @@ -3934,6 +3986,22 @@ components:
type: string
modified:
type: boolean
SlackChannelMessageActionConfig:
required:
- type
- channel
- formatter
type: object
properties:
type:
description: Action type
type: string
channel:
description: Slack channel
type: string
formatter:
description: Object markdown formatter
type: string
SortDirection:
enum:
- Ascending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,19 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.hyperfoil.tools.horreum.api.data.ActionConfig.GithubIssueCommentActionConfig;
import io.hyperfoil.tools.horreum.api.data.ActionConfig.GithubIssueCreateActionConfig;
import io.hyperfoil.tools.horreum.api.data.ActionConfig.HttpActionConfig;
import io.hyperfoil.tools.horreum.api.data.ActionConfig.SlackChannelMessageActionConfig;
import jakarta.validation.constraints.NotNull;

import org.eclipse.microprofile.openapi.annotations.media.DiscriminatorMapping;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;


public class Action {

public static class HttpAction {
public String url;
}
public static class GithubIssueCommentAction {
public String issueUrl;
public String owner;
public String repo;
public String issue;
public String formatter;
}
public static class GithubIssueCreateAction {
public String owner;
public String repo;
public String title;
public String formatter;
}
public static class Secret {
public String token;
public boolean modified;
Expand All @@ -43,14 +33,18 @@ public static class Secret {
public String type;

@NotNull
@JsonProperty( required = true )
@Schema(type = SchemaType.OBJECT,
oneOf = {
Action.HttpAction.class,
Action.GithubIssueCommentAction.class,
Action.GithubIssueCreateAction.class
}
)
@JsonProperty(required = true)
@Schema(type = SchemaType.OBJECT, discriminatorProperty = "type", discriminatorMapping = {
@DiscriminatorMapping(schema = GithubIssueCommentActionConfig.class, value = "github-issue-comment"),
@DiscriminatorMapping(schema = GithubIssueCreateActionConfig.class, value = "github-issue-create"),
@DiscriminatorMapping(schema = HttpActionConfig.class, value = "http"),
@DiscriminatorMapping(schema = SlackChannelMessageActionConfig.class, value = "slack-channel-message"),
}, oneOf = {
GithubIssueCommentActionConfig.class,
GithubIssueCreateActionConfig.class,
HttpActionConfig.class,
SlackChannelMessageActionConfig.class,
})
public ObjectNode config;

@NotNull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.hyperfoil.tools.horreum.api.data.ActionConfig;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.type.TypeReference;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

import java.util.Arrays;

@Schema(type = SchemaType.STRING, required = true, description = "Type of Action")
public enum ActionType {
HTTP("http", new TypeReference<HttpActionConfig>() {
}),
GITHUB_ISSUE_COMMENT("github-issue-comment", new TypeReference<GithubIssueCommentActionConfig>() {
}),
GITHUB_ISSUE_CREATE("github-issue-create", new TypeReference<GithubIssueCreateActionConfig>() {
}),
SLACK_MESSAGE("slack-channel-message", new TypeReference<SlackChannelMessageActionConfig>() {
});

private static final ActionType[] VALUES = values();

private final String name;
private final TypeReference<? extends BaseActionConfig> typeReference;

private <T extends BaseActionConfig> ActionType(String name, TypeReference<T> typeReference) {
this.typeReference = typeReference;
this.name = name;
}

public <T extends BaseActionConfig> TypeReference<T> getTypeReference() {
return (TypeReference<T>) typeReference;
}

@JsonCreator
public static ActionType fromString(String str) {
return Arrays.stream(VALUES).filter(v -> v.name.equals(str)).findAny()
.orElseThrow(() -> new IllegalArgumentException("Unknown action: " + str));
}
}
Loading

0 comments on commit 47391c9

Please sign in to comment.