Skip to content

Instantly share code, notes, and snippets.

@olekstomek
Last active July 21, 2026 08:35
Show Gist options
  • Select an option

  • Save olekstomek/a8178bf123d6830c05dd9e256731aa93 to your computer and use it in GitHub Desktop.

Select an option

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
/**
* 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">&bull; 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">&bull; 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