How I wrote a Cloudflare Worker that notifies about new AMD Chipset Drivers using Discord Webhooks

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.

  1. Manually checking the AMD driver pages from time to time.
  2. Joining the AMD Discord server and trying to follow release announcements.

Neither option is great.

Manually checking the website is tedious and easy to forget. The AMD Discord server is large and noisy.

There is no dedicated channel that only announces chipset driver updates. Messages about GPU drivers, marketing announcements and community events are kinda mixed together.

Forwarding that into your own Discord server using the “Channel Following”-Feature quickly becomes useless.

Instead of a clean update feed you end up with a stream of irrelevant notifications.

I wanted a simple solution: a service that checks AMD’s driver pages periodically and sends exactly one notification when a new chipset driver appears.

That became this project: GitHub: amd-chipset-notifier

The entire system runs as a Cloudflare Worker.


Architecture

The worker performs a very small set of tasks:

  1. Fetch chipset driver metadata from AMD
  2. Compare it with the last known version
  3. Send a Discord webhook if the version changed
  4. Persist the new state in Cloudflare KV

This makes the system completely serverless. There is no VM, no container and no cron job running somewhere.

Cloudflare executes the worker on a schedule using a Cron Trigger.

The persistent state is stored in Workers KV so the worker remembers the last detected version across executions.

The stored structure currently looks like this:

{
  "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"
}

The worker only cares about the version field for change detection.

Internally this state is stored under a single KV key. Each worker run loads the state, performs a comparison and either updates it or only refreshes the timestamp.


Observability endpoints

During development I added two HTTP endpoints which expose the internal state and execution logic.

These endpoints make debugging significantly easier when running the worker in production.

The endpoints are implemented directly inside the worker entrypoint.

In src/index.js:

if (url.pathname === "/run") {
  const result = await runCheck(env, { forceNotify: false, manual: true });
  return jsonResponse(result, 200);
}

Because the endpoint calls the same internal logic used by the scheduled worker run, the behaviour is identical to a real execution.


/state

Returns the current stored driver state.

Example:

{
  "ok": true,
  "state": {
    "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-16T12:00:44.154Z"
  }
}

This endpoint reads directly from KV and returns whatever the worker considers the latest known version.

In src/index.js:

if (url.pathname === "/state") {
  const config = getConfig(env);
  validateConfig(config);
  const state = await loadStoredState(env, config.kvKey);

  return jsonResponse(
    {
      ok: true,
      state: state || null
    },
    200
  );
}
Note
This endpoint is extremely useful for verifying that parsing logic still matches AMD’s page structure after layout changes.

/run

Forces a manual check and returns a detailed execution report.

Example response:

{
  "ok": true,
  "initialized": false,
  "changed": false,
  "notified": false,
  "manual": true,
  "previous": {
    "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-16T12:00:44.154Z"
  },
  "current": {
    "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-16T12:16:01.452Z"
  }
}

The flags in this response describe exactly what happened during execution.

Field Meaning
initialized KV was empty and got initialized
changed the detected version differs from the stored version
notified a Discord webhook was sent
manual execution was triggered via API

This endpoint effectively acts as a debugging interface for the worker.


/notify-test

Runs a check and always sends a Discord notification, because /notify-run sets data.forceNotify to true.

In src/index.js:

if (url.pathname === "/notify-test") {
  // Sends a Discord notification even if the version has not changed
  const result = await runCheck(env, { forceNotify: true, manual: true });
  return jsonResponse(result, 200);
}

And inside src/discord.js:

if (data.forceNotify) {
  return {
    content:
      "\u200B\n" +
      "**TEST NOTIFICATION**\n" +
      "Product: " + config.productName + "\n" +
      "Current version: " + data.currentVersion + "\n" +
      "Release date: " + (data.releaseDate || "unknown") + "\n" +
      "Page: " + config.amdPageUrl
  };
}

Fetching the AMD driver version

AMD does not provide a public API for chipset driver releases. The worker therefore extracts metadata directly from the driver page.

The parsing logic pulls out three pieces of information:

  • version
  • release date
  • download page

Excerpt from src/amd.js:

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,
    pageUrl: config.amdPageUrl
  };
}

The HTML is first downloaded with a custom User-Agent because some AMD endpoints reject requests without 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 is converted to plain text and parsed using regular expressions anchored to the product name.

In src/amd.js:

const versionRegex = new RegExp(
  escapedName +
    "[\\s\\S]{0,1200}?Revision Number[\\s\\S]{0,200}?([0-9]+(?:\\.[0-9]+)+)",
  "i"
);
Note
AMD occasionally changes page markup. Keeping parsing logic small and focused reduces the chance of breakage.

State management with Workers KV

Cloudflare KV is used as the persistence layer.

From src/state.js:

export async function loadStoredState(env, kvKey) {
  return await env.STATE.get(kvKey, "json");
}

Writing a full state snapshot:

export async function saveCurrentState(env, kvKey, state) {
  await env.STATE.put(kvKey, JSON.stringify(state));
}

If the version has not changed, only the timestamp is updated:

export async function updateCheckedAtOnly(env, kvKey, previousState) {
  const updated = {
    ...previousState,
    checkedAt: new Date().toISOString()
  };

  await env.STATE.put(kvKey, JSON.stringify(updated));
}

Workers KV is eventually consistent, but this does not matter for a single scheduled worker execution.


Version comparison

The worker compares semantic version numbers using a helper function.

In src/utils.js:

const versionDiff = compareVersions(current.version, previous.version);
const changed = versionDiff > 0;

The helper splits version segments and compares them numerically.


Sending the Discord webhook

Discord notifications are sent to all configured webhook URLs.

In src/discord.js:

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 body is constructed separately.

function buildDiscordMessage(config, data) {
  return {
    content:
      "\u200B\n" +
      "**New AMD Chipset Driver released!**\n" +
      "Product: " + config.productName + "\n" +
      "Previous: " + data.previousVersion + "\n" +
      "New: " + data.currentVersion + "\n" +
      "Release date: " + (data.releaseDate || "unknown") + "\n" +
      "Download: " + config.amdPageUrl
  };
}

Worker execution flow

The central orchestration logic lives in runCheck, which coordinates fetching the current version, loading the previous state and deciding whether a notification should be sent.

In src/index.js:

const current = await fetchDriverInfo(config);
const previous = await loadStoredState(env, config.kvKey);

const versionDiff = compareVersions(current.version, previous.version);
const changed = versionDiff > 0;

If a new version appears the worker sends a Discord notification and updates the stored state.


Why Cloudflare Workers are perfect for this

This kind of job fits the Workers model extremely well.

The workload is tiny:

  • one HTTP request to AMD
  • one KV read
  • optional KV write
  • optional Discord webhook

Execution time is basically nonexistent. Running this on a traditional server would be unnecessary overhead.

Workers allow the entire system to exist as a few hundred lines of code.


Final result

The worker now runs periodically and checks AMD’s driver page. If the version changes, a single message appears in my Discord channel.

No AMD-Discord spam. No manual checks. Just a clean feed of chipset driver releases.

Exactly what I wanted.

Cheers.


You can find the Project here: amd-chipset-notifier