├── .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 | 
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 | [](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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------