Last active
July 21, 2026 08:35
-
-
Save olekstomek/a8178bf123d6830c05dd9e256731aa93 to your computer and use it in GitHub Desktop.
Apps Script trigger - check dane.gov.pl/source-code and send email if new version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * dane.gov.pl /source-code — new code version notifier. | |
| * | |
| * Time-driven trigger: runs once a day, shortly before midnight. | |
| * The script fetches https://dane.gov.pl/source-code/, and if a new | |
| * backend_*.zip or frontend_*.zip file appeared TODAY, sends a short | |
| * email with: | |
| * - the newest file (version + date + size + working download link), | |
| * - the previous file in the same category (for context), | |
| * skipping *_latest.zip files. | |
| * | |
| * Required scopes (authorized on first run): | |
| * https://www.googleapis.com/auth/script.external_request (UrlFetchApp) | |
| * https://www.googleapis.com/auth/script.send_mail (MailApp) | |
| */ | |
| // --- Configuration ---------------------------------------------------------- | |
| const RECIPIENT = 'my@e-mail.com'; // <- set your address | |
| const SOURCE_URL = 'https://dane.gov.pl/source-code/'; | |
| const TIMEZONE = 'Europe/Warsaw'; // timezone of dates on the page | |
| const CATEGORY_ORDER = ['backend', 'frontend']; // section order in the email (always both) | |
| // --- Main function (bound to the trigger) ------------------------------------ | |
| function checkDaneGovPlSourceCode() { | |
| const response = fetchWithRetry(SOURCE_URL, [5, 7]); // retry after 5s, then after 7s | |
| if (!response) { | |
| return; // fetchWithRetry already logged and alerted | |
| } | |
| const files = parseFiles(response.getContentText()); | |
| if (files.length === 0) { | |
| const message = 'The page at ' + SOURCE_URL + ' was fetched successfully, but no .zip ' + | |
| 'files could be parsed from it. The page format may have changed — the script needs ' + | |
| 'to be reviewed.'; | |
| Logger.log(message); | |
| sendAlertEmail('dane.gov.pl source-code checker — parsing problem', message); | |
| return; | |
| } | |
| const groups = groupByCategory(files); // { backend: [...], frontend: [...] } | |
| const today = formatServerDate(new Date()); // e.g. "02-Jul-2026" | |
| const sections = buildSections(groups, today); // always both categories | |
| const changed = sections.filter(s => s.isNewToday).map(s => s.category); | |
| if (changed.length === 0) { | |
| Logger.log('No new version today (' + today + '). E-mail not sent.'); | |
| return; | |
| } | |
| MailApp.sendEmail({ | |
| to: RECIPIENT, | |
| subject: 'New dane.gov.pl code version! (' + changed.join(' + ') + ')', | |
| htmlBody: renderHtml(sections, today), | |
| body: renderText(sections, today) // fallback for clients without HTML | |
| }); | |
| Logger.log('E-mail sent. Changed categories: ' + changed.join(', ')); | |
| } | |
| // --- Listing parsing ---------------------------------------------------------- | |
| // A listing line looks like this (autoindex): | |
| // <a href="backend_2.59.4.zip">backend_2.59.4.zip</a> 02-Jul-2026 20:14 35M | |
| const FILE_REGEX = /<a href="([^"]+\.zip)">[^<]*<\/a>\s+(\d{2}-[A-Za-z]{3}-\d{4})\s+(\d{2}:\d{2})\s+(\S+)/g; | |
| const MONTHS = { Jan:0, Feb:1, Mar:2, Apr:3, May:4, Jun:5, Jul:6, Aug:7, Sep:8, Oct:9, Nov:10, Dec:11 }; | |
| function parseFiles(html) { | |
| const files = []; | |
| let m; | |
| while ((m = FILE_REGEX.exec(html)) !== null) { | |
| const filename = m[1]; | |
| if (filename.endsWith('_latest.zip')) continue; // skip *_latest.zip | |
| const dateStr = m[2]; // "02-Jul-2026" | |
| const timeStr = m[3]; // "20:14" | |
| const size = m[4]; // "35M" | |
| files.push({ | |
| filename: filename, | |
| category: categoryOf(filename), | |
| version: versionOf(filename), | |
| dateStr: dateStr, | |
| timeStr: timeStr, | |
| size: size, | |
| sortKey: parseListingDate(dateStr, timeStr).getTime(), | |
| url: SOURCE_URL + encodeURIComponent(filename) // absolute, working link | |
| }); | |
| } | |
| return files; | |
| } | |
| function categoryOf(filename) { | |
| if (filename.indexOf('backend_') === 0) return 'backend'; | |
| if (filename.indexOf('frontend_') === 0) return 'frontend'; | |
| return 'other'; | |
| } | |
| function versionOf(filename) { | |
| const m = filename.match(/_(\d+(?:\.\d+)*)\.zip$/); | |
| return m ? m[1] : filename; // "backend_2.59.4.zip" -> "2.59.4" | |
| } | |
| function parseListingDate(dateStr, timeStr) { | |
| const parts = dateStr.split('-'); // ["02","Jul","2026"] | |
| const clock = timeStr.split(':'); // ["20","14"] | |
| return new Date(Number(parts[2]), MONTHS[parts[1]], Number(parts[0]), | |
| Number(clock[0]), Number(clock[1])); | |
| } | |
| // --- Grouping and picking newest / previous ----------------------------------- | |
| function groupByCategory(files) { | |
| const groups = {}; | |
| files.forEach(f => { | |
| if (f.category === 'other') return; | |
| (groups[f.category] = groups[f.category] || []).push(f); | |
| }); | |
| // sort descending by date: newest first | |
| Object.keys(groups).forEach(cat => { | |
| groups[cat].sort((a, b) => b.sortKey - a.sortKey); | |
| }); | |
| return groups; | |
| } | |
| function buildSections(groups, today) { | |
| return CATEGORY_ORDER | |
| .filter(category => groups[category] && groups[category].length > 0) | |
| .map(category => { | |
| const list = groups[category]; | |
| const newest = list[0]; | |
| const previous = list[1] || null; | |
| const isNewToday = newest.dateStr === today; | |
| return { category, newest, previous, isNewToday }; | |
| }); | |
| } | |
| // --- Email rendering ------------------------------------------------------------ | |
| function renderHtml(sections, today) { | |
| const blocks = sections.map(s => { | |
| const label = capitalize(s.category); | |
| if (s.isNewToday) { | |
| const badge = ' <span style="color:#2e7d32">• new version today</span>'; | |
| const prevRow = s.previous | |
| ? fileRowHtml('Previous', s.previous) | |
| : '<div style="margin:2px 0"><strong>Previous:</strong> none</div>'; | |
| return '<h3 style="margin:16px 0 4px">' + label + badge + '</h3>' + | |
| fileRowHtml('New', s.newest) + prevRow; | |
| } | |
| // category with no new version today — show the latest existing version for context | |
| const badge = ' <span style="color:#888">• no new version today</span>'; | |
| return '<h3 style="margin:16px 0 4px">' + label + badge + '</h3>' + | |
| fileRowHtml('Latest', s.newest); | |
| }).join(''); | |
| return '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5">' + | |
| '<p>New code version on <a href="' + SOURCE_URL + '">dane.gov.pl/source-code</a> (' + today + ').</p>' + | |
| blocks + | |
| '</div>'; | |
| } | |
| function fileRowHtml(labelText, f) { | |
| return '<div style="margin:2px 0">' + | |
| '<strong>' + labelText + ':</strong> ' + | |
| '<a href="' + f.url + '">' + f.filename + '</a> ' + | |
| '<span style="color:#666">(' + f.dateStr + ' ' + f.timeStr + ', ' + f.size + ')</span>' + | |
| '</div>'; | |
| } | |
| function renderText(sections, today) { | |
| const lines = ['New code version on ' + SOURCE_URL + ' (' + today + ')', '']; | |
| sections.forEach(s => { | |
| if (s.isNewToday) { | |
| lines.push(s.category.toUpperCase() + ' (new version today)'); | |
| lines.push(' New: ' + fileLineText(s.newest)); | |
| lines.push(' ' + s.newest.url); | |
| if (s.previous) { | |
| lines.push(' Previous: ' + fileLineText(s.previous)); | |
| lines.push(' ' + s.previous.url); | |
| } else { | |
| lines.push(' Previous: none'); | |
| } | |
| } else { | |
| lines.push(s.category.toUpperCase() + ' (no new version today)'); | |
| lines.push(' Latest: ' + fileLineText(s.newest)); | |
| lines.push(' ' + s.newest.url); | |
| } | |
| lines.push(''); | |
| }); | |
| return lines.join('\n'); | |
| } | |
| function fileLineText(f) { | |
| return f.filename + ' (' + f.dateStr + ' ' + f.timeStr + ', ' + f.size + ')'; | |
| } | |
| // --- Helpers ---------------------------------------------------------------- | |
| // Server-formatted date ("02-Jul-2026"), with English month names and a fixed | |
| // timezone enforced regardless of the project's locale/timezone settings. | |
| function formatServerDate(date) { | |
| const day = date.toLocaleString('en-US', { timeZone: TIMEZONE, day: '2-digit' }); | |
| const month = date.toLocaleString('en-US', { timeZone: TIMEZONE, month: 'short' }); | |
| const year = date.toLocaleString('en-US', { timeZone: TIMEZONE, year: 'numeric' }); | |
| return day + '-' + month + '-' + year; | |
| } | |
| function capitalize(text) { | |
| return text.charAt(0).toUpperCase() + text.slice(1); | |
| } | |
| // Fetches url and retries on any non-200 response, waiting delaysSeconds[i] | |
| // seconds before retry attempt i+1. Returns the successful HTTPResponse, or | |
| // null if every attempt failed (an alert e-mail is sent in that case). | |
| function fetchWithRetry(url, delaysSeconds) { | |
| let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true }); | |
| let lastCode = response.getResponseCode(); | |
| for (let attempt = 0; attempt < delaysSeconds.length && lastCode !== 200; attempt++) { | |
| Logger.log('Fetch failed with HTTP ' + lastCode + '. Retrying in ' + | |
| delaysSeconds[attempt] + 's (attempt ' + (attempt + 1) + '/' + delaysSeconds.length + ')...'); | |
| Utilities.sleep(delaysSeconds[attempt] * 1000); | |
| response = UrlFetchApp.fetch(url, { muteHttpExceptions: true }); | |
| lastCode = response.getResponseCode(); | |
| } | |
| if (lastCode !== 200) { | |
| const message = 'Failed to fetch ' + url + ' after ' + (delaysSeconds.length + 1) + | |
| ' attempt(s). Last HTTP status: ' + lastCode + '.'; | |
| Logger.log(message); | |
| sendAlertEmail('dane.gov.pl source-code checker — fetch problem', message); | |
| return null; | |
| } | |
| return response; | |
| } | |
| // Sends a plain-text alert e-mail about a script/page problem. | |
| function sendAlertEmail(subject, message) { | |
| MailApp.sendEmail({ | |
| to: RECIPIENT, | |
| subject: subject, | |
| body: message | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment