feat: Cocoapods support (#4667)

This commit is contained in:
Sergio Zharinov 2020-03-01 13:03:16 +04:00 committed by GitHub
parent da47a0f842
commit 8e60b28ca4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1521 additions and 0 deletions

View file

@ -161,6 +161,12 @@ RUN rm -rf /usr/bin/python && ln /usr/bin/python3.8 /usr/bin/python
RUN curl --silent https://bootstrap.pypa.io/get-pip.py | python RUN curl --silent https://bootstrap.pypa.io/get-pip.py | python
# CocoaPods
RUN apt-get update && apt-get install -y ruby ruby2.5-dev && rm -rf /var/lib/apt/lists/*
RUN ruby --version
ENV COCOAPODS_VERSION 1.9.0
RUN gem install --no-rdoc --no-ri cocoapods -v ${COCOAPODS_VERSION}
# Set up ubuntu user and home directory with access to users in the root group (0) # Set up ubuntu user and home directory with access to users in the root group (0)
ENV HOME=/home/ubuntu ENV HOME=/home/ubuntu

View file

@ -0,0 +1,133 @@
import { api as _api } from '../../platform/github/gh-got-wrapper';
import { getPkgReleases } from '.';
import { mocked } from '../../../test/util';
import { GotResponse } from '../../platform';
import { GetReleasesConfig } from '../common';
const api = mocked(_api);
jest.mock('../../platform/github/gh-got-wrapper');
const config = {
lookupName: 'foo',
registryUrls: ['https://github.com/CocoaPods/Specs'],
};
describe('datasource/cocoapods', () => {
describe('getPkgReleases', () => {
beforeEach(() => global.renovateCache.rmAll());
it('returns null for invalid inputs', async () => {
api.get.mockResolvedValueOnce(null);
expect(
await getPkgReleases({ registryUrls: [] } as GetReleasesConfig)
).toBeNull();
expect(
await getPkgReleases({
lookupName: null,
})
).toBeNull();
expect(
await getPkgReleases({
lookupName: 'foobar',
registryUrls: [],
})
).toBeNull();
});
it('returns null for empty result', async () => {
api.get.mockResolvedValueOnce(null);
expect(await getPkgReleases(config)).toBeNull();
});
it('returns null for missing fields', async () => {
api.get.mockResolvedValueOnce({} as GotResponse);
expect(await getPkgReleases(config)).toBeNull();
api.get.mockResolvedValueOnce({ body: '' } as GotResponse);
expect(await getPkgReleases(config)).toBeNull();
});
it('returns null for 404', async () => {
api.get.mockImplementation(() =>
Promise.reject({
statusCode: 404,
})
);
expect(
await getPkgReleases({
...config,
registryUrls: [
...config.registryUrls,
'invalid',
'https://github.com/foo/bar',
],
})
).toBeNull();
});
it('returns null for 401', async () => {
api.get.mockImplementationOnce(() =>
Promise.reject({
statusCode: 401,
})
);
expect(await getPkgReleases(config)).toBeNull();
});
it('throws for 429', async () => {
api.get.mockImplementationOnce(() =>
Promise.reject({
statusCode: 429,
})
);
await expect(getPkgReleases(config)).rejects.toThrowError(
'registry-failure'
);
});
it('throws for 5xx', async () => {
api.get.mockImplementationOnce(() =>
Promise.reject({
statusCode: 502,
})
);
await expect(getPkgReleases(config)).rejects.toThrowError(
'registry-failure'
);
});
it('returns null for unknown error', async () => {
api.get.mockImplementationOnce(() => {
throw new Error();
});
expect(await getPkgReleases(config)).toBeNull();
});
it('processes real data from CDN', async () => {
api.get.mockResolvedValueOnce({
body: 'foo/1.2.3',
} as GotResponse);
expect(
await getPkgReleases({
...config,
registryUrls: ['https://cdn.cocoapods.org'],
})
).toEqual({
releases: [
{
version: '1.2.3',
},
],
});
});
it('processes real data from Github', async () => {
api.get.mockResolvedValueOnce({
body: [{ name: '1.2.3' }],
} as GotResponse);
expect(
await getPkgReleases({
...config,
registryUrls: ['https://github.com/Artsy/Specs'],
})
).toEqual({
releases: [
{
version: '1.2.3',
},
],
});
});
});
});

170
lib/datasource/pod/index.ts Normal file
View file

@ -0,0 +1,170 @@
import crypto from 'crypto';
import { api } from '../../platform/github/gh-got-wrapper';
import { GetReleasesConfig, ReleaseResult } from '../common';
import { logger } from '../../logger';
export const id = 'pod';
const cacheNamespace = `datasource-${id}`;
const cacheMinutes = 30;
function shardParts(lookupName: string): string[] {
return crypto
.createHash('md5')
.update(lookupName)
.digest('hex')
.slice(0, 3)
.split('');
}
function releasesGithubUrl(
lookupName: string,
opts: { account: string; repo: string; useShard: boolean }
): string {
const { useShard, account, repo } = opts;
const prefix = 'https://api.github.com/repos';
const shard = shardParts(lookupName).join('/');
const suffix = useShard ? `${shard}/${lookupName}` : lookupName;
return `${prefix}/${account}/${repo}/contents/Specs/${suffix}`;
}
async function makeRequest<T = unknown>(
url: string,
lookupName: string,
json = true
): Promise<T | null> {
try {
const resp = await api.get(url, { json });
if (resp && resp.body) {
return resp.body;
}
} catch (err) {
const errorData = { lookupName, err };
if (
err.statusCode === 429 ||
(err.statusCode >= 500 && err.statusCode < 600)
) {
logger.warn({ lookupName, err }, `CocoaPods registry failure`);
throw new Error('registry-failure');
}
if (err.statusCode === 401) {
logger.debug(errorData, 'Authorization error');
} else if (err.statusCode === 404) {
logger.debug(errorData, 'Package lookup error');
} else {
logger.warn(errorData, 'CocoaPods lookup failure: Unknown error');
}
}
return null;
}
const githubRegex = /^https:\/\/github\.com\/(?<account>[^/]+)\/(?<repo>[^/]+?)(\.git|\/.*)?$/;
async function getReleasesFromGithub(
lookupName: string,
registryUrl: string,
useShard = false
): Promise<ReleaseResult | null> {
const match = githubRegex.exec(registryUrl);
const { account, repo } = (match && match.groups) || {};
const opts = { account, repo, useShard };
const url = releasesGithubUrl(lookupName, opts);
const resp = await makeRequest<{ name: string }[]>(url, lookupName);
if (resp) {
const releases = resp.map(({ name }) => ({ version: name }));
return { releases };
}
if (!useShard) {
return getReleasesFromGithub(lookupName, registryUrl, true);
}
return null;
}
function releasesCDNUrl(lookupName: string, registryUrl: string): string {
const shard = shardParts(lookupName).join('_');
return `${registryUrl}/all_pods_versions_${shard}.txt`;
}
async function getReleasesFromCDN(
lookupName: string,
registryUrl: string
): Promise<ReleaseResult | null> {
const url = releasesCDNUrl(lookupName, registryUrl);
const resp = await makeRequest<string>(url, lookupName, false);
if (resp) {
const lines = resp.split('\n');
for (let idx = 0; idx < lines.length; idx += 1) {
const line = lines[idx];
const [name, ...versions] = line.split('/');
if (name === lookupName.replace(/\/.*$/, '')) {
const releases = versions.map(version => ({ version }));
return { releases };
}
}
}
return null;
}
const defaultCDN = 'https://cdn.cocoapods.org';
function isDefaultRepo(url: string): boolean {
const match = githubRegex.exec(url);
if (match) {
const { account, repo } = match.groups || {};
return (
account.toLowerCase() === 'cocoapods' && repo.toLowerCase() === 'specs'
); // https://github.com/CocoaPods/Specs.git
}
return false;
}
export async function getPkgReleases(
config: GetReleasesConfig
): Promise<ReleaseResult | null> {
const { lookupName } = config;
let { registryUrls } = config;
registryUrls =
registryUrls && registryUrls.length ? registryUrls : [defaultCDN];
if (!lookupName) {
logger.debug(config, `CocoaPods: invalid lookup name`);
return null;
}
const podName = lookupName.replace(/\/.*$/, '');
const cachedResult = await renovateCache.get<ReleaseResult>(
cacheNamespace,
podName
);
/* istanbul ignore next line */
if (cachedResult) {
logger.debug(`CocoaPods: Return cached result for ${podName}`);
return cachedResult;
}
let result: ReleaseResult | null = null;
for (let idx = 0; !result && idx < registryUrls.length; idx += 1) {
let registryUrl = registryUrls[idx].replace(/\/+$/, '');
// In order to not abuse github API limits, query CDN instead
if (isDefaultRepo(registryUrl)) registryUrl = defaultCDN;
if (githubRegex.exec(registryUrl)) {
result = await getReleasesFromGithub(podName, registryUrl);
} else {
result = await getReleasesFromCDN(podName, registryUrl);
}
}
if (result) {
await renovateCache.set(cacheNamespace, podName, result, cacheMinutes);
}
return result;
}

