Skip to content

Commit

Permalink
🚨 v2: Create v2.0.0 of howl (#15)
Browse files Browse the repository at this point in the history
**Breaking change**:
The previous input parameter `SLACK_TOKEN` and `CHANNEL`
cannot be used anymore. Use `SLACK_WEBHOOK_URL` and `SLACK_CHANNEL`
instead.

`howl` can now be used as GitHub Action with `with` inputs:

    howl-on-failure:
      steps:
        - uses: foxygoat/howl@v2
          with:
            slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
            slack-channel: C0000000000 # optional; channel ID
            slack-text: <!here> # optional; text or @-mention
            discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
            discord-text: @here # optional; text or @-mention

Discord integration has been added.

GitHub Releases are now created on every merge to `master` distributing the
`howl` script to be used with hermit.

This merges the following commits:
* howl: Rename slack-notify.sh to howl
* bash: Convert local variable names to lowercase
* howl: Use SLACK_WEBHOOK_URL as input
* howl: Rework Slack message format
* howl: Add discord integration
* action: Adapt howl for GitHub actions input
* release: Upload howl script on release
* docs: Add v2.0.0 release notes

     .github/workflows/ci-cd.yaml |  10 +--
     Makefile                     |   8 +-
     README.md                    | 122 ++++++++++++++++--------------
     action.yml                   |  17 ++++-
     docs/release-notes/v2.0.0.md |  24 ++++++
     howl                         | 142 +++++++++++++++++++++++++++++++++++
     slack-notify.sh              |  90 ----------------------
     7 files changed, 257 insertions(+), 156 deletions(-)

Pull-Request: #15
  • Loading branch information
juliaogris committed Jan 20, 2024
2 parents d830351 + c3615f4 commit 83100a2
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 156 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ jobs:
howl-on-failure:
runs-on: ubuntu-latest
needs: [ci, release]
if: always() && github.event_name == 'push' && (needs.ci.result == 'failure' || needs.release.result == 'failure')
if: always() && contains(join(needs.*.result, ','), 'failure')
steps:
- uses: foxygoat/howl@v1
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
SLACK_TEXT: <!here>
- uses: foxygoat/howl@v2
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
slack-text: <!here>
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ all: check ## Lint and check format of shell scripts
.PHONY: all

# --- Lint ---------------------------------------------------------------------
SH_FILES := howl

check: ## Lint and check format of shell scripts
shellcheck *.sh
shfmt -i 4 -d *.sh
shellcheck $(SH_FILES)
shfmt -i 4 -d $(SH_FILES)

format: ## Format shell scripts
shfmt -i 4 -w *.sh
Expand All @@ -29,7 +31,7 @@ release: nexttag ## Tag and create GitHub release
$(if $(RELNOTES),cat $(RELNOTES);) \
echo "## Changelog"; \
git log --pretty="tformat:* %h %s" --no-merges --reverse $(or $(LAST_RELEASE),@^).. ; \
} | gh release create $(NEXTTAG) --title $(NEXTTAG) --notes-file -
} | gh release create $(NEXTTAG) --title $(NEXTTAG) --notes-file - howl

nexttag:
$(eval LAST_RELEASE := $(shell $(LAST_RELEASE_CMD)))
Expand Down
122 changes: 67 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,93 @@
<a href="https://commons.wikimedia.org/wiki/File:DSC09108_-_Guyanan_Red_Howler_Monkey_(36384553204).jpg" title="Howler Monkey on Wikipedia">
<img align="right" width="100" height="150" src="howler.jpeg" alt="Howler monkey">
</a>

# Howl

Because somebody should yell at you when your `master` build breaks.

Howl is a GitHub Action. It writes a custom Slack message with an
optional `@here` or `@project-owners` to your preferred Slack channel.
`howl` is a tool that sends messages to Slack or Discord when your default
branch build fails, typically on `main` or `master`. It is designed to be
triggered on your CI (continuous integration) system when a failure occurs on
your default branch. You can use it as a GitHub Action or as a standalone
program, which can be installed with [Hermit].

[Hermit]: https://cashapp.github.io/hermit

## Slack setup

Set up a [GitHub secret] called `SLACK_WEBHOOK_URL` with your
[Slack Webhook] URL, for example

https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

[Slack Webhook]: https://api.slack.com/messaging/webhooks
[GitHub secret]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions

### Optional inputs

To use a channel other than the default channel set up in the Slack App bound
to the webhook, set the `SLACK_CHANNEL` environment variable, for example

export SLACK_CHANNEL=C0000000000

For a more customized message or @-mentions, set the `SLACK_TEXT` environment
variable, for example

export SLACK_TEXT="<!here> 🚒"

## Discord setup

<div style="text-align:center;width:100%">
<img src="howler.jpeg" alt="Howler Monkey"/>
<br/>
<sub>Howler Monkey -
<a href="https://commons.wikimedia.org/wiki/File:DSC09108_-_Guyanan_Red_Howler_Monkey_(36384553204).jpg">
Wikimedia, CC BY-SA 2.0
</a>
</sub>
</div>
Set up a [GitHub secret] called `DISCORD_WEBHOOK_URL` with your
[Discord Webhook] URL for the target discord channel, for example

