feat(catalog): offer a set of module versions, ordered by semver #12

Merged
picklemustard merged 1 commit from dev into main 2026-09-20 13:45:51 +00:00

Standard builds worked; adding modules did not. Four causes compounded.

Releases were ranked by when their rows were inserted. preferredOrder
sorted by created_at DESC, and that is not merely arbitrary: auto-populate
lists tags newest-first and imports them one transaction at a time, so the
OLDEST tag landed with the greatest timestamp and won. Every module the
importer discovered was offered at its oldest release. Migration 0014
parses the label once, at write time, into columns the database can sort,
and versionOrder replaces preferredOrder at all four of its call sites --
the fourth being ModuleVersions, which backs the admin pin dialog's
default, so an operator correcting a module by hand re-committed the
defect with one click.

A module could offer exactly one release, because godot_module_compat's
primary key is (godot_major, godot_minor, module_id). That is why the
build form had a checkbox per module and no way to choose a version.
store.OffersFor evaluates the rule on read instead and returns every
qualifying release, best first, with one of them the default.

The builder cloned branches. A tag with no gbb.yaml lands with no stated
minimum and is offered by nothing, while prose read at a branch head is
trusted where the same prose at a tag is only a suggestion -- so for the
common module that ships tags but documents itself on master, the branch
was the only qualifying row. A moving ref is now offered but never an
implicit default: a person names it, or an administrator pins it.

Arch and runtime did not exist in the catalog at all. Migration 0015 adds
platforms/arches/runtimes, declared in gbb.yaml. NULL means no restriction,
which is every existing row, so nothing is silently un-offered.

The freeze is gone, and that reverses an argument docs/m2-module-registry.md
makes at length, so that section is rewritten rather than quietly edited.
Its mechanism was a stored row, and a stale row IS the freeze: a module
shipping a fix for an engine it already supported could never reach that
engine's users. What protects a build is jobs.catalog_snapshot and 0009's
source pins -- per build, not per engine, and unable to go stale.
godot_module_compat survives as the administrator's override; migration
0016 removes the generated rows a release later, because the code change
is reversible and the delete is not.

Also fixes a defect found on the way: moduleVersionRequest never declared
max_godot_major/max_godot_minor, and decodeAdmin rejects unknown fields,
so the module page's own "fix the minimum" save -- which resends the
ceiling precisely so filling in a floor does not wipe it -- was answered
400 and a ceiling could not be set by hand at all.