View file

@ -0,0 +1,72 @@
platform :ios, '9.0'
# use_frameworks!
inhibit_all_warnings!
source "https://github.com/CocoaPods/Specs.git"
target 'Sample' do
pod 'IQKeyboardManager', '~> 6.5.0'
pod 'CYLTabBarController', '~> 1.28.3'
pod 'PureLayout', '~> 3.1.4'
pod 'AFNetworking/Serialization', '~> 3.2.1'
pod 'AFNetworking/Security', '~> 3.2.1'
pod 'AFNetworking/Reachability', '~> 3.2.1'
pod 'AFNetworking/NSURLSession', '~> 3.2.1'
# pod 'SVProgressHUD', '~> 2.2.5'
pod 'MBProgressHUD', '~> 1.1.0'
pod 'MJRefresh', '~> 3.1.16'
pod 'MJExtension', '~> 3.1.0'
pod 'TYPagerController', '~> 2.1.2'
pod 'YYImage', '~> 1.0.4'
pod 'SDWebImage', '~> 5.0'
pod 'SDCycleScrollView','~> 1.80'
pod 'NullSafe', '~> 2.0'
# pod 'ZLPhotoBrowser'
pod 'TZImagePickerController', '~> 3.2.1'
pod 'TOCropViewController', '~> 2.5.1'
# pod 'RSKImageCropper', '~> 2.2.3'
# pod 'LBPhotoBrowser', '~> 2.2.2'
# pod 'YBImageBrowser', '~> 3.0.3'
pod 'FMDB', '~> 2.7.5'
pod 'FDStackView', '~> 1.0.1'
pod 'LYEmptyView'
pod 'MMKV', '~> 1.0.22'
pod 'fishhook'
pod 'CocoaLumberjack', '~> 3.5.3'
pod 'GZIP', '~> 1.2'
pod 'LBXScan/LBXNative','~> 2.3'
pod 'LBXScan/LBXZXing','~> 2.3'
# pod 'LBXScan/LBXZBar','~> 2.3'
pod 'LBXScan/UI','~> 2.3'
pod 'MLeaksFinder'
pod 'FBMemoryProfiler'
target 'SampleTests' do
inherit! :search_paths
# Pods for testing
end
target 'SampleUITests' do
inherit! :search_paths
# Pods for testing
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f <= 8.0
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
end
end
end
end

