Skip to content

Commit 21c8412

Browse files
committed
chore: code review
1 parent 156d5a0 commit 21c8412

File tree

7 files changed

+146
-143
lines changed

7 files changed

+146
-143
lines changed

create-node-meeting-artifacts.mjs

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,6 @@
33
/**
44
* Node.js Meeting Artifacts Creator
55
*
6-
* Creates GitHub issues and HackMD documents for Node.js team meetings.
7-
* Reads meeting configuration from templates, fetches calendar events,
8-
* creates meeting minutes documents, and posts GitHub issues.
9-
*
10-
* Environment Variables Required:
11-
* - GITHUB_TOKEN: Personal access token for GitHub API
12-
* - HACKMD_API_TOKEN: HackMD API token for creating documents
13-
* - GOOGLE_API_KEY: Google Calendar API key for read-only calendar access
14-
*
15-
* Optional Environment Variables:
16-
* - HACKMD_TEAM_NAME: HackMD team name/path (optional)
17-
* - MEETINGS_CONFIG_DIR: Directory containing meeting templates (default: ./)
18-
*
196
* Usage:
207
* node create-node-meeting-artifacts.mjs [meetingGroup]
218
* npm run tsc-meeting
@@ -32,65 +19,82 @@ import * as meetings from './src/meeting.mjs';
3219
const config = getConfig();
3320

3421
// Step 2: Initialize Google Calendar client with API Key
35-
const calendarClient = google.createGoogleCalendarClient(config.google);
22+
const calendarClient = google.createCalendarClient(config.google);
3623

37-
// Step 3: Initialize HackMD client
38-
const hackmdClient = hackmd.createHackMDClientInstance(
39-
config.hackmd.apiToken,
40-
config.hackmd.teamName
41-
);
24+
// Step 3: Initialize GitHub client
25+
const githubClient = github.createGitHubClient(config);
26+
27+
// Step 4: Initialize HackMD client
28+
const hackmdClient = hackmd.createHackMDClient(config);
4229

43-
// Step 4: Read meeting configuration from templates
30+
// Step 5: Read meeting configuration from templates
4431
const meetingConfig = await meetings.readMeetingConfig(config);
4532

46-
// Step 5: Find next meeting event in calendar
33+
// Step 6: Find next meeting event in calendar
4734
const event = await google.findNextMeetingEvent(calendarClient, meetingConfig);
4835

49-
// Step 6: Extract meeting date from event
36+
// Step 7: Extract meeting date from event
5037
const meetingDate = new Date(event.start.dateTime);
5138

52-
// Step 7: Get Meeting Title
39+
// Step 8: Get Meeting Title
5340
const meetingTitle = meetings.generateMeetingTitle(
5441
config,
5542
meetingConfig,
5643
meetingDate
5744
);
5845

59-
// Step 8: Generate meeting issue content using native implementation
60-
const issueContent = await meetings.generateMeetingIssue(
46+
// Step 9: Get agenda information using native implementation
47+
const gitHubAgendaIssues = await github.getAgendaIssues(
48+
githubClient,
6149
config,
62-
meetingConfig,
63-
meetingDate
50+
meetingConfig
6451
);
6552

66-
// Step 9: Create GitHub issue with HackMD link
67-
const githubIssue = await github.createGitHubIssue(
68-
config,
69-
meetingConfig,
70-
meetingTitle,
71-
issueContent
53+
// Step 10: Parse meeting agenda from GitHub issues
54+
const meetingAgenda = meetings.generateMeetingAgenda(
55+
gitHubAgendaIssues,
56+
meetingConfig
7257
);
7358

74-
// Step 10: Create HackMD document with meeting notes
59+
// Step 11: Create HackMD document with meeting notes
7560
const hackmdNote = await hackmd.createMeetingNotesDocument(
7661
hackmdClient,
7762
meetingTitle,
7863
''
7964
);
8065

81-
// Step 11: Get the HackMD document link
82-
const noteLink = hackmdNote.publishLink || `https://hackmd.io/${hackmdNote.id}`;
66+
// Step 12: Get the HackMD document link
67+
const minutesDocLink =
68+
hackmdNote.publishLink || `https://hackmd.io/${hackmdNote.id}`;
69+
70+
// Step 13: Generate meeting issue content using native implementation
71+
const issueContent = await meetings.generateMeetingIssue(
72+
config,
73+
meetingConfig,
74+
meetingDate,
75+
meetingAgenda,
76+
minutesDocLink
77+
);
78+
79+
// Step 14: Create GitHub issue with HackMD link
80+
const githubIssue = await github.createGitHubIssue(
81+
githubClient,
82+
meetingConfig,
83+
meetingTitle,
84+
issueContent
85+
);
8386

84-
// Step 12: Update the minutes content with the HackMD link
87+
// Step 15: Update the minutes content with the HackMD link
8588
const minutesContent = await meetings.generateMeetingMinutes(
8689
config,
8790
meetingConfig,
8891
meetingTitle,
89-
noteLink,
92+
meetingAgenda,
93+
minutesDocLink,
9094
githubIssue.html_url
9195
);
9296

93-
// Step 13: Update the HackMD document with the self-referencing link
97+
// Step 16: Update the HackMD document with the self-referencing link
9498
await hackmd.updateMeetingNotesDocument(
9599
hackmdClient,
96100
hackmdNote.id,
@@ -99,4 +103,4 @@ await hackmd.updateMeetingNotesDocument(
99103

100104
// Output success information with links
101105
console.log(`Created GitHub issue: ${githubIssue.html_url}`);
102-
console.log(`Created HackMD document: ${noteLink}`);
106+
console.log(`Created HackMD document: ${minutesDocLink}`);

src/constants.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const DEFAULT_CONFIG = {
33
// Default GitHub organization name
44
githubOrg: 'nodejs',
55
// Default Host of the Meeting
6-
defaultHost: 'Node.js',
6+
host: 'Node.js',
77
};
88

99
// Time constants for date calculations
@@ -27,3 +27,10 @@ export const RELEVANT_TIMEZONES = [
2727
{ label: 'Tokyo', tz: 'Asia/Tokyo' },
2828
{ label: 'Sydney', tz: 'Australia/Sydney' },
2929
];
30+
31+
// Creates the default permissions for our generated docs
32+
export const HACKMD_DEFAULT_PERMISSIONS = {
33+
readPermission: 'guest',
34+
writePermission: 'signed_in',
35+
commentPermission: 'signed_in_users',
36+
};

src/github.mjs

Lines changed: 39 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,83 @@
11
import { Octokit } from '@octokit/rest';
22

3+
import { DEFAULT_CONFIG } from './constants.mjs';
4+
35
/**
4-
* Creates GitHub issue with meeting information and Google Doc link
6+
* Creates a GitHub API client
57
* @param {import('./types.d.ts').AppConfig} config - Application configuration
8+
* @returns {import('@octokit/rest').Octokit} Configured GitHub API client
9+
*/
10+
export const createGitHubClient = ({ githubToken: auth }) =>
11+
new Octokit({ auth });
12+
13+
/**
14+
* Creates GitHub issue with meeting information and Google Doc link
15+
* @param {import('@octokit/rest').Octokit} githubClient - Authenticated GitHub API client
616
* @param {import('./types.d.ts').MeetingConfig} meetingConfig - Meeting configuration object
717
* @param {string} title - Issue title
818
* @param {string} content - Issue content
919
* @returns {Promise<GitHubIssue>} Created issue data
1020
*/
1121
export const createGitHubIssue = async (
12-
config,
13-
meetingConfig,
22+
{ rest },
23+
{ properties },
1424
title,
1525
content
1626
) => {
17-
// Initialize GitHub API client with authentication token
18-
const octokit = new Octokit({ auth: config.githubToken });
27+
const githubOrg = properties.USER ?? DEFAULT_CONFIG.githubOrg;
1928

20-
// Extract issue label from config, removing quotes if present
21-
const issueLabel = meetingConfig.properties.ISSUE_LABEL;
29+
const issueLabel = properties.ISSUE_LABEL
30+
? [properties.ISSUE_LABEL]
31+
: undefined;
2232

2333
// Create the GitHub issue with meeting information
24-
const response = await octokit.rest.issues.create({
25-
// Repository information from meeting config
26-
owner: meetingConfig.properties.USER,
27-
repo: meetingConfig.properties.REPO,
34+
const response = await rest.issues.create({
35+
owner: githubOrg,
36+
repo: properties.REPO,
2837
title,
2938
body: content,
30-
// Add label if specified in config
31-
labels: issueLabel ? [issueLabel] : undefined,
39+
labels: issueLabel,
3240
});
3341

3442
return response.data;
3543
};
3644

3745
/**
3846
* Fetches GitHub issues from all repositories in an organization with a specific label
39-
* @param {string} token - GitHub personal access token
40-
* @param {string} org - GitHub organization name (e.g., 'nodejs')
41-
* @param {string} label - Label to filter by (e.g., 'tsc-agenda')
42-
* @returns {Promise<string>} Formatted markdown string of issues
47+
* @param {import('@octokit/rest').Octokit} githubClient - Authenticated GitHub API client
48+
* @param {import('./types.d.ts').AppConfig} config - Application configuration
49+
* @param {import('./types.d.ts').MeetingConfig} meetingConfig - Meeting configuration
50+
* @returns {Promise<{ repoName: string, issues: Array<GitHubIssue> }> } Formatted markdown string of issues
4351
*/
44-
export const fetchAgendaIssues = async (token, org, label) => {
45-
const octokit = new Octokit({ auth: token });
52+
export const getAgendaIssues = async (
53+
{ paginate, rest },
54+
{ meetingGroup },
55+
{ properties }
56+
) => {
57+
const githubOrg = properties.USER ?? DEFAULT_CONFIG.githubOrg;
58+
const agendaTag = properties.AGENDA_TAG ?? `${meetingGroup}-agenda`;
4659

4760
// Get all public repositories in the organization
48-
const repos = await octokit.paginate(octokit.rest.repos.listForOrg, {
49-
org,
61+
const repos = await paginate(rest.repos.listForOrg, {
62+
org: properties.USER,
5063
type: 'public',
5164
per_page: 100,
5265
});
5366

5467
// Fetch issues from all repositories concurrently
5568
const issuePromises = repos.map(async repo => {
56-
const issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
57-
owner: org,
69+
const issues = await paginate(rest.issues.listForRepo, {
70+
owner: githubOrg,
5871
repo: repo.name,
59-
labels: label,
72+
labels: agendaTag,
6073
state: 'open',
6174
per_page: 100,
6275
});
6376

64-
const filteredIssues = issues.filter(issue => !issue.pull_request); // Exclude PRs
77+
const filteredIssues = issues.filter(({ pull_request }) => !pull_request); // Exclude PRs
6578

6679
return { repoName: repo.name, issues: filteredIssues };
6780
});
6881

69-
const repoIssues = await Promise.all(issuePromises);
70-
71-
// Format issues as markdown
72-
let agendaMarkdown = '';
73-
74-
repoIssues.forEach(({ repoName, issues }) => {
75-
if (issues.length > 0) {
76-
agendaMarkdown += `### ${org}/${repoName}\n\n`;
77-
78-
issues.forEach(issue => {
79-
// Escape markdown characters in title
80-
const cleanTitle = issue.title.replace(/([[\]])/g, '\\$1');
81-
82-
agendaMarkdown += `* ${cleanTitle} [#${issue.number}](${issue.html_url})\n`;
83-
});
84-
85-
agendaMarkdown += '\n';
86-
}
87-
});
88-
89-
return agendaMarkdown.trim();
82+
return Promise.all(issuePromises);
9083
};

src/google.mjs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,8 @@ import { TIME_CONSTANTS } from './constants.mjs';
77
* @param {import('./types.d.ts').GoogleConfig} gConfig - Google configuration object
88
* @returns {CalendarClient} Authenticated Google Calendar client
99
*/
10-
export const createGoogleCalendarClient = gConfig => {
11-
// Use API Key authentication (simpler and more straightforward)
12-
if (gConfig.apiKey) {
13-
return calendar({
14-
version: 'v3',
15-
auth: gConfig.apiKey,
16-
});
17-
}
18-
19-
throw new Error('Google API Key is required for Google Calendar access');
20-
};
10+
export const createCalendarClient = ({ apiKey: auth }) =>
11+
calendar({ version: 'v3', auth });
2112

2213
/**
2314
* Finds the next meeting event in Google Calendar within the next week

src/hackmd.mjs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
import HackMDAPI from '@hackmd/api';
22

3-
/**
4-
* Creates the default permissions for our generated docs
5-
* @type {Pick<import('@hackmd/api/dist/type.d.ts').CreateNoteOptions, 'readPermission' | 'writePermission' | 'commentPermission'>} */
6-
const hackMdPermissions = {
7-
readPermission: 'guest', // Allow signed-in users to read
8-
writePermission: 'signed_in', // Allow signed-in users to write
9-
commentPermission: 'signed_in_users', // Allow signed-in users to comment
10-
};
3+
import { HACKMD_DEFAULT_PERMISSIONS } from './constants.mjs';
114

125
/**
136
* Creates a HackMD API client
14-
* @param {string} apiToken - HackMD API token
15-
* @param {string} teamPath - HackMD team path/name (optional)
7+
* @param {import('./types.d.ts').AppConfig} config - Application configuration
168
* @returns {HackMDClient} Configured HackMD API client
179
*/
18-
export const createHackMDClientInstance = (apiToken, teamPath) => {
10+
export const createHackMDClient = ({ hackmd: { apiToken, teamName } }) => {
1911
// Use team-specific API endpoint if teamPath is provided
20-
const baseURL = teamPath
21-
? `https://api.hackmd.io/v1/teams/${teamPath}`
12+
const baseURL = teamName
13+
? `https://api.hackmd.io/v1/teams/${teamName}`
2214
: 'https://api.hackmd.io/v1';
2315

2416
return new HackMDAPI(apiToken, baseURL);
@@ -31,10 +23,8 @@ export const createHackMDClientInstance = (apiToken, teamPath) => {
3123
* @param {string} content - Document content in Markdown
3224
* @returns {Promise<HackMDNote>} Created note data with ID and URLs
3325
*/
34-
export const createMeetingNotesDocument = (hackmdClient, title, content) => {
35-
// Create a new note with the meeting content
36-
return hackmdClient.createNote({ title, content, ...hackMdPermissions });
37-
};
26+
export const createMeetingNotesDocument = (hackmdClient, title, content) =>
27+
hackmdClient.createNote({ title, content, ...HACKMD_DEFAULT_PERMISSIONS });
3828

3929
/**
4030
* Updates an existing meeting notes document in HackMD

0 commit comments

Comments
 (0)