Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches:
- main

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Expand Down Expand Up @@ -43,6 +46,7 @@ jobs:

- run: pnpm install --prefer-offline --frozen-lockfile
- run: pnpm format:check
- run: pnpm lint

- name: Test affected (PRs)
if: github.event_name == 'pull_request'
Expand All @@ -63,7 +67,8 @@ jobs:

test-results:
runs-on: ubuntu-latest
name: All tests passed
name: All tests pass
permissions: {}
needs: [test]
if: always()
steps:
Expand Down
60 changes: 60 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Agent Notes

## Repository Shape

This is a pnpm/Nx monorepo for `@tryghost/*` framework packages. The root
workspace owns shared tooling; package source, tests, and package READMEs live
under `packages/*`.

Use the repo-pinned package manager:

```bash
corepack pnpm install --frozen-lockfile
```

The local Node version is pinned in `.nvmrc` to Node 24. CI runs tests on Node
22 and Node 24, so avoid introducing APIs that do not work on Node 22 unless
the package support policy is changed deliberately.

## Common Commands

Run these from the repository root:

```bash
corepack pnpm lint
corepack pnpm format:check
corepack pnpm test:ci
```

For a package-local loop:

```bash
cd packages/<package-name>
corepack pnpm test
corepack pnpm lint
```

Most package tests run with Vitest coverage. The shared coverage thresholds are
90% lines, 90% functions, 90% statements, and 80% branches. TypeScript packages
with source in `src/` need a package-level `vitest.config.ts` coverage include
that measures `src/**`.

## CI And Release Notes

The Test workflow installs with pnpm, checks formatting, runs oxlint, and runs
affected package tests on pull requests. Pushes to `main` run the full
`pnpm test:ci` suite. The stable required check is `All tests pass`.

Publishing is handled by `.github/workflows/publish.yml` after Nx release
commits. Use the root `pnpm ship:*` scripts for versioning; they run the
pre-ship test gate before creating release commits and tags.

## Cleanup Boundaries

Keep package-specific usage detail in the relevant package README. Add a root
`docs/` page only when the topic spans multiple packages and would make this
file or the root README hard to scan.

Generated build output, package `coverage/` folders, `node_modules/`, Nx cache,
and TypeScript build info are ignored. Do not commit generated artifacts unless
a package explicitly publishes that artifact from source control.
1 change: 1 addition & 0 deletions CLAUDE.md
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
# Framework

Framework is a monorepo of `@tryghost/*` packages used across Ghost services,
apps, and tooling. Each package lives under `packages/*` and has its own README
with package-specific usage examples.

## Install

Use the repo-pinned package manager from the root of the checkout:

```bash
corepack pnpm install
```

For consumers, install the package you need from npm:

```bash
pnpm add @tryghost/<package-name>
```

## Usage

Read the package README for the package you are using. Common examples:

- [`@tryghost/api-framework`](packages/api-framework/README.md) for API request
pipeline helpers.
- [`@tryghost/errors`](packages/errors/README.md) for shared Ghost error types.
- [`@tryghost/security`](packages/security/README.md) for token, password, and
identifier helpers.
- [`@tryghost/express-test`](packages/express-test/README.md) for HTTP test
helpers.

## Develop