View file

@ -0,0 +1,11 @@
source 'https://github.com/Artsy/Specs.git'
pod 'a'
pod 'a/sub'
pod 'b', '1.2.3'
pod 'c', "1.2.3"
pod 'd', :path => '~/Documents/Alamofire'
pod 'e', :git => 'e.git'
pod 'f', :git => 'f.git', :branch => 'dev'
pod 'g', :git => 'g.git', :tag => '3.2.1'
pod 'h', :git => 'https://github.com/foo/bar.git', :tag => '0.0.1'

View file

@ -0,0 +1,167 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`.updateArtifacts() catches write error 1`] = `
Array [
Object {
"artifactError": Object {
"lockFile": "Podfile.lock",
"stderr": "not found",
},
},
]
`;
exports[`.updateArtifacts() catches write error 2`] = `Array []`;
exports[`.updateArtifacts() dynamically selects Docker image tag 1`] = `
Array [
Object {
"cmd": "docker pull renovate/cocoapods:1.2.4",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker run --rm --user=ubuntu -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/cocoapods:1.2.4 bash -l -c \\"pod install\\"",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
},
},
]
`;
exports[`.updateArtifacts() falls back to the \`latest\` Docker image tag 1`] = `
Array [
Object {
"cmd": "docker pull renovate/cocoapods:latest",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker run --rm --user=ubuntu -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/cocoapods:latest bash -l -c \\"pod install\\"",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
},
},
]
`;
exports[`.updateArtifacts() returns null for invalid local directory 1`] = `Array []`;
exports[`.updateArtifacts() returns null if no Podfile.lock found 1`] = `Array []`;
exports[`.updateArtifacts() returns null if no updatedDeps were provided 1`] = `Array []`;
exports[`.updateArtifacts() returns null if unchanged 1`] = `
Array [
Object {
"cmd": "pod install",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
},
},
]
`;
exports[`.updateArtifacts() returns null if updatedDeps is empty 1`] = `Array []`;
exports[`.updateArtifacts() returns pod exec error 1`] = `
Array [
Object {
"artifactError": Object {
"lockFile": "Podfile.lock",
"stderr": "exec exception",
},
},
]
`;
exports[`.updateArtifacts() returns pod exec error 2`] = `
Array [
Object {
"cmd": "pod install",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
},
},
]
`;
exports[`.updateArtifacts() returns updated Podfile 1`] = `
Array [
Object {
"file": Object {
"contents": "New Podfile",
"name": "Podfile.lock",
},
},
]
`;
exports[`.updateArtifacts() returns updated Podfile 2`] = `
Array [
Object {
"cmd": "docker pull renovate/cocoapods",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker run --rm -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/cocoapods bash -l -c \\"pod install\\"",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
},
},
]
`;

