Skip to content

Commit 0e37163

Browse files
Preserve historical Meetup events (#166)
## Summary - stop removing Meetup-managed calendar entries absent from the current feed - retain historical event pages while continuing to add and update current events ## Verification - `node --check .github/scripts/sync-meetup-events.mjs` - `git diff --check`
1 parent e50ed64 commit 0e37163

4 files changed

Lines changed: 117 additions & 21 deletions

File tree

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

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,41 @@ 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
}
6565

66+
function eventPath(event) {
67+
return path.join(CALENDAR_DIR, `meetup-${eventId(event, event.URL)}.md`);
68+
}
69+
70+
export function calendarSyncPlan(calendar, groupEvents) {
71+
const managed = new Set(calendar
72+
.filter(({ content }) => content.includes('meetupSource: meetup'))
73+
.map(({ file }) => file));
74+
const manualUrls = new Set(calendar
75+
.filter(({ content }) => !content.includes('meetupSource: meetup'))
76+
.map(({ content }) => content.match(/^externalUrl:\s*["']?([^\s"']+)/m)?.[1])
77+
.filter(Boolean));
78+
const desired = new Map();
79+
const cancelled = new Set();
80+
81+
for (const { events, groupName } of groupEvents) {
82+
for (const { event, metadata } of events) {
83+
const file = eventPath(event);
84+
if (event.STATUS === 'CANCELLED') {
85+
if (managed.has(file)) cancelled.add(file);
86+
continue;
87+
}
88+
if (manualUrls.has(event.URL)) continue;
89+
desired.set(file, eventFile(event, metadata, groupName));
90+
}
91+
}
92+
93+
return { desired, cancelled };
94+
}
95+
6696
function eventSchema(html, url) {
6797
const scripts = [...html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)];
6898
for (const [, script] of scripts) {
@@ -158,21 +188,9 @@ async function main() {
158188
}
159189
const groupEvents = await Promise.all(configuredGroups.map(fetchGroupEvents));
160190
await mkdir(CALENDAR_DIR, { recursive: true });
161-
162191
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();
169192

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-
}
193+
const { desired, cancelled } = calendarSyncPlan(calendar, groupEvents);
176194

177195
const changes = [];
178196
for (const [file, content] of desired) {
@@ -181,18 +199,19 @@ async function main() {
181199
changes.push(`${current === undefined ? 'add' : 'update'} ${file}`);
182200
if (!DRY_RUN) await writeFile(file, content);
183201
}
184-
managed.delete(file);
185202
}
186-
187-
for (const file of managed) {
203+
for (const file of cancelled) {
204+
if (desired.has(file)) continue;
188205
changes.push(`remove ${file}`);
189206
if (!DRY_RUN) await rm(file);
190207
}
191208

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

195-
main().catch((error) => {
196-
console.error(error.message);
197-
process.exitCode = 1;
198-
});
212+
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
213+
main().catch((error) => {
214+
console.error(error.message);
215+
process.exitCode = 1;
216+
});
217+
}
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)