import type { StatusResult } from 'simple-git'; import upath from 'upath'; import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util.ts'; import { hostRules } from '~test/host-rules.ts'; import { env, fs, git, partial } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { InternalGlobalConfigOptions, RepoGlobalConfig, } from '../../../config/types.ts'; import * as docker from '../../../util/exec/docker/index.ts'; import type { UpdateArtifactsConfig } from '../types.ts'; import { updateArtifacts } from './index.ts'; vi.mock('../../../util/exec/env.ts'); vi.mock('../../../util/fs/index.ts'); const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { // `join` fixes Windows CI localDir: upath.join('/tmp/github/some/repo'), cacheDir: upath.join('/tmp/renovate/cache'), containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'), binarySource: 'global', }; const dockerAdminConfig = { ...adminConfig, binarySource: 'docker', dockerSidecarImage: 'ghcr.io/renovatebot/base-image', } satisfies RepoGlobalConfig; process.env.CONTAINERBASE = 'true'; const config: UpdateArtifactsConfig = {}; const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true }; const updateInputCmd = `nix \ --extra-experimental-features 'nix-command flakes' \ flake update nixpkgs`; const updateInputTokenCmd = `nix \ --extra-experimental-features 'nix-command flakes' \ --extra-access-tokens github.com=token \ flake update nixpkgs`; const lockfileMaintenanceCmd = `nix \ --extra-experimental-features 'nix-command flakes' \ flake update`; // Under `binarySource=docker` the whole command becomes the single-quoted argument // of `bash -l -c`, so the manager's own single quotes are POSIX-escaped as '"'"'. const updateInputCmdInDocker = `nix \ --extra-experimental-features '"'"'nix-command flakes'"'"' \ flake update nixpkgs`; describe('modules/manager/nix/artifacts', () => { beforeEach(() => { env.getChildProcessEnv.mockReturnValue({ ...envMock.basic, LANG: 'en_US.UTF-8', LC_ALL: 'en_US', }); GlobalConfig.set(adminConfig); docker.resetPrefetchedImages(); }); it('returns if no flake.lock found', async () => { const execSnapshots = mockExecAll(); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [], newPackageFileContent: '', config, }); expect(res).toBeNull(); expect(execSnapshots).toEqual([]); }); it('returns null if unchanged', async () => { fs.readLocalFile.mockResolvedValueOnce('content'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: [''], }), ); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config, }); expect(res).toBeNull(); expect(execSnapshots).toMatchObject([{ cmd: updateInputCmd }]); }); it('returns updated flake.lock', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, }); expect(res).toEqual([ { file: { contents: 'new flake.lock', path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([{ cmd: updateInputCmd }]); }); it('adds GitHub token', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); hostRules.add({ matchHost: 'github.com', token: 'token' }); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, }); expect(res).toEqual([ { file: { contents: 'new flake.lock', path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([{ cmd: updateInputTokenCmd }]); }); it('trims "x-access-token:" prefix from GitHub token', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); hostRules.add({ matchHost: 'github.com', token: 'x-access-token:token' }); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, }); expect(res).toEqual([ { file: { contents: 'new flake.lock', path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([{ cmd: updateInputTokenCmd }]); }); it('quotes a GitHub token containing shell metacharacters', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); // hostRules can be set via a repo's own committed config, not only by a // trusted platform admin, so the token itself isn't inherently trusted hostRules.add({ matchHost: 'github.com', token: 'x$(touch pwned)y' }); await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' } }, }); expect(execSnapshots).toMatchObject([ { cmd: `nix --extra-experimental-features 'nix-command flakes' --extra-access-tokens github.com='x$(touch pwned)y' flake update nixpkgs`, }, ]); }); it('supports docker mode', async () => { GlobalConfig.set(dockerAdminConfig); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: { ...config, constraints: { nix: '2.10.0' } }, }); expect(res).toEqual([ { file: { path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'docker pull ghcr.io/renovatebot/base-image' }, { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, { cmd: `docker run --rm --name=renovate_sidecar --label=renovate_child -v "/tmp/github/some/repo":"/tmp/github/some/repo" -v "/tmp/renovate/cache":"/tmp/renovate/cache" -e CI -e CONTAINERBASE_CACHE_DIR -w "/tmp/github/some/repo" ghcr.io/renovatebot/base-image bash -l -c 'install-tool nix 2.10.0 && ${updateInputCmdInDocker}'`, }, ]); }); it('supports install mode', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: { ...config, constraints: { nix: '2.10.0' } }, }); expect(res).toEqual([ { file: { path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool nix 2.10.0' }, { cmd: updateInputCmd, options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('catches errors', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecSequence([new Error('exec error')]); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config, }); expect(res).toEqual([ { artifactError: { fileName: 'flake.lock', stderr: 'exec error' }, }, ]); expect(execSnapshots).toMatchObject([{ cmd: updateInputCmd }]); }); it('returns updated flake.lock when doing lockfile maintenance', async () => { fs.readLocalFile.mockResolvedValueOnce('current flake.lock'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new flake.lock'); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: '{}', config: lockMaintenanceConfig, }); expect(res).toEqual([ { file: { contents: 'new flake.lock', path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([{ cmd: lockfileMaintenanceCmd }]); }); it('uses nix from config', async () => { GlobalConfig.set(dockerAdminConfig); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['flake.lock'], }), ); fs.readLocalFile.mockResolvedValueOnce('new lock'); const res = await updateArtifacts({ packageFileName: 'flake.nix', updatedDeps: [{ depName: 'nixpkgs' }], newPackageFileContent: 'some new content', config: { ...config, constraints: { nix: '2.10.0' }, }, }); expect(res).toEqual([ { file: { path: 'flake.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'docker pull ghcr.io/renovatebot/base-image' }, { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, { cmd: `docker run --rm --name=renovate_sidecar --label=renovate_child -v "/tmp/github/some/repo":"/tmp/github/some/repo" -v "/tmp/renovate/cache":"/tmp/renovate/cache" -e CI -e CONTAINERBASE_CACHE_DIR -w "/tmp/github/some/repo" ghcr.io/renovatebot/base-image bash -l -c 'install-tool nix 2.10.0 && ${updateInputCmdInDocker}'`, }, ]); }); });