This commit is contained in:
Sandro 2025-01-04 11:36:21 +01:00 committed by GitHub
commit 32cb67429b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 686 additions and 75 deletions

View file

@ -1,74 +1,522 @@
import { fs } from '../../../../test/util';
import { GitRefsDatasource } from '../../datasource/git-refs';
import { id as nixpkgsVersioning } from '../../versioning/nixpkgs';
import { extractPackageFile } from '.';
jest.mock('../../../util/fs');
const flake1Lock = `{
"nodes": {
"root": {}
},
"root": "root",
"version": 7
}`;
const flake2Lock = `{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1720031269,
"narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9f4128e00b0ae8ec65918efeba59db998750ead6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}`;
const flake3Lock = `{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1728650607,
"narHash": "sha256-0lOnVTzRXzpk5uxbHLm3Ti3tyPAvirAIQDfwEUd8arg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "612ee628421ba2c1abca4c99684862f76cb3b089",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}`;
const flake4Lock = `{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1672057183,
"narHash": "sha256-GN7/10DNNvs1FPj9tlZA2qgNdFuYKKuS3qlHTqAxasQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b139e44d78c36c69bcbb825b20dbfa51e7738347",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixpkgs-unstable",
"type": "indirect"
}
},
"patchelf": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1718457448,
"narHash": "sha256-FSoxTcRZMGHNJh8dNtKOkcUtjhmhU6yQXcZZfUPLhQM=",
"ref": "refs/heads/master",
"rev": "a0f54334df36770b335c051e540ba40afcbf8378",
"revCount": 844,
"type": "git",
"url": "https://github.com/NixOS/patchelf.git"
},
"original": {
"type": "git",
"url": "https://github.com/NixOS/patchelf.git"
}
},
"root": {
"inputs": {
"patchelf": "patchelf"
}
}
},
"root": "root",
"version": 7
}`;
const flake5Lock = `{
"nodes": {
"ijq": {
"flake": false,
"locked": {
"lastModified": 1723569650,
"narHash": "sha256-Ho/sAhEUeSug52JALgjrKVUPCBe8+PovbJj/lniKxp8=",
"owner": "~gpanders",
"repo": "ijq",
"rev": "88f0d9ae98942bf49cba302c42b2a0f6e05f9b58",
"type": "sourcehut"
},
"original": {
"owner": "~gpanders",
"repo": "ijq",
"type": "sourcehut"
}
},
"root": {
"inputs": {
"ijq": "ijq"
}
}
},
"root": "root",
"version": 7
}`;
const flake6Lock = `{
"nodes": {
"home-manager": {
"flake": false,
"locked": {
"lastModified": 1728650932,
"narHash": "sha256-mGKzqdsRyLnGNl6WjEr7+sghGgBtYHhJQ4mjpgRTCsU=",
"owner": "rycee",
"repo": "home-manager",
"rev": "65ae9c147349829d3df0222151f53f79821c5134",
"type": "gitlab"
},
"original": {
"owner": "rycee",
"repo": "home-manager",
"type": "gitlab"
}
},
"root": {
"inputs": {
"home-manager": "home-manager"
}
}
},
"root": "root",
"version": 7
}`;
const flake7Lock = `{
"nodes": {
"root": {}
},
"root": "root",
"version": 6
}`;
const flake8Lock = `{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1728492678,
"narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=",
"ref": "nixos-unstable",
"rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7",
"shallow": true,
"type": "git",
"url": "https://github.com/NixOS/nixpkgs"
},
"original": {
"ref": "nixos-unstable",
"shallow": true,
"type": "git",
"url": "https://github.com/NixOS/nixpkgs"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}`;
const flake9Lock = `{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}`;
const flake10Lock = `{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1728492678,
"narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-extra-pkgs": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"host": "github.corp.example.com",
"lastModified": 1728666512,
"narHash": "sha256-p+l16Zzyl2DXG695yks6KQP7NkjsnEksu5GBvtL1QYg=",
"owner": "my-org",
"repo": "nixpkgs-extra-pkgs",
"rev": "6bf2706348447df6f8b86b1c3e54f87b0afda84f",
"type": "github"
},
"original": {
"host": "github.corp.example.com",
"owner": "my-org",
"repo": "nixpkgs-extra-pkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs-extra-pkgs": "nixpkgs-extra-pkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}`;
const flake11Lock = `{
"nodes": {
"data-mesher": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1727355895,
"narHash": "sha256-grZIaLgk5GgoDuTt49RTCLBh458H4YJdIAU4B3onXRw=",
"rev": "c7e39452affcc0f89e023091524e38b3aaf109e9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c7e39452affcc0f89e023091524e38b3aaf109e9.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/data-mesher/archive/main.tar.gz"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"data-mesher",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726153070,
"narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1726871744,
"narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"data-mesher": "data-mesher"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"data-mesher",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726734507,
"narHash": "sha256-VUH5O5AcOSxb0uL/m34dDkxFKP6WLQ6y4I1B4+N3L2w=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "ee41a466c2255a3abe6bc50fc6be927cdee57a9f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}`;
describe('modules/manager/nix/extract', () => {
it('returns null when no nixpkgs', () => {
const content = `{
inputs = {};
}`;
const res = extractPackageFile(content);
expect(res).toBeNull();
it('returns null when no inputs', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake1Lock);
expect(await extractPackageFile('', 'flake.nix')).toBeNull();
});
it('returns nixpkgs', () => {
const content = `{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-21.11";
};
}`;
const res = extractPackageFile(content);
expect(res?.deps).toEqual([
{
depName: 'nixpkgs',
currentValue: 'nixos-21.11',
datasource: GitRefsDatasource.id,
packageName: 'https://github.com/NixOS/nixpkgs',
versioning: nixpkgsVersioning,
},
]);
});
it('is case insensitive', () => {
const content = `{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11";
};
}`;
const res = extractPackageFile(content);
expect(res?.deps).toEqual([
{
depName: 'nixpkgs',
currentValue: 'nixos-21.11',
datasource: GitRefsDatasource.id,
packageName: 'https://github.com/NixOS/nixpkgs',
versioning: nixpkgsVersioning,
},
]);
});
it('includes nixpkgs with no explicit ref', () => {
const content = `{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
};
}`;
const res = extractPackageFile(content);
expect(res).toMatchObject({
it('returns nixpkgs input', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake2Lock);
expect(await extractPackageFile('', 'flake.nix')).toEqual({
deps: [
{
currentValue: undefined,
depName: 'nixpkgs',
currentDigest: '9f4128e00b0ae8ec65918efeba59db998750ead6',
currentValue: 'nixos-unstable',
datasource: GitRefsDatasource.id,
packageName: 'https://github.com/NixOS/nixpkgs',
replaceString: '9f4128e00b0ae8ec65918efeba59db998750ead6',
},
],
});
});
it('includes nixpkgs with no explicit ref', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake3Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: '612ee628421ba2c1abca4c99684862f76cb3b089',
datasource: 'git-refs',
depName: 'nixpkgs',
packageName: 'https://github.com/NixOS/nixpkgs',
versioning: 'nixpkgs',
},
],
});
});
it('includes patchelf from HEAD', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake4Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: 'a0f54334df36770b335c051e540ba40afcbf8378',
datasource: 'git-refs',
depName: 'patchelf',
packageName: 'https://github.com/NixOS/patchelf.git',
},
],
});
});
it('includes ijq from sourcehut without a flake', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake5Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: '88f0d9ae98942bf49cba302c42b2a0f6e05f9b58',
datasource: 'git-refs',
depName: 'ijq',
packageName: 'https://git.sr.ht/~gpanders/ijq',
},
],
});
});
it('includes home-manager from gitlab', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake6Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: '65ae9c147349829d3df0222151f53f79821c5134',
datasource: 'git-refs',
depName: 'home-manager',
packageName: 'https://gitlab.com/rycee/home-manager',
},
],
});
});
it('test other version', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake7Lock);
expect(await extractPackageFile('', 'flake.nix')).toBeNull();
});
it('includes nixpkgs with ref and shallow arguments', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake8Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: '5633bcff0c6162b9e4b5f1264264611e950c8ec7',
datasource: 'git-refs',
depName: 'nixpkgs',
packageName: 'https://github.com/NixOS/nixpkgs',
},
],
});
});
it('includes nixpkgs but using indirect type that cannot be updated', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake9Lock);
expect(await extractPackageFile('', 'flake.nix')).toBeNull();
});
it('includes flake from GitHub Enterprise', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake10Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: '6bf2706348447df6f8b86b1c3e54f87b0afda84f',
datasource: 'git-refs',
depName: 'nixpkgs-extra-pkgs',
packageName:
'https://github.corp.example.com/my-org/nixpkgs-extra-pkgs',
},
],
});
});
it('includes flake with tarball type', async () => {
fs.readLocalFile.mockResolvedValueOnce(flake11Lock);
expect(await extractPackageFile('', 'flake.nix')).toMatchObject({
deps: [
{
currentDigest: 'c7e39452affcc0f89e023091524e38b3aaf109e9',
datasource: 'git-refs',
depName: 'data-mesher',
packageName: 'https://git.clan.lol/clan/data-mesher',
},
],
});

View file

@ -1,23 +1,140 @@
import { logger } from '../../../logger';
import { getSiblingFileName, readLocalFile } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { GitRefsDatasource } from '../../datasource/git-refs';
import { id as nixpkgsVersioning } from '../../versioning/nixpkgs';
import type { PackageDependency, PackageFileContent } from '../types';
import { NixFlakeLock } from './schema';
const nixpkgsRegex = regEx(/"github:nixos\/nixpkgs(\/(?<ref>[a-z0-9-.]+))?"/i);
// TODO: add support to update nixpkgs branches in flakes.nix using nixpkgsVersioning
// as documented upstream
// https://github.com/NixOS/nix/blob/master/doc/manual/source/protocols/tarball-fetcher.md#gitea-and-forgejo-support
const lockableHTTPTarballProtocol = regEx(
'^https://(?<domain>[^/]+)/(?<owner>[^/]+)/(?<repo>[^/]+)/archive/(?<rev>.+).tar.gz$',
);
export async function extractPackageFile(
content: string,
packageFile: string,
): Promise<PackageFileContent | null> {
const packageLockFile = getSiblingFileName(packageFile, 'flake.lock');
const lockContents = await readLocalFile(packageLockFile, 'utf8');
logger.trace(`nix.extractPackageFile(${packageLockFile})`);
export function extractPackageFile(content: string): PackageFileContent | null {
const deps: PackageDependency[] = [];
const match = nixpkgsRegex.exec(content);
if (match?.groups) {
const { ref } = match.groups;
const flakeLockParsed = NixFlakeLock.safeParse(lockContents);
if (!flakeLockParsed.success) {
logger.debug(
{ packageLockFile, error: flakeLockParsed.error },
`invalid flake.lock file`,
);
return null;
}
const flakeLock = flakeLockParsed.data;
const rootInputs = flakeLock.nodes['root'].inputs;
if (!rootInputs) {
logger.debug(
{ packageLockFile, error: flakeLockParsed.error },
`flake.lock is missing "root" node`,
);
return null;
}
for (const [depName, flakeInput] of Object.entries(flakeLock.nodes)) {
// the root input is a magic string for the entrypoint and only references other flake inputs
if (depName === 'root') {
continue;
}
// skip all locked nodes which cannot be updated
if (!(depName in rootInputs)) {
continue;
}
const flakeLocked = flakeInput.locked;
const flakeOriginal = flakeInput.original;
// istanbul ignore if: if we are not in a root node then original and locked always exist which cannot be easily expressed in the type
if (flakeLocked === undefined || flakeOriginal === undefined) {
logger.debug(
{ packageLockFile },
`Found empty flake input '${JSON.stringify(flakeInput)}', skipping`,
);
continue;
}
// indirect inputs cannot be reliable updated because they depend on the flake registry
if (flakeOriginal.type === 'indirect') {
continue;
}
switch (flakeLocked.type) {
case 'github':
deps.push({
depName: 'nixpkgs',
currentValue: ref,
depName,
currentValue: flakeOriginal.ref,
currentDigest: flakeLocked.rev,
replaceString: flakeLocked.rev,
datasource: GitRefsDatasource.id,
packageName: 'https://github.com/NixOS/nixpkgs',
versioning: nixpkgsVersioning,
packageName: `https://${flakeOriginal.host ?? 'github.com'}/${flakeOriginal.owner}/${flakeOriginal.repo}`,
});
break;
case 'gitlab':
deps.push({
depName,
currentValue: flakeOriginal.ref,
currentDigest: flakeLocked.rev,
replaceString: flakeLocked.rev,
datasource: GitRefsDatasource.id,
packageName: `https://${flakeOriginal.host ?? 'gitlab.com'}/${flakeOriginal.owner}/${flakeOriginal.repo}`,
});
break;
case 'git':
deps.push({
depName,
currentValue: flakeOriginal.ref,
currentDigest: flakeLocked.rev,
replaceString: flakeLocked.rev,
datasource: GitRefsDatasource.id,
packageName: flakeOriginal.url,
});
break;
case 'sourcehut':
deps.push({
depName,
currentValue: flakeOriginal.ref,
currentDigest: flakeLocked.rev,
replaceString: flakeLocked.rev,
datasource: GitRefsDatasource.id,
packageName: `https://${flakeOriginal.host ?? 'git.sr.ht'}/${flakeOriginal.owner}/${flakeOriginal.repo}`,
});
break;
case 'tarball':
deps.push({
depName,
currentValue: flakeLocked.ref,
currentDigest: flakeLocked.rev,
replaceString: flakeLocked.rev,
datasource: GitRefsDatasource.id,
// type tarball always contains this link
packageName: flakeOriginal.url!.replace(
lockableHTTPTarballProtocol,
'https://$<domain>/$<owner>/$<repo>',
),
});
break;
// istanbul ignore next: just a safeguard
default:
logger.debug(
{ packageLockFile },
`Unknown flake.lock type "${flakeLocked.type}", skipping`,
);
break;
}
}
if (deps.length) {

View file

@ -9,7 +9,7 @@ export const url = 'https://nix.dev';
export const defaultConfig = {
fileMatch: ['(^|/)flake\\.nix$'],
commitMessageTopic: 'nixpkgs',
commitMessageTopic: 'nix',
commitMessageExtra: 'to {{newValue}}',
enabled: false,
};

View file

@ -1,4 +1,9 @@
The [`nix`](https://github.com/NixOS/nix) manager supports:
- [`lockFileMaintenance`](../../../configuration-options.md#lockfilemaintenance) updates for `flake.lock`
- [nixpkgs](https://github.com/NixOS/nixpkgs) updates
- input updates for `flake.lock`
For specifying `packageRules` it is important to know how `depName` and `packageName` are defined for nix updates:
- The `depName` field is equal to the nix flake input name, eg. `nix.inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";` would have the `depName` of `nixpkgs`
- The `packageName` field is equal to the fully-qualified root URL of the package source, eg. `https://github.com/NixOS/nixpkgs` for the above example.

View file

@ -0,0 +1,41 @@
import { z } from 'zod';
import { Json } from '../../../util/schema-utils';
const InputType = z.enum([
'git',
'github',
'gitlab',
'indirect',
'sourcehut',
'tarball',
]);
const LockedInput = z.object({
ref: z.string().optional(),
rev: z.string(),
type: InputType,
});
const OriginalInput = z.object({
host: z.string().optional(),
owner: z.string().optional(),
repo: z.string().optional(),
ref: z.string().optional(),
type: InputType,
url: z.string().optional(),
});
const NixInput = z.object({
inputs: z.record(z.string(), z.string().or(z.array(z.string()))).optional(),
locked: LockedInput.optional(),
original: OriginalInput.optional(),
});
export const NixFlakeLock = Json.pipe(
z.object({
nodes: z.record(z.string(), NixInput),
version: z.literal(7),
}),
);
export type NixFlakeLock = z.infer<typeof NixFlakeLock>;