summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-08 12:25:07 +0300
committerPaul Buetow <paul@buetow.org>2025-09-08 12:25:07 +0300
commit5c3e0b5cf99d028c4f06be7a825388b296e37a22 (patch)
tree6084ed9513a1cc4e6a8ad673f013e85cd40c6728
parentf9e32a01866831c489a573329b1b6106d5007eaa (diff)
chore(version): bump to 0.9.2v0.9.2
-rw-r--r--internal/cli/release.go4
-rw-r--r--internal/release/release.go146
-rw-r--r--internal/version/version.go2
3 files changed, 137 insertions, 15 deletions
diff --git a/internal/cli/release.go b/internal/cli/release.go
index 848d28a..86cc5f9 100644
--- a/internal/cli/release.go
+++ b/internal/cli/release.go
@@ -325,6 +325,10 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
if len(missingCodeberg) > 0 && codebergOrg != nil {
+ // Ensure Releases feature is enabled on Codeberg before creating releases
+ if err := releaseManager.EnsureCodebergReleasesEnabled(codebergOrg.Name, repoName); err != nil {
+ fmt.Printf(" Warning: Could not ensure Codeberg releases are enabled: %v\n", err)
+ }
for _, tag := range missingCodeberg {
// Skip if configured to skip this repo/tag
if cfg.ShouldSkipRelease(repoName, tag) {
diff --git a/internal/release/release.go b/internal/release/release.go
index 9e9b2e5..2746389 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -52,7 +52,65 @@ func (m *Manager) SetCodebergToken(token string) {
// SetAITool sets the AI tool to use for release notes generation
func (m *Manager) SetAITool(tool string) {
- m.aiTool = tool
+ m.aiTool = tool
+}
+
+// EnsureCodebergReleasesEnabled ensures that the Codeberg repository has the
+// Releases feature enabled. If it's disabled, attempts to enable it via API.
+func (m *Manager) EnsureCodebergReleasesEnabled(owner, repo string) error {
+ if m.codebergToken == "" {
+ return fmt.Errorf("Codeberg token is required to manage repository settings")
+ }
+
+ // Fetch repository metadata
+ infoURL := fmt.Sprintf("https://codeberg.org/api/v1/repos/%s/%s", owner, repo)
+ getReq, err := http.NewRequest("GET", infoURL, nil)
+ if err != nil {
+ return err
+ }
+ getReq.Header.Set("Authorization", "token "+m.codebergToken)
+ resp, err := (&http.Client{}).Do(getReq)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to get repo info: %s - %s", resp.Status, string(body))
+ }
+
+ var repoInfo struct{
+ HasReleases bool `json:"has_releases"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&repoInfo); err != nil {
+ return fmt.Errorf("failed to parse repo info: %w", err)
+ }
+ if repoInfo.HasReleases {
+ return nil
+ }
+
+ // Enable releases via PATCH
+ payload := map[string]any{"has_releases": true}
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+ patchReq, err := http.NewRequest("PATCH", infoURL, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ patchReq.Header.Set("Authorization", "token "+m.codebergToken)
+ patchReq.Header.Set("Content-Type", "application/json")
+ patchResp, err := (&http.Client{}).Do(patchReq)
+ if err != nil {
+ return err
+ }
+ defer patchResp.Body.Close()
+ if patchResp.StatusCode != 200 {
+ pbody, _ := io.ReadAll(patchResp.Body)
+ return fmt.Errorf("failed to enable releases: %s - %s", patchResp.Status, string(pbody))
+ }
+ return nil
}
// isVersionTag checks if a tag name is a version tag
@@ -635,21 +693,81 @@ func (m *Manager) CreateCodebergRelease(owner, repo, tag, releaseNotes string) e
}
defer resp.Body.Close()
- if resp.StatusCode != 201 {
- body, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode != 201 {
+ body, _ := io.ReadAll(resp.Body)
+
+ // Provide a more actionable hint when the repository is missing or owner/repo is wrong
+ if resp.StatusCode == 404 {
+ // Probe repository details to distinguish scenarios
+ probeURL := fmt.Sprintf("https://codeberg.org/api/v1/repos/%s/%s", owner, repo)
+ probeReq, perr := http.NewRequest("GET", probeURL, nil)
+ if perr == nil {
+ // Prefer probing with the same token
+ if m.codebergToken != "" {
+ probeReq.Header.Set("Authorization", "token "+m.codebergToken)
+ }
+ if probeResp, perr2 := (&http.Client{}).Do(probeReq); perr2 == nil {
+ defer probeResp.Body.Close()
+ if probeResp.StatusCode == 200 {
+ // Try to detect if releases are disabled
+ var repoInfo struct{ HasReleases bool `json:"has_releases"` }
+ if data, rerr := io.ReadAll(probeResp.Body); rerr == nil {
+ _ = json.Unmarshal(data, &repoInfo)
+ if !repoInfo.HasReleases {
+ // Try to enable releases automatically and retry creation
+ if err := m.EnsureCodebergReleasesEnabled(owner, repo); err != nil {
+ return fmt.Errorf(
+ "failed to create Codeberg release: releases are disabled for %s/%s and enabling via API failed: %v. Raw response: %s",
+ owner, repo, err, string(body),
+ )
+ }
+ // Retry POST after enabling
+ retryReq, rerr := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
+ if rerr != nil {
+ return rerr
+ }
+ retryReq.Header.Set("Authorization", "token "+m.codebergToken)
+ retryReq.Header.Set("Content-Type", "application/json")
+ retryReq.Header.Set("Accept", "application/json")
+ retryResp, rerr := (&http.Client{}).Do(retryReq)
+ if rerr != nil {
+ return rerr
+ }
+ defer retryResp.Body.Close()
+ if retryResp.StatusCode != 201 {
+ rbody, _ := io.ReadAll(retryResp.Body)
+ return fmt.Errorf("failed to create Codeberg release after enabling releases: %s - %s", retryResp.Status, string(rbody))
+ }
+ // Success after enabling
+ return nil
+ }
+ }
+ // Repo exists and has releases; likely permission/scope issue
+ return fmt.Errorf(
+ "failed to create Codeberg release: repo %s/%s exists but returned 404 on release creation. This usually indicates the token lacks write permissions to this repository or owner. Ensure the token belongs to '%s' (or a collaborator/maintainer) and has repository write access. Raw response: %s",
+ owner, repo, owner, string(body),
+ )
+ }
+ }
+ }
+ return fmt.Errorf(
+ "failed to create Codeberg release: repository %s/%s not found (404). Verify your Codeberg owner ('organizations[].name') matches the actual owner for this repo and that the repository exists. If needed, create it first, e.g.: gitsyncer sync repo %s --create-codeberg-repos. Raw response: %s",
+ owner, repo, repo, string(body),
+ )
+ }
- // Special handling for known Gitea issue
- if resp.StatusCode == 409 && strings.Contains(string(body), "Release is has no Tag") {
- // This is a known Gitea bug - the tag exists but Gitea can't create a release for it
- // Check if it's one of the problematic old tags
- fmt.Printf("\nWARNING: Codeberg/Gitea returned 'Release is has no Tag' error for tag %s\n", tag)
- fmt.Printf("This is a known issue with some old tags. The tag exists but cannot have a release created via API.\n")
- fmt.Printf("You may need to create this release manually through the Codeberg web interface.\n\n")
- return fmt.Errorf("cannot create release for tag %s due to Gitea API limitation", tag)
- }
+ // Special handling for known Gitea issue
+ if resp.StatusCode == 409 && strings.Contains(string(body), "Release is has no Tag") {
+ // This is a known Gitea bug - the tag exists but Gitea can't create a release for it
+ // Check if it's one of the problematic old tags
+ fmt.Printf("\nWARNING: Codeberg/Gitea returned 'Release is has no Tag' error for tag %s\n", tag)
+ fmt.Printf("This is a known issue with some old tags. The tag exists but cannot have a release created via API.\n")
+ fmt.Printf("You may need to create this release manually through the Codeberg web interface.\n\n")
+ return fmt.Errorf("cannot create release for tag %s due to Gitea API limitation", tag)
+ }
- return fmt.Errorf("failed to create Codeberg release: %s - %s", resp.Status, string(body))
- }
+ return fmt.Errorf("failed to create Codeberg release: %s - %s", resp.Status, string(body))
+ }
return nil
}
diff --git a/internal/version/version.go b/internal/version/version.go
index 3cf9fe9..7872b56 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -7,7 +7,7 @@ import (
var (
// Version is the current version of gitsyncer
- Version = "0.9.1"
+ Version = "0.9.2"
// GitCommit is the git commit hash at build time
GitCommit = "unknown"