## Set up Slack token
https://discord.com/api/webhooks/1000000000000000000/XXXXXXXX-xxxxxxxxxxxxxxxxx

Set up a repository or organisation secrets with your [Incoming Slack
Webhook] token. Use, for instance, `SLACK_TOKEN`. The content should
look similar to
[Discord Webhook]: https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
[GitHub secret]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions

T01A*******/B0259*****/GcXTiEux*******************
### Optional inputs

Taken from the Slack provided Incoming Webhook URL:
For a more customized message or @-mentions, set the `DISCORD_TEXT` environment
variable, for example

https://hooks.slack.com/services/T01A*******/B0259*****/GcXTiEux*******************
export DISCORD_TEXT="@here 🚒"

[Incoming Slack Webhook]: https://slack.com/intl/en-au/help/articles/115005265063-Incoming-webhooks-for-Slack
## Local Testing

## Use with GitHub Action Workflow
You can test the integration locally with

export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
export DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1000000000000000000/XXXXXXXX-xxxxxxxxxxxxxxxxx'
howl

## GitHub Action usage

Use the `foxygoat/howl@v2` in you GitHub Actions Workflow that runs on the
default branch, for example:

```yaml
name: ci
on:
push:
branches: [master]
pull_request:
branches: [main]

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
run: make
howl-on-failure:
- uses: actions/checkout@v4
run: make ci
howl-on-fail:
runs-on: ubuntu-latest
needs: [ci]
if: always && github.event_name == 'push' && needs.ci.result == 'failure'
steps:
- uses: foxygoat/howl@v1
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
SLACK_TEXT: <!here> # optional; text or @-mention project owners by slack member ID, e.g. <@U0LAN0Z89>
#CHANNEL: D01J5K3RLQJ # optional; use if different from slack webhook setup, take from channel URL
```

## Use with external CI system

```yaml
name: slack-notify
on:
# choose one of status, check_run, check_suite
status:
check_run:
types: [completed]
check_suite:
types: [completed]
jobs:
slack-notify:
runs-on: ubuntu-latest
if: always() && contains(join(needs.*.result, ','), 'failure')
steps:
- if: ${{ contains(github.event.branches.*.name, 'master') && (github.event.state == 'failure' || github.event.state == 'error')}}
uses: foxygoat/howl@v1
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
BUILD_URL: ${{ github.event.target_url }}
#SLACK_TEXT: <!here> # optional; text or @-mention project owners by slack member ID, e.g. <@U0LAN0Z89>
#CHANNEL: D01J5K3RLQJ # optional; use if different from slack webhook setup, take from channel URL
- uses: foxygoat/howl@v2
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
slack-channel: C0000000000 # optional; channel ID
slack-text: <!here> # optional; text or @-mention
discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
discord-text: @here # optional; text or @-mention
```
17 changes: 14 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
name: 'Slack Howler'
name: 'Howl'
description: 'Send slack message on failed build'
inputs:
slack-webhook-url:
description: 'Slack Webhook URL, e.g. https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
slack-text:
description: 'Prepend text to Slack message, e.g.: <!here>'
slack-channel:
description: 'Channel to send Slack message to, e.g.: C0000000000'
discord-webhook-url:
description: 'Discord Webhook URL, e.g. https://discord.com/api/webhooks/1000000000000000000/XXXXXXXX-xxxxxxxxxxxxxxxxx'
discord-text:
description: 'Prepend text to Discord message, e.g.: @here'
runs:
using: "composite"
using: 'composite'
steps:
- run: ${{ github.action_path }}/slack-notify.sh
- run: ${{ github.action_path }}/howl
shell: bash
24 changes: 24 additions & 0 deletions docs/release-notes/v2.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## API Changes and Discord Integration

**Breaking change**: the previous input parameter `SLACK_TOKEN` and `CHANNEL`
cannot be used anymore. Use `SLACK_WEBHOOK_URL` and `SLACK_CHANNEL`
instead.

`howl` can now be used as GitHub Action with `with` inputs:

```yaml
howl-on-failure:
steps:
- uses: foxygoat/howl@v2
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
slack-channel: C0000000000 # optional; channel ID
slack-text: <!here> # optional; text or @-mention
discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
discord-text: @here # optional; text or @-mention
```
Discord integration has been added.
GitHub Releases are now created on every merge to `master` distributing the
`howl` script to be used with hermit.
142 changes: 142 additions & 0 deletions howl
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env bash

set -euo pipefail
trap 'echo failed: line $LINENO: $BASH_COMMAND' ERR

# GitHub actions inputs starting with `INPUT_` take precedence over
# un-prefixed environment variables. GitHub actions inputs are usually
# written with kebab case which cannot be used in sh/bash so we're replacing
# `-` with `_`.
input() {
upper="${1^^}"
printenv "INPUT_${upper}" || printenv "${upper//-/_}" || echo
}

