Skip to content

Feat: LCFS - Investigate GitHub App/Bot for Workflow Notifications #2127 #8

Feat: LCFS - Investigate GitHub App/Bot for Workflow Notifications #2127

Feat: LCFS - Investigate GitHub App/Bot for Workflow Notifications #2127 #8

name: Teams Notification
on:
pull_request:
types: [closed, reopened, review_requested, synchronize]
pull_request_review:
types: [submitted]
push:
branches:
- "*"
workflow_run:
workflows: ["Testing pipeline", "Feat: LCFS - Investigate GitHub App/Bot for Workflow Notifications #2127"]
types: [completed]
permissions:
# Required for workflow_run events to access other workflow details
actions: read
contents: read
pull-requests: read
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Microsoft Teams Notification
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const event = context.eventName;
const payload = context.payload;
// Prepare variables based on event type
let message = "";
let color = "";
let prNumber = "";
let prTitle = "";
let userName = "";
let userAvatar = "";
let facts = [];
let reviewers = "";
// Function to extract PR info from workflow run
async function getPrInfoFromWorkflowRun(run) {
const octokit = github.rest;
try {
// Find the PR associated with this workflow run
const response = await octokit.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: run.head_sha
});
if (response.data.length > 0) {
const pr = response.data[0];
// Get PR comments if available
const commentsResponse = await octokit.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
return {
number: pr.number,
title: pr.title,
user: {
login: pr.user.login,
avatar_url: pr.user.avatar_url
},
comments: commentsResponse.data
};
}
return null;
} catch (error) {
console.log(`Error finding PR for workflow: ${error}`);
return null;
}
}
// Function to find PR associated with a push event
async function getPrInfoFromPush(ref) {
const octokit = github.rest;
try {
// Get the branch name from the ref
const branch = ref.replace('refs/heads/', '');
// Find PRs associated with this branch
const response = await octokit.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${branch}`
});
if (response.data.length > 0) {
const pr = response.data[0];
return {
number: pr.number,
title: pr.title,
html_url: pr.html_url,
user: {
login: pr.user.login,
avatar_url: pr.user.avatar_url
}
};
}
return null;
} catch (error) {
console.log(`Error finding PR for push: ${error}`);
return null;
}
}
// Set thread identifier based on PR number to group messages
let threadId = "";
if (event === 'pull_request') {
const pr = payload.pull_request;
prNumber = pr.number;
prTitle = pr.title;
userName = pr.user.login;
userAvatar = pr.user.avatar_url;
threadId = `pr-${prNumber}`;
const action = payload.action;
if (action === 'opened' || action === 'reopened') {
message = `New PR #${prNumber} opened: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "0076D7"; // blue
facts = [
{ "name": "Status", "value": action === 'opened' ? "Newly Opened" : "Reopened" },
{ "name": "Base Branch", "value": pr.base.ref },
{ "name": "Created", "value": new Date(pr.created_at).toLocaleString() }
];
} else if (action === 'closed') {
if (pr.merged) {
message = `PR #${prNumber} merged: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "2CBE4E"; // green
facts = [
{ "name": "Status", "value": "Merged" },
{ "name": "Merged by", "value": payload.sender.login },
{ "name": "Merged at", "value": new Date(pr.merged_at).toLocaleString() }
];
} else {
message = `PR #${prNumber} closed without merging: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "D73A49"; // red
facts = [
{ "name": "Status", "value": "Closed" },
{ "name": "Closed by", "value": payload.sender.login },
{ "name": "Closed at", "value": new Date(pr.closed_at).toLocaleString() }
];
}
} else if (action === 'synchronize') {
// This is triggered when new commits are pushed to the PR
message = `PR #${prNumber} updated with new commits: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "0076D7"; // blue
facts = [
{ "name": "Status", "value": "New Commits" },
{ "name": "Updated by", "value": payload.sender.login },
{ "name": "Updated at", "value": new Date().toLocaleString() }
];
} else if (action === 'review_requested') {
reviewers = payload.requested_reviewers.map(reviewer => reviewer.login).join(', ');
message = `Review requested for PR #${prNumber}: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "FBAB19"; // orange
facts = [
{ "name": "Status", "value": "Review Requested" },
{ "name": "Reviewer(s)", "value": reviewers },
{ "name": "Requested by", "value": payload.sender.login }
];
}
} else if (event === 'push') {
// Handle push events
const commits = payload.commits;
const ref = payload.ref;
const branch = ref.replace('refs/heads/', '');
userName = payload.pusher.name;
userAvatar = `https://github.com/${payload.pusher.name}.png`;
// Try to find associated PR
const prInfo = await getPrInfoFromPush(ref);
if (prInfo) {
// Push is associated with a PR
prNumber = prInfo.number;
prTitle = prInfo.title;
threadId = `pr-${prNumber}`;
// Create commit list for message
const commitList = commits.map(commit =>
`• ${commit.message.split('\n')[0]} ([${commit.id.substring(0, 7)}](${commit.url}))`
).join('\n');
message = `PR #${prNumber} received ${commits.length} new commit${commits.length > 1 ? 's' : ''}: **${prTitle}**\n\n${commitList}\n\n[View PR](${prInfo.html_url})`;
color = "0076D7"; // blue
facts = [
{ "name": "Status", "value": "New Commits" },
{ "name": "Branch", "value": branch },
{ "name": "Pushed by", "value": userName },
{ "name": "Commit count", "value": commits.length.toString() }
];
} else {
// Push is not associated with a PR (direct push to branch)
threadId = `branch-${branch}`;
// Create commit list for message
const commitList = commits.map(commit =>
`• ${commit.message.split('\n')[0]} ([${commit.id.substring(0, 7)}](${commit.url}))`
).join('\n');
message = `Branch **${branch}** received ${commits.length} new commit${commits.length > 1 ? 's' : ''}\n\n${commitList}\n\n[View changes](${payload.compare})`;
color = "0076D7"; // blue
facts = [
{ "name": "Branch", "value": branch },
{ "name": "Pushed by", "value": userName },
{ "name": "Commit count", "value": commits.length.toString() },
{ "name": "Repository", "value": `${context.repo.owner}/${context.repo.repo}` }
];
}
} else if (event === 'pull_request_review') {
const review = payload.review;
const pr = payload.pull_request;
prNumber = pr.number;
prTitle = pr.title;
userName = review.user.login;
userAvatar = review.user.avatar_url;
threadId = `pr-${prNumber}`;
let reviewStatus = "";
if (review.state === 'approved') {
message = `PR #${prNumber} approved: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "2CBE4E"; // green
reviewStatus = "Approved";
} else if (review.state === 'changes_requested') {
message = `Changes requested on PR #${prNumber}: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "FBAB19"; // orange
reviewStatus = "Changes Requested";
} else if (review.state === 'commented') {
message = `PR #${prNumber} received comments: **${prTitle}**\n[View PR](${pr.html_url})`;
color = "0076D7"; // blue
reviewStatus = "Commented";
}
facts = [
{ "name": "Review Status", "value": reviewStatus },
{ "name": "Review Comment", "value": review.body || "(No comment provided)" }
];
} else if (event === 'workflow_run') {
const run = payload.workflow_run;
// Get associated PR information
const prInfo = await getPrInfoFromWorkflowRun(run);
if (prInfo) {
prNumber = prInfo.number;
prTitle = prInfo.title;
userName = prInfo.user.login;
userAvatar = prInfo.user.avatar_url;
threadId = `pr-${prNumber}`;
// Base facts for all workflow runs
facts = [
{ "name": "Workflow", "value": run.name },
{ "name": "Branch", "value": run.head_branch },
{ "name": "Triggered by", "value": run.triggering_actor.login }
];
if (run.conclusion === 'success') {
message = `All tests passed for PR #${prNumber}: **${prTitle}**\n\n**Backend tests**: Passed ✅\n**Frontend tests**: Passed ✅\n\n[View test details](${run.html_url})`;
color = "2CBE4E"; // green
facts.push({ "name": "Test Status", "value": "All tests passed" });
} else if (run.conclusion === 'failure') {
// Try to determine which tests failed
message = `Tests failed for PR #${prNumber}: **${prTitle}**\n\nOne or more tests failed in the pipeline.\n\n[View test details](${run.html_url})`;
color = "D73A49"; // red
facts.push({ "name": "Test Status", "value": "One or more tests failed" });
} else {
message = `Workflow "${run.name}" for PR #${prNumber}: **${prTitle}** completed with status: ${run.conclusion}\n\n[View test details](${run.html_url})`;
color = "FBAB19"; // orange
facts.push({ "name": "Test Status", "value": run.conclusion });
}
// Add recent PR comments section if any exist
if (prInfo.comments && prInfo.comments.length > 0) {
// Get the last 3 comments to keep it manageable
const recentComments = prInfo.comments
.slice(-3)
.map(comment => {
return {
"name": `${comment.user.login} wrote:`,
"value": `"${comment.body.length > 100 ? comment.body.substring(0, 100) + '...' : comment.body}" [view](${comment.html_url})`
};
});
// Add comment facts
facts.push({ "name": "Recent Comments", "value": " " });
facts = facts.concat(recentComments);
}
} else {
// Fallback if we can't link to a PR
message = `Workflow "${run.name}" on branch ${run.head_branch} completed with status: ${run.conclusion}\n[View details](${run.html_url})`;
color = run.conclusion === 'success' ? "2CBE4E" : (run.conclusion === 'failure' ? "D73A49" : "FBAB19");
threadId = `workflow-${run.id}`;
userName = run.triggering_actor.login;
userAvatar = run.triggering_actor.avatar_url;
facts = [
{ "name": "Workflow", "value": run.name },
{ "name": "Branch", "value": run.head_branch },
{ "name": "Status", "value": run.conclusion },
{ "name": "Triggered by", "value": run.triggering_actor.login }
];
}
}
// Only proceed if we have a message to send
if (message) {
const webhookUrl = process.env.TEAMS_WEBHOOK_URL;
const card = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": color,
"summary": `GitHub Notification for ${prNumber ? `PR #${prNumber}` : 'Repository'}`,
"sections": [
{
"activityTitle": prNumber
? `GitHub Notification for PR #${prNumber}`
: `GitHub Notification for ${context.repo.owner}/${context.repo.repo}`,
"activitySubtitle": `Repository: ${context.repo.owner}/${context.repo.repo}`,
"activityImage": userAvatar,
"facts": facts,
"text": message,
"markdown": true
}
],
// Add correlation ID to group messages for the same PR
"replyToId": threadId,
"potentialAction": [
{
"@type": "OpenUri",
"name": "View on GitHub",
"targets": [
{
"os": "default",
"uri": event === 'workflow_run'
? payload.workflow_run.html_url
: event === 'push'
? payload.compare
: payload.pull_request?.html_url || payload.repository.html_url
}
]
}
]
};
// Send notification to Teams
const https = require('https');
const url = new URL(webhookUrl);
const options = {
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const req = https.request(options, (res) => {
console.log(`Teams notification status: ${res.statusCode}`);
res.on('data', (chunk) => {
console.log(`Response: ${chunk}`);
});
});
req.on('error', (error) => {
console.error(`Error sending Teams notification: ${error}`);
core.setFailed(`Failed to send Teams notification: ${error.message}`);
});
req.write(JSON.stringify(card));
req.end();
} else {
console.log('No notification configured for this event type or action');
}
env:
TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}