This is a mono repository, managed with [Nx](https://nx.dev).
This is a monorepo, managed with [Nx](https://nx.dev).

1. `git clone` this repo & `cd` into it as usual
2. run `pnpm setup` from the top-level:
Expand All @@ -20,14 +46,16 @@ To add a new package to the repo:

## Run

- `pnpm dev`
- `pnpm dev` is a placeholder at the workspace root. Run package-specific
scripts from the package directory when a package has a development workflow.

## Test

- `pnpm lint` runs `oxlint` across all packages
- `pnpm format` formats `js/ts/json/md` files with `oxfmt`
- `pnpm format:check` checks formatting without writing
- `pnpm test` runs tests (most packages also run lint in `posttest`)
- `pnpm test` runs package tests through Nx
- `pnpm test:ci` runs the full CI test target for every package

## Publish

Expand Down
1 change: 1 addition & 0 deletions packages/errors/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default mergeConfig(
test: {
coverage: {
include: ['src/**'],
exclude: [],
},
},
}),
Expand Down
5 changes: 5 additions & 0 deletions packages/express-test/example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const isLoggedIn = function (req, res, next) {
app.use(express.json());

app.use(
// This is a local test fixture, not a production app; it intentionally
// avoids HTTPS-only cookies and CSRF middleware so package tests can run
// against a plain in-memory HTTP server.
// codeql[js/missing-token-validation]
// codeql[js/clear-text-cookie]
session({
secret: 'verysecretstring',
name: 'testauth',
Expand Down
10 changes: 5 additions & 5 deletions packages/job-manager/test/jobs/timed-job.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ const util = require('util');
const setTimeoutPromise = util.promisify(setTimeout);

const passTime = async (ms) => {
if (Number.isInteger(ms)) {
await setTimeoutPromise(ms);
} else {
await setTimeoutPromise(ms.ms);
const duration = Number.isInteger(ms) ? ms : ms?.ms;

if (Number.isInteger(duration)) {
await setTimeoutPromise(duration);
}
};

if (isMainThread) {
module.exports = passTime;
} else {
(async () => {
await passTime(workerData.ms);
await passTime(workerData && Object.hasOwn(workerData, 'ms') ? workerData.ms : workerData);
parentPort.postMessage('done');
// alternative way to signal "finished" work (not recommended)
// process.exit();
Expand Down
16 changes: 4 additions & 12 deletions packages/job-manager/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { defineConfig, mergeConfig } from 'vitest/config';
import rootConfig from '../../vitest.config';

// Override: Bree spawns background workers that emit unhandled rejections
// during cleanup after tests complete. These are expected and were silently
// ignored by Mocha.
export default mergeConfig(
rootConfig,
defineConfig({
test: {
dangerouslyIgnoreUnhandledErrors: true,
},
}),
);
// Keep unhandled errors fatal for this package. Worker fixtures should clean up
// without leaking rejections, and CI should catch regressions instead of
// matching Mocha's old silent behavior.
export default rootConfig;
3 changes: 3 additions & 0 deletions packages/mw-vhost/test/vhost.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ describe('vhost(hostname, server)', function () {
});

it('should treat dot as a dot', async function () {
// `hostregexp` escapes string hostnames before constructing its
// RegExp; this test asserts literal dots stay literal.
// codeql[js/incomplete-hostname-regexp]
const app = createServer('a.b.com', function (req, res) {
res.end('tobi');
});
Expand Down
4 changes: 4 additions & 0 deletions packages/nodemailer/lib/nodemailer.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ module.exports = function (transport, options = {}) {
case 'ses':
const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2');

// This keeps the legacy Ghost SES ServiceUrl parser compatible with
// existing config shapes; explicit `region` remains the preferred path.
// codeql[js/incomplete-hostname-regexp]
const pattern = /(.*)email(.*)\.(.*).amazonaws.com/i;
// codeql[js/polynomial-redos]
const result = pattern.exec(options.ServiceUrl);
const region = options.region || (result && result[3]) || 'us-east-1';

Expand Down
1 change: 1 addition & 0 deletions packages/prometheus-metrics/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default mergeConfig(
test: {
coverage: {
include: ['src/**'],
exclude: [],
},
},
}),
Expand Down
3 changes: 3 additions & 0 deletions packages/security/lib/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ module.exports.resetToken = {

hash.update(String(expires));
hash.update(email.toLocaleLowerCase());
// Reset tokens are not password storage; the current password hash is
// mixed in only to invalidate old tokens after password changes.
// codeql[js/insufficient-password-hash]
hash.update(password);
hash.update(String(dbHash));

Expand Down
9 changes: 5 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ packages:
overrides:
axios: ^1.15.0
fast-xml-parser: ^5.7.0
js-yaml@<3.15.0: 3.15.0
yauzl: 3.4.0
allowBuilds:
dtrace-provider: true
Expand Down
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
coverage: {
provider: 'v8',
include: ['**/lib/**'],
exclude: ['**/src/**', '**/build/**', '**/test/**'],
exclude: ['**/build/**', '**/test/**'],
reporter: ['text', 'cobertura'],
thresholds: {
lines: 90,
Expand Down