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(mix): Support private hex repos/registries #29775

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/modules/manager/mix/__fixtures__/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule MyProject.MixProject do
{:ecto, github: "elixir-ecto/ecto", ref: "795036d997c7503b21fb64d6bf1a89b83c44f2b5"},
{:secret, "~> 1.0", organization: "acme"},
{:also_secret, "~> 1.0", only: [:dev, :test], organization: "acme", runtime: false},
{:oban_pro, "~> 1.4", repo: "oban"},
{:ex_doc, ">2.1.0 and <=3.0.0"},
{:jason, ">= 1.0.0"},
{:mason, "~> 1.0",
Expand Down
1 change: 1 addition & 0 deletions lib/modules/manager/mix/__fixtures__/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"ecto": {:git, "https://github.com/elixir-ecto/ecto.git", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [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"},
"oban_pro": {:hex, :oban_pro, "1.4.7", "c6703c06bf7f66cecefc4de79f358116ec3db0cfff5950ca57fe36b3fb04bdcf", [:mix], [], "oban", "039eed187190555efe687a6f88547f4a68bbcd5faf423395d322a4ef7ec177cd"},
"ex_doc": {:hex, :ex_doc, "2.2.0", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19c205c8de0e2e5817f2250100281c58e717cb11ff1bb410bf661ee78c24e79b"},
"jason": {:hex, :jason, "1.0.1", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19c205c8de0e2e5817f2250100281c58e717cb11ff1bb410bf661ee78c24e79b"},
"mason": {:hex, :mason, "1.1.0", "344dbbf6610d205760ec37e2848bff2aab5a2de182bb5cdaa72cc2fd19d74535", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19c205c8de0e2e5817f2250100281c58e717cb11ff1bb410bf661ee78c24e79b"},
Expand Down
85 changes: 68 additions & 17 deletions lib/modules/manager/mix/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
import { env, fs, hostRules, mockedFunction } from '../../../../test/util';
import {
env,
fs,
hostRules,
mockedFunction,
partial,
} from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import * as docker from '../../../util/exec/docker';
Expand All @@ -26,7 +32,11 @@ const adminConfig: RepoGlobalConfig = {
// support install mode
process.env.CONTAINERBASE = 'true';

const config: UpdateArtifactsConfig = {};
const config = partial<UpdateArtifactsConfig>({
registryAliases: {
oban: 'https://getoban.pro/repo',
},
});

describe('modules/manager/mix/artifacts', () => {
beforeEach(() => {
Expand Down Expand Up @@ -174,38 +184,36 @@ describe('modules/manager/mix/artifacts', () => {
fs.findLocalSiblingOrParent.mockResolvedValueOnce('mix.lock');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New mix.lock');
hostRules.getAll.mockReturnValueOnce([
{ matchHost: 'https://hex.pm/api/repos/renovate_test/' },
{ matchHost: 'https://hex.pm/api/repos/unauthorized_organization/' },
]);
hostRules.find.mockReturnValueOnce({ token: 'valid_test_token' });
hostRules.find.mockReturnValueOnce({});
hostRules.find.mockReturnValueOnce({}); // unauthorized_organization token missing

// erlang
getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '22.3.4.26' },
{ version: '23.1.1.0' },
{ version: '24.3.4.1' },
{ version: '24.3.4.2' },
{ version: '25.0.0.0' },
],
releases: [{ version: '22.3.4.26' }, { version: '25.0.0.0' }],
});
// elixir
getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: 'v1.8.2' },
{ version: 'v1.13.3' },
{ version: 'v1.13.4' },
],
releases: [{ version: 'v1.8.2' }, { version: 'v1.13.4' }],
});

const config = partial<UpdateArtifactsConfig>({
registryAliases: {},
});

const result = await updateArtifacts({
packageFileName: 'mix.exs',
updatedDeps: [
{
depName: 'private_package',
packageName: 'private_package:renovate_test',
packageName: 'org:renovate_test:private_package',
},
{
depName: 'other_package',
packageName: 'other_package:unauthorized_organization',
packageName: 'org:unauthorized_organization:other_package',
},
],
newPackageFileContent: '{}',
Expand Down Expand Up @@ -266,6 +274,10 @@ describe('modules/manager/mix/artifacts', () => {
],
});

const config = partial<UpdateArtifactsConfig>({
registryAliases: {},
});

const result = await updateArtifacts({
packageFileName: 'mix.exs',
updatedDeps: [{ depName: 'some_package' }],
Expand All @@ -291,6 +303,45 @@ describe('modules/manager/mix/artifacts', () => {
]);
});

it('does not add registries configured in registryAliases but no token in hostRules', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.readLocalFile.mockResolvedValueOnce('Old mix.lock');
fs.findLocalSiblingOrParent.mockResolvedValueOnce('mix.lock');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New mix.lock');
hostRules.getAll.mockReturnValueOnce([
{ matchHost: 'https://getoban.pro/repo' },
]);
hostRules.find.mockReturnValueOnce({});

// erlang
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: '25.0.0.0' }],
});
// elixir
getPkgReleases.mockResolvedValueOnce({
releases: [{ version: 'v1.13.4' }],
});

const result = await updateArtifacts({
packageFileName: 'mix.exs',
updatedDeps: [{ depName: 'oban_pro' }],
newPackageFileContent: '{}',
config,
});

expect(result).toEqual([
{
file: { type: 'addition', path: 'mix.lock', contents: 'New mix.lock' },
},
]);
expect(execSnapshots).toMatchObject([
bryannaegele marked this conversation as resolved.
Show resolved Hide resolved
{ cmd: 'install-tool erlang 25.0.0.0' },
{ cmd: 'install-tool elixir v1.13.4' },
{ cmd: 'mix deps.update oban_pro' },
]);
});

it('returns updated mix.lock in subdir', async () => {
GlobalConfig.set({
...adminConfig,
Expand Down
40 changes: 34 additions & 6 deletions lib/modules/manager/mix/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
} from '../../../util/fs';
import * as hostRules from '../../../util/host-rules';
import { regEx } from '../../../util/regex';

import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { aliasRecordToRepositories } from './utils';

const hexRepoUrl = 'https://hex.pm/';
const hexRepoOrgUrlRegex = regEx(
Expand Down Expand Up @@ -53,6 +53,9 @@ export async function updateArtifacts({
return null;
}

const preCommands: string[] = [];

// auth organizations
const organizations = new Set<string>();

const hexHostRulesWithMatchHost = hostRules
Expand All @@ -75,26 +78,50 @@ export async function updateArtifacts({

for (const { packageName } of updatedDeps) {
if (packageName) {
const [, organization] = packageName.split(':');
const [, organization, ,] = packageName.split(':');
bryannaegele marked this conversation as resolved.
Show resolved Hide resolved

if (organization) {
organizations.add(organization);
}
}
}

const preCommands = Array.from(organizations).reduce((acc, organization) => {
// regex match anything hex.pm to an org
// if not hex, do repo.add
for (const organization of Array.from(organizations)) {
const url = `${hexRepoUrl}api/repos/${organization}/`;
const { token } = hostRules.find({ url });

if (token) {
logger.debug(`Authenticating to hex organization ${organization}`);
const authCommand = `mix hex.organization auth ${organization} --key ${token}`;
return [...acc, authCommand];
preCommands.push(authCommand);
}
}

// auth repo registries

if (config.registryAliases) {
// regex match anything hex.pm to an org
// if not hex, do repo.add
const repoAliases = aliasRecordToRepositories(config.registryAliases);

for (const { name, registry } of repoAliases) {
const hostRule = hostRules.find({ url: registry });

return acc;
}, [] as string[]);
// istanbul ignore if: no good way to test
bryannaegele marked this conversation as resolved.
Show resolved Hide resolved
if (hostRule?.token) {
logger.debug(`Adding repo registry ${name}`);
const authCommand = `mix repo.add ${name} ${registry} --auth-key ${hostRule.token}`;

preCommands.push(authCommand);
}

logger.debug(
`No hostRule token defined for repo ${name} with url ${registry}.`,
);
}
}

const execOptions: ExecOptions = {
cwdFile: packageFileName,
Expand All @@ -113,6 +140,7 @@ export async function updateArtifacts({
],
preCommands,
};

const command = [
'mix',
'deps.update',
Expand Down
Loading