fix(group-tools): skip an unscoped blueprint instead of crashing on it - #390
Merged
Merged
Conversation
`pro group-tools analyze --unused` panicked with a nil pointer dereference on
any tenant holding one blueprint that targets nothing. It died in
addPlatformReferencedGroups after six sweeps of the instance had already been
spent, and the panic names an SDK type rather than the blueprint, so there is
nothing an operator can do about it from the command line.
BlueprintDetail.Scope is *BlueprintScope with omitempty, so a blueprint with
no targets arrives with it nil, and the loop dereferenced it unguarded. The
`if err != nil { continue }` above looks like it covers this and does not:
GetBlueprint returns &result on success, so err is nil and the pointer FIELD
is what is missing.
BenchmarkV2.Target is *TargetV2 with omitempty and was dereferenced the same
way. That half had not fired only because the tenant which exposed the
blueprint case happened to have every benchmark targeted; an untargeted
benchmark reaches it identically. Fixing one loop and not the other would
leave the same crash behind a slightly rarer tenant shape.
Both skip the unscoped record and carry on rather than returning, which is the
part worth stating: a guard that returned would pass a crash test and silently
stop marking the rest of the collection as referenced, and `--unused` would
then offer a group that a blueprint really does scope as a removal candidate.
That is a worse failure than the panic, because it reads as a successful
answer.
Reproduced live against a gateway tenant with one unscoped blueprint, on main
as well as on the branch that surfaced it, and TestAddPlatformReferencedGroups_
ToleratesAnUnscopedBlueprintAndBenchmark fails with the identical panic when
either guard is removed. After the fix that command completes and returns its
rows.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
neilmartin83
enabled auto-merge
September 18, 2026 15:47
grahampugh
approved these changes
Sep 18, 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.
pro group-tools analyze --unusedpanics with a nil pointer dereference on any tenant holding one blueprint that targets nothing. It dies after six sweeps of the instance have already been spent, and the traceback names an SDK type rather than the blueprint, so there is nothing an operator can do about it from the command line.Found while verifying #387 against a second tenant. Pre-existing on
main— reproduced on64671e97as well as on #387's branch, so it is neither #387's nor #389's.Cause
BlueprintDetail.Scopeis*BlueprintScopewithomitempty, so a blueprint with no targets arrives with itnil, and the loop dereferenced it unguarded.The
if err != nil { continue }directly above looks like it covers this and does not:GetBlueprintreturns&resulton success, soerrisnil,detailis non-nil, and the missing thing is the pointer field.BenchmarkV2.Targetis*TargetV2withomitemptyand was dereferenced the same way. That half had not fired only because the tenant which exposed the blueprint case happened to have every benchmark targeted — an untargeted benchmark reaches it identically. Fixing one loop and leaving the other would move the same crash behind a slightly rarer tenant shape.Fix
Skip the unscoped record and carry on.
continue, notreturn, and that distinction is the point: a guard that returned would pass a crash test and silently stop marking the rest of the collection as referenced, so--unusedwould then offer a group that a blueprint really does scope as a removal candidate. That is a worse failure than the panic, because it reads as a successful answer.Verification
TestAddPlatformReferencedGroups_ToleratesAnUnscopedBlueprintAndBenchmarkserves an unscoped blueprint and an untargeted benchmark each followed by a scoped one, so it asserts both that the walk survives and that it keeps reading past the nil record. Mutation-checked: removing either guard reproduces the identical panic inside the test.Live, against the gateway tenant that exposed it: the command completed and returned its rows, where before it produced no output and a stack trace.
go build ./...,go test ./...,make lint(0 issues) all clean.Reproducing it, and why you probably cannot do it through the CLI
The gateway refuses to create this state on the current API version, so a reviewer who tries to manufacture it will hit a 400 and may conclude the crash is unreachable:
Creating a blueprint with
scopeomitted is refused, and so is emptying an existing blueprint's scope by removing its only group. (Probed on a second tenant by the session that wrote #387.)The state is nonetheless common. On the tenant where this crashed, 6 of 23 blueprints return
scope: null— 26% of them. They arrived some other way: the UI, an API version that did not validate, or a scope group deleted out from under the blueprint. So this is not a theoretical nil; it is the normal condition of a quarter of that tenant's blueprints, and any one of them is enough to take the command down.Two consequences for review:
blueprints/types.go:164and:1268in SDK v1.1.0, two separate structs; the benchmark side matches), and the dereference was unguarded. Whether the API should permit an unscoped blueprint is a different question from whether the CLI may crash on one.Not included
Whether
--unusedshould report unscoped blueprints at all is a separate question — right now they simply do not contribute references, which is correct for the reference set but means an operator gets no signal that a blueprint is scoped to nothing. That belongs in its own change if it is wanted.🤖 Generated with Claude Code