diff --git a/.changeset/hot-crews-swim.md b/.changeset/hot-crews-swim.md new file mode 100644 index 000000000..8b65c377b --- /dev/null +++ b/.changeset/hot-crews-swim.md @@ -0,0 +1,5 @@ +--- +'skuba': minor +--- + +lint: Removes obsolete version field from docker-compose.yml files diff --git a/src/cli/__snapshots__/format.int.test.ts.snap b/src/cli/__snapshots__/format.int.test.ts.snap index ee78344fe..87e57d0c6 100644 --- a/src/cli/__snapshots__/format.int.test.ts.snap +++ b/src/cli/__snapshots__/format.int.test.ts.snap @@ -19,6 +19,8 @@ Patch skipped: Move .npmrc out of the .dockerignore managed section - no .docker Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found +Patch skipped: Remove version field from docker-compose files - no docker-compose files found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -94,6 +96,8 @@ Patch skipped: Move .npmrc out of the .dockerignore managed section - no .docker Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found +Patch skipped: Remove version field from docker-compose files - no docker-compose files found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -166,6 +170,8 @@ Patch skipped: Move .npmrc out of the .dockerignore managed section - no .docker Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found +Patch skipped: Remove version field from docker-compose files - no docker-compose files found + skuba update complete. Refreshed .eslintignore. refresh-config-files @@ -207,6 +213,8 @@ Patch skipped: Move .npmrc out of the .dockerignore managed section - no .docker Patch skipped: Ensure the pnpm package manager version specified in package.json is used in Dockerfiles - no packageManager declaration in package.json found +Patch skipped: Remove version field from docker-compose files - no docker-compose files found + skuba update complete. Refreshed .eslintignore. refresh-config-files diff --git a/src/cli/lint/internalLints/upgrade/patches/8.2.1/index.ts b/src/cli/lint/internalLints/upgrade/patches/8.2.1/index.ts new file mode 100644 index 000000000..504283ee0 --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.2.1/index.ts @@ -0,0 +1,10 @@ +import type { Patches } from '../..'; + +import { tryPatchDockerComposeFiles } from './patchDockerCompose'; + +export const patches: Patches = [ + { + apply: tryPatchDockerComposeFiles, + description: 'Remove version field from docker-compose files', + }, +]; diff --git a/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.test.ts b/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.test.ts new file mode 100644 index 000000000..652fb7399 --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.test.ts @@ -0,0 +1,85 @@ +import fg from 'fast-glob'; +import { readFile, writeFile } from 'fs-extra'; + +import type { PatchConfig } from '../..'; + +import { tryPatchDockerComposeFiles } from './patchDockerCompose'; +jest.mock('fast-glob'); +jest.mock('fs-extra'); + +describe('patchDockerComposeFile', () => { + afterEach(() => jest.resetAllMocks()); + + const mockDockerComposeFile = 'docker-compose.yml'; + const mockDockerComposeContents = + 'services:\n' + + 'app:\n' + + "image: ${BUILDKITE_PLUGIN_DOCKER_IMAGE:-''}\n" + + 'init: true\n' + + 'volumes:'; + const mockPatchableDockerComposeContents = `version: '3.8'\n${mockDockerComposeContents}`; + + it('should skip if no Dockerfile is found', async () => { + jest.mocked(fg).mockResolvedValueOnce([]); + await expect( + tryPatchDockerComposeFiles({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'no docker-compose files found', + }); + }); + it('should patch docker-compose files with version field', async () => { + jest.mocked(fg).mockResolvedValueOnce([mockDockerComposeFile]); + jest + .mocked(readFile) + .mockResolvedValueOnce(mockPatchableDockerComposeContents as never); + await expect( + tryPatchDockerComposeFiles({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + expect(writeFile).toHaveBeenCalledWith( + 'docker-compose.yml', + 'services:\n' + + 'app:\n' + + "image: ${BUILDKITE_PLUGIN_DOCKER_IMAGE:-''}\n" + + 'init: true\n' + + 'volumes:', + ); + }); + it('should skip if no docker-compose files contain a version field', async () => { + jest.mocked(fg).mockResolvedValueOnce([mockDockerComposeFile]); + jest + .mocked(readFile) + .mockResolvedValueOnce(mockDockerComposeContents as never); + await expect( + tryPatchDockerComposeFiles({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ + result: 'skip', + reason: 'no docker-compose files to patch', + }); + }); + it('should not remove intended version in docker compose file', async () => { + jest.mocked(fg).mockResolvedValueOnce([mockDockerComposeFile]); + jest + .mocked(readFile) + .mockResolvedValueOnce( + `${mockPatchableDockerComposeContents}\n version: 7\nversion: 0.2` as never, + ); + await expect( + tryPatchDockerComposeFiles({ mode: 'format' } as PatchConfig), + ).resolves.toEqual({ + result: 'apply', + }); + expect(writeFile).toHaveBeenCalledWith( + 'docker-compose.yml', + 'services:\n' + + 'app:\n' + + "image: ${BUILDKITE_PLUGIN_DOCKER_IMAGE:-''}\n" + + 'init: true\n' + + 'volumes:\n' + + ' version: 7\n' + + 'version: 0.2', + ); + }); +}); diff --git a/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.ts b/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.ts new file mode 100644 index 000000000..d6694deae --- /dev/null +++ b/src/cli/lint/internalLints/upgrade/patches/8.2.1/patchDockerCompose.ts @@ -0,0 +1,77 @@ +import { inspect } from 'util'; + +import fg from 'fast-glob'; +import { readFile, writeFile } from 'fs-extra'; + +import type { PatchFunction, PatchReturnType } from '../..'; +import { log } from '../../../../../../utils/logging'; + +const DOCKER_COMPOSE_VERSION_REGEX = /^version: ['"]?\d+(\.\d+)*['"]?\n*/m; + +const fetchFiles = async (files: string[]) => + Promise.all( + files.map(async (file) => { + const contents = await readFile(file, 'utf8'); + + return { + file, + contents, + }; + }), + ); + +const patchDockerComposeFiles: PatchFunction = async ({ + mode, +}): Promise => { + const maybeDockerComposeFiles = await Promise.resolve( + fg(['docker-compose*.yml']), + ); + + if (!maybeDockerComposeFiles.length) { + return { + result: 'skip', + reason: 'no docker-compose files found', + }; + } + + const dockerComposeFiles = await fetchFiles(maybeDockerComposeFiles); + + const dockerComposeFilesToPatch = dockerComposeFiles.filter(({ contents }) => + contents.match(DOCKER_COMPOSE_VERSION_REGEX), + ); + + if (!dockerComposeFilesToPatch.length) { + return { + result: 'skip', + reason: 'no docker-compose files to patch', + }; + } + + if (mode === 'lint') { + return { + result: 'apply', + }; + } + + await Promise.all( + dockerComposeFilesToPatch.map(async ({ file, contents }) => { + const patchedContents = contents.replace( + DOCKER_COMPOSE_VERSION_REGEX, + '', + ); + await writeFile(file, patchedContents); + }), + ); + + return { result: 'apply' }; +}; + +export const tryPatchDockerComposeFiles: PatchFunction = async (config) => { + try { + return await patchDockerComposeFiles(config); + } catch (err) { + log.warn('Failed to patch pnpm packageManager CI configuration.'); + log.subtle(inspect(err)); + return { result: 'skip', reason: 'due to an error' }; + } +};