├── .gitattributes ├── tests ├── tests_v2 │ ├── TestingMetadata │ │ ├── Listings │ │ │ └── en-us │ │ │ │ └── BaseListing │ │ │ │ ├── WebsiteUrl.txt │ │ │ │ ├── LicenseTerms.txt │ │ │ │ ├── PrivacyPolicy.txt │ │ │ │ ├── SupportContact.txt │ │ │ │ ├── RecommendedHardware.txt │ │ │ │ ├── Title.txt │ │ │ │ ├── Keywords.txt │ │ │ │ ├── CopyrightAndTrademarkInfo.txt │ │ │ │ ├── Images │ │ │ │ └── Screenshot │ │ │ │ │ ├── description.1152921504815018704.txt │ │ │ │ │ ├── description.1231243252353.txt │ │ │ │ │ ├── 1231243252353.png │ │ │ │ │ └── 1152921504815018704.png │ │ │ │ ├── Features.txt │ │ │ │ ├── Description.txt │ │ │ │ └── ReleaseNotes.txt │ │ └── Trailers │ │ │ ├── trailer1.trailerAssets │ │ │ └── en-us │ │ │ │ ├── title.txt │ │ │ │ └── images │ │ │ │ ├── trailer1thumbnail.txt │ │ │ │ └── trailer1thumbnail.png │ │ │ ├── trailer2.trailerAssets │ │ │ └── en-us │ │ │ │ ├── title.txt │ │ │ │ └── images │ │ │ │ ├── trailer2thumbnail.txt │ │ │ │ └── trailer2thumbnail.png │ │ │ ├── trailer1.mp4 │ │ │ └── trailer2.mp4 │ ├── TestingMetadataMissingFiles │ │ └── Listings │ │ │ └── en-us │ │ │ └── BaseListing │ │ │ ├── Title.txt │ │ │ ├── Keywords.txt │ │ │ ├── Images │ │ │ └── Screenshot │ │ │ │ ├── description.1152921504815018704.txt │ │ │ │ ├── description.1231243252353.txt │ │ │ │ ├── 1231243252353.png │ │ │ │ └── 1152921504815018704.png │ │ │ ├── Features.txt │ │ │ └── Description.txt │ └── Publish.Tests.ps1 ├── tests_v1 │ ├── requestHelpers.test.ts │ └── apiHelpers.test.ts └── GetAccessToken.ps1 ├── docs ├── add_task.png ├── edit_task.png ├── certificate.png ├── edit_pipeline.png ├── flight_task_ui.png ├── package_task_ui_1.png ├── package_task_ui_2.png ├── publish_task_ui_1.png ├── rollout_task_ui_1.png ├── new_endpoint_clientid.png ├── new_endpoint_proxy_id.png ├── new_endpoint_proxy_name.png ├── publish_task_ui_jsonzip.png ├── publish_task_ui_rollout.png ├── serviceconnection_wif.png ├── publish_task_ui_advanced.png ├── publish_task_ui_packages.png ├── rollout_task_ui_advanced.png ├── trustedcertificatesubject.png ├── WindowsStoreExtension-ThreatModel.png ├── publish_task_ui_metadata_update.png ├── dependencies.md ├── secretsauth.md ├── wifauth.md ├── contributing.md ├── project_structure.md ├── setup.md └── certificateauth.md ├── images └── logo.png ├── .npmrc ├── ThirdPartyNotices.txt ├── tasks ├── store-flight-V1 │ ├── icon.png │ ├── flightUi.ts │ ├── task.json │ └── flight.ts ├── store-package-V3 │ ├── icon.png │ ├── packageUI.ps1 │ └── task.json ├── store-publish-V1 │ ├── icon.png │ ├── publishUi.ts │ └── task.json ├── store-publish-V3 │ ├── icon.png │ └── publishUi.ps1 ├── store-rollout-V3 │ ├── icon.png │ ├── rolloutUi.ps1 │ ├── rollout.psm1 │ └── task.json ├── common │ ├── inputHelper.ts │ └── requestHelper.ts └── ps_common │ ├── vstsHelper.psm1 │ ├── storeBrokerHelper.psm1 │ └── commonHelper.psm1 ├── .gitignore ├── tsconfig.json ├── Pipelines ├── WindowsDevCenterVstsExtension-Build-Prod.yml ├── WindowsDevCenterVstsExtension-Build-NonProd.yml ├── WindowsDevCenterVstsExtension-Release-NonProd.yml ├── WindowsDevCenterVstsExtension-Release-Prod.yml └── templates │ ├── WindowsDevCenterVstsExtension-Release-Base.yml │ └── WindowsDevCenterVstsExtension-Build-Base.yml ├── jest.config.js ├── metadata └── repo.json ├── typings.json ├── LICENSE ├── package.json ├── packages.config ├── SECURITY.md ├── README.md └── vss-extension.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/WebsiteUrl.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/LicenseTerms.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/PrivacyPolicy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/SupportContact.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/RecommendedHardware.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Title.txt: -------------------------------------------------------------------------------- 1 | OHelloWorld Desktop -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Title.txt: -------------------------------------------------------------------------------- 1 | OHelloWorld Desktop -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Keywords.txt: -------------------------------------------------------------------------------- 1 | appfortestingonly 2 | fortesting -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer1.trailerAssets/en-us/title.txt: -------------------------------------------------------------------------------- 1 | Trailer 1 Title en-us -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer2.trailerAssets/en-us/title.txt: -------------------------------------------------------------------------------- 1 | Trailer 1 Title en-us -------------------------------------------------------------------------------- /docs/add_task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/add_task.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/images/logo.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/CopyrightAndTrademarkInfo.txt: -------------------------------------------------------------------------------- 1 | (c) Microsoft Corporation -------------------------------------------------------------------------------- /docs/edit_task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/edit_task.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Keywords.txt: -------------------------------------------------------------------------------- 1 | appfortestingonly 2 | fortesting -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://pkgs.dev.azure.com/Office/CLE/_packaging/CLE_PublicPackages/npm/registry/ 2 | 3 | always-auth=true -------------------------------------------------------------------------------- /ThirdPartyNotices.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/ThirdPartyNotices.txt -------------------------------------------------------------------------------- /docs/certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/certificate.png -------------------------------------------------------------------------------- /docs/edit_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/edit_pipeline.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/description.1152921504815018704.txt: -------------------------------------------------------------------------------- 1 | Test only -------------------------------------------------------------------------------- /docs/flight_task_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/flight_task_ui.png -------------------------------------------------------------------------------- /docs/package_task_ui_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/package_task_ui_1.png -------------------------------------------------------------------------------- /docs/package_task_ui_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/package_task_ui_2.png -------------------------------------------------------------------------------- /docs/publish_task_ui_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_1.png -------------------------------------------------------------------------------- /docs/rollout_task_ui_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/rollout_task_ui_1.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/description.1231243252353.txt: -------------------------------------------------------------------------------- 1 | Another image for test only -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/description.1152921504815018704.txt: -------------------------------------------------------------------------------- 1 | Test only -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer1.trailerAssets/en-us/images/trailer1thumbnail.txt: -------------------------------------------------------------------------------- 1 | Trailer 1 screenshot description en-us -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer2.trailerAssets/en-us/images/trailer2thumbnail.txt: -------------------------------------------------------------------------------- 1 | Trailer 2 screenshot description en-us -------------------------------------------------------------------------------- /docs/new_endpoint_clientid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/new_endpoint_clientid.png -------------------------------------------------------------------------------- /docs/new_endpoint_proxy_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/new_endpoint_proxy_id.png -------------------------------------------------------------------------------- /docs/new_endpoint_proxy_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/new_endpoint_proxy_name.png -------------------------------------------------------------------------------- /docs/publish_task_ui_jsonzip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_jsonzip.png -------------------------------------------------------------------------------- /docs/publish_task_ui_rollout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_rollout.png -------------------------------------------------------------------------------- /docs/serviceconnection_wif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/serviceconnection_wif.png -------------------------------------------------------------------------------- /tasks/store-flight-V1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tasks/store-flight-V1/icon.png -------------------------------------------------------------------------------- /tasks/store-package-V3/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tasks/store-package-V3/icon.png -------------------------------------------------------------------------------- /tasks/store-publish-V1/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tasks/store-publish-V1/icon.png -------------------------------------------------------------------------------- /tasks/store-publish-V3/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tasks/store-publish-V3/icon.png -------------------------------------------------------------------------------- /tasks/store-rollout-V3/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tasks/store-rollout-V3/icon.png -------------------------------------------------------------------------------- /docs/publish_task_ui_advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_advanced.png -------------------------------------------------------------------------------- /docs/publish_task_ui_packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_packages.png -------------------------------------------------------------------------------- /docs/rollout_task_ui_advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/rollout_task_ui_advanced.png -------------------------------------------------------------------------------- /docs/trustedcertificatesubject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/trustedcertificatesubject.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Features.txt: -------------------------------------------------------------------------------- 1 | Microsoft test only. Returns the test app version number. For testing only. -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/description.1231243252353.txt: -------------------------------------------------------------------------------- 1 | Another image for test only -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Description.txt: -------------------------------------------------------------------------------- 1 | OHelloWorld Descriptions. This is a app used for testing the metadata updates. -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Features.txt: -------------------------------------------------------------------------------- 1 | Microsoft test only. Returns the test app version number. For testing only. -------------------------------------------------------------------------------- /docs/WindowsStoreExtension-ThreatModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/WindowsStoreExtension-ThreatModel.png -------------------------------------------------------------------------------- /docs/publish_task_ui_metadata_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/docs/publish_task_ui_metadata_update.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Description.txt: -------------------------------------------------------------------------------- 1 | OHelloWorld Descriptions. This is a app used for testing the metadata updates. -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Trailers/trailer1.mp4 -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Trailers/trailer2.mp4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/** 2 | **/node_modules/** 3 | typings/** 4 | .vscode/** 5 | **/.taskkey 6 | lib/** 7 | .vs 8 | .vs/** 9 | .vscode/ 10 | nuget.exe 11 | packages/** 12 | sblogs/** 13 | test.js -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/1231243252353.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/1231243252353.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer1.trailerAssets/en-us/images/trailer1thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Trailers/trailer1.trailerAssets/en-us/images/trailer1thumbnail.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Trailers/trailer2.trailerAssets/en-us/images/trailer2thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Trailers/trailer2.trailerAssets/en-us/images/trailer2thumbnail.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/1152921504815018704.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/Images/Screenshot/1152921504815018704.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "sourceRoot": "${workspaceRoot}/tasks", 7 | "types": ["jest", "node"], 8 | "skipLibCheck": true 9 | } 10 | } -------------------------------------------------------------------------------- /Pipelines/WindowsDevCenterVstsExtension-Build-Prod.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pr: none 4 | 5 | variables: 6 | - name: System.Debug 7 | value: true 8 | - name: tags 9 | value: production 10 | 11 | extends: 12 | template: templates/WindowsDevCenterVstsExtension-Build-Base.yml -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/1231243252353.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/1231243252353.png -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/1152921504815018704.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/windows-dev-center-vsts-extension/HEAD/tests/tests_v2/TestingMetadataMissingFiles/Listings/en-us/BaseListing/Images/Screenshot/1152921504815018704.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | transform: { 6 | '^.+\\.ts?$': 'ts-jest', 7 | }, 8 | testRegex: '/tests/tests_v1/.*\\.(test|spec)\\.(ts|tsx)$', 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | }; 11 | -------------------------------------------------------------------------------- /Pipelines/WindowsDevCenterVstsExtension-Build-NonProd.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | schedules: 4 | - cron: '0 8 * * *' 5 | displayName: Daily Build 6 | branches: 7 | include: 8 | - master 9 | always: true 10 | 11 | variables: 12 | - name: System.Debug 13 | value: true 14 | - name: tags 15 | value: nonproduction 16 | 17 | extends: 18 | template: templates/WindowsDevCenterVstsExtension-Build-Base.yml -------------------------------------------------------------------------------- /tests/tests_v2/TestingMetadata/Listings/en-us/BaseListing/ReleaseNotes.txt: -------------------------------------------------------------------------------- 1 | • Feature 1: This is the description of the feature. 2 | • Feature 2: This is the description of the feature. 3 | • Feature 3: This is the description of the feature. 4 | • Feature 4: This is the description of the feature. 5 | • Feature 5: This is the description of the feature. 6 | • Feature 6: This is the description of the feature. 7 | • Feature 7: This is the description of the feature. -------------------------------------------------------------------------------- /metadata/repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This is a NodeJS based VSTS extension for releasing app packages to Universal Store.", 3 | "schema": { 4 | "office": "1.0.0" 5 | }, 6 | "deployedToProduction": false, 7 | "complianceTypes": [ 8 | 9 | ], 10 | "inventoryId": [ 11 | 12 | ], 13 | "contacts": [ 14 | "kupatil@microsoft.com" 15 | ] 16 | } -------------------------------------------------------------------------------- /Pipelines/WindowsDevCenterVstsExtension-Release-NonProd.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pr: none 4 | 5 | variables: 6 | - name: System.Debug 7 | value: true 8 | - name: tags 9 | value: nonproduction 10 | 11 | resources: 12 | pipelines: 13 | - pipeline: WindowsDevCenterVstsExtension 14 | source: WindowsDevCenterVstsExtension-Build-NonProd 15 | 16 | extends: 17 | template: templates/WindowsDevCenterVstsExtension-Release-Base.yml 18 | parameters: 19 | isProdEnabled: false -------------------------------------------------------------------------------- /Pipelines/WindowsDevCenterVstsExtension-Release-Prod.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pr: none 4 | 5 | variables: 6 | - name: System.Debug 7 | value: true 8 | - name: tags 9 | value: production 10 | 11 | resources: 12 | pipelines: 13 | - pipeline: WindowsDevCenterVstsExtension 14 | source: WindowsDevCenterVstsExtension-Build-Prod 15 | trigger: 16 | branches: 17 | include: 18 | - master 19 | 20 | extends: 21 | template: templates/WindowsDevCenterVstsExtension-Release-Base.yml -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "form-data": "registry:dt/form-data#0.0.0+20160316155526", 4 | "minimatch": "registry:dt/minimatch#2.0.8+20160317120654", 5 | "node": "registry:dt/node#6.0.0+20160709114037", 6 | "node-uuid": "registry:dt/node-uuid#0.0.0+20160316155526", 7 | "node-uuid/node-uuid-base": "registry:dt/node-uuid/node-uuid-base#0.0.0+20160316155526", 8 | "node-uuid/node-uuid-cjs": "registry:dt/node-uuid/node-uuid-cjs#0.0.0+20160316155526", 9 | "q": "registry:dt/q#0.0.0+20160613154756", 10 | "request": "registry:dt/request#0.0.0+20160524135346" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Pipelines/templates/WindowsDevCenterVstsExtension-Release-Base.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: isProdEnabled 3 | type: boolean 4 | default: true 5 | 6 | resources: 7 | repositories: 8 | - repository: MROPT 9 | type: git 10 | name: 1ESPipelineTemplates/MROPT 11 | ref: refs/heads/releases/templates 12 | 13 | extends: 14 | template: /v1/extends/AzureDevOps/VstsExtensionRelease.yml@MROPT 15 | parameters: 16 | isProdEnabled: ${{ parameters.isProdEnabled }} 17 | pipelineInput: 18 | input: pipelineArtifact 19 | pipeline: WindowsDevCenterVstsExtension 20 | artifactName: drop 21 | targetPath: $(Pipeline.Workspace)/drop 22 | approvers: RDX - MRO Backend-Super_ADO_Admin 23 | stagingVsixFile: $(Pipeline.Workspace)/drop/ms-rdx-mro.windows-store-publish-dev-*.vsix 24 | productionVsixFile: $(Pipeline.Workspace)/drop/ms-rdx-mro.windows-store-publish-2*.vsix 25 | releaseBranch: refs/heads/master -------------------------------------------------------------------------------- /docs/dependencies.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | ## v0.\* 4 | 5 | Tasks with version v0.\* of the extension are written in TypeScript and their dependencies are listed in [package.json](../package.json). 6 | Running ```npm install``` will install them. 7 | 8 | ## v2.\* 9 | 10 | Tasks with version v2.\* of the extension are written in PowerShell and are powered by [StoreBroker](https://github.com/Microsoft/StoreBroker/tree/v2). 11 | Running ```gulp``` will create the necessary folder structure and populate the dependencies as part of the build. For ways of manually getting 12 | the dependencies, visit the links below. 13 | 14 | For StoreBroker, create a directory named ```StoreBroker``` under ```lib``` and follow the instructions in the link. 15 | 16 | - [NugetPackages](../lib/ps_modules/NugetPackages/README.MD) 17 | - [StoreBroker](https://github.com/Microsoft/StoreBroker/blob/v2/Documentation/SETUP.md#installation) 18 | - [VstsTaskSdk](https://github.com/Microsoft/azure-pipelines-task-lib/blob/master/powershell/Docs/Consuming.md#where-to-get-it) 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Team Services Extension for the Windows Dev Center 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | software and associated documentation files (the "Software"), to deal in the Software 8 | without restriction, including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit 10 | persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copiesor 13 | substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 16 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "winstore-extension", 3 | "version": "1.0.0", 4 | "description": "Visual Studio Team Services (VSTS) extension for performing continuous delivery to the Windows Store from your automated CI builds.", 5 | "author": "Microsoft", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Microsoft/windows-dev-center-vsts-extension" 10 | }, 11 | "dependencies": { 12 | "@azure/storage-blob": "^12.28.0", 13 | "adm-zip": "^0.5.16", 14 | "axios": "^1.12.0", 15 | "azure-pipelines-task-lib": "^4.17.3", 16 | "glob": "^7.0.3", 17 | "gulp-git": "^2.11.0", 18 | "gulp-nuget": "^1.3.0", 19 | "jszip": "^3.1.1", 20 | "q": "^1.4.1", 21 | "url": "^0.11.0", 22 | "uuid": "^9.0.1" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^30.0.0", 26 | "@types/node": "^24.1.0", 27 | "@types/q": "^1.5.8", 28 | "del": "^5.1.0", 29 | "fs-extra": "^11.3.0", 30 | "gulp": "^4.0.2", 31 | "gulp-exec": "^3.0.2", 32 | "gulp-if": "^2.0.2", 33 | "gulp-multi-dest": "^1.3.7", 34 | "gulp-rename": "^1.4.0", 35 | "gulp-replace": "^1.1.4", 36 | "gulp-typescript": "^5.0.1", 37 | "jest": "^30.0.5", 38 | "ts-jest": "^29.4.1", 39 | "typescript": "^5.8.3 ", 40 | "yargs": "^13.2.4" 41 | }, 42 | "scripts": { 43 | "test": "jest", 44 | "npmCredentialRefresh": "artifacts-npm-credprovider" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tasks/common/inputHelper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * General helper to process input variables. 3 | */ 4 | 5 | import tl = require('azure-pipelines-task-lib'); 6 | import path = require('path'); 7 | 8 | var glob = require('glob'); 9 | 10 | /** 11 | * Get appropriate files from the provided pattern 12 | * @param {string} path The minimatch pattern of glob to be resolved to file paths 13 | * @returns {string[]} file paths resolved by glob 14 | */ 15 | export function resolvePathPattern(pathPattern: string) : string[] 16 | { 17 | var filesList: string[] = []; 18 | if (pathPattern) { 19 | // Remove unnecessary quotes in path pattern, if any. 20 | pathPattern = pathPattern.replace(/\"/g, ""); 21 | 22 | filesList = filesList.concat(glob.sync(pathPattern)); 23 | } 24 | 25 | return filesList; 26 | } 27 | 28 | /** 29 | * Verifies if the filePath input was supplied by comparing it with the working directory of the release. 30 | * 31 | * VSTS will put by default the working directory as the value of an empty filePath input. 32 | * @param name The name of the input parameter; 33 | * @return true if the path was supplied, false if it is equal to the working directory; 34 | */ 35 | export function inputFilePathSupplied(name: string, required: boolean): boolean 36 | { 37 | var path = tl.getInput(name, required); 38 | return path != tl.getVariable('Agent.ReleaseDirectory'); 39 | } 40 | 41 | /** 42 | * Creates a canonical version of a path. Separators are converted to the current platform, 43 | * '.'.and '..' segments are resolved, and multiple contiguous separators are combined in one. 44 | * If a path contains both kinds of separators, it will be parsed as a posix path (with '/' separators). 45 | * 46 | * For example, the paths 'foo//bar/../quux.txt' and 'foo\\.\\quux.txt' should have the same canonical 47 | * representation. 48 | * 49 | * This function should be idempotent: canonicalizePath(canonicalizePath(x)) === canonicalizePath(x)) 50 | * @param aPath 51 | */ 52 | export function canonicalizePath(aPath: string): string 53 | { 54 | var pathObj: path.ParsedPath; 55 | if (aPath.indexOf('/') != -1) 56 | { 57 | pathObj = path.posix.parse(aPath); 58 | } 59 | else 60 | { 61 | pathObj = path.win32.parse(aPath); 62 | } 63 | 64 | return path.normalize(path.format(pathObj)); 65 | } -------------------------------------------------------------------------------- /Pipelines/templates/WindowsDevCenterVstsExtension-Build-Base.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: MROPT 4 | type: git 5 | name: 1ESPipelineTemplates/MROPT 6 | ref: refs/heads/releases/templates 7 | - repository: MRO 8 | type: git 9 | name: CLE/MRO 10 | ref: refs/heads/dev 11 | 12 | extends: 13 | template: /v1/extends/OfficeDefaultPool.yml@MROPT 14 | parameters: 15 | officialTemplate: true 16 | serviceTreeId: 7bf80b2c-ef84-47f3-bc43-043356dcb9be 17 | sdl: 18 | eslint: 19 | configuration: required 20 | parser: '@typescript-eslint/parser' 21 | parserOptions: 'sourceType:module' 22 | enableExclusions: true 23 | exclusionPatterns: "" 24 | customEnvironments: true 25 | environmentsBrowser: true 26 | environmentsNode: true 27 | environmentsCommonJs: false 28 | environmentsSharedNodeBrowser: false 29 | environmentsEs6: true 30 | environmentsEs2017: true 31 | environmentsEs2020: true 32 | environmentsWorker: false 33 | environmentsAmd: false 34 | environmentsMocha: false 35 | environmentsJasmine: false 36 | environmentsJest: false 37 | environmentsPhantomjs: false 38 | environmentsProtractor: false 39 | environmentsQunit: false 40 | environmentsJquery: false 41 | environmentsPrototypejs: false 42 | environmentsShelljs: false 43 | environmentsMeteor: false 44 | environmentsMongo: false 45 | stages: 46 | - stage: 47 | jobs: 48 | - job: 49 | templateContext: 50 | outputs: 51 | - output: pipelineArtifact 52 | targetPath: $(Build.ArtifactStagingDirectory) 53 | artifactName: drop 54 | steps: 55 | - script: npm install -g tfx-cli 56 | - task: Npm@1 57 | inputs: 58 | command: install 59 | verbose: true 60 | - task: gulp@0 61 | inputs: 62 | gulpFile: gulpfile.js 63 | arguments: '--dev' 64 | - task: gulp@0 65 | inputs: 66 | gulpFile: gulpfile.js 67 | arguments: '--public' 68 | - task: CopyFiles@2 69 | inputs: 70 | SourceFolder: build/extension 71 | Contents: '*.vsix' 72 | TargetFolder: '$(Build.ArtifactStagingDirectory)' 73 | - template: /pipelines/VstsExtensions/templates/AdoExtensionCodeSign.yml@MRO 74 | parameters: 75 | vsixDirectory: $(Build.ArtifactStagingDirectory) 76 | vsixFilePattern: '*.vsix' -------------------------------------------------------------------------------- /tests/tests_v1/requestHelpers.test.ts: -------------------------------------------------------------------------------- 1 | import requestHelpers = require('../../tasks/common/requestHelper'); 2 | 3 | // Test function authenticate 4 | test('authenticate', async () => { 5 | const cred: requestHelpers.Credentials = { 6 | tenant: "", 7 | clientId: "", 8 | clientSecret: "" 9 | } 10 | 11 | var resource = "https://manage.devcenter.microsoft.com"; 12 | 13 | var response = await requestHelpers.authenticate(resource, cred); 14 | // Depending on the API request you are testing you can add what you expect from the response. 15 | // e.g. expect(response).toEqual({ success: true }); 16 | }); 17 | 18 | // Test function performAuthenticatedRequest 19 | test('performAuthenticatedRequest', async () => { 20 | const cred: requestHelpers.Credentials = { 21 | tenant: "", 22 | clientId: "", 23 | clientSecret: "" 24 | } 25 | 26 | const tok: requestHelpers.AccessToken = { 27 | resource: "", 28 | credentials: cred, 29 | expiration: Date.now(), 30 | token: "<>" 31 | }; 32 | 33 | var requestParams = { 34 | url: '<>', 35 | method: 'GET', 36 | }; 37 | 38 | console.log("Begin running performAuthenticatedRequest"); 39 | 40 | var response = await requestHelpers.performAuthenticatedRequest(tok, requestParams); 41 | // Depending on the API request you are testing you can add what you expect from the response. 42 | // e.g. expect(response).toEqual({ success: true }); 43 | }); 44 | 45 | // Test function performAuthenticatedRequestWithRetry 46 | test('performAuthenticatedRequestWithRetry', async () => { 47 | const cred: requestHelpers.Credentials = { 48 | tenant: "", 49 | clientId: "", 50 | clientSecret: "" 51 | } 52 | 53 | const tok: requestHelpers.AccessToken = { 54 | resource: "", 55 | credentials: cred, 56 | expiration: Date.now(), 57 | token: "<>" 58 | }; 59 | 60 | var requestParams = { 61 | url: '<>', 62 | method: 'GET', 63 | }; 64 | 65 | console.log("Begin running performAuthenticatedRequest"); 66 | 67 | var getGenerator = () => requestHelpers.performAuthenticatedRequest(tok, requestParams); 68 | var response = await requestHelpers.withRetry(3, getGenerator, err => requestHelpers.isRetryableError(err)); 69 | // Depending on the API request you are testing you can add what you expect from the response. 70 | // e.g. expect(response).toEqual({ success: true }); 71 | }); -------------------------------------------------------------------------------- /docs/secretsauth.md: -------------------------------------------------------------------------------- 1 | # Using secret and Azure Resource Manager service connection to authenticate to Windows Store Partner Center 2 | 3 | ## Prerequisites 4 | You must have an Azure Active Directory (AAD) and owner access of it. See [setup.md](setup.md) for details. 5 | 6 | ## Step 1: Adding App Secret to your Azure AD application 7 | Go to Azure portal and find your Azure AD application, and do the following steps. 8 | 9 | 1. Go to **Manage** > **Certificates & secrets**. 10 | 2. Select **Client Secrets**. 11 | 3. Select **New Client Secret**. 12 | 4. Enter your **Description** for the secret and set an **Expire** time. 13 | 14 | 15 | ## Step 2: Creating a service connection 16 | The v3\* version of service connection requires using Azure resource manager service connection regardless of what type of authentication scheme you use. So even if you want to keep using App secret, you need to switch to Azure resource manager service connection. Here are the steps to do it: 17 | 1. Go to project settings -> Service Connection click on “New Service Connection”. Select Azure Resource Manager, and then select Service Principal (Manual) for authentication method. 18 | 2. Fill in the service principal ID and tenant ID with the client ID and tenant of your Azure AD application. 19 | 3. For credential, select "Service Principal Key", and fill up the field "Service Principal Key" with the Azure AD secret you created. 20 | 4. If you have an Azure subscription in the same tenant as your service principal, then you can fill in the subscription ID and subscription Name with those of your Azure subscription. It's not mandatory for you to provide the subscription ID and subscription name in order to run the Windows Store extension. You can simply provide any value for subscription ID and subscription name as shown in the screenshot above, and do "save without verification" to create the service connection. To save without doing any verification, you can click on the dropdown on the right of the button "verify and save". 21 | 22 | ## Step 3: Adding the Service Connection to your Pipeline 23 | Make sure you add the service connection to your extension task in your pipeline. If you are using classic release pipeline, you can add the service connection directly using the UI. If you are maintaining a YAML pipeline, you should add the service connection to the serviceEndpoint field under inputs. E.g. 24 | 25 | ``` 26 | - task: MS-RDX-MRO.windows-store-publish-dev.flight-task.store-flight@3 27 | displayName: 'Publish' 28 | inputs: 29 | serviceEndpoint: 30 | appId: XXX 31 | flightNameType: FlightName 32 | flightName: XXX 33 | sourceFolder: XXX 34 | contents: XXX 35 | ``` -------------------------------------------------------------------------------- /docs/wifauth.md: -------------------------------------------------------------------------------- 1 | # Using federated credentials to authenticate to Windows Store Partner Center 2 | 3 | ## Prerequisites 4 | You must have an Azure Active Directory (AAD) and owner access of it. See [setup.md](setup.md) for details. 5 | 6 | ## Step 1: Creating a service connection 7 | On Azure DevOps, create an Azure Resource Manager type service connection and select “Workload Identity Federation (Manual)” as type. You will be asked to provide the following information: 8 | 9 | 1. Subscription ID: ID of an Azure subscription used for verification step required to create the service connection. 10 | 2. Subscription Name: Name of an Azure subscriptions used for verification step required to create the service connection. 11 | 3. Service Principal ID: Azure AD client ID for accessing Partner Center API. 12 | 4. Tenant ID: Tenant ID for Azure AD used for accessing Partner Center API. Use Microsoft tenant ID in this field. 13 | 14 |
15 | 16 | ![alt text](serviceconnection_wif.png) 17 | 18 | It's not mandatory for you to provide the subscription ID and subscription name in order to run the Windows Store extension. You can provide any value for subscription ID and subscription name as shown in the screenshot above, and do "save without verification" to create the service connection. To save without doing any verification, you can click on the dropdown on the right of the button "verify and save". 19 | 20 | Your service connection should contain 2 additional fields called **Issuer** and **Subject Identifier**. These two fields will later be used to create federated credentials for your Azure AD application. 21 | 22 | ## Step 2: Adding federated credential to your Azure AD application 23 | Go to Azure portal and find your Azure AD application, and do the following steps. 24 | 25 | 1. Go to **Manage** > **Certificates & secrets**. 26 | 2. Select **Federated credentials**. 27 | 3. Select **Add credentials**. 28 | 4. Select the **Other issuer** scenario. 29 | 5. Enter values for **Issuer** and **Subject identifier** from those generated by your service connection. 30 | 31 | ## Step 3: Adding the Service Connection to your Pipeline 32 | Make sure you add the service connection to your extension task in your pipeline. If you are using classic release pipeline, you can add the service connection directly using the UI. If you are maintaining a YAML pipeline, you should add the service connection to the serviceEndpoint field under inputs. E.g. 33 | 34 | ``` 35 | - task: MS-RDX-MRO.windows-store-publish-dev.flight-task.store-flight@3 36 | displayName: 'Publish' 37 | inputs: 38 | serviceEndpoint: 39 | appId: XXX 40 | flightNameType: FlightName 41 | flightName: XXX 42 | sourceFolder: XXX 43 | contents: XXX 44 | ``` -------------------------------------------------------------------------------- /tests/GetAccessToken.ps1: -------------------------------------------------------------------------------- 1 | // Use this script to create access token that can be used by other test files under /tests folder 2 | 3 | [CmdletBinding()] 4 | param() 5 | Set-StrictMode -Version 2.0 6 | Write-Verbose "Check if write verbose works" 7 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 8 | Set-Variable -Name "NugetPath" -Value "$PSScriptRoot\..\lib\ps_modules\NugetPackages" -Option Constant -Scope Local -Force 9 | Set-Variable -Name "VstsHelperPath" -Value "$PSScriptRoot\..\tasks\ps_common\vstsHelper.psm1" -Option Constant -Scope Local -Force 10 | Set-Variable -Name "OpenSSLPath" -Value "$PSScriptRoot\..\lib\ps_modules\openssl" -Option Constant -Scope Global -Force 11 | 12 | Import-Module $VstsHelperPath 6>$null 5>$null 4>$null 3>$null 1>$null 13 | Import-Module "$PSScriptRoot\..\tasks\ps_common\vstsHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 14 | Import-Module "$PSScriptRoot\..\lib\ps_modules\VstsTaskSdk\VstsTaskSdk.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 15 | Import-Module "$PSScriptRoot\..\lib\ps_modules\AdoAzureHelper\AdoAzureHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 16 | 17 | # Getting AAD accessToken that would be later used by all the StoreBroker commands to access Partner Center APIs. 18 | Initialize-AdoAzureHelper -msalLibraryDir $NugetPath -adoApiLibraryDir $NugetPath -openSSLExeDir $OpenSSLPath 19 | $resource = "https://manage.devcenter.microsoft.com" 20 | 21 | 22 | // To get a token locally we can create a key vault certificate with subject ID set to CN=f8c7a2d2-b9d1-4c63-988a-6e4cceb58b7e and export it as a PEM file 23 | // Then replace ServicePrincipalCertificate value with the actual content of the .pem file 24 | $endpointId = "XXX" 25 | $endPointObj = @{ 26 | 'Auth' = @{ 27 | 'parameters' = @{ 28 | # This is the tenant Id of Microsoft 29 | 'AuthenticationType' = 'SPNCertificate' 30 | # This is the client ID of Azure Active Directory Service 31 | 'ServicePrincipalId' = "f8c7a2d2-b9d1-4c63-988a-6e4cceb58b7e" 32 | # This is the tenant ID of Azure Active Directory 33 | 'TenantId' = "72f988bf-86f1-41af-91ab-2d7cd011db47" 34 | # This is the certificate used for the service principal 35 | 'ServicePrincipalCertificate' = "XXXX" 36 | } 37 | } 38 | } 39 | 40 | $sendX5C = $true 41 | $useMSAL = $true 42 | if (($endPointObj.Auth.Scheme -eq 'WorkloadIdentityFederation') -or ($endPointObj.Auth.Parameters.AuthenticationType -ne 'SPNCertificate')) 43 | { 44 | $sendX5C = $false 45 | } 46 | 47 | $aadAccessToken = (Get-AzureRMAccessToken $endPointObj $endpointId $resource $sendX5C $useMSAL).access_token 48 | 49 | // Saving access token to output txt file to be used later by other integration tests 50 | Set-Content -Path "./output.txt" -Value $aadAccessToken 51 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure DevOps extension for the Windows Store API (Partner Center API) 2 | 3 | [![Build Status](https://office.visualstudio.com/CLE/_apis/build/status%2FWindows%20Store%20Azure%20DevOps%20Extension%2Fwindows-dev-center-vsts-extension%2FWindowsDevCenterVstsExtension-Build-Prod?repoName=microsoft%2Fwindows-dev-center-vsts-extension&branchName=master)](https://office.visualstudio.com/CLE/_build/latest?definitionId=30507&repoName=microsoft%2Fwindows-dev-center-vsts-extension&branchName=master) 4 | 5 | This extension provides tasks to automate the release of your Windows apps to the Windows Store from your continuous integration environment in Azure DevOps (formely Visual Studio Team Services or VSTS). You no longer need to manually update your apps to the [Windows Partner Center dashboard](https://partner.microsoft.com/en-us/dashboard/windows/overview). 6 | 7 | There are 2 major versions for this extension: v3 and v0. 8 | * V3 is for internal users at Microsoft and it could only be used within Microsoft owned ADO organizations. It authenticates to Windows Store publishing API using certificate or [workload identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation). Both of these methods are available only within Microsoft. It's mandatory for internal Microsoft users to use this version due to the recent security requirement to stop using client secret for authentication in Microsoft Entra ID. 9 | * V0 is for external users outside of Microsoft and it authenticates to Windows Store Publishing API using [client secret](https://learn.microsoft.com/en-us/entra/identity-platform/how-to-add-credentials?tabs=client-secret). 10 | 11 | ## Setup 12 | 13 | See instructions on setting up the extension in [Setup](./docs/setup.md) 14 | 15 | ## Usage 16 | 17 | See instructions on how to use the extension in [Usage](./docs/usage.md) 18 | 19 | ## Developing and contributing 20 | 21 | We welcome your suggestions for enhancements to the extension. Please see the [Contribution Guide](./docs/contributing.md) for information on how to develop and contribute. 22 | 23 | ## Code of conduct 24 | 25 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 26 | 27 | ## Legal and Licensing 28 | 29 | Azure DevOps extension for the Windows Store is licensed under the [MIT license](./LICENSE) 30 | 31 | ## Privacy Policy 32 | 33 | For more information, refer to Microsoft's [Privacy Policy](https://go.microsoft.com/fwlink/?LinkID=521839). 34 | 35 | ## Terms of Use 36 | 37 | For more information, refer to Microsoft's [Terms of Use](https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx). -------------------------------------------------------------------------------- /tests/tests_v1/apiHelpers.test.ts: -------------------------------------------------------------------------------- 1 | import apiHelpers = require('../../tasks/common/apiHelper'); 2 | import request = require('../../tasks/common/requestHelper'); 3 | 4 | var submissionId; 5 | 6 | test('getAppResource', async () => { 7 | const token: request.AccessToken = { 8 | resource: "", 9 | credentials: { 10 | tenant: "", 11 | clientId: "", 12 | clientSecret: "" 13 | }, 14 | expiration: Date.now(), 15 | token: "XXXX" 16 | }; 17 | 18 | apiHelpers.ROOT = "https://manage.devcenter.microsoft.com/v1.0/my/"; 19 | const result = await apiHelpers.getAppResource(token, "9N1XVXWJ30RV"); 20 | expect(result).toBeDefined(); 21 | console.log ("Run getAppResource successfully with result:", result); 22 | }, 100000); 23 | 24 | test('createSubmission', async () => { 25 | const token: request.AccessToken = { 26 | resource: "", 27 | credentials: { 28 | tenant: "", 29 | clientId: "", 30 | clientSecret: "" 31 | }, 32 | expiration: Date.now(), 33 | token: "XXXX" 34 | }; 35 | 36 | var URL = "https://manage.devcenter.microsoft.com/v1.0/my/applications/9N1XVXWJ30RV/submissions"; 37 | const result = await apiHelpers.createSubmission(token, URL); 38 | submissionId = result.id; 39 | expect(result).toBeDefined(); 40 | console.log ("Run createSubmission successfully with result:", result); 41 | }, 100000); 42 | 43 | test('checkSubmissionStatus', async () => { 44 | const token: request.AccessToken = { 45 | resource: "", 46 | credentials: { 47 | tenant: "", 48 | clientId: "", 49 | clientSecret: "" 50 | }, 51 | expiration: Date.now(), 52 | token: "XXXX" 53 | }; 54 | 55 | apiHelpers.ROOT = "https://manage.devcenter.microsoft.com/v1.0/my/"; 56 | // Replace with the actual submission ID 57 | var resourceLocation = `applications/9N1XVXWJ30RV/submissions/`; 58 | const result = await apiHelpers.checkSubmissionStatus(token, resourceLocation, 'Immediate'); 59 | expect(result).toBeDefined(); 60 | console.log ("Run checkSubmissionStatus successfully with result:", result); 61 | }, 100000); 62 | 63 | test('deleteSubmission', async () => { 64 | const token: request.AccessToken = { 65 | resource: "", 66 | credentials: { 67 | tenant: "", 68 | clientId: "", 69 | clientSecret: "" 70 | }, 71 | expiration: Date.now(), 72 | token: "XXXX" 73 | }; 74 | 75 | // Replace with the actual submission ID 76 | var URL = `https://manage.devcenter.microsoft.com/v1.0/my/applications/9N1XVXWJ30RV/submissions/`; 77 | const result = await apiHelpers.deleteSubmission(token, URL); 78 | expect(result).toBeDefined(); 79 | console.log ("Run deleteSubmission successfully with result:", result); 80 | }, 100000); -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Visual Studio Team Services extension for the Windows Store 2 | 3 | ## For general documentation about the extension, visit the [marketplace](https://marketplace.visualstudio.com/items?itemName=MS-RDX-MRO.windows-store-publish) 4 | 5 | *** 6 | ## Guidelines for opening issues 7 | 8 | In all cases, make sure to search the list of issues before opening a new one. Duplicate issues will be closed. 9 | 10 | ### 1. Questions 11 | 12 | You should open an issue if you have questions about the way the extension work, to report a bug or make a suggestion. 13 | 14 | ### 2. Bugs 15 | 16 | To report a bug, please include as much information as possible, namely: 17 | 18 | * Which version of the extension you are using 19 | * Your agent configuration 20 | * If possible, logs from the task that exhibit the erroneous behavior 21 | * The behavior you expect to see 22 | 23 | Please also mark your issue with the 'bug' label. 24 | 25 | ### 3. Suggestions 26 | 27 | We welcome your suggestions for enhancements to the extension. To ensure that we can integrate your suggestions 28 | effectively, try to be as detailed as possible and include: 29 | 30 | * What you want to achieve / what is the problem that you want to address 31 | * What is you approach for solving the problem 32 | * If applicable, a user scenario of the feature/enhancement in action 33 | 34 | Please also mark your issue with the 'suggestion' label. 35 | 36 | *** 37 | ## Guidelines for contributing code 38 | 39 | We welcome all pull requests. To maximize your chances of being accepted, your pull request should include a clear 40 | description of your changes, and your commits should have descriptive messages as well. 41 | 42 | ### Developer prerequisites and tools 43 | 44 | In order to be able to build and contribute code, you will need [node.js](https://nodejs.org) and (npm)[https://npmjs.com] (bundled with node). 45 | 46 | This project is written in [TypeScript](https://www.typescriptlang.org/) (v0.\*) and [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/powershell-scripting?view=powershell-3.0) (v2.\*), 47 | and uses [Gulp](http://gulpjs.com/) for the build system. 48 | 49 | Building this extension will bring all necessary dependencies for you, you can read more about 50 | other ways to install dependencies in [Dependencies](./lib/ps_modules/DEPENDENCIES.MD). 51 | 52 | ### How to build 53 | 54 | 1. Clone this repo in ```$winstore-extension``` 55 | 2. ```cd $winstore-extension``` 56 | 3. ```npm install``` 57 | 4. ```typings install``` 58 | 5. ```gulp``` 59 | 60 | Run ```gulp --tasks``` or consult [gulp.js](./gulp.js) for more build options. 61 | 62 | *** 63 | ## Code of conduct 64 | 65 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 66 | For more information see the [Code of conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact 67 | [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 68 | 69 | *** 70 | ## Legal 71 | 72 | You will need to complete a Contributor License Agreement (CLA). Briefly, this agreement testifies that you are granting 73 | us permission to use the submitted change according to the terms of the project's license, and that the work being 74 | submitted is under appropriate copyright. You only need to do this once. 75 | 76 | When you submit a pull request, @msftclas will automatically determine whether you need to sign a CLA, comment on the PR 77 | and label it appropriately. If you do need to sign a CLA, please visit https://cla.microsoft.com and follow the steps. 78 | -------------------------------------------------------------------------------- /tasks/ps_common/vstsHelper.psm1: -------------------------------------------------------------------------------- 1 | function BasicAuthHeader() 2 | { 3 | [CmdletBinding()] 4 | param( 5 | [string]$authtoken 6 | ) 7 | 8 | $ba = (":{0}" -f $authtoken) 9 | $ba = [System.Text.Encoding]::UTF8.GetBytes($ba) 10 | $ba = [System.Convert]::ToBase64String($ba) 11 | $h = @{Authorization=("Basic{0}" -f $ba);ContentType="application/json"} 12 | return $h 13 | } 14 | 15 | function Set-ReleaseVariable 16 | { 17 | [CmdletBinding()] 18 | param( 19 | [Parameter(Mandatory)] 20 | [string] $teamFoundationServerUri, 21 | 22 | [Parameter(Mandatory)] 23 | [string] $teamProject, 24 | 25 | [Parameter(Mandatory)] 26 | [string] $releaseId, 27 | 28 | [Parameter(Mandatory)] 29 | [string] $accessToken, 30 | 31 | [Parameter(Mandatory)] 32 | [string] $name, 33 | 34 | [Parameter(Mandatory)] 35 | [string] $value 36 | ) 37 | 38 | $resourceApiUri = [IO.Path]::Combine($teamFoundationServerUri, $teamProject, "_apis", "Release", "releases", $releaseId) 39 | $resourceApiUri = "$($resourceApiUri)?api-version=4.1-preview.6" 40 | 41 | $h = BasicAuthHeader $accessToken 42 | 43 | $resource = Invoke-RestMethod -Uri $resourceApiUri -Headers $h -Method Get 44 | 45 | # If the variable does not exist, add it as empty first 46 | if($null -eq $resource.variables.$name) 47 | { 48 | $varObject = [PSCustomObject]@{value=""} 49 | $resource.variables | Add-Member -Value $varObject -Name $name -MemberType 'NoteProperty' 50 | } 51 | $resource.variables.$name.value = $value; 52 | 53 | # Update resource 54 | $updatedResource = $resource | ConvertTo-Json -Depth 10 -Compress 55 | $updatedResource = [Text.Encoding]::UTF8.GetBytes($updatedResource) 56 | 57 | Invoke-RestMethod -Uri $resourceApiUri -Method Put -Headers $h -ContentType "application/json" -Body $updatedResource | Out-Null 58 | 59 | Write-Verbose "Variable $name has been preserved with value $value" 60 | } 61 | 62 | function Set-BuildVariable 63 | { 64 | [CmdletBinding()] 65 | param( 66 | [Parameter(Mandatory)] 67 | [string] $teamFoundationCollectionUri, 68 | 69 | [Parameter(Mandatory)] 70 | [string] $teamProject, 71 | 72 | [Parameter(ParameterSetName="Build", Mandatory)] 73 | [string] $buildId, 74 | 75 | [Parameter(Mandatory)] 76 | [string] $accessToken, 77 | 78 | [Parameter(Mandatory)] 79 | [string] $name, 80 | 81 | [Parameter(Mandatory)] 82 | [string] $value 83 | ) 84 | 85 | $resourceApiUri = [IO.Path]::Combine($teamFoundationCollectionUri, $teamProject, "_apis", "build", "builds", $buildId) 86 | $resourceApiUri = "$($resourceApiUri)?api-version=4.1" 87 | 88 | $h = BasicAuthHeader $accessToken 89 | 90 | $resource = Invoke-RestMethod -Uri $resourceApiUri -Headers $h -Method Get 91 | $resourceParameters = $resource.parameters | ConvertFrom-Json 92 | 93 | # If the variable does not exist, add it as empty first 94 | if($null -eq $resourceParameters.$name) 95 | { 96 | $resourceParameters | Add-Member -Value "" -Name $name -MemberType 'NoteProperty' 97 | } 98 | $resourceParameters.$name = $value; 99 | 100 | # Update resource 101 | $resource.parameters = $resourceParameters | ConvertTo-Json -Depth 10 -Compress 102 | $updatedResource = $resource | ConvertTo-Json -Depth 10 -Compress 103 | $updatedResource = [Text.Encoding]::UTF8.GetBytes($updatedResource) 104 | 105 | Invoke-RestMethod -Uri $resourceApiUri -Method Patch -Headers $h -ContentType "application/json" -Body $updatedResource | Out-Null 106 | 107 | Write-Verbose "Variable $name has been preserved with value $value" 108 | } -------------------------------------------------------------------------------- /tasks/ps_common/storeBrokerHelper.psm1: -------------------------------------------------------------------------------- 1 | function Set-EndPointAuthentication 2 | { 3 | [CmdletBinding()] 4 | param( 5 | [Parameter(Mandatory)] 6 | [string] $TenantId, 7 | 8 | [Parameter(Mandatory)] 9 | [string] $ClientId, 10 | 11 | [Parameter(Mandatory)] 12 | [string] $ClientSecret 13 | ) 14 | 15 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 16 | $securityPassword = ConvertTo-SecureString $ClientSecret -AsPlainText -Force 17 | $myCreds = New-Object System.Management.Automation.PSCredential $ClientId, $securityPassword 18 | Set-StoreBrokerAuthentication -TenantId $TenantId -Credential $myCreds -Verbose:$useVerbose 19 | } 20 | 21 | function Set-ProxyAuthentication 22 | { 23 | [CmdletBinding()] 24 | param( 25 | [Parameter(ParameterSetName="TenantId", Mandatory)] 26 | [string] $TenantId, 27 | 28 | [Parameter(ParameterSetName="TenantName", Mandatory)] 29 | [string] $TenantName, 30 | 31 | [Parameter(Mandatory)] 32 | [string] $ProxyUrl 33 | ) 34 | 35 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 36 | if ($PSCmdlet.ParameterSetName -eq "TenantId") 37 | { 38 | Set-StoreBrokerAuthentication -TenantId $TenantId -UseProxy -ProxyEndpoint $ProxyUrl -Verbose:$useVerbose 39 | } 40 | else 41 | { 42 | Set-StoreBrokerAuthentication -TenantName $TenantName -UseProxy -ProxyEndpoint $ProxyUrl -Verbose:$useVerbose 43 | } 44 | } 45 | 46 | function Set-StoreBrokerSettings 47 | { 48 | [CmdletBinding()] 49 | param ( 50 | [string] $LogPath, 51 | 52 | [string] $NugetPath, 53 | 54 | [boolean] $DisableTelemetry 55 | ) 56 | 57 | $global:SBAlternateAssemblyDir = $NugetPath 58 | $global:SBWebRequestTimeoutSec = 300 59 | $global:SBStoreBrokerClientName = "Office_RDX_Windows_Store_Extension" 60 | $disablePiiProtection = $false 61 | $useInt = $false 62 | 63 | if (-not [string]::IsNullOrWhiteSpace($logPath)) 64 | { 65 | $global:SBLogPath = $LogPath 66 | } 67 | 68 | if($DisableTelemetry) 69 | { 70 | Write-Verbose "Disable Telemetry" 71 | $global:SBDisableTelemetry = $true 72 | } 73 | 74 | if([boolean]::TryParse($Env:disablePiiProtection, [ref]$disablePiiProtection) -and $disablePiiProtection) 75 | { 76 | Write-Verbose "Disable PII Protection" 77 | $global:SBDisablePiiProtection = $true 78 | } 79 | 80 | if (-not ([string]::IsNullOrWhiteSpace($Env:applicationInsightKey))) 81 | { 82 | Write-Verbose "Set StoreBroker Application Insight Key" 83 | $global:SBApplicationInsightsKey = $Env:applicationInsightKey 84 | } 85 | 86 | if([boolean]::TryParse($Env:useInt, [ref]$useInt) -and $useInt) 87 | { 88 | Write-Verbose "Use INT environment" 89 | $global:SBUseInt = $true 90 | } 91 | } 92 | 93 | function Set-StoreBrokerAuthCredentials 94 | { 95 | [CmdletBinding()] 96 | param ( 97 | [Parameter(Mandatory)] 98 | [PSCustomObject] $EndPointObj 99 | ) 100 | 101 | if ($EndPointObj.Auth.scheme -eq "UsernamePassword") 102 | { 103 | Set-EndPointAuthentication -TenantId $EndPointObj.Auth.parameters.tenantIdPassword -ClientId $EndPointObj.Auth.parameters.username -ClientSecret $EndPointObj.Auth.parameters.password 104 | } 105 | elseif ($EndPointObj.Auth.scheme -eq "None") 106 | { 107 | Set-ProxyAuthentication -TenantId $EndPointObj.Auth.parameters.tenantIdProxy -ProxyUrl $EndPointObj.Auth.parameters.proxyUrlTenantId 108 | } 109 | else 110 | { 111 | Set-ProxyAuthentication -TenantName $EndPointObj.Auth.parameters.apitoken -ProxyUrl $EndPointObj.Auth.parameters.proxyUrlTenantName 112 | } 113 | } -------------------------------------------------------------------------------- /docs/project_structure.md: -------------------------------------------------------------------------------- 1 | # Project structure 2 | 3 | This document should help you to understand how the extension is structured. 4 | However, don't rely on it too much, as things can change quickly. Remember that 5 | the code is the source of truth! 6 | 7 | ## Build system 8 | 9 | This project uses [gulp](http://gulpjs.com). The [gulpfile](../gulpfile.js) 10 | can provide some hints about how the build works, but here are some basic 11 | details: 12 | 13 | * ```package``` (also the default task), will build everything and create 14 | the extension VSIX package. If you want to run some tests on your own account, 15 | you can set the --publisher switch to whatever you need. 16 | * ```compile``` will simply take the tasks and common code, compile them and 17 | put them in their appropriate directories. This is useful if you want to do some 18 | "offline" tests, i.e. call your code directly without using the extension. 19 | * ```dependencies``` installs the node packages required by the tasks. This is 20 | ran by ```package```, but if you want to just ```compile``` you should do it 21 | separately. You should run it when you want to test code that relies on a 22 | package that you just added. 23 | * ```clean``` is the standard clean task. You should ideally run this before 24 | publishing a new version of the extension. The ```tfx``` client will take 25 | _everything_ in the build folder to make the .vsix file, so you want to avoid 26 | having garbage in there. 27 | 28 | ## Adding a task 29 | 30 | 1. Create a directory in the tasks sub-folder. 31 | 2. Add in it a task.json file. Create a new GUID to use as an ID for the task, 32 | and add that to task.json. In the ```execution.Node``` field, make sure that the 33 | ```target``` points to your task's main file **with a .js extension** and that 34 | it is under a **local/** directory. Note that the file *itself* should be 35 | directly under the task directory; the build system will move it to a 'local' 36 | subdirectory (consult the gulpfile for why this happens). 37 | 3. Add the task to the [extension manifest](../vss-extension.json). Make sure 38 | that its name is the same as the folder name. 39 | 40 | If you're confused, try copying an existing task. 41 | 42 | ## Structuring a task 43 | 44 | This is a guideline, and not required, but I found that separating code in at 45 | least two files helps with testing. One file is responsible for grabbing values 46 | from the task UI and validating them, and another is responsible for actually 47 | carrying out the task. That way it's possible to run the second file under 48 | node locally without having to publish the extension, which saves a lot of time. 49 | 50 | ## Adding new dependencies 51 | 52 | **Always ```--save``` any dependencies and typings you install. Otherwise others 53 | won't get them when they pull your code.** 54 | 55 | If you find a new package that you'd like to use as part of a task, you need to 56 | add it to the "master" [package.json](../package.json) file in the root of the 57 | project. When packaging, the build system will install the dependencies in the 58 | proper task folders. 59 | 60 | Note that if you're going to do a ```gulp compile``` after adding a dependency, 61 | you should run ```gulp dependencies``` as well (either after, before, or at 62 | the same time). 63 | 64 | When you add a new dependency, you should check if there are typings available 65 | for it. There are two scenarios: 66 | 67 | 1. The typings are part of the package (e.g. ```vsts-task-lib```). In this case 68 | you must also declare the package as a dev dependency, then install it. 69 | Afterwards, you can refer to the typings using a triple-slash reference to 70 | ```../../node_modules//index.d.ts``` (or whatever typings file you 71 | need). 72 | 2. The typings are not part of the package (e.g. ```q```). Try 73 | ```typings search ``` to see if any typings are available, and 74 | install them if so. 75 | 76 | ## Extending the build system 77 | 78 | If you'd like to add a gulp plugin to do something new with the build, make sure 79 | you install it as a dev dependency, and not a regular dependency. 80 | -------------------------------------------------------------------------------- /tasks/ps_common/commonHelper.psm1: -------------------------------------------------------------------------------- 1 | function Get-SubmissionPackages 2 | { 3 | [CmdletBinding()] 4 | param ( 5 | [Parameter(Mandatory)] 6 | [ValidateScript({if (Test-Path -Path $_ -PathType Container) { $true } else { throw "Source Folder '$_' cannot be found." }})] 7 | [string] $SourceFolder, 8 | 9 | [Parameter(Mandatory)] 10 | [string] $Contents 11 | ) 12 | 13 | # Normalize new line to LF 14 | $Contents = $Contents.Replace("`r`n","`n") 15 | $Contents = $Contents.Replace("`r","`n") 16 | $contentsList = $Contents.Split("`n") 17 | $applicationPackages = @() 18 | 19 | foreach($content in $contentsList) { 20 | $pkgFiles = Find-Match -DefaultRoot $SourceFolder -Pattern $content 21 | foreach ($pkg in $pkgFiles) { 22 | $pkgObj = @{ 23 | 'fileName' = $pkg.ToLower().Replace($SourceFolder.ToLower(), "") 24 | } 25 | $applicationPackages += $pkgObj 26 | } 27 | } 28 | 29 | return $applicationPackages 30 | } 31 | 32 | function Get-ProductIdAndFlightId 33 | { 34 | [CmdletBinding()] 35 | param( 36 | [Parameter(Mandatory)] 37 | [ValidateSet('AppName', 'AppId')] 38 | [string] $AppNameType, 39 | 40 | [string] $AppId, 41 | 42 | [string] $AppName, 43 | 44 | [Parameter(Mandatory)] 45 | [ValidateSet('Production', 'Flight', 'Sandbox')] 46 | [string] $ReleaseTrack, 47 | 48 | [ValidateSet('FlightName', 'FlightId', '')] 49 | [string] $FlightNameType, 50 | 51 | [string] $FlightId, 52 | 53 | [string] $FlightName, 54 | 55 | [string] $AccessToken 56 | ) 57 | 58 | if ($AppNameType -eq 'AppName') 59 | { 60 | Write-Verbose "Getting AppId for AppName: $AppName" 61 | $AppId = Get-AppIdFromAppName -AppName $AppName -AccessToken $AccessToken 62 | } 63 | 64 | Write-Verbose "App ID: $AppId" 65 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 66 | $productInfo = Get-Product -AppId $AppId -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 67 | if ($null -eq $productInfo) 68 | { 69 | throw "Cannot find product id based on app id $AppId" 70 | } 71 | 72 | $productId = $productInfo.id 73 | 74 | if ($ReleaseTrack -eq "Flight") 75 | { 76 | if ($FlightNameType -eq 'FlightName') 77 | { 78 | Write-Verbose "Getting FlightId for FlightName: $FlightName" 79 | $FlightId = Get-FlightIdFromFlightName -ProductId $productId -FlightName $FlightName -AccessToken $AccessToken 80 | } 81 | 82 | Write-Verbose "Flight ID: $FlightId" 83 | } 84 | 85 | return [PSCustomObject]@{ 'ProductId' = $productId;'FlightId' = $FlightId } 86 | } 87 | 88 | function Get-AppIdFromAppName 89 | { 90 | [CmdletBinding()] 91 | param ( 92 | [Parameter(Mandatory)] 93 | [string] $AppName, 94 | 95 | [string] $AccessToken 96 | ) 97 | Write-Verbose "App Name: $AppName" 98 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 99 | $productList = Get-Product -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 100 | $appFound = $false 101 | $appId = [string]::Empty 102 | foreach ($productInfo in $ProductList) 103 | { 104 | if ([string]::Equals($productInfo.name, $AppName, [stringcomparison]::OrdinalIgnoreCase)) 105 | { 106 | $appId = $productInfo.externalIds[0].value 107 | $appFound = $true 108 | break 109 | } 110 | } 111 | 112 | if (-not $appFound) 113 | { 114 | throw "Cannot find app $AppName" 115 | } 116 | 117 | return $appId 118 | } 119 | 120 | function Get-FlightIdFromFlightName 121 | { 122 | [CmdletBinding()] 123 | param ( 124 | [Parameter(Mandatory)] 125 | [string] $FlightName, 126 | 127 | [Parameter(Mandatory)] 128 | [string] $ProductId, 129 | 130 | [string] $AccessToken 131 | ) 132 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 133 | $flightList = Get-Flight -ProductId $ProductId -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 134 | $flightId = [string]::Empty 135 | $flightFound = $false 136 | foreach ($flightInfo in $flightList) 137 | { 138 | if ([string]::Equals($flightInfo.name, $FlightName, [stringcomparison]::OrdinalIgnoreCase)) 139 | { 140 | $flightId = $flightInfo.id 141 | $flightFound = $true 142 | break 143 | } 144 | } 145 | 146 | if (-not $flightFound) 147 | { 148 | throw "Cannot find flight $FlightName" 149 | } 150 | 151 | return $flightId 152 | } -------------------------------------------------------------------------------- /tasks/store-flight-V1/flightUi.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the Flight task. Gathers parameters and performs validation. 3 | */ 4 | 5 | import api = require('../common/apiHelper'); 6 | import inputHelper = require('../common/inputHelper'); 7 | import request = require('../common/requestHelper'); 8 | import fli = require('./flight'); 9 | 10 | import path = require('path'); 11 | 12 | import tl = require('azure-pipelines-task-lib'); 13 | 14 | /** Obtain and validate parameters from task UI. */ 15 | function gatherParams() 16 | { 17 | var credentials: request.Credentials; 18 | var endpointId = tl.getInput('serviceEndpoint', true); 19 | 20 | /* Contrary to the other tl.get* functions, the boolean param here 21 | indicates whether the parameter is __optional__ */ 22 | var endpointAuth = tl.getEndpointAuthorization(endpointId, false); 23 | credentials = 24 | { 25 | tenant : endpointAuth.parameters['tenantId'], 26 | clientId : endpointAuth.parameters['servicePrincipalId'], 27 | clientSecret : endpointAuth.parameters['servicePrincipalKey'] 28 | }; 29 | 30 | var endpointUrl: string = endpointAuth.parameters['url']; 31 | if (endpointUrl.lastIndexOf('/') == endpointUrl.length - 1) 32 | { 33 | endpointUrl = endpointUrl.substring(0, endpointUrl.length - 1); 34 | } 35 | 36 | var taskParams: fli.FlightParams = { 37 | appId: '', 38 | appName: '', 39 | flightName: tl.getInput('flightName', true), 40 | authentication: credentials, 41 | endpoint: endpointUrl, 42 | force: tl.getBoolInput('force', true), 43 | zipFilePath: path.join(tl.getVariable('Agent.WorkFolder'), 'temp.zip'), 44 | packages: [], 45 | skipPolling: tl.getBoolInput('skipPolling', true), 46 | numberOfPackagesToKeep: tl.getBoolInput('deletePackages') ? parseInt(tl.getInput('numberOfPackagesToKeep')) : null, 47 | mandatoryUpdateDifferHours: tl.getBoolInput('isMandatoryUpdate') ? parseInt(tl.getInput('mandatoryUpdateDifferHours')) : null 48 | }; 49 | 50 | // Packages 51 | var packages: string[] = []; 52 | if (inputHelper.inputFilePathSupplied('packagePath', false)) 53 | { 54 | packages = packages.concat(inputHelper.resolvePathPattern(tl.getInput('packagePath', false))); 55 | } 56 | var additionalPackages = tl.getDelimitedInput('additionalPackages', '\n', false); 57 | additionalPackages.forEach(packageInput => 58 | { 59 | packages = packages.concat(inputHelper.resolvePathPattern(packageInput)); 60 | } 61 | ) 62 | 63 | taskParams.packages = packages.map(p => p.trim()).filter(p => p.length != 0); 64 | 65 | if (taskParams.packages.length == 0) 66 | { 67 | throw new Error(`At least one package must be provided`); 68 | } 69 | 70 | // App identification 71 | var nameType = tl.getInput('nameType', true); 72 | if (nameType == 'AppId') 73 | { 74 | (taskParams).appId = tl.getInput('appId', true); 75 | } 76 | else if (nameType == 'AppName') 77 | { 78 | (taskParams).appName = tl.getInput('appName', true); 79 | } 80 | else 81 | { 82 | throw new Error(`Invalid name type ${nameType}`); 83 | } 84 | 85 | return taskParams; 86 | } 87 | 88 | function dumpParams(taskParams: fli.FlightParams): void 89 | { 90 | // We won't log the credentials, as they get masked by VSTS anyways. 91 | if (fli.hasAppId(taskParams)) 92 | { 93 | tl.debug(`App ID: ${taskParams.appId}`); 94 | } 95 | else 96 | { 97 | tl.debug(`App name: ${taskParams.appName}`); 98 | } 99 | 100 | tl.debug(`Flight name: ${taskParams.flightName}`); 101 | tl.debug(`Force delete: ${taskParams.force}`); 102 | tl.debug(`Packages: ${taskParams.packages.join(',')}`); 103 | tl.debug(`Local ZIP file path: ${taskParams.zipFilePath}`); 104 | tl.debug(`skipPolling: ${taskParams.skipPolling}`); 105 | tl.debug(`deletePackages: ${taskParams.numberOfPackagesToKeep ? true : false}`); 106 | tl.debug(`numberOfPackagesToKeep: ${taskParams.numberOfPackagesToKeep}`); 107 | tl.debug(`isMandatoryUpdate: ${taskParams.mandatoryUpdateDifferHours ? true : false}`); 108 | tl.debug(`mandatoryUpdateDifferHours: ${taskParams.mandatoryUpdateDifferHours}`); 109 | } 110 | 111 | async function main() 112 | { 113 | try 114 | { 115 | var taskParams: fli.FlightParams = gatherParams(); 116 | dumpParams(taskParams); 117 | await fli.flightTask(taskParams); 118 | } 119 | catch (err) 120 | { 121 | if (err.stack != undefined) 122 | { 123 | tl.error(err.stack); 124 | } 125 | 126 | // This will also log the error for us. 127 | tl.setResult(tl.TaskResult.Failed, err); 128 | } 129 | } 130 | 131 | main(); -------------------------------------------------------------------------------- /tasks/store-publish-V1/publishUi.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the Publish task. Gathers parameters and performs validation. 3 | */ 4 | 5 | import api = require('../common/apiHelper'); 6 | import inputHelper = require('../common/inputHelper'); 7 | import request = require('../common/requestHelper'); 8 | import pub = require('./publish'); 9 | 10 | import path = require('path'); 11 | 12 | import tl = require('azure-pipelines-task-lib'); 13 | 14 | /** Obtain and validate parameters from task UI. */ 15 | function gatherParams() 16 | { 17 | var credentials: request.Credentials; 18 | var endpointId = tl.getInput('serviceEndpoint', true); 19 | 20 | /* Contrary to the other tl.get* functions, the boolean param here 21 | indicates whether the parameter is __optional__ */ 22 | var endpointAuth = tl.getEndpointAuthorization(endpointId, false); 23 | credentials = 24 | { 25 | tenant : endpointAuth.parameters['tenantId'], 26 | clientId : endpointAuth.parameters['servicePrincipalId'], 27 | clientSecret : endpointAuth.parameters['servicePrincipalKey'] 28 | }; 29 | 30 | var endpointUrl: string = endpointAuth.parameters['url']; 31 | if (endpointUrl.lastIndexOf('/') == endpointUrl.length - 1) 32 | { 33 | endpointUrl = endpointUrl.substring(0, endpointUrl.length - 1); 34 | } 35 | 36 | var taskParams: pub.PublishParams = { 37 | appId : '', 38 | appName : '', 39 | authentication : credentials, 40 | endpoint : endpointUrl, 41 | force : tl.getBoolInput('force', true), 42 | metadataUpdateType: pub.MetadataUpdateType[tl.getInput('metadataUpdateMethod', true)], 43 | updateImages: tl.getBoolInput('updateImages', false), 44 | zipFilePath : path.join(tl.getVariable('Agent.WorkFolder'), 'temp.zip'), 45 | packages : [], 46 | skipPolling : tl.getBoolInput('skipPolling', true), 47 | numberOfPackagesToKeep: tl.getBoolInput('deletePackages') ? parseInt(tl.getInput('numberOfPackagesToKeep')) : null, 48 | mandatoryUpdateDifferHours: tl.getBoolInput('isMandatoryUpdate') ? parseInt(tl.getInput('mandatoryUpdateDifferHours')) : null 49 | }; 50 | 51 | // Packages 52 | var packages: string[] = []; 53 | if (inputHelper.inputFilePathSupplied('packagePath', false)) 54 | { 55 | packages = packages.concat(inputHelper.resolvePathPattern(tl.getInput('packagePath', false))); 56 | } 57 | var additionalPackages = tl.getDelimitedInput('additionalPackages', '\n', false); 58 | additionalPackages.forEach(packageInput => 59 | { 60 | packages = packages.concat(inputHelper.resolvePathPattern(packageInput)); 61 | } 62 | ) 63 | 64 | taskParams.packages = packages.map(p => p.trim()).filter(p => p.length != 0); 65 | 66 | // App identification 67 | var nameType = tl.getInput('nameType', true); 68 | if (nameType == 'AppId') 69 | { 70 | (taskParams).appId = tl.getInput('appId', true); 71 | } 72 | else if (nameType == 'AppName') 73 | { 74 | (taskParams).appName = tl.getInput('appName', true); 75 | } 76 | else 77 | { 78 | throw new Error(`Invalid name type ${nameType}`); 79 | } 80 | 81 | taskParams.metadataRoot = inputHelper.canonicalizePath(tl.getPathInput('metadataPath', false, taskParams.metadataUpdateType != pub.MetadataUpdateType.NoUpdate)); 82 | 83 | return taskParams; 84 | } 85 | 86 | function dumpParams(taskParams: pub.PublishParams): void 87 | { 88 | // We won't log the credentials, as they get masked by VSTS anyways. 89 | if (pub.hasAppId(taskParams)) 90 | { 91 | tl.debug(`App ID: ${taskParams.appId}`); 92 | } 93 | else 94 | { 95 | tl.debug(`App name: ${taskParams.appName}`); 96 | } 97 | 98 | tl.debug(`Endpoint: ${taskParams.endpoint}`); 99 | tl.debug(`Force delete: ${taskParams.force}`); 100 | tl.debug(`Metadata update type: ${taskParams.metadataUpdateType}`); 101 | tl.debug(`Update images: ${taskParams.updateImages}`); 102 | tl.debug(`Metadata root: ${taskParams.metadataRoot}`); 103 | tl.debug(`Packages: ${taskParams.packages.join(',')}`); 104 | tl.debug(`skipPolling: ${taskParams.skipPolling}`); 105 | tl.debug(`deletePackages: ${taskParams.numberOfPackagesToKeep ? true : false}`); 106 | tl.debug(`numberOfPackagesToKeep: ${taskParams.numberOfPackagesToKeep}`); 107 | tl.debug(`isMandatoryUpdate: ${taskParams.mandatoryUpdateDifferHours ? true : false}`); 108 | tl.debug(`mandatoryUpdateDifferHours: ${taskParams.mandatoryUpdateDifferHours}`); 109 | } 110 | 111 | async function main() 112 | { 113 | try 114 | { 115 | var taskParams: pub.PublishParams = gatherParams(); 116 | dumpParams(taskParams); 117 | await pub.publishTask(taskParams); 118 | } 119 | catch (err) 120 | { 121 | if (err.stack != undefined) 122 | { 123 | tl.error(err.stack); 124 | } 125 | 126 | // This will also log the error for us. 127 | tl.setResult(tl.TaskResult.Failed, err); 128 | } 129 | } 130 | 131 | main(); -------------------------------------------------------------------------------- /tasks/store-rollout-V3/rolloutUi.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param() 3 | 4 | Set-StrictMode -Version 2.0 5 | 6 | try 7 | { 8 | [boolean]$useVerbose = $false 9 | if ([boolean]::TryParse($Env:SYSTEM_DEBUG, [ref]$useVerbose) -and $useVerbose) 10 | { 11 | $VerbosePreference = 'Continue' 12 | } 13 | Write-Verbose "Verbose preference has been set based on the presence of release variable 'System.Debug'" 14 | 15 | # Const 16 | Set-Variable -Name "NugetPath" -Value "$PSScriptRoot\ps_modules\NugetPackages" -Option Constant -Scope Global -Force 17 | Set-Variable -Name "OpenSSLPath" -Value "$PSScriptRoot\ps_modules\openssl" -Option Constant -Scope Global -Force 18 | 19 | Write-Output "Loading dependencies" 20 | Import-Module "$PSScriptRoot\ps_common\commonHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 21 | Import-Module "$PSScriptRoot\ps_common\storeBrokerHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 22 | Import-Module "$PSScriptRoot\ps_modules\StoreBroker" 6>$null 5>$null 4>$null 3>$null 1>$null 23 | Import-Module "$PSScriptRoot\ps_modules\VstsTaskSdk\VstsTaskSdk.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 24 | Import-Module "$PSScriptRoot\ps_modules\AdoAzureHelper\AdoAzureHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 25 | Import-Module "$PSScriptRoot\rollout.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 26 | 27 | Write-Output "Loading task inputs" 28 | [string]$appId = Get-VstsInput -Name "appId" 29 | [string]$appName = Get-VstsInput -Name "appName" 30 | [string]$appNameType = Get-VstsInput -Name "appNameType" 31 | [string]$currentPackageVersionRegex = Get-VstsInput -Name "currentPackageVersionRegex" 32 | [boolean]$disableTelemetry = Get-VstsInput -Name "disableTelemetry" -AsBool 33 | [string]$endpointId = Get-VstsInput -Name "serviceEndpoint" 34 | [boolean]$failIfNoRollout = Get-VstsInput -Name "failIfNoRollout" -AsBool 35 | [string]$flightId = Get-VstsInput -Name "flightId" 36 | [string]$flightName = Get-VstsInput -Name "flightName" 37 | [string]$flightNameType = Get-VstsInput -Name "flightNameType" 38 | [string]$logPath = Get-VstsInput -Name 'logPath' 39 | [string]$releaseTrack = Get-VstsInput -Name "releaseTrack" 40 | [float]$rollout = Get-VstsInput -Name "rollout" 41 | [string]$rolloutAction = Get-VstsInput -Name "rolloutAction" 42 | [float]$rolloutActionThreshold = Get-VstsInput -Name "rolloutActionThreshold" 43 | [boolean]$skipIfNoMatch = Get-VstsInput -Name "skipIfNoMatch" -AsBool 44 | 45 | Write-Verbose "Endpoint: $endpointId" 46 | $endPointObj = Get-VstsEndpoint -Name $endpointId -Require 47 | 48 | Write-Output "Setting Store Broker environment" 49 | Set-StoreBrokerSettings -LogPath $logPath -NugetPath $NugetPath -DisableTelemetry $disableTelemetry -Verbose:$useVerbose 50 | 51 | # Getting AAD accessToken that would be later used by all the StoreBroker commands to access Partner Center APIs. 52 | Initialize-AdoAzureHelper -msalLibraryDir $NugetPath -adoApiLibraryDir $NugetPath -openSSLExeDir $OpenSSLPath 53 | $resource = "https://api.partner.microsoft.com" 54 | 55 | $sendX5C = $true 56 | $useMSAL = $true 57 | if (($endPointObj.Auth.Scheme -eq 'WorkloadIdentityFederation') -or ($endPointObj.Auth.Parameters.AuthenticationType -ne 'SPNCertificate')) 58 | { 59 | $sendX5C = $false 60 | } 61 | 62 | $aadAccessToken = (Get-AzureRMAccessToken $endPointObj $endpointId $resource $sendX5C $useMSAL).access_token 63 | 64 | $taskParams = @{ 65 | 'AppId' = $appId 66 | 'AppName' = $appName 67 | 'AppNameType' = $appNameType 68 | 'CurrentPackageVersionRegex' = $currentPackageVersionRegex 69 | 'FailIfNoRollout' = $failIfNoRollout 70 | 'FlightId' = $flightId 71 | 'FlightName' = $flightName 72 | 'FlightNameType' = $flightNameType 73 | 'ReleaseTrack' = $releaseTrack 74 | 'RolloutAction' = $rolloutAction 75 | 'RolloutActionThreshold' = $rolloutActionThreshold 76 | 'RolloutValue' = $rollout 77 | 'SkipIfNoMatch' = $skipIfNoMatch 78 | 'Verbose' = $useVerbose 79 | 'AccessToken' = $aadAccessToken 80 | } 81 | 82 | Write-Output "Updating rollout" 83 | 84 | $result = Update-ProductRollout @taskParams 85 | 86 | Write-Output "Finished updating rollout" 87 | 88 | Write-Output "$($result.Message)" 89 | Write-VstsSetResult -Result "Succeeded" -Message $result.Message 90 | 91 | Write-Verbose "Only set output variables if a change was actually made" 92 | if ($result.ShouldPublishValues) 93 | { 94 | Write-Output "Populating out variables for future tasks to consume" 95 | # The varaible names have to match those defined in the task.json 96 | Write-Output "##vso[task.setvariable variable=EXISTING_ROLLOUT_PACKAGE_VERSION]$($result.Version)" 97 | Write-Output "##vso[task.setvariable variable=UPDATED_ROLLOUT_VALUE]$($result.RolloutValue)" 98 | } 99 | else 100 | { 101 | Write-Output "The task has determined no change was made so the output variables won't be populated." 102 | } 103 | } 104 | catch 105 | { 106 | Write-Error "$($_ | Out-String)" 107 | } 108 | finally 109 | { 110 | if ((Test-Path variable:logPath) -and (Test-Path $logPath -PathType Leaf)) 111 | { 112 | Write-Output "Attaching Store Broker log file $logPath. You can download it alongside the agent logs." 113 | Write-Output "##vso[task.uploadfile]$logPath" 114 | } 115 | } -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Quick start 4 | 5 | 1. Ensure you meet the [prerequisites](#prerequisites). 6 | 7 | 2. [Install](https://marketplace.visualstudio.com/items?itemName=MS-RDX-MRO.windows-store-publish) the extension. 8 | 9 | 3. [Obtain](#obtaining-your-credentials) and [configure](#configuring-your-credentials) your Dev Center credentials. 10 | 11 | 4. [Add tasks](#task-reference) to your release definitions. 12 | 13 | ## Prerequisites 14 | 15 | 1. Determine which major version of the extension you should use. If you are Microsoft FTE using Azure DevOps pipeline within Microsoft please use v3. If you are outside of Microsoft please use v0. 16 | 17 | 2. You must have an Azure Active Directory, and you must have [global administrator permission](https://azure.microsoft.com/en-us/documentation/articles/active-directory-assign-admin-roles/) for the directory. You can create a new Azure AD [from Dev Center](https://docs.microsoft.com/en-us/windows/uwp/publish/associate-azure-ad-with-dev-center#create-a-brand-new-azure-ad-to-associate-with-your-partner-center-account). If you already use Office 365 or other business services from Microsoft, 18 | you already have an AAD. Otherwise, you can 19 | [create a new AAD in Partner Center](https://msdn.microsoft.com/windows/uwp/publish/manage-account-users) 20 | for no additional charge. 21 | 22 | 3. You must [associate your AAD with your Partner Center account](https://learn.microsoft.com/en-us/windows/apps/publish/partner-center/associate-existing-azure-ad-tenant-with-partner-center-account) obtain the credentials to allow this extension to access your account and perform actions on your behalf. 23 | 24 | 4. The app you want to publish must already exist: this extension can only publish updates to existing applications. You can [create your app in Partner Center](https://msdn.microsoft.com/windows/uwp/publish/create-your-app-by-reserving-a-name). 25 | 26 | 5. You must have already [created at least one submission](https://msdn.microsoft.com/windows/uwp/publish/app-submissions) for your app before you can use the Publish task provided by this extension. If you have not created a submission, the task will fail. 27 | 28 | 6. More information and extra prerequisites specific to the API can be found [here](https://msdn.microsoft.com/windows/uwp/monetize/create-and-manage-submissions-using-windows-store-services). 29 | 30 | ## Obtaining your credentials in v3.\* tasks (For Microsoft FTEs only) 31 | 32 | In V3 Tasks, we have added support for using [federated credentials](wifauth.md) or [certificate authentication](certificateauth.md) mechanism. You can also keep using the App secret authentication described below for v0.\* tasks. Users within Microsoft are required to use secret-less authentication method as part of our security requirement. 33 | 34 | ## Obtaining your credentials in v0.\* tasks (For users outside of Microsoft) 35 | 36 | ### Authentication in v0.\* tasks 37 | 38 | Your credentials are comprised of three parts: the Azure **Tenant ID**, the **Client ID** and the **Client secret**. After you [add](https://docs.microsoft.com/en-us/windows/uwp/publish/add-users-groups-and-azure-ad-applications#add-azure-ad-applications-from-your-organizations-directory) or [create](https://docs.microsoft.com/en-us/windows/uwp/publish/add-users-groups-and-azure-ad-applications#create-a-new-azure-ad-application-account-in-your-organizations-directory-and-add-it-to-your-partner-center-account) an Azure AD application, you can return to the Users section and select the application name to review settings for the application, including the Tenant ID, Client ID, Reply URL, and App ID URI. 39 | 40 | ### Configuring your credentials in v0\* tasks 41 | 42 | Once you have obtained your credentials, you must configure them in VSTS so that the extension can access your Dev Center account and publish on your behalf. You must [install the extension](https://docs.microsoft.com/en-us/azure/devops/marketplace/install-vsts-extension?view=vsts) before being able to configure your credentials. Once the extension is installed, follow these steps: 43 | 44 | 1. In Azure DevOps, open the **Service connections** page from the [project settings](https://docs.microsoft.com/en-us/azure/devops/project/navigation/go-to-service-page?view=vsts#open-project-settings) page. In TFS, open the **Services** page from the "settings" icon in the top menu bar. 45 | 46 | 2. Choose **+ New service connection** and select the type of service connection you need. Two types of Service connections are offered with this extension. *Windows Dev Center*. This is required for tasks version *v0.\** and is included for backward compatibility. The rest of the article will focus on this service endpoint. For information on the previous version, see [this tag](https://github.com/Microsoft/windows-dev-center-vsts-extension/tree/v0.9.26#configuring-your-credentials). 47 | 48 | 3. In the pop-up box, there are three options to connect to the Windows Store, *Azure Client ID and Secret*, *Proxy - Tenant Id* and *Proxy - Tenant Name*. 49 | 50 | * **Azure Client ID and Secret authentication** 51 | 52 | Select the *Azure Client ID and Secret* option. Fill in your credentials in the corresponding text boxes (**Azure Tenant ID**, **Client ID** and **Client Secret**). 53 | 54 | ![Screenshot of the "Add new Windows Dev Center service connection (Azure Client ID and Secret authentication)" dialog](./new_endpoint_clientid.png) 55 | 56 | 4. Click **OK** to confirm. Your endpoint is now configured and will be accessible by the extension's tasks. 57 | 58 | See more information on adding Service connections on Azure DevOps [here](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=vsts). 59 | 60 | Now that you have set up your extension, you can start using it in your build and release pipelines. See the [Usage](./usage.md) section for more information. -------------------------------------------------------------------------------- /tasks/store-package-V3/packageUI.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param() 3 | 4 | Set-StrictMode -Version 2.0 5 | 6 | try 7 | { 8 | [boolean]$useVerbose = $false 9 | if ([boolean]::TryParse($Env:SYSTEM_DEBUG, [ref]$useVerbose) -and $useVerbose) 10 | { 11 | $VerbosePreference = 'Continue' 12 | } 13 | Write-Verbose "Verbose preference has been set based on the presence of release variable 'System.Debug'" 14 | 15 | # Const 16 | Set-Variable -Name "NugetPath" -Value "$PSScriptRoot\ps_modules\NugetPackages" -Option Constant -Scope Global -Force 17 | Set-Variable -Name "OpenSSLPath" -Value "$PSScriptRoot\ps_modules\openssl" -Option Constant -Scope Global -Force 18 | 19 | Write-Output "Loading dependencies" 20 | Import-Module "$PSScriptRoot\ps_common\commonHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 21 | Import-Module "$PSScriptRoot\ps_common\storeBrokerHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 22 | Import-Module "$PSScriptRoot\ps_modules\StoreBroker" 6>$null 5>$null 4>$null 3>$null 1>$null 23 | Import-Module "$PSScriptRoot\ps_modules\VstsTaskSdk\VstsTaskSdk.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 24 | Import-Module "$PSScriptRoot\ps_modules\AdoAzureHelper\AdoAzureHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 25 | 26 | Write-Output "Loading task inputs" 27 | [string]$appId = Get-VstsInput -Name "appId" 28 | [string]$appName = Get-VstsInput -Name "appName" 29 | [string]$appNameType = Get-VstsInput -Name "appNameType" 30 | [string]$contents = Get-VstsInput -Name "contents" 31 | [boolean]$disableTelemetry = Get-VstsInput -Name "disableTelemetry" -AsBool 32 | [string]$endpointId = Get-VstsInput -Name "serviceEndpoint" 33 | [string]$languageExclude = Get-VstsInput -Name "languageExclude" 34 | [string]$logPath = Get-VstsInput -Name 'logPath' 35 | [string]$outSBName = Get-VstsInput -Name "outSBName" 36 | [string]$outSBPackagePath = Get-VstsInput -Name "outSBPackagePath" 37 | [string]$pdpInclude = Get-VstsInput -Name "pdpInclude" 38 | [string]$pdpMediaPath = Get-VstsInput -Name "pdpMediaPath" 39 | [string]$pdpPath = Get-VstsInput -Name "pdpPath" 40 | [string]$sbConfigPath = Get-VstsInput -Name "sbConfigPath" 41 | [string]$sourceFolder = Get-VstsInput -Name "sourceFolder" 42 | 43 | $endPointObj = Get-VstsEndpoint -Name $endpointId -Require 44 | 45 | Write-Output "Setting Store Broker environment" 46 | Set-StoreBrokerSettings -LogPath $logPath -NugetPath $NugetPath -DisableTelemetry $disableTelemetry -Verbose:$useVerbose 47 | 48 | # Getting AAD accessToken that would be later used by all the StoreBroker commands to access Partner Center APIs. 49 | Initialize-AdoAzureHelper -msalLibraryDir $NugetPath -adoApiLibraryDir $NugetPath -openSSLExeDir $OpenSSLPath 50 | $resource = "https://api.partner.microsoft.com" 51 | 52 | $sendX5C = $true 53 | $useMSAL = $true 54 | if (($endPointObj.Auth.Scheme -eq 'WorkloadIdentityFederation') -or ($endPointObj.Auth.Parameters.AuthenticationType -ne 'SPNCertificate')) 55 | { 56 | $sendX5C = $false 57 | } 58 | 59 | $aadAccessToken = (Get-AzureRMAccessToken $endPointObj $endpointId $resource $sendX5C $useMSAL).access_token 60 | 61 | Write-Output "Creating new Submission package in $outSBPackagePath with common name $outSBName" 62 | 63 | if ($appNameType -eq 'AppName') 64 | { 65 | Write-Verbose "Getting AppId for AppName: $appName" 66 | $appId = Get-AppIdFromAppName -AppName $appName -AccessToken $aadAccessToken 67 | } 68 | 69 | $pdpInclude = $pdpInclude.Replace('`r`n', '`n').Replace('`r', '`n').Split('`n', [System.StringSplitOptions]::RemoveEmptyEntries) 70 | $languageExclude = $languageExclude.Replace('`r`n', '`n').Replace('`r', '`n').Split('`n', [System.StringSplitOptions]::RemoveEmptyEntries) 71 | 72 | $packages = @() 73 | if (-not ([string]::IsNullOrWhiteSpace($sourceFolder))) 74 | { 75 | Write-Output "Source Folder of packages: $sourceFolder" 76 | 77 | # Normalize new line to LF 78 | $contents = $contents.Replace("`r`n","`n") 79 | $contents = $contents.Replace("`r","`n") 80 | $contentsList = $contents.Split("`n") 81 | 82 | foreach($content in $contentsList) { 83 | $pkgFiles = Find-Match -DefaultRoot $sourceFolder -Pattern $content 84 | foreach ($pkg in $pkgFiles) { 85 | $packages += $pkg 86 | } 87 | } 88 | 89 | Write-Output "Package paths after parsing mini-match pattern:" 90 | Write-Output $packages 91 | } 92 | 93 | Write-Output "PDP Root path: $pdpPath" 94 | if ([string]::IsNullOrWhiteSpace($sbConfigPath)) 95 | { 96 | Write-Output "No config file was provided, creating new one" 97 | $null = New-Item -ItemType Directory -Path $outSBPackagePath -Force 98 | $sbConfigPath = [IO.Path]::Combine($outSBPackagePath, 'SBConfig.json') 99 | $null = New-StoreBrokerConfigFile -Path $sbConfigPath -AppId $appId -Verbose:$useVerbose 100 | } 101 | 102 | $newSubmissionPackageParams = @{ 103 | 'ConfigPath' = $sbConfigPath 104 | 'PackagePath' = $packages 105 | 'PDPInclude' = $pdpInclude 106 | 'LanguageExclude' = $languageExclude 107 | 'OutPath' = $outSBPackagePath 108 | 'OutName' = $outSBName 109 | 'Verbose' = $useVerbose 110 | 'AccessToken' = $aadAccessToken 111 | } 112 | 113 | if (-not [string]::IsNullOrWhiteSpace($pdpMediaPath)) 114 | { 115 | $newSubmissionPackageParams['MediaRootPath'] = $pdpMediaPath 116 | } 117 | 118 | if (-not [string]::IsNullOrWhiteSpace($pdpPath)) 119 | { 120 | $newSubmissionPackageParams['PDPRootPath'] = $pdpPath 121 | } 122 | 123 | Write-Output "Calling Store Broker to create a new submission Package" 124 | $null = New-SubmissionPackage @newSubmissionPackageParams 125 | Write-Output "Finished creating new submission pacakge" 126 | Write-VstsSetResult -Result "Succeeded" -Message "Finished creating new submission pacakge" 127 | } 128 | catch 129 | { 130 | Write-Error "$($_ | Out-String)" 131 | } 132 | finally 133 | { 134 | if ((Test-Path variable:logPath) -and (Test-Path $logPath -PathType Leaf)) 135 | { 136 | Write-Output "Attaching Store Broker log file $logPath. You can download it alongside the agent logs." 137 | Write-Output "##vso[task.uploadfile]$logPath" 138 | } 139 | } -------------------------------------------------------------------------------- /tasks/store-flight-V1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "13dee6a7-3698-4b12-bbb4-b393560a3ebc", 3 | "name": "store-flight", 4 | "friendlyName": "Windows Store - Flight V1", 5 | "description": "Make a flight submission to the Windows Store", 6 | "author": "Microsoft Corporation", 7 | "category": "Deploy", 8 | "visibility": [ 9 | "Build", 10 | "Release" 11 | ], 12 | "demands": [ 13 | "node.js" 14 | ], 15 | "version": { 16 | "Major": "0", 17 | "Minor": "2", 18 | "Patch": "38" 19 | }, 20 | "minimumAgentVersion": "2.144.0", 21 | "instanceNameFormat": "Flight $(packagePath) to $(flightName)", 22 | "groups": [ 23 | { 24 | "name": "advanced", 25 | "displayName": "Advanced Options", 26 | "isExpanded": false 27 | } 28 | ], 29 | "inputs": [ 30 | { 31 | "name": "serviceEndpoint", 32 | "type": "connectedService:devCenter", 33 | "label": "Service endpoint", 34 | "defaultValue": "", 35 | "required": true, 36 | "helpMarkDown": "Windows Developer Center endpoint configured with your credentials. For V0 task the service connection must be of type 'Windows Dev Center V1'" 37 | }, 38 | { 39 | "name": "nameType", 40 | "type": "pickList", 41 | "label": "App identification method", 42 | "helpMarkDown": "How to determine the app to publish", 43 | "defaultValue": "AppId", 44 | "options": { 45 | "AppId": "ID", 46 | "AppName": "Primary name" 47 | } 48 | }, 49 | { 50 | "name": "appId", 51 | "type": "string", 52 | "label": "Application ID", 53 | "defaultValue": "", 54 | "required": true, 55 | "helpMarkDown": "ID of the application, found in the URL for the application's page on the Dev Center", 56 | "visibleRule": "nameType = AppId" 57 | }, 58 | { 59 | "name": "appName", 60 | "type": "string", 61 | "label": "Application primary name", 62 | "defaultValue": "", 63 | "required": true, 64 | "helpMarkDown": "Primary name of the application, found at the top of the application's page on the Dev Center", 65 | "visibleRule": "nameType = AppName" 66 | }, 67 | { 68 | "name": "flightName", 69 | "type": "string", 70 | "label": "Flight name", 71 | "defaultValue": "", 72 | "required": true, 73 | "helpMarkDown": "Name of the flight as seen on the Dev Center" 74 | }, 75 | { 76 | "name": "packagePath", 77 | "type": "filePath", 78 | "label": "Package file", 79 | "defaultValue": "", 80 | "required": false, 81 | "helpMarkDown": "Path to the application's package you want to flight. Minimatch pattern is supported." 82 | }, 83 | { 84 | "name": "force", 85 | "type": "boolean", 86 | "label": "Delete pending submissions", 87 | "defaultValue": false, 88 | "required": true, 89 | "helpMarkDown": "Whether to delete an existing submission instead of failing the task" 90 | }, 91 | { 92 | "name": "additionalPackages", 93 | "type": "multiLine", 94 | "label": "Additional package(s)", 95 | "groupName": "advanced", 96 | "required": false, 97 | "helpMarkDown": "Paths to any additional packages required by this application (one path per line). Minimatch pattern is supported." 98 | }, 99 | { 100 | "name": "skipPolling", 101 | "type": "boolean", 102 | "label": "Skip polling", 103 | "defaultValue": false, 104 | "groupName": "advanced", 105 | "required": true, 106 | "helpMarkDown": "Skip polling submission after committing it to Dev Center. **Warning**: if you check this box, you will not see errors, if any, that your submission may run into. You will have to manually check the status of your submission in Dev Center." 107 | }, 108 | { 109 | "name": "deletePackages", 110 | "type": "boolean", 111 | "label": "Delete Packages", 112 | "defaultValue": false, 113 | "groupName": "advanced", 114 | "required": false, 115 | "helpMarkDown": "Delete old unnecessary packages from the flight group you are submitting to." 116 | }, 117 | { 118 | "name": "numberOfPackagesToKeep", 119 | "type": "pickList", 120 | "label": "Number of Packages to keep", 121 | "defaultValue": "25", 122 | "groupName": "advanced", 123 | "required": false, 124 | "helpMarkDown": "Number of packages to keep per unique Target Device Family and Target Platform.", 125 | "options": { 126 | "0": "None", 127 | "1": "1", 128 | "2": "2", 129 | "3": "3", 130 | "4": "4", 131 | "5": "5" 132 | }, 133 | "properties": { 134 | "EditableOptions": "True" 135 | }, 136 | "visibleRule": "deletePackages = true" 137 | }, 138 | { 139 | "name": "isMandatoryUpdate", 140 | "type": "boolean", 141 | "label": "Mandatory Update", 142 | "defaultValue": false, 143 | "groupName": "advanced", 144 | "required": false, 145 | "helpMarkDown": "Indicates whether you want to treat the packages in this submission as mandatory for self-installing app updates." 146 | }, 147 | { 148 | "name": "mandatoryUpdateDifferHours", 149 | "type": "pickList", 150 | "label": "The number of hours until packages become mandatory", 151 | "defaultValue": "2", 152 | "groupName": "advanced", 153 | "required": false, 154 | "helpMarkDown": "The effective time when the packages in this submission become mandatory, in hours from submission time.", 155 | "options": { 156 | "2": "2", 157 | "6": "6", 158 | "12": "12", 159 | "24": "24", 160 | "48": "48" 161 | }, 162 | "properties": { 163 | "EditableOptions": "True" 164 | }, 165 | "visibleRule": "isMandatoryUpdate = true" 166 | } 167 | ], 168 | "execution": { 169 | "Node": { 170 | "target": "local/flightUi.js", 171 | "argumentFormat": "" 172 | }, 173 | "Node10": { 174 | "target": "local/flightUi.js", 175 | "argumentFormat": "" 176 | }, 177 | "Node16": { 178 | "target": "local/flightUi.js", 179 | "argumentFormat": "" 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docs/certificateauth.md: -------------------------------------------------------------------------------- 1 | # Using certificate to authenticate to Windows Store Partner Center 2 | 3 | ## Prerequisites 4 | You must have an Azure Active Directory (AAD) and owner access of it. See [setup.md](setup.md) for details. You also need to have access to a SAW (secure admin workstation) machine. See [New SAW Guide](https://microsoft.sharepoint.com/sites/Security_Tools_Services/SitePages/SAS/SAW%20KB/New-SAW-Guide.aspx?xsdata=MDV8MDJ8fGFmODE0MjZkMzUwMjRmYTk2ZWI5MDhkYzNmOTY3MWU0fDcyZjk4OGJmODZmMTQxYWY5MWFiMmQ3Y2QwMTFkYjQ3fDB8MHw2Mzg0NTUxNjI1NjgyMDgwNTd8VW5rbm93bnxWR1ZoYlhOVFpXTjFjbWwwZVZObGNuWnBZMlY4ZXlKV0lqb2lNQzR3TGpBd01EQWlMQ0pRSWpvaVYybHVNeklpTENKQlRpSTZJazkwYUdWeUlpd2lWMVFpT2pFeGZRPT18MXxMMk5vWVhSekx6RTVPamhqT1RVMFltSTRMV0V5TlRFdE5EY3dNUzA0TXpBeExXSXdNMlV6TlRVM1kyWXpObDg1Wm1ZMU1qWXpOUzA1TkRKbUxUUm1aamd0T1dWaU55MWhNVE5rT0Raa01UUTNaRFZBZFc1eExtZGliQzV6Y0dGalpYTXZiV1Z6YzJGblpYTXZNVGN3T1RreE9UUTFOVGcwTVE9PXxhYTVlZTYyMTY4NTY0Y2NjNmViOTA4ZGMzZjk2NzFlNHxiYjdmMzgyZDc4MDk0OWRlOTRhNWZjZWU1YzFmZDZjMg%3D%3D&sdata=YUltMm5HdGhsSFRjZmloTlIxaElqUmsyNStxQ052UG1razJadlQ3SFpCRT0%3D&ovuser=72f988bf-86f1-41af-91ab-2d7cd011db47%2Cmaarisme%40microsoft.com&OR=Teams-HL&CT=1711074123346&clickparams=eyJBcHBOYW1lIjoiVGVhbXMtRGVza3RvcCIsIkFwcFZlcnNpb24iOiI0OS8yNDAzMTQxNDcwNiIsIkhhc0ZlZGVyYXRlZFVzZXIiOmZhbHNlfQ%3D%3D) and [First Time User Login Guide](https://strikecommunity.azurewebsites.net/articles/6706/first-time-user-login-guide.html). 5 | 6 | ## Step 1: Register a domain in OneCert for your AAD application 7 | 8 | 1. Using credentials from the AAD tenant in which the AAD Application resides, go to aka.ms/onecert (on a SAW) 9 | 2. Domain Registrations -> Register New Domain 10 | 3. Fill out the form as follows: 11 | 1. **Domain Name** - This should be the Guid representing the authentication app that you want certificates to authenticate against. You can re-use the app from v1 or v2. 12 | 2. **Cloud** - Public 13 | 3. **Environment** - Select the environment of your Azure Subscription Id. 14 | 4. **Service Tree Id**: This should be the Guid of the service tree associated with the service. 15 | 5. **Issuer (v1)**: None 16 | 6. **Public Issuer (v2)**: None 17 | 7. **Private Issuer (v2)**: AME 18 | 8. **Subscription Id**: This should be the ID of the Azure Subscription(s) where the KeyVault (s) being used to host the certificates will reside. You can have multiple subscriptions listed here if you plan on having multiple KeyVaults. 19 | 9. **Cloud Settings**: None 20 | 10. **Owners**: The aliases of appropriate owners for the domain registration. Note: You must use an aliases from production, such as your Torus account. e.g. prdtrs01\richwhi_debug 21 | 4. Click Create Domain Registration 22 | 23 | ## Step 2: Create a KeyVault (for generating & renewing the certificate) 24 | Please ensure that the Azure subscription matches the Domain Registration subscription ID above if one does not already exist. 25 | 26 | ## Step 3: Generate a certificate 27 | 1. Navigate to the KeyVault and click on Certificates menu 28 | 2. Click on Certificate Authorities 29 | 3. Click Add to create a Certificate Authority 30 | 4. Give it a name (e.g., OneCertPrivate), choose OneCertV2-PrivateCA as the Provider, and click Create 31 | 5. Return to the Certificates menu and click Generate/Import 32 | 6. Fill out the form as follows: 33 | 1. Certificate Name: Give the certificate a meaningful friendly name 34 | 2. Type of Certificate Authority (CA): Choose Certificate issued by an integrated CA 35 | 3. Certificate Authority (CA): Select the Certificate Authority created in previous step 36 | 4. Subject: CN=, e.g. CN=b883a6b6-f875-43c2-bf69-ce20f6bc2a44 37 | 5. Content Type: Select pem, so later on you can download the pem content and add the pem content to the service connection. 38 | 6. Lifetime Action Type: Ensure it's set to renew at a percentage lifetime 39 | 7. Percentage Liftetime: Set to 24 40 | 8. Click Create
41 | 42 | ![alt text](certificate.png) 43 | 44 | ## Step 4: Configure the AAD application to accept appropriate certificates 45 | 1. Navigate to the AAD App Registration page of the AAD Application you are configuring 46 | 2. First, ensure that there are no existing certificates configured for the AAD application. Remove any Subject Name and Issuer (SNI) based certificates. 47 | 3. Next, click on the Manifest menu 48 | 4. Add a collection of trustedCertificateSubjects to the end of the JSON content displayed on screen (replace the Guid in the subjectName with the Guid of the AAD application). The authorityId is for AME private issuer, which was specified in step 1. 49 | 50 | ![alt text](trustedcertificatesubject.png) 51 | 52 | 5. Click Save (Refresh the page to ensure that the change is now reflected in the Manifest) 53 | 54 | ## Step 5: Create a new service connection using certificate 55 | 1. Go to project settings -> Service Connection click on “New Service Connection”. Select Azure Resource Manager, and then select Service Principal (Manual) for authentication method. 56 | 2. Fill in the service principal ID and tenant ID with the client ID and tenant of your Azure AD application. 57 | 3. For credential, click on “Certificate”. You need to get the content of the .pem file from the certificate you created in previous stage. Simply go to your keyvault and download the certificate there. Make sure to click on “Download in PFX/PEM format”. 58 | 4. If you have an Azure subscription in the same tenant as your service principal, then you can fill in the subscription ID and subscription Name with those of your Azure subscription. It's not mandatory for you to provide the subscription ID and subscription name in order to run the Windows Store extension. You can simply provide any value for subscription ID and subscription name as shown in the screenshot above, and do "save without verification" to create the service connection. To save without doing any verification, you can click on the dropdown on the right of the button "verify and save". 59 | 60 | See more information on adding Service connections on Azure DevOps [here](https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=vsts). 61 | 62 | ## Step 6: Adding the Service Connection to your Pipeline 63 | Make sure you add the service connection to your extension task in your pipeline. If you are using classic release pipeline, you can add the service connection directly using the UI. If you are maintaining a YAML pipeline, you should add the service connection to the serviceEndpoint field under inputs. E.g. 64 | 65 | ``` 66 | - task: MS-RDX-MRO.windows-store-publish-dev.flight-task.store-flight@3 67 | displayName: 'Publish ' 68 | inputs: 69 | serviceEndpoint: 70 | appId: XXX 71 | flightNameType: FlightName 72 | flightName: XXX 73 | sourceFolder: XXX 74 | contents: XXX 75 | ``` 76 | 77 | ## Step 7: Using 1ES Secret Rotator Tool to update service connection when certificate is renewed 78 | Since the certificate will expire at a certain time, the service connection also needs to be updated. To do this automatically, it’s recommended for you to onboard your new AAD key vault certificate to the [1ES Secret Rotator Tool](https://eng.ms/docs/experiences-devices/opg/office-es365/idc-fundamentals-security/oe-secret-rotator/secret-rotator-tool). You can follow the instructions from the documentation to deploy the secret rotator tool, which is an Azure function that executes every 30 minutes, to any Azure Resource Group and let it automatically update the service connection whenever it sees an update from your key vault certificate. Please go through the linked documentation for details on how to onboard. 79 | 80 | Now that you have set up your extension, you can start using it in your build and release pipelines. See the [Usage](./usage.md) section for more information. 81 | -------------------------------------------------------------------------------- /tests/tests_v2/Publish.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module "$PSScriptRoot\..\lib\ps_modules\StoreBroker\StoreBroker" 2 | Import-Module "$PSScriptRoot\..\tasks\store-publish-V3\publish.psm1" 3 | 4 | if (-not (Get-Module -ListAvailable -Name SomeModule)) { 5 | Install-Module -Name PesterMatchHashtable 6 | } 7 | 8 | Set-Variable -Name "TestListingsPath" -Value "$PSScriptRoot\TestingMetadata" 9 | Set-Variable -Name "TestListingsEmptyPath" -Value "$PSScriptRoot\TestingMetadataEmpty" 10 | Set-Variable -Name "TestListingsMissingFiles" -Value "$PSScriptRoot\TestingMetadataMissingFiles" 11 | 12 | 13 | Describe "Update-MetadataListings" { 14 | It "Returns empty because the listing path cannot be found" { 15 | Update-MetadataListings -MetadataSource "" -UpdateImagesAndCaptions | Should BeNullOrEmpty 16 | } 17 | 18 | It "Returns an listing object, with listings for 'en-us', but there is no images" { 19 | $expectedOutput = @{ 20 | "en-us" = @{ 21 | baseListing = @{ 22 | RecommendedHardware = @() 23 | CopyrightAndTrademarkInfo = "(c) Microsoft Corporation" 24 | ReleaseNotes = "• Feature 1: This is the description of the feature.`r`n• Feature 2: This is the description of the feature.`r`n• Feature 3: This is the description of the feature.`r`n• Feature 4: This is the description of the feature.`r`n• Feature 5: This is the description of the feature.`r`n• Feature 6: This is the description of the feature.`r`n• Feature 7: This is the description of the feature." 25 | Description = "OHelloWorld Descriptions. This is a app used for testing the metadata updates." 26 | SupportContact = "" 27 | Features = @("Microsoft test only. Returns the test app version number. For testing only.") 28 | Keywords = @("appfortestingonly", "fortesting") 29 | LicenseTerms = "" 30 | WebsiteUrl = "" 31 | Title = "OHelloWorld Desktop" 32 | PrivacyPolicy = "" 33 | } 34 | } 35 | } 36 | 37 | Update-MetadataListings -MetadataSource $TestListingsPath | Should MatchHashtable $expectedOutput 38 | } 39 | 40 | It "Returns an listing object, with listings for 'en-us', with image metadata" { 41 | $expectedOutput = @{ 42 | "en-us" = @{ 43 | baseListing = @{ 44 | RecommendedHardware = @() 45 | CopyrightAndTrademarkInfo = "(c) Microsoft Corporation" 46 | ReleaseNotes = "• Feature 1: This is the description of the feature.`r`n• Feature 2: This is the description of the feature.`r`n• Feature 3: This is the description of the feature.`r`n• Feature 4: This is the description of the feature.`r`n• Feature 5: This is the description of the feature.`r`n• Feature 6: This is the description of the feature.`r`n• Feature 7: This is the description of the feature." 47 | Description = "OHelloWorld Descriptions. This is a app used for testing the metadata updates." 48 | SupportContact = "" 49 | Features = @("Microsoft test only. Returns the test app version number. For testing only.") 50 | Keywords = @("appfortestingonly", "fortesting") 51 | LicenseTerms = "" 52 | WebsiteUrl = "" 53 | Title = "OHelloWorld Desktop" 54 | PrivacyPolicy = "" 55 | images = @( 56 | @{ 57 | 'fileStatus' = 'PendingUpload' 58 | 'imageType' = 'Screenshot' 59 | 'description.115292150481501' = 'Test only' 60 | 'fileName' = "$TestListingsPath\Listings\en-us\baseListing\images\Screenshot\1152921504815018704.png" 61 | }, 62 | @{ 63 | 'fileStatus' = 'PendingUpload' 64 | 'imageType' = 'Screenshot' 65 | 'fileName' = "$TestListingsPath\Listings\en-us\baseListing\images\Screenshot\1231243252353.png" 66 | 'description.1231243252353' = 'Another image for test only' 67 | } 68 | ) 69 | } 70 | } 71 | } 72 | 73 | Update-MetadataListings -MetadataSource $TestListingsPath -UpdateImagesAndCaptions | Should MatchHashtable $expectedOutput 74 | } 75 | 76 | 77 | It "Returns an listing object, but it's empty because there is no metadata files. There is an empty object for image since UpdateImagesAndCaptions is specified" { 78 | $expectedOutput = @{ 79 | "en-us" = @{ 80 | baseListing = @{ 81 | images = @{} 82 | } 83 | } 84 | } 85 | 86 | Update-MetadataListings -MetadataSource $TestListingsEmptyPath -UpdateImagesAndCaptions | Should MatchHashtable $expectedOutput 87 | } 88 | 89 | It "Returns an listing object, but it's empty because there is no metadata files. No object for image since UpdateImagesAndCaptions is not specified" { 90 | $expectedOutput = @{ 91 | "en-us" = @{ 92 | baseListing = @{ 93 | } 94 | } 95 | } 96 | 97 | Update-MetadataListings -MetadataSource $TestListingsEmptyPath | Should MatchHashtable $expectedOutput 98 | } 99 | 100 | It "Returns an listing object, but there are some fields missing because their corresponding files are missing" { 101 | $expectedOutput = @{ 102 | "en-us" = @{ 103 | baseListing = @{ 104 | Description = "OHelloWorld Descriptions. This is a app used for testing the metadata updates." 105 | Features = @("Microsoft test only. Returns the test app version number. For testing only.") 106 | Keywords = @("appfortestingonly", "fortesting") 107 | Title = "OHelloWorld Desktop" 108 | } 109 | } 110 | } 111 | 112 | Update-MetadataListings -MetadataSource $TestListingsMissingFiles | Should MatchHashtable $expectedOutput 113 | } 114 | } 115 | 116 | Describe "Update-MetadataTrailers" { 117 | It "Returns an object, with trailers for 'en-us'" { 118 | $expectedOutput = @( 119 | @{ 120 | 'trailerAssets' = @{ 121 | 'en-us' = @{ 122 | 'imageList' = @( 123 | @{ 124 | 'fileName' = '\Trailers\trailer1.trailerAssets\en-us\images\trailer1thumbnail.png' 125 | 'description' = 'Trailer 1 screenshot description en-us' 126 | } 127 | ) 128 | 'title' = 'Trailer 1 Title en-us' 129 | } 130 | } 131 | 'videoFileName' = '\Trailers\trailer1.mp4' 132 | }, 133 | @{ 134 | 'trailerAssets' = @{ 135 | 'en-us' = @{ 136 | 'imageList' = @( 137 | @{ 138 | 'fileName' = '\Trailers\trailer2.trailerAssets\en-us\images\trailer2thumbnail.png' 139 | 'description' = 'Trailer 2 screenshot description en-us' 140 | } 141 | ) 142 | 'title' = 'Trailer 2 Title en-us' 143 | } 144 | } 145 | 'videoFileName' = '\Trailers\trailer2.mp4' 146 | } 147 | ) 148 | Update-MetadataTrailers -MetadataSource $TestListingsPath | Should MatchHashtable $expectedOutput 149 | } 150 | } -------------------------------------------------------------------------------- /tasks/store-publish-V1/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "8e70da9d-532d-4416-a07f-5ec10f84339f", 3 | "name": "store-publish", 4 | "friendlyName": "Windows Store - Publish V1", 5 | "description": "Publish your app to the Windows Store", 6 | "author": "Microsoft Corporation", 7 | "category": "Deploy", 8 | "visibility": [ 9 | "Build", 10 | "Release" 11 | ], 12 | "demands": [ 13 | "node.js" 14 | ], 15 | "version": { 16 | "Major": "0", 17 | "Minor": "10", 18 | "Patch": "43" 19 | }, 20 | "minimumAgentVersion": "2.144.0", 21 | "instanceNameFormat": "Publish $(packagePath)", 22 | "groups": [ 23 | { 24 | "name": "advanced", 25 | "displayName": "Advanced Options", 26 | "isExpanded": false 27 | } 28 | ], 29 | "inputs": [ 30 | { 31 | "name": "serviceEndpoint", 32 | "type": "connectedService:devCenter", 33 | "label": "Service endpoint", 34 | "defaultValue": "TO_BE_SET", 35 | "required": true, 36 | "helpMarkDown": "Windows Developer Center endpoint configured with your credentials. For V0 task the service connection must be of type 'Windows Dev Center V1'" 37 | }, 38 | { 39 | "name": "nameType", 40 | "type": "pickList", 41 | "label": "App identification method", 42 | "helpMarkDown": "How to determine the app to publish", 43 | "defaultValue": "AppId", 44 | "options": { 45 | "AppId": "ID", 46 | "AppName": "Primary name" 47 | } 48 | }, 49 | { 50 | "name": "appId", 51 | "type": "string", 52 | "label": "Application ID", 53 | "defaultValue": "", 54 | "required": true, 55 | "helpMarkDown": "ID of the application, found in the URL for the application's page on the Dev Center", 56 | "visibleRule": "nameType = AppId" 57 | }, 58 | { 59 | "name": "appName", 60 | "type": "string", 61 | "label": "Application primary name", 62 | "defaultValue": "", 63 | "required": true, 64 | "helpMarkDown": "Primary name of the application, found at the top of the application's page on the Dev Center", 65 | "visibleRule": "nameType = AppName" 66 | }, 67 | { 68 | "name": "metadataUpdateMethod", 69 | "type": "pickList", 70 | "label": "Metadata update method", 71 | "defaultValue": "NoUpdate", 72 | "options": { 73 | "NoUpdate": "No update", 74 | "JsonMetadata": "JSON-formatted metadata", 75 | "TextMetadata": "Text metadata" 76 | } 77 | }, 78 | { 79 | "name": "metadataPath", 80 | "type": "filePath", 81 | "label": "Metadata root folder", 82 | "defaultValue": "", 83 | "helpMarkDown": "Path to a directory containing the metadata. Consult the documentation for the expected format.", 84 | "required": true, 85 | "visibleRule": "metadataUpdateMethod != NoUpdate" 86 | }, 87 | { 88 | "name": "updateImages", 89 | "type": "boolean", 90 | "label": "Update images", 91 | "defaultValue": false, 92 | "helpMarkDown": "Whether images should also be updated. **Warning**: if you check this box, all the old images of the listings you provide will be deleted.", 93 | "required": true, 94 | "visibleRule": "metadataUpdateMethod != NoUpdate" 95 | }, 96 | { 97 | "name": "packagePath", 98 | "type": "filePath", 99 | "label": "Package file", 100 | "defaultValue": "", 101 | "required": false, 102 | "helpMarkDown": "Path to the application's package. Minimatch pattern is supported." 103 | }, 104 | { 105 | "name": "force", 106 | "type": "boolean", 107 | "label": "Delete pending submissions", 108 | "defaultValue": false, 109 | "required": true, 110 | "helpMarkDown": "Whether to delete an existing submission instead of failing the task" 111 | }, 112 | { 113 | "name": "additionalPackages", 114 | "type": "multiLine", 115 | "label": "Additional package(s)", 116 | "groupName": "advanced", 117 | "required": false, 118 | "helpMarkDown": "Paths to any additional packages required by this application (one path per line). Minimatch pattern is supported." 119 | }, 120 | { 121 | "name": "skipPolling", 122 | "type": "boolean", 123 | "label": "Skip polling", 124 | "defaultValue": false, 125 | "groupName": "advanced", 126 | "required": true, 127 | "helpMarkDown": "Skip polling submission after committing it to Dev Center. **Warning**: if you check this box, you will not see errors, if any, that your submission may run into. You will have to manually check status of your submission in Dev Center." 128 | }, 129 | { 130 | "name": "deletePackages", 131 | "type": "boolean", 132 | "label": "Delete Packages", 133 | "defaultValue": false, 134 | "groupName": "advanced", 135 | "required": false, 136 | "helpMarkDown": "Delete old unnecessary packages from the production group." 137 | }, 138 | { 139 | "name": "numberOfPackagesToKeep", 140 | "type": "pickList", 141 | "label": "Number of Packages to keep", 142 | "defaultValue": "25", 143 | "groupName": "advanced", 144 | "required": false, 145 | "helpMarkDown": "Number of packages to keep per unique Target Device Family and Target Platform.", 146 | "options": { 147 | "0": "None", 148 | "1": "1", 149 | "2": "2", 150 | "3": "3", 151 | "4": "4", 152 | "5": "5" 153 | }, 154 | "properties": { 155 | "EditableOptions": "True" 156 | }, 157 | "visibleRule": "deletePackages = true" 158 | }, 159 | { 160 | "name": "isMandatoryUpdate", 161 | "type": "boolean", 162 | "label": "Mandatory Update", 163 | "defaultValue": false, 164 | "groupName": "advanced", 165 | "required": false, 166 | "helpMarkDown": "Indicates whether you want to treat the packages in this submission as mandatory for self-installing app updates." 167 | }, 168 | { 169 | "name": "mandatoryUpdateDifferHours", 170 | "type": "pickList", 171 | "label": "The number of hours until packages become mandatory", 172 | "defaultValue": "2", 173 | "groupName": "advanced", 174 | "required": true, 175 | "helpMarkDown": "The effective time when the packages in this submission become mandatory, in hours from submission time.", 176 | "options": { 177 | "2": "2", 178 | "6": "6", 179 | "12": "12", 180 | "24": "24", 181 | "48": "48" 182 | }, 183 | "properties": { 184 | "EditableOptions": "True" 185 | }, 186 | "visibleRule": "isMandatoryUpdate = true" 187 | } 188 | ], 189 | "execution": { 190 | "Node": { 191 | "target": "local/publishUi.js", 192 | "argumentFormat": "" 193 | }, 194 | "Node10": { 195 | "target": "local/publishUi.js", 196 | "argumentFormat": "" 197 | }, 198 | "Node16": { 199 | "target": "local/publishUi.js", 200 | "argumentFormat": "" 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tasks/store-rollout-V3/rollout.psm1: -------------------------------------------------------------------------------- 1 | function Update-ProductRollout 2 | { 3 | [CmdletBinding()] 4 | param( 5 | [string] $AppNameType, 6 | 7 | [string] $AppId, 8 | 9 | [string] $AppName, 10 | 11 | [Parameter(Mandatory)] 12 | [ValidateSet('Production', 'Flight')] 13 | [string] $ReleaseTrack, 14 | 15 | [ValidateSet('FlightName', 'FlightId', '')] 16 | [string] $FlightNameType, 17 | 18 | [string] $FlightId, 19 | 20 | [string] $FlightName, 21 | 22 | [Parameter(Mandatory)] 23 | [ValidateSet('set', 'halt', 'finalize')] 24 | [string] $RolloutAction, 25 | 26 | [Parameter(Mandatory)] 27 | [ValidateRange(0.0, 100.0)] 28 | [float] $RolloutValue, 29 | 30 | [Parameter(Mandatory)] 31 | [ValidateRange(0.0, 100.0)] 32 | [float] $RolloutActionThreshold, 33 | 34 | [Parameter(Mandatory)] 35 | [string] $CurrentPackageVersionRegex, 36 | 37 | [string] $AccessToken, 38 | 39 | [switch] $FailIfNoRollout, 40 | 41 | [switch] $SkipIfNoMatch 42 | ) 43 | 44 | [boolean]$useVerbose = $($VerbosePreference -eq "Continue") 45 | $infoParams = @{ 46 | 'AppId' = $AppId 47 | 'AppName' = $AppName 48 | 'AppNameType' = $AppNameType 49 | 'FlightId' = $FlightId 50 | 'FlightName' = $FlightName 51 | 'FlightNameType' = $FlightNameType 52 | 'ReleaseTrack' = $ReleaseTrack 53 | 'AccessToken' = $AccessToken 54 | 'Verbose' = $useVerbose 55 | } 56 | $returnValues = Get-ProductIdAndFlightId @infoParams 57 | $productId = $returnValues.ProductId 58 | $FlightId = $returnValues.FlightId 59 | 60 | $params = @{ 61 | "ProductId" = $productId 62 | "AccessToken" = $AccessToken 63 | "Verbose" = $useVerbose 64 | "NoStatus" = $true 65 | } 66 | 67 | if (-not [string]::IsNullOrWhiteSpace($FlightId)) 68 | { 69 | $params['FlightId'] = $FlightId 70 | } 71 | 72 | $existingSubmission = Get-Submission @params 73 | if ($null -eq $existingSubmission) 74 | { 75 | throw "Get-Submission returned 0 submission. To modify a rollout, a submission must first exist." 76 | } 77 | 78 | $version = "0.0" 79 | if ($existingSubmission.GetType().Name -eq 'Object[]') 80 | { 81 | $result = if ($FailIfNoRollout) {"Failed"} else {"Succeeded"} 82 | foreach ($submission in $existingSubmission) 83 | { 84 | if ($submission.state -ne 'Published') 85 | { 86 | $SubmissionId = $submission.id 87 | break 88 | } 89 | } 90 | 91 | $message = "Setting task status as $result since Get-Submission returns multiple submissions, and there is an unpublished submission $SubmissionId, and fail if no rollout option is set to $FailIfNoRollout" 92 | if ($FailIfNoRollout) 93 | { 94 | throw $message 95 | } 96 | return [PSCustomObject]@{ Message = $message; RolloutValue = $RolloutValue; Version = $version; ShouldPublishValues = $false } 97 | } 98 | 99 | $SubmissionId = $existingSubmission.id 100 | 101 | if (-not ($existingSubmission.resourceType -eq 'Submission' -and $existingSubmission.state -eq 'Published')) 102 | { 103 | $result = if ($FailIfNoRollout) {"Failed"} else {"Succeeded"} 104 | $message = "Setting task status as $result since the only existing submission $SubmissionId is unpublished and fail if no rollout option is set to $FailIfNoRollout" 105 | 106 | if ($FailIfNoRollout) 107 | { 108 | throw $message 109 | } 110 | return [PSCustomObject]@{ Message = $message; RolloutValue = $RolloutValue; Version = $version; ShouldPublishValues = $false } 111 | } 112 | 113 | $existingRollout = Get-SubmissionRollout -ProductId $productId -SubmissionId $SubmissionId -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 114 | 115 | if (($null -eq $existingRollout) -or (-not $existingRollout.isEnabled) -or ($existingRollout.state -ne "Initialized")) 116 | { 117 | $result = if ($FailIfNoRollout) {"Failed"} else {"Succeeded"} 118 | $message = "Setting task status as $result since no rollout is found and fail if no rollout option is set to $FailIfNoRollout" 119 | if ($FailIfNoRollout) 120 | { 121 | throw $message 122 | } 123 | return [PSCustomObject]@{ Message = $message; RolloutValue = $RolloutValue; Version = $version; ShouldPublishValues = $false } 124 | } 125 | 126 | Write-Verbose "Existing rollout detected. Validating current version against packages in rollout..." 127 | $productPackages = Get-ProductPackage -ProductId $productId -SubmissionId $SubmissionId -AccessToken $AccessToken -Verbose:$useVerbose 128 | if ($productPackages.Count -eq 0) 129 | { 130 | throw "Get-ProductPackage returned 0 package" 131 | } 132 | # Based on the regex provided by the user, we validate all packages are from the same Build output 133 | $isCurrentVersionInRollout = $true 134 | foreach ($pkg in $productPackages) 135 | { 136 | if ($pkg.version -notmatch $CurrentPackageVersionRegex) 137 | { 138 | $isCurrentVersionInRollout = $false 139 | $version = $pkg.version 140 | break 141 | } 142 | } 143 | 144 | if (-not $isCurrentVersionInRollout) 145 | { 146 | $result = if (-not $SkipIfNoMatch) {"Failed"} else {"Succeeded"} 147 | $message = "Setting task status as $result since at least one package does not match the regex $CurrentPackageVersionRegex and skip if no match option is set to $SkipIfNoMatch" 148 | if (-not $SkipIfNoMatch) 149 | { 150 | throw $message 151 | } 152 | return [PSCustomObject]@{ Message = $message; RolloutValue = $RolloutValue; Version = $version; ShouldPublishValues = $false } 153 | } 154 | 155 | $version = $productPackages[0].version 156 | Write-Verbose "Choosing $version to advertise to following tasks" 157 | 158 | # Transform input if needed. 159 | if ($RolloutAction -eq "set") 160 | { 161 | if ($RolloutValue -eq 100.0) 162 | { 163 | Write-Warning "Setting the rollout value to 100.0% is equal to finalizing the rollout. Changing RolloutAction to 'Finalize'" 164 | $RolloutAction = 'finalize' 165 | } 166 | elseif ($RolloutValue -eq 0.0) 167 | { 168 | Write-Warning "Setting the rollout value to 0.0% is equal to halting the rollout. Changing RolloutAction to 'Halt'" 169 | $RolloutAction = 'halt' 170 | } 171 | } 172 | 173 | if ($RolloutAction -eq "set") 174 | { 175 | Update-SubmissionRollout -ProductId $productId -SubmissionId $SubmissionId -Percentage $RolloutValue -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 176 | } 177 | else 178 | { 179 | $status = [string]::Empty 180 | 181 | if ($existingRollout.percentage -lt $RolloutActionThreshold) 182 | { 183 | $status = $RolloutAction 184 | } 185 | else 186 | { 187 | $status = if ($RolloutAction -eq "finalize") {"halt"} else {"finalize"} 188 | Write-Warning "Switching rollout action to $status as existing rollout percentage $($existingRollout.percentage) >= threshold $RolloutActionThreshold" 189 | } 190 | 191 | $rolloutActionToStatusMapping = @{ 192 | 'finalize' = 'Completed' 193 | 'halt' = 'RolledBack' 194 | } 195 | $trainId = if ($ReleaseTrack -eq 'Flight') {$FlightId} else {"Prod"} 196 | Write-Verbose "Completing rollout by changing rollout status to $($rolloutActionToStatusMapping[$status]) for $trainId of Product $productId" 197 | Update-SubmissionRollout -ProductId $productId -SubmissionId $SubmissionId -State $rolloutActionToStatusMapping[$status] -AccessToken $AccessToken -Verbose:$useVerbose -NoStatus 198 | 199 | $RolloutValue = $(if ($status -eq "finalize") {100.0} else {0.0}) 200 | } 201 | 202 | return [PSCustomObject]@{ Message = "Rollout succeeded"; RolloutValue = $RolloutValue; Version = $version; ShouldPublishValues = $true } 203 | } -------------------------------------------------------------------------------- /tasks/store-package-V3/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "96d0c453-efe7-4c9b-8b0c-fde9a356a5cd", 3 | "name": "store-package", 4 | "friendlyName": "Windows Store - Package V3", 5 | "description": "Create a Json and Zip file based on Store Broker PDPs and Config file", 6 | "author": "Microsoft Corporation", 7 | "helpMarkDown": "Store package task is supported only on Microsoft owned ADO organization. Visit our GitHub repository for help on usage for the [package task](https://github.com/Microsoft/windows-dev-center-vsts-extension/blob/master/docs/usage.md##windows-store---package)", 8 | "category": "Build", 9 | "visibility": [ 10 | "Build", 11 | "Release" 12 | ], 13 | "demands": [ 14 | "DotNetFramework" 15 | ], 16 | "version": { 17 | "Major": "3", 18 | "Minor": "0", 19 | "Patch": "32" 20 | }, 21 | "minimumAgentVersion": "2.117.0", 22 | "instanceNameFormat": "Package $(appName)", 23 | "groups": [ 24 | { 25 | "name": "advanced", 26 | "displayName": "Advanced Options", 27 | "isExpanded": false 28 | } 29 | ], 30 | "inputs": [ 31 | { 32 | "name": "serviceEndpoint", 33 | "type": "connectedService:azureRM", 34 | "label": "Service endpoint", 35 | "defaultValue": "", 36 | "required": true, 37 | "helpMarkDown": "Windows Partner Center endpoint configured with your credentials. The service connection must be of type 'Azure Resource Manager'" 38 | }, 39 | { 40 | "name": "sbConfigPath", 41 | "type": "filePath", 42 | "label": "Config File Full Path", 43 | "defaultValue": "", 44 | "helpMarkDown": "Full Path to the Config File. If not specified, it will be generated based on the current Prod submission", 45 | "required": false 46 | }, 47 | { 48 | "name": "appNameType", 49 | "type": "pickList", 50 | "label": "App identification method", 51 | "helpMarkDown": "How to determine the app to package. This is only required if no config file is provided.", 52 | "defaultValue": "AppId", 53 | "options": { 54 | "AppId": "ID", 55 | "AppName": "Primary name" 56 | }, 57 | "required": false, 58 | "visibleRule": "sbConfigPath = \"\"" 59 | }, 60 | { 61 | "name": "appId", 62 | "type": "string", 63 | "label": "Application ID", 64 | "defaultValue": "", 65 | "required": false, 66 | "helpMarkDown": "ID of the application, found in the URL for the application's page on the Dev Center. This is only required if no config file is provided.", 67 | "visibleRule": "sbConfigPath = \"\" && appNameType = AppId" 68 | }, 69 | { 70 | "name": "appName", 71 | "type": "string", 72 | "label": "Application primary name", 73 | "defaultValue": "", 74 | "required": false, 75 | "helpMarkDown": "Primary name of the application, found at the top of the application's page on the Dev Center. This is only required if no config file is provided.", 76 | "visibleRule": "sbConfigPath = \"\" && appNameType = AppName" 77 | }, 78 | { 79 | "name": "sourceFolder", 80 | "type": "filePath", 81 | "label": "Source Folder", 82 | "defaultValue": "", 83 | "required": false, 84 | "helpMarkDown": "The source folder of the packages that the publishing task consume" 85 | }, 86 | { 87 | "name": "contents", 88 | "type": "multiLine", 89 | "label": "Contents", 90 | "required": false, 91 | "helpMarkDown": "App packages that are part of the submission. Expect the relative path of the packages with respect to the source folder. Minimatch pattern is supported." 92 | }, 93 | { 94 | "name": "pdpPath", 95 | "type": "string", 96 | "label": "PDP Root Path", 97 | "defaultValue": "", 98 | "helpMarkDown": "Your PDPRootPath is expected to be in the following file structure: [PDPRootPath]\\\\[lang-code]\\\\...\\\\[PDPFilename]", 99 | "required": false 100 | }, 101 | { 102 | "name": "pdpMediaPath", 103 | "type": "string", 104 | "label": "Media Root Path", 105 | "defaultValue": "", 106 | "helpMarkDown": "This is where the screenshots and trailers for your listings are stored. Your media must be placed with this structure: [MediaRootPath]\\\\[Release]\\\\[lang-code]\\\\...\\img.png. The 'Release' is the 'Release' attribute found at the top of the corresponding PDP file. If MediaRootPath is specified here, it will override the value from the config file. This must be specified here or in your config file if you are specifying `PDP Root Path`.", 107 | "required": false 108 | }, 109 | { 110 | "name": "outSBPackagePath", 111 | "type": "string", 112 | "label": "Output Path", 113 | "defaultValue": "$(System.DefaultWorkingDirectory)\\SBOutDir", 114 | "helpMarkDown": "Output directory where Store Broker will output generated files out of `New-SubmissionPackage` command. This overrides the value from the config file. [More info](https://github.com/Microsoft/StoreBroker/blob/v2/Documentation/SETUP.md#getting-your-pdps)", 115 | "required": true 116 | }, 117 | { 118 | "name": "outSBName", 119 | "type": "string", 120 | "label": "Output Name", 121 | "defaultValue": "MyNewSubmissionPackage", 122 | "helpMarkDown": "The common name to use for the .json/.zip pair. If specified, this overrides the value from the config file.", 123 | "required": true 124 | }, 125 | { 126 | "name": "pdpReleasePath", 127 | "type": "string", 128 | "label": "Release", 129 | "defaultValue": "", 130 | "helpMarkDown": "Relative path to the correct subfolder within 'PDPRootPath' to find the PDP files to use. If specified, this overrides the value from the config file.", 131 | "required": false, 132 | "visibleRule": "appNameType = Hidden" 133 | }, 134 | { 135 | "name": "pdpInclude", 136 | "type": "multiLine", 137 | "label": "PDP File Names", 138 | "defaultValue": "", 139 | "helpMarkDown": "List of PDP file names that SHOULD be processed. Each line supports wildcards, eg 'ProductDescription*.xml'. Ex: \"*.xml.lss\", \"*.xml.lct\". Minimatch is not supported. If specified, this overrides the value from the config file.", 140 | "required": false 141 | }, 142 | { 143 | "name": "pdpExclude", 144 | "type": "multiLine", 145 | "label": "PDP Exclude", 146 | "defaultValue": "", 147 | "helpMarkDown": "List of PDP file names that SHOULD NOT be processed. Each line supports wildcards, eg 'ProductDescription*.xml'. Ex: \"*.xml.lss\", \"*.xml.lct\". Minimatch is not supported. If specified, this overrides the value from the config file.", 148 | "required": false, 149 | "visibleRule": "appNameType = Hidden" 150 | }, 151 | { 152 | "name": "languageExclude", 153 | "type": "multiLine", 154 | "label": "Language Exclude", 155 | "defaultValue": "", 156 | "helpMarkDown": "List of lang-code strings that SHOULD NOT be processed. Each line represents a language code. Ex: \"default\", \"qps-ploc\". If specified, this overrides the value from the config file.", 157 | "required": false 158 | }, 159 | { 160 | "name": "logPath", 161 | "type": "string", 162 | "label": "Log Path", 163 | "defaultValue": "$(System.DefaultWorkingDirectory)\\SBLog.txt", 164 | "required": false, 165 | "groupName": "advanced", 166 | "helpMarkDown": "The full path for the Store Broker log file" 167 | }, 168 | { 169 | "name": "disableTelemetry", 170 | "type": "boolean", 171 | "label": "Disable Telemetry", 172 | "defaultValue": false, 173 | "groupName": "advanced", 174 | "required": false, 175 | "helpMarkDown": "Whether to disable or not the collection of data. See Microsoft's [Privacy Policy](https://privacy.microsoft.com/en-US/privacystatement) and [Terms of Use](https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx). For specific information on Store Broker's telemetry, see [Store Broker Telemetry](https://github.com/Microsoft/StoreBroker/blob/v2/Documentation/USAGE.md#telemetry)" 176 | } 177 | ], 178 | "execution": { 179 | "PowerShell3": { 180 | "target": "$(currentDirectory)\\packageUi.ps1", 181 | "argumentFormat": "", 182 | "workingDirectory": "$(currentDirectory)" 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tasks/store-flight-V1/flight.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Behavior for the Flight task. Takes authentication information, app information, and packages, 3 | * and flights to the Store. 4 | */ 5 | 6 | import api = require('../common/apiHelper'); 7 | import request = require('../common/requestHelper'); 8 | 9 | import Q = require('q'); 10 | import tl = require('azure-pipelines-task-lib'); 11 | 12 | /** Core parameters for the flight task. */ 13 | export interface CoreFlightParams 14 | { 15 | endpoint: string; 16 | 17 | /** Name of the flight we are publishing packages to */ 18 | flightName: string; 19 | 20 | /** The credentials used to authenticate to the store. */ 21 | authentication: request.Credentials; 22 | 23 | /** 24 | * If true, delete any pending submissions before starting a new one. 25 | * Otherwise, fail the task if a submission is pending. 26 | */ 27 | force: boolean; 28 | 29 | /** A list of paths to the packages to be uploaded. */ 30 | packages: string[]; 31 | 32 | /** A path where the zip file to be uploaded to the dev center will be stored. */ 33 | zipFilePath: string; 34 | 35 | /** If true, we will exit immediately after commit without polling submission till app is published. */ 36 | skipPolling: boolean; 37 | 38 | /**If provided, specifies number of packages per unique target device family and target platform to keep in the flight group. */ 39 | numberOfPackagesToKeep?: number; 40 | 41 | /** If provided, specified the number of hours to differ until the packages in this submission become mandatory. */ 42 | mandatoryUpdateDifferHours?: number; 43 | } 44 | 45 | export interface AppIdParam 46 | { 47 | appId: string; 48 | } 49 | 50 | export interface AppNameParam 51 | { 52 | appName: string; 53 | } 54 | 55 | export type ParamsWithAppId = AppIdParam & CoreFlightParams; 56 | export type ParamsWithAppName = AppNameParam & CoreFlightParams; 57 | export type FlightParams = ParamsWithAppId | ParamsWithAppName; 58 | 59 | /** 60 | * Type guard: indicates whether these parameters contain an App Id or not. 61 | */ 62 | export function hasAppId(p: FlightParams): p is ParamsWithAppId 63 | { 64 | return (p).appId != undefined; 65 | } 66 | 67 | /** 68 | * The parameters given to the task. They're declared here to be 69 | * available to every step of the task without explicitly threading them through. 70 | */ 71 | var taskParams: FlightParams; 72 | 73 | /** The current token used for authentication. */ 74 | var currentToken: request.AccessToken; 75 | 76 | var appId: string; 77 | var flightId: string; 78 | 79 | export async function flightTask(params: FlightParams) 80 | { 81 | taskParams = params; 82 | 83 | /* We expect the endpoint part of this to not have a slash at the end. 84 | * This is because authenticating to 'endpoint/' will give us an 85 | * invalid token, while authenticating to 'endpoint' will work */ 86 | api.ROOT = taskParams.endpoint + api.API_URL_VERSION_PART; 87 | 88 | console.log('Authenticating...'); 89 | currentToken = await request.authenticate(taskParams.endpoint, taskParams.authentication); 90 | 91 | console.log('Obtaining app information...'); 92 | var appResource = await getAppResource(); 93 | 94 | appId = appResource.id; // Globally set app ID for future steps. 95 | 96 | console.log(`Obtaining flight resource for flight ${taskParams.flightName} in app ${appId}`); 97 | var flightResource = await getFlightResource(taskParams.flightName); 98 | 99 | flightId = flightResource.flightId; // Globally set app ID for future steps. 100 | 101 | // Delete pending submission if force is turned on (only one pending submission can exist) 102 | if (taskParams.force && flightResource.pendingFlightSubmission != undefined) 103 | { 104 | console.log('Deleting existing flight submission...'); 105 | await deleteFlightSubmission(flightResource.pendingFlightSubmission.resourceLocation); 106 | } 107 | 108 | console.log('Creating flight submission...'); 109 | var flightSubmissionResource = await createFlightSubmission(); 110 | var submissionUrl = `https://developer.microsoft.com/en-us/dashboard/apps/${appId}/submissions/${flightSubmissionResource.id}`; 111 | console.log(`Submission ${submissionUrl} was created successfully`); 112 | 113 | if (taskParams.numberOfPackagesToKeep != null) 114 | { 115 | console.log('Deleting old packages...'); 116 | api.deleteOldPackages(flightSubmissionResource.flightPackages, taskParams.numberOfPackagesToKeep); 117 | } 118 | 119 | console.log('Updating package delivery options...'); 120 | await api.updatePackageDeliveryOptions( flightSubmissionResource, taskParams.mandatoryUpdateDifferHours); 121 | 122 | console.log('Updating flight submission...'); 123 | await putFlightSubmission(flightSubmissionResource); 124 | 125 | console.log('Creating zip file...'); 126 | var zip = api.createZipFromPackages(taskParams.packages); 127 | if (Object.keys(zip.files).length > 0) 128 | { 129 | await api.persistZip(zip, taskParams.zipFilePath, flightSubmissionResource.fileUploadUrl); 130 | } 131 | 132 | console.log('Committing flight submission...'); 133 | await commitFlightSubmission(flightSubmissionResource.id); 134 | 135 | if (taskParams.skipPolling) 136 | { 137 | console.log('Skip polling option is checked. Skipping polling...'); 138 | console.log(`Click here ${submissionUrl} to check the status of the submission in Dev Center`); 139 | } 140 | else 141 | { 142 | console.log('Polling flight submission...'); 143 | var resourceLocation = `applications/${appId}/flights/${flightId}/submissions/${flightSubmissionResource.id}`; 144 | await api.pollSubmissionStatus(currentToken, resourceLocation, flightSubmissionResource.targetPublishMode); 145 | } 146 | 147 | // Attach summary file for easy access to submission on Dev Center from release Summary tab 148 | var summaryText = api.buildSummaryText(appResource.primaryName, flightResource.friendlyName, submissionUrl, taskParams.skipPolling ? 'publishing' : 'in the store'); 149 | api.attachSubmissionSummary(summaryText); 150 | 151 | tl.setResult(tl.TaskResult.Succeeded, 'Flight submission completed'); 152 | } 153 | 154 | /** 155 | * @return Promises the resource associated with the application given to the task. 156 | */ 157 | async function getAppResource() 158 | { 159 | var appId; 160 | if (hasAppId(taskParams)) 161 | { 162 | appId = taskParams.appId; 163 | } 164 | else 165 | { 166 | tl.debug(`Getting app ID from name ${taskParams.appName}`); 167 | appId = await api.getAppIdByName(currentToken, taskParams.appName); 168 | } 169 | 170 | return api.getAppResource(currentToken, appId); 171 | } 172 | 173 | function getFlightResource(flightName: string, currentPage?: string): Q.Promise 174 | { 175 | if (currentPage === undefined) 176 | { 177 | currentPage = `applications/${appId}/listflights`; 178 | } 179 | 180 | tl.debug(`\tSearching for flight ${flightName} on ${currentPage}`); 181 | 182 | var requestParams = { 183 | url: api.ROOT + currentPage, 184 | method: 'GET' 185 | }; 186 | 187 | return request.performAuthenticatedRequest(currentToken, requestParams).then(body => 188 | { 189 | var foundFlightResource = (body.value).find(x => x.friendlyName == flightName); 190 | if (foundFlightResource) 191 | { 192 | tl.debug(`Flight found with ID ${foundFlightResource.flightId}`); 193 | return foundFlightResource; 194 | } 195 | 196 | if (body['@nextLink'] === undefined) 197 | { 198 | throw new Error(`No flight with name "${flightName}" was found`); 199 | } 200 | 201 | return getFlightResource(flightName, body['@nextLink']); 202 | }); 203 | } 204 | 205 | /** Promises the deletion of a flight submission resource */ 206 | function deleteFlightSubmission(location: string): Q.Promise 207 | { 208 | return api.deleteSubmission(currentToken, `${api.ROOT}applications/${appId}/${location}`); 209 | } 210 | 211 | /** Promises a resource for a new flight submission. */ 212 | function createFlightSubmission(): Q.Promise 213 | { 214 | return api.createSubmission(currentToken, `${api.ROOT}applications/${appId}/flights/${flightId}/submissions`); 215 | } 216 | 217 | /** 218 | * Adds packages to a flight submission resource as Pending Upload, then commits the submission. 219 | * @return Promises the update of the submission resource. 220 | */ 221 | function putFlightSubmission(flightSubmissionResource: any): Q.Promise 222 | { 223 | api.includePackagesInSubmission(taskParams.packages, flightSubmissionResource.flightPackages); 224 | 225 | var url = `${api.ROOT}applications/${appId}/flights/${flightId}/submissions/${flightSubmissionResource.id}`; 226 | return api.putSubmission(currentToken, url, flightSubmissionResource); 227 | } 228 | 229 | /** Promises the committing of the given flight submission. */ 230 | function commitFlightSubmission(flightSubmissionId: string): Q.Promise 231 | { 232 | return api.commitSubmission(currentToken, `${api.ROOT}applications/${appId}/flights/${flightId}/submissions/${flightSubmissionId}/commit`); 233 | } 234 | -------------------------------------------------------------------------------- /tasks/store-rollout-V3/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "b278571a-a422-432c-873a-780b98240596", 3 | "name": "store-rollout-V2", 4 | "friendlyName": "Windows Store - Rollout V3", 5 | "description": "Update rollout in the Windows Store", 6 | "author": "Microsoft Corporation", 7 | "helpMarkDown": "Store rollout task is supported only on Microsoft owned ADO organization. Visit our GitHub repository for help on usage for the [rollout task](https://github.com/Microsoft/windows-dev-center-vsts-extension/blob/master/docs/usage.md##windows-store---rollout)", 8 | "category": "Deploy", 9 | "visibility": [ 10 | "Build", 11 | "Release" 12 | ], 13 | "demands": [ 14 | "DotNetFramework" 15 | ], 16 | "version": { 17 | "Major": "3", 18 | "Minor": "0", 19 | "Patch": "26" 20 | }, 21 | "minimumAgentVersion": "2.117.0", 22 | "instanceNameFormat": "Rollout $(appName)", 23 | "groups": [ 24 | { 25 | "name": "advanced", 26 | "displayName": "Advanced Options", 27 | "isExpanded": false 28 | } 29 | ], 30 | "inputs": [ 31 | { 32 | "name": "serviceEndpoint", 33 | "type": "connectedService:azureRM", 34 | "label": "Service endpoint", 35 | "defaultValue": "", 36 | "required": true, 37 | "helpMarkDown": "Windows Partner Center endpoint configured with your credentials. The service connection must be of type 'Azure Resource Manager'" 38 | }, 39 | { 40 | "name": "appNameType", 41 | "type": "pickList", 42 | "label": "App identification method", 43 | "helpMarkDown": "How to determine the app to publish", 44 | "defaultValue": "AppId", 45 | "options": { 46 | "AppId": "ID", 47 | "AppName": "Primary name" 48 | } 49 | }, 50 | { 51 | "name": "appId", 52 | "type": "string", 53 | "label": "Application ID", 54 | "defaultValue": "", 55 | "required": true, 56 | "helpMarkDown": "ID of the application, found in the URL for the application's page on the Dev Center", 57 | "visibleRule": "appNameType = AppId" 58 | }, 59 | { 60 | "name": "appName", 61 | "type": "string", 62 | "label": "Application primary name", 63 | "defaultValue": "", 64 | "required": true, 65 | "helpMarkDown": "Primary name of the application, found at the top of the application's page on the Dev Center", 66 | "visibleRule": "appNameType = AppName" 67 | }, 68 | { 69 | "name": "releaseTrack", 70 | "type": "pickList", 71 | "label": "Track", 72 | "defaultValue": "Flight", 73 | "helpMarkDown": "", 74 | "options": { 75 | "Production": "Production", 76 | "Flight": "Flight" 77 | } 78 | }, 79 | { 80 | "name": "flightNameType", 81 | "type": "pickList", 82 | "label": "Flight identification method", 83 | "helpMarkDown": "How to determine the flight to publish to", 84 | "defaultValue": "FlightId", 85 | "options": { 86 | "FlightId": "ID", 87 | "FlightName": "Primary name" 88 | }, 89 | "visibleRule": "releaseTrack == Flight" 90 | }, 91 | { 92 | "name": "flightId", 93 | "type": "string", 94 | "label": "Flight name", 95 | "defaultValue": "", 96 | "required": true, 97 | "helpMarkDown": "ID of the flight as seen on the Dev Center", 98 | "visibleRule": "releaseTrack == Flight && flightNameType == FlightId" 99 | }, 100 | { 101 | "name": "flightName", 102 | "type": "string", 103 | "label": "Flight name", 104 | "defaultValue": "", 105 | "required": true, 106 | "helpMarkDown": "Name of the flight as seen on the Dev Center", 107 | "visibleRule": "releaseTrack == Flight && flightNameType == FlightName" 108 | }, 109 | { 110 | "name": "rolloutAction", 111 | "type": "radio", 112 | "label": "Rollout Action", 113 | "defaultValue": "set", 114 | "required": true, 115 | "helpMarkDown": "Will adjust the current gradual rollout percentage to the provided percentage", 116 | "options": { 117 | "finalize": "Finalize", 118 | "halt": "Halt", 119 | "set": "Set" 120 | } 121 | }, 122 | { 123 | "name": "rollout", 124 | "type": "pickList", 125 | "label": "Rollout Value", 126 | "defaultValue": "0.0", 127 | "required": true, 128 | "helpMarkDown": "Choose rollout percentage value. Valid values are (0.0 - 100.0) exclusive", 129 | "options": { 130 | "0.1": "0.1", 131 | "0.5": "0.5", 132 | "1.0": "1", 133 | "5.0": "5", 134 | "10.0": "10", 135 | "20.0": "20", 136 | "40.0": "40", 137 | "60.0": "60", 138 | "80.0": "80", 139 | "99.99": "99.99" 140 | }, 141 | "properties": { 142 | "EditableOptions": "True" 143 | }, 144 | "visibleRule": "rolloutAction = set" 145 | }, 146 | { 147 | "name": "rolloutActionThreshold", 148 | "type": "pickList", 149 | "label": "Execute the action only if the current rollout is less than", 150 | "defaultValue": "100.0", 151 | "required": true, 152 | "helpMarkDown": "Finalize Context: Execute the action only if the current percentage is less than the provided percentage. Else Halt.\r\n. Halt Context: Execute the action only if the current percentage is less than the provided percentage. Else Finalize.\r\n Valid values are [0.0 - 100.0] inclusive.", 153 | "options": { 154 | "0.0": "0", 155 | "0.1": "0.1", 156 | "0.5": "0.5", 157 | "1.0": "1", 158 | "5.0": "5", 159 | "10.0": "10", 160 | "20.0": "20", 161 | "40.0": "40", 162 | "60.0": "60", 163 | "80.0": "80", 164 | "100.0": "100" 165 | }, 166 | "properties": { 167 | "EditableOptions": "True" 168 | }, 169 | "visibleRule": "rolloutAction = finalize || rolloutAction = halt" 170 | }, 171 | { 172 | "name": "currentPackageVersionRegex", 173 | "type": "string", 174 | "label": "Current Package Version Regex", 175 | "defaultValue": ".*", 176 | "groupName": "advanced", 177 | "required": true, 178 | "helpMarkDown": "Regex representing current package version. Rollout will be done, only if package version in existing rollout in Dev Center matches this regex." 179 | }, 180 | { 181 | "name": "skipIfNoMatch", 182 | "type": "boolean", 183 | "label": "Skip if current packages do not match regex", 184 | "defaultValue": "false", 185 | "groupName": "advanced", 186 | "required": true, 187 | "helpMarkDown": "If checked, the task will be skipped instead of failing when at least one package does not match the Current Package Version Regex" 188 | }, 189 | { 190 | "name": "failIfNoRollout", 191 | "type": "boolean", 192 | "label": "Fail if rollout is not in progress", 193 | "defaultValue": "true", 194 | "groupName": "advanced", 195 | "required": true, 196 | "helpMarkDown": "If checked, this task will fail if no in-progress rollout is found in Dev Center for given app and track." 197 | }, 198 | { 199 | "name": "logPath", 200 | "type": "string", 201 | "label": "Log Path", 202 | "defaultValue": "$(System.DefaultWorkingDirectory)\\SBLog.txt", 203 | "required": false, 204 | "groupName": "advanced", 205 | "helpMarkDown": "Specify the full path for the Store Broker log file" 206 | }, 207 | { 208 | "name": "disableTelemetry", 209 | "type": "boolean", 210 | "label": "Disable Telemetry", 211 | "defaultValue": false, 212 | "required": false, 213 | "groupName": "advanced", 214 | "helpMarkDown": "Whether to disable the telemetry or not" 215 | } 216 | ], 217 | "OutputVariables": [ 218 | { 219 | "name" : "Existing_Rollout_Package_Version", 220 | "description" : "The version of the first package found in the existing submission." 221 | }, 222 | { 223 | "name" : "Updated_Rollout_Value", 224 | "description" : "The rollout value set by this task." 225 | } 226 | ], 227 | "execution": { 228 | "PowerShell3": { 229 | "target": "$(currentDirectory)\\rolloutUi.ps1", 230 | "argumentFormat": "", 231 | "workingDirectory": "$(currentDirectory)" 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "windows-store-publish", 4 | "version": "2.4.51", 5 | "name": "Windows Store", 6 | "publisher": "ms-rdx-mro", 7 | "description": "Publish your applications on the Windows Store.", 8 | "categories": [ 9 | "Azure Pipelines" 10 | ], 11 | "galleryFlags": [], 12 | "targets": [ 13 | { 14 | "id": "Microsoft.VisualStudio.Services" 15 | } 16 | ], 17 | "scopes": [ 18 | "vso.build_execute" 19 | ], 20 | "icons": { 21 | "default": "images/logo.png" 22 | }, 23 | "content": { 24 | "details": { 25 | "path": "README.md" 26 | }, 27 | "thirdpartynotice": { 28 | "path": "ThirdPartyNotices.txt" 29 | } 30 | }, 31 | "files": [ 32 | { 33 | "path": "store-flight\\store-flight-V1" 34 | }, 35 | { 36 | "path": "store-flight\\store-flight-V3" 37 | }, 38 | { 39 | "path": "store-publish\\store-publish-V1" 40 | }, 41 | { 42 | "path": "store-publish\\store-publish-V3" 43 | }, 44 | { 45 | "path": "store-rollout\\store-rollout-V3" 46 | }, 47 | { 48 | "path": "store-package\\store-package-V3" 49 | }, 50 | { 51 | "path": "docs", 52 | "addressable": true 53 | }, 54 | { 55 | "path": "images/logo.png", 56 | "addressable": true 57 | } 58 | ], 59 | "tags": [ 60 | "Windows Store", 61 | "Windows Dev Center", 62 | "Publish", 63 | "Deploy" 64 | ], 65 | "links": { 66 | "learn": { 67 | "uri": "https://github.com/Microsoft/windows-dev-center-vsts-extension" 68 | }, 69 | "license": { 70 | "uri": "https://github.com/Microsoft/windows-dev-center-vsts-extension/blob/master/LICENSE" 71 | }, 72 | "support": { 73 | "uri": "https://github.com/Microsoft/windows-dev-center-vsts-extension/issues" 74 | } 75 | }, 76 | "repository": { 77 | "type": "git", 78 | "uri": "https://github.com/Microsoft/windows-dev-center-vsts-extension" 79 | }, 80 | "contributions": [ 81 | { 82 | "id": "devCenterApiEndpoint", 83 | "description": "DevCenterEndpoint", 84 | "type": "ms.vss-endpoint.service-endpoint-type", 85 | "targets": [ 86 | "ms.vss-endpoint.endpoint-types" 87 | ], 88 | "properties": { 89 | "name": "devCenter", 90 | "displayName": "Windows Dev Center V1", 91 | "url": "https://manage.devcenter.microsoft.com", 92 | "inputDescriptors": [], 93 | "authenticationSchemes": [ 94 | { 95 | "type": "ms.vss-endpoint.endpoint-auth-scheme-basic", 96 | "inputDescriptors": [ 97 | { 98 | "id": "url", 99 | "name": "Windows Store API URL", 100 | "description": "Server URL", 101 | "inputMode": "textbox", 102 | "isConfidential": false, 103 | "validation": { 104 | "isRequired": true, 105 | "dataType": "string" 106 | }, 107 | "values": { 108 | "inputId": "url", 109 | "isDisabled": false, 110 | "defaultValue": "https://manage.devcenter.microsoft.com" 111 | } 112 | }, 113 | { 114 | "id": "tenantId", 115 | "name": "Azure tenant ID", 116 | "description": "ID of the Azure tenant", 117 | "inputMode": "textbox", 118 | "isConfidential": false, 119 | "validation": { 120 | "isRequired": true, 121 | "dataType": "string" 122 | } 123 | }, 124 | { 125 | "id": "servicePrincipalId", 126 | "name": "Client ID", 127 | "description": "ID of the client", 128 | "inputMode": "textbox", 129 | "isConfidential": false, 130 | "validation": { 131 | "isRequired": true, 132 | "dataType": "string" 133 | } 134 | }, 135 | { 136 | "id": "servicePrincipalKey", 137 | "name": "Client secret", 138 | "description": "Secret of the client", 139 | "inputMode": "passwordbox", 140 | "isConfidential": true, 141 | "validation": { 142 | "isRequired": true, 143 | "dataType": "string" 144 | } 145 | } 146 | ] 147 | } 148 | ] 149 | } 150 | }, 151 | { 152 | "id": "devCenterApiEndpoint-V2", 153 | "description": "DevCenterEndpointV2", 154 | "type": "ms.vss-endpoint.service-endpoint-type", 155 | "targets": [ 156 | "ms.vss-endpoint.endpoint-types" 157 | ], 158 | "properties": { 159 | "name": "devCenter-V2", 160 | "displayName": "Windows Dev Center V2", 161 | "url": { 162 | "displayName": "Server Url", 163 | "value": "https://manage.devcenter.microsoft.com", 164 | "isVisible": "false", 165 | "helpText": "" 166 | }, 167 | "inputDescriptors": [ ], 168 | "authenticationSchemes": [ 169 | { 170 | "displayName": "Azure Client ID and Secret", 171 | "type": "ms.vss-endpoint.endpoint-auth-scheme-basic", 172 | "inputDescriptors": [ 173 | { 174 | "id": "tenantIdPassword", 175 | "name": "Azure tenant ID", 176 | "description": "ID of the Azure tenant", 177 | "inputMode": "textbox", 178 | "isConfidential": false, 179 | "validation": { 180 | "isRequired": true, 181 | "dataType": "guid", 182 | "maxLength": 38 183 | } 184 | }, 185 | { 186 | "id": "username", 187 | "name": "Client ID", 188 | "description": "ID of the client", 189 | "inputMode": "textbox", 190 | "isConfidential": false, 191 | "validation": { 192 | "isRequired": true, 193 | "dataType": "guid", 194 | "maxLength": 38 195 | } 196 | }, 197 | { 198 | "id": "password", 199 | "name": "Client secret", 200 | "description": "Secret of the client", 201 | "inputMode": "passwordbox", 202 | "isConfidential": true, 203 | "validation": { 204 | "isRequired": true, 205 | "dataType": "string" 206 | } 207 | } 208 | ] 209 | }, 210 | { 211 | "displayName": "Proxy - Tenant Id", 212 | "type": "ms.vss-endpoint.endpoint-auth-scheme-none", 213 | "inputDescriptors": [ 214 | { 215 | "id": "tenantIdProxy", 216 | "name": "Azure tenant ID", 217 | "description": "ID of the Azure tenant", 218 | "inputMode": "textbox", 219 | "isConfidential": false, 220 | "validation": { 221 | "isRequired": true, 222 | "dataType": "guid", 223 | "maxLength": 38 224 | } 225 | }, 226 | { 227 | "id": "proxyUrlTenantId", 228 | "name": "Proxy URL", 229 | "description": "Specify the proxy endpoint url", 230 | "inputMode": "textbox", 231 | "isConfidential": false, 232 | "validation": { 233 | "isRequired": true, 234 | "dataType": "string", 235 | "maxLength": 255 236 | } 237 | } 238 | ] 239 | }, 240 | { 241 | "displayName": "Proxy - Tenant Name", 242 | "type": "ms.vss-endpoint.endpoint-auth-scheme-token", 243 | "inputDescriptors": [ 244 | { 245 | "id": "apitoken", 246 | "name": "Azure tenant friendly name", 247 | "description": "Friendly name of the Azure tenant as configured in your proxy.", 248 | "inputMode": "textbox", 249 | "isConfidential": false, 250 | "validation": { 251 | "isRequired": true, 252 | "dataType": "string", 253 | "maxLength": 255 254 | } 255 | }, 256 | { 257 | "id": "proxyUrlTenantName", 258 | "name": "Proxy URL", 259 | "description": "Specify the proxy endpoint url", 260 | "inputMode": "textbox", 261 | "isConfidential": false, 262 | "validation": { 263 | "isRequired": true, 264 | "dataType": "string", 265 | "maxLength": 255 266 | } 267 | } 268 | ] 269 | } 270 | ] 271 | } 272 | }, 273 | { 274 | "id": "publish-task", 275 | "type": "ms.vss-distributed-task.task", 276 | "targets": [ 277 | "ms.vss-distributed-task.tasks" 278 | ], 279 | "properties": { 280 | "name": "store-publish" 281 | } 282 | }, 283 | { 284 | "id": "flight-task", 285 | "type": "ms.vss-distributed-task.task", 286 | "targets": [ 287 | "ms.vss-distributed-task.tasks" 288 | ], 289 | "properties": { 290 | "name": "store-flight" 291 | } 292 | }, 293 | { 294 | "id": "rollout-task-V2", 295 | "type": "ms.vss-distributed-task.task", 296 | "targets": [ 297 | "ms.vss-distributed-task.tasks" 298 | ], 299 | "properties": { 300 | "name": "store-rollout" 301 | } 302 | }, 303 | { 304 | "id": "package-task", 305 | "type": "ms.vss-distributed-task.task", 306 | "targets": [ 307 | "ms.vss-distributed-task.tasks" 308 | ], 309 | "properties": { 310 | "name": "store-package" 311 | } 312 | } 313 | ] 314 | } -------------------------------------------------------------------------------- /tasks/common/requestHelper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A helper to perform various HTTP requests, with some default handling to manage errors. 3 | * This is mainly a wrapper for the 'request' npm module that uses promises instead of callbacks. 4 | */ 5 | 6 | import { v4 as uuidv4 } from 'uuid'; 7 | import Q = require('q'); 8 | import tl = require('azure-pipelines-task-lib'); 9 | import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; 10 | 11 | /** How long to wait between retries (in ms) */ 12 | const RETRY_DELAY = 60000; 13 | 14 | /** After how long should a connection be given up (in ms). */ 15 | const TIMEOUT = 600000; 16 | 17 | /** Block size of chunks uploaded to the blob (in bytes). */ 18 | const UPLOAD_BLOCK_SIZE_BYTES = 1024 * 1024; // 1Mb 19 | 20 | /** Credentials used to gain access to a particular resource. */ 21 | export interface Credentials 22 | { 23 | /** The tenant ID associated with these credentials. */ 24 | tenant: string; 25 | 26 | clientId: string; 27 | clientSecret: string; 28 | } 29 | 30 | /** A token used to access a particular resource. */ 31 | export interface AccessToken 32 | { 33 | /** Resource to which this token grants access. */ 34 | resource: string 35 | 36 | /** Credentials used to obtain access. */ 37 | credentials: Credentials; 38 | 39 | /** Expiration timestamp of the token */ 40 | expiration: number; 41 | 42 | /** Actual token to be used in the request. */ 43 | token: string; 44 | } 45 | 46 | /** Whether an access token should be renewed. */ 47 | function isExpired(token: AccessToken): boolean 48 | { 49 | // Date.now() returns a number in miliseconds. 50 | // We say that a token is expired if its expiration date is at most five seconds in the future. 51 | return (Date.now() / 1000) + 5 > token.expiration; 52 | } 53 | 54 | /** All the information given to us by the request module along a response. */ 55 | export class ResponseInformation 56 | { 57 | error: any; 58 | response: AxiosResponse | undefined; 59 | body: any; 60 | 61 | constructor(_err: any, _res: AxiosResponse | undefined, _bod: any) 62 | { 63 | this.error = _err; 64 | this.response = _res; 65 | this.body = _bod; 66 | } 67 | 68 | // For friendly logging 69 | toString(): string 70 | { 71 | var log: string; 72 | 73 | if (this.error != undefined) 74 | { 75 | log = `Error ${JSON.stringify(this.error)}`; 76 | } 77 | else 78 | { 79 | var bodyToPrint = this.body; 80 | if (typeof bodyToPrint != 'string') 81 | { 82 | bodyToPrint = JSON.stringify(bodyToPrint); 83 | } 84 | var statusCode: string = (this.response != undefined && this.response.status != undefined) ? this.response.status.toString() : 'unknown'; 85 | log = `Status ${statusCode}: ${bodyToPrint}`; 86 | } 87 | 88 | if (this.response != undefined && 89 | this.response.headers['ms-correlationid'] != undefined) 90 | { 91 | log = log + ` CorrelationId: ${this.response.headers['ms-correlationid']}`; 92 | } 93 | 94 | return log; 95 | } 96 | } 97 | 98 | /** 99 | * Perform a request with some default handling. 100 | * 101 | * For convenience, parses the body if the content-type is 'application/json'. 102 | * Further, examines the body and logs any errors or warnings. 103 | * 104 | * If an transport or application level error occurs, rejects the returned promise. 105 | * The reason given is an instance of @ResponseInformation@, containing the error 106 | * object, the response and the body. 107 | * 108 | * If no error occurs, resolves the returned promise with the body. 109 | * 110 | * @param options Options describing the request to execute. 111 | * @param stream If specified, pipe this stream into the request. 112 | */ 113 | export function performRequest( 114 | options: AxiosRequestConfig): 115 | Q.Promise 116 | { 117 | var deferred = Q.defer(); 118 | 119 | if (options.timeout === undefined) 120 | { 121 | options.timeout = TIMEOUT; 122 | } 123 | 124 | // Log correlation Id for better diagnosis 125 | var correlationId = uuidv4(); 126 | tl.debug(`Starting request with correlation id: ${correlationId}`); 127 | if (options.headers === undefined) 128 | { 129 | options.headers = { 130 | 'CorrelationId': correlationId 131 | } 132 | } 133 | else 134 | { 135 | options.headers['CorrelationId'] = correlationId; 136 | } 137 | 138 | var payload = options.data !== undefined && options.data !== null ? options.data : ''; 139 | tl.debug(`${options.method} ${options.url} with ${JSON.stringify(payload).length}-byte payload`); 140 | 141 | axios(options) 142 | .then((response: AxiosResponse) => { 143 | logErrorsAndWarnings(response, response.data); 144 | deferred.resolve(response.data); 145 | tl.debug(`Request completed successfully with correlation id: ${correlationId}`); 146 | }) 147 | .catch((error: AxiosError) => { 148 | tl.debug(`Request with correlation id: ${correlationId} failed`); 149 | logAxiosError(error); 150 | let response = error.response; 151 | let body = response ? response.data : undefined; 152 | deferred.reject(new ResponseInformation(error, response, body)); 153 | }); 154 | 155 | return deferred.promise; 156 | } 157 | 158 | /** 159 | * Same as @performRequest@, but additionally requires an authentication token. 160 | * @param auth A token used to identify with the resource. If expired, it will be renewed before executing the request. 161 | */ 162 | export function performAuthenticatedRequest( 163 | auth: AccessToken, 164 | options: AxiosRequestConfig): 165 | Q.Promise 166 | { 167 | // The expiration check is a function that returns a promise 168 | var expirationCheck = function () 169 | { 170 | if (isExpired(auth)) 171 | { 172 | tl.debug(`Access token expired for resource: ${auth.resource}. Will refresh token.`); 173 | return authenticate(auth.resource, auth.credentials).then(function (newAuth) 174 | { 175 | auth.token = newAuth.token; 176 | auth.expiration = newAuth.expiration; 177 | }); 178 | } 179 | else 180 | { 181 | /* This looks strange, but it returns a promise for void, which is exactly what we need. */ 182 | return Q.when(); 183 | } 184 | }; 185 | 186 | 187 | return expirationCheck() // Call the expiration check to obtain a promise for it. 188 | .then(function () // Chain the use of the token to that promise. 189 | { 190 | if (!options.headers) { 191 | options.headers = {}; 192 | } 193 | options.headers['Authorization'] = `Bearer ${auth.token}`; 194 | 195 | return performRequest(options); 196 | }); 197 | } 198 | 199 | /** 200 | * @param resource The resource (URL) to authenticate to. 201 | * @param credentials Credentials to use for authentication. 202 | * @returns Promises an access token to use to communicate with the resource. 203 | */ 204 | export function authenticate(resource: string, credentials: Credentials): Q.Promise 205 | { 206 | var endpoint = 'https://login.microsoftonline.com/' + credentials.tenant + '/oauth2/token'; 207 | var requestParams = new URLSearchParams({ 208 | grant_type: 'client_credentials', 209 | client_id: credentials.clientId, 210 | client_secret: credentials.clientSecret, 211 | resource: resource 212 | }); 213 | 214 | var options: AxiosRequestConfig = { 215 | url: endpoint, 216 | method: 'POST', 217 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 218 | data: requestParams.toString() 219 | }; 220 | 221 | console.log('Authenticating with server...'); 222 | return performRequest(options).then(body => 223 | { 224 | var tok: AccessToken = { 225 | resource: resource, 226 | credentials: credentials, 227 | expiration: body.expires_on, 228 | token: body.access_token 229 | }; 230 | 231 | return tok; 232 | }); 233 | } 234 | 235 | /** 236 | * Transforms a promise so that it is tried again a specific number of times if it fails. 237 | * 238 | * A 'generator' of promises must be supplied. The reason is that if a promise fails, 239 | * then it will stay in a failed state and it won't be possible to await on it anymore. 240 | * Therefore a new promise must be returned every time. 241 | * 242 | * @param numRetries How many times should the promise be tried to be fulfilled. 243 | * @param promiseGenerator A function that will generate the promise to try to fulfill. 244 | * @param errPredicate In case an error occurs, receives the reason and returns whether to continue retrying 245 | */ 246 | export function withRetry( 247 | numRetries: number, 248 | promiseGenerator: () => Q.Promise, 249 | errPredicate?: ((err: any) => boolean)): Q.Promise 250 | { 251 | return promiseGenerator().fail(err => 252 | { 253 | if (numRetries > 0 && (!errPredicate || errPredicate(err))) 254 | { 255 | var randomDelay: number = Math.floor(Math.random() * RETRY_DELAY + RETRY_DELAY); // RETRY_DELAY <= randomDelay < 2 * RETRY_DELAY 256 | console.log(`Operation failed with ${err}`); 257 | console.log(`Waiting ${randomDelay / 1000} seconds then retrying... (${numRetries - 1} retrie(s) left)`); 258 | return Q.delay(randomDelay).then(() => withRetry(numRetries - 1, promiseGenerator, errPredicate)); 259 | } 260 | else 261 | { 262 | /* Don't wrap err in an error because it's already an error 263 | (.fail() is the equivalent of "catch" for promises) */ 264 | throw err; 265 | } 266 | }); 267 | } 268 | 269 | /** 270 | * Indicates whether the given object is an HTTP response for a retryable error. 271 | * @param err The error returned by the API 272 | * @param relax Whether the function will return true for most error codes or not 273 | * @description The Windows Store returns 429 and 503 for retryable errors. Relaxing the check will return true also for any error code greater or equal to 500 274 | */ 275 | export function isRetryableError(err:any, relax:boolean = true): boolean 276 | { 277 | // Does this look like a ResponseInformation? 278 | if (err != undefined && err.response != undefined && typeof err.response.status == 'number') 279 | { 280 | return err.response.status == 429 // 429 code is returned by the API for throttle down. This is retriable 281 | || err.response.status == 503 282 | || (relax && err.response.status >= 500); 283 | } 284 | 285 | // Default to retry if no err information. 286 | return true; 287 | } 288 | 289 | /** 290 | * Examines a response body and logs errors and warnings. 291 | * @param response Response returned by the Store API 292 | * @param body A body in the format given by the Store API 293 | * (Where body.statusDetails.errors and body.statusDetails.warnings 294 | * are arrays of objects containing 'code' and 'details' attributes). 295 | */ 296 | function logErrorsAndWarnings(response: any, body: any) 297 | { 298 | if (body === undefined || body.statusDetails === undefined) 299 | return; 300 | 301 | if (Array.isArray(body.statusDetails.errors) && body.statusDetails.errors.length > 0) 302 | { 303 | console.error('Errors occurred in request'); 304 | (body.statusDetails.errors).forEach(x => console.error(`\t[${x.code}] ${x.details}`)); 305 | } 306 | 307 | if (Array.isArray(body.statusDetails.warnings) && body.statusDetails.warnings.length > 0) 308 | { 309 | tl.debug('Warnings occurred in request'); 310 | (body.statusDetails.warnings).forEach(x => tl.debug(`\t[${x.code}] ${x.details}`)); 311 | } 312 | 313 | if (response != undefined && 314 | response.headers['ms-correlationid'] != undefined) 315 | { 316 | tl.debug(`CorrelationId: ${response.headers['ms-correlationid']}`); 317 | } 318 | } 319 | 320 | function logAxiosError(error: AxiosError): void 321 | { 322 | if (axios.isAxiosError(error)) { 323 | tl.debug('AxiosError caught'); 324 | 325 | // General error info 326 | tl.debug(`Message: ${error.message}`); 327 | tl.debug(`Code: ${error.code}`); 328 | tl.debug(`Name: ${error.name}`); 329 | tl.debug(`Stack: ${error.stack}`); 330 | 331 | // Server responded with a status code 332 | if (error.response) { 333 | tl.debug(`Server Response Status: ${error.response.status}`); 334 | tl.debug(`Server Response Headers: ${JSON.stringify(error.response.headers)}`); 335 | tl.debug(`Server Response Data: ${JSON.stringify(error.response.data)}`); 336 | } 337 | // Request was made but no response received 338 | else if (error.request) { 339 | tl.debug(`No server response received. Raw request: ${error.request}`); 340 | } 341 | // Something went wrong setting up the request 342 | else { 343 | tl.debug(`Error was caught during the request setup.`); 344 | } 345 | 346 | // Config used for the request 347 | if (error.config) { 348 | tl.debug(`Request Config: ${JSON.stringify({ 349 | method: error.config.method, 350 | url: error.config.url, 351 | headers: error.config.headers, 352 | data: error.config.data, 353 | timeout: error.config.timeout 354 | })}`); 355 | } 356 | } else { 357 | // Non-Axios error 358 | tl.debug(`Non-Axios error caught: ${error}`); 359 | } 360 | } -------------------------------------------------------------------------------- /tasks/store-publish-V3/publishUi.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param() 3 | 4 | Set-StrictMode -Version 2.0 5 | 6 | try 7 | { 8 | [boolean]$useVerbose = $false 9 | if ([boolean]::TryParse($Env:SYSTEM_DEBUG, [ref]$useVerbose) -and $useVerbose) 10 | { 11 | $VerbosePreference = 'Continue' 12 | } 13 | Write-Verbose "Verbose preference has been set based on the presence of release variable 'System.Debug'" 14 | 15 | # Const 16 | Set-Variable -Name "NugetPath" -Value "$PSScriptRoot\ps_modules\NugetPackages" -Option Constant -Scope Global -Force 17 | Set-Variable -Name "OpenSSLPath" -Value "$PSScriptRoot\ps_modules\openssl" -Option Constant -Scope Global -Force 18 | Set-Variable -Name "StoreBrokerPath" -Value "$PSScriptRoot\ps_modules\StoreBroker" -Option Constant -Scope Local -Force 19 | Set-Variable -Name "StoreBrokerHelperPath" -Value "$PSScriptRoot\ps_common\storeBrokerHelper.psm1" -Option Constant -Scope Local -Force 20 | # This value has to map the name of the out variable defined in the task.json 21 | Set-Variable -Name "OutSubmissionIdVariableName" -Value "WS_SubmissionId" -Option Constant -Scope Local -Force 22 | 23 | Write-Output "Loading dependencies" 24 | Import-Module "$PSScriptRoot\ps_common\commonHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 25 | Import-Module "$PSScriptRoot\ps_common\vstsHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 26 | Import-Module "$PSScriptRoot\publish.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 27 | Import-Module $StoreBrokerPath 6>$null 5>$null 4>$null 3>$null 1>$null 28 | Import-Module $StoreBrokerHelperPath 6>$null 5>$null 4>$null 3>$null 1>$null 29 | Import-Module "$PSScriptRoot\ps_modules\VstsTaskSdk\VstsTaskSdk.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 30 | Import-Module "$PSScriptRoot\ps_modules\AdoAzureHelper\AdoAzureHelper.psm1" 6>$null 5>$null 4>$null 3>$null 1>$null 31 | 32 | Write-Output "Loading task inputs" 33 | [string]$appId = Get-VstsInput -Name "appId" 34 | [string]$appName = Get-VstsInput -Name "appName" 35 | [string]$appNameType = Get-VstsInput -Name "appNameType" 36 | [string]$contents = Get-VstsInput -Name "contents" 37 | [boolean]$createRollout = Get-VstsInput -Name "createRollout" -AsBool 38 | [boolean]$deletePackages = Get-VstsInput -Name "deletePackages" -AsBool 39 | [boolean]$disableTelemetry = Get-VstsInput -Name "disableTelemetry" -AsBool 40 | [string]$endpointId = Get-VstsInput -Name "serviceEndpoint" 41 | [string]$flightId = Get-VstsInput -Name "flightId" 42 | [string]$flightName = Get-VstsInput -Name "flightName" 43 | [string]$flightNameType = Get-VstsInput -Name "flightNameType" 44 | [string]$sandboxId = Get-VstsInput -Name "sandboxId" 45 | [boolean]$force = Get-VstsInput -Name "force" -AsBool 46 | [boolean]$isSeekEnabled = Get-VstsInput -Name "isSeekEnabled" -AsBool 47 | [boolean]$isSparseBundle = Get-VstsInput -Name "isSparseBundle" -AsBool 48 | [boolean]$isMandatoryUpdate = Get-VstsInput -Name "isMandatoryUpdate" -AsBool 49 | [string]$inputMethod = Get-VstsInput -Name "inputMethod" 50 | [string]$jsonPath = Get-VstsInput -Name "jsonPath" 51 | [boolean]$jsonZipUpdateMetadata = Get-VstsInput -Name "jsonZipUpdateMetadata" -AsBool 52 | [string]$logPath = Get-VstsInput -Name "logPath" 53 | [int]$mandatoryUpdateDifferHours = Get-VstsInput -Name "mandatoryUpdateDifferHours" -AsInt 54 | [string]$metadataUpdateMethod = Get-VstsInput -Name "metadataUpdateMethod" 55 | [string]$metadataSource = Get-VstsInput -Name "metadataPath" 56 | [boolean]$minimumMetadata = Get-VstsInput -Name "minimumMetadata" -AsBool 57 | [int]$numberOfPackagesToKeep = Get-VstsInput -Name "numberOfPackagesToKeep" -AsInt 58 | [boolean]$preserveSubmissionId = Get-VstsInput -Name "preserveSubmissionId" -AsBool 59 | [string]$releaseTrack = Get-VstsInput -Name "releaseTrack" 60 | [string]$rollout = Get-VstsInput -Name "rollout" 61 | [string]$existingPackageRolloutAction = Get-VstsInput -Name "existingPackageRolloutAction" 62 | [boolean]$skipPolling = Get-VstsInput -Name "skipPolling" -AsBool 63 | [string]$sourceFolder = Get-VstsInput -Name "sourceFolder" 64 | [string]$targetPublishMode = Get-VstsInput -Name "targetPublishMode" 65 | [string]$targetPublishDate = Get-VstsInput -Name "targetPublishDate" 66 | [boolean]$updateImages = Get-VstsInput -Name "updateImages" -AsBool 67 | [boolean]$updateVideos = Get-VstsInput -Name "updateVideos" -AsBool 68 | [boolean]$updateText = Get-VstsInput -Name "updateText" -AsBool 69 | [boolean]$updatePublishModeAndVisibility = Get-VstsInput -Name "updatePublishModeAndVisibility" -AsBool 70 | [boolean]$updatePricingAndAvailability = Get-VstsInput -Name "updatePricingAndAvailability" -AsBool 71 | [boolean]$updateAppProperties = Get-VstsInput -Name "updateAppProperties" -AsBool 72 | [boolean]$updateGamingOptions = Get-VstsInput -Name "updateGamingOptions" -AsBool 73 | [boolean]$updateNotesForCertification = Get-VstsInput -Name "updateNotesForCertification" -AsBool 74 | [string]$visibility = Get-VstsInput -Name "visibility" 75 | [string]$zipPath = Get-VstsInput -Name "zipPath" 76 | 77 | $endPointObj = Get-VstsEndpoint -Name $endpointId -Require 78 | 79 | Write-Output "Setting Store Broker environment" 80 | Set-StoreBrokerSettings -LogPath $logPath -NugetPath $NugetPath -DisableTelemetry $disableTelemetry -Verbose:$useVerbose 81 | 82 | # Getting AAD accessToken that would be later used by all the StoreBroker commands to access Partner Center APIs. 83 | Initialize-AdoAzureHelper -msalLibraryDir $NugetPath -adoApiLibraryDir $NugetPath -openSSLExeDir $OpenSSLPath 84 | $resource = "https://api.partner.microsoft.com" 85 | 86 | $sendX5C = $true 87 | $useMSAL = $true 88 | if (($endPointObj.Auth.Scheme -eq 'WorkloadIdentityFederation') -or ($endPointObj.Auth.Parameters.AuthenticationType -ne 'SPNCertificate')) 89 | { 90 | $sendX5C = $false 91 | } 92 | 93 | $aadAccessToken = (Get-AzureRMAccessToken $endPointObj $endpointId $resource $sendX5C $useMSAL).access_token 94 | 95 | $commonParams = @{ 96 | 'AppId' = $appId 97 | 'AppName' = $appName 98 | 'AppNameType' = $appNameType 99 | 'FlightId' = $flightId 100 | 'FlightName' = $flightName 101 | 'FlightNameType' = $flightNameType 102 | 'SandboxId' = $sandboxId 103 | 'ReleaseTrack' = $releaseTrack 104 | 'AccessToken' = $aadAccessToken 105 | 'Verbose' = $useVerbose 106 | } 107 | 108 | # Because there could be multiple flight tasks running at the same time, we need to store the submission IDs into multiple variables 109 | # Therefore we append the flight name or id to the end of the submissionIdVariableName 110 | $submissionIdVariableName = $OutSubmissionIdVariableName 111 | if ($releaseTrack -eq 'Production') 112 | { 113 | $submissionIdVariableName = "WS_SubmissionId_Prod" 114 | } 115 | elseif ($releaseTrack -eq 'Sandbox') 116 | { 117 | $submissionIdVariableName = "WS_SubmissionId_Prod_$sandboxId" 118 | } 119 | elseif ($flightNameType -eq 'FlightId') 120 | { 121 | $submissionIdVariableName = "WS_SubmissionId_$flightId" 122 | } 123 | else 124 | { 125 | $flightNameNoSpace = $flightName.Replace(" ", "_") 126 | $submissionIdVariableName = "WS_SubmissionId_$flightNameNoSpace" 127 | } 128 | 129 | # Try to grab submission Id from release variables in case this is a retry 130 | $shouldPublish = $true 131 | $submissionId = "NoId" 132 | 133 | # See if submissionIdVariableName is part of the environment variable 134 | if (Test-Path "Env:$submissionIdVariableName") 135 | { 136 | $submissionId = (Get-ChildItem "Env:$submissionIdVariableName").value 137 | Write-Output "Found submission Id $submissionId from release variables" 138 | if ($skipPolling) 139 | { 140 | Write-Warning "Skip polling is checked, but a submission Id was found in the release variables.$([Environment]::NewLine)" + ` 141 | "This task populates the submission Id as release variable only if the submission has been submitted and 'Preserve Submission ID' is checked.$([Environment]::NewLine)" + ` 142 | "Assuming $submissionId was created by a previous run of this release.$([Environment]::NewLine)" + ` 143 | "This task will terminate.$([Environment]::NewLine)" + ` 144 | "$([Environment]::NewLine)" + ` 145 | "If you want to retry a failed submission, either uncheck the skip polling option to monitor the existing submission,$([Environment]::NewLine)" + ` 146 | "or remove the value on the SubmissionId release variable to create a new submission. You might need to also check the 'Delete Pending Submission' box." 147 | Write-Output "No action taken. See warnings for more information" 148 | Write-VstsSetResult -Result "Succeeded" -Message "No action taken. See warnings for more information" 149 | $shouldPublish = $false 150 | } 151 | 152 | try 153 | { 154 | if ($(Watch-ExistingSubmission @commonParams -SubmissionId $submissionId)) 155 | { 156 | Write-Output "Existing submission $submissionId completed" 157 | Write-VstsSetResult -Result "Succeeded" -Message "Existing submission $submissionId completed" 158 | $shouldPublish = $false 159 | } 160 | else 161 | { 162 | Write-Output "Existing submission $submissionId did not complete. Retrying with new submission" 163 | $shouldPublish = $true 164 | } 165 | } 166 | catch 167 | { 168 | $exceptionString = $($_ | Out-String) 169 | if ($exceptionString.Contains('(404)')) 170 | { 171 | Write-Warning "Submission Id $submissionId was not found. Retrying with new submission" 172 | $shouldPublish = $true 173 | } 174 | else 175 | { 176 | $newException = New-Object System.Management.Automation.RuntimeException -ArgumentList "Error while trying to watch for existing submission $submissionId. See Inner exception for details", $_.Exception 177 | throw $newException 178 | } 179 | } 180 | } 181 | 182 | if ($shouldPublish) 183 | { 184 | $publishTaskParams = @{ 185 | 'CreateRollout' = $createRollout 186 | 'Contents' = $contents 187 | 'DeletePackages' = $deletePackages 188 | 'DisableTelemetry' = $disableTelemetry 189 | 'EndPointObj' = $endPointObj 190 | 'Force' = $force 191 | 'IsSeekEnabled' = $isSeekEnabled 192 | 'IsSparseBundle' = $isSparseBundle 193 | 'IsMandatoryUpdate' = $isMandatoryUpdate 194 | 'InputMethod' = $inputMethod 195 | 'JsonPath' = $jsonPath 196 | 'JsonZipUpdateMetadata' = $jsonZipUpdateMetadata 197 | 'LogPath' = $logPath 198 | 'MandatoryUpdateDifferHours' = $mandatoryUpdateDifferHours 199 | 'MetadataUpdateMethod' = $metadataUpdateMethod 200 | 'MetadataSource' = $metadataSource 201 | 'MinimumMetadata' = $minimumMetadata 202 | 'NugetPath' = $NugetPath 203 | 'NumberOfPackagesToKeep' = $numberOfPackagesToKeep 204 | 'Rollout' = $rollout 205 | 'ExistingPackageRolloutAction' = $existingPackageRolloutAction 206 | 'SourceFolder' = $sourceFolder 207 | 'StoreBrokerHelperPath' = $StoreBrokerHelperPath 208 | 'StoreBrokerPath' = $StoreBrokerPath 209 | 'TargetPublishMode' = $targetPublishMode 210 | 'TargetPublishDate' = $targetPublishDate 211 | 'UpdateImages' = $updateImages 212 | 'UpdateVideos' = $updateVideos 213 | 'UpdateText' = $updateText 214 | 'UpdatePublishModeAndVisibility' = $updatePublishModeAndVisibility 215 | 'UpdatePricingAndAvailability' = $updatePricingAndAvailability 216 | 'UpdateAppProperties' = $updateAppProperties 217 | 'UpdateGamingOptions' = $updateGamingOptions 218 | 'UpdateNotesForCertification' = $updateNotesForCertification 219 | 'Visibility' = $visibility 220 | 'ZipPath' = $zipPath 221 | } 222 | 223 | Write-Output "Start publishing" 224 | $submissionId = Start-Publishing @commonParams @publishTaskParams 225 | Write-Output "Submitted submission $submissionId" 226 | 227 | if ($preserveSubmissionId) 228 | { 229 | if ($null -eq $Env:SYSTEM_ACCESSTOKEN) 230 | { 231 | Write-Warning $("You chose to preserve the submission ID at the release level, but the agent did not populate the auth token. Skipping operation.$([Environment]::NewLine)" + ` 232 | "Verify you have checked the option 'Allow scripts to access the OAuth token' for this agent phase.$([Environment]::NewLine)" + ` 233 | "For more details, see https://github.com/Microsoft/windows-dev-center-vsts-extension/blob/master/docs/usage.md#advanced-options") 234 | } 235 | else 236 | { 237 | try 238 | { 239 | # This functionality has not been tested in TFS servers 240 | $setVariableParams = @{ 241 | 'TeamProject' = $Env:SYSTEM_TEAMPROJECT 242 | 'AccessToken' = $Env:SYSTEM_ACCESSTOKEN 243 | 'Name' = $submissionIdVariableName 244 | 'Value' = $submissionId 245 | 'Verbose' = $useVerbose 246 | } 247 | 248 | if (-not [string]::IsNullOrWhiteSpace($Env:RELEASE_RELEASEID)) 249 | { 250 | $setVariableParams['TeamFoundationServerUri'] = $Env:SYSTEM_TEAMFOUNDATIONSERVERURI 251 | $setVariableParams['ReleaseId'] = $Env:RELEASE_RELEASEID 252 | Set-ReleaseVariable @setVariableParams 253 | Write-Output "Release has been updated with release variable $submissionIdVariableName = $submissionId" 254 | } 255 | else 256 | { 257 | $setVariableParams['TeamFoundationCollectionUri'] = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI 258 | $setVariableParams['BuildId'] = $Env:BUILD_BUILDID 259 | Set-BuildVariable @setVariableParams 260 | Write-Output "Build has been updated with build variable $submissionIdVariableName = $submissionId" 261 | } 262 | } 263 | catch 264 | { 265 | Write-Warning "There was an error preserving the submission ID. Continuing normal process" 266 | Write-Verbose "Error preserving submission ID on resource. Details:" 267 | Write-Verbose $($_ | Out-String) 268 | Write-Verbose $($_.Exception.ToString()) 269 | } 270 | } 271 | } 272 | else 273 | { 274 | Write-Output "The release won't be modified to store the submission ID." 275 | } 276 | 277 | if ($skipPolling) 278 | { 279 | Write-Output "Submission has been submitted. Skipping polling as requested." 280 | Write-Output "You can view the progress of the submission validation on the Dev Portal here:" 281 | Write-Output "https://partner.microsoft.com/en-us/dashboard/apps/$appId/submissions/$submissionId/" 282 | Write-VstsSetResult -Result "Succeeded" -Message "Submission has been submitted. Skipping polling as requested." 283 | } 284 | else 285 | { 286 | Write-Output "Polling for submission $submissionId to finish" 287 | 288 | $shouldMonitor = $true 289 | while ($shouldMonitor) 290 | { 291 | try 292 | { 293 | # Refresh AccessToken before polling 294 | $commonParams['AccessToken'] = (Get-AzureRMAccessToken $endPointObj $endpointId $resource).access_token 295 | if (-not $(Watch-ExistingSubmission @commonParams -SubmissionId $submissionId -TargetPublishMode $targetPublishMode)) 296 | { 297 | throw $("The submission $submissionId didn't reach the publishing state.$([Environment]::NewLine)" + ` 298 | "Verify issues in dev center: https://partner.microsoft.com/en-us/dashboard/products/$appId/submissions/$submissionId") 299 | } 300 | Write-Output "Submission $submissionId completed" 301 | $shouldMonitor = $false 302 | } 303 | catch 304 | { 305 | # Catch any authorization related exception and retry polling by refreshing the access token 306 | if ($_.Exception.Message -ilike "*Unauthorized*") 307 | { 308 | Write-Output "Got exception with authentication while trying to check on submission. Will try again. The exception was:" -Exception $_ -Level Warning 309 | } 310 | else { 311 | throw $_ 312 | } 313 | } 314 | } 315 | 316 | Write-VstsSetResult -Result "Succeeded" -Message "Submission $submissionId completed" 317 | } 318 | } 319 | 320 | # Always call vsts command to set variable so it is consumable by future tasks in the same job. 321 | Write-Output "##vso[task.setvariable variable=$OutSubmissionIdVariableName;isSecret=false;isOutput=true;]$submissionId" 322 | Write-Output "Future tasks can consume the variable $OutSubmissionIdVariableName using this task's reference name" 323 | Write-Output "For more information on output variables, visit https://github.com/Microsoft/azure-pipelines-agent/blob/master/docs/preview/outputvariable.md" 324 | } 325 | catch 326 | { 327 | Write-Error "$($_ | Out-String)" 328 | } 329 | finally 330 | { 331 | if ((Test-Path variable:logPath) -and (Test-Path $logPath -PathType Leaf)) 332 | { 333 | Write-Output "Attaching Store Broker log file $logPath. You can download it alongside the agent logs." 334 | Write-Output "##vso[task.uploadfile]$logPath" 335 | } 336 | } --------------------------------------------------------------------------------