Skip to content

Commit 329c4a0

Browse files
konardclaude
andcommitted
Add automatic release generation for dependency-only changes
Implements GitHub Bot feature to automatically create releases when commits contain only dependency updates. Features: - New CommitTracker to monitor commits on main branch - DependencyOnlyReleaseTrigger detects dependabot commits affecting only dependency files - Automatic release creation with descriptive tags (deps-YYYY.MM.DD-shortsha) - Support for multiple dependency file types (C#, Node.js, Python, Rust, Go, Ruby) - Duplicate prevention through processed commit tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 3111a50 commit 329c4a0

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

csharp/Platform.Bot/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ private static async Task<int> Main(string[] args)
9797
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
9898
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));
9999
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100+
var commitTracker = new CommitTracker(githubStorage, new DependencyOnlyReleaseTrigger(githubStorage));
100101
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101102
var cancellation = new CancellationTokenSource();
102103
while (true)
@@ -105,6 +106,7 @@ private static async Task<int> Main(string[] args)
105106
{
106107
await issueTracker.Start(cancellation.Token);
107108
await pullRequenstTracker.Start(cancellation.Token);
109+
await commitTracker.Start(cancellation.Token);
108110
// timestampTracker.Start(cancellation.Token);
109111
Thread.Sleep(minimumInteractionInterval);
110112
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Interfaces;
2+
using Octokit;
3+
using Storage.Remote.GitHub;
4+
using System.Collections.Generic;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Platform.Collections.Lists;
8+
using Platform.Threading;
9+
10+
namespace Platform.Bot.Trackers
11+
{
12+
/// <summary>
13+
/// <para>
14+
/// Represents the commit tracker.
15+
/// </para>
16+
/// <para></para>
17+
/// </summary>
18+
public class CommitTracker : ITracker<GitHubCommit>
19+
{
20+
/// <summary>
21+
/// <para>
22+
/// The git hub api.
23+
/// </para>
24+
/// <para></para>
25+
/// </summary>
26+
private GitHubStorage _storage;
27+
28+
/// <summary>
29+
/// <para>
30+
/// The triggers.
31+
/// </para>
32+
/// <para></para>
33+
/// </summary>
34+
private IList<ITrigger<GitHubCommit>> _triggers;
35+
36+
/// <summary>
37+
/// <para>
38+
/// Initializes a new <see cref="CommitTracker"/> instance.
39+
/// </para>
40+
/// <para></para>
41+
/// </summary>
42+
/// <param name="triggers">
43+
/// <para>A triggers.</para>
44+
/// <para></para>
45+
/// </param>
46+
/// <param name="storage">
47+
/// <para>A git hub api.</para>
48+
/// <para></para>
49+
/// </param>
50+
public CommitTracker(GitHubStorage storage, params ITrigger<GitHubCommit>[] triggers)
51+
{
52+
_storage = storage;
53+
_triggers = triggers;
54+
}
55+
56+
/// <summary>
57+
/// <para>
58+
/// Starts the cancellation token.
59+
/// </para>
60+
/// <para></para>
61+
/// </summary>
62+
/// <param name="cancellationToken">
63+
/// <para>The cancellation token.</para>
64+
/// <para></para>
65+
/// </param>
66+
public async Task Start(CancellationToken cancellationToken)
67+
{
68+
foreach (var trigger in _triggers)
69+
{
70+
foreach (var repository in _storage.Client.Repository.GetAllForOrg("linksplatform").AwaitResult())
71+
{
72+
// Get commits from the main branch only
73+
var commits = _storage.GetCommits(repository.Id, new CommitRequest { Sha = repository.DefaultBranch }).AwaitResult();
74+
75+
foreach (var commit in commits)
76+
{
77+
if (cancellationToken.IsCancellationRequested)
78+
{
79+
return;
80+
}
81+
82+
if (await trigger.Condition(commit))
83+
{
84+
await trigger.Action(commit);
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Interfaces;
4+
using Octokit;
5+
using Platform.Threading;
6+
using Storage.Remote.GitHub;
7+
using System.Linq;
8+
using System.Text.RegularExpressions;
9+
using Storage.Local;
10+
using System.Collections.Generic;
11+
12+
namespace Platform.Bot.Triggers
13+
{
14+
public class DependencyOnlyReleaseTrigger : ITrigger<GitHubCommit>
15+
{
16+
private readonly GitHubStorage _githubStorage;
17+
private readonly HashSet<string> _processedCommits;
18+
19+
public DependencyOnlyReleaseTrigger(GitHubStorage storage)
20+
{
21+
_githubStorage = storage;
22+
_processedCommits = new HashSet<string>();
23+
}
24+
25+
public async Task<bool> Condition(GitHubCommit commit)
26+
{
27+
try
28+
{
29+
// Skip if we already processed this commit
30+
if (_processedCommits.Contains(commit.Sha))
31+
{
32+
return false;
33+
}
34+
35+
// Skip if this is not a dependabot commit
36+
if (!IsDependabotCommit(commit))
37+
{
38+
return false;
39+
}
40+
41+
// Get the repository
42+
var repositoryId = commit.Repository?.Id;
43+
if (repositoryId == null)
44+
{
45+
return false;
46+
}
47+
48+
// Check if this commit only changes dependency files
49+
var commitDetails = _githubStorage.Client.Repository.Commit.Get(repositoryId.Value, commit.Sha).AwaitResult();
50+
51+
// Check if files changed are only dependency-related files
52+
var changedFiles = commitDetails.Files;
53+
if (changedFiles == null || !changedFiles.Any())
54+
{
55+
return false;
56+
}
57+
58+
// Check if all changed files are dependency files
59+
var isDependencyOnlyChange = changedFiles.All(file => IsDependencyFile(file.Filename));
60+
61+
if (!isDependencyOnlyChange)
62+
{
63+
return false;
64+
}
65+
66+
// Check if a release with this commit already exists to avoid duplicates
67+
var releases = _githubStorage.Client.Repository.Release.GetAll(repositoryId.Value).AwaitResult();
68+
var existingRelease = releases.FirstOrDefault(r => r.TagName.Contains(commit.Sha.Substring(0, 7)));
69+
70+
return existingRelease == null;
71+
}
72+
catch (Exception ex)
73+
{
74+
Console.WriteLine($"Error in DependencyOnlyReleaseTrigger.Condition: {ex.Message}");
75+
return false;
76+
}
77+
}
78+
79+
public async Task Action(GitHubCommit commit)
80+
{
81+
try
82+
{
83+
var repositoryId = commit.Repository?.Id;
84+
if (repositoryId == null)
85+
{
86+
return;
87+
}
88+
89+
var repository = _githubStorage.Client.Repository.Get(repositoryId.Value).AwaitResult();
90+
91+
// Generate a new version tag based on the current date and commit
92+
var currentDate = DateTime.UtcNow;
93+
var shortCommitSha = commit.Sha.Substring(0, 7);
94+
var tagName = $"deps-{currentDate:yyyy.MM.dd}-{shortCommitSha}";
95+
96+
// Create release name and body
97+
var releaseName = $"Dependency Updates - {currentDate:yyyy-MM-dd}";
98+
var releaseBody = $"Automatic release for dependency updates.\n\nCommit: {commit.HtmlUrl}\nCommit Message: {commit.Commit.Message}\n\n🤖 Generated with [Claude Code](https://claude.ai/code)";
99+
100+
// Create the release
101+
var newRelease = new NewRelease(tagName)
102+
{
103+
Name = releaseName,
104+
Body = releaseBody,
105+
Draft = false,
106+
Prerelease = false,
107+
TargetCommitish = commit.Sha
108+
};
109+
110+
var createdRelease = await _githubStorage.Client.Repository.Release.Create(repositoryId.Value, newRelease);
111+
112+
Console.WriteLine($"Created automatic release for dependency updates: {createdRelease.HtmlUrl}");
113+
Console.WriteLine($"Repository: {repository.FullName}");
114+
Console.WriteLine($"Tag: {tagName}");
115+
116+
// Mark this commit as processed to avoid duplicates
117+
_processedCommits.Add(commit.Sha);
118+
}
119+
catch (Exception ex)
120+
{
121+
Console.WriteLine($"Error in DependencyOnlyReleaseTrigger.Action: {ex.Message}");
122+
}
123+
}
124+
125+
private bool IsDependabotCommit(GitHubCommit commit)
126+
{
127+
// Check if the commit is from dependabot by checking the author or commit message
128+
if (commit.Author?.Id == GitHubStorage.DependabotId)
129+
{
130+
return true;
131+
}
132+
133+
if (commit.Committer?.Id == GitHubStorage.DependabotId)
134+
{
135+
return true;
136+
}
137+
138+
// Check commit message patterns
139+
var commitMessage = commit.Commit?.Message?.ToLower() ?? "";
140+
var dependabotPatterns = new[]
141+
{
142+
"bump ",
143+
"update ",
144+
"dependabot",
145+
"dependency"
146+
};
147+
148+
return dependabotPatterns.Any(pattern => commitMessage.Contains(pattern));
149+
}
150+
151+
private bool IsDependencyFile(string filename)
152+
{
153+
var dependencyFilePatterns = new[]
154+
{
155+
@"\.csproj$", // C# project files
156+
@"packages\.config$", // NuGet packages.config
157+
@"\.sln$", // Solution files (sometimes updated by dependabot)
158+
@"package\.json$", // Node.js package.json
159+
@"package-lock\.json$", // Node.js lock file
160+
@"yarn\.lock$", // Yarn lock file
161+
@"Cargo\.toml$", // Rust Cargo.toml
162+
@"Cargo\.lock$", // Rust Cargo.lock
163+
@"requirements\.txt$", // Python requirements
164+
@"Pipfile$", // Python Pipfile
165+
@"Pipfile\.lock$", // Python Pipfile.lock
166+
@"pyproject\.toml$", // Python pyproject.toml
167+
@"go\.mod$", // Go modules
168+
@"go\.sum$", // Go sum file
169+
@"Gemfile$", // Ruby Gemfile
170+
@"Gemfile\.lock$" // Ruby Gemfile.lock
171+
};
172+
173+
return dependencyFilePatterns.Any(pattern => Regex.IsMatch(filename, pattern, RegexOptions.IgnoreCase));
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)