Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error handling #327

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
37 changes: 31 additions & 6 deletions src/modules/package-downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// SPDX-License-Identifier: Apache-2.0

import { join } from 'node:path';
import { CheckRepoActions, SimpleGit, simpleGit } from 'simple-git';
import { CheckRepoActions, ResetMode, SimpleGit, simpleGit } from 'simple-git';
import { CliFileSystem } from '../utils/fs-bridge';
import { PackageConfig } from './package';
import { BRANCH_PREFIX } from './semver';
Expand All @@ -26,8 +26,11 @@ export class PackageDownloader {
this.packageConfig = packageConfig;
}

private async _cloneRepository(packageDir: string, cloneOpts: string[]): Promise<void> {
await this.git.clone(this.packageConfig.getPackageRepo(), packageDir, cloneOpts);
private async _cloneRepository(packageDir: string, cloneOpts: string[], verbose?: boolean): Promise<void> {
const response = await this.git.clone(this.packageConfig.getPackageRepo(), packageDir, cloneOpts);
if (verbose) {
console.log(`Clone response: ${response}`);
}
}

private async _updateRepository(checkRepoAction: CheckRepoActions, verbose: boolean): Promise<void> {
Expand All @@ -46,18 +49,40 @@ export class PackageDownloader {

private async _checkoutVersion(version: string): Promise<void> {
const branchOrTag = version.startsWith(BRANCH_PREFIX) ? `origin/${version.substring(BRANCH_PREFIX.length)}` : version;
// Make sure that repo is clean to avoid that checkout fails!
await this.git.reset(ResetMode.HARD);
await this.git.checkout(branchOrTag);
}

private async _checkForValidRepo(packageDir: string, cloneOpts: string[], checkRepoAction: CheckRepoActions): Promise<void> {
private async _checkForValidRepo(
packageDir: string,
cloneOpts: string[],
checkRepoAction: CheckRepoActions,
verbose?: boolean,
): Promise<void> {
let directoryExists = CliFileSystem.existsSync(packageDir);
if (directoryExists && !(await this.isValidRepo(packageDir, checkRepoAction))) {
CliFileSystem.removeSync(packageDir);
directoryExists = false;
}

if (!directoryExists) {
await this._cloneRepository(packageDir, cloneOpts);
try {
// simple-git typically throws an error if clone fails
// but not all errors reult in an exception, for instance blocked clone due to rate limitation
// does not result in an exception
await this._cloneRepository(packageDir, cloneOpts, verbose);
} catch (error) {
if (verbose) {
console.error(error);
}
throw new Error(`Cloning of ${this.packageConfig.getPackageRepo()} failed!`);
}

// Do a second check to verify if clone seems to have succeeded
if (!(await this.isValidRepo(packageDir, checkRepoAction))) {
throw new Error(`Problem detected when cloning ${this.packageConfig.getPackageRepo()}!`);
}
}
}

Expand All @@ -80,7 +105,7 @@ export class PackageDownloader {
checkRepoAction = CheckRepoActions.IS_REPO_ROOT;
}

await this._checkForValidRepo(packageDir, cloneOpts, checkRepoAction);
await this._checkForValidRepo(packageDir, cloneOpts, checkRepoAction, verbose);
this.git = simpleGit(packageDir);
await this._updateRepository(checkRepoAction, verbose);

Expand Down
39 changes: 24 additions & 15 deletions src/modules/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,23 @@ export class PackageConfig {
}

async getPackageVersions(verbose?: boolean): Promise<TagResult> {
try {
const packageInformation = await packageDownloader(this).downloadPackage({ checkVersionOnly: true, verbose: verbose });
const packageVersionTags = await packageInformation.tags();
return packageVersionTags;
} catch (error) {
console.log(error);
}
return {
all: [],
latest: '',
};
const packageInformation = await packageDownloader(this).downloadPackage({ checkVersionOnly: true, verbose: verbose });
const packageVersionTags = await packageInformation.tags();
return packageVersionTags;
}

async downloadPackageVersion(verbose?: boolean): Promise<void> {
try {
await packageDownloader(this).downloadPackage({ verbose: verbose });
} catch (error) {
console.error(error);
if (verbose) {
console.error(error);
}
// If repo exist but not version we will end up with default-version
// of that repo, and on subsequent runs tooling will say that the missing version
// actually exists! To prevent this we remove the directory of the missing version!
CliFileSystem.removeSync(this.getPackageDirectoryWithVersion());
throw new Error(`Cannot find package ${this.getPackageName()}:${this.version}`);
throw new Error(`Downloading package ${this.getPackageName()}:${this.version} failed!`);
}
return;
}
Expand All @@ -125,7 +119,20 @@ export class PackageConfig {
}

async isPackageValidRepo(verbose?: boolean): Promise<boolean> {
const isValid = await packageDownloader(this).isValidRepo(this.getPackageDirectoryWithVersion());
// When this is called we have likely already done _resolveVersion so the first check is not likely to fail
let isValid = await packageDownloader(this).isValidRepo(this.getPackageDirectoryWithVersion());
if (isValid) {
// Lets do a sanity check that there actually is a manifest file
const path = this.getManifestFilePath();
try {
let data = CliFileSystem.readFileSync(path);
} catch (error) {
if (verbose) {
console.log(`Cannot find manifest file ${path}`);
}
isValid = false;
}
}
if (!isValid && verbose) {
console.log(`... Corrupted .git directory found for: '${this.getPackageName()}:${this.version}'`);
}
Expand All @@ -138,7 +145,9 @@ export class PackageConfig {
const path = this.getManifestFilePath();
data = CliFileSystem.readFileSync(path);
} catch (error) {
console.log(`Cannot find package ${this.getPackageName()}:${this.version}. Please upgrade or init first!`);
console.log(
`Cannot find package ${this.getPackageName()}:${this.version}. Please verify that it is a valid Velocitas package and do upgrade/init!`,
);
throw new Error(`Cannot find package ${this.getPackageName()}:${this.version}`);
}
try {
Expand Down
6 changes: 4 additions & 2 deletions src/modules/projectConfig/projectConfigFileReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ export class MultiFormatConfigReader implements IProjectConfigReader {
break;
}
} catch (error: any) {
console.warn(`Warning: ${path} not in expected format: ${error.message}, falling back to legacy format reading.`);
// This could be format error, but it could also be that repo/tag-settings are faulty, repo cannot be reached and
// similar, for example that "velocitas init" did not succeed!
console.warn(`Error reported by config reader ${reader.constructor.name}: ${error.message}`);
}
}

if (config === null) {
throw new Error(`Unable to read ${path}: unknown format!`);
throw new Error(`Unable to successfully read and interpret ${path}!`);
}

return config;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/projectConfig/projectConfigLock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ProjectConfigLock implements ProjectConfigLockAttributes {
public findVersion(packageName: string): string {
const packageVersion = this.packages.get(packageName);
if (!packageVersion) {
throw new Error(`Package '${packageName}' not found in lock file.`);
throw new Error(`Package '${packageName}' not found in lock file. Have you performed "velocitas init"?`);
}
return packageVersion;
}
Expand Down
26 changes: 21 additions & 5 deletions test/commands/init/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,37 @@ describe('init', () => {
});

test.do(() => {
// Make sure that one package lack manifest.json
mockFolders({ velocitasConfig: true, packageIndex: true, installedComponents: true });
CliFileSystem.removeSync(
`${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}/manifest.json`,
);
CliFileSystem.promisesMkdir(
`${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`,
);
})
.stdout()
.stub(gitModule, 'simpleGit', (stub) => stub.returns(simpleGitInstanceMock(undefined, false)))
.stub(gitModule, 'simpleGit', (stub) => stub.returns(simpleGitInstanceMock(undefined, true, true)))
.stub(exec, 'runExecSpec', (stub) => stub.returns({}))
.command(['init', '-v'])
.it('downloads corrupted packages again', (ctx) => {
.it('refreshing corrupted package', (ctx) => {
expect(ctx.stdout).to.contain('Initializing Velocitas packages ...');

expect(ctx.stdout).to.contain(
`... Corrupted .git directory found for: '${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion}'`,
`... Corrupted .git directory found for: '${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}'`,
`Some error ${ctx.stdout}`,
);
expect(ctx.stdout).to.contain(
`... Downloading package: '${runtimePackageInfoMock.repo}:${runtimePackageInfoMock.resolvedVersion}'`,
);
expect(ctx.stdout).to.contain(`... Downloading package: '${corePackageInfoMock.repo}:${corePackageInfoMock.resolvedVersion}'`);
expect(
CliFileSystem.existsSync(
`${userHomeDir}/.velocitas/packages/${corePackageInfoMock.repo}/${corePackageInfoMock.resolvedVersion}`,
`${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}`,
),
);
expect(
CliFileSystem.existsSync(
`${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}/manifest.json`,
),
).to.be.true;
});
Expand Down
15 changes: 13 additions & 2 deletions test/helpers/simpleGit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
// SPDX-License-Identifier: Apache-2.0

import { CliFileSystem } from '../../src/utils/fs-bridge';
import { corePackageManifestMock, runtimePackageManifestMock, setupPackageManifestMock } from '../utils/mockConfig';
import { corePackageManifestMock, runtimePackageInfoMock, runtimePackageManifestMock, setupPackageManifestMock } from '../utils/mockConfig';
import { userHomeDir } from '../utils/mockfs';

export const simpleGitInstanceMock = (mockedNewVersionTag?: string, checkRepo: boolean = true) => {
export const simpleGitInstanceMock = (mockedNewVersionTag?: string, checkRepo: boolean = true, resetRuntime: boolean = false) => {
return {
clone: async (repoPath: string, localPath: string, options?: any) => {
await CliFileSystem.promisesMkdir(localPath);
Expand All @@ -33,6 +34,16 @@ export const simpleGitInstanceMock = (mockedNewVersionTag?: string, checkRepo: b
return checkRepo;
},
fetch: () => {},
reset: async () => {
// This is to recover in test case "refreshing corrupted package"
// In the case we have "corrupted" the repo by removing the manifest
if (resetRuntime) {
await CliFileSystem.promisesWriteFile(
`${userHomeDir}/.velocitas/packages/${runtimePackageInfoMock.repo}/${runtimePackageInfoMock.resolvedVersion}/manifest.json`,
JSON.stringify(runtimePackageManifestMock),
);
}
},
checkout: () => {
// Function implementation
},
Expand Down
2 changes: 1 addition & 1 deletion test/unit/projectConfigIO.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ describe('projectConfigIO - module', () => {
it('should handle errors when parsing .velocitas.json file', () => {
const readFileStub = sinon.stub(CliFileSystem, 'readFileSync').throws();
const projectConfigFileReader = () => ProjectConfigIO.read('', configFilePath, true);
expect(projectConfigFileReader).to.throw(`Unable to read ${configFilePath}: unknown format!`);
expect(projectConfigFileReader).to.throw(`Unable to successfully read and interpret ${configFilePath}`);
readFileStub.restore();
});

Expand Down