refactor(registry): load runtime and service versions from JSON#260
refactor(registry): load runtime and service versions from JSON#260pjcdawkins wants to merge 2 commits intomainfrom
Conversation
Replace hardcoded string constants for Runtime and ServiceName types with struct types loaded from an embedded registry.json file at init time. This makes it possible to update supported versions without code changes. - Add registry.go and registry.json (embedded JSON data for all runtimes and services with their supported versions) - Rewrite Runtime from string const to struct with Name, Type, Versions, Docs fields - Rewrite ServiceName from string const to struct with Name, Type, Versions, Disk, Docs fields - Update RuntimeForStack to look up runtimes by type string - Update all callers to use runtime.Type string comparisons instead of const equality - Remove version.go and generate_versions.go (superseded by registry) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Refactors the question/models layer to source runtime and service metadata (including supported versions) from an embedded registry.json instead of hardcoded Go constants/maps, and updates the interactive questionnaire flow to use the new struct-based runtime/service representations.
Changes:
- Introduces embedded
registry.jsonplus init-time loading into globalmodels.Runtimesandmodels.ServiceNames. - Replaces hardcoded Runtime/ServiceName constants and version maps with
Runtime/ServiceNamestructs and helper lookup/default-version methods. - Updates question flow and tests to compare by
runtime.Typestring and to pull versions from the registry.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/question/web_command.go | Updates PHP runtime check to use answers.Type.Runtime.Type. |
| internal/question/type.go | Adapts stack→runtime detection to pointer-based runtime and uses DefaultVersion(). |
| internal/question/socket_family.go | Switches runtime comparisons to answers.Type.Runtime.Type. |
| internal/question/services.go | Sources service supported versions from registry-backed ServiceName.Versions.Supported. |
| internal/question/models/version.go | Removes hardcoded language/service version maps and default-version helper. |
| internal/question/models/stack.go | Changes RuntimeForStack() to return a registry-backed *Runtime (or nil). |
| internal/question/models/service_name.go | Replaces service constants with a registry-backed ServiceName struct + helpers. |
| internal/question/models/runtime.go | Replaces runtime constants with a registry-backed Runtime struct + helpers. |
| internal/question/models/registry.json | Adds embedded upstream registry data for runtimes/services and versions. |
| internal/question/models/registry.go | Loads and partitions registry into Runtimes and ServiceNames at init time. |
| internal/question/models/generate_versions.go | Removes codegen tool previously used to generate version maps. |
| internal/question/models/answer.go | Updates RuntimeType to hold *Runtime and adjusts String/JSON behavior. |
| internal/question/locations.go | Updates PHP runtime check to use answers.Type.Runtime.Type. |
| internal/question/build_steps_test.go | Updates tests to resolve runtimes from models.Runtimes via type. |
| internal/question/build_steps.go | Updates Node.js runtime check to use answers.Type.Runtime.Type. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| runtime := models.RuntimeForStack(answers.Stack) | ||
| if runtime == "" { | ||
| if runtime == nil { | ||
| question := &survey.Select{ | ||
| Message: "What language is your project using? We support the following:", | ||
| Options: models.Runtimes.AllTitles(), |
There was a problem hiding this comment.
Runtime is now a pointer, but the defer above prints answers.Type.Runtime.Title() when answers.Stack != GenericStack. If survey.AskOne() or RuntimeByTitle() returns an error, the function returns before answers.Type.Runtime is assigned, and the deferred print will panic on nil. Add a nil check in the defer (or only print after answers.Type.Runtime is set and the function is returning nil).
| func init() { | ||
| allRuntimes := map[string]*Runtime{} | ||
| if err := json.Unmarshal(registry, &allRuntimes); err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| for _, r := range allRuntimes { | ||
| if r.Runtime { | ||
| Runtimes = append(Runtimes, r) | ||
| } | ||
| } | ||
|
|
||
| allServices := map[string]*ServiceName{} | ||
| if err := json.Unmarshal(registry, &allServices); err != nil { | ||
| log.Fatal(err) | ||
| } | ||
| for _, s := range allServices { | ||
| if !s.Runtime { | ||
| ServiceNames = append(ServiceNames, s) | ||
| } | ||
| } |
There was a problem hiding this comment.
registry.json is unmarshaled into maps and then appended into slices via map iteration, which produces nondeterministic ordering. Because Runtimes.AllTitles() / ServiceNames.AllTitles() preserve slice order, the prompt option ordering will vary between runs and can make tests/UX flaky. Sort Runtimes and ServiceNames (e.g., by Type or Name) after loading; also consider unmarshaling once and partitioning rather than unmarshaling the same bytes twice.
| URL string | ||
| Web struct { | ||
| Commands struct { | ||
| Start string | ||
| } | ||
| } | ||
| Locations map[string]map[string]any | ||
| } |
There was a problem hiding this comment.
The Docs struct layout doesn’t match the JSON structure: in registry.json, locations is under docs.web.locations, but here Locations is a sibling of Web. As a result, Locations will never unmarshal and will always be nil. Move Locations under Web (and add the appropriate json tags) to actually load the embedded docs data.
| URL string | |
| Web struct { | |
| Commands struct { | |
| Start string | |
| } | |
| } | |
| Locations map[string]map[string]any | |
| } | |
| URL string `json:"url"` | |
| Web struct { | |
| Commands struct { | |
| Start string `json:"start"` | |
| } `json:"commands"` | |
| Locations map[string]map[string]any `json:"locations"` | |
| } `json:"web"` | |
| } `json:"docs"` |
| Docs struct { | ||
| Relationship string | ||
| URL string | ||
| } | ||
| ) | ||
| Endpoint string | ||
| MinDiskSize *int | ||
| Versions struct { | ||
| Supported []string |
There was a problem hiding this comment.
Several fields won’t unmarshal from registry.json because the JSON keys are snake_case (e.g., relationship_name, min_disk_size) but the struct has no json tags. That means Docs.Relationship and MinDiskSize will remain empty/nil even though the registry provides them. Add explicit json tags (and ensure field names match the upstream schema) so the registry-backed struct fields are actually populated.
- type.go: add nil check for Runtime in defer to prevent panic when RuntimeByTitle returns an error before Runtime is assigned - registry.go: sort Runtimes and ServiceNames after loading from the map to ensure deterministic ordering - runtime.go: move Locations field inside Web struct to match the JSON nesting (docs.web.locations) - service_name.go: add json tags for relationship_name and min_disk_size so they unmarshal correctly from snake_case JSON keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Replace hardcoded string constants for Runtime and ServiceName types with struct types loaded from an embedded
registry.jsonfile at init time.RuntimeForStack()looks up runtimes by type string instead of returning constantsruntime.Typestring comparisonsversion.goandgenerate_versions.goremoved (superseded by registry)registry.jsonupdated from upstream (https://github.com/platformsh/platformsh-docs/blob/main/shared/data/registry.json), canonical source: https://github.com/upsun/meta/blob/master/resources/image/registry.jsonThis is the first in a series of PRs incorporating changes from @akalipetis's
feature/abstract-fsbranch, cleaned up and split into reviewable pieces. Subsequent PRs will add fs.FS abstraction, a discovery package, and non-interactive mode.Author: @akalipetis
🤖 Generated with Claude Code