in Tasks/AppStoreRelease/app-store-release.ts [80:415]
async function run() {
const appSpecificPasswordEnvVar: string = 'FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD';
const fastlaneSessionEnvVar: string = 'FASTLANE_SESSION';
let apiKeyFilePath: string;
let isTwoFactorAuthEnabled: boolean = false;
let isUsingApiKey: boolean = false;
try {
tl.setResourcePath(path.join(__dirname, 'task.json'));
// Check if this is running on Mac and fail the task if not
if (os.platform() !== 'darwin') {
throw new Error(tl.loc('DarwinOnly'));
}
// Get input variables
let authType: string = tl.getInput('authType', true);
let credentials: UserCredentials = new UserCredentials();
let apiKey: ApiKey;
const createapiKeyFilePath = (apiKeyId: string) => {
const tempPath = tl.getVariable('Agent.TempDirectory') || tl.getVariable('Agent.BuildDirectory');
return path.join(tempPath, `api_key${apiKeyId}.json`);
};
if (authType === 'ServiceEndpoint') {
let serviceEndpoint: tl.EndpointAuthorization = tl.getEndpointAuthorization(tl.getInput('serviceEndpoint', true), false);
if (serviceEndpoint.scheme === 'Token') {
// Using App Store Connect API Key
isUsingApiKey = true;
apiKeyFilePath = createapiKeyFilePath(serviceEndpoint.parameters['apiKeyId']);
apiKey = {
key_id: serviceEndpoint.parameters['apiKeyId'],
issuer_id: serviceEndpoint.parameters['apiKeyIssuerId'],
key: serviceEndpoint.parameters['apitoken'],
in_house: serviceEndpoint.parameters['apiKeyInHouse'] === 'apiKeyInHouse_true',
is_key_content_base64: true
};
} else {
credentials.username = serviceEndpoint.parameters['username'];
credentials.password = serviceEndpoint.parameters['password'];
credentials.appSpecificPassword = serviceEndpoint.parameters['appSpecificPassword'];
if (credentials.appSpecificPassword) {
isTwoFactorAuthEnabled = true;
let fastlaneSession: string = serviceEndpoint.parameters['fastlaneSession'];
if (fastlaneSession) {
credentials.fastlaneSession = fastlaneSession;
}
}
}
} else if (authType === 'UserAndPass') {
credentials.username = tl.getInput('username', true);
credentials.password = tl.getInput('password', true);
isTwoFactorAuthEnabled = tl.getBoolInput('isTwoFactorAuth');
if (isTwoFactorAuthEnabled) {
credentials.appSpecificPassword = tl.getInput('appSpecificPassword', true);
credentials.fastlaneSession = tl.getInput('fastlaneSession', false);
}
} else if (authType === 'ApiKey') {
isUsingApiKey = true;
apiKeyFilePath = createapiKeyFilePath(tl.getInput('apiKeyId', true));
apiKey = {
key_id: tl.getInput('apiKeyId', true),
issuer_id: tl.getInput('apiKeyIssuerId', true),
key: tl.getInput('apitoken', true),
in_house: tl.getBoolInput('apiKeyInHouse', false),
is_key_content_base64: true
};
}
let filePath: string = tl.getInput('ipaPath', false);
let skipBinaryUpload: boolean = tl.getBoolInput('skipBinaryUpload', false);
let uploadMetadata: boolean = tl.getBoolInput('uploadMetadata', false);
let metadataPath: string = tl.getInput('metadataPath', false);
let uploadScreenshots: boolean = tl.getBoolInput('uploadScreenshots', false);
let screenshotsPath: string = tl.getInput('screenshotsPath', false);
let releaseNotes: string = tl.getInput('releaseNotes', false);
let releaseTrack: string = tl.getInput('releaseTrack', true);
let shouldSkipWaitingForProcessing: boolean = tl.getBoolInput('shouldSkipWaitingForProcessing', false);
let shouldSubmitForReview: boolean = tl.getBoolInput('shouldSubmitForReview', false);
let shouldAutoRelease: boolean = tl.getBoolInput('shouldAutoRelease', false);
let shouldSkipSubmission: boolean = tl.getBoolInput('shouldSkipSubmission', false);
let teamId: string = tl.getInput('teamId', false);
let teamName: string = tl.getInput('teamName', false);
let distributeOnly: boolean = tl.getBoolInput('distributeOnly', false);
let appBuildNumber: string = tl.getInput('appBuildNumber', false);
const appSpecificId: string = tl.getInput('appSpecificId', false);
let applicationType: string = tl.getInput('appType', true);
let installFastlane: boolean = tl.getBoolInput('installFastlane', false);
let fastlaneVersionChoice: string = tl.getInput('fastlaneToolsVersion', false);
let fastlaneVersionToInstall: string; //defaults to 'LatestVersion'
if (fastlaneVersionChoice === 'SpecificVersion') {
fastlaneVersionToInstall = tl.getInput('fastlaneToolsSpecificVersion', true);
}
// Set up environment
tl.debug(`GEM_CACHE=${process.env['GEM_CACHE']}`);
let gemCache: string = process.env['GEM_CACHE'] || path.join(process.env['HOME'], '.gem-cache');
tl.debug(`gemCache=${gemCache}`);
process.env['GEM_HOME'] = gemCache;
process.env['FASTLANE_PASSWORD'] = credentials.password;
process.env['FASTLANE_DONT_STORE_PASSWORD'] = 'true';
process.env['FASTLANE_DISABLE_COLORS'] = 'true';
if (isTwoFactorAuthEnabled) {
// Properties required for two-factor authentication:
// 1) Account username and password
// 2) App-specific password (Apple account->Security where two factor authentication is set)
// 3) FASTLANE_SESSION, which is essentially a cookie granting access to Apple accounts
// To get a FASTLANE_SESSION, run 'fastlane spaceauth -u [email]' interactively (requires PIN)
// See: https://github.com/fastlane/fastlane/blob/master/spaceship/README.md
tl.debug('Using two-factor authentication');
if (credentials.fastlaneSession) {
process.env[fastlaneSessionEnvVar] = credentials.fastlaneSession;
} else {
if (!appSpecificId) {
tl.warning(tl.loc('SessionAndAppIdNotSet'));
}
if (!shouldSkipWaitingForProcessing) {
tl.warning(tl.loc('ShouldSkipWaitingForProcessingNotTrue'));
}
}
process.env[appSpecificPasswordEnvVar] = credentials.appSpecificPassword;
}
// Add bin of new gem home so we don't have to resolve it later
process.env['PATH'] = process.env['PATH'] + ':' + gemCache + path.sep + 'bin';
if (!skipBinaryUpload && !distributeOnly) {
if (!filePath) {
throw new Error(tl.loc('IpaPathNotSpecified'));
}
// Ensure there's exactly one ipa before installing fastlane tools
filePath = findIpa(filePath);
}
// Install the ruby gem for fastlane
tl.debug('Checking for ruby install...');
tl.which('ruby', true);
//Whenever a specific version of fastlane is requested, we're going to attempt to uninstall any installed
//versions of fastlane. Note that this doesn't uninstall dependencies of fastlane.
if (installFastlane && fastlaneVersionToInstall) {
try {
let gemRunner: ToolRunner = tl.tool(tl.which('gem', true));
gemRunner.arg(['uninstall', 'fastlane']);
tl.debug(`Uninstalling all fastlane versions...`);
gemRunner.arg(['-a', '-I']); //uninstall all versions
await gemRunner.exec();
} catch (err) {
tl.warning(tl.loc('UninstallFastlaneFailed', err));
}
}
// If desired, install the fastlane tools (if they're already present, should be a no-op)
if (installFastlane) {
tl.debug('Installing fastlane...');
let gemRunner: ToolRunner = tl.tool(tl.which('gem', true));
gemRunner.arg(['install', 'fastlane']);
if (fastlaneVersionToInstall) {
tl.debug(`Installing specific version of fastlane: ${fastlaneVersionToInstall}`);
gemRunner.arg(['-v', fastlaneVersionToInstall]);
}
await gemRunner.exec();
// If desired, update fastlane (if already latest, should be a no-op)
if (!fastlaneVersionToInstall) {
tl.debug('Updating fastlane...');
gemRunner = tl.tool(tl.which('gem', true));
gemRunner.arg(['update', 'fastlane', '-i', gemCache]);
await gemRunner.exec();
}
} else {
tl.debug('Skipped fastlane installation.');
}
let fastlaneArguments: string = tl.getInput('fastlaneArguments');
if (isUsingApiKey) {
if (fs.existsSync(apiKeyFilePath)) {
fs.unlinkSync(apiKeyFilePath);
}
let apiKeyJsonData = JSON.stringify(apiKey);
fs.writeFileSync(apiKeyFilePath, apiKeyJsonData);
}
//gem update fastlane -i ~/.gem-cache
if (releaseTrack === 'TestFlight') {
// Run pilot (via fastlane) to upload to testflight
// See https://github.com/fastlane/fastlane/blob/master/pilot/lib/pilot/options.rb for more information on these arguments
let pilotCommand: ToolRunner = tl.tool('fastlane');
let externalTestersGroups: string = tl.getInput('externalTestersGroups');
let authArgs: string[];
if (isUsingApiKey) {
authArgs = ['--api_key_path', apiKeyFilePath];
} else {
authArgs = ['-u', credentials.username];
}
if (distributeOnly) {
let bundleIdentifier: string = tl.getInput('appIdentifier', true);
pilotCommand.arg(['pilot', 'distribute', ...authArgs]);
pilotCommand.argIf(appBuildNumber, ['--build_number', appBuildNumber]);
pilotCommand.argIf(bundleIdentifier, ['-a', bundleIdentifier]);
pilotCommand.argIf(externalTestersGroups, ['--groups', externalTestersGroups]);
} else {
let bundleIdentifier: string = tl.getInput('appIdentifier', false);
pilotCommand.arg(['pilot', 'upload', ...authArgs]);
pilotCommand.arg(['-i', filePath]);
let usingReleaseNotes: boolean = isValidFilePath(releaseNotes);
if (usingReleaseNotes) {
if (!credentials.fastlaneSession) {
tl.warning(tl.loc('ReleaseNotesRequiresFastlaneSession'));
}
pilotCommand.arg(['--changelog', fs.readFileSync(releaseNotes).toString()]);
}
pilotCommand.argIf(teamId, ['-q', teamId]);
pilotCommand.argIf(teamName, ['-r', teamName]);
pilotCommand.argIf(bundleIdentifier, ['-a', bundleIdentifier]);
pilotCommand.argIf(shouldSkipSubmission, ['--skip_submission', 'true']);
pilotCommand.argIf(shouldSkipWaitingForProcessing, ['--skip_waiting_for_build_processing', 'true']);
pilotCommand.argIf(appSpecificId, ['-p', appSpecificId]);
let distributedToExternalTesters: boolean = tl.getBoolInput('distributedToExternalTesters', false);
if (distributedToExternalTesters) {
tl.debug('Distributing to external testers');
if (!usingReleaseNotes) {
throw new Error(tl.loc('ReleaseNotesRequiredForExternalTesting'));
}
pilotCommand.arg(['--distribute_external', 'true']);
if (shouldSkipSubmission || shouldSkipWaitingForProcessing) {
tl.warning(tl.loc('ExternalTestersCannotSkipWarning'));
}
pilotCommand.argIf(externalTestersGroups, ['--groups', externalTestersGroups]);
}
}
if (fastlaneArguments) {
pilotCommand.line(fastlaneArguments);
}
await pilotCommand.exec();
} else if (releaseTrack === 'Production') {
let bundleIdentifier: string = tl.getInput('appIdentifier', true);
// Run deliver (via fastlane) to publish to Production track
// See https://github.com/fastlane/fastlane/blob/master/deliver/lib/deliver/options.rb for more information on these arguments
let deliverCommand: ToolRunner = tl.tool('fastlane');
if (isUsingApiKey) {
// Prechecking in-app purchases is not supported with API key authorization
console.log(tl.loc('PrecheckInAppPurchasesDisabled'));
deliverCommand.arg(['deliver', '--force', '--precheck_include_in_app_purchases', 'false', '--api_key_path', apiKeyFilePath, '-a', bundleIdentifier]);
} else {
deliverCommand.arg(['deliver', '--force', '-u', credentials.username, '-a', bundleIdentifier]);
}
deliverCommand.argIf(skipBinaryUpload, ['--skip_binary_upload', 'true']);
//Sets -i or -c depending if app submission is for (-i) iOS/tvOS or (-c) MacOS
switch (applicationType.toLocaleLowerCase()) {
case 'macos':
// Use the -C flag for apps
if (!skipBinaryUpload) {
deliverCommand.arg(['-c', filePath]);
}
deliverCommand.arg(['-j', 'osx']); //Fastlane wants arg as OSX
break;
case 'ios':
//Use the -I flag for ipa's
if (!skipBinaryUpload) {
deliverCommand.arg(['-i', filePath]);
}
deliverCommand.arg(['-j', 'ios']);
break;
case 'tvos':
//Use the -I flag for ipa's
if (!skipBinaryUpload) {
deliverCommand.arg(['-i', filePath]);
}
deliverCommand.arg(['-j', 'appletvos']);
break;
default:
throw new Error(tl.loc('NotValidAppType', applicationType));
}
// upload metadata if specified
if (uploadMetadata && metadataPath) {
deliverCommand.arg(['-m', metadataPath]);
} else {
deliverCommand.arg(['--skip_metadata', 'true']);
}
// upload screenshots if specified
if (uploadScreenshots && screenshotsPath) {
deliverCommand.arg(['-w', screenshotsPath]);
} else {
deliverCommand.arg(['--skip_screenshots', 'true']);
}
deliverCommand.argIf(teamId, ['-k', teamId]);
deliverCommand.argIf(teamName, ['--team_name', teamName]);
deliverCommand.argIf(shouldSubmitForReview, ['--submit_for_review', 'true']);
if (shouldAutoRelease) {
deliverCommand.arg(['--automatic_release', 'true']);
} else {
deliverCommand.arg(['--automatic_release', 'false']);
}
if (fastlaneArguments) {
deliverCommand.line(fastlaneArguments);
}
await deliverCommand.exec();
}
tl.setResult(tl.TaskResult.Succeeded, tl.loc('SuccessfullyPublished', releaseTrack));
} catch (err) {
tl.setResult(tl.TaskResult.Failed, err);
} finally {
if (isTwoFactorAuthEnabled) {
tl.debug('Clearing two-factor authentication environment variables');
process.env[fastlaneSessionEnvVar] = '';
process.env[appSpecificPasswordEnvVar] = '';
}
if (isUsingApiKey && apiKeyFilePath && process.env['DEBUG_API_KEY_FILE'] !== 'true') {
tl.debug('Clearing API Key file');
if (fs.existsSync(apiKeyFilePath)) {
fs.unlinkSync(apiKeyFilePath);
}
}
}
}