SLACK_WEBHOOK_URL=$(input slack-webhook-url)
SLACK_TEXT=$(input slack-text)
SLACK_CHANNEL=$(input slack-channel)
DISCORD_WEBHOOK_URL=$(input discord-webhook-url)
DISCORD_TEXT=$(input discord-text)

if [[ -z "${SLACK_WEBHOOK_URL-}" && -z "${DISCORD_WEBHOOK_URL-}" ]]; then
echo "At least one of SLACK_WEBHOOK_URL or DISCORD_WEBHOOK_URL must be set"
exit 1
fi

: "${GITHUB_SERVER_URL:=https://github.com}"
: "${GITHUB_API_URL:=https://api.github.com}"
: "${GITHUB_SHA:=$(git rev-parse HEAD)}"
: "${PICTURE_BASE_URL:=https://github.com/foxygoat/howl/raw/master}"

if [[ -z "${GITHUB_REPOSITORY-}" ]]; then
repo_url=$(git remote get-url origin)
GITHUB_REPOSITORY="${repo_url#"${GITHUB_SERVER_URL}"/}"
else
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}"
fi
repo=${GITHUB_REPOSITORY#*/}

: "${BUILD_URL:=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions${GITHUB_RUN_ID:+/runs/${GITHUB_RUN_ID}}}"
: "${GITHUB_REF:=$(git symbolic-ref HEAD)}"
branch=${GITHUB_REF#refs/heads/}
branch_url="${repo_url}/tree/${branch}"

# prop extracts property from JSON object.
# prop '{"name": "Tricia", "age": 42}' age
# 42
prop() {
jq -r ".$2" <<<"$1"
}

curl_headers=(-H "Accept: application/vnd.github.groot-preview+json")
if [[ -n "${GITHUB_TOKEN-}" ]]; then
curl_headers+=(-H "Authorization: token ${GITHUB_TOKEN}")
fi

commit_url="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}"
pr=$(curl -fsSL "${curl_headers[@]}" "${commit_url}/pulls" |
jq '.[0] | {url: .html_url, number, title, author: .user.login}' || echo '{}')
if [[ $(prop "${pr}" number) != null ]]; then
pr="${GITHUB_REPOSITORY}#$(prop "${pr}" number)"
pr_url=$(prop "${pr}" url)
pr_title=$(prop "${pr}" title)
pr_author=$(prop "${pr}" author)
else
commit=$(curl -fsSL "${curl_headers[@]}" "${commit_url}" |
jq '{url: .html_url, message: .commit.message, author: .author.login}' || echo '{}')
pr="Commit \`${GITHUB_SHA::7}\`"
if [[ $(prop "${commit}" url) != null ]]; then
pr_url=$(prop "${commit}" url)
pr_title=$(prop "${commit}" message | head -n 1)
pr_author=$(prop "${commit}" author)
else
pr_url="${repo_url}"
pr_title=$(git show -s --pretty=format:%s)
pr_author=$(git show -s --pretty=format:%an)
fi
fi

if [[ -n "${SLACK_WEBHOOK_URL-}" ]]; then
channel="${SLACK_CHANNEL:+"\"channel\": \"${SLACK_CHANNEL}\","}"
SLACK_TEXT="${SLACK_TEXT:+"\"text\": \"${SLACK_TEXT}\","}"

curl -fsSL -d @- "${SLACK_WEBHOOK_URL}" <<EOF
{
"icon_url": "${PICTURE_BASE_URL}/icon.png",
${channel}
"username": "${SLACK_USERNAME:-Howl}",
${SLACK_TEXT}
"attachments": [
{
"title": "🚨 Build Failure 🚨",
"title_link": "${BUILD_URL}",
"color": "danger",
"fallback": "Build failure on ${branch}",
"fields": [
{
"title": "PR",
"value": "<${pr_url}|${pr}>\n${pr_title} \`@${pr_author}\`"
},
{
"title": "Branch",
"value": "<${branch_url}|${repo}:${branch}>"
}
],
"footer": "<${repo_url}|${GITHUB_REPOSITORY}>",
"footer_icon": "${PICTURE_BASE_URL}/footer.png"
}
]
}
EOF
fi

if [[ -n "${DISCORD_WEBHOOK_URL-}" ]]; then
curl -fsSL -H "Content-Type:application/json" -d @- "${DISCORD_WEBHOOK_URL}" <<EOF
{
"username": "Howl",
"avatar_url": "${PICTURE_BASE_URL}/icon.png",
"content": "${DISCORD_TEXT:-}",
"embeds": [
{
"title": "🚨 Build Failure 🚨",
"url": "${BUILD_URL}",
"color": 16711680,
"fields": [
{
"name": "PR",
"value": "[${pr}](<${pr_url}>)\n${pr_title} \`@${pr_author}\`"
},
{
"name": "Branch",
"value": "[${repo}:${branch}](${branch_url})"
}
],
"footer": {
"text": "${GITHUB_REPOSITORY}",
"icon_url": "${PICTURE_BASE_URL}/footer.png"
}
}
]
}
EOF
fi
Loading

0 comments on commit 83100a2

Please sign in to comment.