diff --git a/.changeset/unlucky-books-grow.md b/.changeset/unlucky-books-grow.md new file mode 100644 index 000000000..f763d17e3 --- /dev/null +++ b/.changeset/unlucky-books-grow.md @@ -0,0 +1,5 @@ +--- +'skuba': minor +--- + +lint: Patch installing specific pnpm version via Corepack diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index 2354b913b..ee78344fe 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -17,6 +17,8 @@ Patch skipped: Move .npmrc out of the .gitignore managed section - no .gitignore Patch skipped: Move .npmrc out of the .dockerignore managed section - no .dockerignore file found +Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -90,6 +92,8 @@ Patch skipped: Move .npmrc out of the .gitignore managed section - no .gitignore Patch skipped: Move .npmrc out of the .dockerignore managed section - no .dockerignore file found +Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -160,6 +164,8 @@ Patch skipped: Move .npmrc out of the .gitignore managed section - no .gitignore Patch skipped: Move .npmrc out of the .dockerignore managed section - no .dockerignore file found +Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -199,6 +205,8 @@ Patch skipped: Move .npmrc out of the .gitignore managed section - not ignored Patch skipped: Move .npmrc out of the .dockerignore managed section - no .dockerignore file found +Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found + skuba update complete. Refreshed .eslintignore. refresh-config-files diff --git a/src/cli/init/index.ts b/src/cli/init/index.ts index dfa90608f..d8d5332ca 100644 --- a/src/cli/init/index.ts +++ b/src/cli/init/index.ts @@ -8,6 +8,8 @@ import { createInclusionFilter } from '../../utils/dir'; import { createExec, ensureCommands } from '../../utils/exec'; import { createLogger, log } from '../../utils/logging'; import { showLogoAndVersionInfo } from '../../utils/logo'; +import { getConsumerManifest } from '../../utils/manifest'; +import { detectPackageManager } from '../../utils/packageManager'; import { BASE_TEMPLATE_DIR, ensureTemplateConfigDeletion, @@ -87,8 +89,22 @@ export const init = async (args = process.argv.slice(2)) => { log.newline(); await initialiseRepo(destinationDir, templateData); + const [manifest, packageManagerConfig] = await Promise.all([ + getConsumerManifest(), + detectPackageManager(), + ]); + + if (!manifest) { + throw new Error("Repository doesn't contain a package.json file."); + } + // Patch in a baseline Renovate preset based on the configured Git owner. - await tryPatchRenovateConfig('format', destinationDir); + await tryPatchRenovateConfig({ + mode: 'format', + dir: destinationDir, + manifest, + packageManager: packageManagerConfig, + }); const skubaSlug = `skuba@${skubaVersionInfo.local}`; diff --git a/src/cli/lint/internalLints/patchRenovateConfig.test.ts b/src/cli/lint/internalLints/patchRenovateConfig.test.ts index f0a0734a7..a659d29f0 100644 --- a/src/cli/lint/internalLints/patchRenovateConfig.test.ts +++ b/src/cli/lint/internalLints/patchRenovateConfig.test.ts @@ -5,6 +5,7 @@ import memfs, { vol } from 'memfs'; import * as Git from '../../../api/git'; import { tryPatchRenovateConfig } from './patchRenovateConfig'; +import type { PatchConfig } from './upgrade'; jest.mock('fs-extra', () => memfs); @@ -51,7 +52,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ '.git': null, 'renovate.json': JSON }); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -77,7 +80,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ 'foo/.git': null, 'foo/renovate.json': JSON }); - await expect(tryPatchRenovateConfig('format', 'foo')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format', dir: 'foo' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -103,7 +108,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ '.git': null, '.github/renovate.json5': JSON5 }); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -137,7 +144,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no config found', }); @@ -159,7 +168,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'due to an error', }); @@ -181,7 +192,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no Git root found', }); @@ -198,7 +211,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'owner does not map to a SEEK preset', }); @@ -218,7 +233,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'owner does not map to a SEEK preset', }); @@ -238,7 +255,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -260,7 +279,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'config already has a SEEK preset', }); @@ -280,7 +301,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('format')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'config already has a SEEK preset', }); @@ -300,7 +323,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ '.git': null, 'renovate.json': JSON }); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -315,7 +340,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ 'foo/.git': null, 'foo/renovate.json': JSON }); - await expect(tryPatchRenovateConfig('lint', 'foo')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint', dir: 'foo' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -333,7 +360,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON({ '.git': null, '.github/renovate.json5': JSON5 }); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -354,7 +383,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no config found', }); @@ -371,7 +402,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no Git root found', }); @@ -388,7 +421,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'owner does not map to a SEEK preset', }); @@ -408,7 +443,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'owner does not map to a SEEK preset', }); @@ -428,7 +465,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -450,7 +489,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'config already has a SEEK preset', }); @@ -470,7 +511,9 @@ describe('patchRenovateConfig', () => { vol.fromJSON(files); - await expect(tryPatchRenovateConfig('lint')).resolves.toEqual({ + await expect( + tryPatchRenovateConfig({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'config already has a SEEK preset', }); diff --git a/src/cli/lint/internalLints/patchRenovateConfig.ts b/src/cli/lint/internalLints/patchRenovateConfig.ts index cde1c59c1..12782f135 100644 --- a/src/cli/lint/internalLints/patchRenovateConfig.ts +++ b/src/cli/lint/internalLints/patchRenovateConfig.ts @@ -155,10 +155,10 @@ const patchRenovateConfig = async ( return { result: 'apply' }; }; -export const tryPatchRenovateConfig = (async ( - mode: 'format' | 'lint', +export const tryPatchRenovateConfig = (async ({ + mode, dir = process.cwd(), -) => { +}) => { try { // In a monorepo we may be invoked within a subdirectory, but we are working // with Renovate config that should be relative to the repository root. diff --git a/src/cli/lint/internalLints/upgrade/index.ts b/src/cli/lint/internalLints/upgrade/index.ts index 07eb7c995..242d391a0 100644 --- a/src/cli/lint/internalLints/upgrade/index.ts +++ b/src/cli/lint/internalLints/upgrade/index.ts @@ -1,11 +1,15 @@ import path from 'path'; import { readdir, writeFile } from 'fs-extra'; +import type readPkgUp from 'read-pkg-up'; import { gte, sort } from 'semver'; import type { Logger } from '../../../../utils/logging'; import { getConsumerManifest } from '../../../../utils/manifest'; -import { detectPackageManager } from '../../../../utils/packageManager'; +import { + type PackageManagerConfig, + detectPackageManager, +} from '../../../../utils/packageManager'; import { getSkubaVersion } from '../../../../utils/version'; import { formatPackage } from '../../../configure/processing/package'; import type { SkubaPackageJson } from '../../../init/writePackageJson'; @@ -19,9 +23,15 @@ export type Patch = { export type PatchReturnType = | { result: 'apply' } | { result: 'skip'; reason?: string }; -export type PatchFunction = ( - mode: 'format' | 'lint', -) => Promise; + +export type PatchConfig = { + mode: 'format' | 'lint'; + manifest: readPkgUp.NormalizedReadResult; + packageManager: PackageManagerConfig; + dir?: string; +}; + +export type PatchFunction = (config: PatchConfig) => Promise; const getPatches = async (manifestVersion: string): Promise => { const patches = await readdir(path.join(__dirname, 'patches'), { @@ -64,9 +74,10 @@ export const upgradeSkuba = async ( mode: 'lint' | 'format', logger: Logger, ): Promise => { - const [currentVersion, manifest] = await Promise.all([ + const [currentVersion, manifest, packageManager] = await Promise.all([ getSkubaVersion(), getConsumerManifest(), + detectPackageManager(), ]); if (!manifest) { @@ -91,7 +102,14 @@ export const upgradeSkuba = async ( if (mode === 'lint') { const results = await Promise.all( - patches.map(async ({ apply }) => await apply(mode)), + patches.map( + async ({ apply }) => + await apply({ + mode, + manifest, + packageManager, + }), + ), ); // No patches are applicable. Early exit to avoid unnecessary commits. @@ -99,8 +117,6 @@ export const upgradeSkuba = async ( return { ok: true, fixable: false }; } - const packageManager = await detectPackageManager(); - logger.warn( `skuba has patches to apply. Run ${logger.bold( packageManager.exec, @@ -127,7 +143,11 @@ export const upgradeSkuba = async ( // Run these in series in case a subsequent patch relies on a previous patch for (const { apply, description } of patches) { - const result = await apply(mode); + const result = await apply({ + mode, + manifest, + packageManager, + }); logger.newline(); if (result.result === 'skip') { logger.plain( diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.test.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.test.ts index cd0cd75be..1e361597c 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.test.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.test.ts @@ -1,5 +1,6 @@ import fs from 'fs-extra'; +import type { PatchConfig } from '../..'; import * as packageAnalysis from '../../../../../configure/analysis/package'; import * as projectAnalysis from '../../../../../configure/analysis/project'; @@ -26,7 +27,9 @@ describe('tryAddEmptyExports', () => { Promise.resolve(`// ${filename}`), ); - await expect(tryAddEmptyExports('format')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -59,7 +62,9 @@ describe('tryAddEmptyExports', () => { }), ); - await expect(tryAddEmptyExports('format')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); @@ -71,7 +76,9 @@ describe('tryAddEmptyExports', () => { Promise.resolve(undefined), ); - await expect(tryAddEmptyExports('format')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); @@ -85,7 +92,9 @@ describe('tryAddEmptyExports', () => { throw new Error('Something happened!'); }); - await expect(tryAddEmptyExports('format')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'due to an error', }); @@ -106,7 +115,9 @@ describe('tryAddEmptyExports', () => { Promise.resolve(`// ${filename}`), ); - await expect(tryAddEmptyExports('lint')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -128,7 +139,9 @@ describe('tryAddEmptyExports', () => { }), ); - await expect(tryAddEmptyExports('lint')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); @@ -140,7 +153,9 @@ describe('tryAddEmptyExports', () => { Promise.resolve(undefined), ); - await expect(tryAddEmptyExports('lint')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); @@ -154,7 +169,9 @@ describe('tryAddEmptyExports', () => { throw new Error('Something happened!'); }); - await expect(tryAddEmptyExports('lint')).resolves.toEqual({ + await expect( + tryAddEmptyExports({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'due to an error', }); diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.ts index 11c063229..15c12795d 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/addEmptyExports.ts @@ -55,9 +55,7 @@ const addEmptyExports = async (mode: 'format' | 'lint') => { * Tries to add an empty `export {}` statement to the bottom of Jest setup files * for compliance with TypeScript isolated modules. */ -export const tryAddEmptyExports: PatchFunction = async ( - mode: 'format' | 'lint', -) => { +export const tryAddEmptyExports: PatchFunction = async ({ mode }) => { try { return { result: await addEmptyExports(mode) }; } catch (err) { diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.test.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.test.ts index d1f0974a4..e995121b4 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.test.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.test.ts @@ -1,5 +1,6 @@ import fs from 'fs-extra'; +import type { PatchConfig } from '../..'; import * as packageAnalysis from '../../../../../configure/analysis/package'; import * as projectAnalysis from '../../../../../configure/analysis/project'; @@ -32,10 +33,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'format', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'format', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'apply', }); @@ -67,10 +68,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'format', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'format', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'not ignored', @@ -87,10 +88,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'format', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'format', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'already ignored in unmanaged section', @@ -107,10 +108,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'format', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'format', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'not ignored', @@ -129,10 +130,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'lint', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'lint', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'apply', }); @@ -148,10 +149,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'lint', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'lint', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'not ignored', @@ -168,10 +169,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'lint', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'lint', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'already ignored in unmanaged section', @@ -188,10 +189,10 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => { ); await expect( - tryMoveNpmrcOutOfIgnoreManagedSection(fileName)( - 'lint', - '~/project', - ), + tryMoveNpmrcOutOfIgnoreManagedSection(fileName)({ + mode: 'lint', + dir: '~/project', + } as PatchConfig), ).resolves.toEqual({ result: 'skip', reason: 'not ignored', diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.ts index 3b4052980..c0c7d2174 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.ts @@ -76,7 +76,7 @@ const moveNpmrcOutOfIgnoreManagedSection = async ( export const tryMoveNpmrcOutOfIgnoreManagedSection = ( type: '.gitignore' | '.dockerignore', ) => - (async (mode: 'format' | 'lint', dir = process.cwd()) => { + (async ({ mode, dir = process.cwd() }) => { try { return await moveNpmrcOutOfIgnoreManagedSection(mode, dir, type); } catch (err) { diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.test.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.test.ts index d724fb873..ff0cf6f25 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.test.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.test.ts @@ -1,5 +1,7 @@ import memfs, { vol } from 'memfs'; +import type { PatchConfig } from '../..'; + import { tryPatchDockerfile } from './patchDockerfile'; jest.mock('fs-extra', () => memfs); @@ -41,7 +43,9 @@ describe('tryPatchDockerfile', () => { it('patches a Dockerfile with nodejs:18', async () => { vol.fromJSON({ Dockerfile: dockerfile }); - await expect(tryPatchDockerfile('format')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -62,7 +66,9 @@ describe('tryPatchDockerfile', () => { it('patches a Dockerfile with nodejs18-debian11', async () => { vol.fromJSON({ Dockerfile: dockerfileDebian11 }); - await expect(tryPatchDockerfile('format')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -83,7 +89,9 @@ describe('tryPatchDockerfile', () => { it('ignores when a Dockerfile is missing', async () => { vol.fromJSON({}); - await expect(tryPatchDockerfile('format')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no Dockerfile found', }); @@ -92,7 +100,9 @@ describe('tryPatchDockerfile', () => { it('ignores when a Dockerfile is not distroless', async () => { vol.fromJSON({ Dockerfile: dockerfileNonDistroless }); - await expect(tryPatchDockerfile('format')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); @@ -104,7 +114,9 @@ describe('tryPatchDockerfile', () => { it('patches a Dockerfile with nodejs:18', async () => { vol.fromJSON({ Dockerfile: dockerfile }); - await expect(tryPatchDockerfile('lint')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -114,7 +126,9 @@ describe('tryPatchDockerfile', () => { it('patches a Dockerfile with nodejs18-debian11', async () => { vol.fromJSON({ Dockerfile: dockerfileDebian11 }); - await expect(tryPatchDockerfile('lint')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -124,7 +138,9 @@ describe('tryPatchDockerfile', () => { it('ignores when a Dockerfile is missing', async () => { vol.fromJSON({}); - await expect(tryPatchDockerfile('lint')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no Dockerfile found', }); @@ -135,7 +151,9 @@ describe('tryPatchDockerfile', () => { it('ignores when a Dockerfile is not distroless', async () => { vol.fromJSON({ Dockerfile: dockerfileNonDistroless }); - await expect(tryPatchDockerfile('format')).resolves.toEqual({ + await expect( + tryPatchDockerfile({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', }); diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.ts index 1255f2685..1bb4b2d93 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchDockerfile.ts @@ -41,10 +41,10 @@ const patchDockerfile = async ( return { result: 'apply' }; }; -export const tryPatchDockerfile: PatchFunction = async ( - mode: 'format' | 'lint', +export const tryPatchDockerfile: PatchFunction = async ({ + mode, dir = process.cwd(), -) => { +}) => { try { return await patchDockerfile(mode, dir); } catch (err) { diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.test.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.test.ts index 7386bf8bc..213ee4c15 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.test.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.test.ts @@ -4,6 +4,8 @@ import { inspect } from 'util'; import memfs, { vol } from 'memfs'; +import type { PatchConfig } from '../..'; + import { tryPatchServerListener } from './patchServerListener'; jest.mock('fs-extra', () => memfs); @@ -36,7 +38,9 @@ describe('patchServerListener', () => { it('patches a listener with a callback and existing variable reference', async () => { vol.fromJSON({ 'src/listen.ts': LISTENER_WITH_CALLBACK }); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -64,7 +68,9 @@ describe('patchServerListener', () => { it('patches a listener without a callback', async () => { vol.fromJSON({ 'src/listen.ts': LISTENER_WITHOUT_CALLBACK }); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -84,7 +90,9 @@ describe('patchServerListener', () => { }); it('handles a lack of server listener', async () => { - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no listener file found', }); @@ -101,7 +109,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'due to an error', }); @@ -126,7 +136,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -148,7 +160,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -167,7 +181,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -182,7 +198,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no server listener found', }); @@ -197,7 +215,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('format')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no listener file found', }); @@ -212,7 +232,9 @@ describe('patchServerListener', () => { it('patches a listener with a callback and existing variable reference', async () => { vol.fromJSON({ 'src/listen.ts': LISTENER_WITH_CALLBACK }); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -222,7 +244,9 @@ describe('patchServerListener', () => { it('patches a listener without a callback', async () => { vol.fromJSON({ 'src/listen.ts': LISTENER_WITHOUT_CALLBACK }); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'apply', }); @@ -232,7 +256,9 @@ describe('patchServerListener', () => { }); it('handles a lack of server listener', async () => { - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no listener file found', }); @@ -252,7 +278,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -274,7 +302,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -293,7 +323,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'keepAliveTimeout already configured', }); @@ -308,7 +340,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no server listener found', }); @@ -323,7 +357,9 @@ describe('patchServerListener', () => { vol.fromJSON(files); - await expect(tryPatchServerListener('lint')).resolves.toEqual({ + await expect( + tryPatchServerListener({ mode: 'lint' } as PatchConfig), + ).resolves.toEqual({ result: 'skip', reason: 'no listener file found', }); diff --git a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.ts b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.ts index bc25ab0b5..0dea96ae8 100644 --- a/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.ts +++ b/src/cli/lint/internalLints/upgrade/patches/7.3.1/patchServerListener.ts @@ -60,10 +60,10 @@ const patchServerListener = async ( return { result: 'apply' }; }; -export const tryPatchServerListener: PatchFunction = async ( - mode: 'format' | 'lint', +export const tryPatchServerListener: PatchFunction = async ({ + mode, dir = process.cwd(), -) => { +}) => { try { return await patchServerListener(mode, dir); } catch (err) { diff --git a/src/cli/lint/internalLints/upgrade/patches/8.0.0/index.ts b/src/cli/lint/internalLints/upgrade/patches/8.0.0/index.ts new file mode 100644 index 000000000..0bfed04ca --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.0.0/index.ts @@ -0,0 +1,11 @@ +import type { Patches } from '../..'; + +import { tryPatchPnpmPackageManager } from './patchPnpmPackageManager'; + +export const patches: Patches = [ + { + apply: tryPatchPnpmPackageManager, + description: + 'Ensure the pnpm package manager version specified in package.json is used in Dockerfiles', + }, +]; diff --git a/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.test.ts b/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.test.ts new file mode 100644 index 000000000..67e37189d --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.test.ts @@ -0,0 +1,308 @@ +import fg from 'fast-glob'; +import { readFile, writeFile } from 'fs-extra'; +import type { NormalizedPackageJson } from 'read-pkg-up'; + +import type { PatchConfig } from '../..'; +import type { PackageManagerConfig } from '../../../../../../utils/packageManager'; + +import { tryPatchPnpmPackageManager } from './patchPnpmPackageManager'; + +jest.mock('fast-glob'); +jest.mock('fs-extra'); + +describe('patchPnpmPackageManager', () => { + afterEach(() => jest.resetAllMocks()); + + it('should skip if pnpm is not used', async () => { + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'yarn' } as PackageManagerConfig, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'not using pnpm', + }); + }); + + it('should skip if packageManager is not declared in package.json', async () => { + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: { packageJson: {} }, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'no packageManager declaration in package.json found', + }); + }); + + const validManifest = { + packageJson: { + packageManager: 'pnpm', + } as Partial as NormalizedPackageJson, + path: '~/project/package.json', + } as PatchConfig['manifest']; + + it('should skip if no dockerfiles are found', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce(['.buldkite/pipeline.yml']); + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'Either dockerfiles or pipelines were not found', + }); + }); + + it('should skip if no buildkite pipelines are found', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce([]); + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'Either dockerfiles or pipelines were not found', + }); + }); + + it('should skip if dockerfiles and buildkite pipelines do not contain patchable content', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce('RUN pnpm install' as never) + .mockResolvedValueOnce('steps:\n - command: yarn install' as never); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'no pipeline or dockerfiles to patch', + }); + }); + + it('should patch both dockerfiles and pipelines', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce( + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN corepack enable pnpm\n') as never, + ) + .mockResolvedValueOnce( + ('seek-oss/docker-ecr-cache#v2.1.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - pnpm-lock.yaml\n') as never, + ); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + + expect(writeFile).toHaveBeenNthCalledWith( + 1, + 'Dockerfile', + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN --mount=type=bind,source=package.json,target=package.json \\\n' + + ' corepack enable pnpm && corepack install\n') as never, + ); + + expect(writeFile).toHaveBeenNthCalledWith( + 2, + '.buildkite/pipeline.yml', + ('seek-oss/docker-ecr-cache#v2.2.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - package.json#.packageManager\n' + + ' - pnpm-lock.yaml\n') as never, + ); + }); + + it('should not patch in lint mode', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce( + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN corepack enable pnpm\n') as never, + ) + .mockResolvedValueOnce( + ('seek-oss/docker-ecr-cache#v2.1.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - pnpm-lock.yaml\n') as never, + ); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'lint', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + + expect(writeFile).not.toHaveBeenCalled(); + }); + + it('should patch multiple cache entries in pipelines', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce( + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN corepack enable pnpm\n') as never, + ) + .mockResolvedValueOnce( + ('seek-oss/docker-ecr-cache#v2.1.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - pnpm-lock.yaml\n' + + 'seek-oss/docker-ecr-cache#v2.1.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - pnpm-lock.yaml\n') as never, + ); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + + expect(writeFile).toHaveBeenNthCalledWith( + 2, + '.buildkite/pipeline.yml', + ('seek-oss/docker-ecr-cache#v2.2.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - package.json#.packageManager\n' + + ' - pnpm-lock.yaml\n' + + 'seek-oss/docker-ecr-cache#v2.2.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - package.json#.packageManager\n' + + ' - pnpm-lock.yaml\n') as never, + ); + }); + + it('should avoid patching the docker ecr cache plugin version if it is greater than 2.2.0', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce( + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN corepack enable pnpm\n') as never, + ) + .mockResolvedValueOnce( + ('seek-oss/docker-ecr-cache#v2.3.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - pnpm-lock.yaml\n') as never, + ); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + + expect(writeFile).toHaveBeenNthCalledWith( + 2, + '.buildkite/pipeline.yml', + ('seek-oss/docker-ecr-cache#v2.3.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - package.json#.packageManager\n' + + ' - pnpm-lock.yaml\n') as never, + ); + }); + + it('should skip patching if it is already up to date', async () => { + jest + .mocked(fg) + .mockResolvedValueOnce(['Dockerfile']) + .mockResolvedValueOnce(['.buildkite/pipeline.yml']); + jest + .mocked(readFile) + .mockResolvedValueOnce( + ('# syntax=docker/dockerfile:1.7\n' + + 'FROM --platform=arm64 node:20-alpine AS dev-deps\n\n' + + 'RUN --mount=type=bind,source=package.json,target=package.json \\\n' + + ' corepack enable pnpm && corepack install\n') as never, + ) + .mockResolvedValueOnce( + ('seek-oss/docker-ecr-cache#v2.1.0:\n' + + ' cache-on:\n' + + ' - .npmrc\n' + + ' - package.json\n' + + ' - pnpm-lock.yaml\n') as never, + ); + + await expect( + tryPatchPnpmPackageManager({ + mode: 'format', + packageManager: { command: 'pnpm' } as PackageManagerConfig, + manifest: validManifest, + } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'no pipeline or dockerfiles to patch', + }); + + expect(writeFile).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.ts b/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.ts new file mode 100644 index 000000000..d9030d177 --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.0.0/patchPnpmPackageManager.ts @@ -0,0 +1,138 @@ +import { inspect } from 'util'; + +import fg from 'fast-glob'; +import { readFile, writeFile } from 'fs-extra'; +import { lt } from 'semver'; + +import type { PatchFunction, PatchReturnType } from '../..'; +import { log } from '../../../../../../utils/logging'; + +const DOCKERFILE_COREPACK_COMMAND = 'RUN corepack enable pnpm'; +const PACKAGE_JSON_MOUNT = `RUN --mount=type=bind,source=package.json,target=package.json \\ + corepack enable pnpm && corepack install`; +const PACKAGE_JSON_CACHE = '- package.json#.packageManager'; + +const BEFORE_PIPELINE_REGEX = + /(\s*cache-on:\s*\n\s*-\s*\.npmrc\s*\n)((\s*)-\s*pnpm-lock\.yaml)/gm; +const ECR_REGEX = /seek-oss\/docker-ecr-cache#v([\d\.]+)/gm; + +const fetchFiles = async (files: string[]) => + Promise.all( + files.map(async (file) => { + const contents = await readFile(file, 'utf8'); + + return { + file, + contents, + }; + }), + ); + +const patchPnpmPackageManager: PatchFunction = async ({ + mode, + manifest, + packageManager, +}): Promise => { + if (packageManager.command !== 'pnpm') { + return { + result: 'skip', + reason: 'not using pnpm', + }; + } + + if ( + !( + manifest.packageJson as { packageManager?: string } + ).packageManager?.includes('pnpm') + ) { + return { + result: 'skip', + reason: 'no packageManager declaration in package.json found', + }; + } + + const [maybeDockerfiles, maybePipelines] = await Promise.all([ + fg(['Dockerfile*']), + fg(['.buildkite/*.yml']), + ]); + + if (!maybeDockerfiles.length || !maybePipelines.length) { + return { + result: 'skip', + reason: 'Either dockerfiles or pipelines were not found', + }; + } + + const [dockerfiles, pipelines] = await Promise.all([ + fetchFiles(maybeDockerfiles), + fetchFiles(maybePipelines), + ]); + + const dockerFilesToPatch = dockerfiles.filter( + ({ contents }) => + contents.includes(DOCKERFILE_COREPACK_COMMAND) && + !contents.includes('target=package.json'), + ); + + const pipelinesToPatch = pipelines.filter(({ contents }) => + Boolean(BEFORE_PIPELINE_REGEX.exec(contents)), + ); + + if (!dockerFilesToPatch.length && !pipelinesToPatch.length) { + return { + result: 'skip', + reason: 'no pipeline or dockerfiles to patch', + }; + } + + if (mode === 'lint') { + return { result: 'apply' }; + } + + if (dockerFilesToPatch.length) { + await Promise.all( + dockerFilesToPatch.map(async ({ file, contents }) => { + const patchedContent = contents.replace( + DOCKERFILE_COREPACK_COMMAND, + PACKAGE_JSON_MOUNT, + ); + await writeFile(file, patchedContent); + }), + ); + } + + if (pipelinesToPatch.length) { + await Promise.all( + pipelinesToPatch.map(async ({ file, contents }) => { + const patchedContent = contents.replace( + BEFORE_PIPELINE_REGEX, + `$1$3${PACKAGE_JSON_CACHE}\n$2`, + ); + + const patchedEcrContent = patchedContent.replace( + ECR_REGEX, + (match, version) => { + if (typeof version === 'string' && lt(version, '2.2.0')) { + return 'seek-oss/docker-ecr-cache#v2.2.0'; + } + return match; + }, + ); + + await writeFile(file, patchedEcrContent); + }), + ); + } + + return { result: 'apply' }; +}; + +export const tryPatchPnpmPackageManager: PatchFunction = async (config) => { + try { + return await patchPnpmPackageManager(config); + } catch (err) { + log.warn('Failed to patch pnpm packageManager CI configuration.'); + log.subtle(inspect(err)); + return { result: 'skip', reason: 'due to an error' }; + } +};