Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions pkg/hub/skill_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package hub

import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
Expand Down Expand Up @@ -697,34 +698,52 @@ func (s *Server) publishSkillVersion(w http.ResponseWriter, r *http.Request, ski
return
}

// Check for existing published version (immutability)
// Check for an existing version with this number.
// - Draft: reuse it (idempotent retry of an incomplete publish).
// - Published/deprecated/archived: reject as conflict.
var sv *store.SkillVersion
existing, err := s.store.GetSkillVersionByNumber(ctx, skillID, req.Version)
if err == nil && existing.Status == store.SkillVersionStatusPublished {
writeError(w, http.StatusConflict, "conflict",
fmt.Sprintf("version %s is already published and immutable; publish a new version instead", req.Version), nil)
if err != nil && !errors.Is(err, store.ErrNotFound) {
writeErrorFromErr(w, err, "")
return
}

// Create draft version
sv := &store.SkillVersion{
ID: api.NewUUID(),
SkillID: skillID,
Version: req.Version,
Status: store.SkillVersionStatusDraft,
if err == nil {
switch existing.Status {
case store.SkillVersionStatusDraft:
sv = existing
case store.SkillVersionStatusPublished:
writeError(w, http.StatusConflict, "conflict",
fmt.Sprintf("version %s is already published and immutable; publish a new version instead", req.Version), nil)
return
default:
writeError(w, http.StatusConflict, "conflict",
fmt.Sprintf("version %s already exists with status %q", req.Version, existing.Status), nil)
return
}
}
Comment on lines 705 to 723

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code currently ignores any error returned by GetSkillVersionByNumber and proceeds to create a new draft version. If the error is a legitimate database error (such as a connection failure, timeout, or query error) rather than a 'not found' error, ignoring it and attempting to write to the database is unsafe and masks the actual root cause.

We should explicitly check if the error is store.ErrNotFound and return any other error. Note that you will need to import the standard `

existing, err := s.store.GetSkillVersionByNumber(ctx, skillID, req.Version)
	if err != nil && !errors.Is(err, store.ErrNotFound) {
		writeErrorFromErr(w, err, "")
		return
	}
	if err == nil {
		switch existing.Status {
		case store.SkillVersionStatusDraft:
			sv = existing
		case store.SkillVersionStatusPublished:
			writeError(w, http.StatusConflict, "conflict",
				fmt.Sprintf("version %s is already published and immutable; publish a new version instead", req.Version), nil)
			return
		default:
			writeError(w, http.StatusConflict, "conflict",
				fmt.Sprintf("version %s already exists with status %q", req.Version, existing.Status), nil)
			return
		}
	}


if identity := GetIdentityFromContext(ctx); identity != nil {
sv.PublisherID = identity.ID()
}
if sv == nil {
// Create new draft version
sv = &store.SkillVersion{
ID: api.NewUUID(),
SkillID: skillID,
Version: req.Version,
Status: store.SkillVersionStatusDraft,
}

if err := s.store.CreateSkillVersion(ctx, sv); err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
writeError(w, http.StatusConflict, "conflict",
fmt.Sprintf("version %s already exists for this skill", req.Version), nil)
if identity := GetIdentityFromContext(ctx); identity != nil {
sv.PublisherID = identity.ID()
}

if err := s.store.CreateSkillVersion(ctx, sv); err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
writeError(w, http.StatusConflict, "conflict",
fmt.Sprintf("version %s already exists for this skill", req.Version), nil)
return
}
writeErrorFromErr(w, err, "")
return
}
writeErrorFromErr(w, err, "")
return
}

response := PublishVersionResponse{
Expand Down
Loading