From 6ee8c14e06c2cbd607dd528de424f66c8890143d Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 7 Feb 2026 11:44:24 +0000 Subject: [PATCH] ci: Add TypeScript type checking to CI Fix all 215 TypeScript type errors and add `pnpm typecheck` to the lint workflow. This ensures type safety is enforced on all PRs. Changes: - Add @types/semver and tslib dependencies - Add TypedTargetConfig utility for strongly-typed target configs - Fix type issues in targets, utils, and test files - Clean up unused imports in ~40 test files - Enable incremental TypeScript compilation for faster CI - Fix all ESLint warnings (unused directives, catch bindings) --- .github/workflows/lint.yml | 3 + .gitignore | 1 + eslint.config.mjs | 16 +- package.json | 2 + pnpm-lock.yaml | 11 + src/__mocks__/fs.ts | 3 +- src/__tests__/prepare-dry-run.e2e.test.ts | 1 - src/artifact_providers/__tests__/base.test.ts | 7 +- src/commands/__tests__/prepare.test.ts | 22 +- src/commands/publish.ts | 4 +- src/config.ts | 6 +- src/schemas/project_config.ts | 14 + src/targets/__tests__/awsLambda.test.ts | 12 +- .../__tests__/commitOnGitRepository.test.ts | 16 +- src/targets/__tests__/crates.test.ts | 4 +- src/targets/__tests__/docker.test.ts | 326 +++++++++++------- src/targets/__tests__/github.test.ts | 26 +- src/targets/__tests__/index.test.ts | 1 - src/targets/__tests__/maven.test.ts | 207 +++++------ src/targets/__tests__/mavenDiskIo.test.ts | 94 +++-- src/targets/__tests__/npm.test.ts | 40 +-- src/targets/__tests__/powershell.test.ts | 12 +- src/targets/__tests__/pubDev.test.ts | 62 ++-- src/targets/__tests__/sentryPypi.test.ts | 1 - src/targets/__tests__/symbolCollector.test.ts | 39 ++- src/targets/__tests__/upm.test.ts | 10 +- src/targets/awsLambdaLayer.ts | 23 +- src/targets/brew.ts | 52 ++- src/targets/cocoapods.ts | 22 +- src/targets/commitOnGitRepository.ts | 38 +- src/targets/gcs.ts | 44 ++- src/targets/ghPages.ts | 56 +-- src/targets/github.ts | 91 +++-- src/targets/maven.ts | 191 +++++----- src/targets/npm.ts | 22 +- src/targets/nuget.ts | 40 ++- src/targets/powershell.ts | 31 +- src/targets/pubDev.ts | 55 ++- src/targets/registry.ts | 150 ++++---- src/targets/sentryPypi.ts | 48 ++- src/targets/symbolCollector.ts | 28 +- src/targets/upm.ts | 53 +-- src/types/mustache.d.ts | 8 +- src/utils/__tests__/async.test.ts | 40 ++- src/utils/__tests__/autoVersion.test.ts | 15 +- .../__tests__/awsLambdaLayerManager.test.ts | 4 +- src/utils/__tests__/calver.test.ts | 16 +- src/utils/__tests__/changelog-extract.test.ts | 11 +- .../__tests__/changelog-file-ops.test.ts | 32 +- .../__tests__/changelog-generate.test.ts | 8 +- .../changelog-semver-warning.test.ts | 2 +- src/utils/__tests__/changelog-utils.test.ts | 8 +- src/utils/__tests__/env.test.ts | 36 +- src/utils/__tests__/files.test.ts | 3 +- .../__tests__/fixtures/changelog-mocks.ts | 23 +- src/utils/__tests__/gcsAPI.test.ts | 69 ++-- src/utils/__tests__/git.test.ts | 2 +- src/utils/__tests__/githubApi.test.ts | 8 +- src/utils/__tests__/helpers.test.ts | 3 +- src/utils/__tests__/objects.test.ts | 1 - src/utils/__tests__/packagePath.test.ts | 1 - src/utils/__tests__/registry.test.ts | 58 ++-- src/utils/__tests__/strings.test.ts | 25 +- src/utils/__tests__/symlink.test.ts | 29 +- src/utils/__tests__/system.test.ts | 200 ++--------- src/utils/__tests__/version.test.ts | 10 +- src/utils/__tests__/workspaces.test.ts | 272 ++++++++++++--- src/utils/gcsApi.ts | 41 +-- src/utils/git.ts | 23 +- src/utils/helpers.ts | 25 +- src/utils/registry.ts | 17 +- src/utils/tracing.ts | 7 +- src/utils/workspaces.ts | 37 +- tsconfig.json | 4 +- 74 files changed, 1647 insertions(+), 1275 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 24f2e331..0213a03b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,9 +28,12 @@ jobs: path: | node_modules .eslintcache + .tsbuildinfo key: ${{ runner.os }}-${{ hashFiles('package.json', 'pnpm-lock.yaml') }} - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile + - name: Type Check + run: pnpm typecheck - name: Lint run: pnpm lint -f github-annotations diff --git a/.gitignore b/.gitignore index 03e03aaf..d8287832 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ npm-debug.log .Trashes *.env .eslintcache +.tsbuildinfo .idea diff --git a/eslint.config.mjs b/eslint.config.mjs index cb16d013..a7a5ab18 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,14 @@ import prettier from 'eslint-config-prettier'; export default tseslint.config( { - ignores: ['docs/**', 'dist/**', 'node_modules/**', 'coverage/**', '*.mjs', '**/*.js'], + ignores: [ + 'docs/**', + 'dist/**', + 'node_modules/**', + 'coverage/**', + '*.mjs', + '**/*.js', + ], }, eslint.configs.recommended, ...tseslint.configs.recommended, @@ -18,7 +25,10 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': 'off', 'no-constant-condition': ['error', { checkLoops: false }], // Make sure variables marked with _ are ignored (ex. _varName) - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_' }, + ], '@typescript-eslint/ban-ts-comment': [ 'error', { @@ -47,5 +57,5 @@ export default tseslint.config( }, ], }, - } + }, ); diff --git a/package.json b/package.json index 021aabe1..41afce6b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/ora": "^1.3.4", "@types/prompts": "^2.0.11", "@types/rimraf": "^2.0.2", + "@types/semver": "^7.7.1", "@types/shell-quote": "^1.6.0", "@types/tar": "^4.0.0", "@types/tmp": "^0.0.33", @@ -68,6 +69,7 @@ "string-length": "3.1.0", "tar": "7.5.7", "tmp": "0.2.4", + "tslib": "^2.8.1", "typescript": "^5.7.2", "typescript-eslint": "^8.18.2", "vitest": "^3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c64a7d79..c5b39875 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@types/rimraf': specifier: ^2.0.2 version: 2.0.5 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 '@types/shell-quote': specifier: ^1.6.0 version: 1.7.5 @@ -197,6 +200,9 @@ importers: tmp: specifier: 0.2.4 version: 0.2.4 + tslib: + specifier: ^2.8.1 + version: 2.8.1 typescript: specifier: ^5.7.2 version: 5.9.3 @@ -1596,6 +1602,9 @@ packages: '@types/rimraf@2.0.5': resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -4972,6 +4981,8 @@ snapshots: '@types/glob': 9.0.0 '@types/node': 22.19.1 + '@types/semver@7.7.1': {} + '@types/shell-quote@1.7.5': {} '@types/tar@4.0.5': diff --git a/src/__mocks__/fs.ts b/src/__mocks__/fs.ts index dffa974b..9a5373c4 100644 --- a/src/__mocks__/fs.ts +++ b/src/__mocks__/fs.ts @@ -1,7 +1,6 @@ import { vi } from 'vitest'; // Import the actual fs module using require to avoid circular mock issues -// eslint-disable-next-line @typescript-eslint/no-require-imports const actualFs = require('fs') as typeof import('fs'); // Mock existsSync to return true by default @@ -104,4 +103,4 @@ export const readv = actualFs.readv; export default { ...actualFs, existsSync, -}; +} as typeof actualFs; diff --git a/src/__tests__/prepare-dry-run.e2e.test.ts b/src/__tests__/prepare-dry-run.e2e.test.ts index 05cd7291..f2b36596 100644 --- a/src/__tests__/prepare-dry-run.e2e.test.ts +++ b/src/__tests__/prepare-dry-run.e2e.test.ts @@ -13,7 +13,6 @@ import { resolve, join } from 'path'; import { mkdtemp, rm, writeFile, readFile, mkdir, chmod } from 'fs/promises'; import { existsSync } from 'fs'; import { tmpdir } from 'os'; -// eslint-disable-next-line no-restricted-imports, no-restricted-syntax -- Test file needs direct git access for setup/verification import simpleGit from 'simple-git'; const execFileAsync = promisify(execFile); diff --git a/src/artifact_providers/__tests__/base.test.ts b/src/artifact_providers/__tests__/base.test.ts index e3793b4b..8ebce6af 100644 --- a/src/artifact_providers/__tests__/base.test.ts +++ b/src/artifact_providers/__tests__/base.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { parseFilterOptions, RawFilterOptions } from '../base'; describe('parseFilterOptions', () => { @@ -29,12 +28,12 @@ describe('parseFilterOptions', () => { const parsedFilters = parseFilterOptions(rawFilters); expect(parsedFilters.includeNames).toStrictEqual( - includeNames && /include/ + includeNames && /include/, ); expect(parsedFilters.excludeNames).toStrictEqual( - excludeNames && /exclude/ + excludeNames && /exclude/, ); - } + }, ); }); diff --git a/src/commands/__tests__/prepare.test.ts b/src/commands/__tests__/prepare.test.ts index 7dc857c1..cc7b24c2 100644 --- a/src/commands/__tests__/prepare.test.ts +++ b/src/commands/__tests__/prepare.test.ts @@ -1,5 +1,4 @@ import { vi, describe, test, expect, beforeEach, type Mock } from 'vitest'; -import { join as pathJoin } from 'path'; import { spawnProcess } from '../../utils/system'; import { runPreReleaseCommand, checkVersionOrPart } from '../prepare'; @@ -34,7 +33,7 @@ describe('runPreReleaseCommand', () => { CRAFT_NEW_VERSION: newVersion, CRAFT_OLD_VERSION: oldVersion, }, - } + }, ); }); @@ -57,7 +56,7 @@ describe('runPreReleaseCommand', () => { CRAFT_NEW_VERSION: newVersion, CRAFT_OLD_VERSION: oldVersion, }, - } + }, ); }); }); @@ -71,8 +70,8 @@ describe('checkVersionOrPart', () => { { newVersion: v, }, - null - ) + null, + ), ).toBe(true); } }); @@ -83,8 +82,8 @@ describe('checkVersionOrPart', () => { { newVersion: 'auto', }, - null - ) + null, + ), ).toBe(true); }); @@ -96,8 +95,8 @@ describe('checkVersionOrPart', () => { { newVersion: bumpType, }, - null - ) + null, + ), ).toBe(true); } }); @@ -110,8 +109,7 @@ describe('checkVersionOrPart', () => { }, { v: 'v2.3.3', - e: - 'Invalid version or version part specified: "v2.3.3". Removing the "v" prefix will likely fix the issue', + e: 'Invalid version or version part specified: "v2.3.3". Removing the "v" prefix will likely fix the issue', }, ]; for (const t of invalidVersions) { @@ -120,7 +118,7 @@ describe('checkVersionOrPart', () => { { newVersion: t.v, }, - null + null, ); }; expect(fn).toThrow(t.e); diff --git a/src/commands/publish.ts b/src/commands/publish.ts index 002d38ec..4fa84f8a 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -637,7 +637,9 @@ export async function publishMain(argv: PublishOptions): Promise { // finishes then as nothing relies on the removal of this file. safeFs .unlink(publishStateFile) - .catch(err => logger.trace("Couldn't remove publish state file: ", err)); + .catch((err: unknown) => + logger.trace("Couldn't remove publish state file: ", err), + ); logger.success(`Version ${newVersion} has been published!`); } else { const msg = [ diff --git a/src/config.ts b/src/config.ts index 9e370d85..723fd05b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -268,7 +268,7 @@ export function getVersioningPolicy(): VersioningPolicy { // Use explicitly configured policy if available if (config.versioning?.policy) { - return config.versioning.policy; + return config.versioning.policy as VersioningPolicy; } // Default based on minVersion @@ -332,7 +332,7 @@ export async function getGlobalGitHubConfig( export function getGitTagPrefix(): string { const targets = getConfiguration().targets || []; const githubTarget = targets.find(target => target.name === 'github'); - return githubTarget?.tagPrefix || ''; + return (githubTarget?.tagPrefix as string | undefined) || ''; } /** @@ -438,7 +438,7 @@ export function getChangelogConfig(): NormalizedChangelogConfig { logger.warn( 'The "changelogPolicy" option is deprecated. Please use "changelog.policy" instead.', ); - policy = config.changelogPolicy; + policy = config.changelogPolicy as ChangelogPolicy; } // Handle changelog config diff --git a/src/schemas/project_config.ts b/src/schemas/project_config.ts index a1161aab..d689949f 100644 --- a/src/schemas/project_config.ts +++ b/src/schemas/project_config.ts @@ -62,6 +62,20 @@ export const TargetConfigSchema = z export type TargetConfig = z.infer; +/** + * Utility type for strongly-typed target configurations. + * Combines base TargetConfig fields with target-specific fields. + * + * @example + * interface BrewConfigFields { + * tap?: string; + * template: string; + * } + * const config = this.config as TypedTargetConfig; + */ +export type TypedTargetConfig> = + TargetConfig & T; + /** * Which service should be used for status checks */ diff --git a/src/targets/__tests__/awsLambda.test.ts b/src/targets/__tests__/awsLambda.test.ts index a1d6f1a6..24dba7ee 100644 --- a/src/targets/__tests__/awsLambda.test.ts +++ b/src/targets/__tests__/awsLambda.test.ts @@ -1,10 +1,4 @@ -import { - vi, - type Mock, - type MockInstance, - type Mocked, - type MockedFunction, -} from 'vitest'; +import { vi } from 'vitest'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { ConfigurationError } from '../../utils/errors'; import { AwsLambdaLayerTarget } from '../awsLambdaLayer'; @@ -118,7 +112,7 @@ describe('project config parameters', () => { awsTarget.getArtifactsForRevision = getArtifactsFailingMock.bind(AwsLambdaLayerTarget); await awsTarget.publish('', ''); // Should break the mocked function. - fail('Should not reach here'); + expect.fail('Should not reach here'); } catch (error) { expect(new RegExp(failingTestErrorMsg).test(error.message)).toBe(true); } @@ -250,7 +244,7 @@ describe('publish', () => { // This should proceed to call getArtifactsForRevision for a pre-release try { await awsTarget.publish('1.0.0-alpha.1', 'revision'); - } catch (error) { + } catch { // Expected to fail at a later stage, but getArtifactsForRevision should be called } diff --git a/src/targets/__tests__/commitOnGitRepository.test.ts b/src/targets/__tests__/commitOnGitRepository.test.ts index 7e6cdb92..4902cf65 100644 --- a/src/targets/__tests__/commitOnGitRepository.test.ts +++ b/src/targets/__tests__/commitOnGitRepository.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { pushArchiveToGitRepository } from '../commitOnGitRepository'; import childProcess from 'child_process'; @@ -36,13 +36,13 @@ test('Basic commit-on-git-repository functionality', async () => { expect(mockClone).toHaveBeenCalledWith( 'https://github.com/getsentry/sentry-deno', - expect.any(String) + expect.any(String), ); expect(mockCheckout).toHaveBeenCalledWith('main'); expect(mockRaw).toHaveBeenCalledWith('rm', '-r', '.'); expect(execSyncSpy).toHaveBeenCalledWith( 'tar -zxvf /tmp/my-archive.tgz --strip-components 1', - expect.objectContaining({ cwd: expect.any(String) }) + expect.objectContaining({ cwd: expect.any(String) }), ); expect(mockRaw).toHaveBeenCalledWith('add', '--all'); expect(mockCommit).toHaveBeenCalledWith('release: 1.2.3'); @@ -50,12 +50,12 @@ test('Basic commit-on-git-repository functionality', async () => { expect(mockRaw).toHaveBeenCalledWith( 'push', 'https://github.com/getsentry/sentry-deno', - '--force' + '--force', ); expect(mockRaw).toHaveBeenCalledWith( 'push', 'https://github.com/getsentry/sentry-deno', - '--tags' + '--tags', ); }); @@ -88,19 +88,19 @@ describe('With authentication', () => { expect(mockClone).toHaveBeenCalledWith( 'https://test-token@github.com/getsentry/sentry-deno', - expect.any(String) + expect.any(String), ); expect(mockRaw).toHaveBeenCalledWith( 'push', 'https://test-token@github.com/getsentry/sentry-deno', - '--force' + '--force', ); expect(mockRaw).toHaveBeenCalledWith( 'push', 'https://test-token@github.com/getsentry/sentry-deno', - '--tags' + '--tags', ); }); }); diff --git a/src/targets/__tests__/crates.test.ts b/src/targets/__tests__/crates.test.ts index be2e1669..515acc4c 100644 --- a/src/targets/__tests__/crates.test.ts +++ b/src/targets/__tests__/crates.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { CrateDependency, CratePackage, CratesTarget } from '../crates'; import { NoneArtifactProvider } from '../../artifact_providers/none'; @@ -38,7 +38,7 @@ describe('getPublishOrder', () => { noDevDeps: true, }, new NoneArtifactProvider(), - { owner: 'getsentry', repo: 'craft' } + { owner: 'getsentry', repo: 'craft' }, ); test('sorts crate packages properly', () => { diff --git a/src/targets/__tests__/docker.test.ts b/src/targets/__tests__/docker.test.ts index 2fa52f66..99fc84cc 100644 --- a/src/targets/__tests__/docker.test.ts +++ b/src/targets/__tests__/docker.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect, beforeEach, afterEach, afterAll, type Mock, type Mocked } from 'vitest'; +import { vi, type Mocked } from 'vitest'; import * as fs from 'node:fs'; import * as os from 'node:os'; @@ -13,7 +13,7 @@ import { import { NoneArtifactProvider } from '../../artifact_providers/none'; import * as system from '../../utils/system'; -vi.mock('../../utils/system', async (importOriginal) => { +vi.mock('../../utils/system', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -147,14 +147,14 @@ describe('normalizeImageRef', () => { it('throws ConfigurationError when source is missing', () => { const config = { target: 'getsentry/craft' }; expect(() => normalizeImageRef(config, 'source')).toThrow( - "Docker target requires a 'source' property. Please specify the source image." + "Docker target requires a 'source' property. Please specify the source image.", ); }); it('throws ConfigurationError when target is missing', () => { const config = { source: 'ghcr.io/org/image' }; expect(() => normalizeImageRef(config, 'target')).toThrow( - "Docker target requires a 'target' property. Please specify the target image." + "Docker target requires a 'target' property. Please specify the target image.", ); }); }); @@ -184,7 +184,7 @@ describe('extractRegistry', () => { it('extracts other registries with dots', () => { expect(extractRegistry('registry.example.com/image')).toBe( - 'registry.example.com' + 'registry.example.com', ); }); @@ -201,7 +201,7 @@ describe('extractRegistry', () => { it('extracts registries with ports', () => { expect(extractRegistry('localhost:5000/image')).toBe('localhost:5000'); expect(extractRegistry('myregistry:8080/user/image')).toBe( - 'myregistry:8080' + 'myregistry:8080', ); }); }); @@ -223,7 +223,7 @@ describe('registryToEnvPrefix', () => { it('handles hyphens in registry names', () => { expect(registryToEnvPrefix('my-registry.example.com')).toBe( - 'MY_REGISTRY_EXAMPLE_COM' + 'MY_REGISTRY_EXAMPLE_COM', ); }); @@ -255,7 +255,9 @@ describe('isGoogleCloudRegistry', () => { expect(isGoogleCloudRegistry('us-east4-docker.pkg.dev')).toBe(true); expect(isGoogleCloudRegistry('europe-west1-docker.pkg.dev')).toBe(true); expect(isGoogleCloudRegistry('asia-east1-docker.pkg.dev')).toBe(true); - expect(isGoogleCloudRegistry('australia-southeast1-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('australia-southeast1-docker.pkg.dev')).toBe( + true, + ); }); it('returns false for non-Google registries', () => { @@ -288,7 +290,7 @@ describe('hasGcloudCredentials', () => { it('returns true when GOOGLE_APPLICATION_CREDENTIALS points to existing file', () => { process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/creds.json'; vi.mocked(mockFs.existsSync).mockImplementation( - (p: fs.PathLike) => p === '/path/to/creds.json' + (p: fs.PathLike) => p === '/path/to/creds.json', ); expect(hasGcloudCredentials()).toBe(true); @@ -297,7 +299,7 @@ describe('hasGcloudCredentials', () => { it('returns true when GOOGLE_GHA_CREDS_PATH points to existing file', () => { process.env.GOOGLE_GHA_CREDS_PATH = '/tmp/gha-creds.json'; vi.mocked(mockFs.existsSync).mockImplementation( - (p: fs.PathLike) => p === '/tmp/gha-creds.json' + (p: fs.PathLike) => p === '/tmp/gha-creds.json', ); expect(hasGcloudCredentials()).toBe(true); @@ -306,7 +308,7 @@ describe('hasGcloudCredentials', () => { it('returns true when CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE points to existing file', () => { process.env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE = '/override/creds.json'; vi.mocked(mockFs.existsSync).mockImplementation( - (p: fs.PathLike) => p === '/override/creds.json' + (p: fs.PathLike) => p === '/override/creds.json', ); expect(hasGcloudCredentials()).toBe(true); @@ -315,7 +317,7 @@ describe('hasGcloudCredentials', () => { it('returns true when default ADC file exists', () => { vi.mocked(mockFs.existsSync).mockImplementation( (p: fs.PathLike) => - p === '/home/user/.config/gcloud/application_default_credentials.json' + p === '/home/user/.config/gcloud/application_default_credentials.json', ); expect(hasGcloudCredentials()).toBe(true); @@ -365,11 +367,15 @@ describe('DockerTarget', () => { usernameVar: 'MY_USER', passwordVar: 'MY_PASS', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('custom-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('custom-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'custom-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'custom-pass', + ); }); it('throws if only usernameVar is specified', () => { @@ -384,9 +390,11 @@ describe('DockerTarget', () => { target: 'ghcr.io/org/image', usernameVar: 'MY_USER', }, - new NoneArtifactProvider() - ) - ).toThrow('Both usernameVar and passwordVar must be specified together'); + new NoneArtifactProvider(), + ), + ).toThrow( + 'Both usernameVar and passwordVar must be specified together', + ); }); it('throws if only passwordVar is specified', () => { @@ -401,9 +409,11 @@ describe('DockerTarget', () => { target: 'ghcr.io/org/image', passwordVar: 'MY_PASS', }, - new NoneArtifactProvider() - ) - ).toThrow('Both usernameVar and passwordVar must be specified together'); + new NoneArtifactProvider(), + ), + ).toThrow( + 'Both usernameVar and passwordVar must be specified together', + ); }); it('throws if explicit env vars are not set (no fallback)', () => { @@ -421,10 +431,10 @@ describe('DockerTarget', () => { usernameVar: 'NONEXISTENT_USER', passwordVar: 'NONEXISTENT_PASS', }, - new NoneArtifactProvider() - ) + new NoneArtifactProvider(), + ), ).toThrow( - 'Missing credentials: NONEXISTENT_USER and/or NONEXISTENT_PASS environment variable(s) not set' + 'Missing credentials: NONEXISTENT_USER and/or NONEXISTENT_PASS environment variable(s) not set', ); }); }); @@ -442,11 +452,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('ghcr-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('ghcr-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'ghcr-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'ghcr-pass', + ); }); it('falls back to GHCR defaults (GITHUB_ACTOR/GITHUB_TOKEN) for ghcr.io', () => { @@ -459,11 +473,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('github-actor'); - expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'github-actor', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'github-token', + ); }); it('falls back to GITHUB_API_TOKEN for ghcr.io when GITHUB_TOKEN is not set', () => { @@ -476,11 +494,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('github-actor'); - expect(target.dockerConfig.target.credentials!.password).toBe('github-api-token'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'github-actor', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'github-api-token', + ); }); it('falls back to x-access-token username for ghcr.io when GITHUB_ACTOR is not set', () => { @@ -492,11 +514,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); - expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'x-access-token', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'github-token', + ); }); it('falls back to x-access-token username for ghcr.io when GITHUB_ACTOR is empty', () => { @@ -509,11 +535,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); - expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'x-access-token', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'github-token', + ); }); it('uses x-access-token and GITHUB_API_TOKEN for ghcr.io (app token scenario)', () => { @@ -526,11 +556,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); - expect(target.dockerConfig.target.credentials!.password).toBe('release-bot-token'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'x-access-token', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'release-bot-token', + ); }); it('uses default DOCKER_* env vars for Docker Hub', () => { @@ -543,12 +577,18 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); - expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'dockerhub-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'dockerhub-pass', + ); + expect( + target.dockerConfig.target.credentials!.registry, + ).toBeUndefined(); }); it('treats docker.io as Docker Hub and uses default credentials', () => { @@ -561,12 +601,18 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'docker.io/getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); - expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'dockerhub-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'dockerhub-pass', + ); + expect( + target.dockerConfig.target.credentials!.registry, + ).toBeUndefined(); }); it('treats index.docker.io as Docker Hub and uses default credentials', () => { @@ -579,12 +625,18 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'index.docker.io/getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); - expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'dockerhub-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'dockerhub-pass', + ); + expect( + target.dockerConfig.target.credentials!.registry, + ).toBeUndefined(); }); it('falls back to DOCKER_* when registry-specific vars are not set', () => { @@ -597,11 +649,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'gcr.io/project/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('default-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('default-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'default-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'default-pass', + ); }); it('falls back to DOCKER_* when registry-specific vars are empty strings', () => { @@ -616,11 +672,15 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'gcr.io/project/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('default-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('default-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'default-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'default-pass', + ); }); it('throws when no credentials are available', () => { @@ -632,8 +692,8 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() - ) + new NoneArtifactProvider(), + ), ).toThrow('Cannot perform Docker release: missing credentials'); }); @@ -647,8 +707,8 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'custom.registry.io/project/image', }, - new NoneArtifactProvider() - ) + new NoneArtifactProvider(), + ), ).toThrow('DOCKER_CUSTOM_REGISTRY_IO_USERNAME/PASSWORD'); }); }); @@ -665,12 +725,16 @@ describe('DockerTarget', () => { target: 'us.gcr.io/project/image', registry: 'gcr.io', // Override to share creds across regions }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.target.credentials!.registry).toBe('gcr.io'); - expect(target.dockerConfig.target.credentials!.username).toBe('gcr-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('gcr-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'gcr-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'gcr-pass', + ); }); }); }); @@ -688,18 +752,26 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); // Target should use Docker Hub credentials - expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'dockerhub-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'dockerhub-pass', + ); expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); // Source should use GHCR credentials expect(target.dockerConfig.source.credentials).toBeDefined(); - expect(target.dockerConfig.source.credentials?.username).toBe('ghcr-user'); - expect(target.dockerConfig.source.credentials?.password).toBe('ghcr-pass'); + expect(target.dockerConfig.source.credentials?.username).toBe( + 'ghcr-user', + ); + expect(target.dockerConfig.source.credentials?.password).toBe( + 'ghcr-pass', + ); expect(target.dockerConfig.source.credentials?.registry).toBe('ghcr.io'); }); @@ -713,7 +785,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/source-image', target: 'ghcr.io/org/target-image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.source.credentials).toBeUndefined(); @@ -733,11 +805,15 @@ describe('DockerTarget', () => { sourceUsernameVar: 'MY_SOURCE_USER', sourcePasswordVar: 'MY_SOURCE_PASS', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.source.credentials?.username).toBe('source-user'); - expect(target.dockerConfig.source.credentials?.password).toBe('source-pass'); + expect(target.dockerConfig.source.credentials?.username).toBe( + 'source-user', + ); + expect(target.dockerConfig.source.credentials?.password).toBe( + 'source-pass', + ); }); it('throws if only sourceUsernameVar is specified', () => { @@ -754,8 +830,8 @@ describe('DockerTarget', () => { target: 'getsentry/craft', sourceUsernameVar: 'MY_SOURCE_USER', }, - new NoneArtifactProvider() - ) + new NoneArtifactProvider(), + ), ).toThrow('Both usernameVar and passwordVar must be specified together'); }); @@ -770,7 +846,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/public-image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); // Should not throw, source credentials are optional @@ -790,7 +866,7 @@ describe('DockerTarget', () => { target: 'getsentry/craft', sourceRegistry: 'gcr.io', // Use gcr.io creds for us.gcr.io }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.source.credentials?.registry).toBe('gcr.io'); @@ -812,7 +888,7 @@ describe('DockerTarget', () => { image: 'ghcr.io/org/target-image', }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.target.image).toBe('ghcr.io/org/target-image'); @@ -831,7 +907,7 @@ describe('DockerTarget', () => { }, target: 'ghcr.io/org/target-image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.source.image).toBe('ghcr.io/org/source-image'); @@ -853,7 +929,7 @@ describe('DockerTarget', () => { image: 'getsentry/craft', }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.source.image).toBe('ghcr.io/org/source-image'); @@ -873,7 +949,7 @@ describe('DockerTarget', () => { registry: 'gcr.io', // Override to share creds across regions }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.target.credentials!.registry).toBe('gcr.io'); @@ -894,11 +970,15 @@ describe('DockerTarget', () => { passwordVar: 'MY_TARGET_PASS', }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.target.credentials!.username).toBe('target-user'); - expect(target.dockerConfig.target.credentials!.password).toBe('target-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'target-user', + ); + expect(target.dockerConfig.target.credentials!.password).toBe( + 'target-pass', + ); }); it('uses format from object config', () => { @@ -917,14 +997,14 @@ describe('DockerTarget', () => { format: '{{{target}}}:v{{{version}}}', }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.source.format).toBe( - '{{{source}}}:sha-{{{revision}}}' + '{{{source}}}:sha-{{{revision}}}', ); expect(target.dockerConfig.target.format).toBe( - '{{{target}}}:v{{{version}}}' + '{{{target}}}:v{{{version}}}', ); }); @@ -944,12 +1024,18 @@ describe('DockerTarget', () => { }, target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); - expect(target.dockerConfig.source.credentials?.username).toBe('source-user'); - expect(target.dockerConfig.source.credentials?.password).toBe('source-pass'); - expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + expect(target.dockerConfig.source.credentials?.username).toBe( + 'source-user', + ); + expect(target.dockerConfig.source.credentials?.password).toBe( + 'source-pass', + ); + expect(target.dockerConfig.target.credentials!.username).toBe( + 'dockerhub-user', + ); }); }); @@ -964,7 +1050,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'ghcr.io/org/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -973,7 +1059,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=user', '--password-stdin', 'ghcr.io'], {}, - { stdin: 'pass' } + { stdin: 'pass' }, ); }); @@ -987,7 +1073,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -996,7 +1082,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=user', '--password-stdin'], {}, - { stdin: 'pass' } + { stdin: 'pass' }, ); }); @@ -1010,7 +1096,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1034,7 +1120,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1048,7 +1134,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=ghcr-user', '--password-stdin', 'ghcr.io'], {}, - { stdin: 'ghcr-pass' } + { stdin: 'ghcr-pass' }, ); // Second call: login to target (Docker Hub) @@ -1057,7 +1143,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=dockerhub-user', '--password-stdin'], {}, - { stdin: 'dockerhub-pass' } + { stdin: 'dockerhub-pass' }, ); }); @@ -1072,7 +1158,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/public-image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1083,7 +1169,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=dockerhub-user', '--password-stdin'], {}, - { stdin: 'dockerhub-pass' } + { stdin: 'dockerhub-pass' }, ); }); @@ -1097,7 +1183,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/source-image', target: 'ghcr.io/org/target-image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1108,7 +1194,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=ghcr-user', '--password-stdin', 'ghcr.io'], {}, - { stdin: 'ghcr-pass' } + { stdin: 'ghcr-pass' }, ); }); @@ -1124,7 +1210,7 @@ describe('DockerTarget', () => { skipLogin: true, // Auth handled externally (e.g., gcloud workload identity) }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1146,7 +1232,7 @@ describe('DockerTarget', () => { }, target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1157,7 +1243,7 @@ describe('DockerTarget', () => { 'docker', ['login', '--username=dockerhub-user', '--password-stdin'], {}, - { stdin: 'dockerhub-pass' } + { stdin: 'dockerhub-pass' }, ); }); @@ -1174,7 +1260,7 @@ describe('DockerTarget', () => { skipLogin: true, }, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1194,7 +1280,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'gcr.io/project/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1204,7 +1290,7 @@ describe('DockerTarget', () => { 'gcloud', ['auth', 'configure-docker', 'gcr.io', '--quiet'], {}, - {} + {}, ); }); @@ -1218,7 +1304,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'us-docker.pkg.dev/project/repo/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1228,7 +1314,7 @@ describe('DockerTarget', () => { 'gcloud', ['auth', 'configure-docker', 'us-docker.pkg.dev', '--quiet'], {}, - {} + {}, ); }); @@ -1242,7 +1328,7 @@ describe('DockerTarget', () => { source: 'us.gcr.io/project/source', target: 'eu.gcr.io/project/target', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1252,7 +1338,7 @@ describe('DockerTarget', () => { 'gcloud', ['auth', 'configure-docker', 'us.gcr.io,eu.gcr.io', '--quiet'], {}, - {} + {}, ); }); @@ -1270,7 +1356,7 @@ describe('DockerTarget', () => { source: 'gcr.io/project/image', target: 'getsentry/craft', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); await target.login(); @@ -1280,13 +1366,13 @@ describe('DockerTarget', () => { 'gcloud', expect.any(Array), expect.any(Object), - expect.any(Object) + expect.any(Object), ); expect(system.spawnProcess).toHaveBeenCalledWith( 'docker', expect.arrayContaining(['login']), {}, - expect.any(Object) + expect.any(Object), ); }); @@ -1299,7 +1385,7 @@ describe('DockerTarget', () => { source: 'ghcr.io/org/image', target: 'gcr.io/project/image', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); expect(target.dockerConfig.target.credentials).toBeUndefined(); diff --git a/src/targets/__tests__/github.test.ts b/src/targets/__tests__/github.test.ts index a5096020..9e177d97 100644 --- a/src/targets/__tests__/github.test.ts +++ b/src/targets/__tests__/github.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { isLatestRelease, GitHubTarget } from '../github'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { setGlobals } from '../../utils/helpers'; @@ -80,7 +80,7 @@ describe('GitHubTarget', () => { githubTarget = new GitHubTarget( { name: 'github' }, new NoneArtifactProvider(), - { owner: 'testOwner', repo: 'testRepo' } + { owner: 'testOwner', repo: 'testRepo' }, ); }); @@ -106,16 +106,16 @@ describe('GitHubTarget', () => { githubTarget.publishRelease = vi.fn().mockResolvedValue(undefined); githubTarget.github.repos.getLatestRelease = vi.fn().mockRejectedValue({ status: 404, - }); + }) as any; }); it('cleans up draft release when publishRelease fails', async () => { const publishError = new Error('Publish failed'); githubTarget.publishRelease = vi.fn().mockRejectedValue(publishError); - await expect( - githubTarget.publish('1.0.0', 'abc123') - ).rejects.toThrow('Publish failed'); + await expect(githubTarget.publish('1.0.0', 'abc123')).rejects.toThrow( + 'Publish failed', + ); expect(githubTarget.deleteRelease).toHaveBeenCalledWith(mockDraftRelease); }); @@ -127,9 +127,9 @@ describe('GitHubTarget', () => { githubTarget.publishRelease = vi.fn().mockRejectedValue(publishError); githubTarget.deleteRelease = vi.fn().mockRejectedValue(deleteError); - await expect( - githubTarget.publish('1.0.0', 'abc123') - ).rejects.toThrow('Publish failed'); + await expect(githubTarget.publish('1.0.0', 'abc123')).rejects.toThrow( + 'Publish failed', + ); expect(githubTarget.deleteRelease).toHaveBeenCalledWith(mockDraftRelease); }); @@ -151,7 +151,7 @@ describe('GitHubTarget', () => { }; const deleteReleaseSpy = vi.fn().mockResolvedValue({ status: 204 }); - githubTarget.github.repos.deleteRelease = deleteReleaseSpy; + githubTarget.github.repos.deleteRelease = deleteReleaseSpy as any; const result = await githubTarget.deleteRelease(draftRelease); @@ -177,7 +177,7 @@ describe('GitHubTarget', () => { }; const deleteReleaseSpy = vi.fn().mockResolvedValue({ status: 204 }); - githubTarget.github.repos.deleteRelease = deleteReleaseSpy; + githubTarget.github.repos.deleteRelease = deleteReleaseSpy as any; const result = await githubTarget.deleteRelease(publishedRelease); @@ -193,7 +193,7 @@ describe('GitHubTarget', () => { }; const deleteReleaseSpy = vi.fn().mockResolvedValue({ status: 204 }); - githubTarget.github.repos.deleteRelease = deleteReleaseSpy; + githubTarget.github.repos.deleteRelease = deleteReleaseSpy as any; const result = await githubTarget.deleteRelease(releaseWithoutDraftFlag); @@ -212,7 +212,7 @@ describe('GitHubTarget', () => { }; const deleteReleaseSpy = vi.fn().mockResolvedValue({ status: 204 }); - githubTarget.github.repos.deleteRelease = deleteReleaseSpy; + githubTarget.github.repos.deleteRelease = deleteReleaseSpy as any; const result = await githubTarget.deleteRelease(draftRelease); diff --git a/src/targets/__tests__/index.test.ts b/src/targets/__tests__/index.test.ts index ab0096ee..131fb67d 100644 --- a/src/targets/__tests__/index.test.ts +++ b/src/targets/__tests__/index.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { getAllTargetNames, getTargetByName } from '..'; import { GitHubTarget } from '../github'; diff --git a/src/targets/__tests__/maven.test.ts b/src/targets/__tests__/maven.test.ts index 48b19804..8ba15cb7 100644 --- a/src/targets/__tests__/maven.test.ts +++ b/src/targets/__tests__/maven.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type MockedFunction } from 'vitest'; import { URL } from 'url'; import nock from 'nock'; import { NoneArtifactProvider } from '../../artifact_providers/none'; @@ -19,7 +19,7 @@ import * as fs from 'fs'; vi.mock('../../utils/files'); vi.mock('../../utils/gpg'); -vi.mock('fs', async (importOriginal) => { +vi.mock('fs', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -33,7 +33,7 @@ vi.mock('fs', async (importOriginal) => { }; }); -vi.mock('../../utils/system', async (importOriginal) => { +vi.mock('../../utils/system', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -42,7 +42,7 @@ vi.mock('../../utils/system', async (importOriginal) => { }; }); -vi.mock('../../utils/async', async (importOriginal) => { +vi.mock('../../utils/async', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -50,7 +50,7 @@ vi.mock('../../utils/async', async (importOriginal) => { sleep: vi.fn(() => setTimeout(() => { Promise.resolve(); - }, 10) + }, 10), ), }; }); @@ -106,7 +106,7 @@ function getRequiredTargetConfig(): any { } function createMavenTarget( - targetConfig?: Record + targetConfig?: Record, ): MavenTarget { const finalConfig = targetConfig ? targetConfig : getRequiredTargetConfig(); const mergedConfig = { @@ -137,13 +137,13 @@ describe('Maven target configuration', () => { test('no env vars and no options', () => { removeTargetSecretsFromEnv(); expect(createMavenTarget).toThrowErrorMatchingInlineSnapshot( - `[Error: Required value(s) GPG_PASSPHRASE not found in configuration files or the environment. See the documentation for more details.]` + `[Error: Required value(s) GPG_PASSPHRASE not found in configuration files or the environment. See the documentation for more details.]`, ); }); test('env vars without options', () => { expect(() => createMavenTarget({})).toThrowErrorMatchingInlineSnapshot( - `[Error: Required configuration mavenCliPath not found in configuration file. See the documentation for more details.]` + `[Error: Required configuration mavenCliPath not found in configuration file. See the documentation for more details.]`, ); }); @@ -151,7 +151,7 @@ describe('Maven target configuration', () => { const config = getRequiredTargetConfig(); delete config.android; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required Android configuration was not found in the configuration file. See the documentation for more details]` + `[Error: Required Android configuration was not found in the configuration file. See the documentation for more details]`, ); }); @@ -165,7 +165,7 @@ describe('Maven target configuration', () => { const config = getRequiredTargetConfig(); config.android = 'yes'; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required Android configuration is incorrect. See the documentation for more details.]` + `[Error: Required Android configuration is incorrect. See the documentation for more details.]`, ); }); @@ -173,7 +173,7 @@ describe('Maven target configuration', () => { const config = getRequiredTargetConfig(); config.kmp = 'yes'; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]` + `[Error: Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]`, ); }); @@ -193,7 +193,7 @@ describe('Maven target configuration', () => { const config = getFullTargetConfig(); delete config.android.distDirRegex; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required Android configuration is incorrect. See the documentation for more details.]` + `[Error: Required Android configuration is incorrect. See the documentation for more details.]`, ); }); @@ -202,7 +202,7 @@ describe('Maven target configuration', () => { delete config.kmp.rootDistDirRegex; config.kmp.anotherParam = 'unused'; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]` + `[Error: Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]`, ); }); @@ -211,7 +211,7 @@ describe('Maven target configuration', () => { delete config.kmp.appleDistDirRegex; config.kmp.anotherParam = 'unused'; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required apple configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]` + `[Error: Required apple configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.]`, ); }); @@ -220,7 +220,7 @@ describe('Maven target configuration', () => { delete config.android.distDirRegex; config.android.anotherParam = 'unused'; expect(() => createMavenTarget(config)).toThrowErrorMatchingInlineSnapshot( - `[Error: Required Android configuration is incorrect. See the documentation for more details.]` + `[Error: Required Android configuration is incorrect. See the documentation for more details.]`, ); }); @@ -248,8 +248,8 @@ describe('Maven target configuration', () => { expect(mvnTarget.config).toEqual( expect.objectContaining({ [secret]: DEFAULT_OPTION_VALUE, - }) - ) + }), + ), ); }); @@ -260,15 +260,27 @@ describe('Maven target configuration', () => { expect(mvnTarget.config).toEqual( expect.objectContaining({ [secret]: DEFAULT_OPTION_VALUE, - }) - ) + }), + ), + ); + expect(typeof (mvnTarget.config.android as any).distDirRegex).toBe( + 'string', + ); + expect(typeof (mvnTarget.config.android as any).fileReplaceeRegex).toBe( + 'string', + ); + expect(typeof (mvnTarget.config.android as any).fileReplacerStr).toBe( + 'string', + ); + expect(typeof (mvnTarget.config.kmp as any).rootDistDirRegex).toBe( + 'string', + ); + expect(typeof (mvnTarget.config.kmp as any).appleDistDirRegex).toBe( + 'string', + ); + expect(typeof (mvnTarget.config.kmp as any).klibDistDirRegex).toBe( + 'string', ); - expect(typeof mvnTarget.config.android.distDirRegex).toBe('string'); - expect(typeof mvnTarget.config.android.fileReplaceeRegex).toBe('string'); - expect(typeof mvnTarget.config.android.fileReplacerStr).toBe('string'); - expect(typeof mvnTarget.config.kmp.rootDistDirRegex).toBe('string'); - expect(typeof mvnTarget.config.kmp.appleDistDirRegex).toBe('string'); - expect(typeof mvnTarget.config.kmp.klibDistDirRegex).toBe('string'); }); test('import GPG private key if one is present in the environment', async () => { @@ -278,7 +290,7 @@ describe('Maven target configuration', () => { const mvnTarget = createMavenTarget(getFullTargetConfig()); mvnTarget.upload = vi.fn(async () => void callOrder.push('upload')); mvnTarget.closeAndReleaseRepository = vi.fn( - async () => void callOrder.push('closeAndReleaseRepository') + async () => void callOrder.push('closeAndReleaseRepository'), ); await mvnTarget.publish('1.0.0', 'r3v1s10n'); expect(importGPGKey).toHaveBeenCalledWith(DEFAULT_OPTION_VALUE); @@ -291,7 +303,7 @@ describe('publish', () => { const mvnTarget = createMavenTarget(); mvnTarget.upload = vi.fn(async () => void callOrder.push('upload')); mvnTarget.closeAndReleaseRepository = vi.fn( - async () => void callOrder.push('closeAndReleaseRepository') + async () => void callOrder.push('closeAndReleaseRepository'), ); const revision = 'r3v1s10n'; await mvnTarget.publish('1.0.0', revision); @@ -309,7 +321,7 @@ describe('transform KMP artifacts', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(getFullTargetConfig()); @@ -321,13 +333,10 @@ describe('transform KMP artifacts', () => { metadataFile: ``, moduleFile: `${tmpDirName}.module`, }; - const { - sideArtifacts, - classifiers, - types, - } = mvnTarget.transformKmpSideArtifacts(false, false, true, files); + const { sideArtifacts, classifiers, types } = + mvnTarget.transformKmpSideArtifacts(false, false, true, files); expect(sideArtifacts).toEqual( - `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.moduleFile}` + `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.moduleFile}`, ); expect(classifiers).toEqual('javadoc,sources,,'); expect(types).toEqual('jar,jar,klib,module'); @@ -337,7 +346,7 @@ describe('transform KMP artifacts', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(getFullTargetConfig()); @@ -352,16 +361,13 @@ describe('transform KMP artifacts', () => { metadataFile: `${tmpDirName}-metadata.jar`, moduleFile: `${tmpDirName}.module`, }; - const { - sideArtifacts, - classifiers, - types, - } = mvnTarget.transformKmpSideArtifacts(false, true, false, files); + const { sideArtifacts, classifiers, types } = + mvnTarget.transformKmpSideArtifacts(false, true, false, files); expect(sideArtifacts).toEqual( - `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.metadataFile},${files.moduleFile}` + `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.metadataFile},${files.moduleFile}`, ); expect(classifiers).toEqual( - 'javadoc,sources,cinterop-Sentry.NSException,cinterop-Sentry,metadata,' + 'javadoc,sources,cinterop-Sentry.NSException,cinterop-Sentry,metadata,', ); expect(types).toEqual('jar,jar,klib,klib,jar,module'); }); @@ -370,7 +376,7 @@ describe('transform KMP artifacts', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(getFullTargetConfig()); @@ -383,13 +389,10 @@ describe('transform KMP artifacts', () => { moduleFile: `${tmpDirName}.module`, kotlinToolingMetadataFile: `${tmpDirName}-kotlin-tooling-metadata.json`, }; - const { - sideArtifacts, - classifiers, - types, - } = mvnTarget.transformKmpSideArtifacts(true, false, false, files); + const { sideArtifacts, classifiers, types } = + mvnTarget.transformKmpSideArtifacts(true, false, false, files); expect(sideArtifacts).toEqual( - `${files.javadocFile},${files.sourcesFile},${files.allFile},${files.kotlinToolingMetadataFile},${files.moduleFile}` + `${files.javadocFile},${files.sourcesFile},${files.allFile},${files.kotlinToolingMetadataFile},${files.moduleFile}`, ); expect(classifiers).toEqual('javadoc,sources,all,kotlin-tooling-metadata,'); expect(types).toEqual('jar,jar,jar,json,module'); @@ -405,7 +408,7 @@ describe('upload', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(); @@ -423,9 +426,9 @@ describe('upload', () => { await mvnTarget.upload('r3v1s10n'); expect(retrySpawnProcess).toHaveBeenCalledTimes(1); - const callArgs = (retrySpawnProcess as MockedFunction< - typeof retrySpawnProcess - >).mock.calls[0]; + const callArgs = ( + retrySpawnProcess as MockedFunction + ).mock.calls[0]; expect(callArgs).toHaveLength(2); expect(callArgs[0]).toEqual(DEFAULT_OPTION_VALUE); @@ -436,13 +439,13 @@ describe('upload', () => { expect(cmdArgs[1]).toMatch(new RegExp(`-Dfile=${tmpDirName}.+`)); expect(cmdArgs[2]).toMatch( new RegExp( - `-Dfiles=${tmpDirName}.+-javadoc\\.jar,${tmpDirName}.+-sources\\.jar` - ) + `-Dfiles=${tmpDirName}.+-javadoc\\.jar,${tmpDirName}.+-sources\\.jar`, + ), ); expect(cmdArgs[3]).toBe(`-Dclassifiers=javadoc,sources`); expect(cmdArgs[4]).toBe(`-Dtypes=jar,jar`); expect(cmdArgs[5]).toMatch( - new RegExp(`-DpomFile=${tmpDirName}.+pom-default\\.xml`) + new RegExp(`-DpomFile=${tmpDirName}.+pom-default\\.xml`), ); expect(cmdArgs[6]).toBe(`-DrepositoryId=${DEFAULT_OPTION_VALUE}`); expect(cmdArgs[7]).toBe(`-Durl=${DEFAULT_OPTION_VALUE}`); @@ -457,7 +460,7 @@ describe('upload', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(); @@ -476,9 +479,9 @@ describe('upload', () => { await mvnTarget.upload('r3v1s10n'); expect(retrySpawnProcess).toHaveBeenCalledTimes(1); - const callArgs = (retrySpawnProcess as MockedFunction< - typeof retrySpawnProcess - >).mock.calls[0]; + const callArgs = ( + retrySpawnProcess as MockedFunction + ).mock.calls[0]; expect(callArgs).toHaveLength(2); expect(callArgs[0]).toEqual(DEFAULT_OPTION_VALUE); @@ -489,13 +492,13 @@ describe('upload', () => { expect(cmdArgs[1]).toMatch(new RegExp(`-Dfile=${tmpDirName}.+`)); expect(cmdArgs[2]).toMatch( new RegExp( - `-Dfiles=${tmpDirName}.+-javadoc\\.jar,${tmpDirName}.+-sources\\.jar,${tmpDirName}.+\\.module` - ) + `-Dfiles=${tmpDirName}.+-javadoc\\.jar,${tmpDirName}.+-sources\\.jar,${tmpDirName}.+\\.module`, + ), ); expect(cmdArgs[3]).toBe(`-Dclassifiers=javadoc,sources,`); expect(cmdArgs[4]).toBe(`-Dtypes=jar,jar,module`); expect(cmdArgs[5]).toMatch( - new RegExp(`-DpomFile=${tmpDirName}.+pom-default\\.xml`) + new RegExp(`-DpomFile=${tmpDirName}.+pom-default\\.xml`), ); expect(cmdArgs[6]).toBe(`-DrepositoryId=${DEFAULT_OPTION_VALUE}`); expect(cmdArgs[7]).toBe(`-Durl=${DEFAULT_OPTION_VALUE}`); @@ -510,7 +513,7 @@ describe('upload', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(); @@ -526,9 +529,9 @@ describe('upload', () => { await mvnTarget.upload('r3v1s10n'); expect(retrySpawnProcess).toHaveBeenCalledTimes(1); - const callArgs = (retrySpawnProcess as MockedFunction< - typeof retrySpawnProcess - >).mock.calls[0]; + const callArgs = ( + retrySpawnProcess as MockedFunction + ).mock.calls[0]; expect(callArgs).toHaveLength(2); expect(callArgs[0]).toEqual(DEFAULT_OPTION_VALUE); @@ -537,10 +540,10 @@ describe('upload', () => { expect(cmdArgs).toHaveLength(8); expect(cmdArgs[0]).toBe('gpg:sign-and-deploy-file'); expect(cmdArgs[1]).toMatch( - new RegExp(`-Dfile=${tmpDirName}.+${POM_DEFAULT_FILENAME}`) + new RegExp(`-Dfile=${tmpDirName}.+${POM_DEFAULT_FILENAME}`), ); expect(cmdArgs[2]).toMatch( - new RegExp(`-DpomFile=${tmpDirName}.*${POM_DEFAULT_FILENAME}`) + new RegExp(`-DpomFile=${tmpDirName}.*${POM_DEFAULT_FILENAME}`), ); expect(cmdArgs[3]).toBe(`-DrepositoryId=${DEFAULT_OPTION_VALUE}`); expect(cmdArgs[4]).toBe(`-Durl=${DEFAULT_OPTION_VALUE}`); @@ -553,7 +556,7 @@ describe('upload', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); const mvnTarget = createMavenTarget(); @@ -580,7 +583,7 @@ describe('upload', () => { (withTempDir as MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); - } + }, ); // Override fs.promises.readdir for this test to return klib files @@ -615,9 +618,9 @@ describe('upload', () => { await mvnTarget.upload('r3v1s10n'); expect(retrySpawnProcess).toHaveBeenCalledTimes(1); - const callArgs = (retrySpawnProcess as MockedFunction< - typeof retrySpawnProcess - >).mock.calls[0]; + const callArgs = ( + retrySpawnProcess as MockedFunction + ).mock.calls[0]; expect(callArgs).toHaveLength(2); expect(callArgs[0]).toEqual(DEFAULT_OPTION_VALUE); @@ -626,15 +629,15 @@ describe('upload', () => { expect(cmdArgs).toHaveLength(11); expect(cmdArgs[0]).toBe('gpg:sign-and-deploy-file'); expect(cmdArgs[1]).toMatch( - new RegExp(`-Dfile=${klibDistDir}/${klibDistDirName}`) + new RegExp(`-Dfile=${klibDistDir}/${klibDistDirName}`), ); expect(cmdArgs[2]).toBe( - `-Dfiles=${klibDistDir}/${klibDistDirName}-javadoc.jar,${klibDistDir}/${klibDistDirName}-sources.jar,${klibDistDir}/${klibDistDirName}.klib,${klibDistDir}/${klibDistDirName}.module` + `-Dfiles=${klibDistDir}/${klibDistDirName}-javadoc.jar,${klibDistDir}/${klibDistDirName}-sources.jar,${klibDistDir}/${klibDistDirName}.klib,${klibDistDir}/${klibDistDirName}.module`, ); expect(cmdArgs[3]).toBe(`-Dclassifiers=javadoc,sources,,`); expect(cmdArgs[4]).toBe(`-Dtypes=jar,jar,klib,module`); expect(cmdArgs[5]).toMatch( - new RegExp(`-DpomFile=${klibDistDir}/pom-default\\.xml`) + new RegExp(`-DpomFile=${klibDistDir}/pom-default\\.xml`), ); expect(cmdArgs[6]).toBe(`-DrepositoryId=${DEFAULT_OPTION_VALUE}`); expect(cmdArgs[7]).toBe(`-Durl=${DEFAULT_OPTION_VALUE}`); @@ -651,7 +654,7 @@ describe('closeAndReleaseRepository', () => { test('should throw if repository is not opened', async () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); await expect(mvnTarget.closeAndReleaseRepository()).rejects.toThrow(); }); @@ -659,7 +662,7 @@ describe('closeAndReleaseRepository', () => { test('should call closeRepository and releaseRepository with fetched repository ID', async () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('open')) + Promise.resolve(getRepositoryInfo('open')), ); const callOrder: string[] = []; mvnTarget.closeRepository = vi.fn(async () => { @@ -681,14 +684,14 @@ describe('closeAndReleaseRepository', () => { test('should not release repostiory if it was not closed properly', async () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('open')) + Promise.resolve(getRepositoryInfo('open')), ); mvnTarget.closeRepository = vi.fn(() => Promise.reject()); mvnTarget.releaseRepository = vi.fn(() => Promise.resolve(true)); try { await mvnTarget.closeAndReleaseRepository(); - } catch (e) { + } catch { // We only use `closeAndReleaseRepository` to trigger the expected behavior // however, the assertion itself is done on `closeRepository` and `releaseRepository` } @@ -713,7 +716,7 @@ describe('getRepository', () => { const mvnTarget = createMavenTarget(); await expect(mvnTarget.getRepository()).resolves.toStrictEqual( - repositoryInfo + repositoryInfo, ); }); @@ -724,7 +727,7 @@ describe('getRepository', () => { const mvnTarget = createMavenTarget(); await expect(mvnTarget.getRepository()).rejects.toThrow( - new Error('No available repositories. Nothing to publish.') + new Error('No available repositories. Nothing to publish.'), ); }); @@ -738,8 +741,8 @@ describe('getRepository', () => { const mvnTarget = createMavenTarget(); await expect(mvnTarget.getRepository()).rejects.toThrow( new Error( - 'There are more than 1 active repositories. Please close unwanted deployments.' - ) + 'There are more than 1 active repositories. Please close unwanted deployments.', + ), ); }); @@ -748,7 +751,7 @@ describe('getRepository', () => { const mvnTarget = createMavenTarget(); await expect(mvnTarget.getRepository()).rejects.toThrow( - new Error('Unable to fetch repositories: 500, Internal Server Error') + new Error('Unable to fetch repositories: 500, Internal Server Error'), ); }); }); @@ -770,7 +773,7 @@ describe('closeRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); await expect(mvnTarget.closeRepository(repositoryId)).resolves.toBe(true); @@ -790,8 +793,8 @@ describe('closeRepository', () => { const mvnTarget = createMavenTarget(); await expect(mvnTarget.closeRepository(repositoryId)).rejects.toThrow( new Error( - 'Unable to close repository sentry-java: 500, Internal Server Error' - ) + 'Unable to close repository sentry-java: 500, Internal Server Error', + ), ); }); @@ -812,7 +815,7 @@ describe('closeRepository', () => { .mockImplementationOnce(() => Promise.resolve(getRepositoryInfo('open'))) .mockImplementationOnce(() => Promise.resolve(getRepositoryInfo('open'))) .mockImplementationOnce(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); await expect(mvnTarget.closeRepository(repositoryId)).resolves.toBe(true); @@ -833,19 +836,18 @@ describe('closeRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('open')) + Promise.resolve(getRepositoryInfo('open')), ); // Deadline is 2h, so we fake pooling start time and initial read to 1min // and second iteration to something over 2h - vi - .spyOn(Date, 'now') + vi.spyOn(Date, 'now') .mockImplementationOnce(() => 1 * 60 * 1000) .mockImplementationOnce(() => 1 * 60 * 1000) .mockImplementationOnce(() => 122 * 60 * 1000); await expect(mvnTarget.closeRepository(repositoryId)).rejects.toThrow( - new Error('Deadline for Nexus repository status change reached.') + new Error('Deadline for Nexus repository status change reached.'), ); expect(sleep).toHaveBeenCalledTimes(1); expect(mvnTarget.getRepository).toHaveBeenCalledTimes(1); @@ -877,7 +879,7 @@ describe('releaseRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); await expect(mvnTarget.releaseRepository(repositoryId)).resolves.toBe(true); }); @@ -895,12 +897,12 @@ describe('releaseRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); await expect(mvnTarget.releaseRepository(repositoryId)).rejects.toThrow( new Error( - 'Unable to release repository sentry-java: 500, Internal Server Error' - ) + 'Unable to release repository sentry-java: 500, Internal Server Error', + ), ); }); @@ -917,7 +919,7 @@ describe('releaseRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); nock(centralUrl.origin) @@ -950,7 +952,7 @@ describe('releaseRepository', () => { const mvnTarget = createMavenTarget(); mvnTarget.getRepository = vi.fn(() => - Promise.resolve(getRepositoryInfo('closed')) + Promise.resolve(getRepositoryInfo('closed')), ); nock(centralUrl.origin) @@ -961,14 +963,13 @@ describe('releaseRepository', () => { // Deadline is 2h, so we fake pooling start time and initial read to 1min // and second iteration to something over 2h - vi - .spyOn(Date, 'now') + vi.spyOn(Date, 'now') .mockImplementationOnce(() => 1 * 60 * 1000) .mockImplementationOnce(() => 1 * 60 * 1000) .mockImplementationOnce(() => 122 * 60 * 1000); await expect(mvnTarget.releaseRepository(repositoryId)).rejects.toThrow( - new Error('Deadline for Central repository status change reached.') + new Error('Deadline for Central repository status change reached.'), ); expect(sleep).toHaveBeenCalledTimes(1); expect(mvnTarget.getRepository).toHaveBeenCalledTimes(1); diff --git a/src/targets/__tests__/mavenDiskIo.test.ts b/src/targets/__tests__/mavenDiskIo.test.ts index 20d58421..0b1200da 100644 --- a/src/targets/__tests__/mavenDiskIo.test.ts +++ b/src/targets/__tests__/mavenDiskIo.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { MavenTarget } from '../maven'; import { withTempDir } from '../../utils/files'; @@ -25,7 +25,7 @@ function getRequiredTargetConfig(): any { } function createMavenTarget( - targetConfig?: Record + targetConfig?: Record, ): MavenTarget { process.env.GPG_PRIVATE_KEY = DEFAULT_OPTION_VALUE; process.env.GPG_PASSPHRASE = DEFAULT_OPTION_VALUE; @@ -42,71 +42,63 @@ function createMavenTarget( describe('maven disk io', () => { test('fileExists', async () => { - await withTempDir( - async (tmpDir): Promise => { - const target = createMavenTarget(); + await withTempDir(async (tmpDir): Promise => { + const target = createMavenTarget(); - expect(await target.fileExists('a/random/path')).toBe(false); + expect(await target.fileExists('a/random/path')).toBe(false); - // a folder should return false - expect(await target.fileExists(tmpDir)).toBe(false); + // a folder should return false + expect(await target.fileExists(tmpDir)).toBe(false); - const file = join(tmpDir, 'module.json'); + const file = join(tmpDir, 'module.json'); - // when the file doesn't exist it should return false - expect(await target.fileExists(file)).toBe(false); - await fsPromises.writeFile(file, 'abc'); + // when the file doesn't exist it should return false + expect(await target.fileExists(file)).toBe(false); + await fsPromises.writeFile(file, 'abc'); - // once the file is written, it should exist - expect(await target.fileExists(file)).toBe(true); - } - ); + // once the file is written, it should exist + expect(await target.fileExists(file)).toBe(true); + }); }); test('fixModuleFileName', async () => { - await withTempDir( - async (tmpDir): Promise => { - const target = createMavenTarget(); - - const file = join(tmpDir, 'module.json'); - await fsPromises.writeFile(file, 'abc'); - - const moduleFile = join(tmpDir, 'sentry-java-1.0.0.module'); - // when fix module is called with proper file names - await target.fixModuleFileName(tmpDir, moduleFile); - - // it should rename the file - expect(await target.fileExists(file)).toBe(false); - expect(await target.fileExists(moduleFile)).toBe(true); - } - ); + await withTempDir(async (tmpDir): Promise => { + const target = createMavenTarget(); + + const file = join(tmpDir, 'module.json'); + await fsPromises.writeFile(file, 'abc'); + + const moduleFile = join(tmpDir, 'sentry-java-1.0.0.module'); + // when fix module is called with proper file names + await target.fixModuleFileName(tmpDir, moduleFile); + + // it should rename the file + expect(await target.fileExists(file)).toBe(false); + expect(await target.fileExists(moduleFile)).toBe(true); + }); }); test('fixModuleFileName no-op', async () => { - await withTempDir( - async (tmpDir): Promise => { - const target = createMavenTarget(); + await withTempDir(async (tmpDir): Promise => { + const target = createMavenTarget(); - const file = join(tmpDir, 'sentry-java-1.0.0.module'); - await fsPromises.writeFile(file, 'abc'); + const file = join(tmpDir, 'sentry-java-1.0.0.module'); + await fsPromises.writeFile(file, 'abc'); - // when fix module is called, but the proper file already exists - await target.fixModuleFileName(tmpDir, file); + // when fix module is called, but the proper file already exists + await target.fixModuleFileName(tmpDir, file); - // it should still exist after calling fixModuleFileName - expect(await target.fileExists(file)).toBe(true); - } - ); + // it should still exist after calling fixModuleFileName + expect(await target.fileExists(file)).toBe(true); + }); }); test('fixModuleFileName non-existant-files', async () => { - await withTempDir( - async (tmpDir): Promise => { - const target = createMavenTarget(); - - const file = join(tmpDir, 'sentry-java-1.0.0.module'); - await target.fixModuleFileName(tmpDir, file); - } - ); + await withTempDir(async (tmpDir): Promise => { + const target = createMavenTarget(); + + const file = join(tmpDir, 'sentry-java-1.0.0.module'); + await target.fixModuleFileName(tmpDir, file); + }); }); }); diff --git a/src/targets/__tests__/npm.test.ts b/src/targets/__tests__/npm.test.ts index 2867f0bf..6ff53fc3 100644 --- a/src/targets/__tests__/npm.test.ts +++ b/src/targets/__tests__/npm.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type MockInstance } from 'vitest'; import { getPublishTag, getLatestVersion, @@ -29,14 +29,14 @@ describe('getLatestVersion', () => { it('returns undefined if package name does not exist', async () => { const actual = await getLatestVersion( 'sentry-xx-this-does-not-exist', - defaultNpmConfig + defaultNpmConfig, ); expect(actual).toEqual(undefined); expect(spawnProcessMock).toBeCalledTimes(1); expect(spawnProcessMock).toBeCalledWith( 'npm', ['info', 'sentry-xx-this-does-not-exist', 'version'], - expect.objectContaining({}) + expect.objectContaining({}), ); }); @@ -44,7 +44,7 @@ describe('getLatestVersion', () => { spawnProcessMock = vi .spyOn(system, 'spawnProcess') .mockImplementation(() => - Promise.resolve(Buffer.from('7.20.0\n', 'utf-8')) + Promise.resolve(Buffer.from('7.20.0\n', 'utf-8')), ); const actual = await getLatestVersion('@sentry/browser', defaultNpmConfig); expect(actual).toBe('7.20.0'); @@ -52,7 +52,7 @@ describe('getLatestVersion', () => { expect(spawnProcessMock).toBeCalledWith( 'npm', ['info', '@sentry/browser', 'version'], - expect.objectContaining({}) + expect.objectContaining({}), ); }); }); @@ -78,7 +78,7 @@ describe('getPublishTag', () => { '1.0.0', undefined, defaultNpmConfig, - logger + logger, ); expect(actual).toEqual(undefined); expect(logger.warn).not.toHaveBeenCalled(); @@ -93,12 +93,12 @@ describe('getPublishTag', () => { '1.0.0', 'sentry-xx-does-not-exist', defaultNpmConfig, - logger + logger, ); expect(actual).toEqual(undefined); expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).toHaveBeenCalledWith( - 'Could not fetch current version for package sentry-xx-does-not-exist' + 'Could not fetch current version for package sentry-xx-does-not-exist', ); expect(spawnProcessMock).toBeCalledTimes(1); }); @@ -107,7 +107,7 @@ describe('getPublishTag', () => { spawnProcessMock = vi .spyOn(system, 'spawnProcess') .mockImplementation(() => - Promise.resolve(Buffer.from('weird-version', 'utf-8')) + Promise.resolve(Buffer.from('weird-version', 'utf-8')), ); const logger = { @@ -117,12 +117,12 @@ describe('getPublishTag', () => { '1.0.0', '@sentry/browser', defaultNpmConfig, - logger + logger, ); expect(actual).toEqual(undefined); expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).toHaveBeenCalledWith( - 'Could not fetch current version for package @sentry/browser' + 'Could not fetch current version for package @sentry/browser', ); expect(spawnProcessMock).toBeCalledTimes(1); }); @@ -135,15 +135,15 @@ describe('getPublishTag', () => { '1.0.0-alpha.1', undefined, defaultNpmConfig, - logger + logger, ); expect(actual).toBe('next'); expect(logger.warn).toHaveBeenCalledTimes(2); expect(logger.warn).toHaveBeenCalledWith( - 'Detected pre-release version for npm package!' + 'Detected pre-release version for npm package!', ); expect(logger.warn).toHaveBeenCalledWith( - 'Adding tag "next" to not make it "latest" in registry.' + 'Adding tag "next" to not make it "latest" in registry.', ); expect(spawnProcessMock).not.toBeCalled(); }); @@ -152,7 +152,7 @@ describe('getPublishTag', () => { spawnProcessMock = vi .spyOn(system, 'spawnProcess') .mockImplementation(() => - Promise.resolve(Buffer.from('7.20.0\n', 'utf-8')) + Promise.resolve(Buffer.from('7.20.0\n', 'utf-8')), ); const logger = { @@ -163,17 +163,17 @@ describe('getPublishTag', () => { '1.0.0', '@sentry/browser', defaultNpmConfig, - logger + logger, ); expect(actual).toBe('old'); expect(logger.warn).toHaveBeenCalledTimes(2); expect(logger.warn).toHaveBeenCalledWith( expect.stringMatching( - /Detected older version than currently published version \(([\d.]+)\) for @sentry\/browser/ - ) + /Detected older version than currently published version \(([\d.]+)\) for @sentry\/browser/, + ), ); expect(logger.warn).toHaveBeenCalledWith( - 'Adding tag "old" to not make it "latest" in registry.' + 'Adding tag "old" to not make it "latest" in registry.', ); expect(spawnProcessMock).toBeCalledTimes(1); }); @@ -226,7 +226,7 @@ describe('NpmTarget.expand', () => { const config = { name: 'npm', workspaces: true }; await expect(NpmTarget.expand(config, '/root')).rejects.toThrow( - /Public package "@sentry\/browser" depends on private workspace package\(s\): @sentry-internal\/utils/ + /Public package "@sentry\/browser" depends on private workspace package\(s\): @sentry-internal\/utils/, ); }); diff --git a/src/targets/__tests__/powershell.test.ts b/src/targets/__tests__/powershell.test.ts index 0d032203..58faf223 100644 --- a/src/targets/__tests__/powershell.test.ts +++ b/src/targets/__tests__/powershell.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type Mock } from 'vitest'; import { spawnProcess } from '../../utils/system'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { ConfigurationError } from '../../utils/errors'; @@ -15,7 +15,7 @@ function getPwshTarget(): PowerShellTarget { module: 'moduleName', repository: 'repositoryName', }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); } @@ -72,7 +72,7 @@ describe('config', () => { } catch (error) { expect(error).toBeInstanceOf(ConfigurationError); expect(error.message).toBe( - 'Missing project configuration parameter(s): apiKey,repository,module' + 'Missing project configuration parameter(s): apiKey,repository,module', ); } }); @@ -136,7 +136,7 @@ describe('publish', () => { 'pwsh', ['--version'], {}, - spawnOptions + spawnOptions, ); expect(mockedSpawnProcess).toBeCalledWith( 'pwsh', @@ -151,7 +151,7 @@ describe('publish', () => { `, ], {}, - spawnOptions + spawnOptions, ); }); @@ -171,7 +171,7 @@ describe('publish', () => { `, ], {}, - spawnOptions + spawnOptions, ); }); }); diff --git a/src/targets/__tests__/pubDev.test.ts b/src/targets/__tests__/pubDev.test.ts index e31c9011..79a5c743 100644 --- a/src/targets/__tests__/pubDev.test.ts +++ b/src/targets/__tests__/pubDev.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type MockedFunction } from 'vitest'; import { promises as fsPromises } from 'fs'; import { platform } from 'os'; import simpleGit from 'simple-git'; @@ -11,7 +11,7 @@ const TMP_DIR = '/tmp/dir'; vi.mock('../../utils/helpers'); vi.mock('../../utils/system'); -vi.mock('../../utils/files', async (importOriginal) => { +vi.mock('../../utils/files', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -19,7 +19,7 @@ vi.mock('../../utils/files', async (importOriginal) => { }; }); -vi.mock('os', async (importOriginal) => { +vi.mock('os', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -28,7 +28,7 @@ vi.mock('os', async (importOriginal) => { }; }); -vi.mock('fs', async (importOriginal) => { +vi.mock('fs', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -64,7 +64,7 @@ function removeTargetSecretsFromEnv(): void { } function createPubDevTarget( - targetConfig?: Record + targetConfig?: Record, ): PubDevTarget { return new PubDevTarget( { @@ -72,7 +72,7 @@ function createPubDevTarget( ...targetConfig, }, new NoneArtifactProvider(), - { owner: 'testOwner', repo: 'testRepo' } + { owner: 'testOwner', repo: 'testRepo' }, ); } @@ -90,12 +90,12 @@ describe('PubDev target configuration', () => { removeTargetSecretsFromEnv(); expect(createPubDevTarget).toThrowErrorMatchingInlineSnapshot( - `[Error: Required value(s) PUBDEV_ACCESS_TOKEN not found in configuration files or the environment. See the documentation for more details.]` + `[Error: Required value(s) PUBDEV_ACCESS_TOKEN not found in configuration files or the environment. See the documentation for more details.]`, ); process.env.PUBDEV_ACCESS_TOKEN = DEFAULT_OPTION_VALUE; expect(createPubDevTarget).toThrowErrorMatchingInlineSnapshot( - `[Error: Required value(s) PUBDEV_REFRESH_TOKEN not found in configuration files or the environment. See the documentation for more details.]` + `[Error: Required value(s) PUBDEV_REFRESH_TOKEN not found in configuration files or the environment. See the documentation for more details.]`, ); process.env.PUBDEV_REFRESH_TOKEN = DEFAULT_OPTION_VALUE; @@ -145,13 +145,13 @@ describe('publish', () => { const callOrder: string[] = []; const target = createPubDevTarget(); target.createCredentialsFile = vi.fn( - async () => void callOrder.push('createCredentialsFile') + async () => void callOrder.push('createCredentialsFile'), ); target.cloneRepository = vi.fn( - async () => void callOrder.push('cloneRepository') + async () => void callOrder.push('cloneRepository'), ); target.publishPackage = vi.fn( - async () => void callOrder.push('publishPackage') + async () => void callOrder.push('publishPackage'), ); await target.publish('1.0.0', revision); @@ -160,7 +160,7 @@ describe('publish', () => { expect(target.cloneRepository).toHaveBeenCalledWith( target.githubRepo, revision, - TMP_DIR + TMP_DIR, ); expect(target.publishPackage).toHaveBeenCalledWith(TMP_DIR, '.'); expect(callOrder).toStrictEqual([ @@ -181,13 +181,13 @@ describe('publish', () => { }, }); target.createCredentialsFile = vi.fn( - async () => void callOrder.push('createCredentialsFile') + async () => void callOrder.push('createCredentialsFile'), ); target.cloneRepository = vi.fn( - async () => void callOrder.push('cloneRepository') + async () => void callOrder.push('cloneRepository'), ); target.publishPackage = vi.fn( - async () => void callOrder.push('publishPackage') + async () => void callOrder.push('publishPackage'), ); await target.publish('1.0.0', revision); @@ -196,7 +196,7 @@ describe('publish', () => { expect(target.cloneRepository).toHaveBeenCalledWith( target.githubRepo, revision, - TMP_DIR + TMP_DIR, ); expect(target.publishPackage).toHaveBeenNthCalledWith(1, TMP_DIR, 'uno'); expect(target.publishPackage).toHaveBeenNthCalledWith(2, TMP_DIR, 'dos'); @@ -215,13 +215,13 @@ describe('publish', () => { const callOrder: string[] = []; const target = createPubDevTarget(); target.createCredentialsFile = vi.fn( - async () => void callOrder.push('createCredentialsFile') + async () => void callOrder.push('createCredentialsFile'), ); target.cloneRepository = vi.fn( - async () => void callOrder.push('cloneRepository') + async () => void callOrder.push('cloneRepository'), ); target.publishPackage = vi.fn( - async () => void callOrder.push('publishPackage') + async () => void callOrder.push('publishPackage'), ); const isDryRunMock = isDryRun as MockedFunction; @@ -254,17 +254,17 @@ describe('createCredentialsFile', () => { const [path, content] = writeFileMock.mock.calls[0]; expect(path).toBe( - `/usr/Library/Application Support/dart/pub-credentials.json` + `/usr/Library/Application Support/dart/pub-credentials.json`, ); expect(content).toMatchInlineSnapshot( - `"{"accessToken":"my_default_value","refreshToken":"my_default_value","tokenEndpoint":"https://accounts.google.com/o/oauth2/token","scopes":["openid","https://www.googleapis.com/auth/userinfo.email"],"expiration":1645564942000}"` + `"{"accessToken":"my_default_value","refreshToken":"my_default_value","tokenEndpoint":"https://accounts.google.com/o/oauth2/token","scopes":["openid","https://www.googleapis.com/auth/userinfo.email"],"expiration":1645564942000}"`, ); }); test('should make sure that directory exists before writing credentials file', async () => { fsPromises.access = vi.fn(() => Promise.reject()); (platform as MockedFunction).mockImplementationOnce( - () => 'linux' + () => 'linux', ); const target = createPubDevTarget(); await target.createCredentialsFile(); @@ -276,20 +276,20 @@ describe('createCredentialsFile', () => { test('should choose path based on the platform', async () => { fsPromises.access = vi.fn(() => Promise.reject()); (platform as MockedFunction).mockImplementationOnce( - () => 'linux' + () => 'linux', ); const target = createPubDevTarget(); await target.createCredentialsFile(); expect(fsPromises.writeFile).toHaveBeenCalledWith( `/usr/.config/dart/pub-credentials.json`, - expect.any(String) + expect.any(String), ); }); test('should throw when run on unsupported platform', async () => { fsPromises.access = vi.fn(() => Promise.reject()); (platform as MockedFunction).mockImplementationOnce( - () => 'win32' + () => 'win32', ); const target = createPubDevTarget(); await expect(target.createCredentialsFile()).rejects.toThrow(); @@ -308,7 +308,7 @@ describe('cloneRepository', () => { expect(simpleGitMock).toHaveBeenCalledWith(TMP_DIR); expect(simpleGitMockRv.clone).toHaveBeenCalledWith( `https://github.com/${target.githubRepo.owner}/${target.githubRepo.repo}.git`, - TMP_DIR + TMP_DIR, ); expect(simpleGitMockRv.checkout).toHaveBeenCalledWith(revision); }); @@ -342,7 +342,7 @@ dev_dependencies: dependency_overrides: sentry: - path: ../dart`) + path: ../dart`), ); await target.publishPackage(TMP_DIR, pkg); @@ -400,7 +400,7 @@ dependency_overrides: { cwd: `${TMP_DIR}/${pkg}`, }, - { showStdout: true } + { showStdout: true }, ); }); @@ -419,7 +419,7 @@ dependency_overrides: { cwd: `${TMP_DIR}/${pkg}`, }, - { showStdout: true } + { showStdout: true }, ); }); @@ -441,7 +441,7 @@ dependency_overrides: { cwd: `${TMP_DIR}/${pkg}`, }, - { showStdout: true } + { showStdout: true }, ); }); @@ -464,7 +464,7 @@ dependency_overrides: { cwd: `${TMP_DIR}/${pkg}`, }, - { showStdout: true } + { showStdout: true }, ); }); }); diff --git a/src/targets/__tests__/sentryPypi.test.ts b/src/targets/__tests__/sentryPypi.test.ts index e7a63d03..96e7fbed 100644 --- a/src/targets/__tests__/sentryPypi.test.ts +++ b/src/targets/__tests__/sentryPypi.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { WHEEL_REGEX, uniquePackages } from '../sentryPypi'; describe('WHEEL_REGEX', () => { diff --git a/src/targets/__tests__/symbolCollector.test.ts b/src/targets/__tests__/symbolCollector.test.ts index 7dc9f90f..07f9a5c1 100644 --- a/src/targets/__tests__/symbolCollector.test.ts +++ b/src/targets/__tests__/symbolCollector.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type MockedFunction } from 'vitest'; import { withTempDir } from '../../utils/files'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { checkExecutableIsPresent, spawnProcess } from '../../utils/system'; @@ -6,7 +6,7 @@ import { SymbolCollector, SYM_COLLECTOR_BIN_NAME } from '../symbolCollector'; vi.mock('../../utils/files'); vi.mock('../../utils/system'); -vi.mock('fs', async (importOriginal) => { +vi.mock('fs', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -24,31 +24,33 @@ const customConfig = { }; function getSymbolCollectorInstance( - config: Record = { testKey: 'testVal' } + config: Record = { testKey: 'testVal' }, ): SymbolCollector { return new SymbolCollector( { name: 'symbol-collector', ...config, }, - new NoneArtifactProvider() + new NoneArtifactProvider(), ); } describe('target config', () => { test('symbol collector not present in path', () => { - (checkExecutableIsPresent as MockedFunction< - typeof checkExecutableIsPresent - >).mockImplementationOnce(() => { + ( + checkExecutableIsPresent as MockedFunction< + typeof checkExecutableIsPresent + > + ).mockImplementationOnce(() => { throw new Error('Checked for executable'); }); expect(getSymbolCollectorInstance).toThrowErrorMatchingInlineSnapshot( - `[Error: Checked for executable]` + `[Error: Checked for executable]`, ); expect(checkExecutableIsPresent).toHaveBeenCalledTimes(1); expect(checkExecutableIsPresent).toHaveBeenCalledWith( - SYM_COLLECTOR_BIN_NAME + SYM_COLLECTOR_BIN_NAME, ); }); @@ -58,7 +60,7 @@ describe('target config', () => { >) = vi.fn(); expect(getSymbolCollectorInstance).toThrowErrorMatchingInlineSnapshot( - `[Error: The required \`batchType\` parameter is missing in the configuration file. See the documentation for more details.]` + `[Error: The required \`batchType\` parameter is missing in the configuration file. See the documentation for more details.]`, ); }); @@ -71,7 +73,7 @@ describe('target config', () => { const actualConfig = symCollector.symbolCollectorConfig; expect(checkExecutableIsPresent).toHaveBeenCalledTimes(1); expect(checkExecutableIsPresent).toHaveBeenLastCalledWith( - SYM_COLLECTOR_BIN_NAME + SYM_COLLECTOR_BIN_NAME, ); expect(actualConfig).toHaveProperty('serverEndpoint'); expect(actualConfig).toHaveProperty('batchType'); @@ -90,11 +92,11 @@ describe('publish', () => { test('with artifacts', async () => { (withTempDir as MockedFunction).mockImplementation( - async cb => await cb('tmpDir') + async cb => await cb('tmpDir'), + ); + (spawnProcess as MockedFunction).mockImplementation( + () => Promise.resolve(undefined), ); - (spawnProcess as MockedFunction< - typeof spawnProcess - >).mockImplementation(() => Promise.resolve(undefined)); const mockedArtifacts = ['artifact1', 'artifact2', 'artifact3']; @@ -108,13 +110,12 @@ describe('publish', () => { expect(symCollector.getArtifactsForRevision).toHaveBeenCalledTimes(1); expect( - symCollector.artifactProvider.downloadArtifact + symCollector.artifactProvider.downloadArtifact, ).toHaveBeenCalledTimes(mockedArtifacts.length); expect(spawnProcess).toHaveBeenCalledTimes(1); - const [cmd, args] = (spawnProcess as MockedFunction< - typeof spawnProcess - >).mock.calls[0] as string[]; + const [cmd, args] = (spawnProcess as MockedFunction) + .mock.calls[0] as string[]; expect(cmd).toBe(SYM_COLLECTOR_BIN_NAME); expect(args).toMatchInlineSnapshot(` [ diff --git a/src/targets/__tests__/upm.test.ts b/src/targets/__tests__/upm.test.ts index 5334e7ec..7b4ad893 100644 --- a/src/targets/__tests__/upm.test.ts +++ b/src/targets/__tests__/upm.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { setGlobals } from '../../utils/helpers'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import { ARTIFACT_NAME, UpmTarget } from '../upm'; @@ -22,7 +22,7 @@ describe('UPM Target', () => { releaseRepoName: 'unity-test', }, new NoneArtifactProvider(), - { owner: 'testSourceOwner', repo: 'testSourceRepo' } + { owner: 'testSourceOwner', repo: 'testSourceRepo' }, ); }); @@ -42,9 +42,9 @@ describe('UPM Target', () => { .mockResolvedValueOnce(artifacts); await expect(upmTarget.fetchArtifact('revision')).rejects.toThrow( - error + error, ); - } + }, ); }); @@ -61,7 +61,7 @@ describe('UPM Target', () => { test('publish', () => { return expect( - upmTarget.publish('version', 'revision') + upmTarget.publish('version', 'revision'), ).resolves.not.toThrow(); }); }); diff --git a/src/targets/awsLambdaLayer.ts b/src/targets/awsLambdaLayer.ts index 8896d177..9527c683 100644 --- a/src/targets/awsLambdaLayer.ts +++ b/src/targets/awsLambdaLayer.ts @@ -8,7 +8,7 @@ import { GitHubRemote, } from '../utils/githubApi'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { BaseTarget } from './base'; import { BaseArtifactProvider } from '../artifact_providers/base'; import { ConfigurationError, reportError } from '../utils/errors'; @@ -20,7 +20,7 @@ import { } from '../utils/awsLambdaLayerManager'; import { createSymlinks } from '../utils/symlink'; import { withTempDir } from '../utils/files'; -import { cloneRepo, createGitClient } from '../utils/git'; +import { cloneRepo } from '../utils/git'; import { safeExec } from '../utils/dryRun'; import { renderTemplateSafe } from '../utils/strings'; import { isPreviewRelease, parseVersion } from '../utils/version'; @@ -38,6 +38,13 @@ interface AwsLambdaTargetConfig { linkPrereleases: boolean; } +/** Config fields for aws-lambda-layer target from .craft.yml */ +interface AwsLambdaTargetYamlConfig extends Record { + linkPrereleases?: boolean; + compatibleRuntimes?: CompatibleRuntime[]; + license?: string; +} + /** * Target responsible for uploading files to AWS Lambda. */ @@ -72,11 +79,12 @@ export class AwsLambdaLayerTarget extends BaseTarget { Please use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.`, ); } + const config = this.config as TypedTargetConfig; return { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, registryRemote: DEFAULT_REGISTRY_REMOTE, - linkPrereleases: this.config.linkPrereleases || false, + linkPrereleases: config.linkPrereleases || false, }; } @@ -199,7 +207,9 @@ export class AwsLambdaLayerTarget extends BaseTarget { await git.add(['.']); await git.checkout('master'); - const runtimeNames = this.config.compatibleRuntimes.map( + const config = this + .config as TypedTargetConfig; + const runtimeNames = config.compatibleRuntimes!.map( (runtime: CompatibleRuntime) => runtime.name, ); await git.commit( @@ -279,13 +289,14 @@ export class AwsLambdaLayerTarget extends BaseTarget { const resolvedLayerName = this.resolveLayerName(version); this.logger.debug(`Resolved layer name: ${resolvedLayerName}`); + const config = this.config as TypedTargetConfig; await Promise.all( - this.config.compatibleRuntimes.map(async (runtime: CompatibleRuntime) => { + config.compatibleRuntimes!.map(async (runtime: CompatibleRuntime) => { this.logger.debug(`Publishing runtime ${runtime.name}...`); const layerManager = new AwsLambdaLayerManager( runtime, resolvedLayerName, - this.config.license, + config.license!, artifactBuffer, awsRegions, version, diff --git a/src/targets/brew.ts b/src/targets/brew.ts index c4742254..1c946637 100644 --- a/src/targets/brew.ts +++ b/src/targets/brew.ts @@ -1,7 +1,11 @@ import { mapLimit } from 'async'; import { Octokit } from '@octokit/rest'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { ConfigurationError } from '../utils/errors'; import { getGitHubClient } from '../utils/githubApi'; import { renderTemplateSafe } from '../utils/strings'; @@ -14,6 +18,14 @@ import { RemoteArtifact, } from '../artifact_providers/base'; +/** Brew target configuration fields */ +interface BrewConfigFields extends Record { + tap?: string; + template?: string; + formula?: string; + path?: string; +} + /** * Regex used to parse homebrew taps (github repositories) */ @@ -55,7 +67,7 @@ export class BrewTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); this.brewConfig = this.getBrewConfig(); @@ -67,13 +79,14 @@ export class BrewTarget extends BaseTarget { * Extracts Brew target options from the raw configuration */ public getBrewConfig(): BrewTargetOptions { - const template = this.config.template; + const config = this.config as TypedTargetConfig; + const template = config.template; if (!template) { throw new ConfigurationError( - 'Please specify Formula template in the "brew" target configuration.' + 'Please specify Formula template in the "brew" target configuration.', ); } - const { formula, path } = this.config; + const { formula, path } = config; return { formula, path, @@ -92,7 +105,8 @@ export class BrewTarget extends BaseTarget { * @returns The owner and repository of the tap */ public getTapRepo(): TapRepo { - const { tap } = this.config; + const config = this.config as TypedTargetConfig; + const { tap } = config; if (!tap) { return { owner: 'homebrew', @@ -178,7 +192,7 @@ export class BrewTarget extends BaseTarget { // Skip pre-release versions as Homebrew doesn't understand them if (isPreviewRelease(version)) { this.logger.info( - 'Skipping Homebrew release for pre-release version: ' + version + 'Skipping Homebrew release for pre-release version: ' + version, ); return undefined; } @@ -198,14 +212,18 @@ export class BrewTarget extends BaseTarget { const checksums: any = {}; - await mapLimit(filesList, MAX_DOWNLOAD_CONCURRENCY, async (file: RemoteArtifact) => { - const key = file.filename.replace(version, '__VERSION__'); - checksums[key] = await this.artifactProvider.getChecksum( - file, - HashAlgorithm.SHA256, - HashOutputFormat.Hex - ); - }); + await mapLimit( + filesList, + MAX_DOWNLOAD_CONCURRENCY, + async (file: RemoteArtifact) => { + const key = file.filename.replace(version, '__VERSION__'); + checksums[key] = await this.artifactProvider.getChecksum( + file, + HashAlgorithm.SHA256, + HashOutputFormat.Hex, + ); + }, + ); const data = renderTemplateSafe(template, { checksums, @@ -233,12 +251,12 @@ export class BrewTarget extends BaseTarget { this.logger.info( `Releasing ${this.githubRepo.owner}/${this.githubRepo.repo} tag ${version} ` + `to homebrew tap ${tapRepo.owner}/${tapRepo.repo} ` + - `formula ${formulaName}` + `formula ${formulaName}`, ); const action = params.sha ? 'Updating' : 'Creating'; this.logger.debug( - `${action} file ${params.owner}/${params.repo}:${params.path} (${params.sha})` + `${action} file ${params.owner}/${params.repo}:${params.path} (${params.sha})`, ); await this.github.repos.createOrUpdateFileContents(params); diff --git a/src/targets/cocoapods.ts b/src/targets/cocoapods.ts index 5fc1c9b3..b0e1f26f 100644 --- a/src/targets/cocoapods.ts +++ b/src/targets/cocoapods.ts @@ -3,7 +3,11 @@ import * as fs from 'fs'; import { basename, join } from 'path'; import { promisify } from 'util'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { getFile, getGitHubClient } from '../utils/githubApi'; @@ -25,6 +29,11 @@ export interface CocoapodsTargetOptions { specPath: string; } +/** Config fields for cocoapods target */ +interface CocoapodsTargetConfig extends Record { + specPath?: string; +} + /** * Target responsible for publishing to Cocoapods registry */ @@ -41,7 +50,7 @@ export class CocoapodsTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); this.cocoapodsConfig = this.getCocoapodsConfig(); @@ -54,7 +63,8 @@ export class CocoapodsTarget extends BaseTarget { * Extracts Cocoapods target options from the environment */ public getCocoapodsConfig(): CocoapodsTargetOptions { - const specPath = this.config.specPath; + const config = this.config as TypedTargetConfig; + const specPath = config.specPath; if (!specPath) { throw new ConfigurationError('No podspec path provided!'); } @@ -80,7 +90,7 @@ export class CocoapodsTarget extends BaseTarget { owner, repo, specPath, - revision + revision, ); if (!specContents) { @@ -105,11 +115,11 @@ export class CocoapodsTarget extends BaseTarget { env: { ...process.env, }, - } + }, ); }, true, - 'craft-cocoapods-' + 'craft-cocoapods-', ); this.logger.info('Cocoapods release complete'); diff --git a/src/targets/commitOnGitRepository.ts b/src/targets/commitOnGitRepository.ts index c30e750d..4b143f02 100644 --- a/src/targets/commitOnGitRepository.ts +++ b/src/targets/commitOnGitRepository.ts @@ -1,8 +1,8 @@ import { BaseArtifactProvider } from '../artifact_providers/base'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; -import { cloneRepo, createGitClient } from '../utils/git'; +import { cloneRepo } from '../utils/git'; import { BaseTarget } from './base'; import childProcess from 'child_process'; import type { Consola } from 'consola'; @@ -16,6 +16,12 @@ interface GitRepositoryTargetConfig { stripComponents?: number; } +/** Config fields for commit-on-git-repository target from .craft.yml */ +interface GitRepositoryYamlConfig extends Record { + createTag?: boolean; + stripComponents?: number; +} + /** * Target responsible for pushing code to a git repository. */ @@ -25,7 +31,7 @@ export class CommitOnGitRepositoryTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); } @@ -37,13 +43,8 @@ export class CommitOnGitRepositoryTarget extends BaseTarget { * @param revision Git commit SHA to be published */ public async publish(version: string, revision: string): Promise { - const { - archive, - branch, - repositoryUrl, - createTag, - stripComponents, - } = this.getGitRepositoryTargetConfig(); + const { archive, branch, repositoryUrl, createTag, stripComponents } = + this.getGitRepositoryTargetConfig(); this.logger.info(`Finding archive with regexp "${archive}"...`); @@ -64,7 +65,7 @@ export class CommitOnGitRepositoryTarget extends BaseTarget { this.logger.debug('Downloading archive...'); const archivePath = await this.artifactProvider.downloadArtifact( - archives[0] + archives[0], ); this.logger.info('Downloading archive complete'); @@ -83,28 +84,29 @@ export class CommitOnGitRepositoryTarget extends BaseTarget { private getGitRepositoryTargetConfig(): GitRepositoryTargetConfig { if (typeof this.config['archive'] !== 'string') { throw new ConfigurationError( - `\`archive\` option has invalid value ${this.config['archive']}. Needs to be RegExp in the form of a string.` + `\`archive\` option has invalid value ${this.config['archive']}. Needs to be RegExp in the form of a string.`, ); } if (typeof this.config['repositoryUrl'] !== 'string') { throw new ConfigurationError( - `\`repositoryUrl\` option has invalid value ${this.config['repositoryUrl']}. Needs to be string.` + `\`repositoryUrl\` option has invalid value ${this.config['repositoryUrl']}. Needs to be string.`, ); } if (typeof this.config['branch'] !== 'string') { throw new ConfigurationError( - `\`repositoryUrl\` option has invalid value ${this.config['branch']}. Needs to be string.` + `\`repositoryUrl\` option has invalid value ${this.config['branch']}. Needs to be string.`, ); } + const config = this.config as TypedTargetConfig; return { archive: this.config['archive'], repositoryUrl: this.config['repositoryUrl'], branch: this.config['branch'], - createTag: this.config['createTag'] ?? true, - stripComponents: this.config['stripComponents'], + createTag: config.createTag ?? true, + stripComponents: config.stripComponents, }; } } @@ -138,7 +140,7 @@ export async function pushArchiveToGitRepository({ parsedUrl = new URL(repositoryUrl); } catch (e) { logger?.error( - `Error while parsing \`repositoryUrl\`. Make sure this is a valid URL using the http or https protocol!` + `Error while parsing \`repositoryUrl\`. Make sure this is a valid URL using the http or https protocol!`, ); throw e; } @@ -192,6 +194,6 @@ export async function pushArchiveToGitRepository({ } }, true, - 'craft-git-repository-target-' + 'craft-git-repository-target-', ); } diff --git a/src/targets/gcs.ts b/src/targets/gcs.ts index f4654834..735918a5 100644 --- a/src/targets/gcs.ts +++ b/src/targets/gcs.ts @@ -1,4 +1,4 @@ -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { forEachChained } from '../utils/async'; import { ConfigurationError, reportError } from '../utils/errors'; import { @@ -36,6 +36,11 @@ export interface GCSTargetConfig extends GCSBucketConfig { pathTemplates: PathTemplate[]; } +/** Config fields for gcs target from .craft.yml */ +interface GcsYamlConfig extends Record { + bucket?: string; +} + /** * Target responsible for uploading files to GCS */ @@ -49,7 +54,7 @@ export class GcsTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); this.targetConfig = this.getGCSTargetConfig(); @@ -68,17 +73,20 @@ export class GcsTarget extends BaseTarget { { name: 'CRAFT_GCS_TARGET_CREDS_PATH', legacyName: 'CRAFT_GCS_CREDENTIALS_PATH', - } + }, ); - const bucketName = this.config.bucket; + const config = this.config as TypedTargetConfig; + const bucketName = config.bucket; // TODO (kmclb) get rid of this check once config validation is working if (!bucketName) { reportError('No GCS bucket provided!'); + // TypeScript can't infer that reportError throws + throw new Error('No GCS bucket provided!'); } const pathTemplates: PathTemplate[] = this.parseRawPathConfig( - this.config.paths + this.config.paths, ); return { @@ -127,8 +135,8 @@ export class GcsTarget extends BaseTarget { if (typeof configEntry !== 'object') { reportError( `Invalid bucket destination: ${JSON.stringify( - configEntry - )}. Use object notation to specify bucket paths!` + configEntry, + )}. Use object notation to specify bucket paths!`, ); } @@ -140,8 +148,8 @@ export class GcsTarget extends BaseTarget { if (metadata && typeof metadata !== 'object') { reportError( `Invalid metadata for path "${template}": "${JSON.stringify( - metadata - )}. Use object notation to specify metadata!"` + metadata, + )}. Use object notation to specify metadata!"`, ); } @@ -172,7 +180,7 @@ export class GcsTarget extends BaseTarget { private materializePathTemplate( pathTemplate: PathTemplate, version: string, - revision: string + revision: string, ): BucketPath { const { template, metadata } = pathTemplate; @@ -190,7 +198,7 @@ export class GcsTarget extends BaseTarget { realPath = `/${realPath}`; } this.logger.debug( - `Processed path template \`${template}\` and got \`${realPath}\`` + `Processed path template \`${template}\` and got \`${realPath}\``, ); return { path: realPath, metadata }; } @@ -208,7 +216,7 @@ export class GcsTarget extends BaseTarget { const artifacts = await this.getArtifactsForRevision(revision); if (!artifacts.length) { throw new ConfigurationError( - 'No artifacts to publish: please check your configuration!' + 'No artifacts to publish: please check your configuration!', ); } @@ -221,8 +229,8 @@ export class GcsTarget extends BaseTarget { const localFilePaths = await Promise.all( artifacts.map( async (artifact: RemoteArtifact): Promise => - this.artifactProvider.downloadArtifact(artifact) - ) + this.artifactProvider.downloadArtifact(artifact), + ), ); // We intentionally do not make all requests concurrent here, instead @@ -234,7 +242,7 @@ export class GcsTarget extends BaseTarget { const bucketPath = this.materializePathTemplate( pathTemplate, version, - revision + revision, ); this.logger.info(`Uploading files to ${bucketPath.path}.`); @@ -245,10 +253,10 @@ export class GcsTarget extends BaseTarget { return Promise.all( localFilePaths.map(async localPath => - this.gcsClient.uploadArtifact(localPath, bucketPath) - ) + this.gcsClient.uploadArtifact(localPath, bucketPath), + ), ); - } + }, ); this.logger.info('Upload to GCS complete.'); diff --git a/src/targets/ghPages.ts b/src/targets/ghPages.ts index 96a67ca5..2c5f791e 100644 --- a/src/targets/ghPages.ts +++ b/src/targets/ghPages.ts @@ -3,7 +3,11 @@ import * as path from 'path'; import { Octokit } from '@octokit/rest'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { @@ -11,11 +15,18 @@ import { getGitHubClient, GitHubRemote, } from '../utils/githubApi'; -import { cloneRepo, createGitClient } from '../utils/git'; +import { cloneRepo } from '../utils/git'; import { extractZipArchive } from '../utils/system'; import { BaseTarget } from './base'; import { BaseArtifactProvider } from '../artifact_providers/base'; +/** GH Pages target configuration fields */ +interface GhPagesConfigFields extends Record { + branch?: string; + githubOwner?: string; + githubRepo?: string; +} + /** * Regex for docs archives */ @@ -49,7 +60,7 @@ export class GhPagesTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); this.github = getGitHubClient(); @@ -61,21 +72,22 @@ export class GhPagesTarget extends BaseTarget { * Extracts "gh-pages" target options from the raw configuration */ public getGhPagesConfig(): GhPagesConfig { + const config = this.config as TypedTargetConfig; let githubOwner; let githubRepo; - if (this.config.githubOwner && this.config.githubRepo) { - githubOwner = this.config.githubOwner; - githubRepo = this.config.githubRepo; - } else if (!this.config.githubOwner && !this.config.githubRepo) { + if (config.githubOwner && config.githubRepo) { + githubOwner = config.githubOwner; + githubRepo = config.githubRepo; + } else if (!config.githubOwner && !config.githubRepo) { githubOwner = this.githubRepo.owner; githubRepo = this.githubRepo.repo; } else { throw new ConfigurationError( - '[gh-pages] Invalid repository configuration: check repo owner and name' + '[gh-pages] Invalid repository configuration: check repo owner and name', ); } - const branch = this.config.branch || DEFAULT_DEPLOY_BRANCH; + const branch = config.branch || DEFAULT_DEPLOY_BRANCH; return { branch, @@ -97,13 +109,13 @@ export class GhPagesTarget extends BaseTarget { */ public async extractAssets( archivePath: string, - directory: string + directory: string, ): Promise { // Check that the directory is empty const dirContents = fs.readdirSync(directory).filter(f => f !== '.git'); if (dirContents.length > 0) { throw new Error( - 'Destination directory is not empty: cannot extract the acrhive!' + 'Destination directory is not empty: cannot extract the acrhive!', ); } @@ -118,7 +130,7 @@ export class GhPagesTarget extends BaseTarget { fs.statSync(path.join(directory, newDirContents[0])).isDirectory() ) { this.logger.debug( - 'Single top-level directory found, moving files from it...' + 'Single top-level directory found, moving files from it...', ); const innerDirPath = path.join(directory, newDirContents[0]); fs.readdirSync(innerDirPath).forEach(item => { @@ -144,10 +156,10 @@ export class GhPagesTarget extends BaseTarget { remote: GitHubRemote, branch: string, archivePath: string, - version: string + version: string, ): Promise { this.logger.info( - `Cloning "${remote.getRemoteString()}" to "${directory}"...` + `Cloning "${remote.getRemoteString()}" to "${directory}"...`, ); const git = await cloneRepo(remote.getRemoteStringWithAuth(), directory); this.logger.debug(`Checking out branch: "${branch}"`); @@ -158,7 +170,7 @@ export class GhPagesTarget extends BaseTarget { throw e; } this.logger.debug( - `Branch ${branch} does not exist, creating a new orphaned branch...` + `Branch ${branch} does not exist, creating a new orphaned branch...`, ); await git.checkout(['--orphan', branch]); } @@ -167,7 +179,7 @@ export class GhPagesTarget extends BaseTarget { const repoStatus = await git.status(); if (repoStatus.current !== 'No' && repoStatus.current !== branch) { throw new Error( - `Something went very wrong: cannot switch to branch "${branch}"` + `Something went very wrong: cannot switch to branch "${branch}"`, ); } @@ -201,19 +213,19 @@ export class GhPagesTarget extends BaseTarget { } else if (packageFiles.length > 1) { reportError( `Not implemented: more than one gh-pages archive found\nDetails: ${JSON.stringify( - packageFiles - )}` + packageFiles, + )}`, ); return undefined; } const archivePath = await this.artifactProvider.downloadArtifact( - packageFiles[0] + packageFiles[0], ); const remote = new GitHubRemote( githubOwner, githubRepo, - getGitHubApiToken() + getGitHubApiToken(), ); await withTempDir( @@ -223,10 +235,10 @@ export class GhPagesTarget extends BaseTarget { remote, branch, archivePath, - version + version, ), true, - 'craft-gh-pages-' + 'craft-gh-pages-', ); this.logger.info('GitHub pages release complete'); diff --git a/src/targets/github.ts b/src/targets/github.ts index 4829e939..eec71200 100644 --- a/src/targets/github.ts +++ b/src/targets/github.ts @@ -7,6 +7,7 @@ import { ChangelogPolicy, GitHubGlobalConfig, TargetConfig, + TypedTargetConfig, } from '../schemas/project_config'; import { Changeset, @@ -32,6 +33,16 @@ import { logger } from '../logger'; */ export const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; +/** GitHub target configuration fields */ +interface GitHubConfigFields extends Record { + owner?: string; + repo?: string; + tagPrefix?: string; + tagOnly?: boolean; + previewReleases?: boolean; + floatingTags?: string[]; +} + /** * Configuration options for the GitHub target. */ @@ -67,7 +78,8 @@ interface GitHubRelease { draft?: boolean; } -type ReposListAssetsForReleaseResponseItem = RestEndpointMethodTypes['repos']['listReleaseAssets']['response']['data'][0]; +type ReposListAssetsForReleaseResponseItem = + RestEndpointMethodTypes['repos']['listReleaseAssets']['response']['data'][0]; /** * Target responsible for publishing releases on GitHub. @@ -85,12 +97,13 @@ export class GitHubTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); this.githubRepo = githubRepo; - const owner = config.owner || githubRepo.owner; - const repo = config.repo || githubRepo.repo; + const typedConfig = this.config as TypedTargetConfig; + const owner = typedConfig.owner || githubRepo.owner; + const repo = typedConfig.repo || githubRepo.repo; const configChangelog = getConfiguration().changelog; const changelog = typeof configChangelog === 'string' @@ -102,11 +115,11 @@ export class GitHubTarget extends BaseTarget { repo, changelog, previewReleases: - this.config.previewReleases === undefined || - !!this.config.previewReleases, - tagPrefix: this.config.tagPrefix || '', - tagOnly: !!this.config.tagOnly, - floatingTags: this.config.floatingTags || [], + typedConfig.previewReleases === undefined || + !!typedConfig.previewReleases, + tagPrefix: typedConfig.tagPrefix || '', + tagOnly: !!typedConfig.tagOnly, + floatingTags: typedConfig.floatingTags || [], }; this.github = getGitHubClient(); } @@ -126,7 +139,7 @@ export class GitHubTarget extends BaseTarget { public async createDraftRelease( version: string, revision: string, - changes?: Changeset + changes?: Changeset, ): Promise { const tag = versionToTag(version, this.githubConfig.tagPrefix); this.logger.info(`Git tag: "${tag}"`); @@ -185,7 +198,7 @@ export class GitHubTarget extends BaseTarget { * @param asset Asset to delete */ public async deleteAsset( - asset: ReposListAssetsForReleaseResponseItem + asset: ReposListAssetsForReleaseResponseItem, ): Promise { this.logger.debug(`Deleting asset: "${asset.name}"...`); return ( @@ -212,7 +225,7 @@ export class GitHubTarget extends BaseTarget { if (release.draft === false) { this.logger.warn( - `Refusing to delete release "${release.tag_name}" because it is not a draft` + `Refusing to delete release "${release.tag_name}" because it is not a draft`, ); return false; } @@ -235,7 +248,7 @@ export class GitHubTarget extends BaseTarget { * @param release Release to fetch assets from */ public async getAssetsForRelease( - release_id: number + release_id: number, ): Promise { const assetsResponse = await this.github.repos.listReleaseAssets({ owner: this.githubConfig.owner, @@ -254,15 +267,15 @@ export class GitHubTarget extends BaseTarget { */ public async deleteAssetByName( release_id: number, - assetName: string + assetName: string, ): Promise { const assets = await this.getAssetsForRelease(release_id); const assetToDelete = assets.find(({ name }) => name === assetName); if (!assetToDelete) { logger.warn( `No such asset with the name "${assetToDelete}", moving on. We have these instead: ${assets.map( - ({ name }) => name - )}` + ({ name }) => name, + )}`, ); return false; } @@ -279,17 +292,21 @@ export class GitHubTarget extends BaseTarget { public async uploadAsset( release: GitHubRelease, path: string, - contentType?: string + contentType?: string, ): Promise { const name = basename(path); return safeExec(async () => { process.stderr.write( - `Uploading asset "${name}" to ${this.githubConfig.owner}/${this.githubConfig.repo}:${release.tag_name}\n` + `Uploading asset "${name}" to ${this.githubConfig.owner}/${this.githubConfig.repo}:${release.tag_name}\n`, ); try { - const { url } = await this.handleGitHubUpload(release, path, contentType); + const { url } = await this.handleGitHubUpload( + release, + path, + contentType, + ); process.stderr.write(`✔ Uploaded asset "${name}".\n`); return url; } catch (e) { @@ -303,7 +320,7 @@ export class GitHubTarget extends BaseTarget { release: GitHubRelease, path: string, contentType?: string, - retries = 3 + retries = 3, ): Promise<{ url: string; size: number }> { const contentTypeProcessed = contentType || DEFAULT_CONTENT_TYPE; const stats = statSync(path); @@ -338,20 +355,20 @@ export class GitHubTarget extends BaseTarget { if (ret.size != stats.size) { throw new Error( - `Uploaded asset size (${ret.size} bytes) does not match local asset size (${stats.size} bytes) for "${name}".` + `Uploaded asset size (${ret.size} bytes) does not match local asset size (${stats.size} bytes) for "${name}".`, ); } return ret; - } catch (err) { + } catch { if (retries <= 0) { throw new Error( - `Reached maximum retries for trying to upload asset "${params.name}.` + `Reached maximum retries for trying to upload asset "${params.name}.`, ); } logger.info( - 'Got an error when trying to upload an asset, deleting and retrying...' + 'Got an error when trying to upload an asset, deleting and retrying...', ); await this.deleteAssetByName(params.release_id, params.name); return this.handleGitHubUpload(release, path, contentType, --retries); @@ -367,7 +384,7 @@ export class GitHubTarget extends BaseTarget { */ public async publishRelease( release: GitHubRelease, - options: { makeLatest: boolean } = { makeLatest: true } + options: { makeLatest: boolean } = { makeLatest: true }, ) { await this.github.repos.updateRelease({ ...this.githubConfig, @@ -389,7 +406,7 @@ export class GitHubTarget extends BaseTarget { */ protected async createGitTag( version: string, - revision: string + revision: string, ): Promise { const tag = versionToTag(version, this.githubConfig.tagPrefix); const tagRef = `refs/tags/${tag}`; @@ -427,7 +444,7 @@ export class GitHubTarget extends BaseTarget { */ protected async updateFloatingTags( version: string, - revision: string + revision: string, ): Promise { const floatingTags = this.githubConfig.floatingTags; if (!floatingTags || floatingTags.length === 0) { @@ -437,7 +454,7 @@ export class GitHubTarget extends BaseTarget { const parsedVersion = parseVersion(version); if (!parsedVersion) { this.logger.warn( - `Cannot parse version "${version}" for floating tags, skipping` + `Cannot parse version "${version}" for floating tags, skipping`, ); return; } @@ -488,7 +505,7 @@ export class GitHubTarget extends BaseTarget { public async publish(version: string, revision: string): Promise { if (this.githubConfig.tagOnly) { this.logger.info( - `Not creating a GitHub release because "tagOnly" flag was set.` + `Not creating a GitHub release because "tagOnly" flag was set.`, ); await this.createGitTag(version, revision); await this.updateFloatingTags(version, revision); @@ -506,7 +523,7 @@ export class GitHubTarget extends BaseTarget { artifacts.map(async artifact => ({ mimeType: artifact.mimeType, path: await this.artifactProvider.downloadArtifact(artifact), - })) + })), ); let latestRelease: { tag_name: string } | undefined = undefined; @@ -529,7 +546,7 @@ export class GitHubTarget extends BaseTarget { this.logger.info( latestReleaseTag ? `Previous release: ${latestReleaseTag}` - : 'No previous release found' + : 'No previous release found', ); // Preview versions should never be marked as latest @@ -542,14 +559,14 @@ export class GitHubTarget extends BaseTarget { const draftRelease = await this.createDraftRelease( version, revision, - changelog + changelog, ); try { await Promise.all( localArtifacts.map(({ path, mimeType }) => - this.uploadAsset(draftRelease, path, mimeType) - ) + this.uploadAsset(draftRelease, path, mimeType), + ), ); await this.publishRelease(draftRelease, { makeLatest }); @@ -558,11 +575,11 @@ export class GitHubTarget extends BaseTarget { try { await this.deleteRelease(draftRelease); this.logger.info( - `Deleted orphaned draft release: ${draftRelease.tag_name}` + `Deleted orphaned draft release: ${draftRelease.tag_name}`, ); } catch (deleteError) { this.logger.warn( - `Failed to delete orphaned draft release: ${deleteError}` + `Failed to delete orphaned draft release: ${deleteError}`, ); } throw error; @@ -575,7 +592,7 @@ export class GitHubTarget extends BaseTarget { export function isLatestRelease( githubRelease: { tag_name: string } | undefined, - version: string + version: string, ) { const latestVersion = githubRelease && parseVersion(githubRelease.tag_name); const versionToPublish = parseVersion(version); diff --git a/src/targets/maven.ts b/src/targets/maven.ts index 656d9ccb..627923de 100644 --- a/src/targets/maven.ts +++ b/src/targets/maven.ts @@ -1,4 +1,4 @@ -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { BaseArtifactProvider, RemoteArtifact, @@ -38,7 +38,7 @@ export const targetSecrets = [ 'OSSRH_USERNAME', 'OSSRH_PASSWORD', ] as const; -type SecretsType = typeof targetSecrets[number]; +type SecretsType = (typeof targetSecrets)[number]; export const targetOptions = [ 'mavenCliPath', @@ -46,7 +46,7 @@ export const targetOptions = [ 'mavenRepoId', 'mavenRepoUrl', ] as const; -type OptionsType = typeof targetOptions[number]; +type OptionsType = (typeof targetOptions)[number]; type AndroidFields = { android: @@ -70,6 +70,30 @@ type KotlinMultiplatformFields = { type TargetSettingType = SecretsType | OptionsType; +/** KMP (Kotlin Multiplatform) config fields from .craft.yml */ +interface KmpConfigFields { + rootDistDirRegex?: string; + appleDistDirRegex?: string; + klibDistDirRegex?: string; +} + +/** Android config fields from .craft.yml */ +interface AndroidConfigFields { + distDirRegex?: string; + fileReplaceeRegex?: string; + fileReplacerStr?: string; +} + +/** Maven target configuration fields from .craft.yml */ +interface MavenConfigFields extends Record { + kmp?: KmpConfigFields | false; + android?: AndroidConfigFields | false; + mavenCliPath?: string; + mavenSettingsPath?: string; + mavenRepoId?: string; + mavenRepoUrl?: string; +} + /** * Config options for the "maven" target. */ @@ -90,7 +114,7 @@ export class MavenTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); this.mavenConfig = this.getMavenConfig(); @@ -137,86 +161,87 @@ export class MavenTarget extends BaseTarget { } private getOuterTargetSettings(): Record { + const config = this.config as TypedTargetConfig; const settings = targetOptions.map(setting => { - if (!this.config[setting]) { + if (!config[setting]) { throw new ConfigurationError( `Required configuration ${setting} not found in configuration file. ` + - `See the documentation for more details.` + `See the documentation for more details.`, ); } return { name: setting, - value: this.config[setting], + value: config[setting], }; }); return this.reduceConfig(settings); } private getKotlinMultiplatformSettings(): KotlinMultiplatformFields { - if (this.config.kmp === false || !this.config.kmp) { + const config = this.config as TypedTargetConfig; + if (config.kmp === false || !config.kmp) { return { kmp: false, }; } - if (!this.config.kmp.rootDistDirRegex) { + if (!config.kmp.rootDistDirRegex) { throw new ConfigurationError( - 'Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.' + 'Required root configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.', ); } - if (!this.config.kmp.appleDistDirRegex) { + if (!config.kmp.appleDistDirRegex) { throw new ConfigurationError( - 'Required apple configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.' + 'Required apple configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.', ); } - if (!this.config.kmp.klibDistDirRegex) { + if (!config.kmp.klibDistDirRegex) { throw new ConfigurationError( - 'Required klib configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.' + 'Required klib configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.', ); } return { kmp: { - appleDistDirRegex: stringToRegexp(this.config.kmp.appleDistDirRegex), - rootDistDirRegex: stringToRegexp(this.config.kmp.rootDistDirRegex), - klibDistDirRegex: stringToRegexp(this.config.kmp.klibDistDirRegex), + appleDistDirRegex: stringToRegexp(config.kmp.appleDistDirRegex), + rootDistDirRegex: stringToRegexp(config.kmp.rootDistDirRegex), + klibDistDirRegex: stringToRegexp(config.kmp.klibDistDirRegex), }, }; } private getAndroidSettings(): AndroidFields { - if (this.config.android === false) { + const config = this.config as TypedTargetConfig; + if (config.android === false) { return { android: false, }; } - if (!this.config.android) { + if (!config.android) { throw new ConfigurationError( 'Required Android configuration was not found in the configuration file. ' + - 'See the documentation for more details' + 'See the documentation for more details', ); } if ( - !this.config.android.distDirRegex || - !this.config.android.fileReplaceeRegex || - !this.config.android.fileReplacerStr + !config.android.distDirRegex || + !config.android.fileReplaceeRegex || + !config.android.fileReplacerStr ) { throw new ConfigurationError( - 'Required Android configuration is incorrect. See the documentation for more details.' + 'Required Android configuration is incorrect. See the documentation for more details.', ); } return { android: { - distDirRegex: stringToRegexp(this.config.android.distDirRegex), - fileReplaceeRegex: stringToRegexp( - this.config.android.fileReplaceeRegex - ), - fileReplacerStr: this.config.android.fileReplacerStr, + distDirRegex: stringToRegexp(config.android.distDirRegex), + fileReplaceeRegex: stringToRegexp(config.android.fileReplaceeRegex), + fileReplacerStr: config.android.fileReplacerStr, }, }; } @@ -229,7 +254,7 @@ export class MavenTarget extends BaseTarget { private checkRequiredSoftware(config: MavenTargetConfig): void { this.logger.debug( 'Checking if Maven CLI is available: ', - config.mavenCliPath + config.mavenCliPath, ); checkExecutableIsPresent(config.mavenCliPath); this.logger.debug('Checking if GPG is available'); @@ -276,9 +301,8 @@ export class MavenTarget extends BaseTarget { */ private async uploadArtifact(artifact: RemoteArtifact): Promise { this.logger.debug('Downloading:', artifact.filename); - const downloadedPkgPath = await this.artifactProvider.downloadArtifact( - artifact - ); + const downloadedPkgPath = + await this.artifactProvider.downloadArtifact(artifact); this.logger.debug(`Extracting ${artifact.filename}: `, downloadedPkgPath); await withTempDir(async dir => { @@ -307,7 +331,7 @@ export class MavenTarget extends BaseTarget { await this.uploadPomDistribution(distDir); } else { this.logger.warn( - `No BOM/POM file found in: ${distDir}, skipping directory` + `No BOM/POM file found in: ${distDir}, skipping directory`, ); } } @@ -358,7 +382,7 @@ export class MavenTarget extends BaseTarget { this.logger.warn( `Could not determine if path corresponds to a BOM file: ${pomFilepath}\n` + 'Error:\n', - error + error, ); return false; } @@ -380,27 +404,21 @@ export class MavenTarget extends BaseTarget { private async uploadKmpPomDistribution(distDir: string): Promise { if (this.mavenConfig.kmp !== false) { const moduleName = parse(distDir).base; - const isRootDistDir = this.mavenConfig.kmp.rootDistDirRegex.test( - moduleName - ); - const isAppleDistDir = this.mavenConfig.kmp.appleDistDirRegex.test( - moduleName - ); - const isKlibDistDir = this.mavenConfig.kmp.klibDistDirRegex.test( - moduleName - ); + const isRootDistDir = + this.mavenConfig.kmp.rootDistDirRegex.test(moduleName); + const isAppleDistDir = + this.mavenConfig.kmp.appleDistDirRegex.test(moduleName); + const isKlibDistDir = + this.mavenConfig.kmp.klibDistDirRegex.test(moduleName); const files = await this.getFilesForKmpMavenPomDist(distDir); const { targetFile, pomFile } = files; - const { - sideArtifacts, - classifiers, - types, - } = this.transformKmpSideArtifacts( - isRootDistDir, - isAppleDistDir, - isKlibDistDir, - files - ); + const { sideArtifacts, classifiers, types } = + this.transformKmpSideArtifacts( + isRootDistDir, + isAppleDistDir, + isKlibDistDir, + files, + ); await retrySpawnProcess(this.mavenConfig.mavenCliPath, [ 'gpg:sign-and-deploy-file', @@ -447,7 +465,7 @@ export class MavenTarget extends BaseTarget { isRootDistDir: boolean, isAppleDistDir: boolean, isKlibDistDir: boolean, - files: Record + files: Record, ): Record { const { javadocFile, @@ -469,7 +487,7 @@ export class MavenTarget extends BaseTarget { } else if (isAppleDistDir) { if (!Array.isArray(klibFiles)) { throw new ConfigurationError( - 'klib files in apple distributions must be an array' + 'klib files in apple distributions must be an array', ); } sideArtifacts += `,${klibFiles}`; @@ -493,7 +511,7 @@ export class MavenTarget extends BaseTarget { } else if (isKlibDistDir) { if (!Array.isArray(klibFiles) || klibFiles.length !== 1) { throw new ConfigurationError( - 'klib files in klib-only distributions must be an array with exactly one element' + 'klib files in klib-only distributions must be an array with exactly one element', ); } sideArtifacts += `,${klibFiles}`; @@ -525,7 +543,7 @@ export class MavenTarget extends BaseTarget { if (stat.isFile()) { return pomFilepath; } - } catch (e) { + } catch { // ignored } return undefined; @@ -540,7 +558,7 @@ export class MavenTarget extends BaseTarget { if (stat.isFile()) { return true; } - } catch (e) { + } catch { // ignored } return false; @@ -556,7 +574,7 @@ export class MavenTarget extends BaseTarget { */ public async fixModuleFileName( distDir: string, - moduleFile: string + moduleFile: string, ): Promise { const fallbackFile = join(distDir, 'module.json'); if ( @@ -571,13 +589,8 @@ export class MavenTarget extends BaseTarget { if (this.mavenConfig.kmp !== false) { await this.uploadKmpPomDistribution(distDir); } else { - const { - targetFile, - javadocFile, - sourcesFile, - pomFile, - moduleFile, - } = this.getFilesForMavenPomDist(distDir); + const { targetFile, javadocFile, sourcesFile, pomFile, moduleFile } = + this.getFilesForMavenPomDist(distDir); await this.fixModuleFileName(distDir, moduleFile); const hasModule = await this.fileExists(moduleFile); @@ -627,7 +640,7 @@ export class MavenTarget extends BaseTarget { * @returns record of required files. */ private async getFilesForKmpMavenPomDist( - distDir: string + distDir: string, ): Promise> { const files = this.getFilesForMavenPomDist(distDir) as Record< string, @@ -637,11 +650,8 @@ export class MavenTarget extends BaseTarget { const moduleName = parse(distDir).base; if (this.mavenConfig.kmp !== false) { - const { - klibDistDirRegex, - appleDistDirRegex, - rootDistDirRegex, - } = this.mavenConfig.kmp; + const { klibDistDirRegex, appleDistDirRegex, rootDistDirRegex } = + this.mavenConfig.kmp; const isRootDistDir = rootDistDirRegex.test(moduleName); const isAppleDistDir = appleDistDirRegex.test(moduleName); @@ -651,7 +661,7 @@ export class MavenTarget extends BaseTarget { files['allFile'] = join(distDir, `${moduleName}-all.jar`); files['kotlinToolingMetadataFile'] = join( distDir, - `${moduleName}-kotlin-tooling-metadata.json` + `${moduleName}-kotlin-tooling-metadata.json`, ); } else if (isAppleDistDir) { files['metadataFile'] = join(distDir, `${moduleName}-metadata.jar`); @@ -685,13 +695,12 @@ export class MavenTarget extends BaseTarget { const moduleName = parse(distDir).base; if (this.mavenConfig.android !== false) { - const isAndroidDistDir = this.mavenConfig.android.distDirRegex.test( - moduleName - ); + const isAndroidDistDir = + this.mavenConfig.android.distDirRegex.test(moduleName); if (isAndroidDistDir) { return moduleName.replace( this.mavenConfig.android.fileReplaceeRegex, - this.mavenConfig.android.fileReplacerStr + this.mavenConfig.android.fileReplacerStr, ); } } @@ -717,7 +726,7 @@ export class MavenTarget extends BaseTarget { if (state !== 'open') { throw new Error( - 'No open repositories available. Go to Nexus Repository Manager to see what happened.' + 'No open repositories available. Go to Nexus Repository Manager to see what happened.', ); } @@ -730,12 +739,12 @@ export class MavenTarget extends BaseTarget { `${NEXUS_API_BASE_URL}/manual/search/repositories`, { headers: this.getNexusRequestHeaders(), - } + }, ); if (!response.ok) { throw new Error( - `Unable to fetch repositories: ${response.status}, ${response.statusText}` + `Unable to fetch repositories: ${response.status}, ${response.statusText}`, ); } @@ -748,7 +757,7 @@ export class MavenTarget extends BaseTarget { if (repositories.length > 1) { throw new Error( - `There are more than 1 active repositories. Please close unwanted deployments.` + `There are more than 1 active repositories. Please close unwanted deployments.`, ); } @@ -780,12 +789,12 @@ export class MavenTarget extends BaseTarget { autoDropAfterRelease: true, }, }), - } + }, ); if (!response.ok) { throw new Error( - `Unable to close repository ${repositoryId}: ${response.status}, ${response.statusText}` + `Unable to close repository ${repositoryId}: ${response.status}, ${response.statusText}`, ); } @@ -808,7 +817,7 @@ export class MavenTarget extends BaseTarget { this.logger.info( `Nexus repository still not closed. Waiting for ${ NEXUS_RETRY_DELAY / 1000 - }s to try again.` + }s to try again.`, ); } } @@ -830,12 +839,12 @@ export class MavenTarget extends BaseTarget { autoDropAfterRelease: true, }, }), - } + }, ); if (!response.ok) { throw new Error( - `Unable to release repository ${repositoryId}: ${response.status}, ${response.statusText}` + `Unable to release repository ${repositoryId}: ${response.status}, ${response.statusText}`, ); } @@ -844,7 +853,7 @@ export class MavenTarget extends BaseTarget { while (true) { if (Date.now() - poolingStartTime > SONATYPE_RETRY_DEADLINE) { throw new Error( - 'Deadline for Central repository status change reached.' + 'Deadline for Central repository status change reached.', ); } @@ -856,7 +865,7 @@ export class MavenTarget extends BaseTarget { { method: 'POST', headers: this.getNexusRequestHeaders(), - } + }, ); if ( @@ -870,7 +879,7 @@ export class MavenTarget extends BaseTarget { this.logger.info( `Central repository still not published. Waiting for ${ CENTRAL_RETRY_DELAY / 1000 - }s to try again.` + }s to try again.`, ); } } @@ -881,7 +890,7 @@ export class MavenTarget extends BaseTarget { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Basic ${Buffer.from( - `${this.mavenConfig.OSSRH_USERNAME}:${this.mavenConfig.OSSRH_PASSWORD}` + `${this.mavenConfig.OSSRH_USERNAME}:${this.mavenConfig.OSSRH_PASSWORD}`, ).toString(`base64`)}`, }; } diff --git a/src/targets/npm.ts b/src/targets/npm.ts index e176f22f..f3e0f420 100644 --- a/src/targets/npm.ts +++ b/src/targets/npm.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import prompts from 'prompts'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { stringToRegexp } from '../utils/filters'; import { isDryRun } from '../utils/helpers'; @@ -102,6 +102,12 @@ export interface NpmTargetOptions { token: string; } +/** Fields on the npm target config accessed at runtime */ +interface NpmTargetConfigFields extends Record { + access?: NpmPackageAccess; + checkPackageName?: string; +} + /** Options for running the NPM publish command */ interface NpmPublishOptions { /** OTP value to use */ @@ -338,7 +344,7 @@ export class NpmTarget extends BaseTarget { private static async bumpWorkspacePackagesIndividually( bin: string, packages: { name: string; location: string }[], - newVersion: string, + _newVersion: string, baseArgs: string[], ): Promise { for (const pkg of packages) { @@ -429,16 +435,17 @@ export class NpmTarget extends BaseTarget { throw new Error('NPM target: NPM_TOKEN not found in the environment'); } + const config = this.config as TypedTargetConfig; const npmConfig: NpmTargetOptions = { useYarn: !!process.env.USE_YARN || !hasExecutable(NPM_BIN), token, }; - if (this.config.access) { - if (Object.values(NpmPackageAccess).includes(this.config.access)) { - npmConfig.access = this.config.access; + if (config.access) { + if (Object.values(NpmPackageAccess).includes(config.access)) { + npmConfig.access = config.access; } else { throw new ConfigurationError( - `Invalid value for "npm.access" option: ${this.config.access}`, + `Invalid value for "npm.access" option: ${config.access}`, ); } } @@ -531,9 +538,10 @@ export class NpmTarget extends BaseTarget { publishOptions.otp = await this.requestOtp(); } + const config = this.config as TypedTargetConfig; const tag = await getPublishTag( version, - this.config.checkPackageName, + config.checkPackageName, this.npmConfig, this.logger, publishOptions.otp, diff --git a/src/targets/nuget.ts b/src/targets/nuget.ts index 3eaea1b9..6cb5695a 100644 --- a/src/targets/nuget.ts +++ b/src/targets/nuget.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { join } from 'path'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { checkExecutableIsPresent, @@ -39,6 +39,11 @@ export interface NugetTargetOptions { serverUrl: string; } +/** Config fields for nuget target from .craft.yml */ +interface NugetYamlConfig extends Record { + serverUrl?: string; +} + /** * Target responsible for publishing releases on Nuget */ @@ -60,7 +65,7 @@ export class NugetTarget extends BaseTarget { */ public static async bumpVersion( rootDir: string, - newVersion: string + newVersion: string, ): Promise { // Check for .NET project files const csprojFiles = readdirSync(rootDir).filter(f => f.endsWith('.csproj')); @@ -78,17 +83,22 @@ export class NugetTarget extends BaseTarget { NUGET_DOTNET_BIN, ['setversion', newVersion], { cwd: rootDir }, - { enableInDryRunMode: true } + { enableInDryRunMode: true }, ); if (result !== null) { return true; } } catch (error) { const message = error instanceof Error ? error.message : String(error); - if (!message.includes('not installed') && !message.includes('Could not execute')) { + if ( + !message.includes('not installed') && + !message.includes('Could not execute') + ) { throw error; } - logger.debug('dotnet-setversion not available, falling back to manual edit'); + logger.debug( + 'dotnet-setversion not available, falling back to manual edit', + ); } } @@ -117,7 +127,10 @@ export class NugetTarget extends BaseTarget { /** * Update version in an XML project file (.csproj or Directory.Build.props) */ - private static updateVersionInXml(filePath: string, newVersion: string): boolean { + private static updateVersionInXml( + filePath: string, + newVersion: string, + ): boolean { const content = readFileSync(filePath, 'utf-8'); // Match x.y.z or x.y.z @@ -156,7 +169,7 @@ export class NugetTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); this.nugetConfig = this.getNugetConfig(); @@ -170,12 +183,13 @@ export class NugetTarget extends BaseTarget { if (!process.env.NUGET_API_TOKEN) { throw new ConfigurationError( `Cannot perform Nuget release: missing credentials. - Please use NUGET_API_TOKEN environment variable.` + Please use NUGET_API_TOKEN environment variable.`, ); } + const config = this.config as TypedTargetConfig; return { apiToken: process.env.NUGET_API_TOKEN, - serverUrl: this.config.serverUrl || DEFAULT_NUGET_SERVER_URL, + serverUrl: config.serverUrl || DEFAULT_NUGET_SERVER_URL, }; } @@ -221,7 +235,7 @@ export class NugetTarget extends BaseTarget { if (!packageFiles.length) { reportError( - 'Cannot release to Nuget: there are no Nuget packages found!' + 'Cannot release to Nuget: there are no Nuget packages found!', ); } @@ -235,7 +249,7 @@ export class NugetTarget extends BaseTarget { await spawnProcess( NUGET_DOTNET_BIN, ['nuget', '--version'], - DOTNET_SPAWN_OPTIONS + DOTNET_SPAWN_OPTIONS, ); await Promise.all( @@ -260,10 +274,10 @@ export class NugetTarget extends BaseTarget { `Uploading file "${file.filename}" via "dotnet nuget"` + (symbolFile ? `, including symbol file "${symbolFile.filename}"` - : '') + : ''), ); return this.uploadAsset(path); - }) + }), ); this.logger.info('Nuget release complete'); diff --git a/src/targets/powershell.ts b/src/targets/powershell.ts index 0b7b346a..f0875fde 100644 --- a/src/targets/powershell.ts +++ b/src/targets/powershell.ts @@ -1,6 +1,6 @@ import { join } from 'path'; import { BaseArtifactProvider } from '../artifact_providers/base'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { isDryRun } from '../utils/helpers'; @@ -28,6 +28,12 @@ export interface PowerShellTargetOptions { module: string; } +/** Config fields for powershell target from .craft.yml */ +interface PowerShellYamlConfig extends Record { + repository?: string; + module?: string; +} + /** * Target responsible for publishing modules to a PowerShell repository */ @@ -43,13 +49,14 @@ export class PowerShellTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); + const typedConfig = this.config as TypedTargetConfig; this.psConfig = { apiKey: process.env.POWERSHELL_API_KEY || '', - repository: this.config.repository || DEFAULT_POWERSHELL_REPOSITORY, - module: this.config.module || '', + repository: typedConfig.repository || DEFAULT_POWERSHELL_REPOSITORY, + module: typedConfig.module || '', }; checkExecutableIsPresent(POWERSHELL_BIN); } @@ -59,7 +66,7 @@ export class PowerShellTarget extends BaseTarget { */ private async spawnPwsh( command: string, - spawnProcessOptions: SpawnProcessOptions = this.defaultSpawnOptions + spawnProcessOptions: SpawnProcessOptions = this.defaultSpawnOptions, ): Promise { command = `$ErrorActionPreference = 'Stop'\n` + command; this.logger.trace('Executing PowerShell command:', command); @@ -67,7 +74,7 @@ export class PowerShellTarget extends BaseTarget { POWERSHELL_BIN, ['-Command', command], {}, - spawnProcessOptions + spawnProcessOptions, ); } @@ -89,7 +96,7 @@ export class PowerShellTarget extends BaseTarget { } if (missingConfigOptions.length > 0) { throw new ConfigurationError( - 'Missing project configuration parameter(s): ' + missingConfigOptions + 'Missing project configuration parameter(s): ' + missingConfigOptions, ); } } @@ -108,7 +115,7 @@ export class PowerShellTarget extends BaseTarget { POWERSHELL_BIN, ['--version'], {}, - this.defaultSpawnOptions + this.defaultSpawnOptions, ); // Also check the command and its its module version in case there are issues: @@ -123,7 +130,7 @@ export class PowerShellTarget extends BaseTarget { // Escape the given module artifact name to avoid regex issues. let moduleArtifactRegex = `${this.psConfig.module}`.replace( /[/\-\\^$*+?.()|[\]{}]/g, - '\\$&' + '\\$&', ); moduleArtifactRegex = `/^${moduleArtifactRegex}\\.zip$/`; @@ -133,11 +140,11 @@ export class PowerShellTarget extends BaseTarget { }); if (!packageFiles.length) { reportError( - `Cannot release the module to ${this.psConfig.repository}: there are no matching artifacts!` + `Cannot release the module to ${this.psConfig.repository}: there are no matching artifacts!`, ); } else if (packageFiles.length > 1) { reportError( - `Cannot release the module to ${this.psConfig.repository}: found multiple matching artifacts!` + `Cannot release the module to ${this.psConfig.repository}: found multiple matching artifacts!`, ); } const artifact = packageFiles[0]; @@ -155,7 +162,7 @@ export class PowerShellTarget extends BaseTarget { public async publishModule(moduleDir: string): Promise { this.logger.info( - `Publishing PowerShell module "${this.psConfig.module}" to ${this.psConfig.repository}` + `Publishing PowerShell module "${this.psConfig.module}" to ${this.psConfig.repository}`, ); await this.spawnPwsh(` Publish-Module -Path '${moduleDir}' \` diff --git a/src/targets/pubDev.ts b/src/targets/pubDev.ts index a4d59e8e..8786d5d0 100644 --- a/src/targets/pubDev.ts +++ b/src/targets/pubDev.ts @@ -1,11 +1,21 @@ -import { constants, existsSync, promises as fsPromises, readFileSync, writeFileSync } from 'fs'; +import { + constants, + existsSync, + promises as fsPromises, + readFileSync, + writeFileSync, +} from 'fs'; import { homedir, platform } from 'os'; import { join, dirname } from 'path'; import { load, dump } from 'js-yaml'; import { createGitClient } from '../utils/git'; import { BaseTarget } from './base'; import { BaseArtifactProvider } from '../artifact_providers/base'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { forEachChained } from '../utils/async'; import { checkEnvForPrerequisite } from '../utils/env'; import { withTempDir } from '../utils/files'; @@ -18,7 +28,14 @@ export const targetSecrets = [ 'PUBDEV_ACCESS_TOKEN', 'PUBDEV_REFRESH_TOKEN', ] as const; -type SecretsType = typeof targetSecrets[number]; +type SecretsType = (typeof targetSecrets)[number]; + +/** Fields on the pub-dev target config accessed at runtime */ +interface PubDevTargetConfigFields extends Record { + dartCliPath?: string; + packages?: Record; + skipValidation?: boolean; +} /** Target options for "brew" */ export interface PubDevTargetOptions { @@ -63,7 +80,7 @@ export class PubDevTarget extends BaseTarget { */ public static async bumpVersion( rootDir: string, - newVersion: string + newVersion: string, ): Promise { const pubspecPath = join(rootDir, 'pubspec.yaml'); if (!existsSync(pubspecPath)) { @@ -89,7 +106,7 @@ export class PubDevTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); this.pubDevConfig = this.getPubDevConfig(); @@ -106,13 +123,15 @@ export class PubDevTarget extends BaseTarget { private getPubDevConfig(): PubDevTargetConfig { // We could do `...this.config`, but `packages` is in a list, not array format in `.yml` // so I wanted to keep setting the defaults unified. - const config = { - dartCliPath: this.config.dartCliPath || 'dart', - packages: this.config.packages - ? Object.keys(this.config.packages) + const targetConfig = this + .config as TypedTargetConfig; + const config: PubDevTargetConfig = { + dartCliPath: targetConfig.dartCliPath || 'dart', + packages: targetConfig.packages + ? Object.keys(targetConfig.packages) : ['.'], ...this.getTargetSecrets(), - skipValidation: this.config.skipValidation ?? false, + skipValidation: targetConfig.skipValidation ?? false, }; this.checkRequiredSoftware(config); @@ -145,7 +164,7 @@ export class PubDevTarget extends BaseTarget { private checkRequiredSoftware(config: PubDevTargetConfig): void { this.logger.debug( 'Checking if Dart CLI is available: ', - config.dartCliPath + config.dartCliPath, ); checkExecutableIsPresent(config.dartCliPath); } @@ -168,11 +187,11 @@ export class PubDevTarget extends BaseTarget { async directory => { await this.cloneRepository(this.githubRepo, revision, directory); await forEachChained(this.pubDevConfig.packages, async pkg => - this.publishPackage(directory, pkg) + this.publishPackage(directory, pkg), ); }, true, - 'craft-pub-dev-' + 'craft-pub-dev-', ); } @@ -220,7 +239,7 @@ export class PubDevTarget extends BaseTarget { public async cloneRepository( config: GitHubGlobalConfig, revision: string, - directory: string + directory: string, ): Promise { const { owner, repo } = config; const git = createGitClient(directory); @@ -251,7 +270,7 @@ export class PubDevTarget extends BaseTarget { await this.removeDependencyOverrides(directory, pkg); } catch (e) { throw new Error( - `Cannot remove dependency_overrides key from pubspec.yaml: ${e}` + `Cannot remove dependency_overrides key from pubspec.yaml: ${e}`, ); } } @@ -269,16 +288,16 @@ export class PubDevTarget extends BaseTarget { // Dart stops the process and asks user to go to provided url for authorization. // We want the stdout to be visible just in case something goes wrong, otherwise // the process will hang with no clear reason why. - { showStdout: true } + { showStdout: true }, ); this.logger.info( - `Package release complete${pkg !== '.' ? `: ${pkg}` : '.'}` + `Package release complete${pkg !== '.' ? `: ${pkg}` : '.'}`, ); } private async removeDependencyOverrides( directory: string, - pkg: string + pkg: string, ): Promise { const pubSpecPath = join(directory, pkg, 'pubspec.yaml'); const pubSpecContent = await fsPromises.readFile(pubSpecPath, 'utf8'); diff --git a/src/targets/registry.ts b/src/targets/registry.ts index fd2de23a..af92f189 100644 --- a/src/targets/registry.ts +++ b/src/targets/registry.ts @@ -2,7 +2,11 @@ import { mapLimit } from 'async'; import { Octokit } from '@octokit/rest'; import type { SimpleGit } from 'simple-git'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { @@ -34,6 +38,19 @@ import { import { cloneRepo } from '../utils/git'; import { filterAsync, withRetry } from '../utils/async'; +/** Fields on the registry target config accessed at runtime */ +interface RegistryTargetConfigFields extends Record { + remote?: string; + type?: RegistryPackageType; + urlTemplate?: string; + config?: { canonical?: string }; + linkPrereleases?: boolean; + checksums?: unknown[]; + onlyIfPresent?: string; + sdks?: Record; + apps?: Record; +} + /** "registry" target options */ export interface RegistryConfig { /** Type of the registry package */ @@ -93,10 +110,12 @@ export class RegistryTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); - const remote = this.config.remote; + const typedConfig = this + .config as TypedTargetConfig; + const remote = typedConfig.remote; if (remote) { const [owner, repo] = remote.split('/', 2); this.remote = new GitHubRemote(owner, repo); @@ -112,8 +131,14 @@ export class RegistryTarget extends BaseTarget { * Extracts Registry target options from the raw configuration. */ public getRegistryConfig(): RegistryConfig[] { + const typedConfig = this + .config as TypedTargetConfig; const items = Object.entries(BATCH_KEYS).flatMap(([key, type]) => - Object.entries(this.config[key] || {}).map(([canonicalName, conf]) => { + Object.entries( + (typedConfig[key as keyof RegistryTargetConfigFields] as + | Record + | undefined) || {}, + ).map(([canonicalName, conf]) => { const config = conf as RegistryConfig | null; const result = Object.assign(Object.create(null), config, { type, @@ -125,12 +150,12 @@ export class RegistryTarget extends BaseTarget { } return result; - }) + }), ); - if (items.length === 0 && this.config.type) { + if (items.length === 0 && typedConfig.type) { this.logger.warn( - 'You are using a deprecated registry target config, please update.' + 'You are using a deprecated registry target config, please update.', ); return [this.getLegacyRegistryConfig()]; } else { @@ -139,48 +164,51 @@ export class RegistryTarget extends BaseTarget { } private getLegacyRegistryConfig(): RegistryConfig { - const registryType = this.config.type; + const typedConfig = this + .config as TypedTargetConfig; + const registryType = typedConfig.type; if ( + !registryType || [RegistryPackageType.APP, RegistryPackageType.SDK].indexOf( - registryType + registryType, ) === -1 ) { throw new ConfigurationError( - `Invalid registry type specified: "${registryType}"` + `Invalid registry type specified: "${registryType}"`, ); } - let urlTemplate; + let urlTemplate: string | undefined; if (registryType === RegistryPackageType.APP) { - urlTemplate = this.config.urlTemplate; + urlTemplate = typedConfig.urlTemplate; if (urlTemplate && typeof urlTemplate !== 'string') { throw new ConfigurationError( - `Invalid "urlTemplate" specified: ${urlTemplate}` + `Invalid "urlTemplate" specified: ${urlTemplate}`, ); } } - const releaseConfig = this.config.config; + const releaseConfig = typedConfig.config; if (!releaseConfig) { throw new ConfigurationError( - 'Cannot find configuration dictionary for release registry' + 'Cannot find configuration dictionary for release registry', ); } const canonicalName = releaseConfig.canonical; if (!canonicalName) { throw new ConfigurationError( - 'Canonical name not found in the configuration' + 'Canonical name not found in the configuration', ); } - const linkPrereleases = this.config.linkPrereleases || false; + const linkPrereleases = typedConfig.linkPrereleases || false; if (typeof linkPrereleases !== 'boolean') { throw new ConfigurationError('Invlaid type of "linkPrereleases"'); } - const checksums = castChecksums(this.config.checksums); + const checksums = castChecksums(typedConfig.checksums as unknown[]); - const onlyIfPresentStr = this.config.onlyIfPresent || undefined; + const onlyIfPresentStr = typedConfig.onlyIfPresent || undefined; let onlyIfPresent; if (onlyIfPresentStr) { if (typeof onlyIfPresentStr !== 'string') { @@ -214,7 +242,7 @@ export class RegistryTarget extends BaseTarget { registryConfig: RegistryConfig, manifest: { [key: string]: any }, version: string, - revision: string + revision: string, ): Promise { if (!registryConfig.urlTemplate) { return; @@ -223,7 +251,7 @@ export class RegistryTarget extends BaseTarget { const artifacts = await this.getArtifactsForRevision(revision); if (artifacts.length === 0) { this.logger.warn( - 'No artifacts found, not adding any links to the manifest' + 'No artifacts found, not adding any links to the manifest', ); return; } @@ -236,11 +264,11 @@ export class RegistryTarget extends BaseTarget { file: artifact.filename, revision, version, - } + }, ); } this.logger.debug( - `Writing file urls to the manifest, files found: ${artifacts.length}` + `Writing file urls to the manifest, files found: ${artifacts.length}`, ); manifest.file_urls = fileUrls; } @@ -264,7 +292,7 @@ export class RegistryTarget extends BaseTarget { registryConfig: RegistryConfig, artifact: RemoteArtifact, version: string, - revision: string + revision: string, ): Promise { const artifactData: ArtifactData = {}; @@ -280,7 +308,7 @@ export class RegistryTarget extends BaseTarget { artifactData.checksums = await getArtifactChecksums( registryConfig.checksums, artifact, - this.artifactProvider + this.artifactProvider, ); } @@ -305,7 +333,7 @@ export class RegistryTarget extends BaseTarget { registryConfig: RegistryConfig, packageManifest: { [key: string]: any }, version: string, - revision: string + revision: string, ): Promise { // Clear existing data delete packageManifest.files; @@ -315,7 +343,7 @@ export class RegistryTarget extends BaseTarget { !(registryConfig.checksums && registryConfig.checksums.length > 0) ) { this.logger.warn( - 'No URL template or checksums, not adding any file data' + 'No URL template or checksums, not adding any file data', ); return; } @@ -327,19 +355,23 @@ export class RegistryTarget extends BaseTarget { } this.logger.info( - 'Adding extra data (checksums, download links) for available artifacts...' + 'Adding extra data (checksums, download links) for available artifacts...', ); const files: { [key: string]: any } = {}; - await mapLimit(artifacts, MAX_DOWNLOAD_CONCURRENCY, async (artifact: RemoteArtifact) => { - const fileData = await this.getArtifactData( - registryConfig, - artifact, - version, - revision - ); - files[artifact.filename] = fileData; - }); + await mapLimit( + artifacts, + MAX_DOWNLOAD_CONCURRENCY, + async (artifact: RemoteArtifact) => { + const fileData = await this.getArtifactData( + registryConfig, + artifact, + version, + revision, + ); + files[artifact.filename] = fileData; + }, + ); packageManifest.files = files; } @@ -360,13 +392,13 @@ export class RegistryTarget extends BaseTarget { packageManifest: { [key: string]: any }, canonical: string, version: string, - revision: string + revision: string, ): Promise { // Additional check if (canonical !== packageManifest.canonical) { reportError( `Canonical name in "craft" config ("${canonical}") is inconsistent with ` + - `the one in package manifest ("${packageManifest.canonical}")` + `the one in package manifest ("${packageManifest.canonical}")`, ); } // Update the manifest @@ -403,7 +435,7 @@ export class RegistryTarget extends BaseTarget { registryConfig, updatedManifest, version, - revision + revision, ); } @@ -420,7 +452,7 @@ export class RegistryTarget extends BaseTarget { * @returns The initial manifest data */ private buildInitialManifestData( - registryConfig: RegistryConfig + registryConfig: RegistryConfig, ): InitialManifestData { const { owner, repo } = this.githubRepo; return { @@ -444,7 +476,7 @@ export class RegistryTarget extends BaseTarget { registryConfig: RegistryConfig, localRepo: LocalRegistry, version: string, - revision: string + revision: string, ): Promise { const canonicalName = registryConfig.canonicalName; const initialManifestData = this.buildInitialManifestData(registryConfig); @@ -453,7 +485,7 @@ export class RegistryTarget extends BaseTarget { registryConfig.type, canonicalName, version, - initialManifestData + initialManifestData, ); const newManifest = await this.getUpdatedManifest( @@ -461,14 +493,14 @@ export class RegistryTarget extends BaseTarget { packageManifest, canonicalName, version, - revision + revision, ); await updateManifestSymlinks( newManifest, version, versionFilePath, - packageManifest.version || undefined + packageManifest.version || undefined, ); } @@ -477,7 +509,7 @@ export class RegistryTarget extends BaseTarget { remote.setAuth(getGitHubApiToken()); this.logger.info( - `Cloning "${remote.getRemoteString()}" to "${directory}"...` + `Cloning "${remote.getRemoteString()}" to "${directory}"...`, ); return cloneRepo(remote.getRemoteStringWithAuth(), directory, [ '--filter=tree:0', @@ -487,12 +519,12 @@ export class RegistryTarget extends BaseTarget { public async getValidItems( version: string, - revision: string + revision: string, ): Promise { return filterAsync(this.registryConfig, async registryConfig => { if (!registryConfig.linkPrereleases && isPreviewRelease(version)) { this.logger.info( - `Preview release detected, skipping ${registryConfig.canonicalName}` + `Preview release detected, skipping ${registryConfig.canonicalName}`, ); return false; } @@ -500,15 +532,13 @@ export class RegistryTarget extends BaseTarget { // If we have onlyIfPresent specified, check that we have any of matched files const onlyIfPresentPattern = registryConfig.onlyIfPresent; if (onlyIfPresentPattern) { - const artifacts = await this.artifactProvider.filterArtifactsForRevision( - revision, - { + const artifacts = + await this.artifactProvider.filterArtifactsForRevision(revision, { includeNames: onlyIfPresentPattern, - } - ); + }); if (artifacts.length === 0) { this.logger.warn( - `No files found that match "${onlyIfPresentPattern.toString()}", skipping the target.` + `No files found that match "${onlyIfPresentPattern.toString()}", skipping the target.`, ); return false; } @@ -540,26 +570,26 @@ export class RegistryTarget extends BaseTarget { registryConfig, localRepo, version, - revision - ) - ) + revision, + ), + ), ); await localRepo.git .add(['.']) .commit( - `craft: release "${this.githubRepo.repo}", version "${version}"` + `craft: release "${this.githubRepo.repo}", version "${version}"`, ); this.logger.info(`Pushing the changes...`); // Ensure we are still up to date with upstream await withRetry(() => localRepo.git .pull('origin', 'master', ['--rebase']) - .push('origin', 'master') + .push('origin', 'master'), ); }, true, - 'craft-release-registry-' + 'craft-release-registry-', ); this.logger.info('Release registry updated.'); diff --git a/src/targets/sentryPypi.ts b/src/targets/sentryPypi.ts index dd0bafda..725092e3 100644 --- a/src/targets/sentryPypi.ts +++ b/src/targets/sentryPypi.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import { Octokit } from '@octokit/rest'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { BaseArtifactProvider } from '../artifact_providers/base'; import { withTempDir } from '../utils/files'; import { ConfigurationError, reportError } from '../utils/errors'; @@ -14,6 +14,11 @@ import { getGitHubClient } from '../utils/githubApi'; */ export const WHEEL_REGEX = /^([^-]+)-([^-]+)-[^-]+-[^-]+-[^-]+\.whl$/; +/** Config fields for sentry-pypi target from .craft.yml */ +interface SentryPypiYamlConfig extends Record { + internalPypiRepo?: string; +} + export function uniquePackages(filenames: Array): Array { const versions = filenames.map(filename => { const match = WHEEL_REGEX.exec(filename) as RegExpExecArray; @@ -33,13 +38,13 @@ export class SentryPypiTarget extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); if (!('internalPypiRepo' in this.config)) { throw new ConfigurationError( - 'Missing project configuration parameter: internalPypiRepo' + 'Missing project configuration parameter: internalPypiRepo', ); } @@ -65,7 +70,8 @@ export class SentryPypiTarget extends BaseTarget { const versions = uniquePackages(packageFiles.map(f => f.filename)); - const [owner, repo] = this.config.internalPypiRepo.split('/'); + const typedConfig = this.config as TypedTargetConfig; + const [owner, repo] = typedConfig.internalPypiRepo!.split('/'); const [contents, tree, commit] = await withTempDir(async directory => { await spawnProcess( @@ -74,37 +80,41 @@ export class SentryPypiTarget extends BaseTarget { 'clone', '--quiet', '--depth=1', - `https://github.com/${this.config.internalPypiRepo}`, + `https://github.com/${typedConfig.internalPypiRepo}`, directory, ], {}, - { enableInDryRunMode: true } + { enableInDryRunMode: true }, ); await spawnProcess( 'python3', ['-m', 'add_pkg', '--skip-resolve', ...versions], { cwd: directory }, - { enableInDryRunMode: true } + { enableInDryRunMode: true }, ); const contents = fs.readFileSync(path.join(directory, 'packages.ini'), { encoding: 'utf-8', }); - const tree = ((await spawnProcess( - 'git', - ['-C', directory, 'rev-parse', 'HEAD:'], - {}, - { enableInDryRunMode: true } - )) as Buffer) + const tree = ( + (await spawnProcess( + 'git', + ['-C', directory, 'rev-parse', 'HEAD:'], + {}, + { enableInDryRunMode: true }, + )) as Buffer + ) .toString('utf-8') .trim(); - const commit = ((await spawnProcess( - 'git', - ['-C', directory, 'rev-parse', 'HEAD'], - {}, - { enableInDryRunMode: true } - )) as Buffer) + const commit = ( + (await spawnProcess( + 'git', + ['-C', directory, 'rev-parse', 'HEAD'], + {}, + { enableInDryRunMode: true }, + )) as Buffer + ) .toString('utf-8') .trim(); return [contents, tree, commit]; diff --git a/src/targets/symbolCollector.ts b/src/targets/symbolCollector.ts index 5a084f54..e76631b9 100644 --- a/src/targets/symbolCollector.ts +++ b/src/targets/symbolCollector.ts @@ -1,5 +1,5 @@ import { BaseArtifactProvider } from '../artifact_providers/base'; -import { TargetConfig } from '../schemas/project_config'; +import { TargetConfig, TypedTargetConfig } from '../schemas/project_config'; import { ConfigurationError } from '../utils/errors'; import { BaseTarget } from './base'; import { withTempDir } from '../utils/files'; @@ -25,6 +25,13 @@ interface SymbolCollectorTargetConfig { bundleIdPrefix: string; } +/** Config fields for symbol-collector target from .craft.yml */ +interface SymbolCollectorYamlConfig extends Record { + serverEndpoint?: string; + batchType?: string; + bundleIdPrefix?: string; +} + export class SymbolCollector extends BaseTarget { /** Target name */ public readonly name: string = 'symbol-collector'; @@ -33,7 +40,7 @@ export class SymbolCollector extends BaseTarget { public constructor( config: TargetConfig, - artifactProvider: BaseArtifactProvider + artifactProvider: BaseArtifactProvider, ) { super(config, artifactProvider); this.symbolCollectorConfig = this.getSymbolCollectorConfig(); @@ -43,24 +50,25 @@ export class SymbolCollector extends BaseTarget { // The Symbol Collector should be available in the path checkExecutableIsPresent(SYM_COLLECTOR_BIN_NAME); - if (!this.config.batchType) { + const config = this.config as TypedTargetConfig; + if (!config.batchType) { throw new ConfigurationError( 'The required `batchType` parameter is missing in the configuration file. ' + - 'See the documentation for more details.' + 'See the documentation for more details.', ); } - if (!this.config.bundleIdPrefix) { + if (!config.bundleIdPrefix) { throw new ConfigurationError( 'The required `bundleIdPrefix` parameter is missing in the configuration file. ' + - 'See the documentation for more details.' + 'See the documentation for more details.', ); } return { serverEndpoint: - this.config.serverEndpoint || DEFAULT_SYM_COLLECTOR_SERVER_ENDPOINT, - batchType: this.config.batchType, - bundleIdPrefix: this.config.bundleIdPrefix, + config.serverEndpoint || DEFAULT_SYM_COLLECTOR_SERVER_ENDPOINT, + batchType: config.batchType, + bundleIdPrefix: config.bundleIdPrefix, }; } @@ -89,7 +97,7 @@ export class SymbolCollector extends BaseTarget { const subdirPath = join(dir, String(index)); await fsPromises.mkdir(subdirPath); await this.artifactProvider.downloadArtifact(artifact, subdirPath); - }) + }), ); const cmdOutput = await spawnProcess(SYM_COLLECTOR_BIN_NAME, [ diff --git a/src/targets/upm.ts b/src/targets/upm.ts index 8609a5a2..8a64fbd7 100644 --- a/src/targets/upm.ts +++ b/src/targets/upm.ts @@ -6,7 +6,11 @@ import { } from '../utils/githubApi'; import { GitHubTarget } from './github'; -import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config'; +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; import { BaseTarget } from './base'; import { BaseArtifactProvider, @@ -15,13 +19,19 @@ import { import { reportError } from '../utils/errors'; import { extractZipArchive } from '../utils/system'; import { withTempDir } from '../utils/files'; -import { cloneRepo, createGitClient } from '../utils/git'; +import { cloneRepo } from '../utils/git'; import { isPreviewRelease } from '../utils/version'; import { NoneArtifactProvider } from '../artifact_providers/none'; /** Name of the artifact that contains the UPM package */ export const ARTIFACT_NAME = 'package-release.zip'; +/** Config fields for upm target from .craft.yml */ +interface UpmYamlConfig extends Record { + releaseRepoOwner?: string; + releaseRepoName?: string; +} + /** * Target responsible for publishing to upm registry */ @@ -36,7 +46,7 @@ export class UpmTarget extends BaseTarget { public constructor( config: TargetConfig, artifactProvider: BaseArtifactProvider, - githubRepo: GitHubGlobalConfig + githubRepo: GitHubGlobalConfig, ) { super(config, artifactProvider, githubRepo); @@ -52,7 +62,7 @@ export class UpmTarget extends BaseTarget { this.githubTarget = new GitHubTarget( githubTargetConfig, new NoneArtifactProvider(), - githubRepo + githubRepo, ); } @@ -65,7 +75,7 @@ export class UpmTarget extends BaseTarget { * throws an exception in "normal" mode. */ public async fetchArtifact( - revision: string + revision: string, ): Promise { const packageFiles = await this.getArtifactsForRevision(revision); if (packageFiles.length === 0) { @@ -74,11 +84,11 @@ export class UpmTarget extends BaseTarget { } const packageFile = packageFiles.find( - ({ filename }) => filename === ARTIFACT_NAME + ({ filename }) => filename === ARTIFACT_NAME, ); if (packageFile === undefined) { reportError( - `Cannot publish UPM: Failed to find "${ARTIFACT_NAME}" in the artifacts.` + `Cannot publish UPM: Failed to find "${ARTIFACT_NAME}" in the artifacts.`, ); } @@ -99,23 +109,26 @@ export class UpmTarget extends BaseTarget { } this.logger.info( - `Found artifact: "${packageFile.filename}", downloading...` - ); - const artifactPath = await this.artifactProvider.downloadArtifact( - packageFile + `Found artifact: "${packageFile.filename}", downloading...`, ); + const artifactPath = + await this.artifactProvider.downloadArtifact(packageFile); + const typedConfig = this.config as TypedTargetConfig; const remote = new GitHubRemote( - this.config.releaseRepoOwner, - this.config.releaseRepoName, - getGitHubApiToken() + typedConfig.releaseRepoOwner!, + typedConfig.releaseRepoName!, + getGitHubApiToken(), ); const remoteAddr = remote.getRemoteString(); this.logger.debug(`Target release repository: ${remoteAddr}`); await withTempDir( async directory => { - const git = await cloneRepo(remote.getRemoteStringWithAuth(), directory); + const git = await cloneRepo( + remote.getRemoteStringWithAuth(), + directory, + ); this.logger.info('Clearing the repository.'); await git.rm(['-r', '-f', '.']); @@ -128,7 +141,7 @@ export class UpmTarget extends BaseTarget { const commitResult = await git.commit(`release ${version}`); if (!commitResult.commit) { throw new Error( - 'Commit on target repository failed. Maybe there were no changes at all?' + 'Commit on target repository failed. Maybe there were no changes at all?', ); } const targetRevision = await git.revparse([commitResult.commit]); @@ -139,7 +152,7 @@ export class UpmTarget extends BaseTarget { const draftRelease = await this.githubTarget.createDraftRelease( version, targetRevision, - changes + changes, ); try { await this.githubTarget.publishRelease(draftRelease, { @@ -150,18 +163,18 @@ export class UpmTarget extends BaseTarget { try { await this.githubTarget.deleteRelease(draftRelease); this.logger.info( - `Deleted orphaned draft release: ${draftRelease.tag_name}` + `Deleted orphaned draft release: ${draftRelease.tag_name}`, ); } catch (deleteError) { this.logger.warn( - `Failed to delete orphaned draft release: ${deleteError}` + `Failed to delete orphaned draft release: ${deleteError}`, ); } throw error; } }, true, - '_craft-release-upm-' + '_craft-release-upm-', ); this.logger.info('UPM release complete'); diff --git a/src/types/mustache.d.ts b/src/types/mustache.d.ts index b1fb5386..f8330d7a 100644 --- a/src/types/mustache.d.ts +++ b/src/types/mustache.d.ts @@ -79,7 +79,7 @@ declare module 'mustache' { template: string, view: any | MustacheContext, partials?: any, - tags?: string[] + tags?: string[], ): string; /** @@ -102,14 +102,14 @@ declare module 'mustache' { template: string, view: any | MustacheContext, partials?: any, - send?: any + send?: any, ): any; } /** * A simple string scanner that is used by the template parser to find tokens in template strings. */ - class MustacheScanner { + export class MustacheScanner { string: string; tail: string; pos: number; @@ -150,7 +150,7 @@ declare module 'mustache' { /** * Represents a rendering context by wrapping a view object and maintaining a reference to the parent context. */ - class MustacheContext { + export class MustacheContext { view: any; parentContext: MustacheContext; diff --git a/src/utils/__tests__/async.test.ts b/src/utils/__tests__/async.test.ts index 844f27e2..80b053c1 100644 --- a/src/utils/__tests__/async.test.ts +++ b/src/utils/__tests__/async.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { setGlobals } from '../../utils/helpers'; import { filterAsync, forEachChained, withRetry, sleep } from '../async'; import { logger } from '../../logger'; @@ -8,7 +8,7 @@ vi.mock('../../logger'); import { retrySpawnProcess } from '../async'; import { spawnProcess } from '../system'; -vi.mock('../system', async (importOriginal) => { +vi.mock('../system', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -46,7 +46,7 @@ describe('retrySpawnProcess', () => { maxRetries: numRetries, retryDelay: delay, retryExpFactor: expFactor, - } + }, ); } catch (error) { const endTime = new Date().getTime(); @@ -81,7 +81,7 @@ describe('filterAsync', () => { const predicate = (i: number) => new Promise(resolve => - setTimeout(() => resolve(i > 2), i * 100) + setTimeout(() => resolve(i > 2), i * 100), ); const filtered = await filterAsync([1, 2, 3, 4], predicate); expect(filtered).toEqual([3, 4]); @@ -106,7 +106,7 @@ describe('filterAsync', () => { function predicate(): any { expect(this).toBe(that); }, - that + that, ); }); }); @@ -133,7 +133,7 @@ describe('forEachChained', () => { const arr = [500, 300, 100]; fun.mockImplementation( - timeout => new Promise(resolve => setTimeout(resolve, timeout)) + timeout => new Promise(resolve => setTimeout(resolve, timeout)), ); await forEachChained(arr, fun); @@ -153,7 +153,7 @@ describe('forEachChained', () => { function action(): void { expect(this).toBe(that); }, - that + that, ); }); @@ -181,7 +181,7 @@ describe('forEachChained', () => { } async function regularModeExpectCheck( - iteratee: (entry: string) => string | Promise + iteratee: (entry: string) => string | Promise, ): Promise { expect.assertions(3); @@ -189,29 +189,29 @@ describe('forEachChained', () => { // problematic entry await expect(forEachChained(arr, iteratee)).rejects.toThrowError('drat'); expect(logger.debug).toHaveBeenCalledWith( - 'Processing array entry `second`' + 'Processing array entry `second`', ); // we didn't get this far expect(logger.debug).not.toHaveBeenCalledWith( - 'Processing array entry `third`' + 'Processing array entry `third`', ); } async function dryrunModeExpectCheck( - iteratee: (entry: string) => string | Promise + iteratee: (entry: string) => string | Promise, ): Promise { expect.assertions(3); // check that it logs the error rather than throws it await expect(forEachChained(arr, iteratee)).resolves.not.toThrowError(); expect(logger.error).toHaveBeenCalledWith( - expect.stringContaining('drat') + expect.stringContaining('drat'), ); // check that it's gotten all the way through the array expect(logger.debug).toHaveBeenCalledWith( - 'Processing array entry `fourth`' + 'Processing array entry `fourth`', ); } @@ -257,7 +257,7 @@ describe('withRetry', () => { throw new Error('I always fail'); }; await expect(withRetry(fn)).rejects.toThrowErrorMatchingInlineSnapshot( - `[RetryError: Max retries reached: 3]` + `[RetryError: Max retries reached: 3]`, ); }); @@ -283,8 +283,10 @@ describe('withRetry', () => { return 'success'; }; await expect( - withRetry(fn, 5, () => Promise.resolve(false)) - ).rejects.toThrowErrorMatchingInlineSnapshot(`[RetryError: Cancelled retry]`); + withRetry(fn, 5, () => Promise.resolve(false)), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[RetryError: Cancelled retry]`, + ); }); test('fails when onRetry throws', async () => { @@ -297,7 +299,9 @@ describe('withRetry', () => { return 'success'; }; await expect( - withRetry(fn, 5, () => Promise.reject(new Error('no retries'))) - ).rejects.toThrowErrorMatchingInlineSnapshot(`[RetryError: Cancelled retry]`); + withRetry(fn, 5, () => Promise.reject(new Error('no retries'))), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[RetryError: Cancelled retry]`, + ); }); }); diff --git a/src/utils/__tests__/autoVersion.test.ts b/src/utils/__tests__/autoVersion.test.ts index 204f92de..4d531f77 100644 --- a/src/utils/__tests__/autoVersion.test.ts +++ b/src/utils/__tests__/autoVersion.test.ts @@ -1,16 +1,15 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; - +import { vi, type Mock, type MockedFunction } from 'vitest'; vi.mock('../githubApi.ts'); vi.mock('../git'); -vi.mock('fs', async (importOriginal) => { +vi.mock('fs', async importOriginal => { const actual = await importOriginal(); return { ...actual, readFileSync: vi.fn(), }; }); -vi.mock('../../config', async (importOriginal) => { +vi.mock('../../config', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -39,9 +38,7 @@ const getGlobalGitHubConfigMock = config.getGlobalGitHubConfig as MockedFunction< typeof config.getGlobalGitHubConfig >; -const readFileSyncMock = readFileSync as MockedFunction< - typeof readFileSync ->; +const readFileSyncMock = readFileSync as MockedFunction; const getChangesSinceMock = getChangesSince as MockedFunction< typeof getChangesSince >; @@ -83,7 +80,7 @@ describe('validateBumpType', () => { }; expect(() => validateBumpType(result)).toThrow( - 'Cannot determine version automatically: no commits found since the last release.' + 'Cannot determine version automatically: no commits found since the last release.', ); }); @@ -96,7 +93,7 @@ describe('validateBumpType', () => { }; expect(() => validateBumpType(result)).toThrow( - 'Cannot determine version automatically' + 'Cannot determine version automatically', ); }); diff --git a/src/utils/__tests__/awsLambdaLayerManager.test.ts b/src/utils/__tests__/awsLambdaLayerManager.test.ts index e8b7c2db..1734e676 100644 --- a/src/utils/__tests__/awsLambdaLayerManager.test.ts +++ b/src/utils/__tests__/awsLambdaLayerManager.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import * as awsManager from '../awsLambdaLayerManager'; vi.mock('../../logger'); @@ -32,7 +32,7 @@ function getTestAwsLambdaLayerManager(): awsManager.AwsLambdaLayerManager { 'test license', Buffer.alloc(0), AWS_TEST_REGIONS, - '0.0.0' + '0.0.0', ); } diff --git a/src/utils/__tests__/calver.test.ts b/src/utils/__tests__/calver.test.ts index eb57fa12..ea5244e9 100644 --- a/src/utils/__tests__/calver.test.ts +++ b/src/utils/__tests__/calver.test.ts @@ -1,5 +1,9 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; -import { formatCalVerDate, calculateCalVer, DEFAULT_CALVER_CONFIG } from '../calver'; +import { vi, type Mock } from 'vitest'; +import { + formatCalVerDate, + calculateCalVer, + DEFAULT_CALVER_CONFIG, +} from '../calver'; // Mock the config module to control tagPrefix vi.mock('../../config', () => ({ @@ -154,7 +158,9 @@ describe('calculateCalVer', () => { }); it('handles non-numeric patch suffixes gracefully', async () => { - mockGit.tags.mockResolvedValue({ all: ['24.12.0', '24.12.beta', '24.12.1'] }); + mockGit.tags.mockResolvedValue({ + all: ['24.12.0', '24.12.beta', '24.12.1'], + }); const version = await calculateCalVer(mockGit as any, { offset: 0, @@ -186,7 +192,9 @@ describe('calculateCalVer', () => { it('ignores tags without the configured prefix', async () => { mockGetGitTagPrefix.mockReturnValue('v'); // Mix of prefixed and non-prefixed tags - mockGit.tags.mockResolvedValue({ all: ['24.12.5', 'v24.12.0', 'v24.12.1'] }); + mockGit.tags.mockResolvedValue({ + all: ['24.12.5', 'v24.12.0', 'v24.12.1'], + }); const version = await calculateCalVer(mockGit as any, { offset: 0, diff --git a/src/utils/__tests__/changelog-extract.test.ts b/src/utils/__tests__/changelog-extract.test.ts index d7aba9d0..9eefebe9 100644 --- a/src/utils/__tests__/changelog-extract.test.ts +++ b/src/utils/__tests__/changelog-extract.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; /** * Tests for changelog extraction and parsing functions. * - extractScope: Extracts scope from conventional commit titles @@ -6,7 +5,11 @@ import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } fr * - extractChangelogEntry: Extracts custom changelog entries from PR bodies */ -import { extractScope, formatScopeTitle, extractChangelogEntry } from '../changelog'; +import { + extractScope, + formatScopeTitle, + extractChangelogEntry, +} from '../changelog'; describe('extractScope', () => { it.each([ @@ -157,7 +160,8 @@ This PR has no changelog entry section.`; }); it('handles CRLF line endings', () => { - const prBody = '### Changelog Entry\r\n\r\n- Entry with CRLF\r\n- Another entry\r\n\r\n### Next'; + const prBody = + '### Changelog Entry\r\n\r\n- Entry with CRLF\r\n- Another entry\r\n\r\n### Next'; expect(extractChangelogEntry(prBody)).toMatchSnapshot(); }); @@ -193,4 +197,3 @@ Intro paragraph: }); }); }); - diff --git a/src/utils/__tests__/changelog-file-ops.test.ts b/src/utils/__tests__/changelog-file-ops.test.ts index 935a917f..7b20e692 100644 --- a/src/utils/__tests__/changelog-file-ops.test.ts +++ b/src/utils/__tests__/changelog-file-ops.test.ts @@ -1,14 +1,9 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; /** * Tests for changelog file operations: findChangeset, removeChangeset, prependChangeset. * These functions work with CHANGELOG.md file content. */ -import { - findChangeset, - removeChangeset, - prependChangeset, -} from '../changelog'; +import { findChangeset, removeChangeset, prependChangeset } from '../changelog'; import { SAMPLE_CHANGESET, SAMPLE_CHANGESET_WITH_SUBHEADING, @@ -25,7 +20,10 @@ describe('findChangeset', () => { 'with date in parentheses', createFullChangelog('Changelog', [ { version: '1.0.1', body: 'newer' }, - { version: `${SAMPLE_CHANGESET.name} (2019-02-02)`, body: SAMPLE_CHANGESET.body }, + { + version: `${SAMPLE_CHANGESET.name} (2019-02-02)`, + body: SAMPLE_CHANGESET.body, + }, { version: '0.9.0', body: 'older' }, ]), ], @@ -47,9 +45,14 @@ describe('findChangeset', () => { test('supports sub-headings within version section', () => { const markdown = createFullChangelog('Changelog', [ - { version: SAMPLE_CHANGESET_WITH_SUBHEADING.name, body: SAMPLE_CHANGESET_WITH_SUBHEADING.body }, + { + version: SAMPLE_CHANGESET_WITH_SUBHEADING.name, + body: SAMPLE_CHANGESET_WITH_SUBHEADING.body, + }, ]); - expect(findChangeset(markdown, 'v1.0.0')).toEqual(SAMPLE_CHANGESET_WITH_SUBHEADING); + expect(findChangeset(markdown, 'v1.0.0')).toEqual( + SAMPLE_CHANGESET_WITH_SUBHEADING, + ); }); test.each([ @@ -168,7 +171,9 @@ describe('removeChangeset', () => { }); test('returns unchanged when header not found', () => { - expect(removeChangeset(fullChangelog, 'non-existent version')).toEqual(fullChangelog); + expect(removeChangeset(fullChangelog, 'non-existent version')).toEqual( + fullChangelog, + ); }); test('returns unchanged when header is empty', () => { @@ -183,7 +188,11 @@ describe('prependChangeset', () => { }; test.each([ - ['to empty text', '', '## 2.0.0\n\n- rewrote everything from scratch\n- with multiple lines\n\n'], + [ + 'to empty text', + '', + '## 2.0.0\n\n- rewrote everything from scratch\n- with multiple lines\n\n', + ], [ 'without top-level header', '## 1.0.0\n\nthis is a test\n', @@ -208,4 +217,3 @@ describe('prependChangeset', () => { expect(prependChangeset(markdown, newChangeset)).toEqual(expected); }); }); - diff --git a/src/utils/__tests__/changelog-generate.test.ts b/src/utils/__tests__/changelog-generate.test.ts index 1007ac2c..a16af43f 100644 --- a/src/utils/__tests__/changelog-generate.test.ts +++ b/src/utils/__tests__/changelog-generate.test.ts @@ -1,10 +1,4 @@ -import { - vi, - type Mock, - type MockInstance, - type Mocked, - type MockedFunction, -} from 'vitest'; +import { vi, type Mock, type MockedFunction } from 'vitest'; /** * Tests for generateChangesetFromGit - the main changelog generation function. * Uses snapshot testing for output validation to reduce test file size. diff --git a/src/utils/__tests__/changelog-semver-warning.test.ts b/src/utils/__tests__/changelog-semver-warning.test.ts index 09ea59c6..be9fe274 100644 --- a/src/utils/__tests__/changelog-semver-warning.test.ts +++ b/src/utils/__tests__/changelog-semver-warning.test.ts @@ -66,7 +66,7 @@ changelog: const { logger } = await import('../../logger'); // Setup: no config file dir - vi.mocked(getConfigFileDir).mockReturnValue(null); + vi.mocked(getConfigFileDir).mockReturnValue(undefined); const { getNormalizedReleaseConfig, clearReleaseConfigCache } = await import('../changelog'); diff --git a/src/utils/__tests__/changelog-utils.test.ts b/src/utils/__tests__/changelog-utils.test.ts index b676c842..edee27a7 100644 --- a/src/utils/__tests__/changelog-utils.test.ts +++ b/src/utils/__tests__/changelog-utils.test.ts @@ -1,10 +1,4 @@ -import { - vi, - type Mock, - type MockInstance, - type Mocked, - type MockedFunction, -} from 'vitest'; +import { vi } from 'vitest'; /** * Tests for changelog utility functions. * - shouldExcludePR: Checks if a PR should be excluded from changelog diff --git a/src/utils/__tests__/env.test.ts b/src/utils/__tests__/env.test.ts index c99ff105..e675e042 100644 --- a/src/utils/__tests__/env.test.ts +++ b/src/utils/__tests__/env.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { writeFileSync } from 'fs'; import { join } from 'path'; // XXX(BYK): This is to be able to spy on `homedir()` in tests @@ -33,7 +33,7 @@ describe('env utils functions', () => { checkEnvForPrerequisite({ name: 'DOGS' }); expect(logger.debug).toHaveBeenCalledWith( 'Checking for environment variable:', - 'DOGS' + 'DOGS', ); }); @@ -46,7 +46,7 @@ describe('env utils functions', () => { it('errors if var not set', () => { expect(() => checkEnvForPrerequisite({ name: 'DOGS' })).toThrowError( - ConfigurationError + ConfigurationError, ); }); }); // end describe('no legacy name') @@ -58,7 +58,7 @@ describe('env utils functions', () => { checkEnvForPrerequisite({ name: 'DOGS', legacyName: 'CATS' }); expect(logger.warn).toHaveBeenCalledWith( `When searching configuration files and your environment, found DOGS ` + - `but also found legacy CATS. Do you mean to be using both?` + `but also found legacy CATS. Do you mean to be using both?`, ); }); @@ -73,14 +73,14 @@ describe('env utils functions', () => { checkEnvForPrerequisite({ name: 'DOGS', legacyName: 'CATS' }); expect(logger.warn).toHaveBeenCalledWith( `Usage of CATS is deprecated, and will be removed in later versions. ` + - `Please use DOGS instead.` + `Please use DOGS instead.`, ); expect(process.env.DOGS).toEqual('DROOL'); }); it('errors if neither is set', () => { expect(() => - checkEnvForPrerequisite({ name: 'DOGS', legacyName: 'CATS' }) + checkEnvForPrerequisite({ name: 'DOGS', legacyName: 'CATS' }), ).toThrowError(ConfigurationError); }); }); // end describe('with legacy name') @@ -91,33 +91,33 @@ describe('env utils functions', () => { // it error, so `expect` the error to catch it so it doesn't break the // test) expect(() => - checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }) + checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }), ).toThrowError(ConfigurationError); expect(logger.debug).toHaveBeenCalledWith( 'Checking for environment variable(s):', - 'MAISEY or CHARLIE' + 'MAISEY or CHARLIE', ); expect(logger.debug).toHaveBeenCalledWith( 'Checking for environment variable:', - 'MAISEY' + 'MAISEY', ); expect(logger.debug).toHaveBeenCalledWith( 'Checking for environment variable:', - 'CHARLIE' + 'CHARLIE', ); }); it('is happy if either option is defined', () => { process.env.MAISEY = 'GOOD DOG'; expect(() => - checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }) + checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }), ).not.toThrowError(ConfigurationError); expect(logger.debug).toHaveBeenCalledWith('Found MAISEY'); delete process.env.MAISEY; process.env.CHARLIE = 'ALSO GOOD DOG'; expect(() => - checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }) + checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }), ).not.toThrowError(ConfigurationError); expect(logger.debug).toHaveBeenCalledWith('Found CHARLIE'); }); @@ -125,7 +125,7 @@ describe('env utils functions', () => { it('throws if neither one is defined', () => { // skip defining vars here expect(() => - checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }) + checkEnvForPrerequisite({ name: 'MAISEY' }, { name: 'CHARLIE' }), ).toThrowError(ConfigurationError); }); @@ -134,8 +134,8 @@ describe('env utils functions', () => { expect(() => checkEnvForPrerequisite( { name: 'MAISEY' }, - { name: 'CHARLIE', legacyName: 'OPAL' } - ) + { name: 'CHARLIE', legacyName: 'OPAL' }, + ), ).not.toThrowError(ConfigurationError); expect(logger.debug).toHaveBeenCalledWith('Found MAISEY'); @@ -144,12 +144,12 @@ describe('env utils functions', () => { expect(() => checkEnvForPrerequisite( { name: 'MAISEY' }, - { name: 'CHARLIE', legacyName: 'OPAL' } - ) + { name: 'CHARLIE', legacyName: 'OPAL' }, + ), ).not.toThrowError(ConfigurationError); expect(logger.warn).toHaveBeenCalledWith( `Usage of OPAL is deprecated, and will be removed in later versions. ` + - `Please use CHARLIE instead.` + `Please use CHARLIE instead.`, ); expect(process.env.CHARLIE).toEqual('GOOD PUPPY'); }); diff --git a/src/utils/__tests__/files.test.ts b/src/utils/__tests__/files.test.ts index aabf3f93..1652ba48 100644 --- a/src/utils/__tests__/files.test.ts +++ b/src/utils/__tests__/files.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { existsSync, rmdirSync } from 'fs'; import { join, resolve } from 'path'; @@ -18,7 +17,7 @@ describe('listFiles', () => { describe('withTempDir', () => { async function testDirectories( callback: (arg: any) => any, - cleanupEnabled = true + cleanupEnabled = true, ): Promise { let directory = ''; try { diff --git a/src/utils/__tests__/fixtures/changelog-mocks.ts b/src/utils/__tests__/fixtures/changelog-mocks.ts index 024cc0fe..85810d9a 100644 --- a/src/utils/__tests__/fixtures/changelog-mocks.ts +++ b/src/utils/__tests__/fixtures/changelog-mocks.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest'; +import { vi, type Mock, type MockedFunction } from 'vitest'; /** * Shared mock setup for changelog tests. */ @@ -25,12 +25,18 @@ export function initMocks( getChangesSince: any, config: any, readFileSync: any, - clearChangesetCache: () => void + clearChangesetCache: () => void, ): void { mockClient = vi.fn(); - mockGetChangesSince = getChangesSince as MockedFunction; - getConfigFileDirMock = config.getConfigFileDir as MockedFunction; - getGlobalGitHubConfigMock = config.getGlobalGitHubConfig as MockedFunction; + mockGetChangesSince = getChangesSince as MockedFunction< + typeof getChangesSince + >; + getConfigFileDirMock = config.getConfigFileDir as MockedFunction< + typeof config.getConfigFileDir + >; + getGlobalGitHubConfigMock = config.getGlobalGitHubConfig as MockedFunction< + typeof config.getGlobalGitHubConfig + >; readFileSyncMock = readFileSync as MockedFunction; vi.resetAllMocks(); @@ -58,7 +64,7 @@ export function initMocks( */ export function setupGenerateTest( commits: TestCommit[], - releaseConfig?: string | null + releaseConfig?: string | null, ): void { mockGetChangesSince.mockResolvedValueOnce( commits.map(commit => ({ @@ -66,7 +72,7 @@ export function setupGenerateTest( title: commit.title, body: commit.body, pr: commit.pr?.local || null, - })) + })), ); mockClient.mockResolvedValueOnce({ @@ -93,7 +99,7 @@ export function setupGenerateTest( : [], }, }, - ]) + ]), ), }); @@ -118,4 +124,3 @@ export function setupGenerateTest( } } } - diff --git a/src/utils/__tests__/gcsAPI.test.ts b/src/utils/__tests__/gcsAPI.test.ts index 3bfe0a4c..7e9b52fb 100644 --- a/src/utils/__tests__/gcsAPI.test.ts +++ b/src/utils/__tests__/gcsAPI.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; @@ -32,13 +32,12 @@ import { vi.mock('../../logger'); // Mock existsSync to be controllable in tests while keeping other fs functions real -const mockExistsSync = vi.fn<(path: fs.PathLike) => boolean>((path) => { - // eslint-disable-next-line @typescript-eslint/no-require-imports +const mockExistsSync = vi.fn<(path: fs.PathLike) => boolean>(path => { const realFs = require('fs'); return realFs.existsSync(path); }); -vi.mock('fs', async (importOriginal) => { +vi.mock('fs', async importOriginal => { const actual = await importOriginal(); return { ...actual, @@ -92,7 +91,7 @@ describe('gcsApi module', () => { const creds = getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } + { name: 'DOG_CREDS_PATH' }, ); expect(creds).toMatchObject({ @@ -115,7 +114,7 @@ describe('gcsApi module', () => { const creds = getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } + { name: 'DOG_CREDS_PATH' }, ); expect(creds).toMatchObject({ @@ -134,8 +133,8 @@ describe('gcsApi module', () => { expect( getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } - ) + { name: 'DOG_CREDS_PATH' }, + ), ).toBeNull(); }); @@ -145,7 +144,7 @@ describe('gcsApi module', () => { expect(() => { getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } + { name: 'DOG_CREDS_PATH' }, ); }).toThrowError('Error parsing JSON credentials'); }); @@ -159,7 +158,7 @@ describe('gcsApi module', () => { expect(() => { getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } + { name: 'DOG_CREDS_PATH' }, ); }).toThrowError('File does not exist: `./iDontExist.json`!'); }); @@ -173,7 +172,7 @@ describe('gcsApi module', () => { expect(() => { getGCSCredsFromEnv( { name: 'DOG_CREDS_JSON' }, - { name: 'DOG_CREDS_PATH' } + { name: 'DOG_CREDS_PATH' }, ); }).toThrowError('GCS credentials missing `client_email`!'); }); @@ -186,12 +185,12 @@ describe('gcsApi module', () => { await client.uploadArtifact( squirrelStatsLocalPath, - squirrelStatsBucketPath + squirrelStatsBucketPath, ); const { filename } = squirrelStatsArtifact; const destinationPath = path.posix.normalize( - squirrelStatsBucketPath.path + squirrelStatsBucketPath.path, ); expect(mockGCSUpload).toHaveBeenCalledWith(squirrelStatsLocalPath, { @@ -211,7 +210,7 @@ describe('gcsApi module', () => { const { filename } = squirrelStatsArtifact; const destinationPath = path.posix.normalize( - squirrelStatsBucketPath.path + squirrelStatsBucketPath.path, ); expect(mockGCSUpload).toHaveBeenCalledWith(squirrelStatsLocalPath, { @@ -227,7 +226,7 @@ describe('gcsApi module', () => { await client.uploadArtifact( squirrelSimulatorLocalPath, - squirrelSimulatorBucketPath + squirrelSimulatorBucketPath, ); expect(mockGCSUpload).toHaveBeenCalledWith( @@ -236,7 +235,7 @@ describe('gcsApi module', () => { metadata: expect.objectContaining({ contentType: 'application/javascript; charset=utf-8', }), - }) + }), ); }); @@ -245,7 +244,7 @@ describe('gcsApi module', () => { await client.uploadArtifact( squirrelSimulatorLocalPath, - squirrelSimulatorBucketPath + squirrelSimulatorBucketPath, ); const squirrelSimulatorMetadata = squirrelSimulatorBucketPath.metadata; @@ -254,7 +253,7 @@ describe('gcsApi module', () => { squirrelSimulatorLocalPath, expect.objectContaining({ metadata: expect.objectContaining({ ...squirrelSimulatorMetadata }), - }) + }), ); }); @@ -270,10 +269,10 @@ describe('gcsApi module', () => { await expect( client.uploadArtifact( squirrelSimulatorLocalPath, - squirrelSimulatorBucketPath - ) + squirrelSimulatorBucketPath, + ), ).rejects.toThrowError( - `Encountered an error while uploading \`${filename}\`` + `Encountered an error while uploading \`${filename}\``, ); }); @@ -284,7 +283,7 @@ describe('gcsApi module', () => { await client.uploadArtifact( squirrelStatsLocalPath, - squirrelStatsBucketPath + squirrelStatsBucketPath, ); expect(mockGCSUpload).not.toHaveBeenCalled(); @@ -298,7 +297,7 @@ describe('gcsApi module', () => { await withTempDir(async tempDownloadDirectory => { await client.downloadArtifact( squirrelStatsArtifact.storedFile.downloadFilepath, - tempDownloadDirectory + tempDownloadDirectory, ); const { filename } = squirrelStatsArtifact; @@ -318,8 +317,8 @@ describe('gcsApi module', () => { await expect( client.downloadArtifact( squirrelSimulatorArtifact.storedFile.downloadFilepath, - './iDontExist/' - ) + './iDontExist/', + ), ).rejects.toThrowError(`directory does not exist!`); }); @@ -336,10 +335,10 @@ describe('gcsApi module', () => { await expect( client.downloadArtifact( squirrelSimulatorArtifact.storedFile.downloadFilepath, - tempDownloadDirectory - ) + tempDownloadDirectory, + ), ).rejects.toThrowError( - `Encountered an error while downloading \`${filename}\`` + `Encountered an error while downloading \`${filename}\``, ); }); }); @@ -356,7 +355,7 @@ describe('gcsApi module', () => { await client.downloadArtifact( squirrelSimulatorArtifact.storedFile.downloadFilepath, - tempDownloadDirectory + tempDownloadDirectory, ); expect(mockGCSDownload).not.toHaveBeenCalled(); @@ -373,14 +372,14 @@ describe('gcsApi module', () => { await client.listArtifactsForRevision( dogsGHOrg, squirrelRepo, - squirrelSimulatorCommit + squirrelSimulatorCommit, ); expect(mockGCSGetFiles).toHaveBeenCalledWith({ prefix: path.posix.join( dogsGHOrg, squirrelRepo, - squirrelSimulatorCommit + squirrelSimulatorCommit, ), }); }); @@ -393,7 +392,7 @@ describe('gcsApi module', () => { const artifacts = await client.listArtifactsForRevision( dogsGHOrg, squirrelRepo, - squirrelStatsCommit + squirrelStatsCommit, ); expect(artifacts[0]).toEqual(squirrelStatsArtifact); @@ -409,7 +408,7 @@ describe('gcsApi module', () => { const artifacts = await client.listArtifactsForRevision( dogsGHOrg, squirrelRepo, - squirrelStatsCommit + squirrelStatsCommit, ); expect(artifacts.length).toEqual(2); @@ -426,8 +425,8 @@ describe('gcsApi module', () => { client.listArtifactsForRevision( dogsGHOrg, squirrelRepo, - squirrelSimulatorCommit - ) + squirrelSimulatorCommit, + ), ).rejects.toThrowError('Error retrieving artifact list from GCS'); }); }); // end describe('listArtifactsForRevision') diff --git a/src/utils/__tests__/git.test.ts b/src/utils/__tests__/git.test.ts index f3994dea..4fda8896 100644 --- a/src/utils/__tests__/git.test.ts +++ b/src/utils/__tests__/git.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi } from 'vitest'; import { getLatestTag, isRepoDirty } from '../git'; import * as loggerModule from '../../logger'; import type { StatusResult } from 'simple-git'; diff --git a/src/utils/__tests__/githubApi.test.ts b/src/utils/__tests__/githubApi.test.ts index a9c2e88d..d93a6aac 100644 --- a/src/utils/__tests__/githubApi.test.ts +++ b/src/utils/__tests__/githubApi.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type Mock } from 'vitest'; import { Octokit } from '@octokit/rest'; import { getFile } from '../githubApi'; @@ -17,7 +17,7 @@ describe('getFile', () => { const owner = 'owner'; const repo = 'repo'; - const getContent = (github.repos.getContent as unknown) as Mock; + const getContent = github.repos.getContent as unknown as Mock; test('loads and decodes the file', async () => { expect.assertions(2); @@ -32,7 +32,7 @@ describe('getFile', () => { owner, repo, '/path/to/file', - 'v1.0.0' + 'v1.0.0', ); expect(getContent).toHaveBeenCalledWith({ owner: 'owner', @@ -58,7 +58,7 @@ describe('getFile', () => { owner, repo, '/path/to/missing', - 'v1.0.0' + 'v1.0.0', ); expect(content).toBe(undefined); }); diff --git a/src/utils/__tests__/helpers.test.ts b/src/utils/__tests__/helpers.test.ts index fa53fbe2..603c5cb9 100644 --- a/src/utils/__tests__/helpers.test.ts +++ b/src/utils/__tests__/helpers.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { envToBool } from '../helpers'; describe('envToBool', () => @@ -18,5 +17,5 @@ describe('envToBool', () => ['yes', true], ['dogs are great!', true], ])('From %j we should get "%s"', (envVar, result) => - expect(envToBool(envVar)).toBe(result) + expect(envToBool(envVar)).toBe(result), )); diff --git a/src/utils/__tests__/objects.test.ts b/src/utils/__tests__/objects.test.ts index 4c9857a8..f3238561 100644 --- a/src/utils/__tests__/objects.test.ts +++ b/src/utils/__tests__/objects.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { clearObjectProperties } from '../objects'; describe('clearObjectProperties', () => { diff --git a/src/utils/__tests__/packagePath.test.ts b/src/utils/__tests__/packagePath.test.ts index 7430d3fb..d06f702c 100644 --- a/src/utils/__tests__/packagePath.test.ts +++ b/src/utils/__tests__/packagePath.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { parseCanonical } from '../packagePath'; describe('parseCanonical', () => { diff --git a/src/utils/__tests__/registry.test.ts b/src/utils/__tests__/registry.test.ts index afc0e395..0dc4c988 100644 --- a/src/utils/__tests__/registry.test.ts +++ b/src/utils/__tests__/registry.test.ts @@ -1,4 +1,4 @@ -import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import { mkdtempSync, rmSync } from 'fs'; @@ -23,7 +23,13 @@ describe('getPackageManifest', () => { describe('when package already exists', () => { it('reads the existing latest.json manifest', async () => { - const packageDir = path.join(tempDir, 'packages', 'npm', '@sentry', 'browser'); + const packageDir = path.join( + tempDir, + 'packages', + 'npm', + '@sentry', + 'browser', + ); fs.mkdirSync(packageDir, { recursive: true }); const existingManifest = { canonical: 'npm:@sentry/browser', @@ -33,28 +39,32 @@ describe('getPackageManifest', () => { }; fs.writeFileSync( path.join(packageDir, 'latest.json'), - JSON.stringify(existingManifest) + JSON.stringify(existingManifest), ); const result = await getPackageManifest( tempDir, RegistryPackageType.SDK, 'npm:@sentry/browser', - '1.1.0' + '1.1.0', ); expect(result.packageManifest).toEqual(existingManifest); - expect(result.versionFilePath).toBe( - path.join(packageDir, '1.1.0.json') - ); + expect(result.versionFilePath).toBe(path.join(packageDir, '1.1.0.json')); }); it('throws an error if version file already exists', async () => { - const packageDir = path.join(tempDir, 'packages', 'npm', '@sentry', 'browser'); + const packageDir = path.join( + tempDir, + 'packages', + 'npm', + '@sentry', + 'browser', + ); fs.mkdirSync(packageDir, { recursive: true }); fs.writeFileSync( path.join(packageDir, 'latest.json'), - JSON.stringify({ canonical: 'npm:@sentry/browser' }) + JSON.stringify({ canonical: 'npm:@sentry/browser' }), ); fs.writeFileSync(path.join(packageDir, '1.0.0.json'), '{}'); @@ -63,8 +73,8 @@ describe('getPackageManifest', () => { tempDir, RegistryPackageType.SDK, 'npm:@sentry/browser', - '1.0.0' - ) + '1.0.0', + ), ).rejects.toThrow('Version file for "1.0.0" already exists'); }); }); @@ -84,11 +94,17 @@ describe('getPackageManifest', () => { RegistryPackageType.SDK, 'npm:@sentry/wasm', '0.1.0', - initialData + initialData, ); // Check directory was created - const packageDir = path.join(tempDir, 'packages', 'npm', '@sentry', 'wasm'); + const packageDir = path.join( + tempDir, + 'packages', + 'npm', + '@sentry', + 'wasm', + ); expect(fs.existsSync(packageDir)).toBe(true); // Check manifest has correct fields @@ -101,9 +117,7 @@ describe('getPackageManifest', () => { }); // Check version file path - expect(result.versionFilePath).toBe( - path.join(packageDir, '0.1.0.json') - ); + expect(result.versionFilePath).toBe(path.join(packageDir, '0.1.0.json')); }); it('creates initial manifest with only required fields when optional are not provided', async () => { @@ -117,7 +131,7 @@ describe('getPackageManifest', () => { RegistryPackageType.SDK, 'npm:@sentry/minimal', '1.0.0', - initialData + initialData, ); expect(result.packageManifest).toEqual({ @@ -132,10 +146,10 @@ describe('getPackageManifest', () => { tempDir, RegistryPackageType.SDK, 'npm:@sentry/new-package', - '1.0.0' - ) + '1.0.0', + ), ).rejects.toThrow( - 'Package "npm:@sentry/new-package" does not exist in the registry and no initial manifest data was provided' + 'Package "npm:@sentry/new-package" does not exist in the registry and no initial manifest data was provided', ); }); @@ -151,7 +165,7 @@ describe('getPackageManifest', () => { RegistryPackageType.APP, 'app:craft', '2.0.0', - initialData + initialData, ); // Check directory was created @@ -177,7 +191,7 @@ describe('getPackageManifest', () => { RegistryPackageType.SDK, 'npm:@sentry/core', '1.0.0', - initialData + initialData, ); expect(result.packageManifest).toEqual({ diff --git a/src/utils/__tests__/strings.test.ts b/src/utils/__tests__/strings.test.ts index b0fc140b..f2e8c777 100644 --- a/src/utils/__tests__/strings.test.ts +++ b/src/utils/__tests__/strings.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { renderTemplateSafe, sanitizeObject, @@ -60,40 +59,40 @@ describe('renderTemplateSafe', () => { test('renders nested values', () => { expect(renderTemplateSafe('x{{ var.d }}', { var: { d: 123 } })).toBe( - 'x123' + 'x123', ); }); test('renders nested values with dotted keys', () => { expect(renderTemplateSafe('x{{ var.d__1 }}', { var: { 'd.1': 123 } })).toBe( - 'x123' + 'x123', ); }); test('throws error on unknown variable', () => { - expect(() => renderTemplateSafe('{{ unknown }}', { known: 'value' })).toThrow( - ConfigurationError - ); - expect(() => renderTemplateSafe('{{ unknown }}', { known: 'value' })).toThrow( - /Unknown template variable\(s\): unknown/ - ); + expect(() => + renderTemplateSafe('{{ unknown }}', { known: 'value' }), + ).toThrow(ConfigurationError); + expect(() => + renderTemplateSafe('{{ unknown }}', { known: 'value' }), + ).toThrow(/Unknown template variable\(s\): unknown/); }); test('throws error with available variables in message', () => { expect(() => - renderTemplateSafe('{{ missing }}', { foo: 1, bar: 2 }) + renderTemplateSafe('{{ missing }}', { foo: 1, bar: 2 }), ).toThrow(/Available variables: foo, bar/); }); test('throws error for globals (prevents accidental access)', () => { expect(() => renderTemplateSafe('{{ process }}', {})).toThrow( - ConfigurationError + ConfigurationError, ); }); test('throws error listing all unknown variables', () => { expect(() => - renderTemplateSafe('{{ a }} {{ b }} {{ c }}', { x: 1 }) + renderTemplateSafe('{{ a }} {{ b }} {{ c }}', { x: 1 }), ).toThrow(/Unknown template variable\(s\): a, b, c/); }); }); @@ -124,7 +123,7 @@ describe('formatJson', () => { 3, 4 ] -}` +}`, ); }); test('serializes an error', () => { diff --git a/src/utils/__tests__/symlink.test.ts b/src/utils/__tests__/symlink.test.ts index 62167925..ef085275 100644 --- a/src/utils/__tests__/symlink.test.ts +++ b/src/utils/__tests__/symlink.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { createSymlinks } from '../symlink'; import { withTempDir } from '../files'; import { promises as fsPromises } from 'fs'; @@ -66,7 +65,7 @@ describe('createSymlinks', () => { const symlinkDestination = await fsPromises.readlink(symlinkFilePath); expect(symlinkDestination).toBe(versionFile); } - }, true) + }, true), ); it('handles updating an old major version', async () => @@ -110,25 +109,25 @@ describe('createSymlinks', () => { '2.0.json', 'latest.json', - ].sort() + ].sort(), ); const latestLink = await fsPromises.readlink( - path.join(tmpDir, 'latest.json') + path.join(tmpDir, 'latest.json'), ); const major1Link = await fsPromises.readlink(path.join(tmpDir, '1.json')); const major2Link = await fsPromises.readlink(path.join(tmpDir, '2.json')); const minor10Link = await fsPromises.readlink( - path.join(tmpDir, '1.0.json') + path.join(tmpDir, '1.0.json'), ); const minor12Link = await fsPromises.readlink( - path.join(tmpDir, '1.2.json') + path.join(tmpDir, '1.2.json'), ); const minor15Link = await fsPromises.readlink( - path.join(tmpDir, '1.5.json') + path.join(tmpDir, '1.5.json'), ); const minor20Link = await fsPromises.readlink( - path.join(tmpDir, '2.0.json') + path.join(tmpDir, '2.0.json'), ); expect(latestLink).toBe('2.0.0.json'); @@ -166,18 +165,18 @@ describe('createSymlinks', () => { '1.1.json', 'latest.json', - ].sort() + ].sort(), ); const latestLink = await fsPromises.readlink( - path.join(tmpDir, 'latest.json') + path.join(tmpDir, 'latest.json'), ); const major1Link = await fsPromises.readlink(path.join(tmpDir, '1.json')); const minor10Link = await fsPromises.readlink( - path.join(tmpDir, '1.0.json') + path.join(tmpDir, '1.0.json'), ); const minor11Link = await fsPromises.readlink( - path.join(tmpDir, '1.1.json') + path.join(tmpDir, '1.1.json'), ); expect(latestLink).toBe('1.1.0.json'); @@ -211,15 +210,15 @@ it('handles updating a previous patch version on the same minor', async () => '1.0.json', 'latest.json', - ].sort() + ].sort(), ); const latestLink = await fsPromises.readlink( - path.join(tmpDir, 'latest.json') + path.join(tmpDir, 'latest.json'), ); const major1Link = await fsPromises.readlink(path.join(tmpDir, '1.json')); const minor10Link = await fsPromises.readlink( - path.join(tmpDir, '1.0.json') + path.join(tmpDir, '1.0.json'), ); expect(latestLink).toBe('1.0.2.json'); diff --git a/src/utils/__tests__/system.test.ts b/src/utils/__tests__/system.test.ts index 1bf45a7a..0d1e8f42 100644 --- a/src/utils/__tests__/system.test.ts +++ b/src/utils/__tests__/system.test.ts @@ -1,4 +1,4 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; +import { vi, type Mock } from 'vitest'; import * as fs from 'fs'; import { logger } from '../../logger'; @@ -20,7 +20,8 @@ describe('spawnProcess', () => { test('resolves on success with standard output', async () => { expect.assertions(1); const stdout = - (await spawnProcess(process.execPath, ['-e', 'console.log("test")'])) || ''; + (await spawnProcess(process.execPath, ['-e', 'console.log("test")'])) || + ''; expect(stdout.toString()).toBe('test\n'); }); @@ -55,7 +56,9 @@ describe('spawnProcess', () => { test('attaches options on error', async () => { try { expect.assertions(1); - await spawnProcess(process.execPath, ['-e', 'process.exit(1)'], { cwd: '/tmp/' }); + await spawnProcess(process.execPath, ['-e', 'process.exit(1)'], { + cwd: '/tmp/', + }); } catch (e: any) { expect(e.options.cwd).toEqual('/tmp/'); } @@ -64,7 +67,9 @@ describe('spawnProcess', () => { test('strips env from options on error', async () => { try { expect.assertions(1); - await spawnProcess(process.execPath, ['-e', 'process.exit(1)'], { env: { x: '123', password: '456' } }); + await spawnProcess(process.execPath, ['-e', 'process.exit(1)'], { + env: { x: '123', password: '456' }, + }); } catch (e: any) { expect(e.options.env).toBeUndefined(); } @@ -85,7 +90,7 @@ describe('spawnProcess', () => { process.execPath, ['-e', 'process.stdout.write("test-string")'], {}, - { showStdout: true } + { showStdout: true }, ); expect(mockedLogInfo).toHaveBeenCalledTimes(1); @@ -116,7 +121,7 @@ describe('calculateChecksum', () => { const checksum = await calculateChecksum(tmpFilePath); expect(checksum).toBe( - '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b' + '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b', ); }); }); @@ -145,7 +150,7 @@ describe('calculateChecksum', () => { format: HashOutputFormat.Base64, }); expect(checksum).toBe( - '7GZOiJ7WwbJ2PKz3iZ2Vt/NHNz65guUjQZ/uo6o2LYkbO/Al8pImelhUBJCReJw+' + '7GZOiJ7WwbJ2PKz3iZ2Vt/NHNz65guUjQZ/uo6o2LYkbO/Al8pImelhUBJCReJw+', ); }); }); @@ -161,7 +166,9 @@ describe('isExecutableInPath', () => { }); test('checks for existing executable using absolute path', () => { - expect(hasExecutable(`${process.cwd()}/node_modules/.bin/vitest`)).toBe(true); + expect(hasExecutable(`${process.cwd()}/node_modules/.bin/vitest`)).toBe( + true, + ); }); test('checks for non-existing executable using absolute path', () => { @@ -186,176 +193,25 @@ describe('extractZipArchive', () => { const zipf = await fs.promises.open(zip, 'w'); await zipf.writeFile( Buffer.from([ - 80, - 75, - 3, - 4, - 10, - 0, - 0, - 0, - 0, - 0, - 99, - 150, - 109, - 88, - 220, - 199, - 60, - 159, - 40, - 11, - 4, - 0, - 40, - 11, - 4, - 0, - 5, - 0, - 28, - 0, - 116, - 46, - 116, - 120, - 116, - 85, - 84, - 9, - 0, - 3, - 153, - 245, - 241, - 101, - 140, - 245, - 241, - 101, - 117, - 120, - 11, - 0, - 1, - 4, - 0, - 0, - 0, - 0, - 4, - 0, - 0, - 0, - 0, - ]) + 80, 75, 3, 4, 10, 0, 0, 0, 0, 0, 99, 150, 109, 88, 220, 199, 60, 159, + 40, 11, 4, 0, 40, 11, 4, 0, 5, 0, 28, 0, 116, 46, 116, 120, 116, 85, + 84, 9, 0, 3, 153, 245, 241, 101, 140, 245, 241, 101, 117, 120, 11, 0, + 1, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, + ]), ); for (let i = 0; i < 5000; i += 1) { await zipf.writeFile( - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\n' + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\n', ); } await zipf.writeFile( Buffer.from([ - 80, - 75, - 1, - 2, - 30, - 3, - 10, - 0, - 0, - 0, - 0, - 0, - 99, - 150, - 109, - 88, - 220, - 199, - 60, - 159, - 40, - 11, - 4, - 0, - 40, - 11, - 4, - 0, - 5, - 0, - 24, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 164, - 129, - 0, - 0, - 0, - 0, - 116, - 46, - 116, - 120, - 116, - 85, - 84, - 5, - 0, - 3, - 153, - 245, - 241, - 101, - 117, - 120, - 11, - 0, - 1, - 4, - 0, - 0, - 0, - 0, - 4, - 0, - 0, - 0, - 0, - 80, - 75, - 5, - 6, - 0, - 0, - 0, - 0, - 1, - 0, - 1, - 0, - 75, - 0, - 0, - 0, - 103, - 11, - 4, - 0, - 0, - 0, - ]) + 80, 75, 1, 2, 30, 3, 10, 0, 0, 0, 0, 0, 99, 150, 109, 88, 220, 199, + 60, 159, 40, 11, 4, 0, 40, 11, 4, 0, 5, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 164, 129, 0, 0, 0, 0, 116, 46, 116, 120, 116, 85, 84, 5, 0, 3, 153, + 245, 241, 101, 117, 120, 11, 0, 1, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 80, + 75, 5, 6, 0, 0, 0, 0, 1, 0, 1, 0, 75, 0, 0, 0, 103, 11, 4, 0, 0, 0, + ]), ); await zipf.close(); @@ -364,7 +220,7 @@ describe('extractZipArchive', () => { // should not have corrupted our file const checksum = await calculateChecksum(`${tmpdir}/out/t.txt`); expect(checksum).toBe( - '7687e11d941faf48d4cf1692c2473a599ad0d7030e1e5c639a31b2f59cd646ba' + '7687e11d941faf48d4cf1692c2473a599ad0d7030e1e5c639a31b2f59cd646ba', ); }); }); diff --git a/src/utils/__tests__/version.test.ts b/src/utils/__tests__/version.test.ts index 4bff438b..47fad939 100644 --- a/src/utils/__tests__/version.test.ts +++ b/src/utils/__tests__/version.test.ts @@ -1,6 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; - - import { getPackage, getVersion, @@ -140,7 +137,7 @@ describe('isPreviewRelease', () => { 'accepts semver preview release', previewSuffix => { expect(isPreviewRelease(`2.3.4-${previewSuffix}1`)).toBe(true); - } + }, ); test('accepts Python-style preview release', () => { @@ -166,7 +163,7 @@ describe('versionGreaterOrEqualThan', () => { minor: number, patch: number, pre?: string, - build?: string + build?: string, ): SemVer { return { major, minor, patch, pre, build }; } @@ -206,7 +203,6 @@ describe('versionGreaterOrEqualThan', () => { }); test('can compare pre parts', () => { - const v1 = parseVersion('1.2.3-1')!; const v2 = parseVersion('1.2.3-2')!; expect(versionGreaterOrEqualThan(v1, v2)).toBe(false); @@ -257,6 +253,6 @@ describe('semVerToString', () => { 'converts a SemVer object (%s) to a string', (_, semver, expectedString) => { expect(semVerToString(semver)).toBe(expectedString); - } + }, ); }); diff --git a/src/utils/__tests__/workspaces.test.ts b/src/utils/__tests__/workspaces.test.ts index e6dfd9a5..718e2349 100644 --- a/src/utils/__tests__/workspaces.test.ts +++ b/src/utils/__tests__/workspaces.test.ts @@ -1,4 +1,3 @@ -import { vi, type Mock, type MockInstance, type Mocked, type MockedFunction } from 'vitest'; import { resolve } from 'path'; import { @@ -14,7 +13,9 @@ const fixturesDir = resolve(__dirname, '../__fixtures__/workspaces'); describe('discoverWorkspaces', () => { test('discovers npm workspaces', async () => { - const result = await discoverWorkspaces(resolve(fixturesDir, 'npm-workspace')); + const result = await discoverWorkspaces( + resolve(fixturesDir, 'npm-workspace'), + ); expect(result.type).toBe('npm'); expect(result.packages).toHaveLength(2); @@ -34,7 +35,9 @@ describe('discoverWorkspaces', () => { }); test('discovers pnpm workspaces', async () => { - const result = await discoverWorkspaces(resolve(fixturesDir, 'pnpm-workspace')); + const result = await discoverWorkspaces( + resolve(fixturesDir, 'pnpm-workspace'), + ); expect(result.type).toBe('pnpm'); expect(result.packages).toHaveLength(2); @@ -44,14 +47,18 @@ describe('discoverWorkspaces', () => { }); test('returns none type when no workspaces found', async () => { - const result = await discoverWorkspaces(resolve(fixturesDir, 'no-workspace')); + const result = await discoverWorkspaces( + resolve(fixturesDir, 'no-workspace'), + ); expect(result.type).toBe('none'); expect(result.packages).toHaveLength(0); }); test('returns none type for non-existent directory', async () => { - const result = await discoverWorkspaces(resolve(fixturesDir, 'does-not-exist')); + const result = await discoverWorkspaces( + resolve(fixturesDir, 'does-not-exist'), + ); expect(result.type).toBe('none'); expect(result.packages).toHaveLength(0); @@ -60,10 +67,34 @@ describe('discoverWorkspaces', () => { describe('filterWorkspacePackages', () => { const testPackages: WorkspacePackage[] = [ - { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry-internal/utils'] }, - { name: '@sentry/node', location: '/path/node', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry-internal/utils'] }, - { name: '@sentry-internal/utils', location: '/path/utils', private: false, hasPublicAccess: true, workspaceDependencies: [] }, - { name: '@other/package', location: '/path/other', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + { + name: '@sentry/browser', + location: '/path/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry-internal/utils'], + }, + { + name: '@sentry/node', + location: '/path/node', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry-internal/utils'], + }, + { + name: '@sentry-internal/utils', + location: '/path/utils', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + { + name: '@other/package', + location: '/path/other', + private: false, + hasPublicAccess: false, + workspaceDependencies: [], + }, ]; test('returns all packages when no filters provided', () => { @@ -72,20 +103,20 @@ describe('filterWorkspacePackages', () => { }); test('filters packages by include pattern', () => { - const result = filterWorkspacePackages( - testPackages, - /^@sentry\// - ); + const result = filterWorkspacePackages(testPackages, /^@sentry\//); expect(result).toHaveLength(2); - expect(result.map(p => p.name)).toEqual(['@sentry/browser', '@sentry/node']); + expect(result.map(p => p.name)).toEqual([ + '@sentry/browser', + '@sentry/node', + ]); }); test('filters packages by exclude pattern', () => { const result = filterWorkspacePackages( testPackages, undefined, - /^@sentry-internal\// + /^@sentry-internal\//, ); expect(result).toHaveLength(3); @@ -100,11 +131,14 @@ describe('filterWorkspacePackages', () => { const result = filterWorkspacePackages( testPackages, /^@sentry/, - /^@sentry-internal\// + /^@sentry-internal\//, ); expect(result).toHaveLength(2); - expect(result.map(p => p.name)).toEqual(['@sentry/browser', '@sentry/node']); + expect(result.map(p => p.name)).toEqual([ + '@sentry/browser', + '@sentry/node', + ]); }); }); @@ -115,7 +149,9 @@ describe('packageNameToArtifactPattern', () => { }); test('converts nested scoped package name to pattern', () => { - const pattern = packageNameToArtifactPattern('@sentry-internal/browser-utils'); + const pattern = packageNameToArtifactPattern( + '@sentry-internal/browser-utils', + ); expect(pattern).toBe('/^sentry-internal-browser-utils-\\d.*\\.tgz$/'); }); @@ -129,7 +165,7 @@ describe('packageNameToArtifactFromTemplate', () => { test('replaces {{name}} with full package name', () => { const result = packageNameToArtifactFromTemplate( '@sentry/browser', - '{{name}}.tgz' + '{{name}}.tgz', ); expect(result).toBe('/^@sentry\\/browser\\.tgz$/'); }); @@ -137,7 +173,7 @@ describe('packageNameToArtifactFromTemplate', () => { test('replaces {{simpleName}} with normalized name', () => { const result = packageNameToArtifactFromTemplate( '@sentry/browser', - '{{simpleName}}.tgz' + '{{simpleName}}.tgz', ); expect(result).toBe('/^sentry-browser\\.tgz$/'); }); @@ -145,7 +181,7 @@ describe('packageNameToArtifactFromTemplate', () => { test('replaces {{version}} with version placeholder', () => { const result = packageNameToArtifactFromTemplate( '@sentry/browser', - '{{simpleName}}-{{version}}.tgz' + '{{simpleName}}-{{version}}.tgz', ); expect(result).toBe('/^sentry-browser-\\d.*\\.tgz$/'); }); @@ -154,7 +190,7 @@ describe('packageNameToArtifactFromTemplate', () => { const result = packageNameToArtifactFromTemplate( '@sentry/browser', '{{simpleName}}-{{version}}.tgz', - '1.0.0' + '1.0.0', ); expect(result).toBe('/^sentry-browser-1\\.0\\.0\\.tgz$/'); }); @@ -162,34 +198,72 @@ describe('packageNameToArtifactFromTemplate', () => { test('handles complex templates', () => { const result = packageNameToArtifactFromTemplate( '@sentry/browser', - 'dist/{{simpleName}}/{{simpleName}}-{{version}}.tgz' + 'dist/{{simpleName}}/{{simpleName}}-{{version}}.tgz', + ); + expect(result).toBe( + '/^dist\\/sentry-browser\\/sentry-browser-\\d.*\\.tgz$/', ); - expect(result).toBe('/^dist\\/sentry-browser\\/sentry-browser-\\d.*\\.tgz$/'); }); }); describe('topologicalSortPackages', () => { test('returns packages in dependency order', () => { const packages: WorkspacePackage[] = [ - { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, - { name: '@sentry/core', location: '/path/core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/types'] }, - { name: '@sentry/types', location: '/path/types', private: false, hasPublicAccess: true, workspaceDependencies: [] }, + { + name: '@sentry/browser', + location: '/path/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/core'], + }, + { + name: '@sentry/core', + location: '/path/core', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/types'], + }, + { + name: '@sentry/types', + location: '/path/types', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, ]; const sorted = topologicalSortPackages(packages); expect(sorted.map(p => p.name)).toEqual([ - '@sentry/types', // no dependencies, comes first - '@sentry/core', // depends on types + '@sentry/types', // no dependencies, comes first + '@sentry/core', // depends on types '@sentry/browser', // depends on core ]); }); test('handles packages with no dependencies', () => { const packages: WorkspacePackage[] = [ - { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: [] }, - { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: [] }, - { name: 'pkg-c', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + { + name: 'pkg-a', + location: '/path/a', + private: false, + hasPublicAccess: false, + workspaceDependencies: [], + }, + { + name: 'pkg-b', + location: '/path/b', + private: false, + hasPublicAccess: false, + workspaceDependencies: [], + }, + { + name: 'pkg-c', + location: '/path/c', + private: false, + hasPublicAccess: false, + workspaceDependencies: [], + }, ]; const sorted = topologicalSortPackages(packages); @@ -201,10 +275,34 @@ describe('topologicalSortPackages', () => { test('handles diamond dependencies', () => { // Diamond: A depends on B and C, both B and C depend on D const packages: WorkspacePackage[] = [ - { name: 'A', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['B', 'C'] }, - { name: 'B', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['D'] }, - { name: 'C', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: ['D'] }, - { name: 'D', location: '/path/d', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + { + name: 'A', + location: '/path/a', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['B', 'C'], + }, + { + name: 'B', + location: '/path/b', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['D'], + }, + { + name: 'C', + location: '/path/c', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['D'], + }, + { + name: 'D', + location: '/path/d', + private: false, + hasPublicAccess: false, + workspaceDependencies: [], + }, ]; const sorted = topologicalSortPackages(packages); @@ -215,8 +313,20 @@ describe('topologicalSortPackages', () => { test('ignores dependencies not in the package list', () => { const packages: WorkspacePackage[] = [ - { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['external-dep', 'pkg-b'] }, - { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['lodash'] }, + { + name: 'pkg-a', + location: '/path/a', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['external-dep', 'pkg-b'], + }, + { + name: 'pkg-b', + location: '/path/b', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['lodash'], + }, ]; const sorted = topologicalSortPackages(packages); @@ -227,24 +337,80 @@ describe('topologicalSortPackages', () => { test('throws error on circular dependencies', () => { const packages: WorkspacePackage[] = [ - { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-b'] }, - { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-c'] }, - { name: 'pkg-c', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-a'] }, + { + name: 'pkg-a', + location: '/path/a', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['pkg-b'], + }, + { + name: 'pkg-b', + location: '/path/b', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['pkg-c'], + }, + { + name: 'pkg-c', + location: '/path/c', + private: false, + hasPublicAccess: false, + workspaceDependencies: ['pkg-a'], + }, ]; - expect(() => topologicalSortPackages(packages)).toThrow(/Circular dependency/); + expect(() => topologicalSortPackages(packages)).toThrow( + /Circular dependency/, + ); }); test('handles complex dependency graph with multiple branches', () => { // Real-world-like setup similar to sentry-javascript: // types -> core -> (browser, node-core -> node) -> nextjs (depends on browser and node) const packages: WorkspacePackage[] = [ - { name: '@sentry/nextjs', location: '/path/nextjs', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/browser', '@sentry/node'] }, - { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, - { name: '@sentry/node', location: '/path/node', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/node-core'] }, - { name: '@sentry/node-core', location: '/path/node-core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, - { name: '@sentry/core', location: '/path/core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/types'] }, - { name: '@sentry/types', location: '/path/types', private: false, hasPublicAccess: true, workspaceDependencies: [] }, + { + name: '@sentry/nextjs', + location: '/path/nextjs', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/browser', '@sentry/node'], + }, + { + name: '@sentry/browser', + location: '/path/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/core'], + }, + { + name: '@sentry/node', + location: '/path/node', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/node-core'], + }, + { + name: '@sentry/node-core', + location: '/path/node-core', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/core'], + }, + { + name: '@sentry/core', + location: '/path/core', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/types'], + }, + { + name: '@sentry/types', + location: '/path/types', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, ]; const sorted = topologicalSortPackages(packages); @@ -255,12 +421,12 @@ describe('topologicalSortPackages', () => { // types -> core -> node-core -> node (branch 2) // nextjs depends on both browser and node expect(names).toEqual([ - '@sentry/types', // depth 0: no dependencies - '@sentry/core', // depth 1: depends on types - '@sentry/browser', // depth 2: depends on core + '@sentry/types', // depth 0: no dependencies + '@sentry/core', // depth 1: depends on types + '@sentry/browser', // depth 2: depends on core '@sentry/node-core', // depth 2: depends on core - '@sentry/node', // depth 3: depends on node-core - '@sentry/nextjs', // depth 4: depends on browser and node + '@sentry/node', // depth 3: depends on node-core + '@sentry/nextjs', // depth 4: depends on browser and node ]); }); }); diff --git a/src/utils/gcsApi.ts b/src/utils/gcsApi.ts index aa684ab1..143ff11e 100644 --- a/src/utils/gcsApi.ts +++ b/src/utils/gcsApi.ts @@ -71,7 +71,7 @@ interface GCSCreds { */ export function getGCSCredsFromEnv( jsonVar: RequiredConfigVar, - filepathVar: RequiredConfigVar + filepathVar: RequiredConfigVar, ): GCSCreds | null { // Check if either credential source is provided const gcsCredsJson = process.env[jsonVar.name]; @@ -80,7 +80,7 @@ export function getGCSCredsFromEnv( // If no credentials are provided, return null to indicate ADC should be used if (!gcsCredsJson && !gcsCredsPath) { logger.debug( - 'No GCS credentials provided, will use Application Default Credentials' + 'No GCS credentials provided, will use Application Default Credentials', ); return null; } @@ -139,10 +139,10 @@ export class CraftGCSClient { this.bucket = new GCSBucket( new GCSStorage({ credentials, - maxRetries, + retryOptions: { maxRetries }, projectId, }), - bucketName + bucketName, ); } @@ -155,7 +155,7 @@ export class CraftGCSClient { */ public async uploadArtifact( artifactLocalPath: string, - bucketPath: BucketPath + bucketPath: BucketPath, ): Promise { const filename = path.basename(artifactLocalPath); let pathInBucket = bucketPath.path; @@ -171,7 +171,7 @@ export class CraftGCSClient { if (!artifactLocalPath) { reportError( `Unable to upload file \`${filename}\` - ` + - `no local path to file specified!` + `no local path to file specified!`, ); } @@ -189,7 +189,7 @@ export class CraftGCSClient { // any way, so setting it on upload is pointless. Action item: fix this. if (contentType) { logger.debug( - `Detected \`${filename}\` to be of type \`${contentType}\`.` + `Detected \`${filename}\` to be of type \`${contentType}\`.`, ); } const metadata = { @@ -204,12 +204,14 @@ export class CraftGCSClient { }; logger.trace( - `File \`${filename}\`, upload options: ${formatJson(uploadConfig)}` + `File \`${filename}\`, upload options: ${formatJson(uploadConfig)}`, ); const destination = path.posix.join(this.bucketName, pathInBucket); await safeExec(async () => { - logger.debug(`Attempting to upload \`${filename}\` to \`${destination}\`.`); + logger.debug( + `Attempting to upload \`${filename}\` to \`${destination}\`.`, + ); try { await this.bucket.upload(artifactLocalPath, uploadConfig); @@ -225,8 +227,8 @@ export class CraftGCSClient { 'gs://', this.bucketName, pathInBucket, - filename - )} \`.` + filename, + )} \`.`, ); }, `upload ${filename} to ${destination}`); } @@ -245,12 +247,12 @@ export class CraftGCSClient { public async downloadArtifact( downloadFilepath: string, destinationDirectory: string, - destinationFilename: string = path.basename(downloadFilepath) + destinationFilename: string = path.basename(downloadFilepath), ): Promise { if (!fs.existsSync(destinationDirectory)) { reportError( `Unable to download \`${destinationFilename}\` to ` + - `\`${destinationDirectory}\` - directory does not exist!` + `\`${destinationDirectory}\` - directory does not exist!`, ); } @@ -258,7 +260,7 @@ export class CraftGCSClient { const result = await safeExec(async () => { logger.debug( - `Attempting to download \`${destinationFilename}\` to \`${destinationDirectory}\`.` + `Attempting to download \`${destinationFilename}\` to \`${destinationDirectory}\`.`, ); try { @@ -300,7 +302,7 @@ export class CraftGCSClient { filename, mimeType, storedFile: { - downloadFilepath, + downloadFilepath: downloadFilepath ?? name, filename, lastUpdated, size: Number(size), @@ -319,20 +321,19 @@ export class CraftGCSClient { public async listArtifactsForRevision( repoOwner: string, repoName: string, - revision: string + revision: string, ): Promise { - let filesResponse: GCSFile[][] = [[]]; + let files: GCSFile[] = []; const prefix = path.posix.join(repoOwner, repoName, revision); logger.debug(`Looking for files starting with '${prefix}'`); try { - filesResponse = await this.bucket.getFiles({ prefix }); + [files] = await this.bucket.getFiles({ prefix }); } catch (err) { reportError( - `Error retrieving artifact list from GCS: ${formatJson(err)}` + `Error retrieving artifact list from GCS: ${formatJson(err)}`, ); } - const files = filesResponse[0]; return files.map(gcsFile => this.convertToRemoteArtifact(gcsFile)); } } diff --git a/src/utils/git.ts b/src/utils/git.ts index b3def601..523b402b 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,5 +1,9 @@ -// eslint-disable-next-line no-restricted-imports -- This is the wrapper module -import simpleGit, { type SimpleGit, type LogOptions, type Options, type StatusResult } from 'simple-git'; +import simpleGit, { + type SimpleGit, + type LogOptions, + type Options, + type StatusResult, +} from 'simple-git'; import { getConfigFileDir } from '../config'; import { ConfigurationError } from './errors'; @@ -26,14 +30,14 @@ export const defaultInitialTag = '0.0.0'; export async function getDefaultBranch( git: SimpleGit, - remoteName: string + remoteName: string, ): Promise { // This part is courtesy of https://stackoverflow.com/a/62397081/90297 return stripRemoteName( await git .remote(['set-head', remoteName, '--auto']) .revparse(['--abbrev-ref', `${remoteName}/HEAD`]), - remoteName + remoteName, ); } @@ -45,8 +49,7 @@ export async function getLatestTag(git: SimpleGit): Promise { // If there are no tags, return an empty string if ( err instanceof Error && - ( - err.message.startsWith('fatal: No names found') || + (err.message.startsWith('fatal: No names found') || err.message.startsWith('Nothing to describe')) ) { return ''; @@ -58,7 +61,7 @@ export async function getLatestTag(git: SimpleGit): Promise { export async function getChangesSince( git: SimpleGit, rev: string, - until?: string + until?: string, ): Promise { const gitLogArgs: Options | LogOptions = { to: until || 'HEAD', @@ -90,7 +93,7 @@ export async function getChangesSince( export function stripRemoteName( branch: string | undefined, - remoteName: string + remoteName: string, ): string { const branchName = branch || ''; const remotePrefix = `${remoteName}/`; @@ -104,7 +107,7 @@ export async function getGitClient(): Promise { const configFileDir = getConfigFileDir() || '.'; // Move to the directory where the config file is located process.chdir(configFileDir); - logger.debug("Working directory:", process.cwd()); + logger.debug('Working directory:', process.cwd()); // eslint-disable-next-line no-restricted-syntax -- This is the git wrapper module const git = simpleGit(configFileDir); @@ -144,7 +147,7 @@ export function createGitClient(directory: string): SimpleGit { export async function cloneRepo( url: string, targetDirectory: string, - options?: string[] + options?: string[], ): Promise { // eslint-disable-next-line no-restricted-syntax -- This is the git wrapper module const git = simpleGit(); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index da95e731..09d0ebab 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -9,22 +9,35 @@ export function envToBool(envVar: unknown): boolean { return !FALSY_ENV_VALUES.has(normalized); } -interface GlobalFlags { - [flag: string]: any; +export interface GlobalFlags { + [flag: string]: unknown; + 'dry-run'?: boolean; + 'no-input'?: boolean; + 'log-level'?: keyof typeof LogLevel; +} + +/** Internal type with required values (initialized with defaults) */ +interface InternalGlobalFlags { 'dry-run': boolean; 'no-input': boolean; 'log-level': keyof typeof LogLevel; } -const GLOBAL_FLAGS: GlobalFlags = { +const GLOBAL_FLAGS: InternalGlobalFlags = { 'dry-run': false, 'no-input': false, 'log-level': 'Info', }; export function setGlobals(argv: GlobalFlags): void { - for (const globalFlag of Object.keys(GLOBAL_FLAGS)) { - GLOBAL_FLAGS[globalFlag] = argv[globalFlag]; + if (argv['dry-run'] !== undefined) { + GLOBAL_FLAGS['dry-run'] = argv['dry-run']; + } + if (argv['no-input'] !== undefined) { + GLOBAL_FLAGS['no-input'] = argv['no-input']; + } + if (argv['log-level'] !== undefined) { + GLOBAL_FLAGS['log-level'] = argv['log-level']; } logger.trace('Global flags:', GLOBAL_FLAGS); setLevel(LogLevel[GLOBAL_FLAGS['log-level']]); @@ -78,7 +91,7 @@ export function setGitHubActionsOutput(name: string, value: string): void { const delimiter = `EOF_${Date.now()}_${Math.random().toString(36).slice(2)}`; appendFileSync( outputFile, - `${name}<<${delimiter}\n${value}\n${delimiter}\n` + `${name}<<${delimiter}\n${value}\n${delimiter}\n`, ); } else { appendFileSync(outputFile, `${name}=${value}\n`); diff --git a/src/utils/registry.ts b/src/utils/registry.ts index b285a734..81babe96 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -77,7 +77,7 @@ export async function getPackageManifest( type: RegistryPackageType, canonicalName: string, version: string, - initialManifestData?: InitialManifestData + initialManifestData?: InitialManifestData, ): Promise<{ versionFilePath: string; packageManifest: any }> { const packageDirPath = getPackageDirPath(type, canonicalName); const fullPackageDir = path.join(baseDir, packageDirPath); @@ -93,20 +93,23 @@ export async function getPackageManifest( if (!existsSync(packageManifestPath)) { if (!initialManifestData) { reportError( - `Package "${canonicalName}" does not exist in the registry and no initial manifest data was provided.` + `Package "${canonicalName}" does not exist in the registry and no initial manifest data was provided.`, ); + // reportError throws in non-dry-run mode, but TypeScript doesn't know that + // This is unreachable in practice, but needed for type narrowing + throw new Error('Unreachable'); } // Create directory structure if it doesn't exist if (!existsSync(fullPackageDir)) { logger.info( - `Creating new package directory for "${canonicalName}" at "${packageDirPath}"...` + `Creating new package directory for "${canonicalName}" at "${packageDirPath}"...`, ); mkdirSync(fullPackageDir, { recursive: true }); } logger.info( - `Creating initial manifest for new package "${canonicalName}"...` + `Creating initial manifest for new package "${canonicalName}"...`, ); return { versionFilePath, @@ -119,7 +122,7 @@ export async function getPackageManifest( versionFilePath, packageManifest: JSON.parse( - await fsPromises.readFile(packageManifestPath, { encoding: 'utf-8' }) + await fsPromises.readFile(packageManifestPath, { encoding: 'utf-8' }), ) || {}, }; } @@ -137,7 +140,7 @@ export async function updateManifestSymlinks( updatedManifest: unknown, version: string, versionFilePath: string, - previousVersion: string + previousVersion: string, ): Promise { const manifestString = JSON.stringify(updatedManifest, undefined, 2) + '\n'; logger.trace('Updated manifest', manifestString); @@ -148,5 +151,5 @@ export async function updateManifestSymlinks( export const DEFAULT_REGISTRY_REMOTE = new GitHubRemote( 'getsentry', - 'sentry-release-registry' + 'sentry-release-registry', ); diff --git a/src/utils/tracing.ts b/src/utils/tracing.ts index c2f1c3ed..55e084b9 100644 --- a/src/utils/tracing.ts +++ b/src/utils/tracing.ts @@ -1,5 +1,8 @@ import * as Sentry from '@sentry/node'; +/** Inferred type from Sentry.startSpan's first parameter */ +type StartSpanOptions = Parameters[0]; + /** * Wraps a function with Sentry tracing * @@ -9,7 +12,7 @@ import * as Sentry from '@sentry/node'; */ export function withTracing any>( fn: T, - spanOptions: Partial = {} + spanOptions: Partial = {}, ): (...args: Parameters) => ReturnType { return (...args: Parameters): ReturnType => Sentry.startSpan( @@ -18,6 +21,6 @@ export function withTracing any>( attributes: { args: JSON.stringify(args) }, ...spanOptions, }, - () => fn(...args) + () => fn(...args), ); } diff --git a/src/utils/workspaces.ts b/src/utils/workspaces.ts index b4fa5bfa..516ec26d 100644 --- a/src/utils/workspaces.ts +++ b/src/utils/workspaces.ts @@ -94,7 +94,7 @@ function getAllDependencyNames(packageJson: PackageJson): string[] { * Handles both array format and object format with packages property */ function extractWorkspacesGlobs( - workspaces: string[] | { packages?: string[] } | undefined + workspaces: string[] | { packages?: string[] } | undefined, ): string[] { if (!workspaces) { return []; @@ -110,10 +110,13 @@ function extractWorkspacesGlobs( */ async function resolveWorkspaceGlobs( rootDir: string, - patterns: string[] + patterns: string[], ): Promise { // First: collect all workspace package names and locations - const workspaceLocations: Array<{ location: string; packageJson: PackageJson }> = []; + const workspaceLocations: Array<{ + location: string; + packageJson: PackageJson; + }> = []; const workspaceNames = new Set(); for (const pattern of patterns) { @@ -139,7 +142,7 @@ async function resolveWorkspaceGlobs( private: packageJson.private ?? false, hasPublicAccess: packageJson.publishConfig?.access === 'public', workspaceDependencies: getAllDependencyNames(packageJson).filter(dep => - workspaceNames.has(dep) + workspaceNames.has(dep), ), })); } @@ -151,7 +154,7 @@ function fileExists(filePath: string): boolean { try { readFileSync(filePath); return true; - } catch (err) { + } catch { return false; } } @@ -160,7 +163,7 @@ function fileExists(filePath: string): boolean { * Discover npm/yarn workspaces from package.json */ async function discoverNpmYarnWorkspaces( - rootDir: string + rootDir: string, ): Promise { const packageJson = readPackageJson(rootDir); if (!packageJson) { @@ -180,7 +183,7 @@ async function discoverNpmYarnWorkspaces( logger.debug( `Discovered ${ packages.length - } ${type} workspace packages from ${workspacesGlobs.join(', ')}` + } ${type} workspace packages from ${workspacesGlobs.join(', ')}`, ); return { type, packages }; @@ -190,7 +193,7 @@ async function discoverNpmYarnWorkspaces( * Discover pnpm workspaces from pnpm-workspace.yaml */ async function discoverPnpmWorkspaces( - rootDir: string + rootDir: string, ): Promise { const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml'); @@ -214,8 +217,8 @@ async function discoverPnpmWorkspaces( logger.debug( `Discovered ${packages.length} pnpm workspace packages from ${patterns.join( - ', ' - )}` + ', ', + )}`, ); return { type: 'pnpm', packages }; @@ -233,7 +236,7 @@ async function discoverPnpmWorkspaces( * @returns Discovery result with type and packages, or null if not a workspace */ export async function discoverWorkspaces( - rootDir: string + rootDir: string, ): Promise { // Try pnpm first (more specific) const pnpmResult = await discoverPnpmWorkspaces(rootDir); @@ -292,7 +295,7 @@ function escapeRegex(str: string): string { export function packageNameToArtifactFromTemplate( packageName: string, template: string, - version = '\\d.*' + version = '\\d.*', ): string { const simpleName = packageName.replace(/^@/, '').replace(/\//g, '-'); @@ -316,11 +319,11 @@ export function packageNameToArtifactFromTemplate( result = result .replace( new RegExp(escapeRegex(NAME_PLACEHOLDER), 'g'), - escapeRegex(packageName) + escapeRegex(packageName), ) .replace( new RegExp(escapeRegex(SIMPLE_PLACEHOLDER), 'g'), - escapeRegex(simpleName) + escapeRegex(simpleName), ) .replace(new RegExp(escapeRegex(VERSION_PLACEHOLDER), 'g'), versionValue); @@ -338,7 +341,7 @@ export function packageNameToArtifactFromTemplate( export function filterWorkspacePackages( packages: WorkspacePackage[], includePattern?: RegExp, - excludePattern?: RegExp + excludePattern?: RegExp, ): WorkspacePackage[] { return packages.filter(pkg => { // Check exclude pattern first @@ -365,7 +368,7 @@ export function filterWorkspacePackages( * @throws Error if there's a circular dependency */ export function topologicalSortPackages( - packages: WorkspacePackage[] + packages: WorkspacePackage[], ): WorkspacePackage[] { // Map package name to its workspace dependencies const depsMap = new Map(); @@ -387,7 +390,7 @@ export function topologicalSortPackages( if (computing.has(name)) { const cyclePackages = Array.from(computing); throw new Error( - `Circular dependency detected among workspace packages: ${cyclePackages.join(', ')}` + `Circular dependency detected among workspace packages: ${cyclePackages.join(', ')}`, ); } diff --git a/tsconfig.json b/tsconfig.json index d8528c94..b03d6a4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,9 @@ "extends": "./tsconfig.build.json", "compilerOptions": { "types": ["node", "vitest/globals"], - "plugins": [] + "plugins": [], + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" }, "include": ["src/**/*.ts", "**/__mocks__/**/*.ts", "**/__tests_/**/*.ts"], "exclude": ["dist/**/*"]