Created
October 17, 2025 19:56
-
-
Save RikkiGibson/db7b986a3f4304e1581d54aec51f04c5 to your computer and use it in GitHub Desktop.
Unsubscribes from issues that were closed as unplanned. Ripped largely from https://gist.github.com/jaredpar/1605aad9c6b8869c8eaf8868b6c73c25.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // File-based app which unsubscribes from GitHub issues in particular repo which were closed as not planned. | |
| #:package [email protected] | |
| #:package [email protected] | |
| #:package [email protected] | |
| using System.Runtime.CompilerServices; | |
| using Humanizer; | |
| using Microsoft.Extensions.Configuration; | |
| using Octokit; | |
| var config = new ConfigurationBuilder() | |
| .AddUserSecrets<Program>() | |
| .Build(); | |
| string? token = config["GitHub.Token"]; | |
| if (string.IsNullOrEmpty(token)) | |
| { | |
| Console.WriteLine("Required GitHub access token was not found."); | |
| Console.WriteLine("Go to https://github.com/settings/tokens and generate a **classic PAT** with repo/public and notifications scopes."); | |
| var filePath = Path.GetRelativePath(Environment.CurrentDirectory, GetCurrentFilePath()); | |
| Console.WriteLine($"""Then run command 'dotnet user-secrets set GitHub.Token "<pat>" --file "{filePath}"'."""); | |
| return 1; | |
| } | |
| var client = new GitHubClient(new ProductHeaderValue("RikkiGibson.UnsubscribeUnplannedIssues")) | |
| { | |
| Credentials = new Credentials(token) | |
| }; | |
| var user = await client.User.Current(); | |
| Console.WriteLine($"Authenticated as: {user.Login}"); | |
| await MarkUnplannedAsDone(client); | |
| return 0; | |
| string GetCurrentFilePath([CallerFilePath] string? path = null) => path ?? throw new InvalidOperationException(); | |
| static async Task MarkUnplannedAsDone(GitHubClient client) | |
| { | |
| var notificationsClient = client.Activity.Notifications; | |
| Console.WriteLine("List a repository to process (e.g., owner/repo):"); | |
| var line = Console.ReadLine()!; | |
| string owner, repo; | |
| if (line.Contains("/")) | |
| { | |
| var parts = line.Split(['/'], 2); | |
| owner = parts[0]; | |
| repo = parts[1]; | |
| } | |
| else | |
| { | |
| owner = "dotnet"; | |
| repo = line; | |
| } | |
| var apiOptions = new ApiOptions | |
| { | |
| PageSize = 10, | |
| PageCount = 1, | |
| StartPage = 1 | |
| }; | |
| var request = new NotificationsRequest() | |
| { | |
| Since = DateTimeOffset.Now.AddDays(-30) | |
| }; | |
| while (true) | |
| { | |
| var notifications = await notificationsClient.GetAllForRepository(owner, repo, request, apiOptions); | |
| if (notifications.Count == 0) | |
| { | |
| Console.WriteLine("No more notifications to process."); | |
| break; | |
| } | |
| foreach (var notification in notifications) | |
| { | |
| if (notification.Subject.Type != "Issue") | |
| { | |
| continue; | |
| } | |
| var issueNumber = int.Parse(notification.Subject.Url.Split('/').Last()); | |
| var issue = await client.Issue.Get(owner, repo, issueNumber); | |
| if (issue.StateReason == ItemStateReason.NotPlanned) | |
| { | |
| Console.WriteLine($"Marking notification as done: #{issue.Number} (opened {issue.CreatedAt.Humanize()}) - {issue.Title} ({issue.HtmlUrl})"); | |
| await MarkAsDone(notification.Id); | |
| } | |
| } | |
| apiOptions.StartPage++; | |
| } | |
| async Task MarkAsDone(string threadId) | |
| { | |
| var endPoint = new Uri($"notifications/threads/{threadId}", UriKind.Relative); | |
| var result = await client.Connection.Delete<object>( | |
| endPoint, | |
| null, | |
| "application/vnd.github+json"); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment