feat(sdk): publish a type bundle per Flatpak SDK - #14
Merged
Conversation
The published `@girs/*` set is generated from one GIR snapshot — today GNOME 51 development, `libraryVersion` 4.23.3 for Gtk. An app pinned to `gnome//50` (Gtk 4.22.4) is a cycle behind it and gets types offering APIs its runtime does not have. Asked for repeatedly, most recently from Workbench. Each channel is now one package: `ts-for-gir --bundle` emits every namespace of one SDK as a single self-contained npm package, so the whole set installs, resolves and versions as a unit. Per-namespace packages cannot give that — npm is free to satisfy `@girs/glib-2.0` from the registry for one namespace and from the pinned copy for another, and two copies of a namespace are duplicate `declare module 'gi://GLib'` blocks, not a version skew. Nothing is committed. A channel is 34 MB and rebuilt monthly, and it regenerates in seconds from the SDK, so the registry is the artifact store and this repository holds the recipe: `sdk-channels.json`, the workflow, and the planner. `sdk/` is gitignored. Nor is there a state file. Every published channel manifest carries the SDK commit and the generator version it came from, so `plan.mjs` asks the registry whether a rebuild is due — a checked-in copy of that answer is a second truth that drifts. The two decisions it makes both fail silently when wrong (a skip that should have built goes stale on a green run; a version that already exists is refused as EPUBLISHCONFLICT, or reported as a successful no-op), so they are a tested program rather than a shell expression in a step. One of its ten cases already corrected a wrong assumption of mine about prerelease versions. Separate workflow, on purpose: `release.yml` sweeps the committed tree on every push, while the channels move with the SDKs — an SDK updates inside a cycle without ts-for-gir releasing, and ts-for-gir releases without the SDK moving. Hence a weekly poll, plus a `generator-released` dispatch from ts-for-gir. The publisher is the same one, given a `--root` so each sweep publishes exactly the tree it means to. Adding a channel stays one line here, plus one `gjsify onboard` bootstrap for the new package name against this workflow file: Trusted Publishing can update a package but cannot create one, and the 404 reads like a broken publisher. Claude-Session: https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
| const line = `${match[1]}.${match[2]}`; | ||
|
|
||
| const patches = publishedVersions | ||
| .map((version) => new RegExp(`^${line.replace(".", "\\.")}\\.(\\d+)$`).exec(version)) |
yargs ignores flags it does not know, so `--bundle` against a ts-for-gir that predates it is not an error: the run emits per-namespace packages named `@girs/gtk-4.0` into `sdk/`, which is the directory this workflow hands to the publisher. The manifest check downstream does stop it — there is no root manifest to read — but only after a full generation, and only because that check happens to look for something bundle mode alone produces. Asked before any of that, the answer is one `--help` line, and the failure names the cause instead of being inferred from a missing file. This is the window between merging the channels and releasing the generator that carries `--bundle`; the weekly poll runs in it either way. Claude-Session: https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
The channel list had one global `remote: flathub` and included `master`, which
Flathub does not carry. Measured against the Flathub OSTree repo on 2026-09-05:
stable `org.gnome.Sdk` ends at **50**. `master` is on gnome-nightly, and 51
exists so far only as `51beta` on flathub-beta — so the master leg would have
failed at `flatpak install` with a resolution error naming neither the remote
nor the reason.
Reproduced locally while preparing the bootstrap, and it fails one step earlier
than expected: `flatpak install --user gnome-nightly …` against a remote that
is configured system-wide but not per-user reads `gnome-nightly` as a package
NAME ("Nur das letzte Namenssegment darf - enthalten"). The workflow adds the
remote per-user from the URL first, so it does not hit this — but the error is
worth knowing, because it points at the package rather than the remote.
So the remote belongs to the channel, with the URLs in one map, and two checks
that were implicit are now explicit: the plan job refuses a channel naming a
remote the manifest does not define, and the channel job refuses a branch the
remote does not carry — the shape a cycle takes when it is declared here before
Flathub publishes it.
Claude-Session: https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
CodeQL, `js/regex-injection`, high: `nextVersion` assembled its pattern from
`generatorVersion`, a command-line argument. The `^(\d+)\.(\d+)\.` guard above
it does make the value harmless in practice, but the escaping meant to make it
safe was `line.replace(".", "\\.")` — which replaces only the FIRST dot. It read
as escaping and was one input away from not being it.
A constant pattern compared field by field says the same thing without either
problem, and the ten cases pass unchanged.
Also gives the plan job an explicit `permissions: contents: read`
(`actions/missing-workflow-permissions`, medium). It reads the repository and
the public registry and writes nothing; the channel job already declared its
own.
Claude-Session: https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
`release-types.yml`, which sends the `generator-released` dispatch, and `release-app.yml`, which publishes the CLI, both fire on the SAME `release: published` event. So the dispatched run asking npm for `latest` races the publish it is a consequence of: whichever workflow wins, the channels would be built by the PREVIOUS generator and record it as their provenance — and, until the `--bundle` guard, would have built it silently. The dispatch already carries the release tag, so the run now waits for that exact version to appear (up to 20 minutes) instead of asking which is newest. Every other trigger — the weekly poll, a manual dispatch — has no version in mind and keeps asking for the current one. Claude-Session: https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
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.
The published
@girs/*set is generated from one GIR snapshot — today GNOME 51 development (libraryVersion4.23.3 for Gtk). An app pinned tognome//50(Gtk 4.22.4) is a cycle behind it and gets types offering APIs its runtime does not have. Asked for repeatedly, most recently from Workbench in gjsify/ts-for-gir#462.This publishes one package per SDK channel:
@girs/sdk-gnome-49,@girs/sdk-gnome-50,@girs/sdk-gnome-master. Each is the whole GIR set of that SDK, generated as ONE self-contained npm package byts-for-gir --bundle(gjsify/ts-for-gir#463), so the set installs, resolves and versions as a unit. Per-namespace packages cannot give that: npm may satisfy@girs/glib-2.0from the registry for one namespace and from the pinned copy for another, and two copies of a namespace are duplicatedeclare module 'gi://GLib'blocks — ts-for-gir#431.Design
Nothing is committed. A channel is 34 MB, rebuilt monthly, and regenerates from the SDK in seconds — measured:
generate '*'overorg.gnome.Sdk//50is 11 s for 112 namespaces, 4.9 MB packed. The registry is the artifact store; this repository holds the recipe (sdk-channels.json, the workflow, the planner).sdk/is gitignored.No state file. Every published channel manifest carries the SDK commit and generator version it came from, so
plan.mjsasks the registry whether a rebuild is due. A checked-in copy of that answer is a second truth that drifts. Custom manifest fields do survive into the registry — verified:npm view @girs/gtk-4.0 libraryVersion→4.23.3.A separate workflow, because the cadences differ:
release.ymlsweeps the committed tree on every push, while an SDK updates inside a GNOME cycle without ts-for-gir releasing, and ts-for-gir releases without the SDK moving. Hence a weekly poll plus agenerator-releaseddispatch from ts-for-gir. The publisher is the same hardened script, given a--rootso each sweep publishes exactly the tree it means to.The SDK needs no sandbox.
org.gnome.Sdk//50ships 113 GIR XML files (the Platform runtime ships only typelibs), and they lie on disk as ordinary files — the generator runs beside them with the normal toolchain.Tests
plan.mjsmakes the two decisions that fail silently when wrong: a skip that should have built goes stale on a green run, and a version that already exists is refused as EPUBLISHCONFLICT or reported as a successful no-op. Ten cases cover both directions of each — never published, unchanged, SDK moved, generator moved, missing provenance, first patch, next patch, a gap in the line, prereleases, and a non-semver generator. One of them already corrected a wrong assumption of mine.Before the first run
@girs/sdk-gnome-*are new names, and Trusted Publishing can update a package but cannot create one — the 404 reads like a broken trusted publisher. Each needs onegjsify onboardbootstrap againstsdk-types.yml(notrelease.yml). Worth a--dry-rundispatch first.🤖 Generated with Claude Code
https://claude.ai/code/session_01FvMH98Xsfh6rAJK5HRfxFq
Channel availability, measured 2026-09-05
Queried against the Flathub OSTree repo directly (the local
flathubremote is a filtered Fedora one and cannot answer this):49,50master51org.gnome.Sdkends at 5051betaThat is why the remote moved from a single global value into each channel:
masteragainstflathubwould have failed atflatpak install. GNOME 51 is due mid-September; adding it is one line here plus onegjsify onboardrun for@girs/sdk-gnome-51.Bootstrap
The three bundles are generated locally and validated, so the bootstrap is a single command. Dry-run output:
--packages 'sdk/*'scopes discovery to the bundles, so the 704 existing@girs/*packages are not touched.@girs/sdk-gnome-49@girs/sdk-gnome-50@girs/sdk-gnome-masterEach packs to ~5.0 MB (34 MB unpacked, 916 files for
50).