← all writing

Roster: managing a program directory from inside Membrane

A directory repo made of git submodules, a red dot when a program is outdated, and a one-click repin that moves the main branch from inside the platform itself.

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:

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

It's a small tool. The directory index matches reality, and the fix is one click away from the problem.