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 \n Commit: { commit . HtmlUrl } \n Commit 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