diff --git a/Tasks/AppCenterDistributeV3/Strings/resources.resjson/en-US/resources.resjson b/Tasks/AppCenterDistributeV3/Strings/resources.resjson/en-US/resources.resjson index e46988172dba..8af5e90fe886 100644 --- a/Tasks/AppCenterDistributeV3/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/AppCenterDistributeV3/Strings/resources.resjson/en-US/resources.resjson @@ -3,7 +3,7 @@ "loc.helpMarkDown": "For help with this task, visit the Visual Studio App Center [support site](https://aka.ms/appcentersupport/).", "loc.description": "Distribute app builds to testers and users via Visual Studio App Center", "loc.instanceNameFormat": "Deploy $(app) to Visual Studio App Center", - "loc.releaseNotes": "Added support for multiple destinations.", + "loc.releaseNotes": "Added support for forwarding Android mapping to App Center Diagnostics.", "loc.group.displayName.symbols": "Symbols", "loc.input.label.serverEndpoint": "App Center service connection", "loc.input.help.serverEndpoint": "Select the service connection for Visual Studio App Center. To create one, click the Manage link and create a new service connection.", diff --git a/Tasks/AppCenterDistributeV3/Tests/L0.ts b/Tasks/AppCenterDistributeV3/Tests/L0.ts index cf7528b02f9c..7d8ee9d59aec 100644 --- a/Tasks/AppCenterDistributeV3/Tests/L0.ts +++ b/Tasks/AppCenterDistributeV3/Tests/L0.ts @@ -261,4 +261,14 @@ describe('AppCenterDistribute L0 Suite', function () { tr.run(); assert(tr.succeeded, 'task should have succeeded'); }); + + it('Positive path: upload Android mapping txt to diagnostics', function () { + this.timeout(4000); + + let tp = path.join(__dirname, 'L0AndroidMappingTxtProvided.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + assert(tr.succeeded, 'task should have succeeded'); + }); }); diff --git a/Tasks/AppCenterDistributeV3/Tests/L0AndroidMappingTxtProvided.ts b/Tasks/AppCenterDistributeV3/Tests/L0AndroidMappingTxtProvided.ts new file mode 100644 index 000000000000..e5d8f151c8c0 --- /dev/null +++ b/Tasks/AppCenterDistributeV3/Tests/L0AndroidMappingTxtProvided.ts @@ -0,0 +1,169 @@ + +import ma = require('vsts-task-lib/mock-answer'); +import tmrm = require('vsts-task-lib/mock-run'); +import path = require('path'); +import fs = require('fs'); +import azureBlobUploadHelper = require('../azure-blob-upload-helper'); + +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var Stats = require('fs').Stats + +var nock = require('nock'); + +let taskPath = path.join(__dirname, '..', 'appcenterdistribute.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('serverEndpoint', 'MyTestEndpoint'); +tmr.setInput('appSlug', 'testuser/testapp'); +tmr.setInput('app', '/test/path/to/my.apk'); +tmr.setInput('releaseNotesSelection', 'releaseNotesInput'); +tmr.setInput('releaseNotesInput', 'my release notes'); +tmr.setInput('symbolsType', 'Android'); +tmr.setInput('mappingTxtPath', 'a/**/mapping.txt'); + +/* + Mapping folder structure: + a + mapping.txt +*/ + +//prepare upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/release_uploads') + .reply(201, { + upload_id: 1, + upload_url: 'https://example.upload.test/release_upload' + }); + +//upload +nock('https://example.upload.test') + .post('/release_upload') + .reply(201, { + status: 'success' + }); + +//finishing upload, commit the package +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/release_uploads/1', { + status: 'committed' + }) + .reply(200, { + release_id: '1', + release_url: 'my_release_location' + }); + +//make it available +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/releases/1/groups', { + id: "00000000-0000-0000-0000-000000000000" + }) + .reply(200); + + + +nock('https://example.test') + .put('/v0.1/apps/testuser/testapp/releases/1', JSON.stringify({ + release_notes: 'my release notes' + })) + .reply(200); + +nock('https://example.test') + .get('/v0.1/apps/testuser/testapp/releases/1') + .reply(200, { + short_version: "1.0", + version: "1" + }); + +//begin symbol upload +nock('https://example.test') + .post('/v0.1/apps/testuser/testapp/symbol_uploads', { + symbol_type: "AndroidProguard", + file_name: "mapping.txt", + version: "1.0", + build: "1" + }) + .reply(201, { + symbol_upload_id: 100, + upload_url: 'https://example.upload.test/symbol_upload', + expiration_date: 1234567 + }); + +//finishing symbol upload, commit the symbol +nock('https://example.test') + .patch('/v0.1/apps/testuser/testapp/symbol_uploads/100', { + status: 'committed' + }) + .reply(200); + +// provide answers for task mock +let a: ma.TaskLibAnswers = { + 'checkPath' : { + '/test/path/to/my.apk': true, + 'a': true, + 'a/mapping.txt': true + }, + 'findMatch' : { + 'a/**/mapping.txt': [ + 'a/mapping.txt' + ], + '/test/path/to/my.apk': [ + '/test/path/to/my.apk' + ] + } +}; +tmr.setAnswers(a); + +fs.createReadStream = (s: string) => { + let stream = new Readable; + stream.push(s); + stream.push(null); + + return stream; +}; + +fs.createWriteStream = (s: string) => { + let stream = new Writable; + + stream.write = () => {}; + + return stream; +}; + +fs.readdirSync = (folder: string) => { + let files: string[] = []; + + if (folder === 'a') { + files = [ + 'mapping.txt' + ] + } + + return files; +}; + +fs.statSync = (s: string) => { + const stat = new Stats; + + stat.isFile = () => { + return s.endsWith('.txt'); + } + + stat.isDirectory = () => { + return !s.endsWith('.txt'); + } + + stat.size = 100; + + return stat; +} + +azureBlobUploadHelper.AzureBlobUploadHelper.prototype.upload = async () => { + return Promise.resolve(); +} + +tmr.registerMock('azure-blob-upload-helper', azureBlobUploadHelper); +tmr.registerMock('fs', fs); + +tmr.run(); + diff --git a/Tasks/AppCenterDistributeV3/appcenterdistribute.ts b/Tasks/AppCenterDistributeV3/appcenterdistribute.ts index 89b79216affe..b61b9359f4dd 100644 --- a/Tasks/AppCenterDistributeV3/appcenterdistribute.ts +++ b/Tasks/AppCenterDistributeV3/appcenterdistribute.ts @@ -33,6 +33,13 @@ const DestinationTypeParameter = { [DestinationType.Store]: "stores" } +interface Release { + version: string; + short_version: string; +} + +type SymbolType = "Apple" | "AndroidProguard" | "UWP"; + function getEndpointDetails(endpointInputFieldName) { var errorMessage = tl.loc("CannotDecodeEndpoint"); var endpoint = tl.getInput(endpointInputFieldName, true); @@ -248,6 +255,27 @@ function updateRelease(apiServer: string, apiVersion: string, appSlug: string, r return defer.promise; } +function getRelease(apiServer: string, apiVersion: string, appSlug: string, releaseId: string, token: string, userAgent: string): Q.Promise { + tl.debug("-- Getting release."); + let defer = Q.defer(); + let getReleaseUrl: string = `${apiServer}/${apiVersion}/apps/${appSlug}/releases/${releaseId}`; + tl.debug(`---- url: ${getReleaseUrl}`); + + let headers = { + "X-API-Token": token, + "User-Agent": userAgent, + "internal-request-source": "VSTS" + }; + + request.get({ url: getReleaseUrl, headers: headers }, (err, res, body) => { + responseHandler(defer, err, res, body, () => { + defer.resolve(JSON.parse(body)); + }); + }) + + return defer.promise; +} + function getBranchName(ref: string): string { const gitRefsHeadsPrefix = 'refs/heads/'; if (ref) { @@ -278,7 +306,7 @@ function prepareSymbols(symbolsPaths: string[]): Q.Promise { utils.createZipFile(zipStream, zipPath). then(() => { - tl.debug(`---- symbols arechive file: ${zipPath}`) + tl.debug(`---- symbols archive file: ${zipPath}`) defer.resolve(zipPath); }); } else { @@ -289,7 +317,7 @@ function prepareSymbols(symbolsPaths: string[]): Q.Promise { return defer.promise; } -function beginSymbolUpload(apiServer: string, apiVersion: string, appSlug: string, symbol_type: string, token: string, userAgent: string): Q.Promise { +function beginSymbolUpload(apiServer: string, apiVersion: string, appSlug: string, symbol_type: SymbolType, token: string, userAgent: string, version?: string, build?: string): Q.Promise { tl.debug("-- Begin symbols upload") let defer = Q.defer(); @@ -302,7 +330,13 @@ function beginSymbolUpload(apiServer: string, apiVersion: string, appSlug: strin "internal-request-source": "VSTS" }; - let symbolsUploadBody = { "symbol_type": symbol_type }; + const symbolsUploadBody = { "symbol_type": symbol_type }; + + if (symbol_type === "AndroidProguard") { + symbolsUploadBody["file_name"] = "mapping.txt"; + symbolsUploadBody["version"] = version; + symbolsUploadBody["build"] = build; + } request.post({ url: beginSymbolUploadUrl, headers: headers, json: symbolsUploadBody }, (err, res, body) => { responseHandler(defer, err, res, body, () => { @@ -439,18 +473,18 @@ async function run() { let appFilePattern: string = tl.getInput('app', true); /* The task has support for different symbol types but App Center server only support Apple currently, add back these types in the task.json when support is available in App Center. - "AndroidJava": "Android (Java)", "AndroidNative": "Android (native C/C++)", "Windows": "Windows 8.1", "UWP": "Universal Windows Platform (UWP)" */ - let symbolsType: string = tl.getInput('symbolsType', false); + const taskSymbolsType: string = tl.getInput('symbolsType', false); + const symbolsType = (taskSymbolsType === 'Android' ? 'AndroidProguard' : taskSymbolsType) as SymbolType; let symbolVariableName = null; switch (symbolsType) { case "Apple": symbolVariableName = "dsymPath"; break; - case "AndroidJava": + case "AndroidProguard": symbolVariableName = "mappingTxtPath"; break; case "UWP": @@ -526,7 +560,14 @@ async function run() { if (symbolsFile) { // Begin preparing upload symbols - let symbolsUploadInfo = await beginSymbolUpload(effectiveApiServer, effectiveApiVersion, appSlug, symbolsType, apiToken, userAgent); + let version: string; + let build: string; + if (symbolsType === "AndroidProguard") { + const release = await getRelease(effectiveApiServer, effectiveApiVersion, appSlug, releaseId, apiToken, userAgent); + version = release.short_version; + build = release.version; + } + const symbolsUploadInfo = await beginSymbolUpload(effectiveApiServer, effectiveApiVersion, appSlug, symbolsType, apiToken, userAgent, version, build); // upload symbols await uploadSymbols(symbolsUploadInfo.upload_url, symbolsFile); diff --git a/Tasks/AppCenterDistributeV3/task.json b/Tasks/AppCenterDistributeV3/task.json index 24b0c87b6da0..6d3ae84e3852 100644 --- a/Tasks/AppCenterDistributeV3/task.json +++ b/Tasks/AppCenterDistributeV3/task.json @@ -13,10 +13,10 @@ "author": "Microsoft Corporation", "version": { "Major": 3, - "Minor": 152, - "Patch": 1 + "Minor": 154, + "Patch": 0 }, - "releaseNotes": "Added support for multiple destinations.", + "releaseNotes": "Added support for forwarding Android mapping to App Center Diagnostics.", "groups": [ { "name": "symbols", @@ -63,7 +63,8 @@ "defaultValue": "Apple", "groupName": "symbols", "options": { - "Apple": "Apple" + "Apple": "Apple", + "Android": "Android" } }, { @@ -110,7 +111,7 @@ "groupName": "symbols", "required": false, "helpMarkDown": "Relative path from the repo root to Android's mapping.txt file.", - "visibleRule": "symbolsType = AndroidJava" + "visibleRule": "symbolsType = Android" }, { "name": "packParentFolder", @@ -121,7 +122,8 @@ "label": "Include all items in parent folder", "groupName": "symbols", "required": false, - "helpMarkDown": "Upload the selected symbols file or folder and all other items inside the same parent folder. This is required for React Native apps." + "helpMarkDown": "Upload the selected symbols file or folder and all other items inside the same parent folder. This is required for React Native apps.", + "visibleRule": "symbolsType = Apple" }, { "name": "releaseNotesSelection", diff --git a/Tasks/AppCenterDistributeV3/task.loc.json b/Tasks/AppCenterDistributeV3/task.loc.json index 1f8f27cf6b90..c03f386bc912 100644 --- a/Tasks/AppCenterDistributeV3/task.loc.json +++ b/Tasks/AppCenterDistributeV3/task.loc.json @@ -13,8 +13,8 @@ "author": "Microsoft Corporation", "version": { "Major": 3, - "Minor": 152, - "Patch": 1 + "Minor": 154, + "Patch": 0 }, "releaseNotes": "ms-resource:loc.releaseNotes", "groups": [ @@ -63,7 +63,8 @@ "defaultValue": "Apple", "groupName": "symbols", "options": { - "Apple": "Apple" + "Apple": "Apple", + "Android": "Android" } }, { @@ -110,7 +111,7 @@ "groupName": "symbols", "required": false, "helpMarkDown": "ms-resource:loc.input.help.mappingTxtPath", - "visibleRule": "symbolsType = AndroidJava" + "visibleRule": "symbolsType = Android" }, { "name": "packParentFolder", @@ -121,7 +122,8 @@ "label": "ms-resource:loc.input.label.packParentFolder", "groupName": "symbols", "required": false, - "helpMarkDown": "ms-resource:loc.input.help.packParentFolder" + "helpMarkDown": "ms-resource:loc.input.help.packParentFolder", + "visibleRule": "symbolsType = Apple" }, { "name": "releaseNotesSelection",