Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(manager): add devbox manager module #33638

Merged
merged 15 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as copier from './copier';
import * as cpanfile from './cpanfile';
import * as crossplane from './crossplane';
import * as depsEdn from './deps-edn';
import * as devbox from './devbox';
import * as devContainer from './devcontainer';
import * as dockerCompose from './docker-compose';
import * as dockerfile from './dockerfile';
Expand Down Expand Up @@ -135,6 +136,7 @@ api.set('copier', copier);
api.set('cpanfile', cpanfile);
api.set('crossplane', crossplane);
api.set('deps-edn', depsEdn);
api.set('devbox', devbox);
api.set('devcontainer', devContainer);
api.set('docker-compose', dockerCompose);
api.set('dockerfile', dockerfile);
Expand Down
243 changes: 243 additions & 0 deletions lib/modules/manager/devbox/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { codeBlock } from 'common-tags';
import { mockExecAll } from '../../../../test/exec-util';
import { fs, git, partial } from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import type { StatusResult } from '../../../util/git/types';
import type { UpdateArtifact } from '../types';
import { updateArtifacts } from './artifacts';

jest.mock('../../../util/exec/env');
jest.mock('../../../util/git');
jest.mock('../../../util/fs');

const globalConfig: RepoGlobalConfig = {
localDir: '',
};

const devboxJson = codeBlock`
{
"$schema": "https://raw.githubusercontent.com/jetpack-io/devbox/0.10.1/.schema/devbox.schema.json",
"packages": ["nodejs@20", "metabase@0.49.1", "postgresql@latest", "gh@latest"],
}
`;

describe('modules/manager/devbox/artifacts', () => {
describe('updateArtifacts()', () => {
let updateArtifact: UpdateArtifact;

beforeEach(() => {
GlobalConfig.set(globalConfig);
updateArtifact = {
config: {},
newPackageFileContent: '',
packageFileName: '',
updatedDeps: [],
};
});

it('skips if no updatedDeps and no lockFileMaintenance', async () => {
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('skips if no lock file in config', async () => {
updateArtifact.updatedDeps = [{}];
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('skips if cannot read lock file', async () => {
updateArtifact.updatedDeps = [
{ manager: 'devbox', lockFiles: ['devbox.lock'] },
];
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('returns installed devbox.lock', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
const execSnapshots = mockExecAll();
const oldLockFileContent = Buffer.from('Old devbox.lock');
const newLockFileContent = Buffer.from('New devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{ manager: 'devbox', lockFiles: ['devbox.lock'] }],
config: {},
}),
).toEqual([
{
file: {
type: 'addition',
path: 'devbox.lock',
contents: newLockFileContent,
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'devbox install',
options: {
cwd: '.',
encoding: 'utf-8',
env: {},
maxBuffer: 10485760,
timeout: 900000,
},
},
]);
});

it('returns updated devbox.lock', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: ['devbox.lock'],
}),
);
const oldLockFileContent = Buffer.from('old devbox.lock');
const newLockFileContent = Buffer.from('New devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{}],
config: {
isLockFileMaintenance: true,
},
}),
).toEqual([
{
file: {
type: 'addition',
path: 'devbox.lock',
contents: newLockFileContent,
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'devbox update',
options: {
cwd: '.',
encoding: 'utf-8',
env: {},
maxBuffer: 10485760,
timeout: 900000,
},
},
]);
});

it('returns null if no changes are found', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: [],
}),
);
mockExecAll();
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [],
config: {},
}),
).toBeNull();
});

it('returns null if devbox.lock not found after update', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: [],
}),
);
mockExecAll();
const oldLockFileContent = Buffer.from('Old devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{}],
config: {},
}),
).toBeNull();
});

it('returns null if devbox.lock not found', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: [],
}),
);
mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(null);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{}],
config: {},
}),
).toBeNull();
});

it('returns null if no lock file changes are found', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(codeBlock`{}`);
git.getRepoStatus.mockResolvedValueOnce(
partial<StatusResult>({
modified: [],
}),
);
mockExecAll();
const oldLockFileContent = Buffer.from('Old devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{}],
config: {},
}),
).toBeNull();
});

it('returns an artifact error on failure', async () => {
fs.getSiblingFileName.mockReturnValueOnce('devbox.lock');
const newLockFileContent = codeBlock`{}`;
const oldLockFileContent = Buffer.from('New devbox.lock');
fs.readLocalFile.mockResolvedValueOnce(oldLockFileContent as never);
fs.readLocalFile.mockResolvedValueOnce(newLockFileContent as never);
expect(
await updateArtifacts({
packageFileName: 'devbox.json',
newPackageFileContent: devboxJson,
updatedDeps: [{}],
config: {},
}),
).toEqual([
{
artifactError: {
lockFile: 'devbox.lock',
stderr: "Cannot read properties of undefined (reading 'stdout')",
},
},
]);
});
});
});
82 changes: 82 additions & 0 deletions lib/modules/manager/devbox/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { exec } from '../../../util/exec';
import type { ExecOptions } from '../../../util/exec/types';
import { getSiblingFileName, readLocalFile } from '../../../util/fs';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';

export async function updateArtifacts(
updateConfig: UpdateArtifact,
): Promise<UpdateArtifactsResult[] | null> {
const lockFileName = getSiblingFileName(
updateConfig.packageFileName,
'devbox.lock',
);
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
if (!existingLockFileContent) {
logger.debug('No devbox.lock found');
return null;
}
const execOptions: ExecOptions = {
cwdFile: updateConfig.packageFileName,
toolConstraints: [
{
toolName: 'devbox',
constraint: updateConfig.config.constraints?.devbox,
},
],
docker: {},
userConfiguredEnv: updateConfig.config.env,
};

let cmd = '';
if (
updateConfig.config.isLockFileMaintenance ||
updateConfig.config.updateType === 'lockFileMaintenance'
) {
cmd += 'devbox update';
} else if (is.nonEmptyArray(updateConfig.updatedDeps)) {
cmd += 'devbox install';
} else {
logger.trace('No updated devbox packages - returning null');
return null;
}

const oldLockFileContent = await readLocalFile(lockFileName);
if (!oldLockFileContent) {
logger.trace(`No ${lockFileName} found`);
return null;
}

try {
await exec(cmd, execOptions);
const newLockFileContent = await readLocalFile(lockFileName);

if (
!newLockFileContent ||
Buffer.compare(oldLockFileContent, newLockFileContent) === 0
) {
return null;
}
logger.trace('Returning updated devbox.lock');
return [
{
file: {
type: 'addition',
path: lockFileName,
contents: newLockFileContent,
},
},
];
} catch (err) {
logger.warn({ err }, 'Error updating devbox.lock');
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
}
Loading