diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e8e174af..ac22a4b9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,9 @@ on: branches: - main +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -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' @@ -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: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..977d617e2 --- /dev/null +++ b/AGENTS.md @@ -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/ +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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index de3e9f0b1..539fd2822 100644 --- a/README.md +++ b/README.md @@ -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/ +``` + ## 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: @@ -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 diff --git a/packages/errors/vitest.config.ts b/packages/errors/vitest.config.ts index e0f620be4..f8d74ee3d 100644 --- a/packages/errors/vitest.config.ts +++ b/packages/errors/vitest.config.ts @@ -9,6 +9,7 @@ export default mergeConfig( test: { coverage: { include: ['src/**'], + exclude: [], }, }, }), diff --git a/packages/express-test/example/app.js b/packages/express-test/example/app.js index 3e31cab84..238924356 100644 --- a/packages/express-test/example/app.js +++ b/packages/express-test/example/app.js @@ -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', diff --git a/packages/job-manager/test/jobs/timed-job.js b/packages/job-manager/test/jobs/timed-job.js index 41af2e3e5..0daf99bec 100644 --- a/packages/job-manager/test/jobs/timed-job.js +++ b/packages/job-manager/test/jobs/timed-job.js @@ -3,10 +3,10 @@ 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); } }; @@ -14,7 +14,7 @@ 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(); diff --git a/packages/job-manager/vitest.config.ts b/packages/job-manager/vitest.config.ts index 97eb6c098..22a229fae 100644 --- a/packages/job-manager/vitest.config.ts +++ b/packages/job-manager/vitest.config.ts @@ -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; diff --git a/packages/mw-vhost/test/vhost.test.js b/packages/mw-vhost/test/vhost.test.js index c88244180..9541e927f 100644 --- a/packages/mw-vhost/test/vhost.test.js +++ b/packages/mw-vhost/test/vhost.test.js @@ -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'); }); diff --git a/packages/nodemailer/lib/nodemailer.js b/packages/nodemailer/lib/nodemailer.js index d25a69e47..7d5d85f61 100644 --- a/packages/nodemailer/lib/nodemailer.js +++ b/packages/nodemailer/lib/nodemailer.js @@ -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'; diff --git a/packages/prometheus-metrics/vitest.config.ts b/packages/prometheus-metrics/vitest.config.ts index e0f620be4..f8d74ee3d 100644 --- a/packages/prometheus-metrics/vitest.config.ts +++ b/packages/prometheus-metrics/vitest.config.ts @@ -9,6 +9,7 @@ export default mergeConfig( test: { coverage: { include: ['src/**'], + exclude: [], }, }, }), diff --git a/packages/security/lib/tokens.js b/packages/security/lib/tokens.js index d929313a8..bff3f3286 100644 --- a/packages/security/lib/tokens.js +++ b/packages/security/lib/tokens.js @@ -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)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc6ee64ea..150732ece 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,7 @@ catalogs: overrides: axios: ^1.15.0 fast-xml-parser: ^5.7.0 + js-yaml@<3.15.0: 3.15.0 yauzl: 3.4.0 importers: @@ -3865,8 +3866,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} hasBin: true js-yaml@4.2.0: @@ -6468,7 +6469,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.2 + js-yaml: 3.15.0 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} @@ -8511,7 +8512,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: + js-yaml@3.15.0: dependencies: argparse: 1.0.10 esprima: 4.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 27b9ec357..c81ada2d6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/vitest.config.ts b/vitest.config.ts index 1dc3af8bc..096dc4bfa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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,