Skip to content

Commit 5d07e81

Browse files
committed
Event validation and friendly feedback on submission
1 parent 99201d6 commit 5d07e81

1 file changed

Lines changed: 40 additions & 24 deletions

File tree

.github/scripts/process-new-event-issue.mjs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const RUNNER_TEMP = process.env.RUNNER_TEMP ?? path.join(WORKSPACE, '.tmp');
66
const OUTPUT_PATH = process.env.GITHUB_OUTPUT;
77
const EVENT_PATH = process.env.GITHUB_EVENT_PATH;
88
const YEAR = '2026';
9+
const PCD_FORUM_THREAD_URL = 'https://discourse.processing.org/t/pcd-worldwide-2026-call-for-organizers/48081';
910

1011
await fs.mkdir(RUNNER_TEMP, { recursive: true });
1112

@@ -32,9 +33,9 @@ function parseIssueSections(body) {
3233
}));
3334
}
3435

35-
function required(fields, label, errors) {
36+
function required(fields, label, errors, errorOverride) {
3637
const value = fields.get(label)?.trim() ?? '';
37-
if (!value) errors.push(`${label} is required.`);
38+
if (!value) errors.push(errorOverride ?? { field: label, message: 'This field is required.' });
3839
return value;
3940
}
4041

@@ -113,13 +114,22 @@ function parseOrganizers(raw) {
113114
}).filter((organizer) => organizer.name);
114115
}
115116

117+
function formatError({ field, found, message }) {
118+
const foundPart = found !== undefined ? ` (found: \`${found}\`)` : '';
119+
return `- **${field}**${foundPart}${message}`;
120+
}
121+
116122
function buildValidationComment(errors) {
123+
const count = errors.length;
124+
const fieldWord = count === 1 ? 'field needs' : 'fields need';
117125
return [
118-
'Thanks for submitting a new event. I could not generate the branch and pull request yet because a few fields need attention:',
126+
'Thanks for submitting your event to Processing Community Day 2026! 🌍',
127+
'',
128+
`We couldn't create a pull request yet because **${count} ${fieldWord} attention**:`,
119129
'',
120-
...errors.map((error) => `- ${error}`),
130+
...errors.map(formatError),
121131
'',
122-
'Please edit the issue with the missing or corrected information. Opening a fresh submission is also fine if that is easier.',
132+
`Once you've edited the issue with the corrected information, this check will run again automatically. If you need help, post in the [PCD 2026 forum thread](${PCD_FORUM_THREAD_URL}).`,
123133
].join('\n');
124134
}
125135

@@ -151,8 +161,14 @@ const fields = parseIssueSections(issueBody);
151161
const errors = [];
152162

153163
const eventName = required(fields, 'Event name', errors);
154-
const plusCode = required(fields, 'Map placement', errors).replace(/\s+/g, '').toUpperCase();
155-
const eventFormat = required(fields, 'Event format', errors);
164+
const plusCode = required(fields, 'Map placement', errors, {
165+
field: 'Map placement (Plus Code)',
166+
message: 'This field is required. A Plus Code looks like `8FW4V75V+8Q`. [Find your Plus Code →](https://plus.codes/)',
167+
}).replace(/\s+/g, '').toUpperCase();
168+
const eventFormat = required(fields, 'Event format', errors, {
169+
field: 'Event format',
170+
message: 'This field is required. Choose one of: `In person` or `Online`.',
171+
});
156172
const isOnlineEvent = eventFormat === 'Online';
157173
const eventUrl = fields.get('Event URL (only for online events)')?.trim() ?? '';
158174
const primaryContactName = required(fields, 'Primary contact name', errors);
@@ -185,11 +201,11 @@ const VALID_ORG_TYPES = new Set([
185201
]);
186202
const VALID_EVENT_FORMATS = new Set(['In person', 'Online']);
187203

