@@ -2,10 +2,12 @@ package main
22
33import (
44 "fmt"
5+ "net/http"
56 "os"
67 "os/exec"
78 "regexp"
89 "strings"
10+ "time"
911
1012 tea "github.com/charmbracelet/bubbletea"
1113 "github.com/charmbracelet/lipgloss"
@@ -20,6 +22,7 @@ const (
2022 stateConfirming
2123 stateExecuting
2224 stateComplete
25+ statePollingRelease
2326 stateError
2427)
2528
@@ -37,6 +40,8 @@ type model struct {
3740 executed bool
3841 repoSlug string
3942 testMode bool
43+ releaseURL string
44+ pollingAttempts int
4045 width int
4146 height int
4247}
@@ -57,6 +62,14 @@ type executionCompleteMsg struct {
5762 errors []string
5863}
5964
65+ type releaseFoundMsg struct {
66+ url string
67+ }
68+
69+ type releasePollingMsg struct {
70+ attempt int
71+ }
72+
6073// Styles
6174var (
6275 titleStyle = lipgloss .NewStyle ().
@@ -169,13 +182,54 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
169182
170183 case executionCompleteMsg :
171184 if msg .success {
172- m .state = stateComplete
173- m .executed = true
185+ if m .testMode {
186+ m .state = stateComplete
187+ m .executed = true
188+ } else {
189+ m .state = statePollingRelease
190+ return m , pollForRelease (m .repoSlug , m .tag )
191+ }
174192 } else {
175193 m .state = stateError
176194 m .errors = msg .errors
177195 }
178196 return m , nil
197+
198+ case releasePollingMsg :
199+ m .pollingAttempts = msg .attempt
200+ return m , nil
201+
202+ case releaseFoundMsg :
203+ m .releaseURL = msg .url
204+ m .state = stateComplete
205+ m .executed = true
206+ return m , nil
207+
208+ case pollAttemptMsg :
209+ if msg .attempt > 30 {
210+ // Timeout after 30 attempts (5 minutes)
211+ m .state = stateComplete
212+ m .executed = true
213+ return m , nil
214+ }
215+
216+ m .pollingAttempts = msg .attempt
217+
218+ // Check if release is available
219+ releaseURL := fmt .Sprintf ("https://github.com/%s/releases/tag/%s" , msg .repoSlug , msg .tag )
220+ resp , err := http .Get (releaseURL )
221+ if err == nil {
222+ resp .Body .Close ()
223+ if resp .StatusCode == 200 {
224+ m .releaseURL = releaseURL
225+ m .state = stateComplete
226+ m .executed = true
227+ return m , nil
228+ }
229+ }
230+
231+ // Continue polling
232+ return m , startPollingTicker (msg .repoSlug , msg .tag , msg .attempt )
179233 }
180234
181235 return m , nil
@@ -191,6 +245,8 @@ func (m model) View() string {
191245 return m .renderConfirming ()
192246 case stateExecuting :
193247 return m .renderExecuting ()
248+ case statePollingRelease :
249+ return m .renderPollingRelease ()
194250 case stateComplete :
195251 return m .renderComplete ()
196252 case stateError :
@@ -312,6 +368,23 @@ func (m model) renderExecuting() string {
312368 return content
313369}
314370
371+ func (m model ) renderPollingRelease () string {
372+ content := titleStyle .Render ("🏷️ GitHub MCP Server - Tag Release" ) + "\n \n "
373+ content += successStyle .Render ("✅ Successfully tagged and pushed release " + m .tag ) + "\n "
374+ content += successStyle .Render ("✅ 'latest-release' tag has been updated" ) + "\n \n "
375+
376+ content += subtitleStyle .Render ("🔍 Polling for GitHub release..." ) + "\n \n "
377+
378+ dots := strings .Repeat ("." , (m .pollingAttempts % 3 ) + 1 )
379+ content += warningStyle .Render (fmt .Sprintf ("⋯ Checking GitHub releases page%s" , dots )) + "\n "
380+ content += fmt .Sprintf (" Attempt %d/30 (checking every 10 seconds)\n " , m .pollingAttempts + 1 )
381+ content += "\n "
382+ content += subtitleStyle .Render ("Once the release workflow completes, the draft release URL will appear here." ) + "\n "
383+ content += subtitleStyle .Render ("Press Ctrl+C to exit early if needed." )
384+
385+ return content
386+ }
387+
315388func (m model ) renderComplete () string {
316389 content := titleStyle .Render ("🏷️ GitHub MCP Server - Tag Release" )
317390 if m .testMode {
@@ -330,13 +403,21 @@ func (m model) renderComplete() string {
330403 content += subtitleStyle .Render ("To perform the actual release, run without --test flag" ) + "\n \n "
331404 } else {
332405 content += successStyle .Render ("✅ Successfully tagged and pushed release " + m .tag ) + "\n "
333- content += successStyle .Render ("✅ 'latest-release' tag has been updated" ) + "\n \n "
406+ content += successStyle .Render ("✅ 'latest-release' tag has been updated" ) + "\n "
407+
408+ if m .releaseURL != "" {
409+ content += successStyle .Render ("✅ Draft release is now available!" ) + "\n \n "
410+ content += subtitleStyle .Render ("🎉 Release " + m .tag + " has been created!" ) + "\n \n "
411+ content += highlightStyle .Render ("📦 Draft Release URL:" ) + "\n "
412+ content += " " + m .releaseURL + "\n \n "
413+ } else {
414+ content += "\n "
415+ content += subtitleStyle .Render ("🎉 Release " + m .tag + " has been initiated!" ) + "\n \n "
416+ }
334417
335418 // Post-release instructions
336- content += subtitleStyle .Render ("🎉 Release " + m .tag + " has been initiated!" ) + "\n \n "
337419 content += subtitleStyle .Render ("Next steps:" ) + "\n "
338420 steps := []string {
339- fmt .Sprintf ("📋 Check https://github.com/%s/releases and wait for the draft release" , m .repoSlug ),
340421 "✏️ Edit the new release, delete existing notes and click auto-generate button" ,
341422 "✨ Add a section at the top calling out the main features" ,
342423 "🚀 Publish the release" ,
@@ -346,8 +427,7 @@ func (m model) renderComplete() string {
346427 for _ , step := range steps {
347428 content += " " + step + "\n "
348429 }
349-
350- content += "\n " + subtitleStyle .Render (fmt .Sprintf ("📦 Draft Release: https://github.com/%s/releases/tag/%s" , m .repoSlug , m .tag )) + "\n \n "
430+ content += "\n "
351431 }
352432
353433 content += subtitleStyle .Render ("Press Enter to exit" )
@@ -535,6 +615,36 @@ func performExecution(tag, remote string, testMode bool) tea.Cmd {
535615 })
536616}
537617
618+ // pollForRelease polls the GitHub releases page to check if a release is available
619+ func pollForRelease (repoSlug , tag string ) tea.Cmd {
620+ return tea .Cmd (func () tea.Msg {
621+ // Check immediately first
622+ releaseURL := fmt .Sprintf ("https://github.com/%s/releases/tag/%s" , repoSlug , tag )
623+ resp , err := http .Get (releaseURL )
624+ if err == nil {
625+ resp .Body .Close ()
626+ if resp .StatusCode == 200 {
627+ return releaseFoundMsg {url : releaseURL }
628+ }
629+ }
630+
631+ // Start polling with ticker
632+ return startPollingTicker (repoSlug , tag , 0 )
633+ })
634+ }
635+
636+ func startPollingTicker (repoSlug , tag string , attempt int ) tea.Cmd {
637+ return tea .Tick (time .Second * 10 , func (t time.Time ) tea.Msg {
638+ return pollAttemptMsg {repoSlug : repoSlug , tag : tag , attempt : attempt + 1 }
639+ })
640+ }
641+
642+ type pollAttemptMsg struct {
643+ repoSlug string
644+ tag string
645+ attempt int
646+ }
647+
538648func main () {
539649 if len (os .Args ) < 2 {
540650 fmt .Println ("Error: No tag specified." )
@@ -547,7 +657,7 @@ func main() {
547657 tag := os .Args [1 ]
548658 testMode := false
549659 remote := "origin" // default remote
550-
660+
551661 // Parse flags
552662 for i := 2 ; i < len (os .Args ); i ++ {
553663 switch os .Args [i ] {
@@ -563,13 +673,13 @@ func main() {
563673 }
564674 }
565675 }
566-
676+
567677 if testMode {
568678 fmt .Printf ("🧪 Running in TEST MODE - no actual changes will be made (remote: %s)\n " , remote )
569679 } else {
570680 fmt .Printf ("🚀 Running release process (remote: %s)\n " , remote )
571681 }
572-
682+
573683 p := tea .NewProgram (
574684 initialModel (tag , remote , testMode ),
575685 tea .WithAltScreen (),
0 commit comments