Verified against a real Postgres, including a test that reproduces the
reported symptom through the import path that produced it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Standard builds worked; adding modules did not. Four causes compounded. Releases were ranked by when their rows were inserted. preferredOrder sorted by created_at DESC, and that is not merely arbitrary: auto-populate lists tags newest-first and imports them one transaction at a time, so the OLDEST tag landed with the greatest timestamp and won. Every module the importer discovered was offered at its oldest release. Migration 0014 parses the label once, at write time, into columns the database can sort, and versionOrder replaces preferredOrder at all four of its call sites -- the fourth being ModuleVersions, which backs the admin pin dialog's default, so an operator correcting a module by hand re-committed the defect with one click. A module could offer exactly one release, because godot_module_compat's primary key is (godot_major, godot_minor, module_id). That is why the build form had a checkbox per module and no way to choose a version. store.OffersFor evaluates the rule on read instead and returns every qualifying release, best first, with one of them the default. The builder cloned branches. A tag with no gbb.yaml lands with no stated minimum and is offered by nothing, while prose read at a branch head is trusted where the same prose at a tag is only a suggestion -- so for the common module that ships tags but documents itself on master, the branch was the only qualifying row. A moving ref is now offered but never an implicit default: a person names it, or an administrator pins it. Arch and runtime did not exist in the catalog at all. Migration 0015 adds platforms/arches/runtimes, declared in gbb.yaml. NULL means no restriction, which is every existing row, so nothing is silently un-offered. The freeze is gone, and that reverses an argument docs/m2-module-registry.md makes at length, so that section is rewritten rather than quietly edited. Its mechanism was a stored row, and a stale row IS the freeze: a module shipping a fix for an engine it already supported could never reach that engine's users. What protects a build is jobs.catalog_snapshot and 0009's source pins -- per build, not per engine, and unable to go stale. godot_module_compat survives as the administrator's override; migration 0016 removes the generated rows a release later, because the code change is reversible and the delete is not. Also fixes a defect found on the way: moduleVersionRequest never declared max_godot_major/max_godot_minor, and decodeAdmin rejects unknown fields, so the module page's own "fix the minimum" save -- which resends the ceiling precisely so filling in a floor does not wipe it -- was answered 400 and a ceiling could not be set by hand at all. Verified against a real Postgres, including a test that reproduces the reported symptom through the import path that produced it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(catalog): offer a set of module versions, ordered by semver
All checks were successful
CI / Go (push) Successful in 3m36s
CI / Frontend (push) Successful in 30s
CI / Badges (push) Has been skipped
CI / Publish images (push) Has been skipped
CI / Dedupe (push) Successful in 10s
CI / Frontend (pull_request) Successful in 26s
CI / Badges (pull_request) Has been skipped
CI / Publish images (pull_request) Has been skipped
CI / Dedupe (pull_request) Successful in 1s
CI / Go (pull_request) Successful in 1m48s
d7a89ca0a4
Standard builds worked; adding modules did not. Four causes compounded.

Releases were ranked by when their rows were inserted. preferredOrder
sorted by created_at DESC, and that is not merely arbitrary: auto-populate
lists tags newest-first and imports them one transaction at a time, so the
OLDEST tag landed with the greatest timestamp and won. Every module the
importer discovered was offered at its oldest release. Migration 0014
parses the label once, at write time, into columns the database can sort,
and versionOrder replaces preferredOrder at all four of its call sites --
the fourth being ModuleVersions, which backs the admin pin dialog's
default, so an operator correcting a module by hand re-committed the
defect with one click.

A module could offer exactly one release, because godot_module_compat's
primary key is (godot_major, godot_minor, module_id). That is why the
build form had a checkbox per module and no way to choose a version.
store.OffersFor evaluates the rule on read instead and returns every
qualifying release, best first, with one of them the default.

The builder cloned branches. A tag with no gbb.yaml lands with no stated
minimum and is offered by nothing, while prose read at a branch head is
trusted where the same prose at a tag is only a suggestion -- so for the
common module that ships tags but documents itself on master, the branch
was the only qualifying row. A moving ref is now offered but never an
implicit default: a person names it, or an administrator pins it.

Arch and runtime did not exist in the catalog at all. Migration 0015 adds
platforms/arches/runtimes, declared in gbb.yaml. NULL means no restriction,
which is every existing row, so nothing is silently un-offered.

The freeze is gone, and that reverses an argument docs/m2-module-registry.md
makes at length, so that section is rewritten rather than quietly edited.
Its mechanism was a stored row, and a stale row IS the freeze: a module
shipping a fix for an engine it already supported could never reach that
engine's users. What protects a build is jobs.catalog_snapshot and 0009's
source pins -- per build, not per engine, and unable to go stale.
godot_module_compat survives as the administrator's override; migration
0016 removes the generated rows a release later, because the code change
is reversible and the delete is not.

Also fixes a defect found on the way: moduleVersionRequest never declared
max_godot_major/max_godot_minor, and decodeAdmin rejects unknown fields,
so the module page's own "fix the minimum" save -- which resends the
ceiling precisely so filling in a floor does not wipe it -- was answered
400 and a ceiling could not be set by hand at all.

Verified against a real Postgres, including a test that reproduces the
reported symptom through the import path that produced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
StudioTeal/GodotBinaryBuilder!12
No description provided.