feat: enhance BIOS class and notification handling; improve error logging and S3 initialization
All checks were successful
/ run (push) Successful in 3s

This commit is contained in:
nyyu 2025-06-28 20:54:51 +02:00
parent 78f03005a3
commit 2c13420ae6
4 changed files with 92 additions and 58 deletions

View file

@ -1,50 +1,66 @@
import { HTMLElement, parse } from "node-html-parser";
class Bios {
/**
* Represents a BIOS entry with version, date, changelog, and optional download URL.
*/
export class Bios {
/** BIOS release date (YYYY/MM/DD) */
readonly date: string;
/** BIOS version string */
readonly version: string;
/** BIOS changelog/description */
readonly changelog: string;
readonly url: string | null;
/** Optional download URL for the BIOS file */
readonly url?: string;
/**
* @param date - BIOS release date
* @param version - BIOS version string
* @param changelog - BIOS changelog/description
* @param url - Optional download URL
* @throws Error if required fields are missing
*/
constructor(
date: string,
version: string,
changelog: string,
url: string | null,
url?: string,
) {
if (!date || !version || !changelog) {
throw new Error("Required BIOS information missing");
if (!date?.trim() || !version?.trim() || !changelog?.trim()) {
throw new Error(
"Required BIOS information missing: date, version, or changelog",
);
}
this.date = date.trim();
this.version = version.trim();
this.changelog = changelog.trim();
this.url = url?.trim() || null;
this.url = url?.trim();
}
/**
* Returns a formatted string representation of the BIOS entry.
*/
toString(): string {
return `${this.version} (${this.date}):\n${this.changelog}`;
return `${this.version} (${this.date}):\n ${this.changelog}\n ${this.url}`;
}
}
/**
* Fetches and parses BIOS information from a motherboard support page
* Fetches and parses BIOS information from a motherboard support page.
* @param mobo - URL of the motherboard support page
* @returns Promise containing array of Bios objects
* @throws Error if the URL is invalid or parsing fails
*/
async function getBios(mobo: string): Promise<Bios[]> {
if (!mobo || !mobo.startsWith("http")) {
throw new Error("Invalid motherboard URL");
export async function getBios(mobo: string): Promise<Bios[]> {
if (!mobo || !/^https?:\/\//.test(mobo)) {
throw new Error("Invalid motherboard URL: must start with http or https");
}
try {
const response = await fetch(mobo);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const body = await response.text();
const root = parse(body);
const biosList: Bios[] = [];
@ -57,15 +73,12 @@ async function getBios(mobo: string): Promise<Bios[]> {
for (const tr of rows) {
try {
const bios = parseBiosRow(tr);
if (bios) {
biosList.push(bios);
}
biosList.push(parseBiosRow(tr));
} catch (error) {
console.error("Failed to parse BIOS row:", error);
const message = error instanceof Error ? error.message : String(error);
console.error("Failed to parse BIOS row:", message);
}
}
return biosList;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@ -74,22 +87,29 @@ async function getBios(mobo: string): Promise<Bios[]> {
}
/**
* Parses a table row into a Bios object
* Parses a table row into a Bios object.
* @param tr - HTML table row element
* @returns Bios object or null if parsing fails
* @returns Bios object
* @throws Error if the row format is invalid
*/
function parseBiosRow(tr: HTMLElement): Bios | null {
function parseBiosRow(tr: HTMLElement): Bios {
try {
const tds = tr.querySelectorAll("td");
if (tds.length < 6) {
throw new Error("Invalid table row format");
throw new Error(
`Invalid table row format: expected >=6 columns, got ${tds.length}`,
);
}
// Try to resolve the link to an absolute URL if possible
const link = tds[5].querySelector("a")?.getAttribute("href");
const url = link
? encodeURI(link).replace(/[(]/g, "%28").replace(/[)]/g, "%29")
: null;
let url: string | undefined = undefined;
if (link) {
try {
url = encodeURI(link).replace(/[(]/g, "%28").replace(/[)]/g, "%29");
} catch {
url = link;
}
}
return new Bios(
tds[1].text, // date
tds[0].text, // version
@ -97,9 +117,7 @@ function parseBiosRow(tr: HTMLElement): Bios | null {
url,
);
} catch (error) {
console.error("Error parsing row:", error);
return null;
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Error parsing BIOS row: ${message}`);
}
}
export { Bios, getBios };

View file

@ -34,18 +34,17 @@ async function main() {
);
if (!uploaded) {
throw new Error("Failed to upload updated BIOS list");
console.error("Failed to upload updated BIOS list");
}
// Send notification
const message = formatNotificationMessage(newBios);
console.log("Sending notification:", message);
const notified = await notify(message);
if (!notified.success) {
throw new Error(`Failed to send notification: ${notified.error}`);
console.error(`Failed to send notification: ${notified.error}`);
}
console.log("Successfully updated BIOS list and sent notification");
} else {
console.log("No new BIOS versions found");
}

View file

@ -6,17 +6,17 @@ const requiredEnvVars = ["SMS_WEBHOOK_URL", "SMS_USER", "SMS_PASS"] as const;
// Validate environment variables on module load
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
console.error(`Missing required environment variable: ${envVar}`);
}
}
interface NotificationPayload {
export interface NotificationPayload {
msg: string;
user: string;
pass: string;
}
interface NotificationResult {
export interface NotificationResult {
success: boolean;
error?: string;
}
@ -27,7 +27,7 @@ interface NotificationResult {
* @returns Promise with the notification result
* @throws Error if the message is empty or if environment variables are missing
*/
async function notify(msg: string): Promise<NotificationResult> {
export async function notify(msg: string): Promise<NotificationResult> {
// Input validation
if (!msg || msg.trim().length === 0) {
return {
@ -37,6 +37,14 @@ async function notify(msg: string): Promise<NotificationResult> {
}
const webhookUrl = process.env.SMS_WEBHOOK_URL!;
if (!webhookUrl) {
return {
success: false,
error: "Missing webhook url",
};
}
const payload: NotificationPayload = {
msg: msg.trim().substring(0, 160),
user: process.env.SMS_USER!,
@ -72,5 +80,3 @@ async function notify(msg: string): Promise<NotificationResult> {
};
}
}
export { type NotificationPayload, type NotificationResult, notify };

View file

@ -8,18 +8,23 @@ const requiredEnvVars = [
"AWS_SECRET_ACCESS_KEY",
] as const;
for (const envVar of requiredEnvVars) {
function initS3(): S3Client | undefined {
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
console.error(`Missing required environment variable: ${envVar}`);
return undefined;
}
}
const s3client = new S3Client({
return new S3Client({
region: process.env.AWS_DEFAULT_REGION!,
endPoint: process.env.AWS_ENDPOINT_URL!,
accessKey: process.env.AWS_ACCESS_KEY_ID!,
secretKey: process.env.AWS_SECRET_ACCESS_KEY!,
});
});
}
}
const s3client: S3Client | undefined = initS3();
/**
* get object from S3
@ -28,14 +33,18 @@ const s3client = new S3Client({
* @returns Promise containing the content
* @throws Error if key or bucket is empty
*/
async function getObject<Type>(
export async function getObject<Type>(
key: string,
bucket: string,
): Promise<Type | null> {
): Promise<Type | undefined> {
if (!key || !bucket) {
throw new Error("Key and bucket must not be empty");
}
if (!s3client) {
return undefined;
}
try {
const response = await s3client.getObject(key, { bucketName: bucket });
if (!response.ok) {
@ -44,7 +53,7 @@ async function getObject<Type>(
return response.json();
} catch (error) {
console.error(`Unexpected error downloading ${key}:`, error);
return null;
return undefined;
}
}
@ -57,7 +66,7 @@ async function getObject<Type>(
* @returns Promise<boolean> indicating success or failure
* @throws Error if key or bucket is empty
*/
async function putObject<Type>(
export async function putObject<Type>(
content: Type,
key: string,
bucket: string,
@ -67,6 +76,10 @@ async function putObject<Type>(
throw new Error("Key and bucket must not be empty");
}
if (!s3client) {
return false;
}
try {
const response = await s3client.putObject(
key,
@ -86,5 +99,3 @@ async function putObject<Type>(
return false;
}
}
export { getObject, putObject };