diff --git a/.github/scripts/posterboy.mjs b/.github/scripts/posterboy.mjs new file mode 100644 index 0000000..965ca59 --- /dev/null +++ b/.github/scripts/posterboy.mjs @@ -0,0 +1,162 @@ +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(); diff --git a/.gitignore b/.gitignore index 6e55a5a..f663d05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # OS .DS_Store Thumbs.db +.env # IDEs .buildpath @@ -26,6 +27,7 @@ npm-debug.log /src/client.config.json /styleguide/ /docs/ +cache /junit.xml partials/structure/stylesheet.html diff --git a/package-lock.json b/package-lock.json index 943999f..b66fe62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "devDependencies": { "@davidsneighbour/markdownlint-config": "^2024.4.8", "@davidsneighbour/release-config": "2024.4.8", - "@davidsneighbour/tools": "2024.4.8" + "@davidsneighbour/tools": "2024.4.8", + "dotenv": "^16.4.5" } }, "node_modules/@azu/format-text": { diff --git a/package.json b/package.json index a7c280e..afd39ea 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "devDependencies": { "@davidsneighbour/markdownlint-config": "^2024.4.8", "@davidsneighbour/release-config": "2024.4.8", - "@davidsneighbour/tools": "2024.4.8" + "@davidsneighbour/tools": "2024.4.8", + "dotenv": "^16.4.5" }, "scripts": { "deploy": "cd exampleSite; hugo;",