How I get Discord notifications for new AMD Chipset releases
Contents
Preface
There is currently no good way to get notified when AMD releases new chipset drivers.
If you care about keeping systems up to date, you usually end up doing one of two things: checking the AMD driver page yourself every once in a while, or joining the AMD Discord and hoping release announcements are still readable between the GPU driver spam and marketing posts.
Neither is great, and forwarding the AMD Discord into your own server via Channel Following doesn’t fix it either, you just get the noise one server over.
So I built a small Cloudflare Worker that checks AMD’s driver pages on a schedule and sends exactly one Discord message when a new chipset driver shows up. Nothing else.
The same setup later became the base for my ASUS ROG BIOS notifier, which is the second part of this series.
Everything in this post applies to both, the second part only covers what’s different.
Architecture
The worker does four things per check:
- Fetch chipset driver metadata from AMD
- Compare it with the last known version
- Send a Discord webhook if the version changed
- Persist the new state in Cloudflare KV
No VM, no container, no cron job sitting on a server somewhere, Cloudflare just triggers the worker on a schedule.
State lives in Workers KV, one entry per watched chipset page:
{
"productName": "AMD Chipset Drivers",
"version": "8.02.18.557",
"releaseDate": "2026-03-09",
"pageUrl": "https://www.amd.com/en/support/downloads/drivers.html/chipsets/am5/x870e.html",
"checkedAt": "2026-03-15T12:00:44.154Z"
}
Only version is used for the actual change detection, everything else is just there for the Discord message and for /state.
Newer versions of the worker also store fileSize and changelog there, both end up in the Discord message.
Watching more than one chipset page
I originally only watched the X870E page, since that’s what I run. Someone then asked me to add their Discord server too, on a different board, which is where it got interesting.
The driver itself is identical across a whole socket generation, AM5 boards all get the same package. But AMD still hosts a separate download page per chipset, X870E, X670E, B650, and so on.
If I’d just added a second webhook to the existing config, that person would get pinged with a link to my X870E page while sitting on a B650 board, correct version, wrong link.
So instead of one page and a list of webhooks, the worker now watches a list of targets, each with its own page, KV key and webhook. src/config.js:
const TARGETS = [
{
id: "x870e",
amdPageUrl: "https://www.amd.com/en/support/downloads/drivers.html/chipsets/am5/x870e.html",
productName: "AMD Chipset Drivers"
}
// {
// id: "b550e",
// amdPageUrl: "https://www.amd.com/en/support/downloads/drivers.html/chipsets/am4/b550.html",
// productName: "AMD Chipset Drivers"
// }
];
export function getConfig(env) {
return {
targets: TARGETS.map((target) => ({
amdPageUrl: target.amdPageUrl,
productName: target.productName,
kvKey: "amd-" + target.id + "-chipset-version",
discordWebhookUrls: (env["DISCORD_WEBHOOK_URL_" + target.id.toUpperCase()] || "")
.split(",")
.map((u) => u.trim())
.filter(Boolean)
})),
stateBinding: env.STATE
};
}
Adding a chipset is just adding an object to TARGETS and setting a matching secret, e.g. id: "x670e" needs DISCORD_WEBHOOK_URL_X670E in Cloudflare. Everything else, page URL, product name, KV key, is right in the file.
src/index.js runs every target independently and collects the results:
for (const target of config.targets) {
try {
results.push(await runCheckForTarget(env, target, forceNotify, manual));
} catch (error) {
results.push({
ok: false,
kvKey: target.kvKey,
error: error instanceof Error ? error.message : String(error)
});
}
}
One target failing, say AMD changed the markup on one specific page, doesn’t stop the others from being checked and notified.
Fetching the AMD driver version
AMD doesn’t expose an API for this, so the worker pulls the version straight out of the page HTML.
export async function fetchDriverInfo(config) {
const html = await fetchHtml(config.amdPageUrl);
const parsed = extractDriverInfo(html, config.productName);
if (!parsed.version) {
throw new Error("Could not find a version number on the AMD page");
}
return {
productName: config.productName,
version: parsed.version,
releaseDate: parsed.releaseDate,
fileSize: parsed.fileSize,
changelog: parsed.releaseNotesUrl ? await fetchChangelog(parsed.releaseNotesUrl) : null,
pageUrl: config.amdPageUrl
};
}
Some AMD endpoints reject requests without a User-Agent, so fetchHtml sends one:
async function fetchHtml(url) {
const response = await fetch(url, {
method: "GET",
headers: {
"user-agent": "Mozilla/5.0 amd-chipset-notifier/1.0",
"accept-language": "en-US,en;q=0.9"
}
});
if (!response.ok) {
throw new Error("Failed to load AMD page. HTTP " + response.status);
}
return await response.text();
}
The HTML gets stripped down to plain text, then a regex anchored to the product name pulls out the version:
const versionRegex = new RegExp(
escapedName +
"[\\s\\S]{0,1200}?Revision Number[\\s\\S]{0,200}?([0-9]+(?:\\.[0-9]+)+)",
"i"
);
Release date and file size work the same way, one regex each, anchored to “Release Date” and “File Size”.
The changelog lives on a separate release notes page, linked from the driver table.
The worker follows that link and pulls out the “Release Highlights” section:
async function fetchChangelog(releaseNotesUrl) {
try {
const html = await fetchHtml(releaseNotesUrl);
const text = cleanupHtmlToText(html);
const match = text.match(
/Release Highlights([\s\S]{0,1000}?)(Known Issues|Chipset Support|Package Contents|Change Details|$)/i
);
return match ? match[1].trim() || null : null;
} catch {
return null;
}
}
If the release notes are missing or can’t be parsed, it returns null instead of failing.
A new driver without a changelog is still worth a message.
State and version comparison
State handling is three small functions in src/state.js, load, save, and one that only bumps the timestamp when nothing changed. Load and save:
export async function loadStoredState(env, kvKey) {
return await env.STATE.get(kvKey, "json");
}
export async function saveCurrentState(env, kvKey, state) {
await env.STATE.put(kvKey, JSON.stringify(state));
}
Comparing versions is plain numeric segment comparison in src/utils.js, compareVersions("8.02.18.557", "8.02.18.556") returns 1 if the left side is newer.
Sending the Discord webhook
src/discord.js posts to every webhook configured for that target:
export async function sendDiscordNotification(config, data) {
const payload = buildDiscordMessage(config, data);
for (const url of config.discordWebhookUrls) {
const response = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
const body = await response.text();
throw new Error("Discord webhook error. HTTP " + response.status + " Body: " + body);
}
}
}
The message itself is a Discord embed, one field per piece of information:
function buildDiscordMessage(config, data) {
const fields = [
{ name: "Last Version", value: "`" + data.previousVersion + "`", inline: false },
{ name: "Current Version", value: "`" + data.currentVersion + "`", inline: false },
{ name: "Release date", value: "`" + (data.releaseDate || "unknown") + "`", inline: false }
];
if (data.fileSize) {
fields.push({ name: "File size", value: "`" + data.fileSize + "`", inline: false });
}
if (data.changelog) {
fields.push({ name: "Changelog", value: quoteLines(data.changelog), inline: false });
}
return {
embeds: [
{
author: { name: config.productName },
title: data.forceNotify ? "TEST NOTIFICATION" : "New AMD Chipset Driver released!",
url: config.amdPageUrl,
color: EMBED_COLOR,
fields
}
]
};
}
A few details:
- The embed title links to
config.amdPageUrl, which comes from the specific target, so the link always matches the chipset the webhook was configured for. /notify-testsends a message with the title “TEST NOTIFICATION”, so nobody mistakes a test for a real release.- Discord only quotes the first line of a multi-line blockquote, so
quoteLinesputs>in front of every line of the changelog. EMBED_COLORis AMD’s red,0xed1c24.
When a check fails
Sooner or later AMD will change their page and the regex won’t match anymore.
Without any handling, that would just show up in the worker’s logs, which nobody reads.
So a failed check sends its own Discord message, but only once per distinct error:
async function reportFailureOnce(env, target, message) {
try {
const lastError = await loadStoredError(env, target.kvKey);
if (lastError === message) {
return;
}
await sendDiscordError(target, message);
await saveStoredError(env, target.kvKey, message);
} catch {
// A broken notification path shouldn't take down the rest of the run
}
}
The last reported error is stored in KV next to the state, under <kvKey>-last-error.
If the same error happens again an hour later, nothing is sent. A page that stays broken for days shouldn’t ping every single hour.
As soon as a check succeeds again, the stored error is deleted.
That way the next failure gets reported again, even if it’s the same message as last time.
Endpoints
Three HTTP endpoints on the worker, useful for debugging without waiting for the next cron run.
| Path | Description |
|---|---|
| /run | Runs a check manually, notifies only if a version changed |
| /state | Returns the current stored state for every target |
| /notify-test | Runs a check and always sends a Discord notification |
/state now returns one entry per target instead of a single object:
{
"ok": true,
"states": [
{
"kvKey": "amd-x870e-chipset-version",
"productName": "AMD Chipset Drivers",
"state": {
"version": "8.02.18.557",
"releaseDate": "2026-03-09",
"pageUrl": "https://www.amd.com/en/support/downloads/drivers.html/chipsets/am5/x870e.html",
"checkedAt": "2026-03-16T12:00:44.154Z"
}
}
]
}
/run and /notify-test work the same way, one result object per target, with changed, notified and initialized flags for each.
Tests
The parsing is the part most likely to break, so that’s what the tests cover.
extractDriverInfo is exported from src/amd.js so it can be tested against a small fake AMD page, without any network requests.
compareVersions gets its own tests as well.
They use Node’s built-in test runner, so there’s nothing to install:
npm test
A GitHub Actions workflow runs the same tests on every push and pull request.
Running it yourself
- Add your targets to
src/config.js - Create a KV namespace in Cloudflare (Storage & databases -> Worker KV) and put its ID into
wrangler.jsonc - Connect the repository to Cloudflare (Workers & Pages -> Create -> Connect to Git), every push to
maindeploys automatically - Add one secret per target in the worker’s settings:
DISCORD_WEBHOOK_URL_<ID>, e.g.DISCORD_WEBHOOK_URL_X870E
A plain text variable can be read in the dashboard and through the API, a secret can’t.
The cron schedule is in wrangler.jsonc, 0 * * * * runs the check at the start of every hour.
Why Cloudflare Workers are a good fit here
The workload per target is tiny, one HTTP request to AMD, one KV read, maybe a KV write, maybe a Discord webhook. Running this on an actual server would be overkill for something that takes milliseconds and fires once an hour.
The whole thing is a few hundred lines of code and costs nothing to run at this scale.
Final result
The worker checks every configured chipset page hourly. If a version changes, exactly one Discord message goes to exactly the people who need it, linking to their own chipset’s download page.
No AMD Discord spam, no manual checks, no one getting sent to the wrong page.
You can find the project here: amd-chipset-notifier
Cheers.