Membrane's directory is a git repo that lists every program in the platform. Each program is a git submodule pinned at a specific commit. The directory repo is the index; the submodules are the entries.
Keeping that index honest is a chore: every release of every program changes a sha somewhere, and if nobody repins, the directory drifts from reality. Roster does the chore automatically and runs on Membrane itself. It's ~280 lines of TypeScript plus one UI file.
Finding the programs: the submodule signature
The GitHub API returns directory entries without a download_url and with size === 0 for submodules. That's the fingerprint:
const programs = await directory.content.dir.$query(
`{ name sha html_url size download_url }`
);
const isSubmodule = (item: any) => !item.download_url && item.size === 0;
return programs.filter(isSubmodule).map((item) => ({
name: item.name,
html_url: item.html_url,
}));
Everything else in the repo — READMEs, config files — gets ignored. The directory's shape is the list of programs.
The red dot: comparing two shas
For each program, Roster reads two things: the sha pinned in the directory repo (via content.file({ path: name })) and the program repo's latest commit on main. If they differ, the program is outdated:
isOutdated: async (_, { obj }) => {
let lastCommit = obj.commits?.page.items[0].sha; // program repo, latest
let currentCommit = obj.sha; // pinned in directory
return lastCommit !== currentCommit;
}
The UI renders a green dot for up-to-date programs and a red one for outdated. The "Update" button only appears when it's needed.
The repin driver: rewriting the branch in three calls
The interesting part. Updating a program means re-pinning its submodule and moving the directory's main branch to a new commit, all through the GitHub API, from inside the program runtime:
update: async (_, { obj }) => {
const { name, url } = obj;
const [, user, repo] = url.match("https://github.com/([^/]+)/([^/]+)");
// 1. get the parent (directory) main sha and the child's main sha
const parent: any = await nodes.directory.branches.one({ name: "main" })
.commit.sha;
const children: any = await nodes.github.users
.one({ name: user }).repos.one({ name: repo })
.branches.one({ name: "main" }).commit.sha;
// 2. build a tree with the child pinned at its submodule path
const tree: any = await nodes.directory.createTree({
base: parent, tree: children, path: name,
});
// 3. commit the tree and move main to point at it
const commit: any = await nodes.directory.commits.create({
message: `Sync ${name}`, tree, parents: parent,
});
await nodes.directory.branches
.one({ name: "main" }).update({ sha: commit, ref: "heads/main" });
const program = state.programs.find((p) => p.name === name);
if (program) { program.isOutdated = false; }
}
The three steps mirror what a human does by hand: read the parent sha, write the new submodule pointer into a fresh tree, commit, and fast-forward the branch. The comment in the original says it plainly: "repin driver - update master to point to your commit".
The UI: rendered server-side, no client framework
The whole interface is React rendered to static HTML with renderToString. Two routes:
/renders the table: dot, program name, open pull requests (linked straight to the repo), Update button where needed./program?name=renders the detail page: stars, expression count and schema type count parsed from the program's ownmemconfig.json, last commit, and every open PR rendered with its body viamarked.
Program listings are cached in state with a refresh timestamp ("Refreshed 3 minutes ago"), and /refresh clears the cache. No database, no build step, no client bundle; the program is the server.
Lessons
- A repo of submodules is a database with a bad UI. The whole tool exists to turn "check shas, repin, commit, push" into a red dot and a button.
- Compare shas, don't trust timestamps. Two API calls and a string comparison is all the freshness detection needs.
- Admin tools get dogfooded when they run on the platform they administer. Roster manages Membrane from inside Membrane; the fix for a broken directory uses the same interface as the directory itself.
- Server-rendered HTML beats a client app for internal tools. No hydration, no API layer, no deploy of frontend assets; one endpoint renders the page.
It's a small tool. The directory index matches reality, and the fix is one click away from the problem.