View file

@ -0,0 +1,394 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/cocoapods/extract extractPackageFile() extracts all dependencies 1`] = `
Array [
Object {
"depName": "a",
"groupName": "a",
"skipReason": "unknown-version",
},
Object {
"depName": "a/sub",
"groupName": "a",
"skipReason": "unknown-version",
},
Object {
"currentValue": "1.2.3",
"datasource": "pod",
"depName": "b",
"groupName": "b",
"managerData": Object {
"lineNumber": 4,
},
"registryUrls": Array [
"https://github.com/Artsy/Specs.git",
],
},
Object {
"currentValue": "1.2.3",
"datasource": "pod",
"depName": "c",
"groupName": "c",
"managerData": Object {
"lineNumber": 5,
},
"registryUrls": Array [
"https://github.com/Artsy/Specs.git",
],
},
Object {
"depName": "d",
"groupName": "d",
"skipReason": "path-dependency",
},
Object {
"depName": "e",
"groupName": "e",
"skipReason": "git-dependency",
},
Object {
"depName": "f",
"groupName": "f",
"skipReason": "git-dependency",
},
Object {
"managerData": Object {
"lineNumber": 9,
},
},
Object {
"currentValue": "0.0.1",
"datasource": "github-tags",
"depName": "h",
"lookupName": "foo/bar",
"managerData": Object {
"lineNumber": 10,
},
},
]
`;
exports[`lib/manager/cocoapods/extract extractPackageFile() extracts all dependencies 2`] = `
Array [
Object {
"currentValue": "~> 6.5.0",
"datasource": "pod",
"depName": "IQKeyboardManager",
"groupName": "IQKeyboardManager",
"managerData": Object {
"lineNumber": 8,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.28.3",
"datasource": "pod",
"depName": "CYLTabBarController",
"groupName": "CYLTabBarController",
"managerData": Object {
"lineNumber": 9,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.1.4",
"datasource": "pod",
"depName": "PureLayout",
"groupName": "PureLayout",
"managerData": Object {
"lineNumber": 11,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.2.1",
"datasource": "pod",
"depName": "AFNetworking/Serialization",
"groupName": "AFNetworking",
"managerData": Object {
"lineNumber": 12,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.2.1",
"datasource": "pod",
"depName": "AFNetworking/Security",
"groupName": "AFNetworking",
"managerData": Object {
"lineNumber": 13,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.2.1",
"datasource": "pod",
"depName": "AFNetworking/Reachability",
"groupName": "AFNetworking",
"managerData": Object {
"lineNumber": 14,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.2.1",
"datasource": "pod",
"depName": "AFNetworking/NSURLSession",
"groupName": "AFNetworking",
"managerData": Object {
"lineNumber": 15,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.1.0",
"datasource": "pod",
"depName": "MBProgressHUD",
"groupName": "MBProgressHUD",
"managerData": Object {
"lineNumber": 17,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.1.16",
"datasource": "pod",
"depName": "MJRefresh",
"groupName": "MJRefresh",
"managerData": Object {
"lineNumber": 18,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.1.0",
"datasource": "pod",
"depName": "MJExtension",
"groupName": "MJExtension",
"managerData": Object {
"lineNumber": 19,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.1.2",
"datasource": "pod",
"depName": "TYPagerController",
"groupName": "TYPagerController",
"managerData": Object {
"lineNumber": 20,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.0.4",
"datasource": "pod",
"depName": "YYImage",
"groupName": "YYImage",
"managerData": Object {
"lineNumber": 21,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 5.0",
"datasource": "pod",
"depName": "SDWebImage",
"groupName": "SDWebImage",
"managerData": Object {
"lineNumber": 22,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.80",
"datasource": "pod",
"depName": "SDCycleScrollView",
"groupName": "SDCycleScrollView",
"managerData": Object {
"lineNumber": 23,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.0",
"datasource": "pod",
"depName": "NullSafe",
"groupName": "NullSafe",
"managerData": Object {
"lineNumber": 24,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 3.2.1",
"datasource": "pod",
"depName": "TZImagePickerController",
"groupName": "TZImagePickerController",
"managerData": Object {
"lineNumber": 26,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.5.1",
"datasource": "pod",
"depName": "TOCropViewController",
"groupName": "TOCropViewController",
"managerData": Object {
"lineNumber": 27,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.7.5",
"datasource": "pod",
"depName": "FMDB",
"groupName": "FMDB",
"managerData": Object {
"lineNumber": 31,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.0.1",
"datasource": "pod",
"depName": "FDStackView",
"groupName": "FDStackView",
"managerData": Object {
"lineNumber": 32,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"depName": "LYEmptyView",
"groupName": "LYEmptyView",
"skipReason": "unknown-version",
},
Object {
"currentValue": "~> 1.0.22",
"datasource": "pod",
"depName": "MMKV",
"groupName": "MMKV",
"managerData": Object {
"lineNumber": 35,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"depName": "fishhook",
"groupName": "fishhook",
"skipReason": "unknown-version",
},
Object {
"currentValue": "~> 3.5.3",
"datasource": "pod",
"depName": "CocoaLumberjack",
"groupName": "CocoaLumberjack",
"managerData": Object {
"lineNumber": 39,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 1.2",
"datasource": "pod",
"depName": "GZIP",
"groupName": "GZIP",
"managerData": Object {
"lineNumber": 41,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.3",
"datasource": "pod",
"depName": "LBXScan/LBXNative",
"groupName": "LBXScan",
"managerData": Object {
"lineNumber": 43,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.3",
"datasource": "pod",
"depName": "LBXScan/LBXZXing",
"groupName": "LBXScan",
"managerData": Object {
"lineNumber": 44,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"currentValue": "~> 2.3",
"datasource": "pod",
"depName": "LBXScan/UI",
"groupName": "LBXScan",
"managerData": Object {
"lineNumber": 46,
},
"registryUrls": Array [
"https://github.com/CocoaPods/Specs.git",
],
},
Object {
"depName": "MLeaksFinder",
"groupName": "MLeaksFinder",
"skipReason": "unknown-version",
},
Object {
"depName": "FBMemoryProfiler",
"groupName": "FBMemoryProfiler",
"skipReason": "unknown-version",
},
]
`;

View file

@ -0,0 +1,217 @@
import { join } from 'upath';
import _fs from 'fs-extra';
import { exec as _exec } from 'child_process';
import Git from 'simple-git/promise';
import { platform as _platform } from '../../platform';
import { updateArtifacts } from '.';
import * as _datasource from '../../datasource/docker';
import { mocked } from '../../../test/util';
import { envMock, mockExecAll } from '../../../test/execUtil';
import * as _env from '../../util/exec/env';
import { setExecConfig } from '../../util/exec';
import { BinarySource } from '../../util/exec/common';
jest.mock('fs-extra');
jest.mock('child_process');
jest.mock('../../util/exec/env');
jest.mock('../../platform');
jest.mock('../../datasource/docker');
const fs: jest.Mocked<typeof _fs> = _fs as any;
const exec: jest.Mock<typeof _exec> = _exec as any;
const env = mocked(_env);
const platform = mocked(_platform);
const datasource = mocked(_datasource);
const config = {
localDir: join('/tmp/github/some/repo'),
};
describe('.updateArtifacts()', () => {
beforeEach(() => {
jest.resetAllMocks();
env.getChildProcessEnv.mockReturnValue(envMock.basic);
setExecConfig(config);
datasource.getPkgReleases.mockResolvedValue({
releases: [
{ version: '1.2.0' },
{ version: '1.2.1' },
{ version: '1.2.2' },
{ version: '1.2.3' },
{ version: '1.2.4' },
{ version: '1.2.5' },
],
});
});
it('returns null if no Podfile.lock found', async () => {
const execSnapshots = mockExecAll(exec);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
})
).toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns null if no updatedDeps were provided', async () => {
const execSnapshots = mockExecAll(exec);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: [],
newPackageFileContent: '',
config,
})
).toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns null for invalid local directory', async () => {
const execSnapshots = mockExecAll(exec);
const noLocalDirConfig = {
localDir: undefined,
};
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config: noLocalDirConfig,
})
).toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns null if updatedDeps is empty', async () => {
const execSnapshots = mockExecAll(exec);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: [],
newPackageFileContent: '',
config,
})
).toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns null if unchanged', async () => {
const execSnapshots = mockExecAll(exec);
platform.getFile.mockResolvedValueOnce('Current Podfile');
platform.getRepoStatus.mockResolvedValueOnce({
modified: [],
} as Git.StatusResult);
fs.readFile.mockResolvedValueOnce('Current Podfile' as any);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
})
).toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns updated Podfile', async () => {
const execSnapshots = mockExecAll(exec);
setExecConfig({ ...config, binarySource: BinarySource.Docker });
platform.getFile.mockResolvedValueOnce('Old Podfile');
platform.getRepoStatus.mockResolvedValueOnce({
modified: ['Podfile.lock'],
} as Git.StatusResult);
fs.readFile.mockResolvedValueOnce('New Podfile' as any);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
})
).toMatchSnapshot();
expect(execSnapshots).toMatchSnapshot();
});
it('catches write error', async () => {
const execSnapshots = mockExecAll(exec);
platform.getFile.mockResolvedValueOnce('Current Podfile');
fs.outputFile.mockImplementationOnce(() => {
throw new Error('not found');
});
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
})
).toMatchSnapshot();
expect(execSnapshots).toMatchSnapshot();
});
it('returns pod exec error', async () => {
const execSnapshots = mockExecAll(exec, new Error('exec exception'));
platform.getFile.mockResolvedValueOnce('Old Podfile.lock');
fs.outputFile.mockResolvedValueOnce(null as never);
fs.readFile.mockResolvedValueOnce('Old Podfile.lock' as any);
expect(
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
})
).toMatchSnapshot();
expect(execSnapshots).toMatchSnapshot();
});
it('dynamically selects Docker image tag', async () => {
const execSnapshots = mockExecAll(exec);
setExecConfig({
...config,
binarySource: 'docker',
dockerUser: 'ubuntu',
});
platform.getFile.mockResolvedValueOnce('COCOAPODS: 1.2.4');
fs.readFile.mockResolvedValueOnce('New Podfile' as any);
platform.getRepoStatus.mockResolvedValueOnce({
modified: ['Podfile.lock'],
} as Git.StatusResult);
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchSnapshot();
});
it('falls back to the `latest` Docker image tag', async () => {
const execSnapshots = mockExecAll(exec);
setExecConfig({
...config,
binarySource: 'docker',
dockerUser: 'ubuntu',
});
platform.getFile.mockResolvedValueOnce('COCOAPODS: 1.2.4');
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [],
});
fs.readFile.mockResolvedValueOnce('New Podfile' as any);
platform.getRepoStatus.mockResolvedValueOnce({
modified: ['Podfile.lock'],
} as Git.StatusResult);
await updateArtifacts({
packageFileName: 'Podfile',
updatedDeps: ['foo'],
newPackageFileContent: '',
config,
});
expect(execSnapshots).toMatchSnapshot();
});
});

View file

@ -0,0 +1,89 @@
import { platform } from '../../platform';
import { exec, ExecOptions } from '../../util/exec';
import { logger } from '../../logger';
import { UpdateArtifact, UpdateArtifactsResult } from '../common';
import {
getSiblingFileName,
readLocalFile,
writeLocalFile,
} from '../../util/fs';
export async function updateArtifacts({
packageFileName,
updatedDeps,
newPackageFileContent,
config,
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
logger.debug(`cocoapods.getArtifacts(${packageFileName})`);
if (updatedDeps.length < 1) {
logger.debug('CocoaPods: empty update - returning null');
return null;
}
const lockFileName = getSiblingFileName(packageFileName, 'Podfile.lock');
try {
await writeLocalFile(packageFileName, newPackageFileContent);
} catch (err) {
logger.warn({ err }, 'Podfile could not be written');
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
const existingLockFileContent = await platform.getFile(lockFileName);
if (!existingLockFileContent) {
logger.debug(`Lockfile not found: ${lockFileName}`);
return null;
}
const match = new RegExp(/^COCOAPODS: (?<cocoapodsVersion>.*)$/m).exec(
existingLockFileContent
);
const tagConstraint =
match && match.groups ? match.groups.cocoapodsVersion : null;
const cmd = 'pod install';
const execOptions: ExecOptions = {
cwdFile: packageFileName,
docker: {
image: 'renovate/cocoapods',
tagScheme: 'ruby',
tagConstraint,
},
};
try {
await exec(cmd, execOptions);
} catch (err) {
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.stderr || err.stdout || err.message,
},
},
];
}
const status = await platform.getRepoStatus();
if (!status.modified.includes(lockFileName)) {
return null;
}
logger.debug('Returning updated Gemfile.lock');
const lockFileContent = await readLocalFile(lockFileName);
return [
{
file: {
name: lockFileName,
contents: lockFileContent,
},
},
];
}

View file

@ -0,0 +1,25 @@
import fs from 'fs-extra';
import path from 'path';
import { extractPackageFile } from '.';
const simplePodfile = fs.readFileSync(
path.resolve(__dirname, './__fixtures__/Podfile.simple'),
'utf-8'
);
const complexPodfile = fs.readFileSync(
path.resolve(__dirname, './__fixtures__/Podfile.complex'),
'utf-8'
);
describe('lib/manager/cocoapods/extract', () => {
describe('extractPackageFile()', () => {
it('extracts all dependencies', () => {
const simpleResult = extractPackageFile(simplePodfile).deps;
expect(simpleResult).toMatchSnapshot();
const complexResult = extractPackageFile(complexPodfile).deps;
expect(complexResult).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,133 @@
import { logger } from '../../logger';
import { PackageDependency, PackageFile } from '../common';
import * as datasourcePod from '../../datasource/pod';
const regexMappings = [
/^\s*pod\s+(['"])(?<spec>[^'"/]+)(\/(?<subspec>[^'"]+))?\1/,
/^\s*pod\s+(['"])[^'"]+\1\s*,\s*(['"])(?<currentValue>[^'"]+)\2\s*$/,
/,\s*:git\s*=>\s*(['"])(?<git>[^'"]+)\1/,
/,\s*:tag\s*=>\s*(['"])(?<tag>[^'"]+)\1/,
/,\s*:path\s*=>\s*(['"])(?<path>[^'"]+)\1/,
/^\s*source\s*(['"])(?<source>[^'"]+)\1/,
];
export interface ParsedLine {
depName?: string;
groupName?: string;
spec?: string;
subspec?: string;
currentValue?: string;
git?: string;
tag?: string;
path?: string;
source?: string;
}
export function parseLine(line: string): ParsedLine {
const result: ParsedLine = {};
for (const regex of Object.values(regexMappings)) {
const match = regex.exec(line.replace(/#.*$/, ''));
if (match && match.groups) {
Object.assign(result, match.groups);
}
}
if (result.spec) {
const depName = result.subspec
? `${result.spec}/${result.subspec}`
: result.spec;
const groupName = result.spec;
if (depName) result.depName = depName;
if (groupName) result.groupName = groupName;
delete result.spec;
delete result.subspec;
}
return result;
}
export function gitDep(parsedLine: ParsedLine): PackageDependency | null {
const { depName, git, tag } = parsedLine;
if (git && git.startsWith('https://github.com/')) {
const githubMatch = /https:\/\/github\.com\/(?<account>[^/]+)\/(?<repo>[^/]+)/.exec(
git
);
const { account, repo } = (githubMatch && githubMatch.groups) || {};
if (account && repo) {
return {
datasource: 'github-tags',
depName,
lookupName: `${account}/${repo.replace(/\.git$/, '')}`,
currentValue: tag,
};
}
}
return null; // TODO: gitlab or gitTags datasources?
}
export function extractPackageFile(content: string): PackageFile | null {
logger.trace('cocoapods.extractPackageFile()');
const deps: PackageDependency[] = [];
const lines: string[] = content.split('\n');
const registryUrls: string[] = [];
for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
const line = lines[lineNumber];
const parsedLine = parseLine(line);
const {
depName,
groupName,
currentValue,
git,
tag,
path,
source,
}: ParsedLine = parsedLine;
if (source) {
registryUrls.push(source.replace(/\/*$/, ''));
}
if (depName) {
const managerData = { lineNumber };
let dep: PackageDependency = {
depName,
groupName,
skipReason: 'unknown-version',
};
if (currentValue) {
dep = {
depName,
groupName,
datasource: datasourcePod.id,
currentValue,
managerData,
registryUrls,
};
} else if (git) {
if (tag) {
dep = { ...gitDep(parsedLine), managerData };
} else {
dep = {
depName,
groupName,
skipReason: 'git-dependency',
};
}
} else if (path) {
dep = {
depName,
groupName,
skipReason: 'path-dependency',
};
}
deps.push(dep);
}
}
return deps.length ? { deps } : null;
}

View file

@ -0,0 +1,11 @@
import * as rubyVersioning from '../../versioning/ruby';
export { extractPackageFile } from './extract';
export { updateDependency } from './update';
export { updateArtifacts } from './artifacts';
export const defaultConfig = {
enabled: false,
fileMatch: ['(^|/)Podfile$'],
versioning: rubyVersioning.id,
};

View file

@ -0,0 +1,9 @@
The `cocoapods` manager extracts dependencies with`datasource` type `pod`. It is currently in beta so disabled by default. To opt-in to the beta, add the following to your configuration:
```json
{
"cocoapods": {
"enabled": true
}
}
```

View file

@ -0,0 +1,45 @@
import fs from 'fs-extra';
import path from 'path';
import { updateDependency } from '.';
const fileContent = fs.readFileSync(
path.resolve(__dirname, './__fixtures__/Podfile.simple'),
'utf-8'
);
describe('lib/manager/cocoapods/update', () => {
describe('updateDependency', () => {
it('replaces existing value', () => {
const upgrade = {
depName: 'b',
managerData: { lineNumber: 4 },
currentValue: '1.2.3',
newValue: '2.0.0',
};
const res = updateDependency({ fileContent, upgrade });
expect(res).not.toEqual(fileContent);
expect(res.includes(upgrade.newValue)).toBe(true);
});
it('returns same content', () => {
const upgrade = {
depName: 'b',
managerData: { lineNumber: 4 },
currentValue: '1.2.3',
newValue: '1.2.3',
};
const res = updateDependency({ fileContent, upgrade });
expect(res).toEqual(fileContent);
expect(res).toBe(fileContent);
});
it('returns null', () => {
const upgrade = {
depName: 'b',
managerData: { lineNumber: 0 },
currentValue: '1.2.3',
newValue: '2.0.0',
};
const res = updateDependency({ fileContent, upgrade });
expect(res).toBeNull();
});
});
});

View file

@ -0,0 +1,39 @@
import { logger } from '../../logger';
import { UpdateDependencyConfig } from '../common';
import { parseLine } from './extract';
function lineContainsDep(line: string, dep: string): boolean {
const { depName } = parseLine(line);
return dep === depName;
}
export function updateDependency({
fileContent,
upgrade,
}: UpdateDependencyConfig): string | null {
const { currentValue, managerData, depName, newValue } = upgrade;
// istanbul ignore if
if (!currentValue || !managerData || !depName) {
logger.warn('Cocoapods: invalid upgrade object');
return null;
}
logger.debug(`cocoapods.updateDependency: ${newValue}`);
const lines = fileContent.split('\n');
const lineToChange = lines[managerData.lineNumber];
if (!lineContainsDep(lineToChange, depName)) return null;
const regex = new RegExp(`(['"])${currentValue.replace('.', '\\.')}\\1`);
const newLine = lineToChange.replace(regex, `$1${newValue}$1`);
if (newLine === lineToChange) {
logger.debug('No changes necessary');
return fileContent;
}
lines[managerData.lineNumber] = newLine;
return lines.join('\n');
}