From ac15a2433977bbd801fe54e70568bcf176ec0f1b Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Thu, 11 Nov 2021 10:30:02 -0700 Subject: [PATCH] fix: handle a corrupt stash.json file (#291) * fix: handle a corrupt stash.json file * fix: display both error messages and stacks --- messages/deploy.json | 1 + src/deployCommand.ts | 22 ++++++++++++++++++++-- test/commands/source/report.test.ts | 24 ++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/messages/deploy.json b/messages/deploy.json index 4d3b7f077..b51623c92 100644 --- a/messages/deploy.json +++ b/messages/deploy.json @@ -76,6 +76,7 @@ "MissingRequiredParam": "Missing one of the following parameters: %s", "checkOnlySuccess": "Successfully validated the deployment. %s components deployed and %s tests run.\nUse the --verbose parameter to see detailed output.", "MissingDeployId": "No deploy ID was provided or found in deploy history", + "InvalidStashFile": "Invalid deployment stash file encountered. File has been renamed to: %s. Please specify a deploy ID using the command parameter.", "deployCanceled": "The deployment has been canceled by %s", "deployFailed": "Deploy failed.", "asyncDeployQueued": "Deploy has been queued.", diff --git a/src/deployCommand.ts b/src/deployCommand.ts index e09b122f6..0832a225f 100644 --- a/src/deployCommand.ts +++ b/src/deployCommand.ts @@ -5,12 +5,18 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import * as fs from 'fs'; import { ComponentSet, DeployResult, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve'; import { ConfigAggregator, ConfigFile, PollingClient, SfdxError, StatusResult } from '@salesforce/core'; import { AnyJson, asString, getBoolean } from '@salesforce/ts-types'; import { Duration, once } from '@salesforce/kit'; import { SourceCommand } from './sourceCommand'; +interface StashFile { + isGlobal: boolean; + filename: string; +} + export abstract class DeployCommand extends SourceCommand { protected static readonly STASH_KEY = 'SOURCE_DEPLOY'; @@ -45,17 +51,29 @@ export abstract class DeployCommand extends SourceCommand { } protected resolveDeployId(id: string): string { + let stash: ConfigFile; if (id) { return id; } else { try { - const stash = this.getStash(); + stash = this.getStash(); stash.readSync(true); const deployId = asString((stash.get(DeployCommand.STASH_KEY) as { jobid: string }).jobid); this.logger.debug(`Using deploy ID: ${deployId} from ${stash.getPath()}`); return deployId; } catch (err: unknown) { const error = err as Error & { code: string }; + if (error.name === 'JsonParseError') { + const stashFilePath = stash.getPath(); + const corruptFilePath = `${stashFilePath}_corrupted_${Date.now()}`; + fs.renameSync(stashFilePath, corruptFilePath); + const invalidStashErr = SfdxError.create('@salesforce/plugin-source', 'deploy', 'InvalidStashFile', [ + corruptFilePath, + ]); + invalidStashErr.message = `${invalidStashErr.message}\n${error.message}`; + invalidStashErr.stack = `${invalidStashErr.stack}\nDue to:\n${error.stack}`; + throw invalidStashErr; + } if (error.code === 'ENOENT') { throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'MissingDeployId'); } @@ -106,7 +124,7 @@ export abstract class DeployCommand extends SourceCommand { return pollingClient.subscribe() as unknown as Promise; } - private getStash(): ConfigFile<{ isGlobal: true; filename: 'stash.json' }> { + private getStash(): ConfigFile { return new ConfigFile({ isGlobal: true, filename: 'stash.json' }); } } diff --git a/test/commands/source/report.test.ts b/test/commands/source/report.test.ts index 37bdd3a67..9cc31d271 100644 --- a/test/commands/source/report.test.ts +++ b/test/commands/source/report.test.ts @@ -6,8 +6,9 @@ */ import { join } from 'path'; +import * as fs from 'fs'; import * as sinon from 'sinon'; -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { fromStub, spyMethod, stubInterface, stubMethod } from '@salesforce/ts-sinon'; import { ConfigFile, Org, SfdxProject } from '@salesforce/core'; import { IConfig } from '@oclif/config'; @@ -37,6 +38,7 @@ describe('force:source:report', () => { let checkDeployStatusStub: sinon.SinonStub; let uxLogStub: sinon.SinonStub; let pollStatusStub: sinon.SinonStub; + let readSyncStub: sinon.SinonStub; class TestReport extends Report { public async runIt() { @@ -80,13 +82,16 @@ describe('force:source:report', () => { cmd.setOrg(orgStub); }); uxLogStub = stubMethod(sandbox, UX.prototype, 'log'); - stubMethod(sandbox, ConfigFile.prototype, 'readSync'); stubMethod(sandbox, ConfigFile.prototype, 'get').returns({ jobid: stashedDeployId }); checkDeployStatusStub = sandbox.stub().resolves(expectedResults); return cmd.runIt(); }; + beforeEach(() => { + readSyncStub = stubMethod(sandbox, ConfigFile.prototype, 'readSync'); + }); + afterEach(() => { sandbox.restore(); }); @@ -107,6 +112,21 @@ describe('force:source:report', () => { expect(progressBarStub.calledOnce).to.equal(true); }); + it('should rename corrupt stash.json and throw an error', async () => { + const jsonParseError = new Error(); + jsonParseError.name = 'JsonParseError'; + readSyncStub.throws(jsonParseError); + const renameSyncStub = stubMethod(sandbox, fs, 'renameSync'); + try { + await runReportCmd(['--json']); + assert(false, 'Expected report command to throw a JsonParseError'); + } catch (error: unknown) { + const err = error as Error; + expect(err.name).to.equal('InvalidStashFile'); + expect(renameSyncStub.calledOnce).to.be.true; + } + }); + it('should use the jobid flag', async () => { const getStashSpy = spyMethod(sandbox, Report.prototype, 'getStash'); const result = await runReportCmd(['--json', '--jobid', expectedResults.id]);