Open-source project
TryGhost/framework avatar
TryGhost/framework

TryGhost/framework ships in lockstep, so one release command bumps every package

A collection of handy components for building Node.js applications

37 stars25 forksJavaScriptMIT

At a glance

What is it?
A pnpm and Nx monorepo of shared Node.js packages where publishing is split in two: you run a version command locally, and continuous integration does the npm publish over OIDC with no long-lived token. The rule that surprises contributors is that every ship command touches every package unless you pass an explicit project list.
Who is it for?
Use this repository if you are working inside the Ghost codebase and need the shared packages wired locally, since the setup script is what links them. Two things to internalise before your first release.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • TryGhost/framework37 ★ · +0
Star history
Repository2026-08-222026-11-14
TryGhost/framework3737

0 Stars in the last 84 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A ship command bumps every package unless you scope it

The release commands sit at the top level of the repository and there are four of them, one per semver level, plus a fifth reserved for bootstrapping Nx in a repository that has gone a long time without a release. The default behaviour of all of them is the part that catches new contributors out: by default they bump every package under the packages directory to the same level. Lockstep versioning is a defensible choice for a monorepo whose packages are released together, but it means touching one line of code in one package produces a version bump across the whole tree. To avoid that you pass a projects flag with a comma-separated list. The list takes npm package names, not directory names, and that distinction is called out in the documentation because the two differ in exactly this repository. A dry run flag previews the set without committing anything.

code
pnpm ship:patch
pnpm ship:minor
pnpm ship:major
pnpm ship:first-release
code
pnpm ship:minor --projects=@tryghost/errors,@tryghost/domain-events

Publishing happens in CI, over OIDC, with no long-lived token

The publish step is not something a developer runs, and the split is explicit. Locally you run a ship command, which tests, versions the packages, creates a release commit, pushes it, and pushes tags to the main branch. A workflow then takes over and does the actual registry upload. That workflow authenticates to npm using trusted publishing, which the documentation annotates as OIDC with no long-lived tokens, so there is no npm secret to leak or rotate. Before uploading, it compares the version in every package directory against what the registry already has, and it publishes only the versions that are not already there, through the workspace release command. Provenance attestations are enabled for those publishes. The practical consequence for you as a contributor is that a green local ship command does not mean the packages are live; the workflow is the step that decides that, and it is idempotent because of the version comparison.

code
nx release version --git-push --git-remote ${GHOST_UPSTREAM:-origin}

A dirty working tree blocks the release before tests even run

There is a preflight hook wired into the release path, and its failure message is worth knowing because it is the first thing that will stop you. The hook checks both the unstaged and the staged diff, and if either is non-empty it prints an error saying the working tree must be clean before shipping and exits. Only after that check passes does the test suite run. So the release script enforces a strict order: clean tree first, tests second, version and push third. That ordering means an uncommitted experiment in your checkout will block a release rather than being swept into the commit, which is a deliberate guard rather than an accident. It also means the common failure mode of running a ship command from a dirty branch is a clear message instead of a version bump that accidentally contains unrelated work.

The root dev script is a placeholder that prints Implement me!

Running the development script at the workspace root does nothing, and the repository says so in as many words: it is a placeholder. When a package has a development workflow, you are expected to change into that package directory and run its scripts there instead. This is a monorepo managed with Nx, so the tooling is oriented around per-project targets rather than a single root process, and the root script is kept only so that the obvious command does not error out. The same split shows up in the test targets, which run many project targets at once, and in the release targets, which are likewise per project. If you arrive expecting one command to start everything the way a single-package repository would behave, that expectation is the thing to unlearn first.

code
pnpm setup

Formatting is enforced on staged files by a git hook

The formatting story has two layers and the first one is not CI. A prepare hook installs Husky, and the staged file configuration maps a glob covering JavaScript, TypeScript, JSON and Markdown inside the packages tree onto the formatter. The practical effect is that whatever you stage gets rewritten before the commit lands, so formatting drift tends not to accumulate locally. On top of that there are two explicit commands: one that formats the matched files in place and one that checks formatting without writing, the latter being what continuous integration uses. The formatter and the linter are two different tools with their own configuration files at the root, both invoked against the packages directory rather than the whole checkout. It is a small, modern stack, and the notable part is that the enforcement point is the local hook rather than a pull request review.

