Skip to content

Zulip Notification Bot #75

Zulip Notification Bot

Zulip Notification Bot #75

name: Zulip Notification Bot
on:
schedule:
# Runs at 9 AM IST from Monday to Friday
- cron: '30 3 * * 1-5'
workflow_dispatch:
permissions: read-all
jobs:
ib_pipelines_tracker_alerts:
name: IB Pipelines Tracker Alerts
runs-on: ubuntu-latest
environment: actions-cicd
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Latest Completed Run ID
id: get_run_id
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const workflow_id = 'ib_pipelines_check.yml';
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id,
status: 'completed',
per_page: 1
});
if (data.workflow_runs.length === 0) {
core.setFailed('No completed workflow runs found.');
} else {
const run = data.workflow_runs[0];
core.setOutput('run_id', run.id);
core.setOutput('conclusion', run.conclusion || 'unknown');
}
- name: Download Logs from Latest Run
run: |
run_id=${{ steps.get_run_id.outputs.run_id }}
auth_header="Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"
api_url="https://api.github.com/repos/${{ github.repository }}/actions/runs/$run_id/logs"
curl -H "$auth_header" -L "$api_url" -o logs.zip
- name: Unzip Logs
run: |
unzip logs.zip -d logs
- name: Extract and Process Log
id: extract_summary
shell: bash {0}
run: |
# Set the step name
step_name="Check Pipeline Status"
# Find the log file for the required step
step_log=$(find logs -type f -name "*_${step_name}.txt" -print -quit)
if [ -z "$step_log" ]; then
echo "Step log file for '$step_name' not found."
echo "Available log files:"
find logs -type f -name '*.txt'
exit 1
fi
# Extract the content starting from Summary of Pipelines
summary=$(sed -n '/Summary of Pipelines:/,$p' "$step_log")
# If summary is empty, use the entire log
if [ -z "$summary" ]; then
echo "Warning: 'Summary of Pipelines:' not found. Using entire log as summary."
summary=$(cat "$step_log")
fi
# Remove unwanted lines (timestamps and whitespaces)
summary=$(echo "$summary" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:\.]+Z\s+//')
# Get the workflow conclusion
workflow_conclusion="${{ steps.get_run_id.outputs.conclusion }}"
# Determine emoji based on status
if [ "$workflow_conclusion" = "success" ]; then
status_emoji="✅"
elif [ "$workflow_conclusion" = "failure" ]; then
status_emoji="❌"
else
status_emoji="⚠️"
fi
# Construct the pipeline run URL
pipeline_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ steps.get_run_id.outputs.run_id }}"
# Format the summary as a code block
formatted_summary=$(printf '```\n%s\n```' "$summary")
# Create the Message
message=$(cat <<EOM
Pipeline run: [View Pipeline]($pipeline_url)
Date: $(date -u +"%b %d %Y")
Status: $status_emoji **$workflow_conclusion**
$formatted_summary
EOM
)
# Save the message to summary.txt
echo "$message" > summary.txt
# Split the message into chunks of up to 9999 characters
max_length=9999 # Zulip limit is 10,000 characters
total_length=${#message}
start=0
chunk_index=0
mkdir -p chunks
while [ $start -lt $total_length ]; do
chunk="${message:$start:$max_length}"
# Save each chunk to a separate file
echo "$chunk" > "chunks/chunk_${chunk_index}.txt"
start=$((start + max_length))
chunk_index=$((chunk_index + 1))
done
# Save the number of chunks as an output
echo "chunks_count=$chunk_index" >> $GITHUB_OUTPUT
- name: Send to Zulip
if: ${{ always() }}
shell: bash
env:
ZULIP_SERVER_URL: ${{ secrets.ZULIP_SERVER_URL }}
ZULIP_BOT_EMAIL: ${{ secrets.ZULIP_BOT_EMAIL }}
ZULIP_BOT_API_KEY: ${{ secrets.ZULIP_BOT_API_KEY }}
ZULIP_STREAM_NAME: 'community images'
ZULIP_TOPIC_NAME: 'IB Pipeline Tracker 🤖'
run: |
chunks_count=${{ steps.extract_summary.outputs.chunks_count }}
for ((i=0; i<chunks_count; i++)); do
chunk_file="chunks/chunk_${i}.txt"
if [ -f "$chunk_file" ]; then
chunk=$(cat "$chunk_file")
# Send the chunk to Zulip
response=$(curl -sSf -X POST "$ZULIP_SERVER_URL/api/v1/messages" \
-u "$ZULIP_BOT_EMAIL:$ZULIP_BOT_API_KEY" \
-d type="stream" \
-d to="$ZULIP_STREAM_NAME" \
-d topic="$ZULIP_TOPIC_NAME" \
--data-urlencode content="$chunk")
# Check if the curl command was successful
if [ $? -ne 0 ]; then
echo "Failed to send message to Zulip."
echo "Response: $response"
exit 1
fi
else
echo "Chunk file $chunk_file does not exist."
exit 1
fi
done
image_creation_run_alerts:
name: Image Creation Run Alerts
runs-on: ubuntu-latest
environment: actions-cicd
steps:
- name: Get the last two runs of Image Creation Run workflow
id: get_runs
uses: actions/github-script@v7
with:
script: |
const workflowName = 'Image Creation Run';
const owner = context.repo.owner;
const repo = context.repo.repo;
// Get the workflows in the repo
const workflows = await github.rest.actions.listRepoWorkflows({
owner,
repo,
});
// Find the workflow ID for "Image Creation Run"
const workflow = workflows.data.workflows.find(
(wf) => wf.name === workflowName
);
if (!workflow) {
core.setFailed(`Workflow "${workflowName}" not found.`);
return;
}
const workflow_id = workflow.id;
const runsResponse = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id,
status: 'completed',
per_page: 10,
});
const runs = runsResponse.data.workflow_runs;
if (runs.length < 2) {
core.setFailed('Not enough completed workflow runs found.');
return;
}
// Output the run IDs, conclusions, URLs, and dates
core.setOutput('run_id1', runs[0].id.toString());
core.setOutput('run_id2', runs[1].id.toString());
core.setOutput('conclusion1', runs[0].conclusion || 'unknown');
core.setOutput('url1', runs[0].html_url);
core.setOutput('url2', runs[1].html_url);
- name: Get failed ironbank images from the last two runs
id: get_failed_jobs
uses: actions/github-script@v7
env:
RUN_ID1: ${{ steps.get_runs.outputs.run_id1 }}
RUN_ID2: ${{ steps.get_runs.outputs.run_id2 }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
// Function to get all jobs with pagination
async function getAllJobs(run_id) {
let jobs = [];
let page = 1;
let moreJobs = true;
while (moreJobs) {
const response = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run_id,
per_page: 100,
page: page,
});
if (response.data.jobs.length > 0) {
jobs = jobs.concat(response.data.jobs);
page++;
} else {
moreJobs = false;
}
}
return jobs;
}
// Function to get failed jobs ending with "-ib"
async function getFailedJobs(run_id) {
const jobs = await getAllJobs(run_id);
const failedJobs = jobs.filter((job) => {
return job.conclusion === 'failure' && job.name.endsWith('-ib');
});
return failedJobs.map((job) => job.name);
}
const run_id1 = process.env.RUN_ID1;
const run_id2 = process.env.RUN_ID2;
const failedJobs1 = await getFailedJobs(run_id1);
const failedJobs2 = await getFailedJobs(run_id2);
// Find jobs that failed in both runs
const failedInBoth = failedJobs1.filter((job) => failedJobs2.includes(job));
core.setOutput('failed_jobs', failedInBoth.join(','));
- name: Get failed non-ironbank images from the last two runs
id: get_failed_jobs_no_ib
uses: actions/github-script@v7
env:
RUN_ID1: ${{ steps.get_runs.outputs.run_id1 }}
RUN_ID2: ${{ steps.get_runs.outputs.run_id2 }}
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
// Function to get all jobs with pagination
async function getAllJobs(run_id) {
let jobs = [];
let page = 1;
let moreJobs = true;
while (moreJobs) {
const response = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run_id,
per_page: 100,
page: page,
});
if (response.data.jobs.length > 0) {
jobs = jobs.concat(response.data.jobs);
page++;
} else {
moreJobs = false;
}
}
return jobs;
}
// Function to get failed jobs NOT ending with "-ib"
async function getFailedJobs(run_id) {
const jobs = await getAllJobs(run_id);
const failedJobs = jobs.filter((job) => {
return job.conclusion === 'failure' && !job.name.endsWith('-ib');
});
return failedJobs.map((job) => job.name);
}
const run_id1 = process.env.RUN_ID1;
const run_id2 = process.env.RUN_ID2;
const failedJobs1 = await getFailedJobs(run_id1);
const failedJobs2 = await getFailedJobs(run_id2);
// Find jobs that failed in both runs
const failedInBoth = failedJobs1.filter((job) => failedJobs2.includes(job));
core.setOutput('failed_jobs', failedInBoth.join(','));
- name: Extract and Process Log
id: extract_summary
shell: bash {0}
run: |
# Get the failed jobs from outputs
failed_jobs_ib="${{ steps.get_failed_jobs.outputs.failed_jobs }}"
failed_jobs_no_ib="${{ steps.get_failed_jobs_no_ib.outputs.failed_jobs }}"
# Get the conclusions and URLs
conclusion1="${{ steps.get_runs.outputs.conclusion1 }}"
url1="${{ steps.get_runs.outputs.url1 }}"
url2="${{ steps.get_runs.outputs.url2 }}"
# Function to get status emoji
get_status_emoji() {
local conclusion="$1"
if [ "$conclusion" = "success" ]; then
echo "✅"
elif [ "$conclusion" = "failure" ]; then
echo "❌"
else
echo "⚠️"
fi
}
emoji1=$(get_status_emoji "$conclusion1")
# Initialize message
message=""
# Add run details
message+="**Date:** $(date -u +"%b %d %Y")\n"
message+="**Status:** $emoji1 $conclusion1\n\n"
message+="Pipeline Run 1: [View Run]($url1)\n"
message+="Pipeline Run 2: [View Run]($url2)\n\n"
# Process jobs with '-ib' suffix
if [[ -n "$failed_jobs_ib" ]]; then
message+="\n**Ironbank Images failing consecutively in last two runs**:\n"
count=1
IFS=',' read -ra JOBS_IB <<< "$failed_jobs_ib"
for job in "${JOBS_IB[@]}"; do
message+="$count. $job\n"
count=$((count + 1))
done
fi
# Process jobs without '-ib' suffix
if [[ -n "$failed_jobs_no_ib" ]]; then
message+="\n\n**Non-Ironbank Images failing consecutively in last two runs**:\n"
count=1
IFS=',' read -ra JOBS_NO_IB <<< "$failed_jobs_no_ib"
for job in "${JOBS_NO_IB[@]}"; do
message+="$count. $job\n"
count=$((count + 1))
done
fi
# If no failed jobs, set message accordingly
if [[ -z "$failed_jobs_ib" && -z "$failed_jobs_no_ib" ]]; then
message+="All images passed in the last two runs."
fi
# Save message to summary.txt
echo -e "$message" > summary.txt
cat summary.txt
- name: Send to Zulip
if: ${{ always() }}
shell: bash
env:
ZULIP_SERVER_URL: ${{ secrets.ZULIP_SERVER_URL }}
ZULIP_BOT_EMAIL: ${{ secrets.ZULIP_BOT_EMAIL }}
ZULIP_BOT_API_KEY: ${{ secrets.ZULIP_BOT_API_KEY }}
ZULIP_STREAM_NAME: 'community images'
ZULIP_TOPIC_NAME: 'Image Creation Run Alerts 🤖'
run: |
message=$(cat summary.txt)
# Send the message to Zulip
response=$(curl -sSf -X POST "$ZULIP_SERVER_URL/api/v1/messages" \
-u "$ZULIP_BOT_EMAIL:$ZULIP_BOT_API_KEY" \
-d type="stream" \
-d to="$ZULIP_STREAM_NAME" \
-d topic="$ZULIP_TOPIC_NAME" \
--data-urlencode content="$message")
# Check if the curl command was successful
if [ $? -ne 0 ]; then
echo "Failed to send message to Zulip."
echo "Response: $response"
exit 1
fi