The frustration is universal. You’ve got your heart set on that impossible-to-find item — maybe it’s a high-end graphics card, those limited-edition sneakers, or in my case, a Steam Deck OLED. The traditional approach to nabbing one is soul-crushing: obsessively refreshing pages, setting calendar reminders, and inevitably experiencing that gut-wrenching feeling when you realize you forgot to check on the exact day it came back in stock.
While hunting for a refurbished Steam Deck OLED, I remembered stumbling across a post about someone who automated bike promotion alerts using Puppeteer. Lightbulb moment! If they could automate their hunt, why couldn’t I? But the question remained — where should I run this solution? Buy a VPS? There had to be a more elegant (khem, cheap!) approach.
Enter GitHub Actions: The Free Automation Tool You’re Probably Overlooking
Most web scraping solutions force you into one of two unpleasant corners: pay for a service or manage your own server. But there’s a surprisingly powerful alternative hidden in plain sight — GitHub Actions! It’s completely free (with a generous 2000 minutes per month per user), requires zero server maintenance, and already has email notifications baked right in.
TL;DR — Here and below is the complete code that can transform your shopping slash scrapping frustrations:
on:
push:
workflow_dispatch:
schedule:
- cron: '*/60 * * * *'
jobs:
scheduled:
runs-on: ubuntu-latest
steps:
- name: Check out this repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install Puppeteer
run: npm install puppeteer
- name: Create Node.js script for Puppeteer
run: |-
cat << 'EOF' > fetchPage.js
const fs = require("fs");
const puppeteer = require("puppeteer");
(async () => {
try {
const browser = await puppeteer.launch({
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.goto(
"https://store.steampowered.com/sale/steamdeckrefurbished/",
{ waitUntil: "networkidle0" }
);
await page.focus("div.CartBtn");
const btns = await page.$$("div.CartBtn");
let result = [];
for (let t of btns) {
result.push(
await t.evaluate(
(x) =>
x.parentElement.parentElement.parentElement.parentElement
.textContent
)
);
}
const isOled512gbAvailable = result.some(
(x) => x.includes("Steam Deck 512 GB OLED") && x.includes("Add to cart")
);
console.log({ result });
if (isOled512gbAvailable) {
console.log("Oled in stock!");
const timestamp = new Date().toISOString();
fs.appendFileSync(
"in_stock.txt",
`OLED 512GB in stock at ${timestamp}\n`
);
} else {
console.log("No oled in stock.");
}
await browser.close();
} catch (error) {
const timestamp = new Date().toISOString();
fs.appendFileSync(
"error.txt",
`Error at: ${timestamp} - ${error.message}\n`
);
console.error(error);
}
})();
EOF
- name: Run Puppeteer script
run: node fetchPage.js
- name: Commit and push changes
run: |-
git config user.name "Automated"
git config user.email "actions@users.noreply.github.com"
git add in_stock.txt error.txt || true
if git diff --cached --quiet; then
echo "No changes in in_stock.txt or error.txt. Exiting."
exit 0
fi
git commit -m "Automated update at $(date +"%Y/%m/%d %H:%M")"
git push
Don’t let the code intimidate you! It might look complex at first glance, but it’s actually quite straightforward when broken down. Let’s dissect it piece by piece:
Automation Triggers
on:
push:
workflow_dispatch:
schedule:
- cron: '*/60 * * * *'
This section defines when our script springs into action:
- On every push to the repository (useful for testing)
- When manually triggered (for on-demand checks)
- Most importantly, on a schedule! The cron expression */60 * * * * tells it to run every 60 minutes like clockwork
This scheduled automation is the heartbeat of our solution — it ensures our digital scout is consistently checking for stock without your manual intervention. You could adjust the frequency, but remember that each check consumes some of your GitHub Actions minutes.
Environment Setup
jobs:
scheduled:
runs-on: ubuntu-latest
steps:
- name: Check out this repo
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install Puppeteer
run: npm install puppeteer
This section might seem like boring setup code, but it’s where the foundation gets laid. We’re using the latest Ubuntu agent, checking out our repository, installing Node, and — critically — installing Puppeteer the headless browser that will do our actual browsing and checking.
Scraping Script
const fs = require("fs");
const puppeteer = require("puppeteer");
(async () => {
try {
const browser = await puppeteer.launch({
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.goto(
"https://store.steampowered.com/sale/steamdeckrefurbished/",
{ waitUntil: "networkidle0" }
);
await page.focus("div.CartBtn");
const btns = await page.$$("div.CartBtn");
let result = [];
for (let t of btns) {
result.push(
await t.evaluate(
(x) =>
x.parentElement.parentElement.parentElement.parentElement
.textContent
)
);
}
const isOled512gbAvailable = result.some(
(x) => x.includes("Steam Deck 512 GB OLED") && x.includes("Add to cart")
);
console.log({ result });
if (isOled512gbAvailable) {
console.log("Oled in stock!");
const timestamp = new Date().toISOString();
fs.appendFileSync(
"in_stock.txt",
`OLED 512GB in stock at ${timestamp}\n`
);
} else {
console.log("No oled in stock.");
}
await browser.close();
} catch (error) {
const timestamp = new Date().toISOString();
fs.appendFileSync(
"error.txt",
`Error at: ${timestamp} - ${error.message}\n`
);
console.error(error);
}
})();
This is where the real magic happens! Let’s break down what this digital detective is doing:
- Browser Setup : We launch a specialized headless browser (no visual interface needed) with specific arguments required for GitHub’s environment.
- Site Navigation : Our script visits the Steam Deck refurbished page and patiently waits for everything to load.
- Element Detection : It identifies all cart buttons on the page.
- Content Extraction : For each button, it cleverly navigates up the HTML family tree to grab the full product description. (Yes, this is a bit of a hack — in a more robust solution, we might use more targeted selectors.)
- Availability Analysis : The script then scans through all results, checking for both “Steam Deck 512 GB OLED” and “Add to cart” text appearing together.
- Alert Logging : If the coveted item is in stock, it records the exciting news with a timestamp in in_stock.txt.
- Error Handling : If anything goes wrong, it documents the issue in error.txt for troubleshooting.
Running the Script and Recording Results
- name: Run Puppeteer script
run: node fetchPage.js
- name: Commit and push changes
run: |-
git config user.name "Automated"
git config user.email "actions@users.noreply.github.com"
git add in_stock.txt error.txt || true
if git diff --cached --quiet; then
echo "No changes in in_stock.txt or error.txt. Exiting."
exit 0
fi
git commit -m "Automated update at $(date +"%Y/%m/%d %H:%M")"
git push
These final steps execute our carefully crafted plan:
- Run our Node.js script
- Set up Git with an automated user identity
- Stage any changes to our tracking files
- Check if there were actually any changes (if not, exit quietly)
- Commit with a detailed timestamp and push the changes
Here’s the brilliant part: If our script detects the Steam Deck in stock, it modifies in_stock.txt, triggering a commit. GitHub then sends you a notification about this commit—essentially alerting you that your coveted item is available! No custom notification system needed; we're cleverly repurposing GitHub's built-in features.
Why This Approach Shines
This solution offers several compelling advantages:
- Zero Infrastructure Costs : No servers to maintain or cloud resources to pay for.
- Built-in Notification System : GitHub’s commit notifications act as your personal alert system.
- Complete History : Every check is documented in your repository history — perfect for tracking patterns.
- Easy Debugging : The error.txt file helps troubleshoot any issues, or you can run the script locally.
- Infinitely Customizable : You can easily adapt this framework to monitor virtually any website.
Ethical Considerations Before You Deploy
Before implementing this solution, a few important points to consider:
- Respect Rate Limits : Be courteous to the website’s resources. Checking every few minutes might be too aggressive and could potentially get your IP flagged.
- Check Terms of Service : Some websites explicitly prohibit scraping in their ToS. Always verify before deploying.
- Personal Use Only : This solution is designed for personal use, not for enabling scalping or mass reselling.
The Ironic Plot Twist
Did this ingenious solution help me snag my Steam Deck OLED? Not exactly. The only time the console became available was in the middle of the night, and I only saw the notification the next morning — too late!
However, this project wasn’t a total loss. I adapted this script for a completely different purpose: monitoring a Next.js website hosted with a provider notorious for unannounced outages. When the website goes down, the script commits an update to the error.log file on the release branch, triggering a page rebuild and restart. Problem solved!
The Developer’s Satisfaction
There’s something uniquely satisfying about using your programming skills to solve real-world problems. So many of our technical abilities seem confined to our professional work, rarely crossing over to enhance our personal lives. This project was a refreshing reminder that our coding skills can actually make everyday frustrations a little more manageable.
Have you leveraged GitHub Actions or other developer tools to solve real-world problems outside of work? I’d love to hear about your creative solutions in the comments!
Top comments (0)