Skip to content

Publishing to the Typst Universe

The Typst Universe is a git repository: typst/packages. Publishing a package means opening a pull request that adds packages/preview/<name>/<version>/ to it. There is no upload endpoint and no account to register.

gotpm publish does the tedious half of that — the fork, the branch, the correct destination directory, the commit — and hands you a gh pr create command for the rest. gotpm never talks to GitHub itself.

One-time setup

Fork typst/packages on GitHub yourself; gotpm never creates a fork. Then tell gotpm where it is:

$ gotpm config set fork.url https://github.com/you/packages

Optionally, choose where the clone of that fork lives. Unset, it is derived from fork.url — forks/<host>/<owner>/<repo> inside gotpm's data directory:

$ gotpm config set fork.path ~/src/typst-packages
$ gotpm config list

Or set both at once in your editor with gotpm config edit.

That clone is a fork clone: gotpm's staging area, holding no work that does not exist elsewhere. Delete it whenever you like; the next publish clones it again. It is checked out sparsely, scoped to the package directory being published, so cloning the whole Universe is not the cost it sounds like.

If you publish on behalf of more than one organisation, each fork gets its own clone, and switching fork.url is enough to switch between them: the default location follows the url. A fork.path you set yourself does not follow it, so switch the two together — gotpm refuses to publish into a clone of another fork rather than pushing your submission to the wrong owner.

Publishing a version

From the package's working tree:

$ gotpm publish
info: committed "release: mypkg 0.1.0" on branch mypkg-0.1.0
info: pushed mypkg-0.1.0. Open a PR with:
gh pr create --repo typst/packages --base main --head you:mypkg-0.1.0 --draft --title "release: mypkg 0.1.0"

What happened, in order:

  1. The manifest is located, and the package root — the directory holding typst.toml, not necessarily the one you ran the command in — becomes the source.
  2. The fork clone is created if missing, and its origin/main is fetched, so a clone you made months ago does not cut a branch from a stale base.
  3. A branch named <name>-<version> is checked out, tracking the fork's branch of that name if it already exists.
  4. The package files are copied into packages/preview/<name>/<version>/, honouring .gitignore and .typstignore, with .git, .gitignore and .typstignore themselves left out.
  5. The result is committed and pushed to your fork.

Then run the gh pr create line it printed. A submission — one package version on one branch of your fork — is as far as gotpm goes; publication is the Universe maintainers merging it.

Reviewing before you push

--local stops after the commit and prints the push command instead of running it:

$ gotpm publish --local
info: committed "release: mypkg 0.1.0" on branch mypkg-0.1.0
info: push it when you are ready:
git -C /home/you/.local/share/gotpm/forks/github.com/you/packages push origin mypkg-0.1.0

Useful the first time, when you want to see exactly what landed in the fork clone before anything becomes public.

Fix-ups

Publishing the same version again — because a reviewer asked for a change — checks out the existing branch and commits on top of it, so the open pull request updates rather than a second one appearing. For a fix-up run the commit message is taken from your package repository's own HEAD commit, so the reason for the change carries over instead of a second identical release: line.

Override it whenever you want:

$ gotpm publish -m "fix: entrypoint path in the manifest"

Bump instead of re-releasing

Fix-ups are for a submission that has not been merged yet. Once a version is in the Universe, its content is fixed: one coordinate names one content, for good. Publish a fix as a new version.

Generated files

Some files belong in the submission but not in your repository — a thumbnail.png rendered from Typst source, say. Hooks in the manifest generate them for the publish and clean them up after it:

[tool.gotpm]
pre-publish-hook = ["typst compile docs/thumbnail.typ thumbnail.png"]
post-publish-hook = ["rm -f thumbnail.png"]

Each is a list of shell commands, run in order from the package root. The pre-publish hook runs before the package files are copied, and a failing command stops the publish before anything reaches the fork clone. The post-publish hook runs once publish is done, whether it succeeded or not, so what the pre-publish hook left behind is always cleaned up. Commands are interpreted by a built-in POSIX shell, so the same hook works on every platform; the programs it calls, like typst, must still be on your $PATH. Publish prints each command, prefixed with $, before it runs it.

Hooks are not sandboxed: they run as you, with your environment, and can do anything you can. Review changes to [tool.gotpm] the way you would review a script, and pass --no-hooks to publish without running either hook. Files a hook would generate are then missing from the submission unless they already exist.

A generated file is usually listed in .gitignore, and publish honours .gitignore — so re-include it in .typstignore, or it never reaches the submission:

# .typstignore
!thumbnail.png

A release checklist

$ gotpm bump minor          # move the version in typst.toml
$ gotpm check lib.typ       # every import resolves
$ gotpm install -n preview  # try the Universe coordinate locally
$ gotpm publish             # stage the submission and push

Do not forget the parts gotpm cannot do for you: the Universe requires a description, a license and an authors list in the manifest, and it expects a README.md beside it. Read the submission guidelines once before your first package.

Requirements

gotpm publish is the only command that shells out to a system git binary — it needs sparse checkout and commit signing, which the pure-Go git library cannot do. Every other command, remote installs included, needs no git on your $PATH.