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
2 changes: 1 addition & 1 deletion .agents/skills/add-admin-api-endpoint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: Add a new endpoint or endpoints to Ghost's Admin API at `ghost/api/
2. The endpoint file should create a controller object using the JSDoc type from (@tryghost/api-framework).Controller, including at minimum a `docName` and a single endpoint definition, i.e. `browse`.
3. Add routes for each endpoint to `ghost/core/core/server/web/api/endpoints/admin/routes.js`.
4. Add basic `e2e-api` tests for the endpoint in `ghost/core/test/e2e-api/admin` to ensure the new endpoints function as expected.
5. Run the tests and iterate until they pass: `cd ghost/core && yarn test:single test/e2e-api/admin/{test-file-name}`.
5. Run the tests and iterate until they pass: `cd ghost/core && pnpm test:single test/e2e-api/admin/{test-file-name}`.

## Reference
For a detailed reference on Ghost's API framework and how to create API controllers, see [reference.md](reference.md).
4 changes: 2 additions & 2 deletions .agents/skills/add-private-feature-flag/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Adds a new private feature flag to Ghost. Private flags appear in Labs settings
- Add a new entry to the `features` array with `title`, `description`, and `flag` (must match the string in `labs.js`).

3. **Run tests and update the config API snapshot**
- Unit: `cd ghost/core && yarn test:single test/unit/shared/labs.test.js`
- Update snapshot and run e2e: `cd ghost/core && UPDATE_SNAPSHOTS=1 yarn test:single test/e2e-api/admin/config.test.js`
- Unit: `cd ghost/core && pnpm test:single test/unit/shared/labs.test.js`
- Update snapshot and run e2e: `cd ghost/core && UPDATE_SNAPSHOTS=1 pnpm test:single test/e2e-api/admin/config.test.js`
- Review the diff of `ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap` to confirm only your new flag was added.

## Notes
Expand Down
8 changes: 4 additions & 4 deletions .agents/skills/create-database-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ description: Create a database migration to add a table, add columns to an exist

## Instructions

1. Create a new, empty migration file: `cd ghost/core && yarn migrate:create <kebab-case-slug>`. IMPORTANT: do not create the migration file manually; always use this script to create the initial empty migration file. The slug must be kebab-case (e.g. `add-column-to-posts`).
1. Create a new, empty migration file: `cd ghost/core && pnpm migrate:create <kebab-case-slug>`. IMPORTANT: do not create the migration file manually; always use this script to create the initial empty migration file. The slug must be kebab-case (e.g. `add-column-to-posts`).
2. The above command will create a new directory in `ghost/core/core/server/data/migrations/versions` if needed, create the empty migration file with the appropriate name, and bump the core and admin package versions to RC if this is the first migration after a release.
3. Update the migration file with the changes you want to make in the database, following the existing patterns in the codebase. Where appropriate, prefer to use the utility functions in `ghost/core/core/server/data/migrations/utils/*`.
4. Update the schema definition file in `ghost/core/core/server/data/schema/schema.js`, and make sure it aligns with the latest changes from the migration.
5. Test the migration manually: `yarn knex-migrator migrate --v {version directory} --force`
5. Test the migration manually: `cd ghost/core && pnpm knex-migrator migrate --v {version directory} --force`
6. If adding or dropping a table, update `ghost/core/core/server/data/exporter/table-lists.js` as appropriate.
7. If adding or dropping a table, also add or remove the table name from the expected tables list in `ghost/core/test/integration/exporter/exporter.test.js`. This test has a hardcoded alphabetically-sorted array of all database tables — it runs in CI integration tests (not unit tests) and will fail if the new table is missing.
8. Run the schema integrity test, and update the hash: `yarn test:single test/unit/server/data/schema/integrity.test.js`
9. Run unit tests in Ghost core, and iterate until they pass: `cd ghost/core && yarn test:unit`
8. Run the schema integrity test, and update the hash: `cd ghost/core && pnpm test:single test/unit/server/data/schema/integrity.test.js`
9. Run unit tests in Ghost core, and iterate until they pass: `cd ghost/core && pnpm test:unit`

## Examples
See [examples.md](examples.md) for example migrations.
Expand Down
4 changes: 1 addition & 3 deletions .codex/environments/environment.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@ name = "Ghost"

[setup]
script = '''
yarn setup
(cd node_modules/sqlite3 && npm run install)
(cd ghost/parse-email-address && yarn build)
pnpm setup
'''
7 changes: 0 additions & 7 deletions .cursor/rules/yarn.mdc

This file was deleted.

2 changes: 1 addition & 1 deletion .cursor/worktrees.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"setup-worktree": [
"git submodule update --init --recursive",
"yarn"
"pnpm"
]
}
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ There is no need to include what modules have changed in the commit message, as

### Submitting Pull Requests

