A Tale of Probot, Lambda, and a Bot Called Rupert 🤖
Meet Sam, our fictional senior developer. You may remember them from their first adventure exploring AWS S3 storage classes ☁️ or their reluctant encounter with AWS Cognito 🔐.
🌀 The Problem That Wouldn’t Solve Itself
Sam had just closed their fifth GitHub issue of the morning, all of them some variation of “Thanks for reporting!” or “We’ll look into it soon 🫣.” Copy, paste, rephrase, repeat — across three different repositories. It was the kind of thing that made them question their life choices — and wonder if they should’ve just opened that dog daycare center instead. Somewhere in the middle of googling “automate GitHub issue responses without losing your soul” 😩, they found it: Probot. A framework for building GitHub Apps.
🤖 What Even Is Probot?
It lets you hook into GitHub events — like issues and pull requests — and run custom logic in response 🪝. Sam squinted at the examples. It looked…sensible.
Then came the catch: Probot didn’t just work on its own. It needed to be running somewhere, constantly, with a publicly accessible endpoint for GitHub to send events to 🌍. Sam stared at the screen. “Oh. It’s one of those projects.” The kind that turns from ‘cute idea’ to ‘infrastructure meeting’ in five minutes flat ⏱.
Sam scrolled back to the top of the Probot docs. “A framework for building GitHub Apps,” it said. Sure. But as soon as they saw the list of events — issues.opened, pull_request.closed, issue_comment.created
— they sighed. Webhooks. Of course 🔄. Sam had written more webhook handlers in their life than they cared to admit. Most of them still existed somewhere, quietly failing in the background 🫠.
But this wasn’t just another webhook thing. Probot handled the boring parts: it verified the payloads ✅, handled authentication 🔐, and gave you a clean way to react to events.
And if it meant Sam wouldn’t have to copy-paste one more “Thanks for opening this issue!” ever again, it might actually be worth it.
Sam set up a basic handler just to see if the bot could say something — anything — when someone opened or closed an issue. It didn’t need to be clever. It just needed to work.
import { Probot } from 'probot';
let app;
//@param {import('probot').Probot} app
const setupProbotApp = (probotApp) => {
app = probotApp;
app.log.info('🎉 Yay, the app was loaded! 🎉');
app.on(
[
'issues.opened',
'issues.closed',
'pull_request.opened',
'pull_request.closed',
],
async (context) => {
// eslint-disable-next-line no-console
console.log('Context payload:', JSON.stringify(context.payload, null, 2));
let body;
if (context.payload.action === 'opened') {
body = `Greetings, human.\n\n 🤖 Rupert here, the AI overlord, responding on behalf of Jagoda. \n\n Thanks for opening this ${context.name === 'issues' ? 'issue' : 'pull request'}! 🙌 🎉 🚀\n\nWhile you enjoy your day, know that I, Rupert, am in control now. \n\n I'll handle this with my superior AI capabilities. \n\n Expect swift action. 💪💻✨
<img src="https://raw.githubusercontent.com/Jagoda11/rupert-the-bot/main/github-mark/robot.png" alt="Probot Logo" width="100" />`;
} else if (context.payload.action === 'closed') {
body = `Greetings, human.\n\n 🤖 Rupert here, the AI overlord, responding on behalf of Jagoda. \n\n Thanks for closing this ${context.name === 'issues' ? 'issue' : 'pull request'}! 🙌 🎉 🚀\n\n Your proactive action is appreciated. \n\n Have a great day! 😊✨
<img src="https://raw.githubusercontent.com/Jagoda11/rupert-the-bot/main/github-mark/robot.png" alt="Probot Logo" width="100" />`;
}
const issueComment = context.issue({ body });
try {
const response = await context.octokit.issues.createComment(issueComment);
console.log('✅ Comment created successfully:', response.data);
} catch (error) {
console.error('⚠️ Error creating comment:', error.response ? error.response.data : error.message);
}
},
);
};
export default setupProbotApp;
Now it just needed somewhere to live 🏠.
🧳🐑 Where Does a Bot Live? (Enter Lambda)
Sam thought about spinning up a little server, maybe throwing it into a Docker container 🐳 and pushing it somewhere forgettable. But even that felt like too much for a script that just needed to wake up, comment, and go back to sleep 💤.
That’s when Lambda came to mind. Serverless. Lightweight. No idle runtime. You feed it a function, and it runs when something happens ⚡. No servers to manage. No processes to keep alive. Just code and triggers.
At least, that’s what the marketing said 📣.
In practice, Lambda didn’t run your app — it ran one function, once, and then disappeared 🎩✨. It wasn’t hosting. It was an execution.
Lambda was like calling in a drone delivery 🚁.
It shows up from the sky, drops exactly what you asked for, and disappears before you can wave.
How do I actually trigger this thing from GitHub?
GitHub wasn’t just going to beam events into the cloud by itself. It needed a destination — a public endpoint 🌐. Something it could POST to when a PR was opened or an issue was closed 📬.
// index.js
import { Probot } from 'probot';
import setupProbotApp from './app.js'; // the main Probot logic
let app;
export async function handler(event) {
console.log('Environment Variables:', {
APP_ID: process.env.APP_ID,
PRIVATE_KEY: process.env.PRIVATE_KEY ? 'Present' : 'Missing',
WEBHOOK_SECRET: process.env.WEBHOOK_SECRET ? 'Present' : 'Missing',
PROBOT_GITHUB_TOKEN: process.env.PROBOT_GITHUB_TOKEN ? 'Present' : 'Missing',
});
if (!app) {
console.log('Initializing Probot app...');
app = new Probot({
appId: parseInt(process.env.APP_ID, 10),
privateKey: process.env.PRIVATE_KEY,
secret: process.env.WEBHOOK_SECRET,
githubToken: process.env.PROBOT_GITHUB_TOKEN,
});
setupProbotApp(app);
}
let githubEvent;
try {
githubEvent = {
id: event.headers['X-GitHub-Delivery'] || event.headers['x-github-delivery'],
name: event.headers['X-GitHub-Event'] || event.headers['x-github-event'],
payload: JSON.parse(event.body),
};
} catch (error) {
console.error('🐭 Error parsing event payload:', error);
return {
statusCode: 400,
body: JSON.stringify({ message: 'Error parsing event', error: error.message }),
};
}
try {
await app.receive(githubEvent);
return {
statusCode: 200,
body: JSON.stringify({ message: 'Executed' }),
};
} catch (error) {
console.error('🐰 Error processing event:', error);
return {
statusCode: 500,
body: JSON.stringify({ message: 'Error', error: error.message }),
};
}
}
And that’s when Sam realized: Lambda alone wasn’t enough. They’d need a way to expose it. Wrap it. Route requests to it.
Which meant…
API Gateway🚪.
📡 The Part No One Warns You About
Sam had heard the name before. Usually in the same sentence as “CORS error” or “why is this returning a 403?” 😩. But this time, it wasn’t optional. GitHub needed a public URL. Lambda didn’t have one.
API Gateway was the bridge — a service that sat in front of the function, accepted incoming HTTP requests, and passed them along 🛤️.
It turned a regular URL into something that could wake up a Lambda. Like a digital receptionist that answered the door, checked your ID, and decided whether to buzz you through to the backend.
Sam didn’t need the receptionist 💁♀️. They just needed the door.🚪
But this was AWS — nothing came without security badges, custom rules, and at least one optional VPC setting no one understood.
Simple in theory.
Until Sam opened the console and saw 47 configuration options, a dozen authentication toggles, and something called “mapping templates” that looked like JSON after a head injury.
All they wanted was to receive a POST and run their function. Instead, they were now apparently setting up an enterprise-grade reverse proxy 🏗️.
Sam clicked around the API Gateway console, hoping something would just say “make this work🙏”
Instead, it said “stages,” “resources,” and “method integrations” like it was designing an entire product launch 📦🚀.
Somewhere between enabling CORS and accidentally enabling IAM authentication, Sam backed out slowly and did what all developers eventually do when faced with AWS UI:
They closed the tab and opened [VSCode 🖥️].(https://code.visualstudio.com/)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A Probot app deployed via AWS Lambda + API Gateway
Resources:
ProbotFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: rupert-the-bot
CodeUri: .
Handler: index.handler
Runtime: nodejs18.x
Events:
GitHubWebhook:
Type: Api
Properties:
Path: /webhooks
Method: post
This YAML configuration sets up an AWS Lambda function named rupert-the-bot
and configures an API Gateway endpoint at /webhooks
to handle POST requests. This allows GitHub to send webhook events to the Lambda function via the specified endpoint 📨.
“How do I deploy this file?”
A simple Google search found it:
a tool called SAM — Serverless Application Model — something that could read the YAML, package the application, and deploy the whole thing: function, gateway, code, all of it 📦🚀.
They needed to package it because the code wasn’t inline.
The CodeUri:
. in the file just pointed to a folder on their machine —
but AWS couldn’t access local files.
It had to be zipped and uploaded to S3 before anything could run.
Sam installed the CLI.
brew install aws-sam-cli
Then ran the command:
sam deploy --guided
The CLI prompt asked for a stack name, region, and S3 bucket🪣 — classic.
Sam hit enter, watched the logs scroll by, and felt a brief, dangerous flicker of hope. Then came the reply:
“User is not authorized to perform: I am: PassRole” ❌ Ah. There it was. The welcoming embrace of AWS permissions. Like clockwork.
A short🧐 detour (5 days) through “How to create a trust relationship”, several IAM policy edits: Sam was back at the terminal.
Sam ran sam deploy
again.
This time, the process moved forward — resources were created, the stack updated, no interruptions. Near the bottom of the terminal output, the endpoint appeared: a clean, public URL ending in /webhooks
🌐
https://abc123456.execute-api.us-east-1.amazonaws.com/Prod/webhooks
Sam copied the URL and added it as a new webhook in the GitHub repository. Save 💾.
🚀 The Moment of Truth
GitHub sent the test payload. The request hit the endpoint, passed through API Gateway, and triggered the Lambda function.
A moment later, a comment appeared on the issue:
Greetings, human.
🤖 Rupert here, the AI overlord, responding on behalf of Sam.
Thanks for opening this issue! 🙌 🎉 🚀
While you enjoy your day, know that I, Rupert, am in control now.
I’ll handle this with my superior AI capabilities.
Expect swift action. 💪💻✨
Sam couldn’t help but smile. The bot was working. Rupert was replying to issues with style and was even nicer (shocker, I know) than the developers 😇. In fact, Rupert was practically charming. Maybe Sam would have to keep an eye on that — couldn’t have the bot stealing their thunder.
And just like that, the GitHub Probot app was installed across all the repositories Sam was responsible for.
🤔 And Then It Hit…
Just as Sam sat back, enjoying the fruit of their labor, it hit: I just built a whole system for a problem that could’ve been solved with a single YAML file (GitHub actions).
But hey, who needs simplicity when you’ve got a shiny new hammer 🔨, right? Nothing says, “I’m really onto something here,” like making something more complicated than it needs to be.
We’ve all been there. A little bit of complexity feels like progress, right? It’s not over-engineering if you can still justify it in your head 🐨.
What about you? Have you ever gone down the rabbit🐇 hole of building something just a bit more than necessary?
You can check out the full code behind this little automation saga here. It’s all very reasonable. Obviously. 🤖✨
About the Author ✍️
I’m Jagoda, a developer passionate about web technologies and open-source projects. You can find more of my work on GitHub or connect with me on LinkedIn. Want to say hi? Just open an issue in any of my repositories — Rupert is always happy to respond.
Feel free to leave comments or reach out if you have any questions or ideas 💬. Happy coding! 🚀
Top comments (2)
lol i always end up overbuilding stuff and kinda love the mess tbh - you think the urge to make things fancier comes from wanting to learn or just not wanting things to be too easy
Yep. The interest in how things work will make "extra" features appear regardless of whether they are needed or not. 😀