diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1893457..5b435e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,18 +3,23 @@ name: CI on: push: branches: - - develop - - master + - main pull_request: types: [opened, synchronize, reopened] branches: - - develop - - master - + - main + +concurrency: + group: ${{github.workflow}} - ${{github.ref}} + cancel-in-progress: true + +env: + GitVersion_Version: '5.6.x' + jobs: - build: - name: Build + build-tools: + name: Build Tools runs-on: windows-2022 steps: - name: Checkout @@ -35,7 +40,7 @@ jobs: - name: Setup GitVersion uses: gittools/actions/gitversion/setup@v0.9.9 with: - versionSpec: '5.6.x' + versionSpec: ${{ env.GitVersion_Version }} - name: GitVersion id: gitversion @@ -60,12 +65,65 @@ jobs: name: NuGet path: .\artifacts - publish: + build-extensions: + name: Build Extensions + runs-on: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '5.0.x' + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.300' + + - name: Setup GitVersion + uses: gittools/actions/gitversion/setup@v0.9.9 + with: + versionSpec: ${{ env.GitVersion_Version }} + + - name: GitVersion + id: gitversion + uses: gittools/actions/gitversion/execute@v0.9.9 + with: + useConfigFile: true + configFilePath: gitversion.yml + + - name: Install tfx + working-directory: extensions/azuredevops + run: npm install tfx-cli@0.7.x -g --no-audit --no-fund + + - name: npm install + working-directory: extensions/azuredevops + run: npm install + + - name: Compile + working-directory: extensions/azuredevops + run: .\node_modules\.bin\tsc -project .\tsconfig.json --listEmittedFiles --locale en-US --isolatedModules + + - name: Package Extension + working-directory: extensions/azuredevops + run: tfx extension create --json --no-color --output-path .\artifacts\Build.Tasks.${{ steps.gitversion.outputs.MajorMinorPatch }}.vsix --override "{""version"":""${{ steps.gitversion.outputs.MajorMinorPatch }}""}" + + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: extensions + path: extensions/azuredevops/artifacts + + publish-tools: name: Publish if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/master')) }} runs-on: windows-latest needs: - - build + - build-tools steps: - name: Checkout uses: actions/checkout@v2 diff --git a/extensions/azuredevops/.azure-pipelines.yml b/extensions/azuredevops/.azure-pipelines.yml new file mode 100644 index 0000000..c3cf7e9 --- /dev/null +++ b/extensions/azuredevops/.azure-pipelines.yml @@ -0,0 +1,36 @@ +pool: 'Windows 1809' + +trigger: + batch: true + branches: + include: + - master + +steps: +- task: GitVersion@4 + +- task: NodeTool@0 + +- task: TfxInstaller@2 + inputs: + version: 'v0.7.x' + +- task: Npm@1 + inputs: + command: install + +- script: .\node_modules\.bin\tsc -project .\tsconfig.json --listEmittedFiles --locale en-US --isolatedModules + +- task: PackageAzureDevOpsExtension@2 + inputs: + rootFolder: + outputPath: '$(Build.ArtifactStagingDirectory)\Build.Tasks.$(GitVersion.MajorMinorPatch).vsix' + extensionVersion: '$(GitVersion.MajorMinorPatch)' + updateTasksVersion: true + +- task: PublishBuildArtifacts@1 + inputs: + ArtifactName: Extension + +- task: PostBuildCleanup@3 + condition: always() \ No newline at end of file diff --git a/extensions/azuredevops/.gitignore b/extensions/azuredevops/.gitignore new file mode 100644 index 0000000..d5a06f7 --- /dev/null +++ b/extensions/azuredevops/.gitignore @@ -0,0 +1,116 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/extensions/azuredevops/LICENSE b/extensions/azuredevops/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/extensions/azuredevops/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/extensions/azuredevops/README.md b/extensions/azuredevops/README.md new file mode 100644 index 0000000..abe5a3c --- /dev/null +++ b/extensions/azuredevops/README.md @@ -0,0 +1,24 @@ +# unoplatform-Build-Tools + +This is the code for the [unoplatform Build Tools](https://marketplace.visualstudio.com/items?itemName=nventivecorp.unoplatform) Azure Pipelines extension. + +## Features + +Check [overview.md](overview.md). + +## Breaking Changes + +Please consult [BREAKING_CHANGES.md](BREAKING_CHANGES.md) for more information about version +history and compatibility. + +## License + +This project is licensed under the Apache 2.0 license - see the +[LICENSE](LICENSE) file for details. + +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process for +contributing to this project. + +Be mindful of our [Code of Conduct](CODE_OF_CONDUCT.md). \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/Readme.md b/extensions/azuredevops/canaryUpdater/Readme.md new file mode 100644 index 0000000..0ef50fb --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/Readme.md @@ -0,0 +1,18 @@ +# unoplatform Canary Updater task + +This task is meant to be used by unoplatform Canary process. The flow of this task is as follows + +## 1. Merge +If requested, the task merges the current branch with the one indicated in the parameters. To do so, and if the branch is not in the current repository, a remote is added to the Git repository and the specfied branch is fetched. +Once the branch is fetched, and once again if requested, it is pushed to the current repository. This is used to keep an updated version of the target branch in the current repository. +Finally, a merge is run between the two branches, taking the target branch changes in priority. This is achieved using the `-X theirs` parameters of the merge command. This means that changes from the current branches might be overriden by the target branch, but it helps with the maintenance of the canaries. + +## 2. NuGet update +This is the core of the Canary task. This step updates the NuGet packages present in the branch, using [NuGet Updater](https://github.com/unoplatform/NuGet.Updater/tree/develop/src/NvGet.Tools.Updater#readme). The majority of the NuGet Updater parameters are mapped directly in the task, and the full command is printed in the output, making it easy to debug directly. + +The target versions parameter can be either explicit or calculated from the source branch. If it is not specified, the target version will be set to whatever is after `canaries/` in the branch name. Multiple versions can be specified using the `+` sign. If the branch doesn't have this format, the task will fail. `stable` will always be included if the target version is calculated this way. + +## 3. Branch push +Once again, this step is optional. It pushes a new branch with all the changes introduced with both the merge and the NuGet update. The new branch will have a name generated from the target version specified in either the parameters or in the branch name directly, the build number and be prefixed with `canaries/build`. It is advised to configure the pipeline to have the following build number pattern: `$(Date:yyyyMMdd)$(Rev:.r)`. + +For example, in the scenario where this task would be run on a branch named `canaries/dev`, the pushed branch would be named `canaries/build/dev/20211004.1` \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/icon.png b/extensions/azuredevops/canaryUpdater/icon.png new file mode 100644 index 0000000..1a199b9 Binary files /dev/null and b/extensions/azuredevops/canaryUpdater/icon.png differ diff --git a/extensions/azuredevops/canaryUpdater/nuget/commandhelper.ts b/extensions/azuredevops/canaryUpdater/nuget/commandhelper.ts new file mode 100644 index 0000000..2593002 --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/nuget/commandhelper.ts @@ -0,0 +1,54 @@ +import * as path from "path"; +import * as tl from "azure-pipelines-task-lib/task"; + +export interface LocateOptions { + /** if true, search along the system path in addition to the hard-coded NuGet tool paths */ + fallbackToSystemPath?: boolean; + + /** Array of filenames to use when searching for the tool. Defaults to the tool name. */ + toolFilenames?: string[]; + + /** Array of paths to search under. Defaults to agent NuGet locations */ + searchPath?: string[]; + + /** root that searchPaths are relative to. Defaults to the Agent.HomeDirectory build variable */ + root?: string; +} + +export function locateTool(tool: string, opts?: LocateOptions) { + const defaultSearchPath = [""]; + const defaultAgentRoot = tl.getVariable("Agent.HomeDirectory"); + + opts = opts || {}; + opts.toolFilenames = opts.toolFilenames || [tool]; + + let searchPath = opts.searchPath || defaultSearchPath; + let agentRoot = opts.root || defaultAgentRoot; + + tl.debug(`looking for tool ${tool}`); + + for (let thisVariant of opts.toolFilenames) { + tl.debug(`looking for tool variant ${thisVariant}`); + + for (let possibleLocation of searchPath) { + let fullPath = path.join(agentRoot, possibleLocation, thisVariant); + tl.debug(`checking ${fullPath}`); + if (tl.exist(fullPath)) { + return fullPath; + } + } + + if (opts.fallbackToSystemPath) { + tl.debug("Checking system path"); + let whichResult = tl.which(thisVariant); + if (whichResult) { + tl.debug(`found ${whichResult}`); + return whichResult; + } + } + + tl.debug("not found"); + } + + return null; +} \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/nuget/locationUtilities.ts b/extensions/azuredevops/canaryUpdater/nuget/locationUtilities.ts new file mode 100644 index 0000000..c81cd10 --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/nuget/locationUtilities.ts @@ -0,0 +1,249 @@ +import * as vsts from 'azure-devops-node-api'; +import * as interfaces from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; +import * as tl from 'azure-pipelines-task-lib/task'; +import { IRequestOptions } from 'azure-devops-node-api/interfaces/common/VsoBaseInterfaces'; + +import * as provenance from "./provenance"; + +export enum ProtocolType { + NuGet, + Maven, + Npm, + PyPi +} + +export enum RegistryType { + npm, + NuGetV2, + NuGetV3, + PyPiSimple, + PyPiUpload +} + +export interface PackagingLocation { + PackagingUris: string[]; + DefaultPackagingUri: string; +} + +// Getting service urls from resource areas api +export async function getServiceUriFromAreaId(serviceUri: string, accessToken: string, areaId: string): Promise { + const serverType = tl.getVariable('System.ServerType'); + if (!serverType || serverType.toLowerCase() !== 'hosted') { + return serviceUri; + } + + const webApi = getWebApiWithProxy(serviceUri, accessToken); + const locationApi = await webApi.getLocationsApi(); + + tl.debug(`Getting URI for area ID ${areaId} from ${serviceUri}`); + try { + const serviceUriFromArea = await locationApi.getResourceArea(areaId); + return serviceUriFromArea.locationUrl; + } catch (error) { + throw new Error(error); + } +} + +export async function getNuGetUriFromBaseServiceUri(serviceUri: string, accesstoken: string): Promise { + const nugetAreaId = 'B3BE7473-68EA-4A81-BFC7-9530BAAA19AD'; + + return getServiceUriFromAreaId(serviceUri, accesstoken, nugetAreaId); +} + +// Feeds url from location service +export async function getFeedUriFromBaseServiceUri(serviceUri: string, accesstoken: string): Promise { + const feedAreaId = '7ab4e64e-c4d8-4f50-ae73-5ef2e21642a5'; + + return getServiceUriFromAreaId(serviceUri, accesstoken, feedAreaId); +} + +export async function getBlobstoreUriFromBaseServiceUri(serviceUri: string, accesstoken: string): Promise { + const blobAreaId = '5294ef93-12a1-4d13-8671-9d9d014072c8'; + + return getServiceUriFromAreaId(serviceUri, accesstoken, blobAreaId); +} + +/** + * PackagingLocation.PackagingUris: + * The first URI will always be the TFS collection URI + * The second URI, if existent, will be Packaging's default access point + * The remaining URI's will be alternate Packaging's access points + */ +export async function getPackagingUris(protocolType: ProtocolType): Promise { + tl.debug('Getting Packaging service access points'); + const collectionUrl = tl.getVariable('System.TeamFoundationCollectionUri'); + + const pkgLocation: PackagingLocation = { + PackagingUris: [collectionUrl], + DefaultPackagingUri: collectionUrl + }; + + const serverType = tl.getVariable('System.ServerType'); + if (!serverType || serverType.toLowerCase() !== 'hosted') { + return pkgLocation; + } + + const accessToken = getSystemAccessToken(); + const areaId = getAreaIdForProtocol(protocolType); + + const serviceUri = await getServiceUriFromAreaId(collectionUrl, accessToken, areaId); + + const webApi = getWebApiWithProxy(serviceUri); + + const locationApi = await webApi.getLocationsApi(); + + tl.debug('Acquiring Packaging endpoints from ' + serviceUri); + + const connectionData = await Retry(async () => { + tl.debug('Attempting to get connection data'); + return await locationApi.getConnectionData(interfaces.ConnectOptions.IncludeServices); + }, 4, 100); + + tl.debug('Successfully acquired the connection data'); + const defaultAccessPoint: string = connectionData.locationServiceData.accessMappings.find((mapping) => + mapping.moniker === connectionData.locationServiceData.defaultAccessMappingMoniker + ).accessPoint; + + pkgLocation.DefaultPackagingUri = defaultAccessPoint; + pkgLocation.PackagingUris.push(defaultAccessPoint); + pkgLocation.PackagingUris = pkgLocation.PackagingUris.concat( + connectionData.locationServiceData.accessMappings.map((mapping) => { + return mapping.accessPoint; + })); + + tl.debug('Acquired location'); + tl.debug(JSON.stringify(pkgLocation)); + return pkgLocation; +} + +export function getSystemAccessToken(): string { + tl.debug('Getting credentials for local feeds'); + const auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false); + if (auth.scheme === 'OAuth') { + tl.debug('Got auth token'); + return auth.parameters['AccessToken']; + } else { + tl.warning('Could not determine credentials to use'); + } +} + +function getAreaIdForProtocol(protocolType: ProtocolType): string { + switch (protocolType) { + case ProtocolType.Maven: + return '6F7F8C07-FF36-473C-BCF3-BD6CC9B6C066'; + case ProtocolType.Npm: + return '4C83CFC1-F33A-477E-A789-29D38FFCA52E'; + default: + case ProtocolType.NuGet: + return 'B3BE7473-68EA-4A81-BFC7-9530BAAA19AD'; + } +} + +export function getWebApiWithProxy(serviceUri: string, accessToken?: string): vsts.WebApi { + if (!accessToken) { + accessToken = getSystemAccessToken(); + } + + const credentialHandler = vsts.getBasicHandler('vsts', accessToken); + const options: IRequestOptions = { + proxy: tl.getHttpProxyConfiguration(serviceUri) + }; + return new vsts.WebApi(serviceUri, credentialHandler, options); +} + +interface RegistryLocation { + apiVersion: string, + area: string, + locationId: string +}; + +export async function getFeedRegistryUrl( + packagingUrl: string, + registryType: RegistryType, + feedId: string, + accessToken?: string, + useSession?: boolean): Promise { + let loc : RegistryLocation; + switch (registryType) { + case RegistryType.npm: + loc = { + apiVersion: '3.0-preview.1', + area: 'npm', + locationId: 'D9B75B07-F1D9-4A67-AAA6-A4D9E66B3352' + }; + break; + case RegistryType.NuGetV2: + loc = { + apiVersion: '3.0-preview.1', + area: 'nuget', + locationId: "5D6FC3B3-EF78-4342-9B6E-B3799C866CFA" + }; + break; + case RegistryType.PyPiSimple: + loc = { + apiVersion: '5.0', + area: 'pypi', + locationId: "93377A2C-F5FB-48B9-A8DC-7781441CABF1" + }; + break; + case RegistryType.PyPiUpload: + loc = { + apiVersion: '5.0', + area: 'pypi', + locationId: "C7A75C1B-08AC-4B11-B468-6C7EF835C85E" + }; + break; + default: + case RegistryType.NuGetV3: + loc = { + apiVersion: '3.0-preview.1', + area: 'nuget', + locationId: "9D3A4E8E-2F8F-4AE1-ABC2-B461A51CB3B3" + }; + break; + } + + tl.debug("Getting registry url from " + packagingUrl); + + const vssConnection = getWebApiWithProxy(packagingUrl, accessToken); + + let sessionId = feedId; + if (useSession) { + sessionId = await provenance.ProvenanceHelper.GetSessionId( + feedId, + loc.area /* protocol */, + vssConnection.serverUrl, + [vssConnection.authHandler], + vssConnection.options); + } + + const data = await Retry(async () => { + return await vssConnection.vsoClient.getVersioningData(loc.apiVersion, loc.area, loc.locationId, { feedId: sessionId }); + }, 4, 100); + + tl.debug("Feed registry url: " + data.requestUrl); + return data.requestUrl; +} + +// This should be replaced when retry is implemented in vso client. +async function Retry(cb : () => Promise, max_retry: number, retry_delay: number) : Promise { + try { + return await cb(); + } catch(exception) { + tl.debug(JSON.stringify(exception)); + if(max_retry > 0) + { + tl.debug("Waiting " + retry_delay + "ms..."); + await delay(retry_delay); + tl.debug("Retrying..."); + return await Retry(cb, max_retry-1, retry_delay*2); + } else { + throw new Error(exception); + } + } +} +function delay(delayMs:number) { + return new Promise(function(resolve) { + setTimeout(resolve, delayMs); + }); + } \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/nuget/provenance.ts b/extensions/azuredevops/canaryUpdater/nuget/provenance.ts new file mode 100644 index 0000000..8e1e2da --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/nuget/provenance.ts @@ -0,0 +1,167 @@ +import * as tl from "azure-pipelines-task-lib"; + +import * as VsoBaseInterfaces from 'azure-devops-node-api/interfaces/common/VsoBaseInterfaces'; +import { ClientVersioningData } from 'azure-devops-node-api/VsoClient'; +import vstsClientBases = require("azure-devops-node-api/ClientApiBases"); + +import * as restclient from 'typed-rest-client/RestClient'; + +export interface SessionRequest { + /** + * Generic property bag to store data about the session + */ + data: { [key: string] : string; }; + /** + * The feed name or id for the session + */ + feed: string; + /** + * The type of session If a known value is provided, the Data dictionary will be validated for the presence of properties required by that type + */ + source: string; +} + +export interface SessionResponse { + /** + * The identifier for the session + */ + sessionId: string; +} + +export class ProvenanceHelper { + public static CreateSessionRequest(feedId: string): SessionRequest { + + var releaseId = tl.getVariable("Release.ReleaseId"); + if (releaseId != null) { + return ProvenanceHelper.CreateReleaseSessionRequest(feedId, releaseId); + } + + var buildId = tl.getVariable("Build.BuildId"); + if (buildId != null) { + return ProvenanceHelper.CreateBuildSessionRequest(feedId, buildId); + } + + throw new Error("Could not resolve Release.ReleaseId or Build.BuildId"); + } + + public static async GetSessionId( + feedId: string, + protocol: string, + baseUrl: string, + handlers: VsoBaseInterfaces.IRequestHandler[], + options: VsoBaseInterfaces.IRequestOptions): Promise { + + const publishPackageMetadata = tl.getInput("publishPackageMetadata"); + let shouldCreateSession = publishPackageMetadata && publishPackageMetadata.toLowerCase() == 'true'; + if (shouldCreateSession) { + const useSessionEnabled = tl.getVariable("Packaging.SavePublishMetadata"); + shouldCreateSession = shouldCreateSession && !(useSessionEnabled && useSessionEnabled.toLowerCase() == 'false') + } + if (shouldCreateSession) { + tl.debug("Creating provenance session to save pipeline metadata. This can be disabled in the task settings, or by setting build variable Packaging.SavePublishMetadata to false"); + const prov = new ProvenanceApi(baseUrl, handlers, options); + const sessionRequest = ProvenanceHelper.CreateSessionRequest(feedId); + try { + const session = await prov.createSession(sessionRequest, protocol); + return session.sessionId; + } catch (error) { + tl.warning(tl.loc("Warning_SessionCreationFailed", JSON.stringify(error))); + } + } + return feedId; + } + + private static CreateReleaseSessionRequest(feedId: string, releaseId: string): SessionRequest { + let releaseData = { + "System.CollectionId": tl.getVariable("System.CollectionId"), + "System.TeamProjectId": tl.getVariable("System.TeamProjectId"), + "Release.ReleaseId": releaseId, + "Release.ReleaseName": tl.getVariable("Release.ReleaseName"), + "Release.DefinitionName": tl.getVariable("Release.DefinitionName"), + "Release.DefinitionId": tl.getVariable("Release.DefinitionId") + } + + var sessionRequest: SessionRequest = { + feed: feedId, + source: "InternalRelease", + data: releaseData + } + + return sessionRequest; + } + + private static CreateBuildSessionRequest(feedId: string, buildId: string): SessionRequest { + let buildData = { + "System.CollectionId": tl.getVariable("System.CollectionId"), + "System.DefinitionId": tl.getVariable("System.DefinitionId"), + "System.TeamProjectId": tl.getVariable("System.TeamProjectId"), + "Build.BuildId": buildId, + "Build.BuildNumber": tl.getVariable("Build.BuildNumber"), + "Build.DefinitionName": tl.getVariable("Build.DefinitionName"), + "Build.Repository.Name": tl.getVariable("Build.Repository.Name"), + "Build.Repository.Provider": tl.getVariable("Build.Repository.Provider"), + "Build.Repository.Id": tl.getVariable("Build.Repository.Id"), + "Build.Repository.Uri": tl.getVariable("Build.Repository.Uri"), + "Build.SourceBranch": tl.getVariable("Build.SourceBranch"), + "Build.SourceBranchName": tl.getVariable("Build.SourceBranchName"), + "Build.SourceVersion": tl.getVariable("Build.SourceVersion") + } + + var sessionRequest: SessionRequest = { + feed: feedId, + source: "InternalBuild", + data: buildData + } + + return sessionRequest; + } +} + +class ProvenanceApi extends vstsClientBases.ClientApiBase { + constructor(baseUrl: string, handlers: VsoBaseInterfaces.IRequestHandler[], options?: VsoBaseInterfaces.IRequestOptions) { + super(baseUrl, handlers, "node-packageprovenance-api", options); + } + + /** + * Creates a session, a wrapper around a feed that can store additional metadata on the packages published to the session. + * + * @param {SessionRequest} sessionRequest - The feed and metadata for the session + * @param {string} protocol - The protocol that the session will target + */ + public async createSession( + sessionRequest: SessionRequest, + protocol: string + ): Promise { + + return new Promise(async (resolve, reject) => { + + let routeValues: any = { + protocol: protocol + }; + + try { + let verData: ClientVersioningData = await this.vsoClient.getVersioningData( + "5.0-preview.1", + "Provenance", + "503B4E54-EBF4-4D04-8EEE-21C00823C2AC", + routeValues); + + let url: string = verData.requestUrl; + + let options: restclient.IRequestOptions = this.createRequestOptions('application/json', + verData.apiVersion); + + let res: restclient.IRestResponse; + res = await this.rest.create(url, sessionRequest, options); + let ret = this.formatResponse(res.result, + null, + false); + + resolve(ret); + } + catch (err) { + reject(err); + } + }); + } +} \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/nuget/utility.ts b/extensions/azuredevops/canaryUpdater/nuget/utility.ts new file mode 100644 index 0000000..3fa5370 --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/nuget/utility.ts @@ -0,0 +1,44 @@ +import * as tl from "azure-pipelines-task-lib/task"; +import * as locationUtilities from "./locationUtilities"; + +export enum ProtocolType { + NuGet, + Maven, + Npm, + PyPi +} + +export enum RegistryType { + npm, + NuGetV2, + NuGetV3, + PyPiSimple, + PyPiUpload +} + +interface RegistryLocation { + apiVersion: string, + area: string, + locationId: string +}; + +export async function getNuGetFeedRegistryUrl( + packagingCollectionUrl: string, + feedId: string, + accessToken?: string, + useSession?: boolean): Promise +{ + // If no version is received, V3 is assumed + const registryType: RegistryType = RegistryType.NuGetV3; + + const overwritePackagingCollectionUrl = tl.getVariable("NuGet.OverwritePackagingCollectionUrl"); + if (overwritePackagingCollectionUrl) { + tl.debug("Overwriting packaging collection URL"); + packagingCollectionUrl = overwritePackagingCollectionUrl; + } else if (!packagingCollectionUrl) { + const collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri"); + packagingCollectionUrl = collectionUrl; + } + + return await locationUtilities.getFeedRegistryUrl(packagingCollectionUrl, registryType, feedId, accessToken, useSession); +} \ No newline at end of file diff --git a/extensions/azuredevops/canaryUpdater/package.json b/extensions/azuredevops/canaryUpdater/package.json new file mode 100644 index 0000000..17a5435 --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/package.json @@ -0,0 +1,22 @@ +{ + "name": "canary.updater", + "description": "Task allowing easy update of Canary build", + "scripts": { + "test": "echo 'Hello world" + }, + "license": "ISC", + "devDependencies": { + "azure-pipelines-task-lib": "^2.7.7", + "azure-devops-node-api": "^9.0.1" + }, + "repository": { + "type": "git", + "url": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "keywords": [ + "VSTS", + "Build", + "Task" + ], + "author": "unoplatform" +} diff --git a/extensions/azuredevops/canaryUpdater/task.json b/extensions/azuredevops/canaryUpdater/task.json new file mode 100644 index 0000000..ad70a4e --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/task.json @@ -0,0 +1,392 @@ +{ + "id": "95170664-b64e-4571-9cd4-936b3a626292", + "name": "nventiveCanaryUpdater", + "friendlyName": "Canary Updater", + "description": "A task to update a Canary build. The update is split in 3 phases :\n - Optional: merge of the canary branch with a given target branch (usually master) \n - Update of the NuGet packages; the versions used by the updater are calculated through the branch name (canaries/dev for dev, canaries/beta+dev for beta and stable, etc.); stable is always appended to the versions. \n - Optional: commit the changes and push the changes.", + "helpMarkDown": "[unoplatform](http://www.unoplatform.com/)", + "category": "Build", + "author": "unoplatform", + "version": { + "Major": 0, + "Minor": 0, + "Patch": 0 + }, + "visibility": [ + "Build" + ], + "demands": [ + "DotNetCore" + ], + "instanceNameFormat": "Canary Update", + "groups": [ + { + "name": "git", + "displayName": "Git options", + "isExpanded": true + }, + { + "name": "updater", + "displayName": "NuGet Updater ", + "isExpanded": false + }, + { + "name": "log", + "displayName": "Log Options", + "isExpanded": false + } + ], + "inputs": [ + { + "name": "solution", + "type": "filePath", + "label": "Solution to update", + "defaultValue": "", + "required": true, + "helpMarkDown": "The solution to update", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "usePrivateFeed", + "type": "boolean", + "label": "Use packages from this Azure DevOps account", + "defaultValue": true, + "required": true, + "helpMarkDown": "Indicates whether to use packages from this Azure DevOps account", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "nugetFeed", + "type": "pickList", + "label": "Source Feed", + "defaultValue": "", + "required": true, + "visibleRule": "usePrivateFeed = true", + "helpMarkDown": "The NuGet feed from which to update the packages", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "useNuGetOrg", + "type": "boolean", + "label": "Use packages from NuGet.org", + "defaultValue": true, + "required": true, + "helpMarkDown": "Indicates whether packages from NuGet.org should be updated", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "mergeBranch", + "type": "boolean", + "label": "Merge with working branch", + "groupName": "git", + "defaultValue": true, + "required": true, + "helpMarkDown": "Merge the code from the current branch with a working branch.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "branchToMerge", + "type": "string", + "label": "Branch to merge", + "groupName": "git", + "defaultValue": "master", + "required": true, + "visibleRule": "mergeBranch = true", + "helpMarkDown": "The branch to merge changes from.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "isBranchToMergeAway", + "type": "boolean", + "label": "Branch to merge is in another repository", + "groupName": "git", + "defaultValue": false, + "required": true, + "visibleRule": "mergeBranch = true", + "helpMarkDown": "", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "mergeRepositoryConnection", + "type": "connectedService:git", + "label": "Repository", + "groupName": "git", + "defaultValue": "", + "required": true, + "visibleRule": "isBranchToMergeAway = true", + "helpMarkDown": "Service connection to connect to the repository where the target branch is located.", + "properties": { + "EditableOptions": "True" + } + }, + { + "name": "pushMergeBranch", + "type": "boolean", + "label": "Push merge branch", + "groupName": "git", + "defaultValue": false, + "required": false, + "visibleRule": "isBranchToMergeAway = true", + "helpMarkDown": "Push the branch used for the merge to the current repository. The build service must be given the right to contribute and create branches in the repository.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "pushBranch", + "type": "boolean", + "label": "Push updated branch", + "groupName": "git", + "defaultValue": false, + "required": true, + "helpMarkDown": "Push the updated branch to the repository. The build service must be given the right to contribute and create branches in the repository.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "gitUserName", + "type": "string", + "label": "Git user name", + "groupName": "git", + "required": true, + "visibleRule": "pushBranch = true", + "helpMarkDown": "The email for Git to use when executing operations.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "gitUserEmail", + "type": "string", + "label": "Git user email", + "groupName": "git", + "required": true, + "visibleRule": "pushBranch = true", + "helpMarkDown": "The name for Git to use when executing operations.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "summaryFile", + "type": "filePath", + "label": "Summary file", + "groupName": "log", + "defaultValue": "Canary.md", + "required": false, + "helpMarkDown": "Path to a file where to write the package update and git merge summary", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "resultFile", + "type": "filePath", + "label": "Result file", + "groupName": "log", + "defaultValue": "result.json", + "required": false, + "helpMarkDown": "Path to a file where to write the result of the update", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "nugetUpdaterVersion", + "type": "string", + "label": "NuGet Updater version", + "groupName": "updater", + "defaultValue": "2.1.1", + "required": true, + "helpMarkDown": "The of the NuGet updater to use. See https://www.nuget.org/packages/unoplatform.nuget.updater.tool for the list of available versions", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "nugetVersion", + "type": "string", + "label": "Target Versions", + "groupName": "updater", + "defaultValue": "", + "required": false, + "helpMarkDown": "The versions to update packages to; use comma-separated values; defaults to the name of the branch + stable", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "allowDowngrade", + "type": "boolean", + "label": "Allow downgrade", + "groupName": "updater", + "defaultValue": false, + "required": false, + "helpMarkDown": "Indicates whether the packages can be downgraded if the matching version is lower", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "strict", + "type": "boolean", + "label": "Use strict update", + "groupName": "updater", + "defaultValue": true, + "required": false, + "helpMarkDown": "Indicates whether the version found should only contain the target tag (ie. dev) or can contain other tags as well (ie. dev.test)", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "packageAuthor", + "type": "string", + "label": "Package author", + "groupName": "updater", + "defaultValue": "", + "required": false, + "helpMarkDown": "Filters the packages to update to the ones from a specific author; only applies to NuGet.org packages", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "ignorePackages", + "type": "string", + "label": "Ignore Packages", + "groupName": "updater", + "defaultValue": "", + "required": false, + "helpMarkDown": "Indicates which packages (separated by ';') to exclude from the update", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "updatePackages", + "type": "string", + "label": "Update Packages", + "groupName": "updater", + "defaultValue": "", + "required": false, + "helpMarkDown": "Indicates which packages (separated by ';') to update. If null, all found pacakges will be updated.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "additionalPublicSources", + "type": "multiline", + "label": "Additional Public Sources", + "groupName": "updater", + "defaultValue": "", + "required": false, + "helpMarkDown": "Additional public package sources from where to update; separate sources on different lines", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "useVersionOverrides", + "type": "boolean", + "label": "Use version overrides", + "groupName": "updater", + "defaultValue": false, + "required": false, + "helpMarkDown": "Whether or not to use a version overrides file", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "versionOverridesFile", + "type": "string", + "label": "Version overrides file", + "groupName": "updater", + "defaultValue": null, + "required": false, + "visibleRule": "useVersionOverrides = true", + "helpMarkDown": "Path/URL to a file to use for the version overrides", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "useUpdateProperties", + "type": "boolean", + "label": "Update properties", + "groupName": "updater", + "defaultValue": false, + "required": false, + "helpMarkDown": "Whether or not to use a update properties file", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "updatePropertiesFile", + "type": "string", + "label": "Update properties file", + "groupName": "updater", + "defaultValue": null, + "required": false, + "visibleRule": "useUpdateProperties = true", + "helpMarkDown": "Path/URL to a file to use for updating properties", + "properties": { + "DisableManageLink": "True" + } + } + ], + "dataSourceBindings": [ + { + "target": "nugetFeed", + "endpointId": "tfs:feed", + "endpointUrl": "{{endpoint.url}}/_apis/packaging/feeds", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\" : \"{{{id}}}\", \"DisplayValue\" : \"{{{name}}}\" }" + }, + { + "endpointId": "tfs:teamfoundation", + "target": "targetBranchProject", + "endpointUrl": "{{endpoint.url}}/_apis/projects?$skip={{skip}}&$top=1000", + "resultSelector": "jsonpath:$.value[?(@.state=='wellFormed')]", + "resultTemplate": "{ \"Value\" : \"{{{id}}}\", \"DisplayValue\" : \"{{{name}}}\" }", + "callbackContextTemplate": "{\"skip\": \"{{add skip 1000}}\"}", + "callbackRequiredTemplate": "{{isEqualNumber result.count 1000}}", + "initialContextTemplate": "{\"skip\": \"0\"}" + }, + { + "endpointId": "tfs:teamfoundation", + "target": "targetBranchRepository", + "endpointUrl": "{{endpoint.url}}/{{project}}/_apis/git/repositories", + "resultSelector": "jsonpath:$.value[*]", + "parameters": { + "project": "$(targetBranchProject)" + }, + "resultTemplate": "{ \"Value\" : \"{{{remoteUrl}}}\", \"DisplayValue\" : \"{{{name}}}\" }" + } + ], + "sourceDefinitions": [ + ], + "execution": + { + "Node": { + "target": "task.js" + } + } +} diff --git a/extensions/azuredevops/canaryUpdater/task.ts b/extensions/azuredevops/canaryUpdater/task.ts new file mode 100644 index 0000000..4782180 --- /dev/null +++ b/extensions/azuredevops/canaryUpdater/task.ts @@ -0,0 +1,438 @@ +import tl = require("azure-pipelines-task-lib"); +import fs = require('fs'); +import * as nutil from "./nuget/utility"; +import * as pkgLocationUtils from "./nuget/locationUtilities"; +import { IExecOptions } from "azure-pipelines-task-lib/toolrunner"; +import { Writable } from "stream"; +import path = require("path"); + +const canaryUpdateVariableName: string = "IsCanaryUpdated"; +const canaryBranchPrefix: string = "refs/heads/canaries/"; +const mergedBranchFolder: string = "build"; + +let targetVersions : string[] = []; +let pushedBranchFolder: string = ""; +let mergeOutput = "\n# Merge summary\n```\n"; + +let isGitUserSet = false; + +async function run() { + console.log("Updating Canary build"); + + let isMergeSuccessful = await gitMerge(); + + if (!isMergeSuccessful) { + tl.setResult(tl.TaskResult.Failed, "Failed to complete the merge"); + return; + } + + let isNugetUpdateSuccessful = await nugetUpdate(); + + if (!isNugetUpdateSuccessful) { + tl.setResult(tl.TaskResult.Failed, "Failed to update nuget packages"); + return; + } + + let isWriteSummarySuccessful = await writeMergeSummary(); + + if(!isWriteSummarySuccessful) { + tl.setResult(tl.TaskResult.Failed, "Failed to write canary update summary to " + tl.getInput("summaryFile")); + return; + } + + let isBranchPushSuccessful = await pushBranch(); + + if (!isBranchPushSuccessful) { + tl.setResult(tl.TaskResult.Failed, "Failed to push new branch. Make sure that the Build Service has the proper rights on the target repository."); + return; + } + + tl.setVariable(canaryUpdateVariableName, "True"); + tl.setResult(tl.TaskResult.Succeeded, "Canary Updated"); +} + +async function setGitUser(): Promise { + + if(isGitUserSet) { + return true; + } + let email = tl.getInput("gitUserEmail"); + let name = tl.getInput("gitUserName"); + + let gitTool = tl.tool(tl.which("git")) + .arg([ "config", "user.email", email ]); + + await gitTool.exec(); + + gitTool = tl.tool(tl.which("git")) + .arg(["config", "user.name", name]); + + await gitTool.exec(); + + isGitUserSet = true; + + return true; +} + +async function gitMerge(): Promise { + try { + if(!tl.getBoolInput("mergeBranch")) { + return true; + } + + await setGitUser(); + + let remote = "origin"; + + let isMergeBranchAway = tl.getBoolInput("isBranchToMergeAway"); + let branch = tl.getInput("branchToMerge"); + + if(isMergeBranchAway) { + remote = "merge"; + try { + await tl.tool(tl.which("git")) + .arg([ "remote", "remove", remote]) + .exec(); + } catch (error) { + //remote doesn't exist + } + + await tl.tool(tl.which("git")) + .arg([ "remote", "add", remote, getMergeRepositoryUrl()]) + .exec(); + + await tl.tool(tl.which("git")) + .arg(["fetch", "--prune", "--progress", remote, branch ]) + .exec(); + } else if(tl.getVariable("Build.Repository.Provider") == "TfsGit") { //Azure DevOps repository + let accessToken = tl.getVariable("System.AccessToken"); + + await tl.tool(tl.which("git")) + .line('-c http.extraheader="AUTHORIZATION: bearer ' + accessToken + '"') + .arg(["fetch", "--prune", "--progress", remote, branch ]) + .exec(); + } else { + await tl.tool(tl.which("git")) + .arg(["fetch", "--prune", "--progress", remote, branch ]) + .exec(); + } + + if(isMergeBranchAway && tl.getBoolInput("pushMergeBranch")) { + + console.log("Pushing " + branch + " to origin"); + + await tl.tool(tl.which("git")) + .arg(["push", "--force", "origin", remote + "/" + branch + ":refs/heads/" + branch]) + .exec(); + } + + console.log("Merging with " + branch); + + let mergeTool = tl.tool(tl.which("git")) + .arg([ "merge", remote + "/" + branch, "-s", "recursive", "-X", "theirs" ]); + + let returnCode = await mergeTool.exec({ outStream: getConsoleStream(true) }); + + mergeOutput += "```\n"; + + return returnCode == 0; + } + catch (ex) { + tl.error(ex); + + return false; + } +} + +function getMergeRepositoryUrl(): string { + let connection = tl.getInput("mergeRepositoryConnection", true); + let url = encodeURI(tl.getEndpointUrl(connection, false)); + let token = tl.getEndpointAuthorizationParameter(connection, "password", false); + //Including the token in the remote url + return url.replace(/https:\/\/.*@/g, "https://" + token + "@"); +} + +async function nugetUpdate(): Promise { + let solution = tl.getInput("solution"); + let nugetFeed = await getFeedUrl(); + let packageAuthor = tl.getInput("packageAuthor"); + let allowDowngrade = tl.getBoolInput("allowDowngrade"); + let useNuGetOrg = tl.getBoolInput("useNuGetOrg"); + let strict = tl.getBoolInput("strict"); + let useVersionOverrides = tl.getBoolInput("useVersionOverrides"); + let versionOverridesFile = tl.getInput("versionOverridesFile"); + let useUpdateProperties = tl.getBoolInput("useUpdateProperties"); + let updatePropertiesFile = tl.getInput("updatePropertiesFile"); + let summaryFile = tl.getInput("summaryFile"); + let resultFile = tl.getInput("resultFile"); + + targetVersions = await getTargetVersions(); + + if (targetVersions.length > 0) { + console.log("Target versions: [" + targetVersions.join(",") + "]"); + } + + try { + await installNuGetUpdater(); + + console.log("Executing Nuget.Updater task"); + + let summaryFileArg = null; + let versionOverridesFileArg = null; + let updatePropertiesFileArg = null; + let resultFileArg = null; + let feedArg = null; + let downgradeArg = null; + let useNuGetOrgArg = null; + let strictArg = null; + + if(summaryFile) { + summaryFileArg = "--outputFile=" + summaryFile; + } + + if(useVersionOverrides && versionOverridesFile) { + versionOverridesFileArg = "--versionOverrides=" + versionOverridesFile; + } + + if (useUpdateProperties && updatePropertiesFile) { + updatePropertiesFileArg = "--updateProperties=" + updatePropertiesFile; + } + + if (resultFile) { + resultFileArg = "--result=" + resultFile; + } + + if(nugetFeed) { + feedArg = "--feed=" + nugetFeed + "|" + tl.getVariable("System.AccessToken"); + } + + if(allowDowngrade) { + downgradeArg = "--allowDowngrade"; + } + + if(useNuGetOrg) { + useNuGetOrgArg = "--useNuGetorg"; + } + + if(strict) { + strictArg = "--strict"; + } + + let updateTool = tl + .tool(path.join(tl.getVariable("Agent.TempDirectory"), "nugetupdater")) + .arg([ + "--solution=" + solution, + feedArg, + useNuGetOrgArg, + "--packageAuthor=" + packageAuthor, + downgradeArg, + summaryFileArg, + resultFileArg, + strictArg, + versionOverridesFileArg, + updatePropertiesFileArg + ] + .concat(targetVersions.map(v => "--version=" + v)) + .concat(getListArgument("ignorePackages", ";", "ignore")) + .concat(getListArgument("updatePackages", ";", "update")) + .concat(getListArgument("additionalPublicSources", "\n", "feed")) + .filter(isNotNull) + ); + + await updateTool.exec({ outStream: getConsoleStream(false) }); + + return true; + } + catch (ex) { + tl.error(ex); + return false; + } +} + +function isNotNull(element, index, array) { + return element != null; +} + +async function getTargetVersions(): Promise { + var inputTargetVersion = tl.getDelimitedInput("nugetVersion", ","); + + if(!inputTargetVersion || inputTargetVersion.length == 0) { + try + { + let branch = tl.getVariable("Build.SourceBranch"); + + if(branch.startsWith(canaryBranchPrefix)) { + let branchParts = branch.replace(canaryBranchPrefix, "").split("/"); + let branchName = branchParts.pop(); + + //if there's a folder before the actual branch name, we keep it aside to push under the same path + pushedBranchFolder = branchParts.join("/"); + + let targetVersions = []; + + if(branchName.includes("+")) { + targetVersions = branchName.split("+"); + } else { + targetVersions = [ branchName ]; + } + + if(!targetVersions.includes("stable")) { + targetVersions.push("stable"); + } + + return targetVersions; + } + + throw "Invalid source branch. This task must be used on a branch named canaries/{target package version}"; + } + catch(ex) + { + tl.error(ex); + return []; + } + } + + return inputTargetVersion; +} + +function getListArgument(inputName: string, inputDelimiter: string, argumentName: string): string[] { + var input = tl.getDelimitedInput(inputName, inputDelimiter); + + if(input && input.length > 0) { + return input.map(i => "--" + argumentName + "=" + i); + } + + return []; +} + +async function writeMergeSummary(): Promise { + + let summaryFile = tl.getInput("summaryFile"); + + if(!summaryFile || summaryFile == "") { + return true; + } + + try { + if(fs.existsSync(summaryFile)) { + const summaryFileStats = fs.statSync(summaryFile); + var file = fs.createWriteStream(summaryFile, { flags: "r+", autoClose: true, start: summaryFileStats.size }); + + file.write(mergeOutput); + } else { + var file = fs.createWriteStream(summaryFile, { autoClose: true }); + + file.write(mergeOutput); + } + + return true; + } catch(ex) + { + tl.error(ex); + return false; + } +} + +async function pushBranch(): Promise { + try { + if(!tl.getBoolInput("pushBranch")) { + return true; + } + + await setGitUser(); + + //for a dev canary branch name will be "canaries/build/dev/build_XXXXXXXXXX.X" + let branchName = "canaries/" + mergedBranchFolder + "/"; + + if(pushedBranchFolder != "") { + branchName += pushedBranchFolder + "/"; + } + + branchName += targetVersions[0] + "/" + mergedBranchFolder + "_" + tl.getVariable("Build.BuildNumber"); + + let summaryFile = tl.getInput("summaryFile"); + if(summaryFile) { + await tl.tool(tl.which("git")) + .arg([ "add", summaryFile ]) + .exec(); + } + + let resultFile = tl.getInput("resultFile"); + if(resultFile) { + await tl.tool(tl.which("git")) + .arg([ "add", resultFile ]) + .exec(); + } + + let commitMessage = "Updated packages to " + targetVersions.join(","); + + if(tl.getBoolInput("mergeBranch")){ + commitMessage = commitMessage + " and merged " + tl.getInput("branchToMerge"); + } + + let commitTool = tl.tool(tl.which("git")) + .arg([ "commit", "-am", commitMessage ]); + + await commitTool.exec(); + + let pushTool = tl.tool(tl.which("git")) + .arg([ "push", "origin", "HEAD:refs/heads/" + branchName ]); + + await pushTool.exec(); + + return true; + } + catch (ex) { + tl.error(ex); + + return false; + } +} + +async function installNuGetUpdater(): Promise { + let toolVersion = tl.getInput("nugetUpdaterVersion"); + let dotnetPath = tl.which("dotnet"); + let dotnet = tl.tool(dotnetPath); + + let installationTool = dotnet + .arg([ "tool", "install", "unoplatform.NuGet.Updater.Tool", "--version", toolVersion, "--tool-path", tl.getVariable("Agent.TempDirectory"), "--ignore-failed-sources" ]); + + await installationTool.exec({ outStream: getConsoleStream(false) }); + + return true; +} + +function getConsoleStream(writeToMergeOutput: boolean) : Writable { + + var stream = new Writable(); + + stream._write = (chunk, e, next) => { + let line = chunk.toString(); + console.log(line); + if(writeToMergeOutput) { + mergeOutput += line + "\n"; + } + next(); + }; + + return stream; +} + +async function getFeedUrl(): Promise { + var feed = tl.getInput("nugetFeed"); + + if(!feed) { + return null; + } + + try { + let packagingLocation = await pkgLocationUtils.getPackagingUris(pkgLocationUtils.ProtocolType.NuGet); + return await nutil.getNuGetFeedRegistryUrl(packagingLocation.DefaultPackagingUri, feed, pkgLocationUtils.getSystemAccessToken()); + } + catch (error) { + tl.setResult(tl.TaskResult.Failed, error.message); + return null; + } +} + +run(); \ No newline at end of file diff --git a/extensions/azuredevops/dependencies.js b/extensions/azuredevops/dependencies.js new file mode 100644 index 0000000..595ac8a --- /dev/null +++ b/extensions/azuredevops/dependencies.js @@ -0,0 +1,46 @@ +const path = require('path') +const fs = require('fs') +const child_process = require('child_process') + +const root = process.cwd() +npm_install_recursive(root) + +// Since this script is intended to be run as a "preinstall" command, +// it will be `npm install` inside root in the end. +console.log('===================================================================') +console.log(`Performing "npm install" inside root folder`) +console.log('===================================================================') + +function npm_install_recursive(folder) { + "use strict"; + const has_package_json = fs.existsSync(path.join(folder, 'package.json')) + + if (!has_package_json && path.basename(folder) !== 'code') { + return + } + + // Since this script is intended to be run as a "preinstall" command, + // skip the root folder, because it will be `npm install`ed in the end. + if (folder !== root && has_package_json) { + console.log('===================================================================') + console.log(`Performing "npm install" inside ${folder === root ? 'root folder' : './' + path.relative(root, folder)}`) + console.log('===================================================================') + + npm_install(folder) + } + + for (let subfolder of subfolders(folder)) { + npm_install_recursive(subfolder) + } +} + +function npm_install(where) { + child_process.execSync('npm install', { cwd: where, env: process.env, stdio: 'inherit' }) +} + +function subfolders(folder) { + return fs.readdirSync(folder) + .filter(subfolder => fs.statSync(path.join(folder, subfolder)).isDirectory()) + .filter(subfolder => subfolder !== 'node_modules' && subfolder[0] !== '.') + .map(subfolder => path.join(folder, subfolder)) +} \ No newline at end of file diff --git a/extensions/azuredevops/logo.png b/extensions/azuredevops/logo.png new file mode 100644 index 0000000..1a199b9 Binary files /dev/null and b/extensions/azuredevops/logo.png differ diff --git a/extensions/azuredevops/overview.md b/extensions/azuredevops/overview.md new file mode 100644 index 0000000..b2c5f04 --- /dev/null +++ b/extensions/azuredevops/overview.md @@ -0,0 +1,7 @@ +This extensions gives access the following build tasks : + +# Canary Updater +A task allowing to automatically update NuGet packages to the latest version. +The canary process is meant to be run in its own branch and is a two step process: +- Merge a working branch in the canary branch +- Use [NuGet.Updater](https://github.com/unoplatform/NuGet.Updater/blob/develop/src/NvGet.Tools.Updater/Readme.md) to update the packages to the latest matching version diff --git a/extensions/azuredevops/package.json b/extensions/azuredevops/package.json new file mode 100644 index 0000000..023e0be --- /dev/null +++ b/extensions/azuredevops/package.json @@ -0,0 +1,27 @@ +{ + "name": "unoplatform.buildtools", + "version": "0.1.0", + "description": "Build tools for unoplatform", + "scripts": { + "preinstall": "node dependencies.js" + }, + "license": "ISC", + "devDependencies": { + "@types/node": "^12.6.9", + "@types/q": "^1.5.2", + "azure-pipelines-task-lib": "^2.7.7", + "typescript": "^3.5.3", + "azure-devops-node-api": "^9.0.1", + "typed-rest-client": "^1.5.0" + }, + "repository": { + "type": "git", + "url": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "keywords": [ + "VSTS", + "Build", + "Task" + ], + "author": "unoplatform" +} diff --git a/extensions/azuredevops/src/.vs/NvGet/DesignTimeBuild/.dtbcache.v2 b/extensions/azuredevops/src/.vs/NvGet/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..7dd7d31 Binary files /dev/null and b/extensions/azuredevops/src/.vs/NvGet/DesignTimeBuild/.dtbcache.v2 differ diff --git a/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.metadata.v8.bin b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.metadata.v8.bin new file mode 100644 index 0000000..ddc2953 Binary files /dev/null and b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.metadata.v8.bin differ diff --git a/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.projects.v8.bin b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.projects.v8.bin new file mode 100644 index 0000000..010da1f Binary files /dev/null and b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.projects.v8.bin differ diff --git a/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.strings.v8.bin b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.strings.v8.bin new file mode 100644 index 0000000..c4f6ddc Binary files /dev/null and b/extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.strings.v8.bin differ diff --git a/extensions/azuredevops/tsconfig.json b/extensions/azuredevops/tsconfig.json new file mode 100644 index 0000000..fbe1f99 --- /dev/null +++ b/extensions/azuredevops/tsconfig.json @@ -0,0 +1,53 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation: */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + // "strict": true /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": [ "node", "q" ] /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} \ No newline at end of file diff --git a/extensions/azuredevops/vss-extension.json b/extensions/azuredevops/vss-extension.json new file mode 100644 index 0000000..202a4b1 --- /dev/null +++ b/extensions/azuredevops/vss-extension.json @@ -0,0 +1,51 @@ +{ + "manifestVersion": 1, + "id": "unoplatform-build-tools", + "version": "0.0.0", + "name": "unoplatform Build Tools", + "description": "The Canary Updater tool by Uno Platform", + "publisher": "unoplatform", + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "categories": [ + "Build and release" + ], + "icons": { + "default": "logo.png" + }, + "content": { + "details": { + "path": "overview.md" + } + }, + "repository": { + "type": "git", + "uri": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "contributions": [ + { + "id": "unoplatform.canaryUpdater", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "canaryUpdater" + } + } + ], + "scopes": [ + "vso.work" + ], + "files": [ + { + "path": "canaryUpdater" + }, + { + "path": "node_modules" + } + ] +}