Skip to content
Open
Show file tree
Hide file tree
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
48 changes: 35 additions & 13 deletions cmd/vsp/devops.go
Original file line number Diff line number Diff line change
Expand Up @@ -3058,11 +3058,18 @@ func runInstallZadtVsp(cmd *cobra.Command, args []string) error {
// Phase 1: Check prerequisites
fmt.Fprintf(os.Stderr, "Checking prerequisites...\n")

// Check if package exists
packageExists := false
pkg, err := client.GetPackage(ctx, packageName)
if err == nil && pkg.URI != "" {
packageExists = true
// Check if package exists.
// GetPackage reads the nodestructure API and cannot distinguish
// "package does not exist" from "package exists but has no children",
// so we use the direct PackageExists probe here. If the probe itself
// errors (5xx, network), we fall through to the create path and let
// SAP's own error surface there.
packageExists, err := client.PackageExists(ctx, packageName)
if err != nil {
fmt.Fprintf(os.Stderr, " Package %s existence check failed: %v — will attempt create\n", packageName, err)
packageExists = false
}
if packageExists {
fmt.Fprintf(os.Stderr, " Package %s exists\n", packageName)
} else {
fmt.Fprintf(os.Stderr, " Package %s will be created\n", packageName)
Expand Down Expand Up @@ -3158,14 +3165,23 @@ func runInstallZadtVsp(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, " [%d/%d] %s ... ", i+1, len(objects), obj.Name)

opts := &adt.WriteSourceOptions{
Package: packageName,
Mode: adt.WriteModeUpsert,
Package: packageName,
Description: obj.Description,
Mode: adt.WriteModeUpsert,
}
_, err := client.WriteSource(ctx, obj.Type, obj.Name, obj.Source, opts)
if err != nil {
res, err := client.WriteSource(ctx, obj.Type, obj.Name, obj.Source, opts)
switch {
case err != nil:
fmt.Fprintf(os.Stderr, "FAILED: %v\n", err)
failed++
} else {
case res == nil || !res.Success:
msg := "unknown failure"
if res != nil && res.Message != "" {
msg = res.Message
}
fmt.Fprintf(os.Stderr, "FAILED: %s\n", msg)
failed++
default:
fmt.Fprintf(os.Stderr, "OK\n")
deployed++
}
Expand Down Expand Up @@ -3301,10 +3317,16 @@ func runInstallAbapGit(cmd *cobra.Command, args []string) error {

ctx := context.Background()

// Ensure package exists
// Ensure package exists. PackageExists probes /packages/{name} directly;
// a GetPackage-based check cannot distinguish "absent" from "present but
// empty" because nodestructure returns an empty tree in both cases.
fmt.Fprintf(os.Stderr, "Checking package %s...\n", packageName)
pkg, pkgErr := client.GetPackage(ctx, packageName)
if pkgErr != nil || pkg.URI == "" {
exists, pkgErr := client.PackageExists(ctx, packageName)
if pkgErr != nil {
fmt.Fprintf(os.Stderr, " Package existence check failed: %v — will attempt create\n", pkgErr)
exists = false
}
if !exists {
fmt.Fprintf(os.Stderr, "Creating package %s...\n", packageName)
err = client.CreateObject(ctx, adt.CreateObjectOptions{
ObjectType: adt.ObjectTypePackage,
Expand Down
19 changes: 14 additions & 5 deletions internal/mcp/handlers_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,14 +427,23 @@ func (s *Server) handleInstallZADTVSP(ctx context.Context, request mcp.CallToolR

// Use WriteSource to create/update
opts := &adt.WriteSourceOptions{
Package: packageName,
Mode: adt.WriteModeUpsert,
Package: packageName,
Description: obj.Description,
Mode: adt.WriteModeUpsert,
}
_, err := s.adtClient.WriteSource(ctx, obj.Type, obj.Name, obj.Source, opts)
if err != nil {
res, err := s.adtClient.WriteSource(ctx, obj.Type, obj.Name, obj.Source, opts)
switch {
case err != nil:
fmt.Fprintf(&sb, "✗ Failed: %v\n", err)
failed = append(failed, obj.Name+": "+err.Error())
} else {
case res == nil || !res.Success:
msg := "unknown failure"
if res != nil && res.Message != "" {
msg = res.Message
}
fmt.Fprintf(&sb, "✗ Failed: %s\n", msg)
failed = append(failed, obj.Name+": "+msg)
default:
sb.WriteString("✓ Deployed\n")
deployed = append(deployed, obj.Name)
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/adt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package adt
import (
"context"
"encoding/xml"
"errors"
"fmt"
"html"
"net/http"
Expand Down Expand Up @@ -811,6 +812,33 @@ func (c *Client) GetMessageClass(ctx context.Context, msgClassName string) (*Mes

// --- Package Operations ---

// PackageExists returns true if SAP has a package with the given name.
// It probes /sap/bc/adt/packages/{name} directly: 200 → exists, 404 → not,
// any other outcome (5xx, network, auth) is returned as an error so the
// caller does not silently classify a transient failure as "missing".
//
// Unlike GetPackage, which reads the nodestructure API and cannot distinguish
// "package does not exist" from "package exists but has no children" (both
// return an empty tree), this is a definitive existence check.
func (c *Client) PackageExists(ctx context.Context, packageName string) (bool, error) {
if packageName == "" {
return false, fmt.Errorf("empty package name")
}
objectURL := fmt.Sprintf("/sap/bc/adt/packages/%s", url.PathEscape(strings.ToUpper(packageName)))
_, err := c.transport.Request(ctx, objectURL, &RequestOptions{
Method: http.MethodGet,
Accept: "application/*",
})
if err == nil {
return true, nil
}
var apiErr *APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}

// GetPackage retrieves the contents of a package using the nodestructure API.
func (c *Client) GetPackage(ctx context.Context, packageName string) (*PackageContent, error) {
packageName = strings.ToUpper(packageName)
Expand Down