188-
if (eventDate && !isValidDate(eventDate)) errors.push(`Event date must use YYYY-MM-DD and be a real date. Received "${eventDate}".`);
189-
if (eventEndDate && !isValidDate(eventEndDate)) errors.push(`End date must use YYYY-MM-DD and be a real date. Received "${eventEndDate}".`);
190-
if (isValidDate(eventDate) && eventEndDate && isValidDate(eventEndDate) && eventEndDate < eventDate) errors.push('End date cannot be earlier than event date.');
191-
if (startTime && !isValidTime(startTime)) errors.push(`Start time must use 24-hour HH:MM. Received "${startTime}".`);
192-
if (endTime && !isValidTime(endTime)) errors.push(`End time must use 24-hour HH:MM. Received "${endTime}".`);
204+
if (eventDate && !isValidDate(eventDate)) errors.push({ field: 'Event date', found: eventDate, message: 'Invalid format. Please use `YYYY-MM-DD`, e.g. `2026-03-21`.' });
205+
if (eventEndDate && !isValidDate(eventEndDate)) errors.push({ field: 'End date', found: eventEndDate, message: 'Invalid format. Please use `YYYY-MM-DD`, e.g. `2026-03-22`.' });
206+
if (isValidDate(eventDate) && eventEndDate && isValidDate(eventEndDate) && eventEndDate < eventDate) errors.push({ field: 'End date', found: eventEndDate, message: 'The end date must be on or after the event date.' });
207+
if (startTime && !isValidTime(startTime)) errors.push({ field: 'Start time', found: startTime, message: 'Invalid format. Please use 24-hour `HH:MM`, e.g. `14:00`.' });
208+
if (endTime && !isValidTime(endTime)) errors.push({ field: 'End time', found: endTime, message: 'Invalid format. Please use 24-hour `HH:MM`, e.g. `16:30`.' });
193209
if (
194210
startTime &&
195211
endTime &&
@@ -198,17 +214,17 @@ if (
198214
(!eventEndDate || eventEndDate === eventDate) &&
199215
endTime <= startTime
200216
) {
201-
errors.push('End time must be later than start time for single-day events.');
217+
errors.push({ field: 'End time', found: endTime, message: 'End time must be later than start time for single-day events.' });
202218
}
203-
if (eventWebsite && !isValidHttpUrl(eventWebsite)) errors.push(`Event website must be a valid http or https URL. Received "${eventWebsite}".`);
204-
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push(`Forum discussion URL must be a valid http or https URL. Received "${forumThreadUrl}".`);
205-
if (contactEmail && !isValidEmail(contactEmail)) errors.push(`Primary contact email is not valid. Received "${contactEmail}".`);
206-
if (plusCode && !isValidPlusCode(plusCode)) errors.push(`Full global plus code is not valid. Received "${plusCode}".`);
207-
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push(`Online event URL must be a valid http or https URL. Received "${eventUrl}".`);
208-
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push(`Organization website must be a valid http or https URL. Received "${organizationUrl}".`);
209-
if (eventFormat && !VALID_EVENT_FORMATS.has(eventFormat)) errors.push(`Event format "${eventFormat}" is not one of the valid options.`);
210-
if (organizationType && !VALID_ORG_TYPES.has(organizationType)) errors.push(`Organization type "${organizationType}" is not one of the valid options.`);
211-
if (isOnlineEvent && !eventUrl) errors.push('Event URL is required for online events.');
219+
if (eventWebsite && !isValidHttpUrl(eventWebsite)) errors.push({ field: 'Event website', found: eventWebsite, message: 'Must be a valid URL starting with `http://` or `https://`, e.g. `https://example.com/pcd`.' });
220+
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push({ field: 'Forum discussion URL', found: forumThreadUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
221+
if (contactEmail && !isValidEmail(contactEmail)) errors.push({ field: 'Primary contact email', found: contactEmail, message: 'Not a valid email address. Please provide a valid email like `you@example.com`.' });
222+
if (plusCode && !isValidPlusCode(plusCode)) errors.push({ field: 'Map placement (Plus Code)', found: plusCode, message: 'Not a valid full global Plus Code. It should look like `8FW4V75V+8Q`. [Find your Plus Code →](https://plus.codes/)' });
223+
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push({ field: 'Event URL', found: eventUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
224+
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push({ field: 'Organization website', found: organizationUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
225+
if (eventFormat && !VALID_EVENT_FORMATS.has(eventFormat)) errors.push({ field: 'Event format', found: eventFormat, message: 'Not a recognized option. Please choose one of: `In person` or `Online`.' });
226+
if (organizationType && !VALID_ORG_TYPES.has(organizationType)) errors.push({ field: 'Organization type', found: organizationType, message: 'Not a recognized option. Please choose one of the valid options from the form.' });
227+
if (isOnlineEvent && !eventUrl) errors.push({ field: 'Event URL', message: 'An event URL is required for online events. Please provide the URL where people can join.' });
212228

213229
const normalizedEventName = slugify(eventName);
214230
const eventId = normalizedEventName.startsWith('pcd-')
@@ -221,14 +237,14 @@ const metadataPath = path.join(eventDirPath, 'metadata.json');
221237

222238
try {
223239
await fs.access(eventDirPath);
224-
errors.push(`An event with the generated id "${eventId}" already exists. Update the existing event instead of creating a duplicate.`);
240+
errors.push({ field: 'Event name', found: eventName, message: `An event with the generated ID \`${eventId}\` already exists. If you need to update an existing event, post in the [PCD 2026 forum thread](${PCD_FORUM_THREAD_URL}) or contact ${PCD_EMAIL}.` });
225241
} catch {
226242
// Directory does not exist yet.
227243
}
228244

229245
if (errors.length > 0) {
230246
console.log(`[process-new-event-issue] validation failed with ${errors.length} error(s):`);
231-
errors.forEach((e) => console.log(` - ${e}`));
247+
errors.forEach((e) => console.log(` - [${e.field}]${e.found !== undefined ? ` (found: "${e.found}")` : ''} ${e.message}`));
232248
const validationCommentPath = path.join(RUNNER_TEMP, `new-event-${issueNumber}-validation.md`);
233249
await fs.writeFile(validationCommentPath, buildValidationComment(errors));
234250
await setOutput('valid', 'false');

0 commit comments

Comments
 (0)