import { readFile, writeFile, access, mkdir } from 'node:fs/promises'; import path from 'node:path'; import fs from 'node:fs'; import dotenv from 'dotenv'; import { homedir } from 'node:os'; const userHomeDir = homedir(); // Load .env files const GLOBAL_ENV_PATH = path.join(userHomeDir, '.env'); const LOCAL_ENV_PATH = path.resolve('.env'); /** * Load environment variables from a file if it exists. * @param {string} filePath * @returns {object} Parsed environment variables */ function loadEnvFile(filePath) { if (fs.existsSync(filePath)) { return dotenv.parse(fs.readFileSync(filePath)); } return {}; } // Merge global and local .env configurations const globalEnv = loadEnvFile(GLOBAL_ENV_PATH); const localEnv = loadEnvFile(LOCAL_ENV_PATH); process.env = { ...globalEnv, ...process.env, ...localEnv }; // Configurable values const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK || ''; const GITHUB_DEV_TOKEN = process.env.GITHUB_DEV_TOKEN || ''; const GITHUB_REPO = process.env.GITHUB_REPO || 'theNewDynamic/gohugo-theme-ananke'; const DEFAULT_MESSAGE_TEMPLATE = 'New release: {{tag_name}} - {{html_url}}'; const MESSAGE_TEMPLATE = process.env.MESSAGE_TEMPLATE || DEFAULT_MESSAGE_TEMPLATE; const CACHE_DIR = './cache'; const CACHE_FILE = 'github-releases.json'; const CACHE_FILE_PATH = path.join(CACHE_DIR, CACHE_FILE); /** * Ensures the cache directory exists, creating it if necessary. * @returns {Promise} */ async function ensureCacheDirectory() { try { await access(CACHE_DIR); } catch { try { await mkdir(CACHE_DIR, { recursive: true }); } catch (err) { console.error(`Failed to create cache directory: ${err.message}`); process.exit(1); } } } /** * Reads the cache file or returns an empty array if not found. * @returns {Promise} */ async function readCache() { try { const data = await readFile(CACHE_FILE_PATH, 'utf8'); return JSON.parse(data) || []; } catch { return []; } } /** * Writes data to the cache file. * @param {string[]} data */ async function writeCache(data) { await writeFile(CACHE_FILE_PATH, JSON.stringify(data, null, 2)); } /** * Fetches the latest release from the GitHub REST API. * @returns {Promise<{ tag_name: string, html_url: string } | null>} */ async function fetchLatestRelease() { try { const response = await fetch('https://api.github.com/repos/' + GITHUB_REPO + '/releases', { headers: { Authorization: `token ${GITHUB_DEV_TOKEN}`, }, }); if (!response.ok) { throw new Error(`GitHub API request failed: ${response.statusText}`); } const releases = await response.json(); if (!Array.isArray(releases) || releases.length === 0) { console.log('No releases found.'); return null; } return releases[0]; } catch (err) { console.error('Failed to fetch releases:', err.message); return null; } } /** * Posts a message to Discord using a webhook. * @param {string} message */ async function postToDiscord(message) { try { const response = await fetch(DISCORD_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: message }), }); if (!response.ok) { throw new Error(`Failed to post to Discord: ${response.statusText}`); } console.log('Posted to Discord successfully.'); } catch (err) { console.error('Failed to post to Discord:', err.message); } } /** * Formats the release message using the template. * @param {Record} releaseData * @returns {string} */ function formatMessage(releaseData) { return MESSAGE_TEMPLATE.replace(/{{\s*(\w+)\s*}}/g, (_, key) => releaseData[key] || ''); } /** * Main function to fetch the latest GitHub release and post it to Discord. */ async function main() { try { await ensureCacheDirectory(); const cachedIds = await readCache(); const latestRelease = await fetchLatestRelease(); if (latestRelease && !cachedIds.includes(latestRelease.tag_name)) { const message = formatMessage(latestRelease); await postToDiscord(message); cachedIds.push(latestRelease.tag_name); await writeCache(cachedIds); } else { console.log('No new releases to post.'); } } catch (err) { console.error('Error:', err.message); } } main();