The two test targets differ only in parallelism and output style

There are two test commands and the difference between them is smaller than you would expect. Both run many project test targets through Nx. The development one runs ten in parallel with a dynamic output style, so a long suite keeps the terminal busy and finishes faster on a developer machine. The continuous integration one drops the parallelism setting and switches to a static output style, which is what you want in a log: deterministic, ordered, and readable when a single package is failing at three in the morning. The test runner underneath is Vitest, with a coverage provider as a development dependency, and a test configuration file at the root. Unit tests are written with a mocking library whose version comes from the workspace catalog rather than being pinned in each package, which is the subject of the next section.

code
nx run-many -t test --parallel=10 --outputStyle=dynamic-legacy
code
nx run-many -t test --outputStyle=static

One dependency version is declared once, through a catalog entry

Most dependencies in this repository are ordinary version ranges or exact pins in the development dependency block, and the versions are strict enough to remove ambiguity: the workspace tool, its JavaScript plugin, the test runner, its coverage provider, the formatter, the linter, the hook runner and the staged file runner all carry exact versions rather than ranges. One entry does not. The mocking library is referenced through the catalog mechanism, which is a workspace level declaration that packages point at instead of repeating a version. The practical difference is that bumping the mocking library is a single edit rather than a search and replace across every package that uses it. The repository also carries a dependency update bot configuration at the root, so routine upgrades arrive as their own pull requests rather than being folded into feature work.

New packages are scaffolded by a separate tool, not by hand

Adding a package has its own documented path and it does not involve creating a directory and writing a manifest. You install a separate scaffolding tool from the same organisation and then run its new command with the package name. That tool exists because a monorepo has conventions a hand written directory would miss: the workspace file, the shared tooling configuration, the license header, the lint and format globs, and the release registration are all things you would otherwise have to remember. The consumer side is simpler than any of this, because packages are published to npm individually. A project that depends on Ghost services installs the one package it needs rather than this repository, and the documentation tells you to read that package's own readme for its usage examples, with three named as common starting points: shared error types, security helpers covering tokens, passwords and identifiers, and HTTP test helpers.

code
pnpm add @tryghost/<package-name>

Editorial conclusion

Use this repository if you are working inside the Ghost codebase and need the shared packages wired locally, since the setup script is what links them. Two things to internalise before your first release. A ship command bumps every package in the packages directory to the same level unless you scope it, and the scoping flag takes npm package names rather than directory names, which is an easy mistake. Second, you never publish locally: you version and tag, and a workflow publishes to the registry using short lived identity instead of a token. If you are an external consumer, none of this applies and the right move is to install the single package you need from npm.

Frequently asked questions

What does "framework" mean in the TryGhost repository?

It is a monorepo of shared Node.js packages scoped under the @tryghost namespace, used across Ghost services, apps and tooling. Every package lives in its own directory with its own readme and its own usage examples, and the repository itself is not published as a single installable package.

how to use framework in web development

As a consumer you install only the package you need from npm rather than this repository. Three are named as common examples: shared Ghost error types, security helpers for tokens, passwords and identifiers, and HTTP test helpers. Inside the Ghost codebase you would instead run the setup script, which installs external dependencies and links the internal ones.

how to install framework

From a checkout, use the repository-pinned package manager through Corepack at the root, then run the setup script to install external dependencies and link internal ones. Outside the repository there is nothing to clone, because consumers add the individual package they need from the registry.

Who is framework owned by?

The repository is authored by the Ghost Foundation and the copyright line runs from 2013 to 2026, released under the MIT license. The manifest marks the repository itself as private, which is consistent with a workspace whose packages, not the monorepo, are what get published.

Official sources

  1. Official README
  2. Project repository
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tryghost-framework.svg)](https://hysenlabs.com/projects/tryghost-framework)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/tryghost-framework.svg)](https://hysenlabs.com/projects/tryghost-framework)

Was this useful?