Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 40 additions & 21 deletions .github/scripts/sync-meetup-events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,41 @@ function date(value, field, url) {
function eventId(event, url) {
const match = event.UID?.match(/^event_(.+?)@meetup\.com$/);
if (match) return match[1];
const urlMatch = url.match(/\/events\/([^/?#]+)/);
const urlMatch = url?.match(/\/events\/([^/?#]+)/);
if (urlMatch) return urlMatch[1];
throw new Error(`Meetup calendar event has no recognized ID: ${url}`);
}

function eventPath(event) {
return path.join(CALENDAR_DIR, `meetup-${eventId(event, event.URL)}.md`);
}

export function calendarSyncPlan(calendar, groupEvents) {
const managed = new Set(calendar
.filter(({ content }) => content.includes('meetupSource: meetup'))
.map(({ file }) => file));
const manualUrls = new Set(calendar
.filter(({ content }) => !content.includes('meetupSource: meetup'))
.map(({ content }) => content.match(/^externalUrl:\s*["']?([^\s"']+)/m)?.[1])
.filter(Boolean));
const desired = new Map();
const cancelled = new Set();

for (const { events, groupName } of groupEvents) {
for (const { event, metadata } of events) {
const file = eventPath(event);
if (event.STATUS === 'CANCELLED') {
if (managed.has(file)) cancelled.add(file);
continue;
}
if (manualUrls.has(event.URL)) continue;
desired.set(file, eventFile(event, metadata, groupName));
}
}

return { desired, cancelled };
}

function eventSchema(html, url) {
const scripts = [...html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)];
for (const [, script] of scripts) {
Expand Down Expand Up @@ -158,21 +188,9 @@ async function main() {
}
const groupEvents = await Promise.all(configuredGroups.map(fetchGroupEvents));
await mkdir(CALENDAR_DIR, { recursive: true });

const calendar = await calendarFiles();
const managed = new Set(calendar.filter(({ content }) => content.includes('meetupSource: meetup')).map(({ file }) => file));
const manualUrls = new Set(calendar
.filter(({ content }) => !content.includes('meetupSource: meetup'))
.map(({ content }) => content.match(/^externalUrl:\s*["']?([^\s"']+)/m)?.[1])
.filter(Boolean));
const desired = new Map();

for (const { events, groupName } of groupEvents) {
for (const { event, metadata } of events) {
if (event.STATUS === 'CANCELLED' || manualUrls.has(event.URL)) continue;
desired.set(path.join(CALENDAR_DIR, `meetup-${eventId(event, event.URL)}.md`), eventFile(event, metadata, groupName));
}
}
const { desired, cancelled } = calendarSyncPlan(calendar, groupEvents);

const changes = [];
for (const [file, content] of desired) {
Expand All @@ -181,18 +199,19 @@ async function main() {
changes.push(`${current === undefined ? 'add' : 'update'} ${file}`);
if (!DRY_RUN) await writeFile(file, content);
}
managed.delete(file);
}

for (const file of managed) {
for (const file of cancelled) {
if (desired.has(file)) continue;
changes.push(`remove ${file}`);
if (!DRY_RUN) await rm(file);
}

console.log(changes.length ? changes.join('\n') : 'Meetup events are already synchronized.');
}

main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
36 changes: 36 additions & 0 deletions .github/scripts/sync-meetup-events.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { calendarSyncPlan } from './sync-meetup-events.mjs';

const managedEvent = {
file: 'content/calendar/meetup-123.md',
content: 'meetupSource: meetup\nexternalUrl: "https://www.meetup.com/example/events/123/"',
};

test('removes a generated event explicitly cancelled by Meetup', () => {
const plan = calendarSyncPlan([managedEvent], [{
events: [{ event: { STATUS: 'CANCELLED', UID: 'event_123@meetup.com' } }],
groupName: 'Example group',
}]);

assert.deepEqual([...plan.desired], []);
assert.deepEqual([...plan.cancelled], [managedEvent.file]);
});

test('retains generated historical events absent from the current feed', () => {
const plan = calendarSyncPlan([managedEvent], [{ events: [], groupName: 'Example group' }]);

assert.deepEqual([...plan.desired], []);
assert.deepEqual([...plan.cancelled], []);
});

test('retains manual historical events when Meetup reports cancellation', () => {
const historicalEvent = { ...managedEvent, content: 'externalUrl: "https://www.meetup.com/example/events/123/"' };
const plan = calendarSyncPlan([historicalEvent], [{
events: [{ event: { STATUS: 'CANCELLED', UID: 'event_123@meetup.com' } }],
groupName: 'Example group',
}]);

assert.deepEqual([...plan.cancelled], []);
});
23 changes: 23 additions & 0 deletions content/calendar/meetup-316283278.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
meetupEventId: "316283278"
meetupSource: meetup
startDate: "2026-09-02"
title: "NeoVIM for PowerShell!"
externalUrl: "https://www.meetup.com/research-triangle-powershell-users-group/events/316283278/"
virtual: true
where: "Online"
---
Research Triangle PowerShell Users Group
PowerShell development doesn't require VSCode. In this session, Rob shares his approach to building an efficient, terminal-first workflow using NeoVIM—and demonstrates how this setup powers real work in DevOps, Cloud Security, and Application Security environments.

This session explores alternative approaches to PowerShell development—specifically, building an efficient workflow without relying on traditional GUI-based IDEs.
Drawing from 13+ years across System Administration, DevOps, Cloud Security, and Application Security, Rob Pleau shares the tools, configurations, and strategies that enable productive PowerShell development in a terminal environment.

**Topics include:**

* Why terminal-first workflows can be more efficient for certain tasks
* Editor options and setup for cross-platform consistency
* Practical tooling and configurations
* Real-world examples from professional PowerShell work
* Tips applicable to any development environment
*
18 changes: 18 additions & 0 deletions content/calendar/meetup-316366917.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
meetupEventId: "316366917"
meetupSource: meetup
startDate: "2026-09-10"
title: "PowerShell UserGroup InnSalzach Meeting – 10th September 2026"
externalUrl: "https://www.meetup.com/powershell-usergroup-inn-salzach/events/316366917/"
virtual: true
where: "Online"
---
PowerShell UserGroup Inn-Salzach
LINK TO JOIN THE ONLINE MEETUP: https://app.gather.town/events/PUsQmCyFTWauNuWymZv1

Join us for an exciting PowerShell UserGroup InnSalzach session where we dive into the art of scripting!

**Topic:** EverythingFast: Developing Performant Modules
**Speaker:** Justin Grote
**Date:** 10th September 2026 at 7pm CEST
There is no fast...only fast enough, but sometimes fast enough needs to be FAST. We will demonstrate ModuleFast and ExcelFast, the specific issues they address, and techniques in both native PowerShell and C# to make things go vroom!