This repository contains the registry for lde.
The registry is just an easily indexable list of JSON files describing information for a package, and where to get it from.
No pre-built executables or library source code will be stored here.
You can use lde publish which will create a pull request with the changes to add your package to the registry.
Pull requests that touch packages/** are processed automatically by a bot (see Automation):
- New packages are validated against the schema and approved, but merged manually by a maintainer. The owner is recorded after merge.
- Updates to existing packages are auto-approved and auto-merged only if the author is the recorded package owner (or a repo admin) and the change adds exactly one new version without touching existing versions.
- Invalid JSON/schema → the bot requests changes with a list of errors.
- Non-owners touching an existing package → the PR is closed automatically.
Packages may be namespaced: a package named namespace/pkg is stored at packages/namespace/pkg.json, and the portfile's name field must equal the path relative to packages/ minus the .json extension.
Names follow these rules (applied to both the namespace and package parts):
- Lowercase only (
a-z), digits,_and-. No dots, no.., no empty segments, no leading/trailing/. - Must start with a letter and end with a letter or digit (no purely-special names like
---;a-ais fine). - A namespace is exactly one level deep:
foo/baris valid,foo/bar/bazis not. - The namespace part must be at least 3 characters long.
- The full name (
namespace+/+package) must be at most 128 characters. - Flat packages (no
/) keep working exactly as before.
{
"name": "hood",
"description": "Cross-platform rendering in pure LuaJIT.",
"authors": ["David Cruz <codebycruz@gmail.com>"],
"tags": ["rendering", "graphics"],
"git": "https://github.com/codebycruz/hood",
"branch": "master",
"versions": {
"0.1.0": "5d4bb28703d8f1c17a0e241810145194a51042f0"
}
}tags is an optional list of up to 10 unique, lowercase tags (a-z, digits and
-, at most 32 characters) used by clients to categorize packages.
Version objects store only what differs from the top-level source. A normal
hash-pinned git version is { "commit": "...", "sha256": "..." }; git and
branch appear only when that version moved to another repository or branch.
A version normally resolves to a 40-character lowercase commit hash in the
portfile's git/branch. When a release lives in a different repository or
branch, that version can carry its own source instead. New git versions also
carry a sha256 of the archive clients download, so a changed source server is
detected before extraction:
{
"versions": {
"0.1.0": "5d4bb28703d8f1c17a0e241810145194a51042f0",
"0.2.0": {
"commit": "0123456789abcdef0123456789abcdef01234567",
"git": "https://github.com/codebycruz/hood-next",
"branch": "main",
"sha256": "9f2c4e8a7b6d5c3e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e"
}
}
}A version can also be a hash-pinned tarball instead of a git commit. In that
case the top-level git is optional and every version must name an archive:
{
"name": "hood",
"versions": {
"1.0.0": {
"archive": "https://github.com/codebycruz/hood/releases/download/v1.0.0/hood-1.0.0.tar.gz",
"sha256": "9f2c4e8a7b6d5c3e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e"
}
}
}The client verifies sha256 before extracting. A mismatch means the source
changed after publication and the download is rejected.
A version can be revoked without deleting its entry or changing its commit/hash:
{
"revoked": ["1.0.0"]
}Revoked versions are skipped by ranges and latest; an exact pin fails with a
clear error. Only the package owner or a repo admin may revoke a version, and
only repo admins may un-revoke one. This is a content
commitment in the registry, not an author signature: it protects against a
changed or compromised source server as long as the registry entry itself is
trusted. Author-key signatures require a separate key distribution and trust
policy.
One-off data changes live in scripts/ and are named YYYY-MM-DD-<slug>.lua
so a directory listing is chronological. They are never run automatically.
Run one through the Run Migration workflow (workflow_dispatch) with a
script input such as scripts/2026-11-07-add-sha256.lua. Use dry_run first.
The workflow validates the path, runs the script, runs lde test, and commits
the resulting data changes.
Scripts must be idempotent and support --dry-run; see
scripts/README.md.
On creation of a package, the bot will ensure that your PR is valid by verifying it fits the JSON schema, and that you aren't overwriting old versions.
If it fails, it will request changes with a list of errors.
If there are no errors, it will be approved, and wait pending a maintainer accepting creation of a package. This part is not automatic as to avoid abuse, name-squatting, etc.
The automation is written in Lua and runs on lde. Its modules live under src/registry/ and the entry points under src/:
lde run src/generate-index.lua # dist/index.json
lde run src/check-pr.lua # PR verdict (check-pr workflow)
lde run src/record-metadata.lua # owners, first published, last updated
lde run src/namespace-request.lua # namespace issues
lde run src/backfill.lua # one-off metadata migration
lde test # automation test suiteDependencies (json, fs, path, process) and the lde-test dev dependency are declared in lde.json; there is no Bun or Node toolchain anymore. Scripts use process.exec for git and gh (argument lists, never shell interpolation), and the json package for JSON.
If your PR is successfully approved, the bot will make note of you as the owner of the package. This means that any PRs you make touching the package in the future will be automatically approved and accepted.
It works based off your GitHub id, so there will be no conflicts if your username changes.
Ownership lives in authority.json, which has two sections:
top: flat package name → list of owner GitHub ids.namespaces: namespace name → list of owner GitHub ids. A namespaced packagens/pkgis owned by the owner of its namespace (ns), so claiming a namespace claims every package in it.
Owners are stored as arrays (a package may have several). The bot only writes a fresh single-owner array when it records a new package. An existing owner can add co-owners through a pull request that only appends GitHub ids to keys they already own; the bot approves that PR and a maintainer merges it. Removing or reordering owners still requires a repo admin.
Repo admins bypass this check and can obviously modify packages at will.
last-updated.json is the registry's centralized package metadata file. For every package it records exactly two dates:
{
"html": {
"firstPublished": "2026-08-17T10:28:27Z",
"lastUpdated": "2026-08-20T09:14:03Z"
}
}firstPublished— when the package was first published to the registry (the "new package" date).lastUpdated— when a new version was last added (the "updated" date). It equalsfirstPublishedfor a package that has never been updated.backfilled— optional; present on entries reconstructed by the one-off migration rather than recorded as packages were published.
Both dates are derived from this repository's own history, and only from it. A portfile changes for exactly one reason — a version was added — so the commit that last touched packages/<name>.json is the last update, and the commit that first added it is the first publish. That keeps the metadata authoritative without trusting anything user-submitted: portfiles still contain nothing but the fields in the schema (no timestamps, nothing auto-generated beyond the commit hashes), and no date ever comes from a package's own repository.
Dates are taken as written and never moved backwards, so an incorrect entry can be corrected by hand, but history being rewritten cannot silently rewind the file.
Only repo admins may edit last-updated.json directly. The bot keeps it current on every push touching packages/**: it refreshes entries from git history, commits the file to master, then regenerates the index. The index generator only reads this file and never runs git log, so the index — and the dates the website shows — cannot be invented at build time, and building it needs nothing more than the file itself.
Packages published before the metadata file existed have no recorded dates. src/backfill.lua reconstructs them once from registry history — the commit that first added the portfile becomes firstPublished, the newest commit that touched it becomes lastUpdated — and flags each entry with "backfilled": true so the website can show them as estimates:
lde run src/backfill.lua
lde run src/backfill.lua -- --forceIt leaves entries that were recorded live alone, so it is safe to re-run, and --force recomputes backfilled entries. Run it on a full clone: a shallow one would date everything to the tip commit.
Namespaces aren't claimed through package PRs — they're requested through an issue. Open an issue containing /request-namespace <name> (this is what the lde website generates), and the bot will reply that a moderator must approve it. A repo moderator then comments !approve (or @robolde approve), and the bot creates the namespace in authority.json, assigns ownership to the issue author, and closes the issue. If the namespace already exists, the issue is closed with a notice.
- Schema: every changed package JSON must validate against
schemas/registry.schema.json, and thenamefield must match the path relative topackages/minus.json(see Naming). - Ownership: modifying or deleting an existing package requires its owner (for
ns/pkg, the namespace owner) or a repo admin. New packages are claimable by whoever submits them. - Versions: updates may not modify or remove existing versions. Exactly one new version must be added.
authority.json: repo admins may edit it directly. An existing owner may add co-owners to a package or namespace they own; the PR must only append ids to those owner arrays, and a maintainer merges it.last-updated.json: only repo admins may edit it directly (it is maintained by the bot).- New packages are never auto-merged. The bot approves them and a maintainer merges manually; the owner is recorded on
masterright after the merge.
The workflows authenticate with a GitHub App so the bot can comment, review, merge and push:
- Create a GitHub App (Settings → Developer settings → GitHub Apps).
- Grant it these repository permissions: Contents: Read and write (commits and the
distbranch), Pull requests: Read and write (comments, reviews, merges), Issues: Read and write (namespace requests), and Metadata: Read-only (mandatory). - Add the App id as an Actions variable named
REGISTRY_APP_ID, and a private key as an Actions secret namedREGISTRY_APP_PRIVATE_KEY. - Install the App on
lde-org/registry. If the App is installed on the organization with Only select repositories, add this repository explicitly. A token step that fails withNot Found - .../get-a-repository-installation-for-the-authenticated-appmeans the App is not installed on this repository, or the App id/private key belong to a different App.
check-pr.yml uses pull_request_target because fork PRs need the App token to comment, review and merge. It checks out the base revision and only reads PR contents via git show; it never executes fork code. GitHub's default event policy blocks pull_request_target on public repositories after December 2, 2026, so allow pull_request_target in the repository or organization Actions event policy if this workflow should keep running.
Each workflow mints a short-lived installation token with actions/create-github-app-token@v3 and passes it as GH_TOKEN (and as the checkout token where a push is needed). Nothing is stored in the repository. lde-org/setup-lde@v2 installs lde; the workflows no longer set up a JavaScript runtime or install JavaScript dependencies.
The bot App is not used for user login: an installation token is scoped to the repositories the App is installed on, so it cannot write a contributor's fork. Create a separate GitHub OAuth App for users:
- Set the callback URL to
https://lde.sh/api/auth/callback(add a local URL for development). - Enable Device flow so
lde loginworks from a terminal. - The CLI ships with the official OAuth App client ID, so users need no configuration. Override it with
github.clientIdin~/.lde/config.json(orLDE_GITHUB_CLIENT_ID) for a self-hosted app. The CLI requestspublic_repo offline_access; no client secret is stored. - Set
GITHUB_CLIENT_SECRETas a Cloudflare secret for the website (wrangler secret put). The client ID defaults to the official app; setGITHUB_CLIENT_IDonly for a self-hosted app.REGISTRY_REPO,REGISTRY_BRANCH, andSITE_URLare non-secret vars inwrangler.jsonc.
public_repo lets a signed-in user create issues and pull requests and write their own fork, which is how contributors without write access publish.
An installation token cannot call GET /user, so the bot's commits default to robolde / robolde@users.noreply.github.com. Set the optional REGISTRY_BOT_NAME and REGISTRY_BOT_EMAIL secrets to attribute them to a real account (for example a dedicated bot user's noreply address).