We aim to merge any straightforward, well-understood bug fixes or improvements immediately, as long as they pass our tests (run `yarn test` to check locally). We generally don’t merge new features and larger changes without prior discussion with the core product team for tech/design specification.
We aim to merge any straightforward, well-understood bug fixes or improvements immediately, as long as they pass our tests (run `pnpm test` to check locally). We generally don’t merge new features and larger changes without prior discussion with the core product team for tech/design specification.

Please provide plenty of context and reasoning around your changes, to help us merge quickly. Closing an already open issue is our preferred workflow. If your PR gets out of date, we may ask you to rebase as you are more familiar with your changes than we will be.

Expand Down
7 changes: 7 additions & 0 deletions .github/actions/restore-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ runs:
echo "::warning::Dependency cache could not be restored after retry - installing dependencies from scratch"
bash .github/scripts/install-deps.sh

- name: Re-link dependencies after cache restore
if: steps.dep-cache.outputs.cache-hit == 'true' || steps.dep-cache-retry-attempt.outputs.cache-hit == 'true'
shell: bash
run: |
echo "::notice::Re-linking pnpm dependencies after cache restore"
bash .github/scripts/install-deps.sh --force

- name: Set cache miss output
id: check-cache
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[ -n "$CI" ] && exit 0

yarn lint-staged --relative
pnpm lint-staged --relative
lintStatus=$?

if [ $lintStatus -ne 0 ]; then
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/check-app-version-bump.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ function main() {

if (compareSemver(prVersion, mainVersion) <= 0) {
failedApps.push(
`${app.key} (${app.packageName}) was changed but version was not bumped above main (${prVersion} <= ${mainVersion}). Please run "yarn ship" in ${app.path} to bump the package version.`
`${app.key} (${app.packageName}) was changed but version was not bumped above main (${prVersion} <= ${mainVersion}). Please run "pnpm ship" in ${app.path} to bump the package version.`
);
continue;
}
Expand Down
11 changes: 0 additions & 11 deletions .github/scripts/clean.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// NOTE: this file can't use any NPM dependencies because it needs to run even if dependencies aren't installed yet or are corrupted
const {execSync} = require('child_process');

cleanYarnCache();
resetNxCache();
deleteNodeModules();
deleteBuildArtifacts();
Expand Down Expand Up @@ -43,13 +42,3 @@ function resetNxCache() {
process.exit(1);
}
}

function cleanYarnCache() {
console.log('Cleaning yarn cache...');
try {
execSync('rm -rf .yarncache/* .yarncachecopy/*');
} catch (error) {
console.error('Failed to clean yarn cache:', error);
process.exit(1);
}
}
100 changes: 58 additions & 42 deletions .github/scripts/dependency-inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,31 @@ const path = require('path');
const jsonc = require('jsonc-parser');
const { execSync } = require('child_process');

/**
* Parse pnpm outdated --json output into an array of
* [packageName, current, wanted, latest, dependencyType] tuples.
*
* pnpm's JSON output is an object keyed by package name:
* { "pkg": { "current": "1.0.0", "wanted": "1.0.1", "latest": "2.0.0", "dependencyType": "dependencies" } }
*/
function parsePnpmOutdatedOutput(stdout) {
if (!stdout || !stdout.trim()) {
return [];
}

const data = JSON.parse(stdout);
return Object.entries(data).map(([name, info]) => [
name,
info.current,
info.wanted,
info.latest,
info.dependencyType
]);
}

/**
* Smart lockfile drift detector that focuses on actionable updates
* and avoids API rate limits by using yarn's built-in commands where possible
* and avoids API rate limits by using pnpm's built-in commands where possible
*/

class LockfileDriftDetector {
Expand Down Expand Up @@ -94,7 +116,30 @@ With a severity flag, shows all packages with that update type.
// Read from project root (two levels up from .github/scripts/)
const rootDir = path.join(__dirname, '../..');
const rootPackage = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
const workspacePatterns = rootPackage.workspaces || [];

// Read workspace patterns from pnpm-workspace.yaml (primary) or package.json (fallback)
let workspacePatterns = [];
const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml');
if (fs.existsSync(pnpmWorkspacePath)) {
const content = fs.readFileSync(pnpmWorkspacePath, 'utf8');
let inPackages = false;
for (const line of content.split('\n')) {
if (/^packages:/.test(line)) {
inPackages = true;
continue;
}
if (inPackages) {
const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
if (match) {
workspacePatterns.push(match[1]);
} else if (/^\S/.test(line)) {
break;
}
}
}
} else {
workspacePatterns = rootPackage.workspaces || [];
}

console.log('📦 Scanning workspaces...');

Expand Down Expand Up @@ -189,58 +234,29 @@ With a severity flag, shows all packages with that update type.
}

