mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
5b1749983c
As noted in #43761, we had recently set `CI=true` for the Renovate and Containerbase orgs on Mend-hosted apps due to a pnpm v11 requirement: ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY Aborted removal of modules directory due to no TTY If you are running pnpm in CI, set the CI environment variable to "true", or set "confirmModulesPurge" to "false". Similarly in #43606, it was noted that we should do this for `mise lock` to mark the workspace as "trusted". Although we went with a different situation, there will be cases where this is useful for tools being executed, for instance when we invoke `yarn`. We also make sure to keep our documentation up-to-date as we add to the specific overrides. With help from Claude Sonnet and Opus to: - perform some of the mechanical updates as part of this change - ensure that the Docker sidecar always has `hardcodedProcessEnv` passed into it - catch a needed change to `exposeAllEnv` handling Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io> Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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<StatusResult>({
|
|
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}'`,
|
|
},
|
|
]);
|
|
});
|
|
});
|