dev #9

Merged
picklemustard merged 4 commits from dev into main 2026-09-18 23:23:15 +00:00
No description provided.
feat(discover): read compatibility from module documentation
All checks were successful
CI / Dedupe (push) Successful in 1s
CI / Go (push) Successful in 3m5s
CI / Frontend (push) Successful in 28s
CI / Dedupe (pull_request) Successful in 1s
CI / Go (pull_request) Successful in 1m50s
CI / Badges (push) Has been skipped
CI / Publish images (push) Has been skipped
CI / Frontend (pull_request) Successful in 19s
CI / Badges (pull_request) Has been skipped
CI / Publish images (pull_request) Has been skipped
cb05414ffc
Discovery worked out which Godot a release targets from two places: a
gbb.yaml at the tag, and the tag name. A repository whose tags are plain
version numbers and whose README carries the support table -- which is
most of them -- imported every release with no minimum, and a release
with no minimum is offered by no engine until an admin types the number
in by hand.

The crawl now also reads, all over git and all through the mirror the
build path already keeps warm:

  - README, docs/**.md and CHANGELOG at the tag
  - the README on the default branch
  - the repository wiki, which GitHub and Forgejo both serve as an
    ordinary git repository at <endpoint>.wiki.git
  - the annotated tag message, which is what a forge shows as the release
    notes for a tag-created release

Only a source that names *that release* sets a minimum: the manifest, the
tag name, a support table row matching the tag, a version-keyed changelog
section, or a tag message naming exactly one engine. Everything else --
prose, a README at HEAD, a wiki page -- is recorded as a suggestion,
attributed to the document and quoted from the line that matched, for an
admin to accept with one click. A README sentence describes the
repository as its author last edited it, not the tag being imported, and
applying it to every tag is the silent wrong answer this package exists
to refuse. A release with suggestions still needs review.

Also adds an optional forge client for the one thing git cannot see: a
Release written in a web UI on a lightweight tag, whose text lives only
in the forge's database. It is an allowlist -- a URL cannot tell you what
software serves it, so a host not named in configuration is never
contacted and never sent a token -- and every failure it meets is a
source that said nothing rather than an import that failed.

Releases already recorded are not crawled deeply. The crawl holds the
per-module lock that builds take, and spending it to describe a row the
import will not write would make every build of that module wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /api/v1/builds took status, limit and before. Someone looking for a
binary asks "Linux, x86_64, editor, 4.4" and had no way to say any of it,
so the frontend fetched fifty rows and showed them all -- every build on
the instance, to everyone, failures included.

The facets are not columns: migration 0002 stores the spec as a document
because it was "never queried field by field", and that is now false.
ListFilter gains platform, arch, target, runtime, precision, godot_ref,
module membership, cache_key and owner_hash; Postgres.List adds one
predicate per facet, with modules as a jsonb containment so a single
predicate is an AND over every module named.

runtime and precision are omitempty, so a standard single-precision
build -- the commonest there is -- has neither key in its stored
document. Every predicate on those two is COALESCE(...) against the
default, reproducing spec.BuildSpec.normalized(). Without it the most
popular query on the page matches nothing, silently. It is also why this
is seven expression indexes rather than one GIN over spec: containment
cannot express a key that is not there.

Visibility lands in one named function that returns the filter the store
will execute, so a test can assert the rule rather than the rows. It
cannot live in the frontend, since the route carries guard "none" in
anonymous mode, and it cannot live in the store, which has no request.
Non-admins see succeeded builds and nothing else -- not as a
confidentiality boundary, since those artifacts are already downloadable
by any verified session, but because other people's failures are noise
on a listing about downloads. scope=mine is the exception, and it is
what lets a submitter watch the build they just started.

Two ways to get that wrong, both of which return the whole history:

An absent device must not become owner_hash = ''. That column is empty
for every token-mode and admin-created job, so the query would return
exactly what the caller must not see.

An empty status set is not a filter that matches nothing. Intersecting
?status=failed down to [] and handing it over returns everything,
because Postgres.List skips the clause when the slice is empty. The
first implementation did precisely that, and the Postgres-backed test
caught it where the fake-store one could not -- that test recorded the
filter and never executed it. browser_integration_test.go exists for
this reason.

POST /api/v1/builds/lookup answers "has this exact configuration been
built". It takes a spec rather than a key: CacheKey is sha256 over Go's
own marshalling of the normalized struct, and a TypeScript port would
break silently the first time BuildSpec gained a field. It is advisory
and says so in docs/api.md -- dedup proper is TODO(M7) and is blocked on
establishing that one unchanged spec reproduces byte-identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The API half of the build browser, kept separate from the pages that use
it so the restructure that follows is a restructure and nothing else.

BuildListOptions gains the spec facets under the API's own parameter
names -- the same names the browser will put in the address bar, so a URL
can be pasted into curl unchanged. buildListQuery repeats `module` as it
already repeats `status`, omits absent facets entirely rather than
sending them empty, and leaves out scope=all since that is the server's
default anyway.

lookupBuilds posts a spec rather than a key. Reproducing CacheKey in
TypeScript is possible and is a trap: it is sha256 over Go's marshalling
of the normalized struct, so one added BuildSpec field would stop the
lookup matching with nothing to notice it. CONTRIBUTING.md says adding a
mirror is a decision rather than a default, and this is not one worth
making.

build-filters.ts reads and writes the URL query, as pure functions with
no DOM. It drops values outside the vocabulary rather than reporting
them -- only a hand-edited URL produces one, and an error banner would be
a worse answer than the unfiltered list already on screen -- and leaves
limit and before out of what it writes, so a shared link does not drop
the reader into the middle of the sharer's paging.

option-tiers.ts decides which tier of the form each server-owned option
is asked in. An unknown key falls through to the tuning tier rather than
vanishing: the schema is backend-owned and versioned, so a new option
will arrive before this table hears about it, and a form that silently
stopped offering one would be the worse failure.

download.ts lifts the artifact download out of the detail page, since
the browser's cards now offer the same thing. It clears the verification
gate first, because the artifact route is the one build endpoint that
stays session-gated in anonymous mode.

No change to types.ts: no Go domain type changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Users landed on the build form. A full build is measured in hours, and
any verified session could already download any succeeded build's
artifacts -- the UI simply never offered to. So / becomes the browser,
/new the form, and /builds the operator's all-status view, which is the
one place where seeing every failure is the job.

The browser keeps its filters in the URL and nowhere else. No mirrored
state, so Back, Forward, a reload and a pasted link agree by
construction rather than by two copies being kept in step, and the
parameter names are the API's own. Refetching is an effect of the
filters rather than something each control remembers to call, which is
also what resets the cursor: the cursor is the last row's id, so
replacing the list replaces it and there is no paging state to forget.

Cards rather than a table, because the download is the primary action
and a build's identity is "Godot 4.4, linuxbsd/x86_64, editor" rather
than the ULID that used to lead the row. Badges appear only for what
differs from a default build; one on every field for every build would
say nothing, which is the failure the form had. One artifact is a
download, several is a link to the detail page -- a build with four
files is a page, not a dropdown.

Polling is now a function of whether anything listed can change. The
default listing is succeeded-only and a succeeded build stays succeeded,
so no timer is created at all. Only the "your builds" strip polls, and
only while something in it is running and the tab is visible. That strip
is what answers "I just submitted, where did it go" on a page whose
default hides work in progress.

The empty state is the point of the whole thing: "no builds match, build
this configuration", carrying the filters into the form.

The form becomes three tiers -- what you're building, what it can do, how
it's built -- with the third collapsed and its summary counting
non-default values, so defaults stop competing for attention without
hiding anything. Runtime is tier one because it selects the toolchain
image rather than an argument, and renders as text when there is one
option, since a select with one option is a decision that isn't one.
enable_3d is tier two despite reading like an optimisation: it removes a
feature domain, and the server refuses an editor without it. Precision
is tier three despite changing the ABI, because it has a correct default
and tier one should not ask 99% of users a question they should not
answer. Grafted catalog modules and the built-in module tri-states sit
in one tier in labelled subgroups: they share the word "module" and
nothing else, and previously sat in different sections with no
explanation.

Focusing a control inside a closed <details> focuses nothing, so the
focus-first-invalid effect opens the containing one first -- otherwise a
rejected build would show a disabled button and no visible cause. It is
also scoped to the form now, since a document-wide query can find an
aria-invalid belonging to the layout. jsdom renders a closed <details>
as though it were open, so the existing assertions kept passing while no
longer asserting what they appear to; the new tests open it explicitly.

The draft key is deliberately not bumped: the stored shape did not
change, and bumping would discard every user's draft for a layout
change.

Above the Build button, a banner offering an identical build that
already succeeded. It never blocks: M7 records two builds of one
unchanged spec producing different digests, so CacheKey identifies a
request rather than an output, and refusing on it would assert something
this project has explicitly not established.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(roadmap): M12, the frontend people actually use
All checks were successful
CI / Dedupe (push) Successful in 10s
CI / Dedupe (pull_request) Successful in 1s
CI / Go (push) Successful in 3m6s
CI / Frontend (push) Successful in 30s
CI / Frontend (pull_request) Successful in 23s
CI / Go (pull_request) Successful in 1m38s
CI / Badges (push) Has been skipped
CI / Publish images (push) Has been skipped
CI / Badges (pull_request) Has been skipped
CI / Publish images (pull_request) Has been skipped
c5bf65ce71
Records the milestone and its seven tickets, taken ahead of M7 and M8
for the reason M9 and M10 were: nothing it is built against is still
moving. The listing API landed in M3, the anonymous device identity in
M10, and the option schema with the immutable-configuration work.

The Deviations section carries the ones worth the next reader's time:
that an empty status set is not a filter matching nothing and assuming
so was a disclosure bug; that scope=mine was not in the stated
visibility rule and the rule is wrong without it; that the nine build
options are on the card rather than in the query, because nine facets of
up to six values is a filter UI larger than the form it would spare you;
that the module facet needs an engine version first, since there is no
version-independent module list; and that dedup stays advisory until M7
settles whether one spec reproduces byte-identically.

The hand check in a real browser is marked outstanding rather than
claimed, for the same reason M4-10's and M10's browser halves are: there
is no browser automation here. These flows were driven through jsdom and
over HTTP against a real Postgres.

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!9
No description provided.