/**
* Use yarn outdated to get comprehensive outdated info
* Use pnpm outdated to get comprehensive outdated info
* This is much faster and more reliable than manual API calls
*/
async getOutdatedPackages() {
console.log('🔄 Running yarn outdated (this may take a moment)...');
console.log('🔄 Running pnpm outdated (this may take a moment)...');

let stdout;
try {
// yarn outdated returns non-zero exit code when packages are outdated
// so we need to handle that
const result = execSync('yarn outdated --json', {
stdout = execSync('pnpm outdated --json', {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output
});

const lines = result.trim().split('\n');
const outdatedData = [];

for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.type === 'table' && data.data && data.data.body) {
outdatedData.push(...data.data.body);
}
} catch (e) {
// Skip non-JSON lines
}
}

return outdatedData;
} catch (error) {
// yarn outdated exits with code 1 when there are outdated packages
// pnpm outdated exits with code 1 when there are outdated packages
if (error.status === 1 && error.stdout) {
const lines = error.stdout.trim().split('\n');
const outdatedData = [];

for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.type === 'table' && data.data && data.data.body) {
outdatedData.push(...data.data.body);
}
} catch (e) {
// Skip non-JSON lines
}
}

return outdatedData;
stdout = error.stdout;
} else {
console.error('Failed to run yarn outdated:', error.message);
console.error('Failed to run pnpm outdated:', error.message);
return [];
}
}

return parsePnpmOutdatedOutput(stdout);
}

/**
Expand Down Expand Up @@ -441,7 +457,7 @@ With a severity flag, shows all packages with that update type.
console.log('\n🚀 UPDATE COMMANDS:');
console.log('─'.repeat(80));
for (const pkg of filteredDirect) {
console.log(` yarn upgrade ${pkg.name}@latest`);
console.log(` pnpm update ${pkg.name}@latest`);
}
}

Expand Down Expand Up @@ -581,7 +597,7 @@ With a severity flag, shows all packages with that update type.
console.log('🚀 SUGGESTED COMMANDS (highest impact first):');
for (const pkg of topUpdates) {
const impactNote = pkg.workspaceCount > 1 ? ` (affects ${pkg.workspaceCount} workspaces)` : '';
console.log(` yarn upgrade ${pkg.name}@latest${impactNote}`);
console.log(` pnpm update ${pkg.name}@latest${impactNote}`);
}
console.log('');
}
Expand Down
25 changes: 25 additions & 0 deletions .github/scripts/enforce-package-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const userAgent = process.env.npm_config_user_agent || '';

if (/\bpnpm\//.test(userAgent)) {
process.exit(0);
}

const detectedPackageManager = userAgent.split(' ')[0] || 'unknown';

console.error(`
Ghost now uses pnpm for dependency installation.

Detected package manager: ${detectedPackageManager}

Use one of these instead:
corepack enable pnpm
pnpm install

Common command replacements:
yarn setup -> pnpm setup
yarn dev -> pnpm dev
yarn test -> pnpm test
yarn lint -> pnpm lint
`);

process.exit(1);
26 changes: 5 additions & 21 deletions .github/scripts/install-deps.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
#!/bin/bash
set -euo pipefail

# Install dependencies with --ignore-scripts and selectively run sqlite3 postinstall
# This maintains security while ensuring sqlite3 binaries are built when needed
# Install dependencies with retry logic for flaky npm registry connections in CI.
# TODO: This script only adds retry logic over a bare `pnpm install --frozen-lockfile`.
# Consider removing it and using GHA's built-in retry mechanisms instead.

max_attempts=4

install_dependencies() {
yarn install --frozen-lockfile --prefer-offline --ignore-scripts "$@"
pnpm install --frozen-lockfile --prefer-offline "$@"
}

for attempt in $(seq 1 "$max_attempts"); do
echo "Installing dependencies with --ignore-scripts... (attempt ${attempt}/${max_attempts})"
echo "Installing dependencies... (attempt ${attempt}/${max_attempts})"

if install_dependencies "$@"; then
break
Expand All @@ -26,20 +27,3 @@ for attempt in $(seq 1 "$max_attempts"); do
echo "::warning::Dependency installation failed, retrying in ${sleep_seconds} seconds..."
sleep "$sleep_seconds"
done

# Check if sqlite3 binary already exists (from cache or previous build)
if [ -d "node_modules/sqlite3" ]; then
# Check both possible binary locations:
# 1. build/Release/node_sqlite3.node (built by node-gyp rebuild)
# 2. lib/binding/*/node_sqlite3.node (downloaded by prebuild-install)
if [ -f "node_modules/sqlite3/build/Release/node_sqlite3.node" ]; then
echo "✓ sqlite3 binary found in build/Release/, skipping rebuild"
elif find node_modules/sqlite3/lib/binding -name "node_sqlite3.node" 2>/dev/null | grep -q .; then
echo "✓ sqlite3 prebuilt binary found in lib/binding/, skipping rebuild"
else
echo "Building sqlite3 native module..."
(cd node_modules/sqlite3 && npm run install)
fi
else
echo "⚠ sqlite3 package not found in node_modules"
fi
Loading