---
# PARITY MANIFEST SCHEMA — copy to services/<svc>/PARITY.md, fill, keep updated.
# Purpose: record audit state so the NEXT audit diffs the delta instead of rescanning.
# Re-audit protocol: `git diff <last_audit_commit>..HEAD -- services/<svc>/` for local drift,
# AND check the SDK module for ops added since sdk_version. Only audit changed/new surface;
# trust rows marked ok whose files are unchanged since last_audit_commit.
service: codedeploy
sdk_module: aws-sdk-go-v2/service/codedeploy@v1.43.0 # version audited against; corrected from stale v1.37.0 pin
last_audit_commit: b1905140e # gopherstack-xhu2t reqfielddiff tier-1 sweep
last_audit_date: 2026-10-10
overall: A # A = genuine fixes found; B = already-accurate, proven op-by-op
# Per-op or per-op-family status. Values: ok | partial | gap | deferred.
# wire=response/request shape vs SDK; errors=code+HTTP status; state=real mutate/read; persist=in backendSnapshot.
ops:
CreateApplication: {wire: ok, errors: ok, state: ok, persist: ok}
GetApplication: {wire: ok, errors: ok, state: ok, persist: ok, note: "createTime was UnixMilli int64, fixed to awstime.Epoch float64"}
ListApplications: {wire: ok, errors: ok, state: ok, persist: ok, note: "TRIAGED (gopherstack-a250): input was a literal struct{}; real ListApplicationsInput (api_op_ListApplications.go) has an optional NextToken member, discarded. NOT wired: this service never truncates any List* response (verified across all 8 List ops in this file, not just the struct{} ones -- ListDeployments/ListOnPremisesInstances/ListDeploymentGroups/etc. have no pagination either), so NextToken has no continuation state to represent and no caller-supplied token could ever produce a response different from the unconditional full list already returned. x
DeleteApplication: {wire: ok, errors: partial, state: ok, persist: ok, note: "TRIAGED (gopherstack-3pz8): DeleteApplication's own deserializer models no ApplicationDoesNotExistException at all (only ApplicationNameRequiredException/InvalidApplicationNameException/InvalidRoleException) -- the not-found path is provably wrong, but whether the real op is a silent idempotent success or maps to a different code is unconfirmed (doc page shows only generic '200 empty body' boilerplate, not a semantic idempotent-delete sentence). Landmine comment left at the call site; unfixed pending evidence"}
UpdateApplication: {wire: ok, errors: ok, state: ok, persist: ok}
BatchGetApplications: {wire: ok, errors: n/a, state: ok, persist: ok, note: "same createTime fix as GetApplication"}
CreateDeploymentGroup: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED (gopherstack-455l): Ec2TagFilters/Ec2TagSet and OnPremisesInstanceTagFilters/OnPremisesTagSet were both accepted unconditionally though both ops model 'only one of these data types can be used in a single call' (InvalidEC2TagCombinationException/InvalidOnPremisesTagCombinationException, types/errors.go:1579-1580,2122-2123); now rejected when both halves of a pair are set. Also added Ec2TagFilters[].Type validation against the real KEY_ONLY|VALUE_ONLY|KEY_AND_VALUE enum (InvalidEC2TagException, types/errors.go:1606) -- empty Type stays legal since validators.go does not require EC2TagFilter.Type. OnPremisesInstanceTagFilters[].Type is NOT validated: no matching 'invalid format' error is modeled on this op (InvalidTagFilterException belongs to ListOnPremisesInstances only)"}
GetDeploymentGroup: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED (6flj wrapper-key sweep): real DeploymentGroupInfo has 23 keys (deserializers.go's awsAwsjson11_deserializeDocumentDeploymentGroupInfo); gopherstack had 20, missing lastAttemptedDeployment/lastSuccessfulDeployment/targetRevision. Added InMemoryBackend.LastDeploymentsForGroup deriving both from real per-group deployment history (Deployment.CreateTime/Status/Revision, already tracked); targetRevision taken from the most-recently-ATTEMPTED deployment's own revision (the plain reading of 'target' -- the SDK's own doc comment does not distinguish attempted-vs-successful, so this specific choice is an interpretation, disclosed here, not independently confirmed against a live account)"}
ListDeploymentGroups: {wire: ok, errors: ok, state: ok, persist: ok}
DeleteDeploymentGroup: {wire: ok, errors: partial, state: ok, persist: ok, note: "TRIAGED (gopherstack-3pz8): DeleteDeploymentGroup's own deserializer models neither ApplicationDoesNotExistException nor DeploymentGroupDoesNotExistException (only *NameRequiredException/Invalid*NameException/InvalidRoleException) -- both current not-found paths are provably wrong, remedy (silent success vs. a different code) unconfirmed. Landmine comments left at both call sites; unfixed pending evidence"}
UpdateDeploymentGroup: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED (gopherstack-455l): same Ec2TagFilters/Ec2TagSet and OnPremisesInstanceTagFilters/OnPremisesTagSet mutual-exclusion + Ec2TagFilters[].Type validation as CreateDeploymentGroup -- see that entry"}
BatchGetDeploymentGroups: {wire: ok, errors: ok, state: ok, persist: ok, note: "same lastAttemptedDeployment/lastSuccessfulDeployment/targetRevision fix as GetDeploymentGroup (shared deploymentGroupOutputWithHistory converter)"}
CreateDeployment: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED this pass: fileExistsBehavior was accepted and stored unvalidated (any garbage string round-tripped); now validated against the real DISALLOW|OVERWRITE|RETAIN enum, InvalidFileExistsBehaviorException (confirmed in CreateDeployment's own error set, deserializers.go) for anything else. FIXED (2026-09-18, gopherstack-xhu2t reqfielddiff tier-1): deploymentConfigName was entirely undeclared on createDeploymentInput -- CreateDeployment always silently used the deployment group's own DeploymentConfigName, dropping any per-deployment override. Now read, validated against b.deploymentConfigs (DeploymentConfigDoesNotExistException if unknown, a documented CreateDeployment error), and used in place of the group's default when supplied. Proven by Test_SDKRoundTrip_CreateDeployment_DeploymentConfigNameOverride (real SDK client). DeploymentMode is a separate, NOT fixed finding -- see items_still_open. AutoRollbackConfiguration, OverrideAlarmConfiguration and TargetInstances are stored and echoed (tagFilters together with ec2TagSet is InvalidTargetInstancesException); the deployment records computePlatform and deploymentStyle from its application and group."}
GetDeployment: {wire: ok, errors: ok, state: ok, persist: ok, note: "createTime/completeTime were UnixMilli int64, fixed to awstime.Epoch float64 DeploymentInfo carries autoRollbackConfiguration, overrideAlarmConfiguration, targetInstances, computePlatform and deploymentStyle."}
ListDeployments: {wire: ok, errors: n/a, state: ok, persist: ok, note: "createTimeRange.start/end request fields were parsed as epoch-millis (time.UnixMilli), fixed to epoch-seconds float64 matching smithytime.FormatEpochSeconds. FIXED (gopherstack-3qel): externalId was modeled on ListDeploymentsInput but never parsed, so the filter was silently ignored. Nothing in this backend can populate a deployment's ExternalID -- CreateDeploymentInput has no such field (api_op_CreateDeployment.go); the real value only ever comes from AWS-side CodePipeline/CloudFormation integrations, per DeploymentInfo.ExternalId's own doc comment (types/types.go:420) -- so now parses/filters on it, correctly returning zero results for any non-empty value instead of ignoring it"}
StopDeployment: {wire: ok, errors: ok, state: ok, persist: ok, note: "output status was returning the deployment's own status literal (Stopped), fixed to the real StopStatus enum (Succeeded); deployment status itself still correctly becomes Stopped. FIXED (6flj wrapper-key sweep): real StopDeploymentOutput also has statusMessage (deserializers.go's awsAwsjson11_deserializeOpDocumentStopDeploymentOutput case 'statusMessage'), never modeled at all; added, text sourced verbatim from the SDK's own doc comment for the Succeeded StopStatus value"}
ContinueDeployment: {wire: ok, errors: ok, state: ok, note: "FIXED this pass: READY_WAIT/TERMINATION_WAIT are not DeploymentStatus values at all (they're ContinueDeploymentInput.DeploymentWaitType, an input enum) -- the prior 'blue/green wait-state' framing conflated the two. The real gap was narrower: ContinueDeployment accepted a deployment in ANY status and deploymentWaitType was read off the wire and never validated or used. Added the real precondition (status must be Ready, else DeploymentIsNotInReadyStateException/DeploymentAlreadyCompletedException per types/errors.go:221,556-557) and deploymentWaitType enum validation (InvalidDeploymentWaitTypeException). Since CreateDeployment completes synchronously and no op ever sets status=Ready, ContinueDeployment now always errors in practice -- which is the honest behavior for a backend with no genuine blue/green wait state, not a regression"}
SkipWaitTimeForInstanceTermination: {wire: ok, errors: ok, state: ok, persist: n/a, note: "was missing the deploymentId existence check every sibling deployment-scoped op has; fixed"}
BatchGetDeployments: {wire: ok, errors: n/a, state: ok, persist: ok, note: "same createTime/completeTime fix as GetDeployment same DeploymentInfo members as GetDeployment."}
BatchGetDeploymentInstances: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: resolves real on-premises instances matched against the deployment group's tag filters (matchesOnPremisesTargeting), status derived from the deployment's own Status via targetStatusForDeployment instead of hardcoded Succeeded; unmatched/never-registered IDs silently omitted matching the codebase's Batch* convention"}
BatchGetDeploymentTargets: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: instanceTarget/ecsTarget/lambdaTarget union resolved from real deployment group config (on-prem tag matching / ECSServices list / single Lambda target); deploymentTargetType + nested member wire shape verified against types.DeploymentTarget deserializer"}
GetDeploymentInstance: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: real lookup against computed targets, InstanceDoesNotExistException for a real-but-non-participating or unknown instance instead of fabricating a match for any ID"}
GetDeploymentTarget: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: real lookup against computed targets, new DeploymentTargetDoesNotExistException (404) sentinel for an unknown target ID instead of fabricating a match"}
ListDeploymentInstances: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: returns the real matched instanceTarget IDs for Server/Lambda platforms; empty for ECS (no per-instance concept) -- previously always empty regardless of real targets"}
ListDeploymentTargets: {wire: ok, errors: ok, state: ok, persist: n/a, note: "FIXED this pass: returns the real computed target IDs (sorted) -- previously always empty regardless of real targets"}
PutLifecycleEventHookExecutionStatus: {wire: ok, errors: ok, state: ok, persist: n/a, note: "was missing the deploymentId existence check every sibling deployment-scoped op has; fixed"}
CreateDeploymentConfig: {wire: ok, errors: ok, state: ok, persist: ok}
GetDeploymentConfig: {wire: ok, errors: ok, state: ok, persist: ok, note: "createTime was UnixMilli int64, fixed to awstime.Epoch float64"}
ListDeploymentConfigs: {wire: ok, errors: n/a, state: ok, persist: ok, note: "TRIAGED (gopherstack-a250): same NextToken-inert finding as ListApplications -- see that row."}
DeleteDeploymentConfig: {wire: ok, errors: partial, state: ok, persist: ok, note: "TRIAGED (gopherstack-3pz8): DeleteDeploymentConfig's own deserializer models no DeploymentConfigDoesNotExistException (only DeploymentConfigInUseException/DeploymentConfigNameRequiredException/InvalidDeploymentConfigNameException/InvalidOperationException) -- the not-found path is provably wrong. InvalidOperationException (already used a few lines below for the built-in-config case) is a plausible candidate but its description is generic, not evidence this case reuses it; silent success is equally plausible. Landmine comment left at the call site; unfixed pending evidence"}
RegisterApplicationRevision: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED this pass: persists to a real applicationRevisions store.Table keyed by (appName, canonical revision JSON); re-registering an already-known revision refreshes description, preserves original registerTime"}
GetApplicationRevision: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED this pass: reads the persisted revision, populates revisionInfo (GenericRevisionInfo: description/registerTime/firstUsedTime/lastUsedTime/deploymentGroups, field names+epoch-seconds verified against deserializers.go), new RevisionDoesNotExistException (404) for an unregistered revision instead of echoing the request back"}
ListApplicationRevisions: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED this pass: returns real registered revisions for the application with deployed/s3Bucket/s3KeyPrefix/sortBy/sortOrder filtering; previously always empty since nothing was ever persisted"}
BatchGetApplicationRevisions: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED this pass: genericRevisionInfo populated for revisions that are actually registered, omitted for ones that are not, instead of echoing the input unconditionally with no real lookup"}
DeleteGitHubAccountToken: {wire: ok, errors: ok, state: ok, persist: ok}
ListGitHubAccountTokenNames: {wire: ok, errors: n/a, state: ok, persist: ok, note: "TRIAGED (gopherstack-a250): same NextToken-inert finding as ListApplications -- see that row."}
RegisterOnPremisesInstance: {wire: ok, errors: ok, state: ok, persist: ok}
DeregisterOnPremisesInstance: {wire: ok, errors: partial, state: ok, persist: ok, note: "ErrOnPremisesInstanceNotFound had the wrong error code (InstanceNameRequiredException) and no errorMappings entry at all, so it fell through to 500 ServiceException; fixed to InstanceDoesNotExistException + 404. TRIAGED (gopherstack-3pz8): that fix itself is still wrong -- DeregisterOnPremisesInstance's own deserializer models neither InstanceDoesNotExistException nor any not-found code at all (only InstanceNameRequiredException/InvalidInstanceNameException). Remedy (silent success vs. a different code) unconfirmed; landmine comment left at the call site, unfixed pending evidence"}
GetOnPremisesInstance: {wire: ok, errors: ok, state: ok, persist: ok, note: "same registerTime/deregisterTime epoch fix + errorMappings fix as DeregisterOnPremisesInstance. FIXED (6flj wrapper-key sweep): real InstanceInfo has 7 keys (deserializers.go's awsAwsjson11_deserializeDocumentInstanceInfo); gopherstack had 6, missing instanceArn. Added InMemoryBackend.OnPremisesInstanceARN, reusing the same 'instance:<name>' resource format already used for the identical resource type's InstanceTarget.TargetArn (deployment_instances.go). FIXED (gopherstack-3pz8): not-found path used ErrOnPremisesInstanceNotFound (InstanceDoesNotExistException), but GetOnPremisesInstance's own deserializer models InstanceNotRegisteredException instead (confirmed against both deserializers.go and the API_GetOnPremisesInstance.html doc page's Errors section); new ErrOnPremisesInstanceNotRegistered sentinel wired in"}
ListOnPremisesInstances: {wire: ok, errors: n/a, state: ok, persist: ok}
BatchGetOnPremisesInstances: {wire: ok, errors: n/a, state: ok, persist: ok, note: "same registerTime/deregisterTime epoch fix; same instanceArn fix as GetOnPremisesInstance"}
AddTagsToOnPremisesInstances: {wire: ok, errors: ok, state: ok, persist: ok}
RemoveTagsFromOnPremisesInstances: {wire: ok, errors: ok, state: ok, persist: ok}
TagResource: {wire: ok, errors: ok, state: ok, persist: ok, note: "FIXED (6flj wrapper-key sweep, request-side, not independently observable): real TagResourceInput uses PascalCase (ResourceArn/Tags) -- the shared generic tagging shape, unlike this service's own camelCase convention -- fixed for wire-shape correctness though pkgs/service's encoding/json.Unmarshal already bound the old lowercase-tagged fields via its case-insensitive fallback, so this was never a live request-side bug"}
UntagResource: {wire: ok, errors: ok, state: ok, persist: ok, note: "same PascalCase (ResourceArn/TagKeys) request-side fix as TagResource, same non-observability caveat"}
ListTagsForResource: {wire: ok, errors: ok, state: ok, persist: ok, note: "FLAGSHIP FIX (6flj wrapper-key sweep): response was wire-tagged json:\"tags\" (lowercase); real deserializer (deserializers.go:20417, awsAwsjson11_deserializeOpDocumentListTagsForResourceOutput) switches on the case-sensitive PascalCase key \"Tags\" -- this protocol has zero body-field EqualFold calls, so a lowercase key was silently dropped by EVERY real client's ListTagsForResource call regardless of what had actually been tagged. This is the one op family in the service using AWS's shared generic tagging shape (PascalCase) instead of CodeDeploy's own camelCase convention. Fixed response (live bug) and request (ResourceArn, not independently observable -- see TagResource note) sides. Two pre-existing tests (tags_test.go) had decoded the response with a local json:\"tags\" (lowercase) struct -- because both sides used plain encoding/json with its case-insensitive fallback, those tests would have passed identically whether or not the bug was fixed, so they provided zero signal on this bug either way; updated for accuracy, but real verification is wire_field_fixes_test.go's real-SDK-client test, whose response decode goes through the actual case-sensitive generated deserializer"}
DeleteResourcesByExternalId: {wire: ok, errors: n/a, state: ok, persist: n/a, note: "no resource-by-external-id tracking exists anywhere in this backend (or any other gopherstack service); an idempotent no-op matches real AWS's own best-effort cleanup semantics"}
families:
Application: {status: ok, note: "verified wire shapes (applicationId/applicationName/computePlatform/createTime), error codes, and persistence against aws-sdk-go-v2/service/codedeploy@v1.37.0 deserializers.go"}
DeploymentGroup: {status: ok, note: "full field-by-field mapping (blue/green, alarms, triggers, ECS, tag sets, load balancer info) verified against dgToOutput/dgInputFromWire"}
Deployment: {status: ok, note: "lifecycle is synchronous: CreateDeployment immediately sets status=Succeeded with completeTime = now+5s, which is a deliberate simplification (documented in Notes) rather than a bug"}
DeploymentConfig: {status: ok, note: "9 built-in AWS default configs correctly seeded and protected from deletion (DeploymentConfigInUseException)"}
Tags: {status: ok, note: "ARN-based dispatch to application/deploymentgroup tag stores verified; on-premises instance tagging is a separate, also-correct path"}
OnPremisesInstance: {status: ok, note: "registerTime/deregisterTime epoch fix + error-code fix (earlier pass); this pass decomposed matchesTagFilters (banned gocognit nolint) and added matchesTagSetGroups/matchesOnPremisesTargeting, reused by the new deployment-target computation"}
ApplicationRevision: {status: ok, note: "FIXED this pass: real applicationRevisions store.Table (composite key appName+canonical-revision-JSON, byApplication index), wired into backendSnapshot as a 'clean' table (no live tags.Tags field). RegisterApplicationRevision persists; CreateDeployment auto-registers an unseen revision and stamps FirstUsedTime/LastUsedTime/DeploymentGroups (touchApplicationRevisionForDeployment); DeleteApplication cascades deletes (deleteApplicationRevisions), UpdateApplication rename moves revisions to the new app name (renameApplicationRevisions) -- no ghost rows in either case"}
DeploymentTarget: {status: ok, note: "FIXED this pass: GetDeploymentTarget/ListDeploymentTargets/BatchGetDeploymentTargets/GetDeploymentInstance/ListDeploymentInstances/BatchGetDeploymentInstances all resolve from deploymentTargets(), a real (not fabricated) computation over the deployment's owning deployment group: matched on-premises instances (Server), one target per configured ECS service (ECS), or the single Lambda target (Lambda) real AWS always has exactly one of for that platform. Target Status is mapped from the deployment's own current Status via targetStatusForDeployment instead of a hardcoded literal. FIXED THIS PASS (previously the known limitation): Ec2TagFilters/Ec2TagSet now resolve against real services/ec2 instances via cross_service.go's lazy SetAppConfig wiring (same pattern as services/mgn), matching non-terminated EC2 instances whose TagsForResource tags satisfy the deployment group's targeting config (matchesEc2Targeting, mirroring matchesOnPremisesTargeting's Ec2TagSet-precedes-Ec2TagFilters rule). Falls back to zero EC2-side targets when the EC2 backend isn't wired (e.g. unit tests constructing InMemoryBackend directly) -- documented, not fabricated. CONFIRMED ACCURATE (6flj wrapper-key sweep): the real DeploymentTarget union's 5th member, cloudFormationTarget, is deliberately never modeled -- this backend has no CloudFormation blue/green stack-set integration anywhere, so it can never be populated honestly; the code's own doc comment already stated this, promoted into this manifest for visibility rather than left code-only."}
cross-service: {status: fixed, note: "codedeploy now resolves the services/ec2 backend on demand via cross_service.go's siblingServices interface (GetEC2Handler), matched structurally against *CLI -- the same lazy SetAppConfig pattern services/mgn, services/grafana, and services/resiliencehub already use. *CLI already exposed GetEC2Handler() (cli.go:1134) for those services, so this needed zero cli.go changes: only provider.go gained one line (backend.SetAppConfig(ctx.Config)) and a new services/codedeploy/cross_service.go file."}
gaps: []
items_still_open: []
structural_gaps:
- "ApplicationInfo gitHubAccountName/linkedToGitHub: set only by console-driven GitHub OAuth linking; no request member exists to populate them."
- "InstanceSummary/InstanceTarget/ECSTarget/LambdaTarget lifecycleEvents, ECSTarget taskSetsInfo, LambdaTarget lambdaFunctionInfo: produced by the CodeDeploy agent / ECS and Lambda traffic shifting on real infrastructure; nothing executes hooks here."
- "CreateDeployment DeploymentMode (STANDARD/RESTART): only shortens the per-instance lifecycle-event sequence run by the on-instance agent; request-only, not echoed in DeploymentInfo, no observable effect."
deferred: # consciously not audited this pass (scope) — next pass targets
- "CreateDeployment completes synchronously (Succeeded, no Created/Queued/InProgress/Baking window): no agent or traffic shifting runs. The blue/green Ready wait is real: a BLUE_GREEN group with deploymentReadyOption.actionOnTimeout=STOP_DEPLOYMENT holds new deployments in Ready until ContinueDeployment (Succeeded) or StopDeployment (Stopped)."
leaks: {status: clean, note: "no goroutines/janitors in this service; Reset/Snapshot/Restore all close tags.Tags handles correctly on the three dirty tables (applications, deploymentGroups, onPremisesInstances). The new applicationRevisions table carries no live handles (no tags.Tags field), so it needs no Close() calls -- registered as a 'clean' store.Table on b.registry like deployments/deploymentConfigs, reset via registry.ResetAll()."}
---
-
2026-09-18 (gopherstack-xhu2t reqfielddiff tier-1 sweep): 2 tier-1 undeclared request fields.
CreateDeployment.DeploymentConfigNamefixed -- was entirely undeclared, so a per-deployment override always silently fell back to the deployment group's own config.CreateDeployment.DeploymentModerecorded as a missing feature -- no per-instance lifecycle-event execution exists anywhere in this backend for STANDARD/RESTART to shorten, and the field isn't even echoed by real AWS's DeploymentInfo. tier1 count 2 -> 1 (the DeploymentMode line is structural, not a false positive). -
2026-09-12 (typed coverage sweep, gopherstack-n3zi): added
realclient_application_and_deployment_management_test.go, driving the 22 previously typed-client-uncovered ops (BatchGetApplications, BatchGet/List DeploymentInstances/Targets, BatchGetDeployments, ContinueDeployment, Delete{DeploymentConfig,DeploymentGroup,ResourcesByExternalId}, DeregisterOnPremisesInstance, ListApplicationRevisions, ListDeploymentConfigs, ListDeploymentGroups, ListGitHubAccountTokenNames, ListOnPremisesInstances, PutLifecycleEventHookExecutionStatus, RemoveTagsFromOnPremisesInstances, SkipWaitTimeForInstanceTermination, UntagResource, UpdateApplication, UpdateDeploymentGroup) through the realaws-sdk-go-v2client. Zero real wire bugs found — consistent with this service's existing A-grade, heavily pre-audited history (6flj wrapper-key sweep, 3pz8 error-code triage, a250 pagination triage all predate this slice and already covered this surface's wire shapes). codedeploy: 25/47 -> 47/47 typed-client covered. Three of the newly-covered ops (BatchGetDeploymentInstances, ListDeploymentInstances, SkipWaitTimeForInstanceTermination) are AWS-deprecated in favor of their Target-suffixed/ContinueDeployment replacements;.golangci.ymlgained a per-file staticcheck exclusion for the expected SA1019s, following this repo's existing iotanalytics/opsworks/mediapackage precedent rather than per-line nolints. -
Protocol: awsjson1.1, single POST endpoint,
X-Amz-Target: CodeDeploy_20141006.<Op>dispatch viaRouteMatcher/ExtractOperationin handler.go. Verified every op inGetSupportedOperations()has adispatchTable()entry and is reachable — no stub registrations, no ops silently dropped. -
Epoch-seconds timestamp bug (the big one this pass): every
Timestampshape in CodeDeploy's model (createTime,completeTime,registerTime,deregisterTime, and thecreateTimeRange.start/endrequest filter) is serialized by the real SDK as an epoch-seconds JSON number (smithytime.FormatEpochSeconds/ parsed withsmithytime.ParseEpochSeconds— confirmed by readingaws-sdk-go-v2/service/codedeploy@v1.37.0/deserializers.goandserializers.godirectly). The handler was usingtime.Time.UnixMilli()(epoch milliseconds,int64) for every response timestamp andtime.UnixMilli()to parse the request-side range filter — a 1000x wire-format mismatch in both directions. Fixed by switching every timestamp field tofloat64and usingpkgs/awstime.Epoch()for output / a smallepochSecondsToTimehelper (mirrors thesecretsmanagerpackage'stime.Unix(0, int64(sec*float64(time.Second)))pattern) for input. Proven with a real-SDK-client round-trip test (handler_sdk_roundtrip_test.go) rather than just unit-level JSON assertions, since a scale-wrong-but-well-typed number silently decodes to a garbagetime.Timeinstead of erroring — exactly the kind of bug unit tests miss (see parity-principles.md rule 3). -
StopDeploymentOutput.status is a different enum than Deployment.status: real AWS's
StopStatusenum is only ever"Pending"or"Succeeded"— it describes the outcome of the stop request itself (this backend performs it synchronously, so always"Succeeded"). The Deployment's own resulting lifecycle status ("Stopped") is a completely separate field returned byGetDeployment. The handler was previously reusingstatusStoppedfor both, which is not a validStopStatusvalue and would fail real SDK unmarshaling into thetypes.StopStatustype in strict validation paths. This is a "looks-right-but-is-wrong" trap: don't re-flagGetDeploymentreturning"Stopped"after a stop — that part was and remains correct. -
ErrOnPremisesInstanceNotFoundsentinel had the wrong error code baked in ("InstanceNameRequiredException", which is actually a different real AWS exception for a missing/empty instance name) and noerrorMappingsentry at all, so any not-found lookup on an on-premises instance (GetOnPremisesInstance,DeregisterOnPremisesInstance) fell through to the generic 500ServiceExceptionbranch regardless of the sentinel's own code — the exact "missing errCodeLookup entry" bug class called out in parity-principles.md rule 2. Fixed the sentinel's code toInstanceDoesNotExistException(confirmed againsttypes.InstanceDoesNotExistExceptionin the real SDK) and added theerrorMappingsrow. -
SkipWaitTimeForInstanceTerminationandPutLifecycleEventHookExecutionStatusskipped the deployment-existence check every sibling deployment-scoped op (GetDeploymentInstance,GetDeploymentTarget,ListDeploymentInstances,ListDeploymentTargets,ContinueDeployment,StopDeployment) performs viah.Backend.GetDeployment(...). Both previously returned 200 OK for a nonexistentdeploymentId. Fixed to match the sibling pattern; real AWS returnsDeploymentDoesNotExistExceptionin both cases. -
Deliberate simplification, not a bug:
CreateDeploymentsetsStatus: "Succeeded"andCompleteTime: now + 5simmediately at creation time rather than modeling a genuineCreated → Queued → InProgress → Succeededprogression over time.ListDeployments' status filter andStopDeployment's transition to"Stopped"both work correctly against this synchronous model. Don't re-flag this as a "stuck deployment" bug — it's the opposite problem (instant-complete, not stuck), and every consumer-visible field derived from it (deploymentOverview, status filters) is internally consistent. -
DeleteResourcesByExternalIdempty-envelope return is correct, not a stub: this backend has no external-id-linked-resource tracking anywhere (nothing populates it), and the real AWS operation itself is a best-effort async cleanup with no required side effect visible to the caller synchronously. This is the "void-result op" pattern from parity-principles.md rule 4, not a disguised no-op. -
ApplicationRevision family de-stubbed (this pass):
RegisterApplicationRevision,GetApplicationRevision,ListApplicationRevisions, andBatchGetApplicationRevisionspreviously had no backing store at all —RegisterApplicationRevisionvalidated the application existed and then discarded the revision, soGetApplicationRevisionechoed the request straight back,ListApplicationRevisionsalways returned an empty list, andBatchGetApplicationRevisionsechoed each requested revision with no real lookup. Added anapplicationRevisions *store.Table[ApplicationRevision]keyed byappName + "\x00" + canonical-JSON(RevisionLocation)(applicationRevisionKeyin store_setup.go — twoRevisionLocationvalues with identical fields always produce the same key, matching how real CodeDeploy deduplicates revision registrations) with abyApplicationindex. Registered directly onb.registryas a "clean" table (no live*tags.Tagsfield, no DTO wrapper needed — see persistence.go's dirty-table doc comment).CreateDeploymentnow callstouchApplicationRevisionForDeployment, which auto-registers an unseen revision (real CodeDeploy auto-registers revisions supplied directly toCreateDeployment) and stampsFirstUsedTime/LastUsedTime/DeploymentGroups— the last of which also removes the deployment group from every other revision of the same application, since a deployment group targets exactly one revision at a time.DeleteApplication/UpdateApplicationgaineddeleteApplicationRevisions/renameApplicationRevisionscascades so no ghost revision rows survive an app delete or outlive an app rename under the old name. Wire shapes (genericRevisionInfo/revisionInfo/revisionLocationfield names,GenericRevisionInfo'sregisterTime/firstUsedTime/lastUsedTimeas epoch-secondsfloat64) verified againstaws-sdk-go-v2/service/codedeploy@v1.37.0/deserializers.go'sawsAwsjson11_deserializeDocumentGenericRevisionInfo/...RevisionInfoand proven with a real SDK-client round trip (Test_SDKRoundTrip_ApplicationRevision_EpochSeconds). New sentinelErrRevisionNotFound→RevisionDoesNotExistException, 404 (confirmed againsttypes.RevisionDoesNotExistException). -
Deployment-instance/target family de-stubbed (this pass):
GetDeploymentInstance,GetDeploymentTarget,ListDeploymentInstances,ListDeploymentTargets,BatchGetDeploymentInstances, andBatchGetDeploymentTargetspreviously fabricated aSucceededrecord for literally any requested ID (Get*/Batch*) or always returned an empty list (List*), regardless of whether that ID (or any target at all) actually existed. Added(b *InMemoryBackend) deploymentTargets(d *Deployment) []DeploymentTargetRecord(deployment_instances.go), computed on read from the deployment's owning deployment group's real configuration rather than persisted as a separate table (so it always reflects current on-premises instance/tag state, and needs no snapshot-version bump): forComputePlatform == "Server", oneinstanceTargetper registered, non-deregistered on-premises instance whose tags satisfy the deployment group'sOnPremisesInstanceTagFilters/OnPremisesTagSet(newmatchesOnPremisesTargeting/matchesTagSetGroupshelpers in on_premises_instances.go); for"ECS", oneecsTargetperECSServicesentry (clusterName/serviceNameas the deterministicTargetID); for"Lambda", exactly onelambdaTargetkeyed by the deployment ID itself (real CodeDeploy Lambda/ECS deployments always have exactly one and, respectively,len(ECSServices)targets — confirmed againsttypes.DeploymentTargetListSizeExceededException's doc comment). Every target'sStatusis derived from the deployment's own currentStatusviatargetStatusForDeployment(Succeeded/Failed/Skipped/InProgress) instead of a hardcoded literal that ignored what actually happened to the deployment (e.g. a stopped deployment's targets now correctly reportSkipped, proven byTestDeploymentTargets_StatusTracksDeploymentStatus).Get*/GetDeploymentTargeton an unresolvable ID now returns the newErrDeploymentTargetNotFound→DeploymentTargetDoesNotExistException(404) sentinel instead of fabricating a match;Batch*silently omits unresolvable IDs, matching this codebase's established Batch* convention (BatchGetApplicationsetc.).DeploymentTargetunion wire shape (deploymentTargetTypePascalCase enum valuesInstanceTarget/ECSTarget/LambdaTarget, nestedinstanceTarget/ecsTarget/lambdaTargetmember field names,lastUpdatedAtas epoch-secondsfloat64) verified againstawsAwsjson11_deserializeDocumentDeploymentTarget/...InstanceTarget/...ECSTarget/...LambdaTargetand proven with a real SDK-client round trip (Test_SDKRoundTrip_DeploymentTarget_EpochSeconds). KNOWN, DOCUMENTED LIMITATION: this backend has no live EC2 instance registry, soEc2TagFilters/Ec2TagSetresolve zero targets — only the on-premises side of"Server"targeting is modeled (see gaps). -
ContinueDeployment's real gap was narrower than "blue/green lifecycle" (this pass):
READY_WAIT/TERMINATION_WAITaretypes.DeploymentWaitTypevalues (an input parameter onContinueDeploymentInput), nottypes.DeploymentStatusvalues — a deployment's status is never"READY_WAIT"; the closest status is the separate"Ready"value. The actual fixable gap:ContinueDeploymentaccepted a deployment in any status and never readdeploymentWaitTypeoff the wire at all. Added the real precondition (types/errors.go:556-557DeploymentIsNotInReadyStateException,:221DeploymentAlreadyCompletedException) and wait-type enum validation (InvalidDeploymentWaitTypeException).CreateDeployment's synchronous completion means no deployment here ever reaches"Ready", soContinueDeploymentnow always errors — correct given this backend's lifecycle, not a regression from a previously-working feature. -
Ec2TagFilters/Ec2TagSet were stored but never evaluated, not dropped on input (this pass):
CreateDeploymentGroup/UpdateDeploymentGroupalways persisted them correctly (dg.Ec2TagFilters = input.Ec2TagFilterset al.) andGetDeploymentGroupechoed them back — the gap was entirely indeploymentTargets(), which had no EC2 instance data to match against. Wiredservices/codedeploytoservices/ec2via the same lazySetAppConfig/siblingServicespatternservices/mgnuses (cross_service.go);*CLIalready exposedGetEC2Handler()for that pattern, so nocli.gochanges were needed. -
CreateDeployment's
fileExistsBehaviorwas accepted and persisted with no enum check (this pass): any string round-tripped throughGetDeployment/BatchGetDeployments. Real AWS definesInvalidFileExistsBehaviorExceptionspecifically forCreateDeployment(confirmed in its deserializer's error switch); added validation againstDISALLOW/OVERWRITE/RETAIN. -
Two banned
//nolint:gocognit,cyclop,funlenremoved via decomposition (this pass):dgToOutput/dgInputFromWirein handler_deployment_groups.go were single ~130-line functions doing wire-format conversion for every optional deployment-group sub-structure (load balancer info, blue/green config, alarms, auto-rollback, EC2/on-premises tag sets) inline. Split each into onedgXToOutput/dgXFromWirehelper per sub-structure (7 helpers each direction); the top-level functions now just assemble the struct literal and loop over the flat slice fields, with zero behavior change (proven by the existingdeployment_groups_test.gosuite passing unmodified). One more banned//nolint:gocognitremoved frommatchesTagFiltersin on_premises_instances.go by extracting the per-filterKEY_ONLY/VALUE_ONLY/default switch intomatchesOneTagFilter(also fixed its comment, which incorrectly called the default case "EQUALS" — the realTagFilterTypeenum value isKEY_AND_VALUE, confirmed againsttypes.TagFilterTypein enums.go; the code was already correct, only the comment was wrong). All three were the full set of banned nolints flagged for this service —grep -rnE 'nolint:[a-z,]*(cyclop|gocyclo|gocognit|funlen)' services/codedeploy/now returns empty. -
ERROR path verified against
cmd/errcodeaudit's near-miss sweep (this pass). Four confident findings, two distinct wrong constants each reused at multiple call sites:ErrValidation("InvalidParameterValueException", not a real CodeDeploy type at all) was reused across four semantically unrelated failures. Each now uses the code its own op'sdeserializeOpError<Op>actually models:CreateDeploymentConfig's badcomputePlatform→ the pre-existingErrInvalidComputePlatform(InvalidComputePlatformException, was wired to the wrong sentinel);BatchGetApplicationRevisions' >25-revision case → newErrBatchLimitExceeded(BatchLimitExceededException);RegisterOnPremisesInstance's malformed (not missing) instance name → newErrInvalidInstanceName(InvalidInstanceNameException);TagResource's reserved-prefix/oversized-key/oversized-value tag rejection → newErrInvalidTagsToAdd(InvalidTagsToAddException).errInvalidRequest("InvalidRequestException", also not a real type) backed the "field is required" check at ~35 call sites across nearly every operation in this package — the single-wrong-constant-reused pattern this campaign looks for. Each op models its own distinct<Field>RequiredException; replaced with nine per-field sentinels (ErrApplicationNameRequired,ErrDeploymentGroupNameRequired,ErrDeploymentIDRequired,ErrInstanceIDRequired,ErrDeploymentTargetIDRequired,ErrDeploymentConfigNameRequired,ErrInstanceNameRequired,ErrResourceArnRequired,ErrGitHubTokenNameRequired), each verified against the specific op(s) that raise it, splitting combined two-field checks (e.g.CreateDeploymentGroup's applicationName+deploymentGroupName) into two ordered checks so each field gets its own correct code.errInvalidRequestitself is now unused and removed.errUnknownAction(dispatch-miss for an unrecognizedAction) still maps toInvalidRequestException— deliberately left unfixed: no CodeDeploy operation models this condition (there is no operation to consult; the routed action itself is unrecognized), so inventing a replacement code would be exactly the fabrication this campaign exists to remove. This is the one remaining confidenterrcodeauditfinding for this service.- Adjacent bug found while in this code, not from the tool:
DeleteDeploymentConfig's built-in-config guard usedErrDeploymentConfigInUse(DeploymentConfigInUseException) — a real CodeDeploy exception, but the wrong one for this op.DeleteDeploymentConfig's own deserializer modelsInvalidOperationExceptionfor exactly this case;DeploymentConfigInUseExceptionis only modeled byAddTagsToOnPremisesInstances/RemoveTagsFromOnPremisesInstances/UpdateDeploymentGroup's tag-limit case, none of which this backend ever triggered it from. Renamed toErrDeploymentConfigIsDefaultmapped toInvalidOperationException. Same root cause asErrOnPremisesInstanceNotFoundabove: a real code borrowed from the wrong operation in the same family. - Same trap caught
ErrTagLimitExceeded(TagLimitExceededException, also real but only modeled by the three ops above, neverTagResource):TagResource's own too-many-tags check now usesErrInvalidTagsToAddlike its other tag-content rejections. - Five existing tests asserted the fabricated/misrouted codes as correct and are fixed:
TestDeploymentConfigs_DefaultsCannotDelete(renamed assertion fromDeploymentConfigInUseExceptiontoInvalidOperationException),TestDeploymentConfigs_ErrValidationMapping(renamed toTestDeploymentConfigs_ErrInvalidComputePlatformMapping, assertsInvalidComputePlatformException), two cases inTestOnPremisesInstance-adjacent table tests inon_premises_instances_test.go(now assertInvalidInstanceNameException), andTestTags_ResourceTagLimits/TestTags_ResourceExceedsMaxTagsintags_test.go(now assertInvalidTagsToAddException). - New coverage driving the real typed SDK client end-to-end, asserting the specific typed
exception via
errors.As(not string/presence checks):error_codes_fixes_test.go.
-
Re-verified independently, 2026-08-30 (gopherstack-r3pr, no code change): re-ran
cmd/errcodeaudit;errUnknownAction→InvalidRequestException(handler.go:285) is still the only confident finding. Confirmed againsttypes/errors.go(aws-sdk-go-v2/service/codedeploy@v1.38.4): noInvalidRequestExceptiontype exists anywhere in the module, and none of its 47deserializeOpError<Op>functions could — an unrecognized routedActionstring doesn't correspond to any real CodeDeploy operation, so there is no operation's own deserializer to consult. Left unfixed, per the existing comment at the call site; verdict unchanged. -
Triaged, 2026-09-07 (gopherstack-3pz8,
errtargetauditclass A findings): 6 findings, alldomain=Handler mechanism=sentinel reference, grouped into 4 root causes by code. Verified each againstawk "/deserializeOpError<Op>\(/,/^}/" \ deserializers.go | grep -oE '"[A-Za-z0-9]+"'plus the matchingdocs.aws.amazon.com/codedeploy/latest/APIReference/API_<Op>.htmlErrors section. No override-helper exists anywhere in this package (grep -rn "errors.Is(err" *.go→ only handler.go's own dispatch loop), so class-7 (handler overrides the code) does not apply to any of the six.- Fixed (1, unambiguous):
GetOnPremisesInstanceemittedInstanceDoesNotExistException(ErrOnPremisesInstanceNotFound) for a missing instance, but its own deserializer modelsInstanceNotRegisteredExceptioninstead — confirmed both indeserializers.goand the doc page's Errors list ("The specified on-premises instance is not registered."). Exactly one declared code fit, so this was a clean sentinel swap: newErrOnPremisesInstanceNotRegisteredsentinel, wired intoerrorMappings, call site inon_premises_instances.goupdated. Pre-existing testTestOnPremisesInstances_NotFoundErrorMapping/GetOnPremisesInstancehad been asserting the wrong code (InstanceDoesNotExistException) since it was written to close a different prior gap (missing errorMappings entry entirely, causing a 500) — fixed to assertInstanceNotRegisteredException. AddedTestGetOnPremisesInstance_NotRegisteredNotDoesNotExistasserting the correct code, the wrong code's absence, and that a failed lookup creates no instance. - Left unfixed (5, evidence gap):
DeleteApplication/ApplicationDoesNotExistException,DeleteDeploymentGroup/ApplicationDoesNotExistException,DeleteDeploymentGroup/DeploymentGroupDoesNotExistException,DeleteDeploymentConfig/DeploymentConfigDoesNotExistException, andDeregisterOnPremisesInstance/InstanceDoesNotExistException. Each op's own deserializer models no not-found code at all for the missing-resource case (confirmed above) — the declared-set mismatch proves the current 404 is wrong, but does not by itself prove the remedy is silent success: all four ops' doc pages carry only the generic "If the action is successful, the service sends back an HTTP 200 response with an empty HTTP body" boilerplate (the same sentence that appears oncodepipeline'sDisableStageTransition, which does still error on a missing resource), not a semantic idempotent-delete sentence like workmail's genuinely-earned "Deleting already deleted and non-existing rules does not produce an error."DeleteDeploymentConfigadditionally has an ambiguous candidate (InvalidOperationException, already used a few lines below for the built-in-config case) that is plausible but not evidenced. Landmine comments left at all five call sites naming the gap and citing this issue; no behavior changed for these five. Neutered the one line that was changed (GetOnPremisesInstance's sentinel swap, and separately the newerrorMappingsentry) — each reversion compiled and made the corresponding test fail as expected, then was restored. go test -race -count=1 ./services/codedeploy/...andgolangci-lint run services/codedeploy/...(0 issues.) both pass after the fix.
- Fixed (1, unambiguous):
-
Re-verified independently, 2026-09-07 (gopherstack-l81f, no code change): re-derived each of the five left-unfixed ops' declared sets individually (not by count) via
awk "/deserializeOpError<Op>\(/,/^}/" deserializers.go | grep -oE '"[A-Za-z0-9]+"'against codedeploy@v1.38.4 —DeleteApplication{ApplicationNameRequiredException, InvalidApplicationNameException, InvalidRoleException},DeleteDeploymentGroup{ApplicationNameRequiredException, DeploymentGroupNameRequiredException, InvalidApplicationNameException, InvalidDeploymentGroupNameException, InvalidRoleException},DeleteDeploymentConfig{DeploymentConfigInUseException, DeploymentConfigNameRequiredException, InvalidDeploymentConfigNameException, InvalidOperationException},DeregisterOnPremisesInstance{InstanceNameRequiredException, InvalidInstanceNameException} — all confirmed to contain no not-found code, matching 3pz8 exactly. Read every declared candidate's doc comment intypes/errors.go: none reads as not-found (InvalidOperationExceptionis "An invalid operation was detected." — no word-for-word link to a missing resource). Independently fetched the liveAPI_<Op>.htmlreference pages for all four (DeleteApplication,DeleteDeploymentGroup,DeleteDeploymentConfig,DeregisterOnPremisesInstance) and confirmed each still carries only the generic "If the action is successful, the service sends back an HTTP 200 response with an empty HTTP body" boilerplate — no idempotent-delete sentence anywhere, same conclusion 3pz8 reached.cmd/errtargetaudit -dir codedeploystill reports exactly these 5 class-A findings, byte-for-byte the same emission sites/codes as 3pz8's table. Verdict unchanged: no safe remedy for any of the five; landmine comments at all five call sites left as-is (already accurate and complete). No.gofiles touched for this issue.
FIXED: ListDeployments accepted applicationName without deploymentGroupName (and the reverse). api_op_ListDeployments.go documents "If applicationName is specified, then deploymentGroupName must be specified" and the converse; now DeploymentGroupNameRequiredException / ApplicationNameRequiredException, plus ApplicationDoesNotExist / DeploymentGroupDoesNotExist for unknown names. TestDeployments_SortedList had listed by application alone and now passes the group too. Proven by list_deployments_scope_test.go.
Recorded: createTimeRange bounds and on-premises tagFilters combination are not specified by the SDK docs; left as inclusive bounds / AND.
CodeDeploy is region-isolated: each non-home region gets a lazily built sibling Handler (own applications, deployment groups, deployments, configs, on-premises instances; region-correct ARNs) via pkgs/regionpeers; siblings inherit the app config so deployment targets resolve against the EC2 backend of the deployment group's own region. Same-named resources coexist per region. Snapshots gain an additive regions key only when a sibling exists (no version bump; older snapshots restore). Proof: TestHandler_MultiRegionIsolation, TestHandler_MultiRegionPersistence, TestRegionIsolation/codedeploy. CodeDeploy makes no Lambda or ECS calls, so EC2 is the only cross-service path. The tagging-API bridge lists the request region and resolves Tag/Untag by ARN region.
Still recorded, reason made precise: types.DeploymentInfo in codedeploy@v1.43.0 has no deploymentMode member to echo, the SDK documents only that RESTART runs "a shortened set of lifecycle events" without naming them, and this backend produces no per-instance lifecycle events for either mode to differ in. Inventing an event list would be fabrication.
No List op truncates (the SDK documents no page size), so no token is ever issued: ListApplications, ListApplicationRevisions, ListDeploymentConfigs, ListDeploymentGroups, ListDeploymentInstances, ListDeploymentTargets, ListDeployments, ListGitHubAccountTokenNames and ListOnPremisesInstances now reject a supplied nextToken with InvalidNextTokenException (types/errors.go:3694; the per-op error sets are not in the pinned schema SDK) instead of ignoring it. ListTagsForResource NextToken is a false positive (50-tag cap). Proof: TestListOps_RejectUnissuedNextToken.
CreateDeploymentGroup/UpdateDeploymentGroup now accept autoScalingGroups as bare names (SDK AutoScalingGroups []string); object entries still decode. UpdateDeploymentGroup keeps omitted AutoScalingGroups (empty list removes them), TerminationHookEnabled and the structured members (alarm, rollback, style, load balancer, blue/green, tag sets), interpreted from "add or change" (api_op_UpdateDeploymentGroup.go:42-61); omitted triggers and ECS services are kept too (SDK silent, interpretation). Ec2TagFilters and OnPremisesInstanceTagFilters stay full replacement per api_op_UpdateDeploymentGroup.go docs. Proof: TestUpdateDeploymentGroup_OmittedMembersKept. Changed test: TestDeploymentGroups_Update/hooks_cleaned_on_alarm_removal now sends alarmConfiguration enabled=false, since omission no longer removes alarms.
CreateDeployment no longer drops AutoRollbackConfiguration, OverrideAlarmConfiguration or TargetInstances. DeploymentInfo still omits blueGreenDeploymentConfiguration, loadBalancerInfo, startTime, errorInformation, relatedDeployments, rollbackInfo and previousRevision: none has an input or lifecycle model here. Whether real AWS fills autoRollbackConfiguration from the group when the request omits it is unverified, so only the request value is echoed. Proof: dropped_members_sdk_test.go. Additive omitempty fields on Deployment.
StopDeployment on a Succeeded/Failed/Stopped deployment now returns DeploymentAlreadyCompletedException instead of rewriting it to Stopped. A BLUE_GREEN group with deploymentReadyOption.actionOnTimeout=STOP_DEPLOYMENT creates deployments in Ready; ContinueDeployment moves them to Succeeded and StopDeployment to Stopped (the earlier stop tests now use that path). CreateDeploymentGroup/UpdateDeploymentGroup reject a non-ARN serviceRoleArn (InvalidRoleException), an unknown deploymentConfigName (DeploymentConfigDoesNotExistException), bad deploymentStyle enums (InvalidDeploymentStyleException) and names over 100 characters; CreateApplication rejects names over 100. CreateDeployment rejects a revisionType with no matching location (InvalidRevisionException). completeTime is no longer 5 seconds in the future and startTime is returned. A missing serviceRoleArn stays accepted because in-repo callers create groups without one. Messages no longer repeat the code; not-found reads "No application found for name: ". Tests: lifecycle_validation_test.go, ready_wait_test.go.