Skip to content

Instantly share code, notes, and snippets.

@RikkiGibson
Created October 17, 2025 19:56
Show Gist options
  • Select an option

  • Save RikkiGibson/db7b986a3f4304e1581d54aec51f04c5 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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