fix(install): propagate Description, detect pre-existing package, stop silent success#106
Open
dme007 wants to merge 2 commits into
Open
fix(install): propagate Description, detect pre-existing package, stop silent success#106dme007 wants to merge 2 commits into
dme007 wants to merge 2 commits into
Conversation
…ploy Two chained defects silently faked success in `vsp install zadt-vsp` and the MCP InstallZadtVsp handler: 1. Neither caller populated WriteSourceOptions.Description. For each of the 9 embedded objects, writeSourceCreate() bailed out at the "Description is required for creating new objects" guard — but it does so by stuffing the message into result.Message and returning err=nil. 2. The deploy loops used `_, err := client.WriteSource(...)` and only branched on err. With err always nil the loop printed "OK" / "✓ Deployed" and reported "DEPLOYMENT COMPLETE, Deployed: 9" while the HTTP trace showed only GET /source/main → 404 per object and no POST at all. The package was created, the objects were not. The minimal fix: - Pass obj.Description (already present on ObjectInfo) into opts. - Inspect the returned WriteSourceResult: if err != nil OR res == nil OR !res.Success, mark the object as FAILED and show res.Message. This closes the silent-success path for install. The broader bug class — writeSourceCreate/Update returning (result, nil) on failure — is left for a follow-up; this change at least makes install fail loudly when writeSourceCreate does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GetPackage uses the nodestructure API, and its parser never populates
PackageContent.URI. The install flows at devops.go:3063 (zadt-vsp) and
devops.go:3315 (abapgit) gated the "create package" step on `pkg.URI != ""`
so the gate always failed open and CreateObject was invoked on every run.
Rerunning the installer against an already-installed system therefore
reached CreateObject, SAP returned 400 ExceptionResourceAlreadyExists,
and reconcileFailedCreate then treated the 400 as partial persistence
and tried to LOCK + DELETE the pre-existing package — which only
did not destroy user data because the package already contained objects
and SAP refused the delete with PAK/051.
This patch adds a public Client.PackageExists probe that GETs
/sap/bc/adt/packages/{name} directly (200 → exists, 404 → not, anything
else is an error) and uses it in both install flows. The reconcile path
is untouched here; the data-safety follow-up to make reconcileFailedCreate
treat ExceptionResourceAlreadyExists as "pre-existing, do not cleanup"
is tracked separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent defects in
vsp install zadt-vsp(and its MCP-handlertwin,
InstallZadtVsp). Surfaced running the installer end-to-endagainst a fresh S/4HANA DEV client with
VSP_HTTP_TRACE=1— the toolreported
DEPLOYMENT COMPLETE, Deployed: 9twice in a row while thetrace showed no object ever reached SAP. A follow-up rerun against an
already-installed system then escalated: the same code tried to LOCK +
DELETE the pre-existing
$ZADT_VSPpackage and was only saved by SAPrefusing the delete because the package still contained objects
(
PAK/051).Description+ surface!result.Success— per-objectdeploy silently reported OK when the object was never written.
create a package that already exists, fell into the partial-create
reconcile path, and attempted a destructive cleanup on foreign state.
A third, deeper defect —
reconcileFailedCreatetreating a 400ExceptionResourceAlreadyExistsas partial persistence and runningLOCK + DELETE on pre-existing state — is out of scope here and tracked
as a data-safety follow-up. The install flows no longer reach it
after fix #2, but the reconciler is still latently dangerous for any
other
CreateObjectcaller that can race with pre-existing state.1. Propagate
Description; treat!result.Successas failureChained defect between the install driver and
WriteSource:cmd/vsp/devops.go:3160andinternal/mcp/handlers_install.go:429both build
WriteSourceOptionswithoutDescription, even thoughevery
embedded.ObjectInfoalready carries one.pkg/adt/workflows_source.go:300-303,writeSourceCreateshort-circuits with
result.Message = "Description is required for creating new objects"and returnsnilas the error — it stuffs thefailure into the result struct.
*WriteSourceResult(_, err := …) andonly branch on
err. Witherralwaysnil, every object printedOK/✓ Deployedand the summary reported full success.Observable symptom in the HTTP trace: per object a single
GET /source/main → 404(the existence probe) and then the nextobject's probe. No POST, no PUT, no activation — and nine green ticks.
Fix:
obj.Descriptionintoopts.err != nilorres == nilor!res.Success, mark the object as FAILED and printres.Message.The broader "return
(result, nil)on failure" pattern inwriteSourceCreate/writeSourceUpdateis a bug class, not a singlebug, and is left for a follow-up; this change at least makes the
install loops fail loudly when they trip it.
2. Detect pre-existing package via direct probe
GetPackagereads the nodestructure API(
POST /repository/nodestructure?parent_name=$ZADT_VSP&parent_type=DEVC/K),and its parser in
pkg/adt/client.gonever populatesPackageContent.URI. Both install flows gated their "create package"step on
pkg.URI != "":Because
URIis never set, the gate always failed open. On first runthe package did not exist and the subsequent
CreateObjectsucceededso nothing looked wrong. On second run:
The reconciler in
pkg/adt/crud.go:reconcileFailedCreateinterpretedthe 200 on the existence probe as "SAP partially persisted our create"
and ran the compensating cleanup — on a package full of the user's
prior objects. Only
PAK/051prevented data loss.Why nodestructure cannot fix this: for both "package does not exist"
and "package exists but has no children" it returns the same empty
tree. There is no way to tell the two apart from its response.
Fix — add a public
Client.PackageExistsprobe that hits/sap/bc/adt/packages/{name}directly:Both install flows switch to
PackageExists. On a probe error we fallthrough to the create path and let SAP's own error surface there,
preserving existing behaviour for transient failures.
After this change the rerun cleanly reports
Package $ZADT_VSP existsand skips
CreateObjectentirely; the dangerous reconcile branchis not reached.
Files changed
Related issues
None filed yet — both defects were caught during live install
verification on a fresh DEV client.
Non-goals / follow-ups
Data-safety: reconcileFailedCreate guard on ExceptionResourceAlreadyExists.
pkg/adt/crud.go:392currently treats anyCreateObjectfailurewhose existence probe returns 200 as partial persistence, including
the 400
ExceptionResourceAlreadyExistscase where the object wasthere before our request. LOCK + DELETE on pre-existing state is
categorically wrong and only failed safe here because SAP refused to
delete a non-empty package. To track separately; requires sniffing
the exception
typefrom the original error body and short-circuitingcleanup when it is
ExceptionResourceAlreadyExists.Bug class:
writeSourceCreate/writeSourceUpdatereturn(result, nil)on failure. Every failure path in these functionsstuffs the reason into
result.Messageand returns a nil error. Anycaller that uses the Go-idiomatic
_, err := …pattern silently seessuccess. The install loops are one known victim; a sweep for the
pattern and a conversion to proper error returns is the real fix.