Skip to content

Commit ae3e30b

Browse files
committed
feat: enhance discussion copying by preserving reactions, locked status, and pinned indicators
1 parent eb3f21a commit ae3e30b

File tree

2 files changed

+145
-11
lines changed

2 files changed

+145
-11
lines changed

scripts/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Features:
6060
- Creates labels in the target repository if they don't exist
6161
- Copies all comments and threaded replies with proper attribution
6262
- Copies poll results as static snapshots (with table and optional Mermaid chart)
63+
- Preserves reaction counts on discussions, comments, and replies
64+
- Maintains locked status of discussions
65+
- Indicates pinned discussions with a visual indicator
6366
- Handles rate limiting with exponential backoff
6467
- Provides colored console output for better visibility
6568

@@ -72,6 +75,9 @@ Notes:
7275
- If a category doesn't exist in the target repository, discussions will be created in the "General" category
7376
- The script preserves discussion metadata by adding attribution text to the body and comments
7477
- Poll results are copied as static snapshots - voting is not available in copied discussions
78+
- Reactions are copied as read-only summaries (users cannot add new reactions)
79+
- Locked discussions will be locked in the target repository
80+
- Pinned status is indicated in the discussion body (GitHub API doesn't allow pinning via GraphQL)
7581
- Both source and target repositories must have GitHub Discussions enabled
7682

7783
## delete-branch-protection-rules.ps1

scripts/copy-discussions.js

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,33 @@ function formatPollData(poll) {
139139
return pollMarkdown;
140140
}
141141

142+
function formatReactions(reactionGroups) {
143+
if (!reactionGroups || reactionGroups.length === 0) {
144+
return '';
145+
}
146+
147+
const reactionMap = {
148+
'THUMBS_UP': '👍',
149+
'THUMBS_DOWN': '👎',
150+
'LAUGH': '😄',
151+
'HOORAY': '🎉',
152+
'CONFUSED': '😕',
153+
'HEART': '❤️',
154+
'ROCKET': '🚀',
155+
'EYES': '👀'
156+
};
157+
158+
const formattedReactions = reactionGroups
159+
.filter(group => group.users.totalCount > 0)
160+
.map(group => {
161+
const emoji = reactionMap[group.content] || group.content;
162+
return `${emoji} ${group.users.totalCount}`;
163+
})
164+
.join(' | ');
165+
166+
return formattedReactions ? `\n\n**Reactions:** ${formattedReactions}` : '';
167+
}
168+
142169
// GraphQL Queries and Mutations
143170
const CHECK_DISCUSSIONS_ENABLED_QUERY = `
144171
query($owner: String!, $repo: String!) {
@@ -229,6 +256,19 @@ const FETCH_DISCUSSIONS_QUERY = `
229256
}
230257
}
231258
}
259+
reactionGroups {
260+
content
261+
users {
262+
totalCount
263+
}
264+
}
265+
}
266+
}
267+
pinnedDiscussions(first: 100) {
268+
nodes {
269+
discussion {
270+
id
271+
}
232272
}
233273
}
234274
}
@@ -248,6 +288,12 @@ const FETCH_DISCUSSION_COMMENTS_QUERY = `
248288
}
249289
createdAt
250290
upvoteCount
291+
reactionGroups {
292+
content
293+
users {
294+
totalCount
295+
}
296+
}
251297
replies(first: 50) {
252298
nodes {
253299
id
@@ -257,6 +303,12 @@ const FETCH_DISCUSSION_COMMENTS_QUERY = `
257303
}
258304
createdAt
259305
upvoteCount
306+
reactionGroups {
307+
content
308+
users {
309+
totalCount
310+
}
311+
}
260312
}
261313
}
262314
}
@@ -362,6 +414,18 @@ const CLOSE_DISCUSSION_MUTATION = `
362414
}
363415
`;
364416

