diff --git a/lib/modules/manager/mix/__fixtures__/mix.exs b/lib/modules/manager/mix/__fixtures__/mix.exs index 9bc2b75a800d38..2ad51c305e58e1 100644 --- a/lib/modules/manager/mix/__fixtures__/mix.exs +++ b/lib/modules/manager/mix/__fixtures__/mix.exs @@ -27,13 +27,19 @@ defmodule MyProject.MixProject do {:secret, "~> 1.0", organization: "acme"}, {:also_secret, "~> 1.0", only: [:dev, :test], organization: "acme", runtime: false}, {:metrics, ">0.2.0 and <=1.0.0"}, - {:jason, ">= 1.0.0"}, + {:jason, ">= 1.0.0", only: :prod}, {:hackney, "~> 1.0", optional: true}, - {:hammer_backend_redis, "~> 6.1"}, + {:hammer_backend_redis, "~> 6.1", only: [:dev, :prod, :test]}, {:castore, "== 1.0.10"}, {:gun, "~> 2.0.0", hex: "grpc_gun"}, {:another_gun, "~> 0.4.0", hex: :raygun}, + {:credo, "~> 1.7", only: + [:test, + # prod, + :dev], + runtime: false}, + {:floki, "== 0.37.0", only: :test}, ] end end diff --git a/lib/modules/manager/mix/__fixtures__/mix.lock b/lib/modules/manager/mix/__fixtures__/mix.lock index d90b44ee2d1c38..106d497cd6b91e 100644 --- a/lib/modules/manager/mix/__fixtures__/mix.lock +++ b/lib/modules/manager/mix/__fixtures__/mix.lock @@ -1,13 +1,17 @@ %{ "another_gun": {:hex, :raygun, "0.4.0", "7744e99dd695f61e78ad5e047cce0affb3edfc6f93a92278598ab553b9c5091f", [:mix], [{:httpoison, "~> 0.8 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.1", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "eee4b891e6e65c6a4b15386dc7b7a72b717f3c123cc0012cfd19e8f2ab21116d"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "cowboy": {:git, "https://github.com/ninenines/cowboy.git", "0c2e2224e372f01e6cf51a8e12d4856edb4cb8ac", [tag: "0.6.0"]}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "795036d997c7503b21fb64d6bf1a89b83c44f2b5", [ref: "795036d997c7503b21fb64d6bf1a89b83c44f2b5"]}, "secret": {:hex, :secret, "1.5.0", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19c205c8de0e2e5817f2250100281c58e717cb11ff1bb410bf661ee78c24e79b"}, "also_secret": {:hex, :also_secret, "1.3.4", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19c205c8de0e2e5817f2250100281c58e717cb11ff1bb410bf661ee78c24e79b"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, "gun": {:hex, :grpc_gun, "2.0.1", "221b792df3a93e8fead96f697cbaf920120deacced85c6cd3329d2e67f0871f8", [:rebar3], [{:cowlib, "~> 2.11", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "795a65eb9d0ba16697e6b0e1886009ce024799e43bb42753f0c59b029f592831"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hammer": {:hex, :hammer, "6.2.1", "5ae9c33e3dceaeb42de0db46bf505bd9c35f259c8defb03390cd7556fea67ee2", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b9476d0c13883d2dc0cc72e786bac6ac28911fba7cc2e04b70ce6a6d9c4b2bdc"}, diff --git a/lib/modules/manager/mix/extract.spec.ts b/lib/modules/manager/mix/extract.spec.ts index 3663073aabc20c..a20ea0327fd715 100644 --- a/lib/modules/manager/mix/extract.spec.ts +++ b/lib/modules/manager/mix/extract.spec.ts @@ -20,12 +20,14 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 0.8.1', datasource: 'hex', depName: 'postgrex', + depType: 'prod', packageName: 'postgrex', }, { currentValue: '<1.7.0 or ~>1.7.1', datasource: 'hex', depName: 'ranch', + depType: 'prod', packageName: 'ranch', }, { @@ -33,6 +35,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '0.6.0', datasource: 'github-tags', depName: 'cowboy', + depType: 'prod', packageName: 'ninenines/cowboy', }, { @@ -40,6 +43,7 @@ describe('modules/manager/mix/extract', () => { currentValue: 'main', datasource: 'git-tags', depName: 'phoenix', + depType: 'prod', packageName: 'https://github.com/phoenixframework/phoenix.git', }, { @@ -47,42 +51,49 @@ describe('modules/manager/mix/extract', () => { currentValue: undefined, datasource: 'github-tags', depName: 'ecto', + depType: 'prod', packageName: 'elixir-ecto/ecto', }, { currentValue: '~> 1.0', datasource: 'hex', depName: 'secret', + depType: 'prod', packageName: 'secret:acme', }, { currentValue: '~> 1.0', datasource: 'hex', depName: 'also_secret', + depType: 'dev', packageName: 'also_secret:acme', }, { currentValue: '>0.2.0 and <=1.0.0', datasource: 'hex', depName: 'metrics', + depType: 'prod', packageName: 'metrics', }, { currentValue: '>= 1.0.0', datasource: 'hex', depName: 'jason', + depType: 'prod', packageName: 'jason', }, { currentValue: '~> 1.0', datasource: 'hex', depName: 'hackney', + depType: 'prod', packageName: 'hackney', }, { currentValue: '~> 6.1', datasource: 'hex', depName: 'hammer_backend_redis', + depType: 'prod', packageName: 'hammer_backend_redis', }, { @@ -90,20 +101,38 @@ describe('modules/manager/mix/extract', () => { currentVersion: '1.0.10', datasource: 'hex', depName: 'castore', + depType: 'prod', packageName: 'castore', }, { currentValue: '~> 2.0.0', datasource: 'hex', depName: 'gun', + depType: 'prod', packageName: 'grpc_gun', }, { currentValue: '~> 0.4.0', datasource: 'hex', depName: 'another_gun', + depType: 'prod', packageName: 'raygun', }, + { + currentValue: '~> 1.7', + datasource: 'hex', + depName: 'credo', + depType: 'dev', + packageName: 'credo', + }, + { + currentValue: '== 0.37.0', + currentVersion: '0.37.0', + datasource: 'hex', + depName: 'floki', + depType: 'dev', + packageName: 'floki', + }, ]); }); @@ -116,6 +145,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 0.8.1', datasource: 'hex', depName: 'postgrex', + depType: 'prod', packageName: 'postgrex', lockedVersion: '0.8.4', }, @@ -123,6 +153,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '<1.7.0 or ~>1.7.1', datasource: 'hex', depName: 'ranch', + depType: 'prod', packageName: 'ranch', lockedVersion: '1.7.1', }, @@ -131,6 +162,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '0.6.0', datasource: 'github-tags', depName: 'cowboy', + depType: 'prod', packageName: 'ninenines/cowboy', lockedVersion: '0.6.0', }, @@ -139,6 +171,7 @@ describe('modules/manager/mix/extract', () => { currentValue: 'main', datasource: 'git-tags', depName: 'phoenix', + depType: 'prod', packageName: 'https://github.com/phoenixframework/phoenix.git', lockedVersion: undefined, }, @@ -147,6 +180,7 @@ describe('modules/manager/mix/extract', () => { currentValue: undefined, datasource: 'github-tags', depName: 'ecto', + depType: 'prod', packageName: 'elixir-ecto/ecto', lockedVersion: undefined, }, @@ -154,6 +188,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 1.0', datasource: 'hex', depName: 'secret', + depType: 'prod', packageName: 'secret:acme', lockedVersion: '1.5.0', }, @@ -161,6 +196,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 1.0', datasource: 'hex', depName: 'also_secret', + depType: 'dev', packageName: 'also_secret:acme', lockedVersion: '1.3.4', }, @@ -168,6 +204,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '>0.2.0 and <=1.0.0', datasource: 'hex', depName: 'metrics', + depType: 'prod', packageName: 'metrics', lockedVersion: '1.0.0', }, @@ -175,6 +212,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '>= 1.0.0', datasource: 'hex', depName: 'jason', + depType: 'prod', packageName: 'jason', lockedVersion: '1.4.4', }, @@ -182,6 +220,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 1.0', datasource: 'hex', depName: 'hackney', + depType: 'prod', packageName: 'hackney', lockedVersion: '1.20.1', }, @@ -189,6 +228,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 6.1', datasource: 'hex', depName: 'hammer_backend_redis', + depType: 'prod', packageName: 'hammer_backend_redis', lockedVersion: '6.2.0', }, @@ -197,6 +237,7 @@ describe('modules/manager/mix/extract', () => { currentVersion: '1.0.10', datasource: 'hex', depName: 'castore', + depType: 'prod', packageName: 'castore', lockedVersion: '1.0.10', }, @@ -204,6 +245,7 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 2.0.0', datasource: 'hex', depName: 'gun', + depType: 'prod', packageName: 'grpc_gun', lockedVersion: '2.0.1', }, @@ -211,9 +253,27 @@ describe('modules/manager/mix/extract', () => { currentValue: '~> 0.4.0', datasource: 'hex', depName: 'another_gun', + depType: 'prod', packageName: 'raygun', lockedVersion: '0.4.0', }, + { + currentValue: '~> 1.7', + datasource: 'hex', + depName: 'credo', + depType: 'dev', + packageName: 'credo', + lockedVersion: '1.7.10', + }, + { + currentValue: '== 0.37.0', + currentVersion: '0.37.0', + datasource: 'hex', + depName: 'floki', + depType: 'dev', + lockedVersion: '0.37.0', + packageName: 'floki', + }, ]); }); }); diff --git a/lib/modules/manager/mix/extract.ts b/lib/modules/manager/mix/extract.ts index 3d9c45e00d74f4..663b6cce6becc0 100644 --- a/lib/modules/manager/mix/extract.ts +++ b/lib/modules/manager/mix/extract.ts @@ -20,6 +20,8 @@ const lockedVersionRegExp = regEx( /^\s+"(?\w+)".*?"(?\d+\.\d+\.\d+)"/, ); const hexRegexp = regEx(/hex:\s*(?:"(?[^"]+)"|:(?\w+))/); +const onlyValueRegexp = /only:\s*(?\[[^\]]*\]|:\w+)/; +const onlyEnvironmentsRegexp = /:(\w+)/gm; export async function extractPackageFile( content: string, @@ -48,22 +50,28 @@ export async function extractPackageFile( const hexGroups = hexRegexp.exec(opts)?.groups; const hex = hexGroups?.strValue ?? hexGroups?.atomValue; - let dep: PackageDependency; + const onlyValue = onlyValueRegexp.exec(opts)?.groups?.only; + const onlyEnvironments = []; + let match; + if (onlyValue) { + while ((match = onlyEnvironmentsRegexp.exec(onlyValue)) !== null) { + onlyEnvironments.push(match[1]); + } + } + + const dep: PackageDependency = { + depName: app, + depType: 'prod', + }; if (git ?? github) { - dep = { - depName: app, - currentDigest: ref, - currentValue: branchOrTag, - datasource: git ? GitTagsDatasource.id : GithubTagsDatasource.id, - packageName: git ?? github, - }; + dep.currentDigest = ref; + dep.currentValue = branchOrTag; + dep.datasource = git ? GitTagsDatasource.id : GithubTagsDatasource.id; + dep.packageName = git ?? github; } else { - dep = { - depName: app, - currentValue: requirement, - datasource: HexDatasource.id, - }; + dep.currentValue = requirement; + dep.datasource = HexDatasource.id; if (organization) { dep.packageName = `${app}:${organization}`; } else if (hex) { @@ -71,11 +79,16 @@ export async function extractPackageFile( } else { dep.packageName = app; } + if (requirement?.startsWith('==')) { dep.currentVersion = requirement.replace(regEx(/^==\s*/), ''); } } + if (onlyValue !== undefined && !onlyEnvironments.includes('prod')) { + dep.depType = 'dev'; + } + deps.set(app, dep); logger.trace({ dep }, `setting ${app}`); depMatchGroups = depMatchRegExp.exec(depBuffer)?.groups; diff --git a/lib/modules/manager/mix/index.ts b/lib/modules/manager/mix/index.ts index e9722265580591..df1fa52ec6cf6d 100644 --- a/lib/modules/manager/mix/index.ts +++ b/lib/modules/manager/mix/index.ts @@ -5,6 +5,7 @@ import { HexDatasource } from '../../datasource/hex'; export { extractPackageFile } from './extract'; export { updateArtifacts } from './artifacts'; +export { getRangeStrategy } from './range'; export const url = 'https://hexdocs.pm/mix/Mix.html'; export const categories: Category[] = ['elixir']; diff --git a/lib/modules/manager/mix/range.spec.ts b/lib/modules/manager/mix/range.spec.ts new file mode 100644 index 00000000000000..0efd919e8455cb --- /dev/null +++ b/lib/modules/manager/mix/range.spec.ts @@ -0,0 +1,47 @@ +import type { RangeConfig } from '../types'; +import { getRangeStrategy } from '.'; + +describe('modules/manager/mix/range', () => { + it('returns same if not auto', () => { + const config: RangeConfig = { rangeStrategy: 'pin' }; + expect(getRangeStrategy(config)).toBe('pin'); + + config.rangeStrategy = 'widen'; + expect(getRangeStrategy(config)).toBe('widen'); + }); + + it('widens complex bump', () => { + const config: RangeConfig = { + rangeStrategy: 'bump', + depType: 'prod', + currentValue: '>= 1.6.0 and < 2.0.0', + }; + expect(getRangeStrategy(config)).toBe('widen'); + }); + + it('bumps non-complex bump', () => { + const config: RangeConfig = { + rangeStrategy: 'bump', + depType: 'prod', + currentValue: '~>1.0.0', + }; + expect(getRangeStrategy(config)).toBe('bump'); + }); + + it('widens complex auto', () => { + const config: RangeConfig = { + rangeStrategy: 'auto', + depType: 'prod', + currentValue: '<1.7.0 or ~>1.7.1', + }; + expect(getRangeStrategy(config)).toBe('widen'); + }); + + it('defaults to update-lockfile', () => { + const config: RangeConfig = { + rangeStrategy: 'auto', + depType: 'prod', + }; + expect(getRangeStrategy(config)).toBe('update-lockfile'); + }); +}); diff --git a/lib/modules/manager/mix/range.ts b/lib/modules/manager/mix/range.ts new file mode 100644 index 00000000000000..4d6e563eb5123e --- /dev/null +++ b/lib/modules/manager/mix/range.ts @@ -0,0 +1,26 @@ +import { parseRange } from 'semver-utils'; +import { logger } from '../../../logger'; +import type { RangeStrategy } from '../../../types'; +import type { RangeConfig } from '../types'; + +export function getRangeStrategy(config: RangeConfig): RangeStrategy { + const { currentValue, rangeStrategy } = config; + const isComplexRange = currentValue + ? parseRange(currentValue).length > 1 + : false; + + if (rangeStrategy === 'bump' && isComplexRange) { + logger.debug( + { currentValue }, + 'Replacing bump strategy for complex range with widen', + ); + return 'widen'; + } + if (rangeStrategy !== 'auto') { + return rangeStrategy; + } + if (isComplexRange) { + return 'widen'; + } + return 'update-lockfile'; +} diff --git a/lib/modules/manager/mix/readme.md b/lib/modules/manager/mix/readme.md index f63ab44019224e..7924c9704caa2e 100644 --- a/lib/modules/manager/mix/readme.md +++ b/lib/modules/manager/mix/readme.md @@ -1,3 +1,27 @@ -The `mix` manager extracts dependencies for the `hex` datasource and uses Renovate's implementation of Hex SemVer to evaluate updates. +The `mix` manager uses Renovate's implementation of [Elixir SemVer](https://hexdocs.pm/elixir/Version.html#module-requirements) to evaluate update ranges. -The `mix` package manager itself is also used to keep the lock file up-to-date. +The `mix` package manager itself is used to keep the lock file up-to-date. + +The following `depTypes` are currently supported by the `mix` manager : + +- `prod`: all dependencies by default +- `dev`: dependencies with [`:only` option](https://hexdocs.pm/mix/Mix.Tasks.Deps.html#module-dependency-definition-options) not containing `:prod` + +### Default `rangeStrategy=auto` behavior + +Renovate's default [`rangeStrategy`](../../../configuration-options.md#rangestrategy) is `"auto"`. +Here's how `"auto"` works with the `mix` manager: + +| Version type | New version | Old range | New range after update | What Renovate does | +| :----------------------- | :---------- | :-------------------- | :--------------------- | :------------------------------------------------------------------------ | +| Complex range | `1.7.2` | `< 1.7.0 or ~> 1.7.1` | `< 1.7.0 or ~> 1.7.2` | Widen range to include the new version. | +| Simple range | `0.39.0` | `<= 0.38.0` | `<= 0.39.0` | If update outside current range: widens range to include the new version. | +| Exact version constraint | `0.13.0` | `== 0.12.0` | `== 0.13.0` | Replace old version with new version. | + +### Recommended `rangeStrategy` for apps and libraries + +For applications, we recommend using `rangeStrategy=pin`. +This pins your dependencies to exact versions, which is generally considered [best practice for apps](../../../dependency-pinning.md). + +For libraries, use `rangeStrategy=widen` with version ranges in your `mix.exs`. +This allows for greater compatibility with other projects that may use your library as a dependency.