Skip to content

Commit b44fbc6

Browse files
konardclaude
andcommitted
Add Codacy badge update trigger for GitHub Bot
Implements automatic detection and update of broken or outdated Codacy badges in README.md files. The trigger: - Detects issues with titles containing "codacy", "badge", and "update" - Extracts existing project IDs from Codacy badges or generates consistent ones - Removes duplicate/broken badges and replaces with standardized format - Updates to modern URL format with proper UTM parameters - Commits changes with "Codacy badge is updated." message 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 8daa26e commit b44fbc6

File tree

2 files changed

+135
-1
lines changed

2 files changed

+135
-1
lines changed

csharp/Platform.Bot/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private static async Task<int> Main(string[] args)
9595
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
9696
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
9797
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
98-
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
98+
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new CodacyBadgeUpdateTrigger(githubStorage), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
9999
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100100
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101101
var cancellation = new CancellationTokenSource();
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.Threading.Tasks;
2+
using Interfaces;
3+
using Octokit;
4+
using Storage.Remote.GitHub;
5+
using System.Text.RegularExpressions;
6+
using System;
7+
using System.Linq;
8+
using System.Security.Cryptography;
9+
10+
namespace Platform.Bot.Triggers
11+
{
12+
using TContext = Issue;
13+
/// <summary>
14+
/// <para>
15+
/// Represents the Codacy badge update trigger.
16+
/// </para>
17+
/// <para></para>
18+
/// </summary>
19+
/// <seealso cref="ITrigger{TContext}"/>
20+
internal class CodacyBadgeUpdateTrigger : ITrigger<TContext>
21+
{
22+
private readonly GitHubStorage _storage;
23+
24+
/// <summary>
25+
/// <para>
26+
/// Initializes a new <see cref="CodacyBadgeUpdateTrigger"/> instance.
27+
/// </para>
28+
/// <para></para>
29+
/// </summary>
30+
/// <param name="storage">
31+
/// <para>A git hub storage.</para>
32+
/// <para></para>
33+
/// </param>
34+
public CodacyBadgeUpdateTrigger(GitHubStorage storage)
35+
{
36+
this._storage = storage;
37+
}
38+
39+
/// <summary>
40+
/// <para>
41+
/// Actions the context.
42+
/// </para>
43+
/// <para></para>
44+
/// </summary>
45+
/// <param name="context">
46+
/// <para>The context.</para>
47+
/// <para></para>
48+
/// </param>
49+
public async Task Action(TContext context)
50+
{
51+
var repository = context.Repository;
52+
var readmeContent = await GetReadmeContent(repository);
53+
54+
if (readmeContent == null) return;
55+
56+
var updatedContent = UpdateCodacyBadges(readmeContent, repository.Owner.Login, repository.Name);
57+
58+
if (updatedContent != readmeContent)
59+
{
60+
await _storage.CreateOrUpdateFile(updatedContent, repository, repository.DefaultBranch, "README.md", "Codacy badge is updated.");
61+
}
62+
63+
_storage.CloseIssue(context);
64+
}
65+
66+
/// <summary>
67+
/// <para>
68+
/// Determines whether this instance condition.
69+
/// </para>
70+
/// <para></para>
71+
/// </summary>
72+
/// <param name="context">
73+
/// <para>The context.</para>
74+
/// <para></para>
75+
/// </param>
76+
/// <returns>
77+
/// <para>The bool</para>
78+
/// <para></para>
79+
/// </returns>
80+
public async Task<bool> Condition(TContext context) =>
81+
context.Title.ToLower().Contains("codacy") &&
82+
context.Title.ToLower().Contains("badge") &&
83+
context.Title.ToLower().Contains("update");
84+
85+
private async Task<string?> GetReadmeContent(Repository repository)
86+
{
87+
try
88+
{
89+
var readmeFiles = await _storage.Client.Repository.Content.GetAllContents(repository.Id, "README.md");
90+
return readmeFiles.FirstOrDefault()?.Content;
91+
}
92+
catch
93+
{
94+
return null;
95+
}
96+
}
97+
98+
private string UpdateCodacyBadges(string content, string owner, string repositoryName)
99+
{
100+
// Pattern to match Codacy badges - matches both old and new formats
101+
var codacyBadgePattern = @"\[\!\[Codacy Badge\]\(https://api\.codacy\.com/project/badge/Grade/[a-f0-9]+\)\]\(https://[^)]+\)";
102+
103+
// Find all Codacy badges
104+
var matches = Regex.Matches(content, codacyBadgePattern);
105+
106+
if (matches.Count == 0) return content;
107+
108+
// Extract the project ID from the first badge found
109+
var firstMatch = matches[0].Value;
110+
var projectIdMatch = Regex.Match(firstMatch, @"Grade/([a-f0-9]+)");
111+
string projectId = projectIdMatch.Success ? projectIdMatch.Groups[1].Value : GenerateProjectId(owner, repositoryName);
112+
113+
// Remove all existing Codacy badges
114+
var cleanedContent = Regex.Replace(content, codacyBadgePattern, "");
115+
116+
// Remove extra empty lines that might have been left behind
117+
cleanedContent = Regex.Replace(cleanedContent, @"\n\s*\n\s*\n", "\n\n");
118+
119+
// Create the new standardized Codacy badge with modern URL format
120+
var newBadge = $"[![Codacy Badge](https://api.codacy.com/project/badge/Grade/{projectId})](https://app.codacy.com/gh/{owner}/{repositoryName}?utm_source=github.com&utm_medium=referral&utm_content={owner}/{repositoryName}&utm_campaign=Badge_Grade_Settings)";
121+
122+
// Insert the badge at the beginning of the file (before any other content)
123+
return newBadge + "\n" + cleanedContent.TrimStart('\n');
124+
}
125+
126+
private string GenerateProjectId(string owner, string repositoryName)
127+
{
128+
// Generate a consistent 32-character hex string based on repository info
129+
var input = $"{owner.ToLower()}/{repositoryName.ToLower()}";
130+
var hash = System.Security.Cryptography.MD5.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
131+
return BitConverter.ToString(hash).Replace("-", "").ToLower();
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)