diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-12 14:21:06 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-12 14:21:06 +0300 |
| commit | b3b599b7b645f7fab6fd8c22e8475a054b137225 (patch) | |
| tree | c9a1631309292e8b7d2785edf9fce990e8308823 /internal | |
| parent | ceb164c6a8826db1e763aecca49098a8c2584b7f (diff) | |
feat: add AI-powered release notes generation
- Add --ai-release-notes flag to generate prose release notes using Claude CLI
- Add --update-releases flag to update existing releases with AI notes
- Implement GetDiffBetweenTags to extract code changes between versions
- Integrate Claude CLI for intelligent release note generation
- Support fallback to multiple Claude models (sonnet-3.5, sonnet-4, default)
- Always print release notes to stdout for visibility
- Fix token loading messages to only show when falling back from config
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/flags.go | 4 | ||||
| -rw-r--r-- | internal/cli/release.go | 239 | ||||
| -rw-r--r-- | internal/release/release.go | 276 |
3 files changed, 493 insertions, 26 deletions
diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 72ffa1a..fb367cb 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -33,6 +33,8 @@ type Flags struct { CheckReleases bool NoCheckReleases bool AutoCreateReleases bool + AIReleaseNotes bool + UpdateReleases bool // Internal fields for batch run state management (not set by flags) BatchRunStateManager *state.Manager @@ -68,6 +70,8 @@ func ParseFlags() *Flags { flag.BoolVar(&f.CheckReleases, "check-releases", false, "manually check for version tags without releases and create them (with confirmation)") flag.BoolVar(&f.NoCheckReleases, "no-check-releases", false, "disable automatic release checking after sync operations") flag.BoolVar(&f.AutoCreateReleases, "auto-create-releases", false, "automatically create releases without confirmation prompts") + flag.BoolVar(&f.AIReleaseNotes, "ai-release-notes", false, "generate release notes using Claude AI based on git diff") + flag.BoolVar(&f.UpdateReleases, "update-releases", false, "update existing releases with new AI-generated notes") flag.Parse() diff --git a/internal/cli/release.go b/internal/cli/release.go index a83283d..e861533 100644 --- a/internal/cli/release.go +++ b/internal/cli/release.go @@ -4,14 +4,36 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "codeberg.org/snonux/gitsyncer/internal/config" "codeberg.org/snonux/gitsyncer/internal/release" ) +// isVersionTag checks if a tag name is a version tag +// Supports formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X +func isVersionTag(tag string) bool { + // Pattern matches version tags with optional 'v' prefix + pattern := `^v?\d+(\.\d+)?(\.\d+)?$` + matched, _ := regexp.MatchString(pattern, tag) + return matched +} + // HandleCheckReleases checks for version tags without releases and creates them with confirmation func HandleCheckReleases(cfg *config.Config, flags *Flags) int { + // Check all configured repositories + return HandleCheckReleasesForRepos(cfg, flags, cfg.Repositories) +} + +// HandleCheckReleasesForRepo checks releases for a specific repository +func HandleCheckReleasesForRepo(cfg *config.Config, flags *Flags, repoName string) int { + // Check only the specified repository + return HandleCheckReleasesForRepos(cfg, flags, []string{repoName}) +} + +// HandleCheckReleasesForRepos checks for version tags without releases and creates them with confirmation +func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories []string) int { releaseManager := release.NewManager(flags.WorkDir) // Set tokens from config with fallback to environment variables and files @@ -24,24 +46,23 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { if token == "" { fmt.Println("No GitHub token in config, checking environment variable...") token = os.Getenv("GITHUB_TOKEN") - } - if token == "" { - fmt.Println("No GITHUB_TOKEN env var, checking ~/.gitsyncer_github_token file...") - home, err := os.UserHomeDir() - if err == nil { - tokenFile := filepath.Join(home, ".gitsyncer_github_token") - data, err := os.ReadFile(tokenFile) + if token == "" { + fmt.Println("No GITHUB_TOKEN env var, checking ~/.gitsyncer_github_token file...") + home, err := os.UserHomeDir() if err == nil { - token = strings.TrimSpace(string(data)) + tokenFile := filepath.Join(home, ".gitsyncer_github_token") + data, err := os.ReadFile(tokenFile) + if err == nil { + token = strings.TrimSpace(string(data)) + } } } } if token != "" { - fmt.Printf("Setting GitHub token (length: %d)\n", len(token)) releaseManager.SetGitHubToken(token) } else { - fmt.Println("No GitHub token found") + fmt.Println("WARNING: No GitHub token found - cannot create GitHub releases") } } else { fmt.Println("No GitHub organization found in config") @@ -56,31 +77,30 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { if token == "" { fmt.Println("No Codeberg token in config, checking environment variable...") token = os.Getenv("CODEBERG_TOKEN") - } - if token == "" { - fmt.Println("No CODEBERG_TOKEN env var, checking ~/.gitsyncer_codeberg_token file...") - home, err := os.UserHomeDir() - if err == nil { - tokenFile := filepath.Join(home, ".gitsyncer_codeberg_token") - data, err := os.ReadFile(tokenFile) + if token == "" { + fmt.Println("No CODEBERG_TOKEN env var, checking ~/.gitsyncer_codeberg_token file...") + home, err := os.UserHomeDir() if err == nil { - token = strings.TrimSpace(string(data)) + tokenFile := filepath.Join(home, ".gitsyncer_codeberg_token") + data, err := os.ReadFile(tokenFile) + if err == nil { + token = strings.TrimSpace(string(data)) + } } } } if token != "" { - fmt.Printf("Setting Codeberg token (length: %d)\n", len(token)) releaseManager.SetCodebergToken(token) } else { - fmt.Println("No Codeberg token found") + fmt.Println("WARNING: No Codeberg token found - cannot create Codeberg releases") } } else { fmt.Println("No Codeberg organization found in config") } - // Process all configured repositories - for _, repoName := range cfg.Repositories { + // Process the specified repositories + for _, repoName := range repositories { fmt.Printf("\nChecking releases for repository: %s\n", repoName) // Check if the repository is cloned locally @@ -137,8 +157,35 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { // Create missing releases with confirmation if len(missingGitHub) > 0 && githubOrg != nil { for _, tag := range missingGitHub { + // Get commits for this tag + commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) + if err != nil { + commits = []string{} + } + // Generate release notes - releaseNotes := releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + var releaseNotes string + if flags.AIReleaseNotes { + fmt.Printf(" Generating AI release notes for %s...\n", tag) + aiNotes, err := releaseManager.GenerateAIReleaseNotes(repoPath, repoName, tag, localTags, commits) + if err != nil { + fmt.Printf(" Warning: Failed to generate AI release notes: %v\n", err) + fmt.Printf(" Falling back to standard release notes\n") + releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + } else { + releaseNotes = aiNotes + fmt.Printf(" AI release notes generated successfully\n") + } + } else { + releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + } + + // Print release notes to stdout + fmt.Printf("\n%s\n", strings.Repeat("=", 70)) + fmt.Printf("Release Notes for %s/%s tag %s:\n", githubOrg.Name, repoName, tag) + fmt.Printf("%s\n", strings.Repeat("-", 70)) + fmt.Println(releaseNotes) + fmt.Printf("%s\n\n", strings.Repeat("=", 70)) msg := fmt.Sprintf("Create GitHub release for %s/%s tag %s?", githubOrg.Name, repoName, tag) @@ -148,7 +195,7 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { fmt.Printf(" Auto-creating GitHub release for %s/%s tag %s\n", githubOrg.Name, repoName, tag) createRelease = true } else { - createRelease = release.PromptConfirmationWithNotes(msg, releaseNotes) + createRelease = release.PromptConfirmation(msg) } if createRelease { @@ -163,8 +210,35 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { if len(missingCodeberg) > 0 && codebergOrg != nil { for _, tag := range missingCodeberg { + // Get commits for this tag + commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) + if err != nil { + commits = []string{} + } + // Generate release notes - releaseNotes := releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + var releaseNotes string + if flags.AIReleaseNotes { + fmt.Printf(" Generating AI release notes for %s...\n", tag) + aiNotes, err := releaseManager.GenerateAIReleaseNotes(repoPath, repoName, tag, localTags, commits) + if err != nil { + fmt.Printf(" Warning: Failed to generate AI release notes: %v\n", err) + fmt.Printf(" Falling back to standard release notes\n") + releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + } else { + releaseNotes = aiNotes + fmt.Printf(" AI release notes generated successfully\n") + } + } else { + releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags) + } + + // Print release notes to stdout + fmt.Printf("\n%s\n", strings.Repeat("=", 70)) + fmt.Printf("Release Notes for %s/%s tag %s:\n", codebergOrg.Name, repoName, tag) + fmt.Printf("%s\n", strings.Repeat("-", 70)) + fmt.Println(releaseNotes) + fmt.Printf("%s\n\n", strings.Repeat("=", 70)) msg := fmt.Sprintf("Create Codeberg release for %s/%s tag %s?", codebergOrg.Name, repoName, tag) @@ -174,7 +248,7 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { fmt.Printf(" Auto-creating Codeberg release for %s/%s tag %s\n", codebergOrg.Name, repoName, tag) createRelease = true } else { - createRelease = release.PromptConfirmationWithNotes(msg, releaseNotes) + createRelease = release.PromptConfirmation(msg) } if createRelease { @@ -186,6 +260,119 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int { } } } + + // Update existing releases if requested + if flags.UpdateReleases { + // Update GitHub releases + if githubOrg != nil && githubOrg.Name != "" { + githubReleases, err := releaseManager.GetGitHubReleases(githubOrg.Name, repoName) + if err == nil && len(githubReleases) > 0 { + fmt.Printf("\n Updating existing GitHub releases...\n") + for _, tag := range githubReleases { + // Check if this is a version tag + if !isVersionTag(tag) { + continue + } + + // Get commits for this tag + commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) + if err != nil { + commits = []string{} + } + + // Generate AI release notes + if flags.AIReleaseNotes { + fmt.Printf(" Generating AI release notes for existing release %s...\n", tag) + aiNotes, err := releaseManager.GenerateAIReleaseNotes(repoPath, repoName, tag, localTags, commits) + if err != nil { + fmt.Printf(" Warning: Failed to generate AI release notes: %v\n", err) + continue + } + + // Print release notes to stdout + fmt.Printf("\n%s\n", strings.Repeat("=", 70)) + fmt.Printf("Updated Release Notes for %s/%s tag %s:\n", githubOrg.Name, repoName, tag) + fmt.Printf("%s\n", strings.Repeat("-", 70)) + fmt.Println(aiNotes) + fmt.Printf("%s\n\n", strings.Repeat("=", 70)) + + msg := fmt.Sprintf("Update GitHub release for %s/%s tag %s?", githubOrg.Name, repoName, tag) + + updateRelease := false + if flags.AutoCreateReleases { + fmt.Printf(" Auto-updating GitHub release for %s/%s tag %s\n", githubOrg.Name, repoName, tag) + updateRelease = true + } else { + updateRelease = release.PromptConfirmation(msg) + } + + if updateRelease { + if err := releaseManager.UpdateGitHubRelease(githubOrg.Name, repoName, tag, aiNotes); err != nil { + fmt.Printf(" Error updating GitHub release: %v\n", err) + } else { + fmt.Printf(" Updated GitHub release for tag %s\n", tag) + } + } + } + } + } + } + + // Update Codeberg releases + if codebergOrg != nil && codebergOrg.Name != "" { + codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName) + if err == nil && len(codebergReleases) > 0 { + fmt.Printf("\n Updating existing Codeberg releases...\n") + for _, tag := range codebergReleases { + // Check if this is a version tag + if !isVersionTag(tag) { + continue + } + + // Get commits for this tag + commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag) + if err != nil { + commits = []string{} + } + + // Generate AI release notes + if flags.AIReleaseNotes { + fmt.Printf(" Generating AI release notes for existing release %s...\n", tag) + aiNotes, err := releaseManager.GenerateAIReleaseNotes(repoPath, repoName, tag, localTags, commits) + if err != nil { + fmt.Printf(" Warning: Failed to generate AI release notes: %v\n", err) + continue + } + + // Print release notes to stdout + fmt.Printf("\n%s\n", strings.Repeat("=", 70)) + fmt.Printf("Updated Release Notes for %s/%s tag %s:\n", codebergOrg.Name, repoName, tag) + fmt.Printf("%s\n", strings.Repeat("-", 70)) + fmt.Println(aiNotes) + fmt.Printf("%s\n\n", strings.Repeat("=", 70)) + + msg := fmt.Sprintf("Update Codeberg release for %s/%s tag %s?", codebergOrg.Name, repoName, tag) + + updateRelease := false + if flags.AutoCreateReleases { + fmt.Printf(" Auto-updating Codeberg release for %s/%s tag %s\n", codebergOrg.Name, repoName, tag) + updateRelease = true + } else { + updateRelease = release.PromptConfirmation(msg) + } + + if updateRelease { + if err := releaseManager.UpdateCodebergRelease(codebergOrg.Name, repoName, tag, aiNotes); err != nil { + fmt.Printf(" Error updating Codeberg release: %v\n", err) + } else { + fmt.Printf(" Updated Codeberg release for tag %s\n", tag) + } + } + } + } + } + } + } } // Also check public repositories if they're synced diff --git a/internal/release/release.go b/internal/release/release.go index c66d5f9..67350cf 100644 --- a/internal/release/release.go +++ b/internal/release/release.go @@ -230,6 +230,138 @@ func (m *Manager) GenerateReleaseNotes(repoPath, tag string, allTags []string) s return notes.String() } +// GetDiffBetweenTags gets the diff between two tags +func (m *Manager) GetDiffBetweenTags(repoPath, fromTag, toTag string) (string, error) { + // Use git diff to get changes between tags + // If fromTag is empty, get all changes up to toTag + var cmd *exec.Cmd + if fromTag == "" { + // Get diff from the beginning to toTag + cmd = exec.Command("git", "-C", repoPath, "show", "--format=", "--no-patch", toTag) + // This won't work well, so let's get the first commit + firstCommitCmd := exec.Command("git", "-C", repoPath, "rev-list", "--max-parents=0", "HEAD") + firstCommitOutput, err := firstCommitCmd.Output() + if err != nil { + // Fallback to just showing the tag + cmd = exec.Command("git", "-C", repoPath, "diff", "--stat", toTag) + } else { + firstCommit := strings.TrimSpace(string(firstCommitOutput)) + cmd = exec.Command("git", "-C", repoPath, "diff", "--stat", fmt.Sprintf("%s..%s", firstCommit, toTag)) + } + } else { + cmd = exec.Command("git", "-C", repoPath, "diff", "--stat", fmt.Sprintf("%s..%s", fromTag, toTag)) + } + + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get diff: %w", err) + } + + // Also get the actual diff for key files (limit to prevent huge outputs) + var diffCmd *exec.Cmd + if fromTag == "" { + diffCmd = exec.Command("git", "-C", repoPath, "show", toTag, "--", "*.go", "*.js", "*.py", "*.rs", "*.c", "*.cpp", "*.java", "*.ts", "*.jsx", "*.tsx", "README*", "*.md") + } else { + diffCmd = exec.Command("git", "-C", repoPath, "diff", fmt.Sprintf("%s..%s", fromTag, toTag), "--", "*.go", "*.js", "*.py", "*.rs", "*.c", "*.cpp", "*.java", "*.ts", "*.jsx", "*.tsx", "README*", "*.md") + } + + diffOutput, err := diffCmd.Output() + if err != nil { + // If error, just use the stat output + return string(output), nil + } + + // Combine stat and limited diff (truncate if too long) + fullOutput := string(output) + "\n\n" + string(diffOutput) + maxLength := 50000 // Limit to 50KB to avoid overwhelming Claude + if len(fullOutput) > maxLength { + fullOutput = fullOutput[:maxLength] + "\n\n... (diff truncated)" + } + + return fullOutput, nil +} + +// GenerateAIReleaseNotes generates prose release notes using Claude CLI +func (m *Manager) GenerateAIReleaseNotes(repoPath, repoName, tag string, allTags []string, commits []string) (string, error) { + // Find the previous tag + var prevTag string + tagIndex := -1 + for i, t := range allTags { + if t == tag { + tagIndex = i + break + } + } + + if tagIndex > 0 { + prevTag = allTags[tagIndex-1] + } + + // Get the diff between tags + diff, err := m.GetDiffBetweenTags(repoPath, prevTag, tag) + if err != nil { + return "", fmt.Errorf("failed to get diff: %w", err) + } + + // Prepare the prompt for Claude + var prompt strings.Builder + prompt.WriteString(fmt.Sprintf("Generate professional release notes for %s version %s.\n\n", repoName, tag)) + + if prevTag != "" { + prompt.WriteString(fmt.Sprintf("Previous version: %s\n", prevTag)) + } + + prompt.WriteString("\nCommit messages:\n") + for _, commit := range commits { + prompt.WriteString(fmt.Sprintf("- %s\n", commit)) + } + + prompt.WriteString("\nCode changes:\n") + prompt.WriteString(diff) + prompt.WriteString("\n\nBased on the commits and code changes above, write professional release notes that:\n") + prompt.WriteString("1. Start with a brief overview of what this release accomplishes\n") + prompt.WriteString("2. Group changes into logical sections (Features, Improvements, Bug Fixes, etc.)\n") + prompt.WriteString("3. Explain WHY each change is useful to users, not just what changed\n") + prompt.WriteString("4. Use clear, non-technical language where possible\n") + prompt.WriteString("5. Highlight any breaking changes or migration steps\n") + prompt.WriteString("6. Keep it concise but informative\n") + prompt.WriteString("7. Format using Markdown\n") + prompt.WriteString("\nDo not include the version number in the title as it will be added automatically.") + + // Run Claude CLI + cmd := exec.Command("claude", "--model", "sonnet-3.5", prompt.String()) + output, err := cmd.Output() + if err != nil { + // Try with sonnet-4 model + cmd = exec.Command("claude", "--model", "sonnet-4", prompt.String()) + output, err = cmd.Output() + if err != nil { + // Try with default model + cmd = exec.Command("claude", prompt.String()) + output, err = cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to run claude: %w", err) + } + } + } + + releaseNotes := strings.TrimSpace(string(output)) + if releaseNotes == "" { + return "", fmt.Errorf("received empty release notes from claude") + } + + // Add header and footer + var finalNotes strings.Builder + finalNotes.WriteString(fmt.Sprintf("# Release %s\n\n", tag)) + finalNotes.WriteString(releaseNotes) + finalNotes.WriteString("\n\n---\n\n") + if prevTag != "" { + finalNotes.WriteString(fmt.Sprintf("**Full Changelog**: %s...%s", prevTag, tag)) + } + + return finalNotes.String(), nil +} + // GetGitHubReleases fetches releases from GitHub func (m *Manager) GetGitHubReleases(owner, repo string) ([]string, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo) @@ -458,4 +590,148 @@ func PromptConfirmationWithNotes(message, releaseNotes string) bool { response = strings.ToLower(strings.TrimSpace(response)) return response == "y" || response == "yes" +} + +// UpdateGitHubRelease updates an existing release on GitHub +func (m *Manager) UpdateGitHubRelease(owner, repo, tag, releaseNotes string) error { + if m.githubToken == "" { + return fmt.Errorf("GitHub token is required for updating releases") + } + + // First, get the release ID + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+m.githubToken) + req.Header.Set("Accept", "application/vnd.github.v3+json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to get release: %s - %s", resp.Status, string(body)) + } + + var releaseInfo struct { + ID int64 `json:"id"` + } + if err := json.NewDecoder(resp.Body).Decode(&releaseInfo); err != nil { + return err + } + + // Now update the release + updateURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%d", owner, repo, releaseInfo.ID) + + release := Release{ + TagName: tag, + Name: tag, + Body: releaseNotes, + } + + jsonData, err := json.Marshal(release) + if err != nil { + return err + } + + updateReq, err := http.NewRequest("PATCH", updateURL, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + updateReq.Header.Set("Authorization", "Bearer "+m.githubToken) + updateReq.Header.Set("Content-Type", "application/json") + updateReq.Header.Set("Accept", "application/vnd.github.v3+json") + + updateResp, err := client.Do(updateReq) + if err != nil { + return err + } + defer updateResp.Body.Close() + + if updateResp.StatusCode != 200 { + body, _ := io.ReadAll(updateResp.Body) + return fmt.Errorf("failed to update GitHub release: %s - %s", updateResp.Status, string(body)) + } + + return nil +} + +// UpdateCodebergRelease updates an existing release on Codeberg +func (m *Manager) UpdateCodebergRelease(owner, repo, tag, releaseNotes string) error { + if m.codebergToken == "" { + return fmt.Errorf("Codeberg token is required for updating releases") + } + + // First, get the release ID + url := fmt.Sprintf("https://codeberg.org/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", "token "+m.codebergToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to get release: %s - %s", resp.Status, string(body)) + } + + var releaseInfo struct { + ID int64 `json:"id"` + } + if err := json.NewDecoder(resp.Body).Decode(&releaseInfo); err != nil { + return err + } + + // Now update the release + updateURL := fmt.Sprintf("https://codeberg.org/api/v1/repos/%s/%s/releases/%d", owner, repo, releaseInfo.ID) + + release := Release{ + TagName: tag, + Name: tag, + Body: releaseNotes, + } + + jsonData, err := json.Marshal(release) + if err != nil { + return err + } + + updateReq, err := http.NewRequest("PATCH", updateURL, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + updateReq.Header.Set("Authorization", "token "+m.codebergToken) + updateReq.Header.Set("Content-Type", "application/json") + + updateResp, err := client.Do(updateReq) + if err != nil { + return err + } + defer updateResp.Body.Close() + + if updateResp.StatusCode != 200 { + body, _ := io.ReadAll(updateResp.Body) + return fmt.Errorf("failed to update Codeberg release: %s - %s", updateResp.Status, string(body)) + } + + return nil }
\ No newline at end of file |
