From e73ba9cc6421f62b00c2ac61db9a22c8c82d9f04 Mon Sep 17 00:00:00 2001 From: Vignesh Rao Date: Tue, 10 Sep 2024 13:25:40 -0500 Subject: [PATCH] Allow notifications via `ntfy` and `telegram` --- README.md | 56 +++++++++++++++++++++- scripts/start.sh | 118 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 165 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 97c56b3..f4dde2d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ docker compose up -d - **GIT_TOKEN** - Required for authentication to add runners. - **GIT_OWNER** - GitHub account username [OR] organization name. -- **GIT_REPOSITORY** - Repository name (required to create runners dedicated to a particular repo) +- **GIT_REPOSITORY** - Repository name _(required to create runners dedicated to a particular repo)_ - **RUNNER_GROUP** - Runner group. Uses `default` - **RUNNER_NAME** - Runner name. Uses a random instance ID. - **WORK_DIR** - Work directory. Uses `_work` @@ -31,7 +31,50 @@ docker compose up -d - **REUSE_EXISTING** - Re-use existing configuration. Defaults to `false` > `REUSE_EXISTING` can come in handy when a container restarts due to a problem, -> or when a container is deleted without gracefully shutting down. +> or when a container is terminated without gracefully shutting down. + +> [!WARNING] +Using this image **without** the env var `GIT_REPOSITORY` will create an organization level runner. +Using self-hosted runners in public repositories pose some considerable security threats. +> - [#self-hosted-runner-security] +> - [#restricting-the-use-of-self-hosted-runners] +> - [#configuring-required-approval-for-workflows-from-public-forks] +> +> **Author Note:** _Be mindful of the env vars you set when spinning up containers_ + +
+Env vars for startup notifications + +> This project supports [ntfy] and [telegram bot] for startup notifications. + +**NTFY** + +Choose ntfy setup instructions with [basic][ntfy-setup-basic] **OR** [authentication][ntfy-setup-auth] abilities + +- **NTFY_USERNAME** - Ntfy username for authentication _(if topic is protected)_ +- **NTFY_PASSWORD** - Ntfy password for authentication _(if topic is protected)_ +- **NTFY_URL** - Ntfy endpoint for notifications. +- **NTFY_TOPIC** - Topic to which the notifications have to be sent. + +**Telegram** + +Steps for telegram bot configuration + +1. Use [BotFather] to create a telegram bot token +2. Send a test message to the Telegram bot you created +3. Use the URL https://api.telegram.org/bot{token}/getUpdates to get the Chat ID + - You can also use Thread ID to send notifications to a particular thread within a chat window + +```shell +export TELEGRAM_BOT_TOKEN="your-bot-token" +export CHAT_ID=$(curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates" | jq -r '.result[0].message.chat.id') +export THREAD_ID=$(curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates" | jq -r '.result[0]|.update_id') +``` + +- **TELEGRAM_BOT_TOKEN** - Telegram Bot token +- **TELEGRAM_CHAT_ID** - Chat ID to which the notifications have to be sent. + +
## Development @@ -57,3 +100,12 @@ docker build --build-arg RUNNER_VERSION=$RUNNER_VERSION -t runner . [badges-build]: https://github.com/thevickypedia/github-runner-linux/actions/workflows/main.yml/badge.svg [links-build]: https://github.com/thevickypedia/github-runner-linux/actions/workflows/main.yml [docker-compose]: https://github.com/thevickypedia/github-runner-linux/blob/main/docker-compose.yml +[ntfy]: https://ntfy.sh/ +[telegram bot]: https://core.telegram.org/bots/api +[ntfy-setup-basic]: https://docs.ntfy.sh/install/ +[ntfy-setup-auth]: https://community.home-assistant.io/t/setting-up-private-and-secure-ntfy-messaging-for-ha-notifications/632952 +[BotFather]: https://t.me/botfather + +[#restricting-the-use-of-self-hosted-runners]: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#restricting-the-use-of-self-hosted-runners +[#self-hosted-runner-security]: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security +[#configuring-required-approval-for-workflows-from-public-forks]: https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization#configuring-required-approval-for-workflows-from-public-forks diff --git a/scripts/start.sh b/scripts/start.sh index 00248c3..40f84cd 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -11,6 +11,11 @@ architecture="" current_dir="$(dirname "$(realpath "$0")")" source "${current_dir}/detector.sh" +log() { + dt_stamp=$(date -u +"%Y-%m-%d %H:%M:%SZ") + echo "${dt_stamp}: $1" +} + instance_id() { # Use randomly generated instance IDs (AWS format) as default runner names letters=$(tr -dc '[:lower:]' < /dev/urandom | head -c 4) @@ -25,6 +30,95 @@ RUNNER_GROUP="${RUNNER_GROUP:-"default"}" WORK_DIR="${WORK_DIR:-"_work"}" LABELS="${LABELS:-"docker-node,$os_name-$architecture"}" REUSE_EXISTING="${REUSE_EXISTING:-"false"}" +NOTIFICATION_TITLE="GitHub Actions Runner - Docker Node" + +ntfy_fn() { + # Send NTFY notification + body="$1" + + if [[ -n "$NTFY_TOPIC" && -n "$NTFY_URL" ]]; then + # Remove trailing '/' if present + # https://github.com/binwiederhier/ntfy/issues/370 + NTFY_URL=${NTFY_URL%/} + response=$(curl -s -o /tmp/ntfy -w "%{http_code}" -X POST \ + -u "$NTFY_USERNAME:$NTFY_PASSWORD" \ + -H "X-Title: ${NOTIFICATION_TITLE}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data "${body}" \ + "$NTFY_URL/$NTFY_TOPIC") + status_code="${response: -3}" + if [ "$status_code" -eq 200 ]; then + log "Ntfy notification was successful" + elif [[ -f "/tmp/ntfy" ]]; then + log "Failed to send ntfy notification" + response_payload="$(cat /tmp/ntfy)" + reason=$(echo "$response_payload" | jq '.error') + # Output the extracted description or the full response if jq fails + if [ "$reason" != "null" ]; then + log "[$status_code]: $reason" + else + log "[$status_code]: $(cat /tmp/ntfy)" + fi + else + log "Failed to send ntfy notification - ${status_code}" + fi + rm -f /tmp/ntfy + else + log "Ntfy notifications is not setup" + fi +} + +telegram_fn() { + # Send Telegram notification + body="$1" + + if [[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]]; then + notification_preference=${DISABLE_TELEGRAM_NOTIFICATION:-false} + + # Base JSON payload + message=$(printf "*%s*\n\n%s" "${NOTIFICATION_TITLE}" "${body}") + payload=$(jq -n \ + --arg chat_id "$TELEGRAM_CHAT_ID" \ + --arg text "$message" \ + --arg parse_mode "markdown" \ + --arg disable_notification "$notification_preference" \ + '{ + chat_id: $chat_id, + text: $text, + parse_mode: $parse_mode, + disable_notification: $disable_notification + }') + + # Add 'message_thread_id' if TELEGRAM_THREAD_ID is available and not null + if [ -n "$TELEGRAM_THREAD_ID" ]; then + payload=$(echo "$payload" | jq --arg thread_id "$TELEGRAM_THREAD_ID" '. + {message_thread_id: $thread_id}') + fi + + response=$(curl -s -o /tmp/telegram -w "%{http_code}" -X POST \ + -H 'Content-Type: application/json' \ + -d "$payload" \ + "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage") + status_code="${response: -3}" + if [ "$status_code" -eq 200 ]; then + log "Telegram notification was successful" + elif [[ -f "/tmp/telegram" ]]; then + log "Failed to send telegram notification" + response_payload="$(cat /tmp/telegram)" + reason=$(echo "$response_payload" | jq '.description') + # Output the extracted description or the full response if jq fails + if [ "$reason" != "null" ]; then + log "[$status_code]: $reason" + else + log "[$status_code]: $(cat /tmp/telegram)" + fi + else + log "Failed to send telegram notification - ${status_code}" + fi + rm -f /tmp/telegram + else + log "Telegram notifications is not setup" + fi +} repo_level_runner() { # https://docs.github.com/en/rest/actions/self-hosted-runners#create-a-registration-token-for-a-repository @@ -64,25 +158,35 @@ org_level_runner() { if [[ "$REUSE_EXISTING" == "true" || "$REUSE_EXISTING" == "1" ]] && [[ -d "/home/docker/actions-runner" ]] && + [[ -f "/home/docker/actions-runner/.credentials" ]] && + [[ -f "/home/docker/actions-runner/.credentials_rsaparams" ]] && [[ -f "/home/docker/actions-runner/config.sh" ]] && [[ -f "/home/docker/actions-runner/run.sh" ]]; then - echo "Existing configuration found. Re-using it..." + log "Existing configuration found. Re-using it..." + reused="reusing existing configuration" + cd "/home/docker/actions-runner" || exit 1 else if [[ -n "$GIT_REPOSITORY" ]]; then - echo "Creating a repository level self-hosted runner ['${RUNNER_NAME}'] for ${GIT_REPOSITORY}" - repo_level_runner + log "Creating a repository level self-hosted runner ['${RUNNER_NAME}'] for ${GIT_REPOSITORY}" + repo_level_runner else - echo "Creating an organization level self-hosted runner '${RUNNER_NAME}'" - org_level_runner + log "Creating an organization level self-hosted runner '${RUNNER_NAME}'" + org_level_runner fi + reused="creating a new configuration" fi cleanup() { - echo "Removing runner..." - ./config.sh remove --token "${REG_TOKEN}" + log "Removing runner..." + ntfy_fn "Removing runner: '${RUNNER_NAME}'" + telegram_fn "Removing runner: '${RUNNER_NAME}'" + ./config.sh remove --token "${REG_TOKEN}" } trap 'cleanup; exit 130' INT trap 'cleanup; exit 143' TERM +ntfy_fn "Starting GitHub actions runner: '${RUNNER_NAME}' using docker node by ${reused}" & +telegram_fn "Starting GitHub actions runner: '${RUNNER_NAME}' using docker node by ${reused}" & + ./run.sh & wait $!