|
| 1 | +--- |
| 2 | +title: 'GitHub Discussions Migration Utility' |
| 3 | +author: Josh Johanning |
| 4 | +date: 2025-10-07 12:00:00 -0500 |
| 5 | +description: A utility to migrate GitHub Discussions between repositories, including categories, labels, comments, and replies |
| 6 | +categories: [GitHub, Migrations] |
| 7 | +tags: [GitHub, Scripts, GitHub Discussions, Migrations, Node.js] |
| 8 | +media_subpath: /assets/screenshots/2025-10-07-github-discussions-migration-utility |
| 9 | +image: |
| 10 | + path: github-discussions-light.png |
| 11 | + width: 100% |
| 12 | + height: 100% |
| 13 | + alt: GitHub Discussions in a repository |
| 14 | +--- |
| 15 | + |
| 16 | +## Overview |
| 17 | + |
| 18 | +GitHub Discussions are a great way to facilitate conversations within your repository or organization, but what happens when you need to move them? Whether you're migrating between GitHub instances (server to cloud, cloud to cloud, etc.), moving discussions to a different organization, or consolidating repositories across organizations, moving discussions manually can be a tedious process. Today, the GitHub Enterprise Importer (GEI) [does not support](https://docs.github.com/en/enterprise-cloud@latest/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products#data-that-is-not-migrated-1) migrating discussions, so I built a [Node.js utility](https://github.com/joshjohanning/github-misc-scripts/blob/main/scripts/migrate-discussions/migrate-discussions.js) ([README](https://github.com/joshjohanning/github-misc-scripts/blob/main/scripts/migrate-discussions/README.md)) to help with this task. |
| 19 | + |
| 20 | +The utility handles discussions, comments, replies, categories, labels, and even preserves metadata like reactions and poll results. It includes intelligent rate limiting and resume capabilities to handle large migrations reliably. I should note, "reliably" does not mean quickly - GitHub has strict rate limits on content-creating operations to prevent abuse, so patience is key when migrating large numbers of discussions. (See [rate limiting details](#rate-limiting-details) below for more information.) |
| 21 | + |
| 22 | +> See my [GitHub Migration Tools Collection](/posts/github-migration-tools/) post. |
| 23 | +{: .prompt-info } |
| 24 | + |
| 25 | +> If you're moving discussions within the same organization, use GitHub's native [transfer discussion](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-discussions#transferring-a-discussion) feature instead. This utility is best for cross-organization or cross-instance migrations, or when you need to automate transfers (since there's no GraphQL endpoint for the native transfer feature). |
| 26 | +{: .prompt-tip } |
| 27 | + |
| 28 | +## Running the Script |
| 29 | + |
| 30 | +### Prerequisites |
| 31 | + |
| 32 | +1. A clone of my [github-misc-scripts](https://github.com/joshjohanning/github-misc-scripts) repo |
| 33 | +2. Node.js installed |
| 34 | +3. Both source and target repositories must have GitHub Discussions enabled |
| 35 | +4. GitHub tokens with appropriate permissions: |
| 36 | + - Source token: `repo` scope with read access to discussions |
| 37 | + - Target token: `repo` scope with write access to discussions |
| 38 | + - GitHub App tokens recommended for better rate limits (but note they expire after 1 hour - see [Future Improvements](#future-improvements)) |
| 39 | +5. Dependencies installed via `npm i` |
| 40 | + |
| 41 | +You can generate a GitHub App token using the [`gh-token`](https://github.com/Link-/gh-token) extension: |
| 42 | + |
| 43 | +```bash |
| 44 | +export SOURCE_TOKEN=$(gh token generate --app-id YOUR_APP_ID --installation-id YOUR_INSTALLATION_ID --key /path/to/private-key.pem --token-only) |
| 45 | +export TARGET_TOKEN=$(gh token generate --app-id YOUR_APP_ID --installation-id YOUR_INSTALLATION_ID --key /path/to/private-key.pem --token-only) |
| 46 | +``` |
| 47 | +{: .nolineno} |
| 48 | + |
| 49 | +### Usage |
| 50 | + |
| 51 | +You can call the script via: |
| 52 | + |
| 53 | +```bash |
| 54 | +cd github-misc-scripts |
| 55 | +export SOURCE_TOKEN=ghp_abc |
| 56 | +export TARGET_TOKEN=ghp_xyz |
| 57 | +cd ./scripts/migrate-discussions |
| 58 | +npm i |
| 59 | +node ./migrate-discussions.js source-org source-repo target-org target-repo |
| 60 | +``` |
| 61 | +{: .nolineno} |
| 62 | + |
| 63 | +To resume from a specific discussion number (useful if interrupted): |
| 64 | + |
| 65 | +```bash |
| 66 | +node ./migrate-discussions.js source-org source-repo target-org target-repo --start-from 50 |
| 67 | +``` |
| 68 | +{: .nolineno} |
| 69 | + |
| 70 | +### Example |
| 71 | + |
| 72 | +An example of this in practice: |
| 73 | + |
| 74 | +```bash |
| 75 | +export SOURCE_TOKEN=ghp_abc |
| 76 | +export TARGET_TOKEN=ghp_xyz |
| 77 | +cd ./scripts/migrate-discussions |
| 78 | +npm i |
| 79 | +node ./migrate-discussions.js joshjohanning-org discussions-source joshjohanning-org discussions-target |
| 80 | +``` |
| 81 | +{: .nolineno} |
| 82 | + |
| 83 | +For GitHub Enterprise Server instances, set the API URL environment variables: |
| 84 | + |
| 85 | +```bash |
| 86 | +export SOURCE_API_URL=https://github.mycompany.com/api/v3 |
| 87 | +export TARGET_API_URL=https://api.github.com |
| 88 | +export SOURCE_TOKEN=ghp_abc |
| 89 | +export TARGET_TOKEN=ghp_xyz |
| 90 | +node ./migrate-discussions.js source-org source-repo target-org target-repo |
| 91 | +``` |
| 92 | +{: .nolineno} |
| 93 | + |
| 94 | +## Features |
| 95 | + |
| 96 | +The script migrates discussions with comprehensive support for: |
| 97 | + |
| 98 | +### Content Migration |
| 99 | + |
| 100 | +- **Discussion categories** - Automatically creates missing categories in the target repository (or uses "General" as fallback) |
| 101 | +- **Labels** - Creates labels in the target repository if they don't exist |
| 102 | +- **Comments and replies** - Copies all comments and threaded replies with proper attribution |
| 103 | +- **Poll results** - Copies poll results as static snapshots with tables and optional Mermaid charts |
| 104 | +- **Reactions** - Preserves reaction counts on discussions, comments, and replies |
| 105 | +- **Discussion states** - Maintains locked and closed status |
| 106 | +- **Answered discussions** - Marks answered discussions and preserves the accepted answer |
| 107 | +- **Pinned discussions** - Indicates pinned discussions with a visual indicator (GraphQL API doesn't support pinning) |
| 108 | + |
| 109 | +### Rate Limiting and Reliability |
| 110 | + |
| 111 | +- **Automatic rate limit handling** - Uses Octokit's built-in throttling plugin with retry logic |
| 112 | +- **GitHub-recommended delays** - Waits 3 seconds between discussions/comments to stay under secondary rate limits |
| 113 | +- **Resume capability** - Use `--start-from <number>` to resume from a specific discussion if interrupted |
| 114 | +- **Configurable retries** - Retries up to 15 times for both rate-limit and non-rate-limit errors |
| 115 | + |
| 116 | +## Summary Output |
| 117 | + |
| 118 | +After completion, the script displays comprehensive statistics: |
| 119 | + |
| 120 | +```text |
| 121 | +[2025-10-02 19:38:44] ============================================================ |
| 122 | +[2025-10-02 19:38:44] Discussion copy completed! |
| 123 | +[2025-10-02 19:38:44] Total discussions found: 10 |
| 124 | +[2025-10-02 19:38:44] Discussions created: 10 |
| 125 | +[2025-10-02 19:38:44] Discussions skipped: 0 |
| 126 | +[2025-10-02 19:38:44] Total comments found: 9 |
| 127 | +[2025-10-02 19:38:44] Comments copied: 9 |
| 128 | +[2025-10-02 19:38:44] Primary rate limits hit: 0 |
| 129 | +[2025-10-02 19:38:44] Secondary rate limits hit: 0 |
| 130 | +[2025-10-02 19:38:44] WARNING: |
| 131 | +The following categories were missing and need to be created manually: |
| 132 | +[2025-10-02 19:38:44] WARNING: - Blog posts! |
| 133 | +[2025-10-02 19:38:44] WARNING: |
| 134 | +[2025-10-02 19:38:44] WARNING: To create categories manually: |
| 135 | +[2025-10-02 19:38:44] WARNING: 1. Go to https://github.com/joshjohanning-emu/discussions-test/discussions |
| 136 | +[2025-10-02 19:38:44] WARNING: 2. Click 'New discussion' |
| 137 | +[2025-10-02 19:38:44] WARNING: 3. Look for category management options |
| 138 | +[2025-10-02 19:38:44] WARNING: 4. Create the missing categories with appropriate names and descriptions |
| 139 | +[2025-10-02 19:38:44] |
| 140 | +All done! ✨ |
| 141 | +``` |
| 142 | + |
| 143 | +## Limitations & Notes |
| 144 | + |
| 145 | +### What It Doesn't Do |
| 146 | + |
| 147 | +While the utility handles most discussion migration scenarios, there are some limitations: |
| 148 | + |
| 149 | +- **Discussion categories** - Cannot create categories via API; must be created manually in the target repository beforehand (discussions will use "General" as fallback) |
| 150 | +- **Pinning discussions** - GitHub API doesn't allow pinning via GraphQL; pinned status is indicated in the discussion body |
| 151 | +- **Live polls** - Poll results are copied as static snapshots; users cannot vote on migrated polls |
| 152 | +- **Attachments** - Images and files referenced in discussions may not copy over and require manual handling |
| 153 | +- **Reactions** - Copied as read-only summaries; users cannot add new reactions to migrated content |
| 154 | + |
| 155 | +### Category Handling |
| 156 | + |
| 157 | +- Discussion categories must exist in the target repository before running the script |
| 158 | +- If a category doesn't exist, discussions will be created in the "General" category as a fallback |
| 159 | +- Missing categories are tracked and reported at the end of the script |
| 160 | + |
| 161 | +### Rate Limiting Details |
| 162 | + |
| 163 | +GitHub limits content-generating requests to avoid abuse: |
| 164 | + |
| 165 | +- No more than 80 content-generating requests per minute |
| 166 | +- No more than 500 content-generating requests per hour |
| 167 | + |
| 168 | +The script automatically handles this by: |
| 169 | + |
| 170 | +- Staying under 1 discussion or comment created every 3 seconds (GitHub's recommendation) |
| 171 | +- Automatic retry with wait times from GitHub's `retry-after` headers |
| 172 | +- Retrying up to 15 times if rate limits are consistently hit |
| 173 | + |
| 174 | +### Configuration Options |
| 175 | + |
| 176 | +You can edit these constants at the top of the script to customize behavior: |
| 177 | + |
| 178 | +- `INCLUDE_POLL_MERMAID_CHART` - Set to `false` to disable Mermaid pie charts for polls (default: `true`) |
| 179 | +- `RATE_LIMIT_SLEEP_SECONDS` - Sleep duration between API calls (default: `0.5` seconds) |
| 180 | +- `DISCUSSION_PROCESSING_DELAY_SECONDS` - Delay between processing discussions/comments (default: `3` seconds) |
| 181 | +- `MAX_RETRIES` - Maximum retries for both rate-limit and non-rate-limit errors (default: `15`) |
| 182 | + |
| 183 | +### Content Preservation |
| 184 | + |
| 185 | +- The script adds attribution text to preserve original author and timestamp information (the API doesn't allow setting creation date or author metadata - all migrated content will show as created by the token user) |
| 186 | +- Poll results are copied as static snapshots - voting is not available in copied discussions |
| 187 | +- Reactions are copied as read-only summaries |
| 188 | +- Locked discussions will be locked in the target repository |
| 189 | +- Closed discussions will be closed in the target repository |
| 190 | +- Answered discussions will have the same comment marked as the answer |
| 191 | + |
| 192 | +## Future Improvements |
| 193 | + |
| 194 | +- [ ] Native support for GitHub Apps and ability to fetch new tokens before they expire in an hour |
| 195 | + |
| 196 | +## Summary |
| 197 | + |
| 198 | +This utility makes it possible to migrate GitHub Discussions between repositories, whether you're moving between GitHub instances or just consolidating your discussions. The script handles the heavy lifting of copying discussions, comments, and metadata while respecting GitHub's rate limits. |
| 199 | + |
| 200 | +Drop a comment here or an issue or PR on my [github-misc-scripts repo](https://github.com/joshjohanning/github-misc-scripts/tree/main/scripts/migrate-discussions) if you have any feedback or suggestions! Happy migrating! 🚀 |
0 commit comments