Skip to content

Commit 0c2ace9

Browse files
committed
Preserve historical Meetup events
1 parent 4ab61cc commit 0c2ace9

4 files changed

Lines changed: 116 additions & 21 deletions

File tree

‎.github/scripts/sync-meetup-events.mjs‎

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,39 @@ function date(value, field, url) {
5858
function eventId(event, url) {
5959
const match = event.UID?.match(/^event_(.+?)@meetup\.com$/);
6060
if (match) return match[1];
61-
const urlMatch = url.match(/\/events\/([^/?#]+)/);
61+
const urlMatch = url?.match(/\/events\/([^/?#]+)/);
6262
if (urlMatch) return urlMatch[1];
6363
throw new Error(`Meetup calendar event has no recognized ID: ${url}`);
6464
}
65+
function eventPath(event) {
66+
return path.join(CALENDAR_DIR, `meetup-${eventId(event, event.URL)}.md`);
67+
}
68+
69+
export function calendarSyncPlan(calendar, groupEvents) {
70+
const managed = new Set(calendar
71+
.filter(({ content }) => content.includes('meetupSource: meetup'))
72+
.map(({ file }) => file));
73+
const manualUrls = new Set(calendar
74+
.filter(({ content }) => !content.includes('meetupSource: meetup'))
75+
.map(({ content }) => content.match(/^externalUrl:\s*["']?([^\s"']+)/m)?.[1])
76+
.filter(Boolean));
77+
const desired = new Map();
78+
const cancelled = new Set();
79+
80+
for (const { events, groupName } of groupEvents) {
81+
for (const { event, metadata } of events) {
82+
const file = eventPath(event);
83+
if (event.STATUS === 'CANCELLED') {
84+
if (managed.has(file)) cancelled.add(file);
85+
continue;
86+
}
87+
if (manualUrls.has(event.URL)) continue;
88+
desired.set(file, eventFile(event, metadata, groupName));
89+
}
90+
}
91+
92+
return { desired, cancelled };
93+
}
6594

6695
function eventSchema(html, url) {
6796
const scripts = [...html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)];
@@ -158,21 +187,9 @@ async function main() {
158187
}
159188
const groupEvents = await Promise.all(configuredGroups.map(fetchGroupEvents));
160189
await mkdir(CALENDAR_DIR, { recursive: true });
161-
162190
const calendar = await calendarFiles();
163-
const managed = new Set(calendar.filter(({ content }) => content.includes('meetupSource: meetup')).map(({ file }) => file));
164-
const manualUrls = new Set(calendar
165-
.filter(({ content }) => !content.includes('meetupSource: meetup'))
166-
.map(({ content }) => content.match(/^externalUrl:\s*["']?([^\s"']+)/m)?.[1])
167-
.filter(Boolean));
168-
const desired = new Map();
169191

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

177194
const changes = [];
178195
for (const [file, content] of desired) {
@@ -181,18 +198,19 @@ async function main() {
181198
changes.push(`${current === undefined ? 'add' : 'update'} ${file}`);
182199
if (!DRY_RUN) await writeFile(file, content);
183200
}
184-
managed.delete(file);
185201
}
186-
187-
for (const file of managed) {
202+
for (const file of cancelled) {
203+
if (desired.has(file)) continue;
188204
changes.push(`remove ${file}`);
189205
if (!DRY_RUN) await rm(file);
190206
}
191207

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

195-
main().catch((error) => {
196-
console.error(error.message);
197-
process.exitCode = 1;
198-
});
211+
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
212+
main().catch((error) => {
213+
console.error(error.message);
214+
process.exitCode = 1;
215+
});
216+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
4+
import { calendarSyncPlan } from './sync-meetup-events.mjs';
5+
6+
const managedEvent = {
7+
file: 'content/calendar/meetup-123.md',
8+
content: 'meetupSource: meetup\nexternalUrl: "https://www.meetup.com/example/events/123/"',
9+
};
10+
11+
test('removes a generated event explicitly cancelled by Meetup', () => {
12+
const plan = calendarSyncPlan([managedEvent], [{
13+
events: [{ event: { STATUS: 'CANCELLED', UID: 'event_123@meetup.com' } }],
14+
groupName: 'Example group',
15+
}]);
16+
17+
assert.deepEqual([...plan.desired], []);
18+
assert.deepEqual([...plan.cancelled], [managedEvent.file]);
19+
});
20+
21+
test('retains generated historical events absent from the current feed', () => {
22+
const plan = calendarSyncPlan([managedEvent], [{ events: [], groupName: 'Example group' }]);
23+
24+
assert.deepEqual([...plan.desired], []);
25+
assert.deepEqual([...plan.cancelled], []);
26+
});
27+
28+
test('retains manual historical events when Meetup reports cancellation', () => {
29+
const historicalEvent = { ...managedEvent, content: 'externalUrl: "https://www.meetup.com/example/events/123/"' };
30+
const plan = calendarSyncPlan([historicalEvent], [{
31+
events: [{ event: { STATUS: 'CANCELLED', UID: 'event_123@meetup.com' } }],
32+
groupName: 'Example group',
33+
}]);
34+
35+
assert.deepEqual([...plan.cancelled], []);
36+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
meetupEventId: "316283278"
3+
meetupSource: meetup
4+
startDate: "2026-09-02"
5+
title: "NeoVIM for PowerShell!"
6+
externalUrl: "https://www.meetup.com/research-triangle-powershell-users-group/events/316283278/"
7+
virtual: true
8+
where: "Online"
9+
---
10+
Research Triangle PowerShell Users Group
11+
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.
12+
13+
This session explores alternative approaches to PowerShell development—specifically, building an efficient workflow without relying on traditional GUI-based IDEs.
14+
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.
15+
16+
**Topics include:**
17+
18+
* Why terminal-first workflows can be more efficient for certain tasks
19+
* Editor options and setup for cross-platform consistency
20+
* Practical tooling and configurations
21+
* Real-world examples from professional PowerShell work
22+
* Tips applicable to any development environment
23+
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
meetupEventId: "316366917"
3+
meetupSource: meetup
4+
startDate: "2026-09-10"
5+
title: "PowerShell UserGroup InnSalzach Meeting – 10th September 2026"
6+
externalUrl: "https://www.meetup.com/powershell-usergroup-inn-salzach/events/316366917/"
7+
virtual: true
8+
where: "Online"
9+
---
10+
PowerShell UserGroup Inn-Salzach
11+
LINK TO JOIN THE ONLINE MEETUP: https://app.gather.town/events/PUsQmCyFTWauNuWymZv1
12+
13+
Join us for an exciting PowerShell UserGroup InnSalzach session where we dive into the art of scripting!
14+
15+
**Topic:** EverythingFast: Developing Performant Modules
16+
**Speaker:** Justin Grote
17+
**Date:** 10th September 2026 at 7pm CEST
18+
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!

0 commit comments

Comments
 (0)