Improve slackme script features

pull/85/head
Kyle Carberry 2 years ago
parent 94115c967e
commit 12a7b67d18

@ -61,3 +61,21 @@ $ slackme npm run long-build
auth_provider_id = "slack" auth_provider_id = "slack"
} }
``` ```
## Examples
### Custom Slack Message
- `$COMMAND` is replaced with the command the user executed.
- `$DURATION` is replaced with a human-readable duration the command took to execute.
```hcl
module "slackme" {
source = "https://registry.coder.com/modules/slackme"
agent_id = coder_agent.example.id
auth_provider_id = "slack"
slack_message = <<EOF
👋 Hey there from Coder! $COMMAND took $DURATION to execute!
EOF
}
```

@ -1,15 +1,14 @@
import { serve } from "bun";
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
createJSONResponse, createJSONResponse,
execContainer, execContainer,
executeScriptInContainer,
findResourceInstance, findResourceInstance,
runContainer, runContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
} from "../test"; } from "../test";
import { serve } from "bun";
describe("slackme", async () => { describe("slackme", async () => {
await runTerraformInit(import.meta.dir); await runTerraformInit(import.meta.dir);
@ -19,25 +18,6 @@ describe("slackme", async () => {
auth_provider_id: "foo", auth_provider_id: "foo",
}); });
const setupContainer = async (image = "alpine") => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
auth_provider_id: "foo",
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
return { id, instance };
};
const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
};
it("writes to path as executable", async () => { it("writes to path as executable", async () => {
const { instance, id } = await setupContainer(); const { instance, id } = await setupContainer();
await writeCoder(id, "exit 0"); await writeCoder(id, "exit 0");
@ -55,7 +35,7 @@ describe("slackme", async () => {
expect(exec.exitCode).toBe(0); expect(exec.exitCode).toBe(0);
exec = await execContainer(id, ["sh", "-c", "slackme"]); exec = await execContainer(id, ["sh", "-c", "slackme"]);
expect(exec.stdout.trim()).toStartWith( expect(exec.stdout.trim()).toStartWith(
"slackme — Send a Slack notification when a command finishes", "slackme — Send a Slack notification when a command finishes"
); );
}); });
@ -68,31 +48,122 @@ describe("slackme", async () => {
expect(exec.stdout.trim()).toEndWith("some-url"); expect(exec.stdout.trim()).toEndWith("some-url");
}); });
it("curls url when authenticated", async () => { it("default output", async () => {
let url: URL; await assertSlackMessage({
const fakeSlackHost = serve({ command: "echo test",
fetch: (req) => { durationMS: 2,
url = new URL(req.url); output: "👨‍💻 `echo test` completed in 2ms",
if (url.pathname === "/api/chat.postMessage")
return createJSONResponse({
ok: true,
});
return createJSONResponse({}, 404);
},
port: 0,
}); });
});
const { instance, id } = await setupContainer("alpine/curl"); it("formats multiline message", async () => {
await writeCoder(id, "echo 'token'"); await assertSlackMessage({
let exec = await execContainer(id, ["sh", "-c", instance.script]); command: "echo test",
expect(exec.exitCode).toBe(0); format: `this command:
exec = await execContainer(id, [ \`$COMMAND\`
"sh", executed`,
"-c", output: `this command:
`SLACK_URL="http://${fakeSlackHost.hostname}:${fakeSlackHost.port}" slackme echo test`, \`echo test\`
]); executed`,
expect(exec.stdout.trim()).toEndWith("test"); });
expect(url.pathname).toEqual("/api/chat.postMessage"); });
expect(url.searchParams.get("channel")).toEqual("token");
it("formats execution with milliseconds", async () => {
await assertSlackMessage({
command: "echo test",
format: `$COMMAND took $DURATION`,
durationMS: 150,
output: "echo test took 150ms",
});
});
it("formats execution with seconds", async () => {
await assertSlackMessage({
command: "echo test",
format: `$COMMAND took $DURATION`,
durationMS: 15000,
output: "echo test took 15.0s",
});
});
it("formats execution with minutes", async () => {
await assertSlackMessage({
command: "echo test",
format: `$COMMAND took $DURATION`,
durationMS: 120000,
output: "echo test took 2m 0.0s",
});
});
it("formats execution with hours", async () => {
await assertSlackMessage({
command: "echo test",
format: `$COMMAND took $DURATION`,
durationMS: 60000 * 60,
output: "echo test took 1hr 0m 0.0s",
});
}); });
}); });
const setupContainer = async (
image = "alpine",
vars: Record<string, string> = {}
) => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
auth_provider_id: "foo",
...vars,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
return { id, instance };
};
const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
};
const assertSlackMessage = async (opts: {
command: string;
format?: string;
durationMS?: number;
output: string;
}) => {
let url: URL;
const fakeSlackHost = serve({
fetch: (req) => {
url = new URL(req.url);
if (url.pathname === "/api/chat.postMessage")
return createJSONResponse({
ok: true,
});
return createJSONResponse({}, 404);
},
port: 0,
});
const { instance, id } = await setupContainer(
"alpine/curl",
opts.format && {
slack_message: opts.format,
}
);
await writeCoder(id, "echo 'token'");
let exec = await execContainer(id, ["sh", "-c", instance.script]);
expect(exec.exitCode).toBe(0);
exec = await execContainer(id, [
"sh",
"-c",
`DURATION_MS=${opts.durationMS || 0} SLACK_URL="http://${
fakeSlackHost.hostname
}:${fakeSlackHost.port}" slackme ${opts.command}`,
]);
expect(exec.stderr.trim()).toBe("");
expect(url.pathname).toEqual("/api/chat.postMessage");
expect(url.searchParams.get("channel")).toEqual("token");
expect(url.searchParams.get("text")).toEqual(opts.output);
};