417+
const LOCK_DISCUSSION_MUTATION = `
418+
mutation($discussionId: ID!) {
419+
lockLockable(input: {
420+
lockableId: $discussionId
421+
}) {
422+
lockedRecord {
423+
locked
424+
}
425+
}
426+
}
427+
`;
428+
365429
const MARK_DISCUSSION_COMMENT_AS_ANSWER_MUTATION = `
366430
mutation($commentId: ID!) {
367431
markDiscussionCommentAsAnswer(input: {
@@ -552,17 +616,28 @@ async function addLabelsToDiscussion(octokit, discussionId, labelIds) {
552616
}
553617
}
554618

555-
async function createDiscussion(octokit, repositoryId, categoryId, title, body, sourceUrl, sourceAuthor, sourceCreated, poll = null) {
619+
async function createDiscussion(octokit, repositoryId, categoryId, title, body, sourceUrl, sourceAuthor, sourceCreated, poll = null, locked = false, isPinned = false, reactionGroups = []) {
556620
let enhancedBody = body;
557621

622+
// Add pinned indicator if discussion was pinned
623+
if (isPinned) {
624+
enhancedBody = `📌 _This discussion was pinned in the source repository_\n\n${enhancedBody}`;
625+
}
626+
627+
// Add reactions if present
628+
const reactionsMarkdown = formatReactions(reactionGroups);
629+
if (reactionsMarkdown) {
630+
enhancedBody += reactionsMarkdown;
631+
}
632+
558633
// Add poll data if present
559634
if (poll) {
560635
const pollMarkdown = formatPollData(poll);
561636
enhancedBody += pollMarkdown;
562637
}
563638

564639
// Add metadata
565-
enhancedBody += `\n\n---\n<details>\n<summary><i>Original discussion metadata</i></summary>\n\n_Original discussion by @${sourceAuthor} on ${sourceCreated}_\n_Source: ${sourceUrl}_\n</details>`;
640+
enhancedBody += `\n\n---\n<details>\n<summary><i>Original discussion metadata</i></summary>\n\n_Original discussion by @${sourceAuthor} on ${sourceCreated}_\n_Source: ${sourceUrl}_\n${locked ? '\n_🔒 This discussion was locked in the source repository_' : ''}\n</details>`;
566641

567642
log(`Creating discussion: '${title}'`);
568643

@@ -576,13 +651,35 @@ async function createDiscussion(octokit, repositoryId, categoryId, title, body,
576651
body: enhancedBody
577652
});
578653

579-
return response.createDiscussion.discussion;
654+
const newDiscussion = response.createDiscussion.discussion;
655+
656+
// Lock the discussion if it was locked in the source
657+
if (locked) {
658+
await lockDiscussion(octokit, newDiscussion.id);
659+
}
660+
661+
return newDiscussion;
580662
} catch (err) {
581663
error(`Failed to create discussion: ${err.message}`);
582664
throw err;
583665
}
584666
}
585667

668+
async function lockDiscussion(octokit, discussionId) {
669+
log(`Locking discussion ${discussionId}...`);
670+
671+
await rateLimitSleep(2);
672+
673+
try {
674+
await octokit.graphql(LOCK_DISCUSSION_MUTATION, {
675+
discussionId
676+
});
677+
log(`Discussion locked successfully`);
678+
} catch (err) {
679+
error(`Failed to lock discussion: ${err.message}`);
680+
}
681+
}
682+
586683
async function fetchDiscussionComments(octokit, discussionId) {
587684
log(`Fetching comments for discussion ${discussionId}...`);
588685

@@ -600,8 +697,16 @@ async function fetchDiscussionComments(octokit, discussionId) {
600697
}
601698
}
602699

603-
async function addDiscussionComment(octokit, discussionId, body, originalAuthor, originalCreated) {
604-
const enhancedBody = `${body}\n\n---\n<details>\n<summary><i>Original comment metadata</i></summary>\n\n_Original comment by @${originalAuthor} on ${originalCreated}_\n</details>`;
700+
async function addDiscussionComment(octokit, discussionId, body, originalAuthor, originalCreated, reactionGroups = []) {
701+
let enhancedBody = body;
702+
703+
// Add reactions if present
704+
const reactionsMarkdown = formatReactions(reactionGroups);
705+
if (reactionsMarkdown) {
706+
enhancedBody += reactionsMarkdown;
707+
}
708+
709+
enhancedBody += `\n\n---\n<details>\n<summary><i>Original comment metadata</i></summary>\n\n_Original comment by @${originalAuthor} on ${originalCreated}_\n</details>`;
605710

606711
log("Adding comment to discussion");
607712

@@ -622,8 +727,16 @@ async function addDiscussionComment(octokit, discussionId, body, originalAuthor,
622727
}
623728
}
624729

625-
async function addDiscussionCommentReply(octokit, discussionId, replyToId, body, originalAuthor, originalCreated) {
626-
const enhancedBody = `${body}\n\n---\n_Original reply by @${originalAuthor} on ${originalCreated}_`;
730+
async function addDiscussionCommentReply(octokit, discussionId, replyToId, body, originalAuthor, originalCreated, reactionGroups = []) {
731+
let enhancedBody = body;
732+
733+
// Add reactions if present
734+
const reactionsMarkdown = formatReactions(reactionGroups);
735+
if (reactionsMarkdown) {
736+
enhancedBody += reactionsMarkdown;
737+
}
738+
739+
enhancedBody += `\n\n---\n_Original reply by @${originalAuthor} on ${originalCreated}_`;
627740

628741
log(`Adding reply to comment ${replyToId}`);
629742

@@ -707,7 +820,8 @@ async function copyDiscussionComments(octokit, discussionId, comments, answerCom
707820
discussionId,
708821
comment.body,
709822
author,
710-
createdAt
823+
createdAt,
824+
comment.reactionGroups || []
711825
);
712826

713827
if (newCommentId) {
@@ -735,7 +849,8 @@ async function copyDiscussionComments(octokit, discussionId, comments, answerCom
735849
newCommentId,
736850
reply.body,
737851
replyAuthor,
738-
replyCreated
852+
replyCreated,
853+
reply.reactionGroups || []
739854
);
740855
}
741856
}
@@ -800,16 +915,29 @@ async function processDiscussionsPage(sourceOctokit, targetOctokit, owner, repo,
800915
discussion.url,
801916
discussion.author?.login || "unknown",
802917
discussion.createdAt,
803-
discussion.poll || null
918+
discussion.poll || null,
919+
discussion.locked || false,
920+
discussion.isPinned || false,
921+
discussion.reactionGroups || []
804922
);
805923

806924
createdDiscussions++;
807925
log(`✓ Created discussion #${discussion.number}: '${discussion.title}'`);
808926

809-
// Log poll info if present
927+
// Log additional metadata info
810928
if (discussion.poll && discussion.poll.options?.nodes?.length > 0) {
811929
log(` ℹ️ Poll included with ${discussion.poll.options.nodes.length} options (${discussion.poll.totalVoteCount} total votes)`);
812930
}
931+
if (discussion.locked) {
932+
log(` 🔒 Discussion was locked in source and has been locked in target`);
933+
}
934+
if (discussion.isPinned) {
935+
log(` 📌 Discussion was pinned in source (indicator added to body)`);
936+
}
937+
const totalReactions = discussion.reactionGroups?.reduce((sum, group) => sum + (group.users.totalCount || 0), 0) || 0;
938+
if (totalReactions > 0) {
939+
log(` ❤️ ${totalReactions} reaction${totalReactions !== 1 ? 's' : ''} copied`);
940+
}
813941

814942
// Process labels
815943
if (discussion.labels.nodes.length > 0) {

0 commit comments

Comments
 (0)