Skip to content

Commit

Permalink
feat: initial infra for slack intergration (#1131)
Browse files Browse the repository at this point in the history
Co-authored-by: Sergio Moya <[email protected]>%0ACo-authored-by: Quetzalli <[email protected]>%0ACo-authored-by: asyncapi-bot <[email protected]>
  • Loading branch information
Shurtu-gal and asyncapi-bot authored May 8, 2024
1 parent 806e31a commit 494354c
Show file tree
Hide file tree
Showing 14 changed files with 1,777 additions and 5 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/slack-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Automatic Slack Management

on:
push:
paths:
- '**/slack/**/*'
- 'MAINTAINERS.yaml'
- 'WORKING_GROUPS.yaml'

jobs:
deploy-changes-to-slack:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy changes to Slack
run: |
cd .github/workflows/slack
terraform init
terraform apply \
-var "slack_token=${{ secrets.SLACK_TOKEN }}" \
-auto-approve
- name: Check if there are any uncommitted changes
id: git-check
run: |
# Set the output should_push to true if there are uncommitted changes
if [ -n "$(git status --porcelain)" ]; then
echo "Changes detected"
echo "should_push=true" >> $GITHUB_OUTPUT
else
echo "No changes detected"
echo "should_push=false" >> $GITHUB_OUTPUT
fi
- name: Push changes to GitHub
if: steps.git-check.outputs.should_push == 'true'
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # use 4.2.4 https://github.com/peter-evans/create-pull-request/releases/tag/v4.2.4
with:
token: ${{ secrets.GH_TOKEN }}
commit-message: 'chore(slack): update slack configuration'
committer: asyncapi-bot <[email protected]>
author: asyncapi-bot <[email protected]>
title: 'ci(slack): update slack configuration'
body: 'This PR was automatically created by the Automatic Slack Management GitHub Action.'
branch: 'chore/slack-update-${{ github.run_number }}'
base: 'main'
25 changes: 25 additions & 0 deletions .github/workflows/slack/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

147 changes: 147 additions & 0 deletions .github/workflows/slack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
## Infrastructure for slack integration

This directory contains the infrastructure for Slack integration. It is used to create/manage Slack channels and groups and invite users to Slack channels. The Slack integration is implemented using the [slack-terraform-provider](https://github.com/pablovarela/terraform-provider-slack).

### Prerequisites

- [A slack App](https://api.slack.com/apps) with the following scopes under `User Token Scopes` in `OAuth & Permissions`:

Write Permissions:
- `channels:write`
- `groups:write`
- `usergroups:write`

Read Permissions:
- `channels:read`
- `groups:read`
- `usergroups:read`

> [!CAUTION]
> Try to use a bot to log into Slack to prevent any changes from being attributed to the workspace owner. This is due to using a `user token` for authentication, which does the changes on behalf of the user who created the token.
- [API Token](https://api.slack.com/apps) after installing the app in your workspace. ( `xoxp-<your-slack-token>` )

- [Terraform](https://www.terraform.io/downloads.html) installed on your local machine.

### Usage

- Create a `terraform.tfvars` file in the `slack` directory with the following content:

```hcl
slack_token = "xoxp-<your-slack-token>"
```

- Run the following commands to create the Slack resources:

```bash
terraform init
terraform apply
```

> [!TIP]
> The `terraform apply` command will create the resources better to use `terraform plan` to see the changes before applying.
### How it works

Three main resources are created using the slack integration:

- `slack_channel`: This resource creates a slack channel. The channels are defined in the [channels.yaml](./channels/channels.yaml) file. with the structure explained there.

- `slack_usergroup`: This resource creates a Slack user group. The usergroups are defined in the [usergroups.yaml](./groups/groups.yaml) file, and their structure is explained there.

> [!CAUTION]
> The user groups should be unique across the workspace (i.e., no channel, user, or user group should have the same handle). Also, in case of user groups mentioned in the yaml existing in the workspace, you have to run the following command to import it to terraform state:
> ```bash
> terraform import module.groups.slack_usergroup.<resource name>[\"<usergroup name>\"] <usergroup id>
>
> # Example
> terraform import module.groups.slack_usergroup.wg_groups[\"Developer Experience\"] <actual_group_id>
> ```
- `slack_user`: This resource invites users to the Slack workspace. The users are defined in the [users.tf](./users/users.tf) file, and their structure is explained there.
### Pitfalls
- Use of bot token of the format `xoxo-<your-slack-token>` is not supported for creating user groups.
- The user group should be unique across the workspace (i.e., no channel, user, or user group should have the same handle).
- Please [import](#importing-existing-resources) the user groups to terraform state if they already exist in the workspace, as they **cannot be deleted** in Slack 😢.
> [!IMPORTANT]
> The terraform state will overwrite any description, name, or topic change. It is better to manage the changes in the YAML files and then apply them. However, the terraform state will not affect bookmarks, pinned items, etc.
### Importing existing resources
In case you have existing resources such as channels, user groups in the workspace, you can import them to the terraform state by transforming the `json` response from the slack API. An example script can be seen below:
```javascript
const fs = require('fs');
const fetch = require('node-fetch');
const token = 'xoxp-<your-slack-token>';
const fetchResource = async (resource, url_params) => {
// convert the url_params to query string
const url = new URL(`https://slack.com/api/${resource}`);
Object.keys(url_params).forEach(key => url.searchParams.append(key, url_params[key]));
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
return data;
}
async function main() {
const channels = await fetchResource('conversations.list', { exclude_archived: true });
const usergroups = await fetchResource('usergroups.list', { include_users: true });
channels.channels.forEach(channel => {
console.log(`terraform import module.channels.slack_conversation.channels[\\"${channel.name}\\"] ${channel.id}`);
});
usergroups.usergroups.forEach(usergroup => {
console.log(`terraform import module.groups.slack_usergroup.wg_groups[\\"${usergroup.name}\\"] ${usergroup.id}`);
});
}
main();
```
### What all can be done?
#### Groups
The groups can be mentioned in the slack messages using the `@<group-name>` syntax. Addition of groups can be done by adding the group to the [groups.yaml](./groups/groups.yaml) file.
The following groups are being created currently:
- `tsc`
This group is for the Technical Steering Committee members mentioned in the [TSC_MEMBERS](../../../TSC_MEMBERS.json) file. Can be used to mention all the TSC members at once.
- `maintainers`
This group is for the all maintainers of the repository mentioned in the [MAINTAINERS](../../../MAINTAINERS.yaml) file. Can be used to mention all the maintainers at once.
- `studio`
This group consists of members actively working on the studio project.
- `coc_commitee`
This group consists of members of the Code of Conduct committee.
In addition to these groups are also being created for each working group mentioned in the [WORKING_GROUPS](../../../WORKING_GROUPS.yaml) file. Example: `@dx_wg`.
We are also having groups for maintainers of each repository mentioned in the [MAINTAINERS](../../../MAINTAINERS.yaml) file. You can mention the maintainers of a repository using `@maintainers_<repo-name>`. Example: `@maintainers_studio`.
#### Channels
Two types of channels are being created currently:
- General channels: The channels are defined in the [channels.yaml](./channels/channels.yaml) file with the structure explained there.
- Working group channels: The working group channels are created for each working group mentioned in the [WORKING_GROUPS](../../../WORKING_GROUPS.yaml) file. The channels are created with the name `wg_<working-group-name>` or custom nameas configured in the [WORKING_GROUPS](../../../WORKING_GROUPS.yaml) file.
85 changes: 85 additions & 0 deletions .github/workflows/slack/channels/channels.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
terraform {
required_providers {
slack = {
source = "pablovarela/slack"
version = "~> 1.0"
}
}
}

variable "data_sources" {
default = {
tsc_members_user_ids = []
maintainers_user_ids = []
repo_maintainers = {}
}
description = "Data sources for the slack channels from the users module"
}

locals {
channel_data = yamldecode(file("${path.module}/channels.yaml"))
channels = {
for channel in local.channel_data : channel.name => {
name = channel.name
topic = channel.topic
purpose = channel.purpose

# if permanent_members is not provided, then it wil be taken from local with the name in data sources
permanent_members = lookup(channel, "permanent_members", lookup(var.data_sources, lookup(channel, "data_source", channel.name), []))
is_private = channel.is_private
action_on_destroy = channel.action_on_destroy

# if private channel, then kick all users on update else none
action_on_update_permanent_members = channel.is_private ? "kick" : "none"
adopt_existing_channel = true
}
}
}

resource "slack_conversation" "channels" {
for_each = local.channels
name = each.value.name
topic = each.value.topic
purpose = each.value.purpose
permanent_members = each.value.permanent_members

is_private = each.value.is_private
action_on_destroy = each.value.action_on_destroy
action_on_update_permanent_members = each.value.action_on_update_permanent_members
adopt_existing_channel = each.value.adopt_existing_channel
}

locals {
working_groups_data = yamldecode(file("${path.module}/../../../../WORKING_GROUPS.yaml")).working_groups
wg_channels = {
for wg_data in local.working_groups_data : wg_data.name => {
name = lookup(lookup(lookup(wg_data, "slack", {}), "channel", {}), "handle", "wg-${replace(lower(wg_data.name), " ", "-")}")
purpose = lookup(lookup(lookup(wg_data, "slack", {}), "channel", {}), "description", lookup(wg_data, "description", ""))
topic = lookup(lookup(lookup(wg_data, "slack", {}), "channel", {}), "topic", "")

permanent_members = concat([for member in wg_data.chairpersons : member.slack], [for member in wg_data.members : member.slack])
is_private = false

action_on_destroy = "archive"
action_on_update_permanent_members = "none"
adopt_existing_channel = true
}
}
}

resource "slack_conversation" "wg_channels" {
for_each = local.wg_channels
name = each.value.name
topic = each.value.topic
purpose = each.value.purpose
permanent_members = each.value.permanent_members

is_private = each.value.is_private
action_on_destroy = each.value.action_on_destroy
action_on_update_permanent_members = each.value.action_on_update_permanent_members
adopt_existing_channel = each.value.adopt_existing_channel
}

output "wg_channels" {
value = slack_conversation.wg_channels
}
Loading

0 comments on commit 494354c

Please sign in to comment.