@ -22,7 +22,7 @@ variable "auth_provider_id" {
variable "slack_message" { variable "slack_message" {
type = string type = string
description = "The message to send to Slack." description = "The message to send to Slack."
default = "Your command completed!" default = "👨‍💻 `$COMMAND` completed in $DURATION"
} }
resource "coder_script" "install_slackme" { resource "coder_script" "install_slackme" {
@ -37,7 +37,7 @@ resource "coder_script" "install_slackme" {
cat > $CODER_DIR/slackme <<INNER cat > $CODER_DIR/slackme <<INNER
${replace(templatefile("${path.module}/slackme.sh", { ${replace(templatefile("${path.module}/slackme.sh", {
PROVIDER_ID : var.auth_provider_id, PROVIDER_ID : var.auth_provider_id,
SLACK_MESSAGE : urlencode(var.slack_message), SLACK_MESSAGE : replace(var.slack_message, "`", "\\`"),
}), "$", "\\$")} }), "$", "\\$")}
INNER INNER

@ -1,7 +1,10 @@
#!/usr/bin/env sh #!/usr/bin/env sh
PROVIDER_ID=${PROVIDER_ID} PROVIDER_ID=${PROVIDER_ID}
SLACK_MESSAGE="${SLACK_MESSAGE}" SLACK_MESSAGE=$(cat << "EOF"
${SLACK_MESSAGE}
EOF
)
SLACK_URL=$${SLACK_URL:-https://slack.com} SLACK_URL=$${SLACK_URL:-https://slack.com}
usage() { usage() {
@ -13,6 +16,43 @@ Example: slackme npm run long-build
EOF EOF
} }
pretty_duration() {
local duration_ms=$1
# If the duration is less than 1 second, display in milliseconds
if [ $duration_ms -lt 1000 ]; then
echo "$${duration_ms}ms"
return
fi
# Convert the duration to seconds
local duration_sec=$((duration_ms / 1000))
local remaining_ms=$((duration_ms % 1000))
# If the duration is less than 1 minute, display in seconds (with ms)
if [ $duration_sec -lt 60 ]; then
echo "$${duration_sec}.$${remaining_ms}s"
return
fi
# Convert the duration to minutes
local duration_min=$((duration_sec / 60))
local remaining_sec=$((duration_sec % 60))
# If the duration is less than 1 hour, display in minutes and seconds
if [ $duration_min -lt 60 ]; then
echo "$${duration_min}m $${remaining_sec}.$${remaining_ms}s"
return
fi
# Convert the duration to hours
local duration_hr=$((duration_min / 60))
local remaining_min=$((duration_min % 60))
# Display in hours, minutes, and seconds
echo "$${duration_hr}hr $${remaining_min}m $${remaining_sec}.$${remaining_ms}s"
}
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
usage usage
exit 1 exit 1
@ -30,9 +70,18 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
START=$(date +%s%N)
# Run all arguments as a command # Run all arguments as a command
$@ $@
END=$(date +%s%N)
DURATION_MS=$${DURATION_MS:-$(( (END - START) / 1000000 ))}
PRETTY_DURATION=$(pretty_duration $DURATION_MS)
set -e set -e
COMMAND=$(echo $@)
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$COMMAND|$COMMAND|g")
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g")
curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \ curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \
"$SLACK_URL/api/chat.postMessage?channel=$USER_ID&text=$SLACK_MESSAGE&pretty=1" -G --data-urlencode "text=$${SLACK_MESSAGE}" \
"$SLACK_URL/api/chat.postMessage?channel=$USER_ID&pretty=1"

Loading…
Cancel
Save