gastownhall/beads · error

failed to create milestone: %w

Error message

failed to create milestone: %w

What it means

CreateMilestone POSTs to /projects/:id/milestones through doRequest, which handles authentication, retries, and HTTP error statuses. This error wraps any doRequest failure — network errors, timeouts, context cancellation, or HTTP errors such as 400 (invalid/duplicate title), 401/403 (auth), or 422. It is raised before JSON parsing, so the write never completed.

Source

Thrown at internal/gitlab/client.go:471

	if len(milestones) == 0 {
		return nil, nil
	}

	return &milestones[0], nil
}

// CreateMilestone creates a new milestone in GitLab.
func (c *Client) CreateMilestone(ctx context.Context, title, description string) (*Milestone, error) {
	body := map[string]interface{}{
		"title":       title,
		"description": description,
	}

	urlStr := c.buildURL("/projects/"+c.projectPath()+"/milestones", nil)
	respBody, _, err := c.doRequest(ctx, http.MethodPost, urlStr, body)
	if err != nil {
		return nil, fmt.Errorf("failed to create milestone: %w", err)
	}

	var milestone Milestone
	if err := json.Unmarshal(respBody, &milestone); err != nil {
		return nil, fmt.Errorf("failed to parse milestone response: %w", err)
	}

	return &milestone, nil
}

// UpdateMilestone updates an existing milestone in GitLab.
func (c *Client) UpdateMilestone(ctx context.Context, milestoneID int, updates map[string]interface{}) (*Milestone, error) {
	urlStr := c.buildURL("/projects/"+c.projectPath()+"/milestones/"+strconv.Itoa(milestoneID), nil)
	respBody, _, err := c.doRequest(ctx, http.MethodPut, urlStr, updates)
	if err != nil {
		return nil, fmt.Errorf("failed to update milestone: %w", err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap the error to read the HTTP status; 400/422 typically means the title already exists or is invalid
  2. Use a unique title (e.g. version-suffixed) and validate it is non-empty before calling
  3. Ensure the token has api scope and Developer+ role with milestone-create permission on the project
  4. Confirm the configured project path is correct and retry with backoff on transient transport errors

Example fix

// before
m, err := client.CreateMilestone(ctx, title, desc)
if err != nil { return err }
// after
if title == "" { return errors.New("milestone title required") }
m, err := client.CreateMilestone(ctx, title, desc)
if err != nil {
    log.Printf("create milestone %q failed: %v", title, err)
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(title) == "" {
    return errors.New("milestone title is required")
}
// Pre-check duplicates to avoid a 400 from GitLab
existing, _ := client.FetchMilestones(ctx, "active")
for _, m := range existing {
    if m.Title == title {
        return fmt.Errorf("milestone %q already exists", title)
    }
}

Type guard

func isConflict(err error) bool {
    return strings.Contains(err.Error(), "400") || strings.Contains(err.Error(), "422")
}

Try / catch

m, err := client.CreateMilestone(ctx, title, desc)
if err != nil {
    if isConflict(err) {
        return findOrCreateMilestone(ctx, title, desc) // idempotent path
    }
    return fmt.Errorf("create milestone %q: %w", title, err)
}

Prevention

When it happens

Trigger: Calling Client.CreateMilestone with a title that already exists in the project (400), invalid/expired token (401/403), missing create permissions on the project, network outage, or a cancelled context.

Common situations: Duplicate milestone titles in the same project; token scoped without api/write access; read-only tokens used in automation; CI runners without egress to GitLab; project namespace misconfigured so the POST targets the wrong (or nonexistent) project.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/76a263533fdaf007. Report an issue: GitHub.