Skip to content

Commit 8fd76e2

Browse files
authored
Fixed Corepack pnpm signature error handling (#2167)
no ref When Ghost installs a pnpm-based release without packageManager metadata, Corepack can resolve pnpm@latest and fail with an opaque signing-key error. This keeps Corepack from defaulting to latest during dependency installation and turns the known keyid failure into actionable guidance.
1 parent d89fc5c commit 8fd76e2

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

lib/tasks/install-dependencies.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ module.exports = function installDependencies(ui, archiveFile) {
101101
if (usePnpm(ctx.installPath)) {
102102
observable = pnpm(['install', '--prod', '--reporter=append-only'], {
103103
cwd: ctx.installPath,
104-
env: {NODE_ENV: 'production'},
104+
env: {NODE_ENV: 'production', COREPACK_DEFAULT_TO_LATEST: '0'},
105105
observe: true
106106
});
107107
} else {

lib/utils/pnpm.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,34 @@ function runPnpm(pnpmArgs, options) {
2222
}));
2323
}
2424

25+
function isCorepackSignatureError(error) {
26+
const output = [
27+
error.message,
28+
error.stderr,
29+
error.stdout,
30+
error.shortMessage,
31+
error.originalMessage
32+
].filter(Boolean).join('\n');
33+
34+
return output.includes('Cannot find matching keyid') && output.includes('corepack');
35+
}
36+
37+
function toPnpmError(error) {
38+
if (error instanceof SystemError) {
39+
return error;
40+
}
41+
42+
if (isCorepackSignatureError(error)) {
43+
return new SystemError({
44+
message: 'Corepack could not verify pnpm because its package-signing keys are out of date.',
45+
help: 'Update Corepack, enable it, and activate pnpm before running the Ghost command again.',
46+
suggestion: 'npm install -g corepack@latest && corepack enable'
47+
});
48+
}
49+
50+
return new ProcessError(error);
51+
}
52+
2553
/**
2654
* Runs a pnpm command. Can return an Observer which allows
2755
* listr to output the current status of pnpm
@@ -35,7 +63,7 @@ module.exports = function pnpm(pnpmArgs, options) {
3563
const cp = runPnpm(pnpmArgs || [], options);
3664

3765
if (!observe) {
38-
return cp.catch(error => Promise.reject(error instanceof SystemError ? error : new ProcessError(error)));
66+
return cp.catch(error => Promise.reject(toPnpmError(error)));
3967
}
4068

4169
return new Observable((observer) => {
@@ -47,7 +75,7 @@ module.exports = function pnpm(pnpmArgs, options) {
4775
cp.then(() => {
4876
observer.complete();
4977
}).catch((error) => {
50-
observer.error(error instanceof SystemError ? error : new ProcessError(error));
78+
observer.error(toPnpmError(error));
5179
});
5280
});
5381
};

test/unit/tasks/install-dependencies-spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ describe('Unit: Tasks > install-dependencies', function () {
161161
expect(pnpmStub.args[0][0]).to.deep.equal(['install', '--prod', '--reporter=append-only']);
162162
expect(pnpmStub.args[0][1]).to.deep.equal({
163163
cwd: '/var/www/ghost/versions/1.5.0',
164-
env: {NODE_ENV: 'production'},
164+
env: {NODE_ENV: 'production', COREPACK_DEFAULT_TO_LATEST: '0'},
165165
observe: true
166166
});
167167
});
@@ -229,7 +229,7 @@ describe('Unit: Tasks > install-dependencies', function () {
229229

230230
return installDependencies({listr: listrStub}).then(() => {
231231
const pnpmOpts = pnpmStub.args[0][1];
232-
expect(pnpmOpts.env).to.deep.equal({NODE_ENV: 'production'});
232+
expect(pnpmOpts.env).to.deep.equal({NODE_ENV: 'production', COREPACK_DEFAULT_TO_LATEST: '0'});
233233
expect(pnpmOpts.env).to.not.have.property('YARN_IGNORE_PATH');
234234
});
235235
});

test/unit/utils/pnpm-spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ describe('Unit: pnpm', function () {
9696
});
9797
});
9898

99+
it('returns a helpful system error when corepack cannot verify pnpm signatures', function () {
100+
const execa = sinon.stub().rejects({
101+
message: 'Command failed: pnpm install',
102+
stderr: '/usr/local/lib/node_modules/corepack/dist/lib/corepack.cjs:21535\nError: Cannot find matching keyid'
103+
});
104+
const pnpm = setup({execa});
105+
106+
return pnpm(['install']).then(() => {
107+
expect(false, 'Promise should have rejected').to.be.true;
108+
}).catch((error) => {
109+
expect(error).to.be.an.instanceOf(SystemError);
110+
expect(error.message).to.equal('Corepack could not verify pnpm because its package-signing keys are out of date.');
111+
expect(error.options.suggestion).to.equal('npm install -g corepack@latest && corepack enable');
112+
});
113+
});
114+
99115
describe('can return observables', function () {
100116
it('ends properly', function () {
101117
const execa = sinon.stub().callsFake(() => {
@@ -153,6 +169,35 @@ describe('Unit: pnpm', function () {
153169
});
154170
});
155171

172+
it('passes corepack signature errors through as helpful system errors', function () {
173+
const execa = sinon.stub().callsFake(() => {
174+
const promise = Promise.reject({
175+
message: 'Command failed: pnpm install',
176+
stderr: '/usr/local/lib/node_modules/corepack/dist/lib/corepack.cjs:21535\nError: Cannot find matching keyid'
177+
});
178+
promise.stdout = getReadableStream();
179+
return promise;
180+
});
181+
const pnpm = setup({execa});
182+
183+
const res = pnpm([], {observe: true});
184+
const subscriber = {
185+
next: sinon.stub(),
186+
error: sinon.stub(),
187+
complete: sinon.stub()
188+
};
189+
190+
res.subscribe(subscriber);
191+
192+
return res.toPromise().catch(() => {
193+
expect(subscriber.error.calledOnce).to.be.true;
194+
expect(subscriber.error.args[0][0]).to.be.an.instanceOf(SystemError);
195+
expect(subscriber.error.args[0][0].options.suggestion).to.contain('corepack@latest');
196+
expect(subscriber.error.args[0][0].options.suggestion).to.not.contain('prepare');
197+
expect(subscriber.complete.called).to.be.false;
198+
});
199+
});
200+
156201
it('passes data through', function () {
157202
const execa = sinon.stub().callsFake(() => {
158203
const promise = Promise.resolve();

0 commit comments

Comments
 (0)