From 395f76bf1c2ae9d91bd0ab26344db1101bd43efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20L=C3=A9vesque?= Date: Wed, 2 Aug 2023 12:11:35 -0400 Subject: [PATCH 01/15] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/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. From 20eb5119fb662171d8055a4f87fa4c7ee19403be Mon Sep 17 00:00:00 2001 From: Jean-Philippe Levesque Date: Wed, 2 Aug 2023 12:19:17 -0400 Subject: [PATCH 02/15] chore: Import code. --- .azure-pipelines.yml | 36 ++ .gitignore | 116 +++++ GitVersion.yml | 5 + canaryUpdater/Readme.md | 18 + canaryUpdater/icon.png | Bin 0 -> 20423 bytes canaryUpdater/nuget/commandhelper.ts | 54 +++ canaryUpdater/nuget/locationUtilities.ts | 249 ++++++++++ canaryUpdater/nuget/provenance.ts | 167 +++++++ canaryUpdater/nuget/utility.ts | 44 ++ canaryUpdater/package.json | 22 + canaryUpdater/task.json | 367 +++++++++++++++ canaryUpdater/task.ts | 430 ++++++++++++++++++ dependencies.js | 46 ++ logo.png | Bin 0 -> 20423 bytes overview.md | 25 + package.json | 27 ++ releaseNotesCompiler/Readme.md | 13 + releaseNotesCompiler/icon.png | Bin 0 -> 20423 bytes releaseNotesCompiler/package.json | 22 + releaseNotesCompiler/task.json | 130 ++++++ releaseNotesCompiler/task.ts | 191 ++++++++ tsconfig.json | 53 +++ vss-extension.json | 77 ++++ websiteVersion/Readme.md | 11 + .../azure-arm-rest/AzureServiceClient.ts | 275 +++++++++++ .../azure-arm-rest/azure-arm-common.ts | 283 ++++++++++++ .../azure-arm-rest/azure-arm-endpoint.ts | 152 +++++++ .../azure-arm-rest/azure-arm-storage.ts | 201 ++++++++ websiteVersion/azure-arm-rest/azureModels.ts | 355 +++++++++++++++ websiteVersion/azure-arm-rest/constants.ts | 29 ++ websiteVersion/azure-arm-rest/webClient.ts | 117 +++++ websiteVersion/icon.png | Bin 0 -> 20423 bytes websiteVersion/package.json | 29 ++ websiteVersion/task.json | 80 ++++ websiteVersion/task.ts | 266 +++++++++++ 35 files changed, 3890 insertions(+) create mode 100644 .azure-pipelines.yml create mode 100644 .gitignore create mode 100644 GitVersion.yml create mode 100644 canaryUpdater/Readme.md create mode 100644 canaryUpdater/icon.png create mode 100644 canaryUpdater/nuget/commandhelper.ts create mode 100644 canaryUpdater/nuget/locationUtilities.ts create mode 100644 canaryUpdater/nuget/provenance.ts create mode 100644 canaryUpdater/nuget/utility.ts create mode 100644 canaryUpdater/package.json create mode 100644 canaryUpdater/task.json create mode 100644 canaryUpdater/task.ts create mode 100644 dependencies.js create mode 100644 logo.png create mode 100644 overview.md create mode 100644 package.json create mode 100644 releaseNotesCompiler/Readme.md create mode 100644 releaseNotesCompiler/icon.png create mode 100644 releaseNotesCompiler/package.json create mode 100644 releaseNotesCompiler/task.json create mode 100644 releaseNotesCompiler/task.ts create mode 100644 tsconfig.json create mode 100644 vss-extension.json create mode 100644 websiteVersion/Readme.md create mode 100644 websiteVersion/azure-arm-rest/AzureServiceClient.ts create mode 100644 websiteVersion/azure-arm-rest/azure-arm-common.ts create mode 100644 websiteVersion/azure-arm-rest/azure-arm-endpoint.ts create mode 100644 websiteVersion/azure-arm-rest/azure-arm-storage.ts create mode 100644 websiteVersion/azure-arm-rest/azureModels.ts create mode 100644 websiteVersion/azure-arm-rest/constants.ts create mode 100644 websiteVersion/azure-arm-rest/webClient.ts create mode 100644 websiteVersion/icon.png create mode 100644 websiteVersion/package.json create mode 100644 websiteVersion/task.json create mode 100644 websiteVersion/task.ts diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml new file mode 100644 index 0000000..c3cf7e9 --- /dev/null +++ b/.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/.gitignore b/.gitignore new file mode 100644 index 0000000..d5a06f7 --- /dev/null +++ b/.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/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..7b42093 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,5 @@ +assembly-versioning-scheme: MajorMinorPatch +mode: Mainline +next-version: 5.0 +ignore: + sha: [] diff --git a/canaryUpdater/Readme.md b/canaryUpdater/Readme.md new file mode 100644 index 0000000..4b93180 --- /dev/null +++ b/canaryUpdater/Readme.md @@ -0,0 +1,18 @@ +# nventive Canary Updater task + +This task is meant to be used by nventive 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/nventive/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/canaryUpdater/icon.png b/canaryUpdater/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1a199b961ba0e18eaebb962cb88f921118642bf3 GIT binary patch literal 20423 zcmdSAbyS?o(l~p?- z?)$E9t^3Cv)-z8}b^S`Zs;iqv-l!-^BO?$X0001FSs6*Sm-Fzi0}kfpyH=+z@8tyH zswOQCsGA@S#){(VPR0J@*wBZ1-5EuZ+Uo0Y-5)%_ z7algi%Qt}h<#2&u|E-Pn(oT)E20;CRt#~>A`pCQ-FH(O$i+m@h@8ofPd*5 zE`a=3`lH`)sZt&6t?paxe;4wn97(XNnTxfPo3*0@*)O>!rjG7zLKGCg6#e!2-A=HV^}jSZ zxc=eRi-WAcdRW<5K&<}+GdKIoJSTS-yWfm6H)93cf$hN#ZmuslwtvLE43eVaU(kQk z=Vjvbn>u^@e?+>vz4CbR;g2!-N2IHUw-cCE4eaXZ?qUXhITpV12zlG)R z~{xz?Oon|Dyhl^|#pb%UHX*Il6fN&C?fuf6IP#u=BrD|Lm}{{w<50Ok7;Szd|fT z@n?+xO8dXE`WNQ6RN;4Zv~cq@aRG~2zT|=M|5R55S^o+3FYEtO@P~9yEAX$eWoKgs zaWb=WYy4J<7(whW)#y)Azp?yx3Dyp-ZYB<9U|C7wmyuzywl?Pn^KqGQa0lek&1aj~NkUqS}v zVFq!4dD+;xLFR0r|EsvUnt1$QM(y`l{`-s)Wc_b5Q1JH-WfyBpYX=iMNk=pHUuFD{ z%ryUBXXbwu^RIaPq3UlceqScd9nD_U{8bM5xp_b!UJh<^W*$CHUS>8fPE%$R9uOBZ z2gl1i;IuGf=Qd;i)1ki!`#%)P{-3k#e;2Laefb;wZ=!!K6a2qwke&Ol`K9S#?I!$U z%|DU<6ZJC2FC%O6+q7i+1M+v;-}qlF;8%1sx3=(>e3`xfwb=Xz+kXg<_-*R{j{^S2 z_D=#FT{IjW?fw@<^p`Qe$@&BFAHo!0c0%SRZYKYuJ^#k|HzEH@kl$9um*D{ma|p8j z6Zs!ef2NAqOX2;s4!Qn&vHNWu5;w8@cj~X|@keQV@sH)#d|=}PnSj~MxR^QF%w8%r z2L}f;pBX0`GaDNxpEEdAaeOd92w| zfn@I$8x&G7l#D5giLB{38;mOvoG~u~6zN_A9~2w}rui7r68m5u<9kl=Z2U>#+55E? z*4wW8L-&O_xB2f`fj%Fr?gBaHpYPt^A73B)U$0(2^?Ys>)WSg}-;a9jB6v-(p4727 zWH`1LebN%da`$oOTCb~iVqLS_f;GST;mI{0sHg~0Pb{OZJ;9oz3{j8u6Jj$hB;etF zSrBUCdk4dA7eo3D$N@&^#~XV^2vRcp)sBFj4z(WGC~bth)8%ZSNqvj!QdTF$l&xR= zrRyipw8>Yb+IMdsS2TsSAN6<63Dz~cK2NOM-Wu$j=ig&lStHy4tPgh25xbXObTuwv zUClcF?`d9nuuFDfZATl1=hxX?j9uEti$`~DqkC2P)qZt1bq@vy?VIgqo~0)~$D`|k z$La;OFV>gTzFnx#(SA{UDkmlq@LIhGR^~NNy%FF4;O*o&8Oz~^qp7+ZL-ZrAKy5$m zTm4gmOHZK}`3(7t{tK?!PjxphuG&6!4z3^F-5%UyEvmh!SyDT@&*Bk)_6y?lMZDcv z{@>H=?gWRmD5q?n41YZQ=1pGv%(~t2S$21IfZ^B)mw)}`xn6)smvV5~uK<-fkaRn} z{uP(ktH6M#mr#%+01~opvGdwg7%ksG00c+=tMhQ zTHNFIR1zvkR$d8?I)kmBP&77cKXEJDtT&z94>1%^HJlTJVDl7u>( zGs>e6lL71+`jKNONs0?YBtc)IHcR1DEC^EpMYlSSX$>0tSi&>@sIy7d;#lQN9Rl+dmACc8yM&xD&{rA1MY%u~xn_|Y@r56EJVHEJM@5zQF( zT#A#^#e6Doa;Zl2rWOYv&9iRzqXD(hpsiB@3?)*V)nQdZaJP|i}zZ_6j{X@0`ee_28|$e%N!T6Lo4x8c~4&xk6||JRn2zLa(G5D}|J1I(1R#Gsvco zE2xMHUO)owi*@m1c6N-ZC}GW@m++Zol|iTt+Yo4FnlNS@sQopm6ktWd14xZ5QLSL58807NnzOWZcZLgL4X6+C?DGlAaWaa(I{+h+dh^ zN~Q!1KFP>MOd;d~#Bdl?qi3L2Wa-%u%V0C=(i1m~d#VA&ZOZV|&;`h0O=r=Pa==xv zOlg4v_NEKY#*NfRi9GpNJ60?#h_`T}_$u_8OcHuf;&g<7D9$m3gr1&BJQzCoN3;*O z=|f-ij4}7cXsoe1qD8gu;?BBoCWmXx^+Tu>VEU74ppm8Kvkt@+(&eGsIP%gK-0Fu{ z-^8u}t=Z7fWz(V?b>e|KDsK}}_Lx(ZA+U1$MIn9^bbPGAw%g~(JI@8oOs$N}8$T&+ z=Z67_p|6#9fM=%fCAu1u6U#GF&VVI|n!Iuia2sOba$@mVa)~a~E3A+OCRHY>AtWJ; zR~D`LjDgzoenB7Tw!J1`;3PaJ&Ab#E;3gn>yLEb^AcbxpiRrzY1*q#~S%$l0WPPvk z0rUnzbA=t>@CwTM3zNh&XOEpOwrH^pbOQ#r?G-z7 zANDgFbv^0lUA<>yM`iuncC~AW&7#CD@2uD`!wg85t2_0na-7}_RGRa%_&h#uvxUYB zQzQ`dr3E^q4SRDE)_&!*#A|p_I}?RkvSLX!P>?k>El8WPxp$xCe#U#OGMI0=D@QgdV{^Q+IfEVehvDdU-VuZZjY3&y1-GyuLif~FXPNPTXZq73Bil$a2&`tfGhgheVFkelSNMt5Xm!gC8Uy!2~Qs&q+{@Xiu{I$tUfTT~Qa)y&xYf3&rKe+jl8#lRxx38|_} z>}3mkh02degFgwgT`pa=qwT3AhY`9yo5f3WOvcYUt>tZmUsc4C)ysW9#^nUys4gHQ zJ4!e~W72rXEddf-X!l0ybT!G6U(W8(^V8r?7Hs1MYm=^1GH6vfWz zU$oy9%*K}q8<@!JOEwb(37$xpit8_l^wbb5Y$DIkG|Wz#V}}`07x#&6$qFt`LEAO8 z;xp5g*ohi9DZm=MEEbZ_3}+oH$NktGwu&wzbpcUBFy?W)zkil|Rx8!3*zKu7RQra#CAX+f@7(iTfxp5RWe?tQLn&}nDVOxtw zEgHj)fJ+F)>+M{6ja>3}hdme8T2HyRWTy52$qjOleds~K2z-&^yKIFwl>SYPkVQw9 zELS7u+)VzNW8WmHRyE;lzem2u2irz)E>Q$ni3S5C#jy%9!@k4#7v+V?U6#c+6TzOF zPoLZL)Q1FtKqu8a@R_LvY$8GO`DbyT9Z4qqrZCFCa8yL+ z2$&r3s(y%V%o~355VMAVd-+awWCMLw6JSf6a!@T7boIC;k0!kgBW|K_hCdkSviCtS zRX;fcA|Ash;Hrxngzle>a044$fD({MYtEKN1p=5g#FL6jtr|e0xH**QsR#K;wIIIh zErWu`2)_76xV!op8eXw)nBbN|q($i)5aLycWHA-e*r}cV+Dsc$*0lAlCoZgNe&dDc zWf6q<=hpLi7{K_Rc}7ReqKmGP+ByT!mDDW2C5!~Q#Gk% z5Mn<(?nDHIwN6hu8)0t72lHO5a8b!%QCW%B1hk+OxH9U9o7K#ttt7}yBsE27(+_5Y zF!2;VJ~d4zbn+m8b(F4WRiwsa^v{b=Ro3KBmesm|Bi~v0f@ZqVn@*t{e#Ha<~}rss!fj~)PhrdMwgS^;OKc{wICx;>fjD`c#2DjpQ=v` zz3zsjc?Dr5TP7cTe4I<7zA6C}S*2VeRC5V>>;eUpD(OeFGP3E3qW+sVg}N14@V9~T zvTba6!BTL>MWRQX2vB-x$*7l{Q=K@}iyt|Gby;rO)A|@a^OC>*t?Ddu^wq!01XmM= z$GAWof8!}WecsFC*w`-`aAo`%%{+2mD0H?nMSkmJ)!Ko!!i-BM0ac|6O9P!^Tto+R z1O;^Wif3D--NGtNH;A>)K&mxt)x<6Z+rir`v1_pF^+t@vxY!;Pt!VL6UZ*FF*x#P- zx5c=Sh|;H;87tf=ibVwR-6qsAN3ak{Xi#@dShsNM?!A*EW4T}uX9@#ArQ5>+l*rd< zl@bmpY7LxfRl|`mD_=2PnWp?!O_$+XJ;LE6-Z%_6QjF|4ddc_EL)!u_>&f>BL#5Tt6no_1 zjVkc)NZE*)!Ui%msCX>jVP1dnVNK-L;M1ik{yOcWo!bAVMi7#A=sK4)NmwD?<0ti0 zYz($@C8vo#F66@4!rHn`5HBr)z1VkRMY9E8AMbA6`!dFWANMT_^Re3Zu?uw<_(r

q0lKaOXr!{C%K-f)B~?kZ&7cdoevCmTa2C9q$p%RpZ1m8M zP}|;iZ{Gq$ahL)$KNmJJUJbbU82{WFOD90@9q-pd4b!z|uj%pCYFjDbS!en6NTs;s zVBb%n*+xtnq}qB@dtdmz5rGzfn0=;DplXCPEcj?pdu)gjb!0Eb*WA+?N9Y=1tydo! z?=7a)5pLU(82rjBtpZ!PNu>4j`Xn@SbV_uMd(h-%DMKC92e*{4Xbm=h2WsRpgC)tr z*p}_V>h!x9XXt}4^a9M}qgVLJ<(sWzSyK%w`X9+6f6DisX$E=qR%bv%TBOeqf52lx zqSunVRe_QW(W{{R8B{|4>PqsRFT$W`Ahc(yaAva%aoxKj{FR!z6&aSxG1XUQmGrh|!DJ~Vm}_lM#!)oThE7xyF)5o676d)$UpRXQ4z*K-x-;D)%!aW%iKziq zW6d~7liJq`wS^JhdQgHQ9ugTs7==Sff7NECGxf>{=90V8 zJ>l}}=QSf9wRy#R!%nmL>hF0DVb@>c7Fl@4(4KSF3w)sg`&B z5WV9Qs`79~zK>yiz|+_a*J5?&$E!I-B{)cy9vEhW9>doXnNBO}*{LFkYs~8lr=e$` zZ-n_YGE+h0eb6<0+O%>&f@hXf%<9T=XvNPz##kU#ASOktI=H)-9FGhK*@MC+`Y;y_ z6+1G(@1c3%3f`m4os1b10Yf_VAQ-TP@@cyK<*wFz>pS*5^Hcllg(>#B-chmoee-@Z zXeNhxm2~O*xJ!;d1ebHUH;7!Q0)-OZ5Nk5?vuuWsPwOz{S}IUfIPJshE7J&J!-At8 zDjI&o+-qI!Lj&Da4GI&=4!9bf^?4!Gtl;pEAIhG}pA59q^=CU*uK?4`b^sJ&UhyOr zTvvAgvyzpk{kPkz-H&W+4Rzf-+KBu@m?KmGCo?tMc%!$rg%)&Msqd_zGWNY^E zhQUMvXi0lZVC9+;bEt1m_c71j-1Ij}??NOXQPEi}7g{!+#vQs{wlMXfp#|%?qvXNV z59S%6QZ=#nhF{z`x_r26eoIbcJ@E1q84X6Hocvku z;_`MYhNDKce#jtfEg_euW3YwGOf_8Mm09hu?Z)NX`DV5~C863+63dWbjNE4%Fd=3@ zvv>g#-}a8JP!ZO)Pc=^N0iFgGgnuj`NPT}bbh3U4ix@-vp#9*EDrpgvj3mYy{>^TsG5hGHo9Azqix zHiDpys-Xty?VJvG8>X7YAzhWkA&}wwtS=KM)Tcf|yK;6o#U1d$C1RQL@=%k$>68r+ zxdL1bWOvj2u(g7cC)DT&7uRw4)TQTX#xvq=u;-%KxHu#E)i6W_oXzJeHE8JE-tpMq zO$NTmiJ49L`hBLf9*)cQa2pFIYi_v`!`KPlAkP8279;t5yphXN?g zav13C1I1-$<38hgU}fxtPPdNDzFXc=2`FMYw|BEE!hniEbJret1*|#}T9JHB%R9Z? z?6r7tQsP@WDb-Bmvpw@8!NCVY;o7A=mwkmCh$qPuZa56)kHwN*z_Xb0%nSwCY&$1dTXaRLU79A+<+vkODwD!7DM(U4ngcPj1cDSER z6pW#xgW!954O_3_n>|9m?0p%qAnyF8;fmSb(Qb*wxR}7LAy&2@tt6D5Q5;A^x85{s>@#8d^KFHjn z+>(}M*#G1)pzVhMX-~%x69EZc(Egr)9Vg0~CF?{l&`i(X-2f(7Y6`-9?T}uY@na_Ivnsi>j9uk?1X7v!Eyw-6DrIWq}m=qJZT@UP|coDl6*4l<$U8Bj6p% zf}?0?T+uP*w?uPw>#VmriF6vQN;GF#hjVu*^N5@dF|cQrXgGozC=)T9;#0ffTueXd zm$kE#MUXn=@{~H{JkrZgQ92^Yh3Js!OsscYpxF~g2`x$tP&)&m(`$0vf-=iaDxtr^ zV0eK2a`RdRML!_ZJs-sHs*rwY_ZE&rM#FISUQKwa2%|(FVK#F?;?jhNQ0>1&D5UF9 z&2VYtMO8Kk0GDkVNq$tvR-H$-Zt*e3{;-X^lJLy=xJfjy;e&mLMD=cZV5kpuOwvo# z2V^8ZA8q^f0K#I3GtSD`;Bo1lQ*(a@Z#0I~91)1zfcYAco8SOXUhKZ+!*mMiy>H=@CEwcuje8$18R)Two z#N0(h`()dOz{7#7F_j?&@P<=wT}UbGNlacXvqrCLMYns-(xW zhtAv&g%00qoaHozbeGsP#M%i(j2eDTY`Ha{AXRinhsT3LPk9!+FVy1l?VFCbk}zhQ z<`avH&LAkOq87%L7hf;X>XomdA{I^cp)E8K5u}>U`MJQ$l@Y?8%x_C&P-sIWg0L-@ zj>RJBsE5RapH_#Pd=9?e!>kd?R?4my?nEB~Ma{$ZA^KY1epXE8Nbu4 zf2V>DnirK56)9|W=1QsU9jBRD89Hp_@s$0Z8jp+vmjSl65JTyB;$nAkQebx-L{A6_ z=TXwy%5_D|eFmyrpZS@m&O1Ut-PN1B^5 zsBN2}x)ZAkMU1vT))`@q+|G&isuuI5a9$XH-aDU}#!l+=TxGsRB0%n>*bXOPvVB`b zfVM0yK8LZx#P3=eEdN)+<)|gaM5-GwR1G97dI4hn8&9XNbOuu;uH_HY9cq%O&}%>_f4qmC7DWsl2uy)X?R%>mZyMY#r)Ql-4#O^bT3|cPI-N+sN z;Zx;#RFO^;J7nu{iy2WH&np>ZX-#5+(=SqaZS0*mV7DFT&v^@SK$;u_y`U9Z#SO2h zUfQk2d}g@%rGis){ru^pu?U_Md|FRe2eamrq_>?(IL7X^R06ObIJ%h=$}lift2&os zQ=$SGGFBeGC0Py{af}>}rfc6gvVC#i`JA&y4rDvW#!c4@r30g=i+ot&@gx$| z@rLR)yu&X{qNlEa)s{y?L_(RMr^m>JuQ1<;7!{}Qr&Bnb@k$$5J}X$4)=w))g@cMt z!lDKCXQGy;?!@x@`yxjnfxZm)p2Bab-hrG};XS=_20|E*(ffP2+nKCbJ(gC>K2ECv z$djrO_4}l(lImCvh?nW9Ynb<(5;qFIhcd{0mq7(aiqc~>2;7P$3<&&Gq;0Y)tR&dI z!;Uz9#-4}I(i<~^ovVrICCM71S!DA~nG|cc@mR1GKn?lwEhvwaCzn}rN1TP~^bn@H zDis(JH@#h`bo#b_?)#-^xSbOJ{e7vcu?m6?WS?lI*g^^r_o+a;`tg*Q+Xm%OM>~*q zSGhrxrUwjMb9~~iPQCm}^r070Nc`x-m_rY7Kd@=UxLjd_QA?J|%mTF4+~Vi0c;^yo zOPrxWr2sXNFq=wtWA4T!I&3%U#4Nd;4l%6PRFezlpg$@WC?Ol!ozbXqV`W4^SaK?B z?Y~chVZ$KS91P6W4~w2(T5hXT{$jJza`g2gmK_&jz$EuzP8J&>e|*~gJFd7uLpE_f zd952!>bM8lgjF-n8G5Zw>?E{yZl+j%SQl61CEtp>b7#+$b@V{N#S7 zn{hu+)oU#XbtjGVsYdjwtlYi3P`&wS{GoW-P6V9x=)@BaP@IqVTb{35^&fjeE0??% z?~>iyLY;Be$vg1&`)Tu$`L%{fY(6!{DrX@hQU*(S1v#)W=?P>dY@IK;gJhJL8&mA=a2IQkoy1+XvJxS78l_0Qaymr8mbJbKhBhr8Yg~;sc3aCt z%I?ROgL~69G#&Xny|apv9!1}6Dc~JB{tB6dv;f9O961p)t*p$Vo!nsLn$WA!mV@t3 zkwih3OKygD*H@-QA&!at`HTdEFjG1-qfd#!O{eCWS17jbR4NMo)6*L{*aRjuk^Yb}V$VIjue~ zH}~eggNH9PlEj@I?k9Uot)mY~y6;x1l;8K>1dvO!8vE(pP9>Vp{1oUTz%Zze%dlB* zJ^gy?1>Y%3IztD^RS9Tnn{PbQhdbzr4_k=P+V@~M{LaIU_obRrf3_*3iSKnC3=|Ax zhQ~}k16)f5cSE7Q+3DkDX6`jygB=ZPF;A>qBP?lWST?p~Lew*Zd!n5E%<}fvf=~b* z44bl~Ug2))3euGfwSveoJU@0rIu*QR?#SRyOr_Fen1R4l{SLr1dw=v<&lnv~uytW9 zhJMN4D&t7FLp^;rT@wr{_;@Dx9*k6B!udlj>&H+XdnySlxOfJ;YtH+fruD1~;N476 ze2hN24ujLsfIOroBq9(I?mnUl9Syzux-jq6Ci#-DdB)+4Sgcrqnr#8qQeH5{oDP&k zY0Ytnc(O_nXpwS=;^oN~NjZ(keTqq@Q^FsqqR@bHj9y9Gk#0rQL?39XP}dHXabq)l zr^fkwcof1TzV#htiFvQ^1*n;shvb!?(U9yNmNMzM+C31O0QPt9KN-ohmd5l(4X_>H zvh?Cs&!EjKC~M05mj`DCt0+B@6DL`!%O1=+F7~D`!U!n{J=xks7^9Xcpju2?fI{PK z5z$^dAtF@EY09S_dcs8z30KUDtWfi;yQi3^H$=x13Qvj1UoSU0I`&gC>8$(+legsi zVhkdV0~4AJl%_G_(+{i17~G6%;FTHrArLg}XLz5zCxf5o6+6{Ekyi7%^)7uAeI<9B ziSrn-uWyMj(w$tlctnq^Ksj^gkpFtb1j1F<8^CT?06mhFPI$aV`&itWY|zuCQqg>z zq*+SS5XsfabXGn`!Twrsk>A=s)o*Y$w-{ls48EOBuZz9bN$3Y*rH?_4!G z$;q%QBKUTl>1ssD4o5p=pzuwO{T|ocmb~w}?cK}cHjGZ43!IbH)X-T0)($wNTALps zn;pWf<35A8UeTm5Nw~zu#}EIg?|jv(c1uCC7?u(N1q@IiT|&dXqX+A^`Zc&$X@qeV zh3)dj4w2}JeP;VI8?IdAOQHs)bd;&Gvq@W}D?~O;`k5NsftKXJ`v&Kod`F943%|b} z3R(Kbkr*#*qthwyYCumdg_Zb+g0f|1o6k~hoVIUsa`;eqICR@%FXheRL?Um2bq`-?3eKT7^%3QlRFs~k7`Sm6gXklW(36Uc91+F~w_nB%0=-5{Oj z|CpODAjB89a(vTFk2IsQ;B)tJr%U=I>|Rhp8DwZw zYS0P)erhOz{9HyQ~` zP(fa|lcD%c4g_h{VJJ+EVUzKi;WKMiXirlNLYo&u!aC$h54Sj^L1WYjQJ*g%q_RS! ziAohpEE>;+h)rZqt^%Idlu{8eGtF3kzPf8BI-Z`c)0(W1JnZXYo)b<=h{^5c62t*& z3eL(KfH41i6vLx&=w^}Y` zw(5H6GW?z<5U|SL#Xrw8!|3R>_nj(Nu~!+<7v+p&TBQ|BWh1gd4y|9`J=q~^`A8$y z4Mqq8%Hu(!93^$Iv?;USg`N^y#vK^ewEdvJ97!W>UWe9$#NWjb`$jxFWQoQvfe|g_ zrpTc>G94(>5)B@rdS@P~&wE!iuDq!};amays$oabe^UpuxuG%p3CI|bGGErt+r%VI zBFKa)?AAT5VD_D`0`~!hZ+-q-q#PQ%ge|PzTR0y#o||{id-Ni%dZ~}4)`6UenQ?n% z_{e&aOxe0l#<6vi`nD07yn-mG#QMB4og!7Q295MbHyU7_sdzz#hNS2!hekItrDOn;|TeWrOfKxv@ zlO#=3Zi38J+A_8ZkHJ-wx>~0ZXO;j6DDb?9Ty9u_xzXH0MrYb8nSw5Jh zt;I{tBZKDKar_RHM!RtVFBbFY-VE_s?F1WkgHO#@%;(yp=9wIVpRys<)@E0+*P!p?Hx! z@-f>s-=`j!_sK-M?UsXcWm!6IxY}-Ni<5~CwRYF6)-6h5o4)adLL*Q1C#pOw?rk{b z3tnxlQ@>dYV|y-Y-5D@}-&KIQbExItt3{}r$Mi49f8IX>PcKS(8%ku+=c7#+4{ z>=H{l$7~MCpu~k&HOZ{Q8?tkIv0W4UgCP0j>DnS)%nyuOkW(6ck3X^XYAeH(TnZiH8Onj5DV zR+w)%h~TwohX-mcDjKIPe5dDiPY{vsl`FfupE$ACz__Vd#uK=&c}v;O$D48}^+<9+MPiXqUjp;)wMrLY?v^7rWDKui_FR$ z&%}o$BErJ-x<@~Uce)Q-p$|+XxE7B(%LMzPal_$HD6ZTC8@x0OwRssJiHZ5;pB^ne zzQZ`RZZm=n>OSi_aEZ3V3NnggVuQ1^HrgWj7{B zLiSklaGgu&d4iUbfQK-yVU?Dla@>%CoOF5TNtQv0&;*aokwXGhbMp_G9>qPJBaxhg zQ18Sbl4XE%kn83!}jb;)`72Nx>kW z46?D9!w1i9BxKaFEOlJ--OUoJP3Q5CZxc+@K~cy~f_?0b82gni4T91tjPxW{+10FB zmW0o?6jgGSZ_ZFpvU(o-tXyaqoAxZNt-hJ1P3IdI#2rH?8#64@K65{CwFTbFpVf^} zo(;JgG-5JBwu@hAnUTwyJxm*?N*lI)%~-`;ufBHu)XHb`Ui-A%uIhE5O(B*eWt$B3k)_Qb&&pGsl=TmaWm z5yXa6t#8((TVKQ#)L5@Ys@YR$SOS@1ym|rT*>A+BkvY{5Hi5AyL-Jw{;Um7w{LBgl zS5Q*%O!}(|yYW*PefAhLxv4qlfPwim&u8d!Y-5yOsrr+kTlFiU_wHynGC;iBfK%in z1qtR2QQxO6RyjI?wAZBmDq3al28j$kF7zfP#`jrD_hltOvwDN;aOBnN?~G0Sk5efx6 z+8d;f^xj&d2b#e~*amVQZsF)rUPn%Wez4O+H_!Vfhj|mSqTc8GcQB`y;nN1kESjOGl9aCkd~M=vB9A{RQ&HsEds( z&7`ut1^SmdIBxd{TVY%SXLy`Y#q|>Tb$|?nptwuLY$;$f_mH%HAED1@$t{y6v~wt~ z0#W52hg`8~DfXyW*cesNC!yo&&4S75SmMLMy$BVn|2Mv+cebr^l$YeiC57MLP<|L5 zI^#kx+~Se*rJU5iZ50+34F}+fcG@qk7Y>>?qxjukt(>zKcqR-dNgFaBgflJ1>+!9< zQXXwV){xd0Dxp5x8Mn;N3v4?M{@C%+j_e0e_)|kiGZS0Tw~Zg|J3Y1cV5hwxxul)j z)N%bxNDIc$9k`%yNw*^vlYsO$&u%J5o1j;`5Jl8tkJBci2+j_4al}UHWJ2nQ__7z& zn$R8hpFM{k6INnQ_>WWT3N!ug%$}v6p00(-1w2l)3Sh~EZZtnqlp^o%Bh>Nee|mY3 zM^kvg)$)mF{Y`x0-rLgt%Lc_&4qNx-#PEJTP63Rp&K51bB zNQ#47<+R&#F_z{J15kotmf!#n)tk~987kmNWUxQh`sJ2rrCOw%sXOX>!Ij*usLzbi zi;lb*{fn?wYLlY*NPx`tYvj?ow}a+ZlLhI4By|r${Sxd?ePd^^B4@(euRL=D=K80K z09%lygr%8?R4j*W%k>WP7r~4HTZ_l#b!V`8#AH;#wN&1@J^>I8%VIL|piDcDvm!T| z`)z~#8{z}vUSK}Q9(7dS>Hy^jf-BU!Jg~T74Mgw7*E^(JG`z1_O`O=fyDbn65D49+ z)6>`)UKHf`vD$F)S?aDX5a8^$hKb-03+3+~Rd5iS5p>7Pe^QF7tlH)2;XRn-T@8in z7t(b>!-@WMbusY2v7p59jRRnXnnBJ8Ekge^+1Zg(VqfX)%FI|JhD>HP-^P4Cj5I&q zmOWT%$90v`!77VDBg#Zdj{6S;bW9Z%r497m1+I%BZSY;~tc zDQS<-Qr0Xj_$bYDxIOTmr@#k_T1bc*42rvY*#G9v9Mu;a1i?|f7YP$)IES3~^=!^C zB`GUU=#1}ke~x#;CsT}1$7h`EXjm(>ym9LYqK&+WJ;vgVi6IM2`Zu>6o&2M40JX1@ z#&SwmpWaPNqJHTc!1fzC4h!HY4MV0?tHkQ^4e3z9CwA>TvW(dE-5QNxU_wtCba4p~Lx-hl$+D(JsWc?$1CWw*od=_+R zdIa##y&wB3Y5^vbP=8(hqx17@+;c6A)McAuu(8Q49sSKMG`r9|vw-lI$4J$V!w(m1 zvn_;P`7|MdgM>T!vMx{<8cYFhsljQ++DOXH608dbwCef1k>}l6r|-FUJhO z`;%@u#Gj*`J4rk}E76}cJpe&FLuldV(}se@@yqAQ_Au`6yz?J}0sU z;>ef>3_};0V57Y!;uaD{G4_6WCe#}T_(9hrB(92LG8DSzpvHsFgJ;_^O$*76q7w#JFQ+*bh3l~V2DGb`{ z*bM|IP@^j}@zR1S^iDgVMyB~V>Cl$u5a(mXpQsXO|EP;@Dx@CSrAbc9;i48$%a2py&Y z6z5^8I)CfvFRvtcPmTX^M{CLdJIhGvH^X3AVRnWg(x3XO5w%_t6DzsvPRT5e6|O?ye))5n3jk4Hnrz_#t|!E1SN}9gqHPbUD<@C z@HL8{Q@K<3I?{M`m?k_;tsH3}-;UtQyKzHgENE$DCpQOZW}QcpCe4wueKt{wvCuvD zo?N1%wBv6iw|DuEX%Dx`kGOWn{(3OiBtfD`rY8~;qhw?(rE$8-C#L99R_hy8ot8lj zsHQZR0p^ld8J7WG+w^nt3u~6^He-XaI)-Z-LRI9>A4*ai=J%(|^OLi-%E1fv(&lKS zmnA0{mLDo7&7dYTPWd;0Q;7*J=bt#n2F8a3dxRvXNpep#OTwVTJKrl9B8$2vICBYX zBhw6GZ%sYaT+>rKiEHBvZ&ef{jo=L;J@i0SM7zqL%|!+(K3$o0Q<~K5?c*+2|bmp7?IZ53QkSUq+1^WWwldAONxBW~6(?0%tX~1hgFmjUKDpLUFf&+`KijP98w zl4ENLD38*shIP-oag3+JY@R;gHSAt8cDb=NFp9mrO?%+z8o`@Qf#k6fqbz$@H?!$d+lbKl>PyvP_mEYvUnfDKU~QF~*jC(0G}YXEe$% znnsq_Gq!i9)1U8;_uu{Fo^!vy`@QEb=brDUTQ(P`6zTTZ!Rodiq-@~T;ZI6 z*&s~(v#{Sw;1*^lC{Wzj1vPDk3RBL(;!!@vgT29$Vkj^FVGAkm5#Li!#Nx}Ny#(__ zZ5tV>>;%-|?N6Pa(^W~i#62O7eR)e#f~^qZvpL z1&(68`wZNyUTxm|V3LIh*To*!>RQ zm>s_ZOsH7RL%aWu9rYNn~>_=&}nL$t7=(=VgCOKTp&mO~uM z>pVQAr*c)glo4)}Z&H`uE% zEUs>E+>xfOWfJ|DPUEv3E1G3fvl?ny1vQGcmK7npb|RiPV>l9~B}!k8D_$X2O2dvo zVqM>6k};Vo(VqHd=CTsvyDxkolStF5-g6wQd`#vcdqxcvdU|Oj#j$Iz-x&+avkGC} zc<^M=_7BtqYX+w`5%G|CZ+3dWQ@ai~a(dr-pneg!DDC`=0C#J&5J?DUU~6@!jAGEy ztYCwDgJ`&3p7L%}Z&W1Xu6>A${G&!en~wpl2c5Ue3Z*u`lvF^Oss3t***6z2*SmH@$4VQ0}<%M9j8G;DL}r80q2 zLJJZrEI}?N=#dNq2GkIx3Qm>BvNuqS;&&7u?Z-HtsicuiZN!z?KcA>K>PRl6Ze-@S zVvK#a+E)B0A1zIP^N&rZJDSETboUj7>}?hf z9=XHcxOG*P%5EtwrLpdc#AGz>JMaek80q{5 znyi3OfXJ+eSjrDNj#NHDTN6*f*fT?)PYFHdkBWrw=i`Wd&S@IBKcx6&qum#ooo&Hx{*p5JWJ*BwtoXuSc#l;#5Y%NBw*onH7Kw>JecTqbj*s8OKdM#*>3LT@ z#M){d6twERYFF>Jx&wqk5bY?RJDQ=~ro-*_sO1l)1c#Z2f*qF#=mf&>?^3|a4ZT5| z5g4_y7MnQDv(u4ZDhjtI$}Rpkir_!N3%_$3%H(vo#c5ABsp`F10EeI(3q}%x1F0a& zfj&L}CmB17ibW6!9FlIp5>W`KMpo~CrSW@@xtx~r^Tgvw6;;xZoJsa>m1?JJ zG-2bVjrEtVo{*bb0qG&2x4y(`U%g0N9Xd)_-_)%f@P+irh|aJsVlBIUHFha|%UgHj z&sS_Y=df=85CfG96Rr-&KQB}9BVTPx%_swI~AFHH)flK=n! literal 0 HcmV?d00001 diff --git a/canaryUpdater/nuget/commandhelper.ts b/canaryUpdater/nuget/commandhelper.ts new file mode 100644 index 0000000..2593002 --- /dev/null +++ b/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/canaryUpdater/nuget/locationUtilities.ts b/canaryUpdater/nuget/locationUtilities.ts new file mode 100644 index 0000000..c81cd10 --- /dev/null +++ b/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/canaryUpdater/nuget/provenance.ts b/canaryUpdater/nuget/provenance.ts new file mode 100644 index 0000000..8e1e2da --- /dev/null +++ b/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/canaryUpdater/nuget/utility.ts b/canaryUpdater/nuget/utility.ts new file mode 100644 index 0000000..3fa5370 --- /dev/null +++ b/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/canaryUpdater/package.json b/canaryUpdater/package.json new file mode 100644 index 0000000..fbdc835 --- /dev/null +++ b/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://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "keywords": [ + "VSTS", + "Build", + "Task" + ], + "author": "nventive" +} diff --git a/canaryUpdater/task.json b/canaryUpdater/task.json new file mode 100644 index 0000000..3d14d15 --- /dev/null +++ b/canaryUpdater/task.json @@ -0,0 +1,367 @@ +{ + "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": "[nventive](http://www.nventive.com/)", + "category": "Build", + "author": "nventive", + "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/nventive.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" + } + } + ], + "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/canaryUpdater/task.ts b/canaryUpdater/task.ts new file mode 100644 index 0000000..002a596 --- /dev/null +++ b/canaryUpdater/task.ts @@ -0,0 +1,430 @@ +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 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 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(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 + ] + .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", "nventive.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/dependencies.js b/dependencies.js new file mode 100644 index 0000000..595ac8a --- /dev/null +++ b/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/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1a199b961ba0e18eaebb962cb88f921118642bf3 GIT binary patch literal 20423 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 literal 0 HcmV?d00001 diff --git a/overview.md b/overview.md new file mode 100644 index 0000000..76a4004 --- /dev/null +++ b/overview.md @@ -0,0 +1,25 @@ +This extensions gives acess 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/nventive/NuGet.Updater/blob/develop/src/NvGet.Tools.Updater/Readme.md) to update the packages to the latest matching version + +# Release Notes Compiler +A task generating a simple set of release notes in the markdown format. By default, these notes contain the following information: +- The name and a link to the branch from which the build was run +- The commit id and a link to it +- A link the pipeline run where this task was executed +- Optionally, the name of an environment provided to the task +- Another release note file can also be appended to the notes generated. + +A truncated version of the release notes can be generated to accomodate limitations from certain services. +# Website versioning +A task providing a custom solution for website versioning. It works as follows: +- At the location where the website is stored, a "versions" folder is created, in which a version-specific folder is created for the current version of the website being deployed +- The current version of the website is uploaded in the version folder +- The index page at the root of website is configured to redirect to the version folder +- Another index file is present in the versions folder, allowing access to all previous versions + +This task currently only supports a website hosted in an Azure Storage account. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b625ce --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "nventive.buildtools", + "version": "0.1.0", + "description": "Build tools for nventive", + "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://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "keywords": [ + "VSTS", + "Build", + "Task" + ], + "author": "nventive" +} diff --git a/releaseNotesCompiler/Readme.md b/releaseNotesCompiler/Readme.md new file mode 100644 index 0000000..75856b1 --- /dev/null +++ b/releaseNotesCompiler/Readme.md @@ -0,0 +1,13 @@ +# nventive Release Notes compiler task + +This straightforward task is used to help with the generation of basic release notes in the markdown format. The primary use case for those notes was AppCenter, but it can be adapted to fit any use. The flow is pretty simple, and consists mostly of gathering information on the pipeline run and writing those in a file. Here's a list of the information included in the resulting file: +- An optional environment passed as a parameter to the task (`EnvironmentName`) - useful to make sure that we're using the right build +- The name of the source branch, including a link to Azure DevOps +- The ID of the source commit, including a link to Azure DevOps +- The URL of the pipeline run, including a link to Azure DevOps +- Additional release notes taken from another file specified in the parameters (`AdditionalReleaseNotesFile`) + +This task is able to produce a truncated version of the resulting release note file. This is very useful since some services (especially AppCenter) have a limit on the number of characters that can be included in the release notes. To do so, the `CreateTruncatedVersion`, `CharacterLimit` and `TruncatedOutputFilePath` parameters must be set. +It is also possible to remove the hyperlinks present in the release notes if they are not deemed necessary using the `RemoveHyperlinks` parameter. + +The full-size file will be generated under the path specified in the `OutputFilePath` parameter. The task also provides an output variable called `ReleaseNotesPath` to use in subsequent steps. \ No newline at end of file diff --git a/releaseNotesCompiler/icon.png b/releaseNotesCompiler/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1a199b961ba0e18eaebb962cb88f921118642bf3 GIT binary patch literal 20423 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 literal 0 HcmV?d00001 diff --git a/releaseNotesCompiler/package.json b/releaseNotesCompiler/package.json new file mode 100644 index 0000000..8db842f --- /dev/null +++ b/releaseNotesCompiler/package.json @@ -0,0 +1,22 @@ +{ + "name": "releasenotes.compiler", + "description": "Task allowing the compiling of release notes", + "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://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "keywords": [ + "Azure DevOps", + "Azure Pipelines", + "Task" + ], + "author": "nventive" +} diff --git a/releaseNotesCompiler/task.json b/releaseNotesCompiler/task.json new file mode 100644 index 0000000..c343d44 --- /dev/null +++ b/releaseNotesCompiler/task.json @@ -0,0 +1,130 @@ +{ + "id": "b4cb4ecf-9677-4a0a-bc98-45fc0dd140c1", + "name": "nventiveReleaseNotesCompiler", + "friendlyName": "Release Notes Compiler", + "description": "A task to generate a markdown file containing release notes for a given release.", + "helpMarkDown": "[nventive](http://www.nventive.com/)", + "category": "Azure Pipelines", + "author": "nventive", + "version": { + "Major": 0, + "Minor": 0, + "Patch": 0 + }, + "instanceNameFormat": "Compile release notes", + "groups": [ + { + "name": "input", + "displayName": "Input", + "isExpanded": true + }, + { + "name": "output", + "displayName": "Output", + "isExpanded": true + } + ], + "inputs": [ + { + "name": "EnvironmentName", + "type": "string", + "label": "Application environment", + "defaultValue": "", + "group": "input", + "required": false, + "helpMarkDown": "The name of the environment targeted by the application (Staging, Production, etc.)", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "AdditionalReleaseNotesFile", + "type": "filePath", + "label": "Additional release notes file", + "group": "input", + "defaultValue": "", + "required": false, + "helpMarkDown": "A file containing additional release notes to append at the end of the ones generated.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "OutputFilePath", + "type": "filePath", + "label": "Output file path", + "group": "output", + "defaultValue": "", + "required": true, + "helpMarkDown": "A file where to save the release notes.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "CreateTruncatedVersion", + "type": "boolean", + "label": "Create a truncated version of the release notes", + "group": "output", + "defaultValue": false, + "helpMarkDown": "Creating a truncated version of the release notes can be helpful when using services with a character limit on release notes markdonwn (like AppCenter); this will cause 2 files to be generated." + }, + { + "name": "TruncatedOutputFilePath", + "type": "filePath", + "label": "Truncated output file path", + "group": "output", + "defaultValue": "", + "required": true, + "visibleRule": "CreateTruncatedVersion = true", + "helpMarkDown": "A file where to save the truncated release notes.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "CharacterLimit", + "type": "string", + "label": "Character limit", + "group": "output", + "defaultValue": "", + "required": true, + "visibleRule": "CreateTruncatedVersion = true", + "helpMarkDown": "The maximum number of character the markdown file should have.", + "properties": { + "DisableManageLink": "True" + } + }, + { + "name": "RemoveHyperlinks", + "type": "bool", + "label": "Remove hyperlinks from release notes", + "group": "output", + "defaultValue": false, + "required": true, + "visibleRule": "CreateTruncatedVersion = true", + "helpMarkDown": "Replace all markdown hyperlinks with only the name (ex: `[Check this out](https://dev.azure.com/)` will be replace with Check this out.", + "properties": { + "DisableManageLink": "True" + } + } + ], + "dataSourceBindings": [], + "sourceDefinitions": [], + "OutputVariables": [ + { + "name" : "ReleaseNotesPath", + "description" : "The path to the full release notes." + }, + { + "name" : "TruncatedReleaseNotesPath", + "description" : "The path to the truncated release notes." + } + ], + "execution": + { + "Node": { + "target": "task.js" + } + } +} diff --git a/releaseNotesCompiler/task.ts b/releaseNotesCompiler/task.ts new file mode 100644 index 0000000..b5682c4 --- /dev/null +++ b/releaseNotesCompiler/task.ts @@ -0,0 +1,191 @@ +import tl = require("azure-pipelines-task-lib"); +import fs = require('fs'); + +async function run() { + let output = addLine("", "# Details"); + + output = addLine(output, getEnvironmentContent()); + + output = addLine(output, getSourceBranchContent()); + + output = addLine(output, getSourceCommitContent()); + + output = addLine(output, getPipelineUrl()); + + output = addLine(output, getAdditionalReleaseNotesContent()); + + let outputFile = tl.getInput("OutputFilePath"); + + console.log("Generating release notes in " + outputFile); + + await writeOutputToFile(output, outputFile); + + tl.setVariable("ReleaseNotesPath", outputFile); + + if(tl.getBoolInput("CreateTruncatedVersion")) { + let characterLimit = parseInt(tl.getInput("CharacterLimit")); + let truncatedFilePath = tl.getInput("TruncatedOutputFilePath"); + + console.log("Generating release notes truncated at " + characterLimit + " characters in " + truncatedFilePath); + + await writeOutputToFile(trimOutput(output, characterLimit, "..."), truncatedFilePath); + + tl.setVariable("TruncatedReleaseNotesPath", truncatedFilePath); + } +} + +function getEnvironmentContent() : string { + let environment = tl.getInput("EnvironmentName"); + + if(environment) { + return "- Environment: " + environment; + } + + return null; +} + +function getSourceBranchContent() : string { + let branchName = tl.getVariable("Build.SourceBranch").replace("refs/heads/",""); + let repositoryUrl = GetRepositoryUrl(); + let branchUrl: string; + + if(repositoryUrl) { + branchUrl = repositoryUrl + "?version=GB" + encodeURIComponent(branchName); + } + + return "- Source branch: " + getHyperlinkMarkdown(branchName, branchUrl); +} + +function getSourceCommitContent() : string { + let sourceVersion = tl.getVariable("Build.SourceVersion"); + let repositoryUrl = GetRepositoryUrl(); + let commitUrl: string; + + if(repositoryUrl) { + commitUrl = repositoryUrl + "/commit/" + sourceVersion + "?refName=" + encodeURIComponent(tl.getVariable("Build.SourceBranch")); + } + + return "- Source commit: " + getHyperlinkMarkdown(sourceVersion, commitUrl); +} + +function getPipelineUrl() : string { + let buildNumber = tl.getVariable("Build.BuildNumber"); + let buildId = tl.getVariable("Build.BuildId"); + let projectUrl = GetProjectUrl(); + let pipelineUrl: string; + + if(projectUrl && buildNumber) { + pipelineUrl = projectUrl + "/_build/results?buildId=" + buildId + "&view=results"; + } + + return "- Pipeline run : " + getHyperlinkMarkdown(buildNumber, pipelineUrl); +} + +function getAdditionalReleaseNotesContent() : string { + let additionalNotes = tl.getInput("AdditionalReleaseNotesFile"); + if(additionalNotes && fs.existsSync(additionalNotes)) { + return fs.readFileSync(additionalNotes).toString(); + } + + return null; +} + +function GetRepositoryUrl() : string { + let repositoryUrl = tl.getVariable("Build.Repository.Uri"); + if(repositoryUrl) { + repositoryUrl = repositoryUrl.replace(/https:\/\/.*@/gi, "https://"); //Remove user@ in the repo URL + } + + return repositoryUrl; +} + +function GetProjectUrl() : string { + let collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri"); + let project = tl.getVariable("System.TeamProject"); + + return collectionUrl + project.replace(" ", "%20"); +} + +function getHyperlinkMarkdown(text: string, url: string) : string { + if(url) { + return "[" + text + "](" + url + ")"; + } + else { + return text; + } +} + +function trimOutput(output: string, limit: number, trimCharacters: string) : string { + if(tl.getBoolInput("RemoveHyperlinks")) { + output = output.replace(/\[(.*?)\]\(.*?\)/g, '$1'); + } + + let characterCount = 0; + let trimmedOutput : string[] = []; + let limitWithCharacter = limit - trimCharacters.length; + + for(var line of output.split('\n')) { + var newCharacterCount = characterCount + line.length + 1; //Adding 1 for the newline character we will add at the end + //new line doesn't fit + if(newCharacterCount > limit) { + tl.debug("Cannot fit current line; character count is " + characterCount); + var lineLimit = limitWithCharacter - characterCount; + //Check if we have enough space to put the lines + the trim characters + if(lineLimit >= 0) { + tl.debug("Trimming current line to " + lineLimit + " characters"); + trimmedOutput.push(line.substring(0, lineLimit) + trimCharacters); + } + //otherwise, we add the trim characters to the previous eligible line + else if(trimmedOutput.length > 0) { + var previousLine = trimmedOutput.pop(); + var previousLineLimit = limitWithCharacter - characterCount - previousLine.length; + + tl.debug("Trimming previous line to " + previousLineLimit + " characters"); + + trimmedOutput.push(previousLine.substring(0, previousLineLimit) + trimCharacters); + } + break; + } + + characterCount = newCharacterCount; + + trimmedOutput.push(line); + } + return trimmedOutput.join('\n'); +} + +function addLine(input: string, line: string) : string { + if(line) { + return input + line + '\n'; + } + else { + return input; + } +} + +async function writeOutputToFile(output: string, outputFilePath: string): Promise { + if(!outputFilePath) { + return; + } + + try { + if(fs.existsSync(outputFilePath)) { + const fileStates = fs.statSync(outputFilePath); + var file = fs.createWriteStream(outputFilePath, { flags: "r+", autoClose: true, start: fileStates.size }); + + file.write(output); + } else { + var file = fs.createWriteStream(outputFilePath, { autoClose: true }); + + file.write(output); + } + + return true; + } catch(ex) + { + tl.error(ex); + return false; + } +} + +run(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fbe1f99 --- /dev/null +++ b/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/vss-extension.json b/vss-extension.json new file mode 100644 index 0000000..aabcc2b --- /dev/null +++ b/vss-extension.json @@ -0,0 +1,77 @@ +{ + "manifestVersion": 1, + "id": "nventive", + "version": "0.0.0", + "name": "nventive Build Tools", + "description": "A set of build tools developed by nventive", + "publisher": "nventivecorp", + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "categories": [ + "Build and release" + ], + "icons": { + "default": "logo.png" + }, + "content": { + "details": { + "path": "overview.md" + } + }, + "repository": { + "type": "git", + "uri": "https://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + }, + "contributions": [ + { + "id": "nventive.canaryUpdater", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "canaryUpdater" + } + }, + { + "id": "nventive.releaseNotesCompiler", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "releaseNotesCompiler" + } + }, + { + "id": "nventive.websiteVersion", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "websiteVersion" + } + } + ], + "scopes": [ + "vso.work" + ], + "files": [ + { + "path": "canaryUpdater" + }, + { + "path": "releaseNotesCompiler" + }, + { + "path": "websiteVersion" + }, + { + "path": "node_modules" + } + ] +} \ No newline at end of file diff --git a/websiteVersion/Readme.md b/websiteVersion/Readme.md new file mode 100644 index 0000000..66db150 --- /dev/null +++ b/websiteVersion/Readme.md @@ -0,0 +1,11 @@ +# nventive Website versioning + +This task is meant to help with versioning a static website. It was developed as a mean to maintain multiple versions of WASM-based Uno applications. The concept is very simple: +- The contents of the website are stored in an Azure Blob storage +- All the versions of the website can be found under a `Versions` folder +- The list of the available versions can be found by navigating to /Versions +- When navigating to the root of the website, the user is immediately redirected to the latest version, using `http-equiv="refresh"` + +This task is in charge of uploading the files to the storage account and generating the 2 HTML files - the root index and the versions index. + +The website should be somehow versioned for this to work properly, using a file named Version.txt placed at the root of the website. \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/AzureServiceClient.ts b/websiteVersion/azure-arm-rest/AzureServiceClient.ts new file mode 100644 index 0000000..17c2151 --- /dev/null +++ b/websiteVersion/azure-arm-rest/AzureServiceClient.ts @@ -0,0 +1,275 @@ +import tl = require('azure-pipelines-task-lib'); +import util = require("util") +import msRestAzure = require("./azure-arm-common"); +import webClient = require("./webClient"); + +export class ApiResult { + public error; + public result; + public request; + public response; + + constructor(error, result?, request?, response?) { + this.error = error; + this.result = result; + this.request = request; + this.response = response; + } +} + +export class AzureError { + public code; + public message; + public statusCode; + public details; +} + +export interface ApiCallback { + (error: any, result?: any, request?: any, response?: any): void +} + +export function ToError(response: webClient.WebResponse): AzureError { + var error = new AzureError(); + error.statusCode = response.statusCode; + error.message = response.body + if (response.body && response.body.error) { + error.code = response.body.error.code; + error.message = response.body.error.message; + error.details = response.body.error.details; + + console.log("##vso[task.logissue type=error;code="+error.code+";]"); + } + + return error; +} + +export class ServiceClient { + private credentials: msRestAzure.ApplicationTokenCredentials; + protected apiVersion: string; + protected baseUri: string; + protected acceptLanguage: string; + protected longRunningOperationRetryTimeout: number; + protected generateClientRequestId: boolean; + + public subscriptionId: string; + + constructor(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string, timeout?: number) { + this.validateInputs(credentials, subscriptionId); + + this.credentials = credentials; + this.subscriptionId = subscriptionId + this.baseUri = this.credentials.baseUrl; + this.longRunningOperationRetryTimeout = !!timeout ? timeout : 0; // In minutes + } + + protected validateInputs(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string) { + if (!credentials) { + throw new Error(tl.loc("CredentialsCannotBeNull")); + } + if (!subscriptionId) { + throw new Error(tl.loc("SubscriptionIdCannotBeNull")); + } + } + + public getCredentials(): msRestAzure.ApplicationTokenCredentials { + return this.credentials; + } + + public getRequestUri(uriFormat: string, parameters: {}, queryParameters?: string[], apiVersion?: string): string { + return this.getRequestUriForBaseUri(this.baseUri, uriFormat, parameters, queryParameters, apiVersion); + } + + public getRequestUriForBaseUri(baseUri: string, uriFormat: string, parameters: {}, queryParameters?: string[], apiVersion?: string): string { + var requestUri = baseUri + uriFormat; + requestUri = requestUri.replace('{subscriptionId}', encodeURIComponent(this.subscriptionId)); + for (var key in parameters) { + requestUri = requestUri.replace(key, encodeURIComponent(parameters[key])); + } + + // trim all duplicate forward slashes in the url + var regex = /([^:]\/)\/+/gi; + requestUri = requestUri.replace(regex, '$1'); + + // process query paramerters + queryParameters = queryParameters || []; + queryParameters.push('api-version=' + encodeURIComponent(apiVersion || this.apiVersion)); + if (queryParameters.length > 0) { + requestUri += '?' + queryParameters.join('&'); + } + + return requestUri + } + + public setCustomHeaders(options: Object): {} { + var headers = {}; + if (options) { + for (var headerName in options['customHeaders']) { + if (options['customHeaders'].hasOwnProperty(headerName)) { + headers[headerName] = options['customHeaders'][headerName]; + } + } + } + return headers; + } + + public async beginRequest(request: webClient.WebRequest): Promise { + var token = await this.credentials.getToken(); + + request.headers = request.headers || {}; + request.headers["Authorization"] = "Bearer " + token; + if (this.acceptLanguage) { + request.headers['accept-language'] = this.acceptLanguage; + } + request.headers['Content-Type'] = 'application/json; charset=utf-8'; + + var httpResponse = null; + + try + { + httpResponse = await webClient.sendRequest(request); + if (httpResponse.statusCode === 401 && httpResponse.body && httpResponse.body.error && httpResponse.body.error.code === "ExpiredAuthenticationToken") { + // The access token might have expire. Re-issue the request after refreshing the token. + token = await this.credentials.getToken(true); + request.headers["Authorization"] = "Bearer " + token; + httpResponse = await webClient.sendRequest(request); + } + } catch(exception) { + let exceptionString: string = exception.toString(); + if(exceptionString.indexOf("Hostname/IP doesn't match certificates's altnames") != -1 + || exceptionString.indexOf("unable to verify the first certificate") != -1 + || exceptionString.indexOf("unable to get local issuer certificate") != -1) { + tl.warning(tl.loc('ASE_SSLIssueRecommendation')); + } + + throw exception; + } + + return httpResponse; + } + + public async getLongRunningOperationResult(response: webClient.WebResponse, timeoutInMinutes?: number): Promise { + timeoutInMinutes = timeoutInMinutes || this.longRunningOperationRetryTimeout; + var timeout = new Date().getTime() + timeoutInMinutes * 60 * 1000; + var waitIndefinitely = timeoutInMinutes == 0; + var request = new webClient.WebRequest(); + request.method = "GET"; + request.uri = response.headers["azure-asyncoperation"] || response.headers["location"]; + if (!request.uri) { + throw new Error(tl.loc("InvalidResponseLongRunningOperation")); + } + + while (true) { + response = await this.beginRequest(request); + tl.debug(`Response status code : ${response.statusCode}`); + if (response.statusCode === 202 || (response.body && (response.body.status == "Accepted" || response.body.status == "Running" || response.body.status == "InProgress"))) { + // If timeout; throw; + if (!waitIndefinitely && timeout < new Date().getTime()) { + throw new Error(tl.loc("TimeoutWhileWaiting")); + } + + // Retry after given interval. + var sleepDuration = 15; + if (response.headers["retry-after"]) { + sleepDuration = parseInt(response.headers["retry-after"]); + } + await this.sleepFor(sleepDuration); + } else { + break; + } + } + + return response; + } + + public async beginRequestExpBackoff(request: webClient.WebRequest, maxAttempt: number): Promise { + var sleepDuration = 1; + for(var i = 1; true; i++) { + var response : webClient.WebResponse = await this.beginRequest(request); + //not a server error; + if(response.statusCode <500) { + return response; + } + + // response of last attempt + if(i == maxAttempt) { + return response; + } + + // Retry after given interval. + sleepDuration = sleepDuration + i; + if (response.headers["retry-after"]) { + sleepDuration = parseInt(response.headers["retry-after"]); + } + + tl.debug(tl.loc("RetryingRequest", sleepDuration)); + await this.sleepFor(sleepDuration); + } + } + + public async accumulateResultFromPagedResult(nextLinkUrl: string): Promise { + var result = []; + while (nextLinkUrl) { + var nextRequest = new webClient.WebRequest(); + nextRequest.method = 'GET'; + nextRequest.uri = nextLinkUrl; + var response = await this.beginRequest(nextRequest); + if (response.statusCode == 200 && response.body) { + if (response.body.value) { + result = result.concat(response.body.value); + } + + nextLinkUrl = response.body.nextLink; + } + else { + return new ApiResult(ToError(response)); + } + } + + return new ApiResult(null, result); + } + + public isValidResourceGroupName(resourceGroupName: string) { + if (!resourceGroupName === null || resourceGroupName === undefined || typeof resourceGroupName.valueOf() !== 'string') { + throw new Error(tl.loc("ResourceGroupCannotBeNull")); + } + if (resourceGroupName !== null && resourceGroupName !== undefined) { + if (resourceGroupName.length > 90) { + throw new Error(tl.loc("ResourceGroupExceededLength")); + } + if (resourceGroupName.length < 1) { + throw new Error(tl.loc("ResourceGroupDeceededLength")); + } + if (resourceGroupName.match(/^[-\w\._\(\)]+$/) === null) { + throw new Error(tl.loc("ResourceGroupDoesntMatchPattern")); + } + } + } + + public isNameValid(name: string): boolean { + if (name === null || name === undefined || typeof name.valueOf() !== 'string') { + return false; + }else{ + return true; + } + } + + public getFormattedError(error: any): string { + if(error && error.message) { + if(error.statusCode) { + var errorMessage = typeof error.message.valueOf() == 'string' ? error.message + : (error.message.Code || error.message.code) + " - " + (error.message.Message || error.message.message) + error.message = `${errorMessage} (CODE: ${error.statusCode})` + } + + return error.message; + } + + return error; + } + + private sleepFor(sleepDurationInSeconds): Promise { + return new Promise((resolve, reeject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); + } +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/azure-arm-common.ts b/websiteVersion/azure-arm-rest/azure-arm-common.ts new file mode 100644 index 0000000..6a2a96a --- /dev/null +++ b/websiteVersion/azure-arm-rest/azure-arm-common.ts @@ -0,0 +1,283 @@ +import tl = require('azure-pipelines-task-lib'); +import Q = require('q'); +import querystring = require('querystring'); +import webClient = require('./webClient'); +import AzureModels = require('./azureModels'); +import constants = require('./constants'); +import path = require('path'); +import fs = require('fs'); +var jwt = require('jsonwebtoken'); + +export class ApplicationTokenCredentials { + private clientId: string; + private domain: string; + private authType: string; + private secret?: string; + private certFilePath?: string; + private isADFSEnabled?: boolean; + public baseUrl: string; + public authorityUrl: string; + public activeDirectoryResourceId: string; + public isAzureStackEnvironment: boolean; + public scheme: number; + public msiClientId: string; + private token_deferred: Q.Promise; + + constructor(clientId: string, domain: string, secret: string, baseUrl: string, authorityUrl: string, activeDirectoryResourceId: string, isAzureStackEnvironment: boolean, scheme?: string, msiClientId?: string, authType?: string, certFilePath?: string, isADFSEnabled?: boolean) { + + if (!Boolean(domain) || typeof domain.valueOf() !== 'string') { + throw new Error(tl.loc("DomainCannotBeEmpty")); + } + + if((!scheme ||scheme ==='ServicePrincipal')){ + if (!Boolean(clientId) || typeof clientId.valueOf() !== 'string') { + throw new Error(tl.loc("ClientIdCannotBeEmpty")); + } + + if(!authType || authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { + if (!Boolean(secret) || typeof secret.valueOf() !== 'string') { + throw new Error(tl.loc("SecretCannotBeEmpty")); + } + } + else { + if (!Boolean(certFilePath) || typeof certFilePath.valueOf() !== 'string') { + throw new Error(tl.loc("InvalidCertFileProvided")); + } + } + + } + + if (!Boolean(baseUrl) || typeof baseUrl.valueOf() !== 'string') { + throw new Error(tl.loc("armUrlCannotBeEmpty")); + } + + if (!Boolean(authorityUrl) || typeof authorityUrl.valueOf() !== 'string') { + throw new Error(tl.loc("authorityUrlCannotBeEmpty")); + } + + if (!Boolean(activeDirectoryResourceId) || typeof activeDirectoryResourceId.valueOf() !== 'string') { + throw new Error(tl.loc("activeDirectoryResourceIdUrlCannotBeEmpty")); + } + + if(!Boolean(isAzureStackEnvironment) || typeof isAzureStackEnvironment.valueOf() != 'boolean') { + isAzureStackEnvironment = false; + } + + this.clientId = clientId; + this.domain = domain; + this.baseUrl = baseUrl; + this.authorityUrl = authorityUrl; + this.activeDirectoryResourceId = activeDirectoryResourceId; + this.isAzureStackEnvironment = isAzureStackEnvironment; + + this.scheme = scheme ? AzureModels.Scheme[scheme] : AzureModels.Scheme['ServicePrincipal'] ; + this.msiClientId = msiClientId ; + if(this.scheme == AzureModels.Scheme['ServicePrincipal']) { + this.authType = authType ? authType : constants.AzureServicePrinicipalAuthentications.servicePrincipalKey; + if(this.authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { + this.secret = secret; + } + else { + this.certFilePath = certFilePath; + } + } + + this.isADFSEnabled = isADFSEnabled; + + } + + public getToken(force?: boolean): Q.Promise { + if (!this.token_deferred || force) { + if(this.scheme === AzureModels.Scheme.ManagedServiceIdentity) + { + this.token_deferred = this._getMSIAuthorizationToken(0, 0); + } + else + { + this.token_deferred = this._getSPNAuthorizationToken(); + } + } + + return this.token_deferred; + } + + public getDomain(): string { + return this.domain; + } + + public getClientId(): string { + return this.clientId; + } + + private _getMSIAuthorizationToken(retyCount: number ,timeToWait: number): Q.Promise { + var deferred = Q.defer(); + let webRequest = new webClient.WebRequest(); + webRequest.method = "GET"; + let apiVersion = "2018-02-01"; + const retryLimit = 5; + let msiClientId = this.msiClientId ? "&client_id=" + this.msiClientId : ""; + webRequest.uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=" + apiVersion + "&resource="+ this.baseUrl + msiClientId; + webRequest.headers = { + "Metadata": true + }; + + webClient.sendRequest(webRequest).then( + (response: webClient.WebResponse) => { + if (response.statusCode == 200) + { + deferred.resolve(response.body.access_token); + } + else if (response.statusCode == 429 || response.statusCode == 500) + { + if(retyCount < retryLimit) + { + let waitedTime = 2000 + timeToWait * 2; + retyCount +=1; + setTimeout(() => { + deferred.resolve(this._getMSIAuthorizationToken(retyCount, waitedTime)); + }, waitedTime); + } + else + { + deferred.reject(tl.loc('CouldNotFetchAccessTokenforMSIStatusCode', response.statusCode, response.statusMessage)); + } + + } + else + { + deferred.reject(tl.loc('CouldNotFetchAccessTokenforMSIDueToMSINotConfiguredProperlyStatusCode', response.statusCode, response.statusMessage)); + } + }, + (error) => { + deferred.reject(error) + } + ); + + return deferred.promise; + } + + private _getSPNAuthorizationToken(): Q.Promise { + if(this.authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { + return this._getSPNAuthorizationTokenFromKey(); + } + + return this._getSPNAuthorizationTokenFromCertificate() + } + + private _getSPNAuthorizationTokenFromCertificate(): Q.Promise { + var deferred = Q.defer(); + let webRequest = new webClient.WebRequest(); + webRequest.method = "POST"; + webRequest.uri = this.authorityUrl + (this.isADFSEnabled ? "" : this.domain) + "/oauth2/token/"; + webRequest.body = querystring.stringify({ + resource: this.activeDirectoryResourceId, + client_id: this.clientId, + grant_type: "client_credentials", + client_assertion: this._getSPNCertificateAuthorizationToken(), + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + }); + + webClient.sendRequest(webRequest).then( + (response: webClient.WebResponse) => { + if (response.statusCode == 200) { + deferred.resolve(response.body.access_token); + } + else { + deferred.reject(tl.loc('CouldNotFetchAccessTokenforAzureStatusCode', response.statusCode, response.statusMessage)); + } + }, + (error) => { + deferred.reject(error) + } + ); + return deferred.promise; + } + + + private _getSPNAuthorizationTokenFromKey(): Q.Promise { + var deferred = Q.defer(); + let webRequest = new webClient.WebRequest(); + webRequest.method = "POST"; + webRequest.uri = this.authorityUrl + this.domain + "/oauth2/token/"; + webRequest.body = querystring.stringify({ + resource: this.activeDirectoryResourceId, + client_id: this.clientId, + grant_type: "client_credentials", + client_secret: this.secret + }); + webRequest.headers = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" + }; + + webClient.sendRequest(webRequest).then( + (response: webClient.WebResponse) => { + if (response.statusCode == 200) + { + deferred.resolve(response.body.access_token); + } + else + { + deferred.reject(tl.loc('CouldNotFetchAccessTokenforAzureStatusCode', response.statusCode, response.statusMessage)); + } + }, + (error) => { + deferred.reject(error) + } + ); + + return deferred.promise; + } + + private _getSPNCertificateAuthorizationToken(): string { + var openSSLPath = tl.osType().match(/^Win/) ? tl.which(path.join(__dirname, 'openssl', 'openssl')) : tl.which('openssl'); + var openSSLArgsArray= [ + "x509", + "-noout", + "-in" , + this.certFilePath, + "-fingerprint" + ]; + + var pemExecutionResult = tl.execSync(openSSLPath, openSSLArgsArray); + var additionalHeaders = { + "alg": "RS256", + "typ": "JWT", + }; + + if(pemExecutionResult.code == 0) { + tl.debug("FINGERPRINT CREATION SUCCESSFUL"); + let shaFingerprint = pemExecutionResult.stdout; + let shaFingerPrintHashCode = shaFingerprint.split("=")[1].replace(new RegExp(":", 'g'), ""); + let fingerPrintHashBase64: string = Buffer.from( + shaFingerPrintHashCode.match(/\w{2}/g).map(function(a) { + return String.fromCharCode(parseInt(a, 16)); + } ).join(""), + 'binary' + ).toString('base64'); + additionalHeaders["x5t"] = fingerPrintHashBase64; + } + else { + console.log(pemExecutionResult); + throw new Error(pemExecutionResult.stderr); + } + + return getJWT(this.authorityUrl, this.clientId, this.domain, this.certFilePath, additionalHeaders, this.isADFSEnabled); + } + +} + +function getJWT(url: string, clientId: string, tenantId: string, pemFilePath: string, additionalHeaders, isADFSEnabled: boolean) { + + var pemFileContent = fs.readFileSync(pemFilePath); + var jwtObject = { + "aud": (`${url}/${!isADFSEnabled ? tenantId : ""}/oauth2/token`).replace(/([^:]\/)\/+/g, "$1"), + "iss": clientId, + "sub": clientId, + "jti": "" + Math.random(), + "nbf": (Math.floor(Date.now()/1000)-1000), + "exp": (Math.floor(Date.now()/1000)+8640000) + }; + + var token = jwt.sign(jwtObject, pemFileContent,{ algorithm: 'RS256', header :additionalHeaders }); + return token; +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts b/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts new file mode 100644 index 0000000..5c177f6 --- /dev/null +++ b/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts @@ -0,0 +1,152 @@ +import tl = require('azure-pipelines-task-lib'); +import Q = require('q'); +import webClient = require("./webClient"); +import { AzureEndpoint } from "./azureModels"; +import { ApplicationTokenCredentials } from './azure-arm-common'; +import constants = require('./constants'); +import fs = require('fs'); +import path = require('path'); +const certFilePath: string = path.join(tl.getVariable('Agent.TempDirectory'), 'spnCert.pem'); + +export class AzureRMEndpoint { + public endpoint: AzureEndpoint; + private _connectedServiceName: string; + private applicationTokenCredentials: ApplicationTokenCredentials; + + // Add an entry here and separate function for each new environment + private _environments = { + 'AzureStack': 'azurestack' + } + + constructor(connectedServiceName: string) { + this._connectedServiceName = connectedServiceName; + this.endpoint = null; + } + + public async getEndpoint(): Promise { + if(!!this.endpoint) { + return this.endpoint; + } + else { + this.endpoint = { + subscriptionID: tl.getEndpointDataParameter(this._connectedServiceName, 'subscriptionid', true), + subscriptionName: tl.getEndpointDataParameter(this._connectedServiceName, 'subscriptionname', true), + servicePrincipalClientID: tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'serviceprincipalid', true), + environmentAuthorityUrl: tl.getEndpointDataParameter(this._connectedServiceName, 'environmentAuthorityUrl', true), + tenantID: tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'tenantid', false), + url: tl.getEndpointUrl(this._connectedServiceName, true), + environment: tl.getEndpointDataParameter(this._connectedServiceName, 'environment', true), + scheme: tl.getEndpointAuthorizationScheme(this._connectedServiceName, true), + msiClientId: tl.getEndpointDataParameter(this._connectedServiceName, 'msiclientId', true), + activeDirectoryResourceID: tl.getEndpointDataParameter(this._connectedServiceName, 'activeDirectoryServiceEndpointResourceId', true), + azureKeyVaultServiceEndpointResourceId: tl.getEndpointDataParameter(this._connectedServiceName, 'AzureKeyVaultServiceEndpointResourceId', true), + azureKeyVaultDnsSuffix: tl.getEndpointDataParameter(this._connectedServiceName, 'AzureKeyVaultDnsSuffix', true), + } as AzureEndpoint; + + this.endpoint.authenticationType = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'authenticationType', true); + + // if scheme is null, we assume the scheme to be ServicePrincipal + let isServicePrincipalAuthenticationScheme = !this.endpoint.scheme || this.endpoint.scheme.toLowerCase() == constants.AzureRmEndpointAuthenticationScheme.ServicePrincipal; + if (isServicePrincipalAuthenticationScheme) { + if(this.endpoint.authenticationType && this.endpoint.authenticationType == constants.AzureServicePrinicipalAuthentications.servicePrincipalCertificate) { + tl.debug('certificate spn endpoint'); + this.endpoint.servicePrincipalCertificate = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'servicePrincipalCertificate', false); + this.endpoint.servicePrincipalCertificatePath = certFilePath; + fs.writeFileSync(this.endpoint.servicePrincipalCertificatePath, this.endpoint.servicePrincipalCertificate); + } + else { + tl.debug('credentials spn endpoint'); + this.endpoint.servicePrincipalKey = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'serviceprincipalkey', false); + } + } + + var isADFSEnabled = tl.getEndpointDataParameter(this._connectedServiceName, 'EnableAdfsAuthentication', true); + this.endpoint.isADFSEnabled = isADFSEnabled && (isADFSEnabled.toLowerCase() == "true"); + + if(!!this.endpoint.environment && this.endpoint.environment.toLowerCase() == this._environments.AzureStack) { + if(!this.endpoint.environmentAuthorityUrl || !this.endpoint.activeDirectoryResourceID) { + this.endpoint = await this._updateAzureStackData(this.endpoint); + } + } + else { + this.endpoint.environmentAuthorityUrl = (!!this.endpoint.environmentAuthorityUrl) ? this.endpoint.environmentAuthorityUrl : "https://login.windows.net/"; + this.endpoint.activeDirectoryResourceID = this.endpoint.url; + } + + this.endpoint.applicationTokenCredentials = new ApplicationTokenCredentials(this.endpoint.servicePrincipalClientID, this.endpoint.tenantID, this.endpoint.servicePrincipalKey, + this.endpoint.url, this.endpoint.environmentAuthorityUrl, this.endpoint.activeDirectoryResourceID, !!this.endpoint.environment && this.endpoint.environment.toLowerCase() == constants.AzureEnvironments.AzureStack, this.endpoint.scheme, this.endpoint.msiClientId, this.endpoint.authenticationType, this.endpoint.servicePrincipalCertificatePath, this.endpoint.isADFSEnabled); + } + + tl.debug(JSON.stringify(this.endpoint)); + return this.endpoint; + } + + private async _updateAzureStackData(endpoint: AzureEndpoint): Promise { + let dataDeferred = Q.defer(); + let webRequest = new webClient.WebRequest(); + webRequest.uri = `${endpoint.url}metadata/endpoints?api-version=2015-01-01`; + webRequest.method = 'GET'; + webRequest.headers = { + 'Content-Type': 'application/json' + } + + let azureStackResult; + try { + let response: webClient.WebResponse = await webClient.sendRequest(webRequest); + if(response.statusCode != 200) { + tl.debug("Action: _updateAzureStackData, Response: " + JSON.stringify(response)); + throw new Error(response.statusCode + ' ' + response.statusMessage) + } + + azureStackResult = response.body; + } + catch(error) { + throw new Error(tl.loc("FailedToFetchAzureStackDependencyData", error.toString())); + } + + endpoint.graphEndpoint = azureStackResult.graphEndpoint; + endpoint.galleryUrl = azureStackResult.galleryUrl; + endpoint.portalEndpoint = azureStackResult.portalEndpoint; + var authenticationData = azureStackResult.authentication; + if(!!authenticationData) { + var loginEndpoint: string = authenticationData.loginEndpoint; + if(!!loginEndpoint) { + loginEndpoint += (loginEndpoint[loginEndpoint.length - 1] == "/") ? "" : "/"; + endpoint.activeDirectoryAuthority = loginEndpoint; + endpoint.environmentAuthorityUrl = loginEndpoint; + endpoint.isADFSEnabled = loginEndpoint.endsWith('/adfs/'); + } + else { + // change to login endpoint + throw new Error(tl.loc('UnableToFetchAuthorityURL')); + } + + var audiences = authenticationData.audiences; + if(audiences && audiences.length > 0) { + endpoint.activeDirectoryResourceID = audiences[0]; + } + + try { + var endpointUrl = endpoint.url; + endpointUrl += (endpointUrl[endpointUrl.length-1] == "/") ? "" : "/"; + var index = endpointUrl.indexOf('.'); + var domain = endpointUrl.substring(index+1); + domain = (domain.lastIndexOf("/") == domain.length-1) ? domain.substring(0, domain.length-1): domain; + endpoint.azureKeyVaultDnsSuffix = ("vault." + domain).toLowerCase(); + endpoint.azureKeyVaultServiceEndpointResourceId = ("https://vault." + domain).toLowerCase(); + } + catch(error) { + throw new Error(tl.loc("SpecifiedAzureRmEndpointIsInvalid", endpointUrl)); + } + } + + return endpoint; + } +} + +export function dispose() { + if(tl.exist(certFilePath)) { + tl.rmRF(certFilePath); + tl.debug('Removed cert endpoint file'); + } +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/azure-arm-storage.ts b/websiteVersion/azure-arm-rest/azure-arm-storage.ts new file mode 100644 index 0000000..d393b9c --- /dev/null +++ b/websiteVersion/azure-arm-rest/azure-arm-storage.ts @@ -0,0 +1,201 @@ +import msRestAzure = require("./azure-arm-common"); +import azureServiceClient = require("./AzureServiceClient"); +import Model = require("./azureModels"); +import webClient = require("./webClient"); +import tl = require('azure-pipelines-task-lib'); +import Q = require('q'); +import util = require("util"); + +export class StorageManagementClient extends azureServiceClient.ServiceClient { + public storageAccounts: StorageAccounts; + + constructor(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string, baseUri?: any, options?: any) { + super(credentials, subscriptionId); + + this.acceptLanguage = 'en-US'; + this.generateClientRequestId = true; + this.apiVersion = (credentials.isAzureStackEnvironment) ? '2015-06-15' : '2017-06-01'; + + if (!options) + options = {}; + + if (baseUri) { + this.baseUri = baseUri; + } + + if (options.acceptLanguage) { + this.acceptLanguage = options.acceptLanguage; + } + if (options.longRunningOperationRetryTimeout) { + this.longRunningOperationRetryTimeout = options.longRunningOperationRetryTimeout; + } + if (options.generateClientRequestId) { + this.generateClientRequestId = options.generateClientRequestId; + } + this.storageAccounts = new StorageAccounts(this); + } +} + +export class StorageAccounts { + private client: StorageManagementClient; + + constructor(client) { + this.client = client; + } + + public async list(options): Promise { + var httpRequest = new webClient.WebRequest(); + httpRequest.method = 'GET'; + httpRequest.headers = this.client.setCustomHeaders(options); + // Getting all azure rm storage accounts (along with resource group names) for the given subscription. + httpRequest.uri = this.client.getRequestUri('//subscriptions/{subscriptionId}/providers/Microsoft.Storage/storageAccounts', {}); + + var deferred = Q.defer(); + var result = []; + this.client.beginRequest(httpRequest).then(async (response) => { + if (response.statusCode == 200) { + if (response.body.value) { + let storageAccounts: Model.StorageAccount[] = response.body.value; + result = result.concat(storageAccounts); + } + + if (response.body.nextLink) { + var nextResult = await this.client.accumulateResultFromPagedResult(response.body.nextLink); + if (nextResult.error) { + deferred.reject(nextResult.error); + } + + let storageAccounts: Model.StorageAccount[] = nextResult.result; + result = result.concat(storageAccounts); + } + + deferred.resolve(result); + } + else { + deferred.reject(azureServiceClient.ToError(response)); + } + }).catch(function (error) { + deferred.reject(error); + }); + + return deferred.promise; + } + + public async listClassicAndRMAccounts(options): Promise { + var httpRequest = new webClient.WebRequest(); + httpRequest.method = 'GET'; + httpRequest.headers = this.client.setCustomHeaders(options); + + // Getting all storage accounts (azure rm and classic, along with resource group names) for the given subscription. + httpRequest.uri = "https://management.azure.com/resources?api-version=2014-04-01-preview&%24filter=(subscriptionId%20eq%20'{subscriptionId}')%20and%20(resourceType%20eq%20'microsoft.storage%2Fstorageaccounts'%20or%20resourceType%20eq%20'microsoft.classicstorage%2Fstorageaccounts')"; + httpRequest.uri = httpRequest.uri.replace('{subscriptionId}', this.client.subscriptionId); + + var deferred = Q.defer(); + var result = []; + this.client.beginRequest(httpRequest).then(async (response) => { + if (response.statusCode == 200) { + if (response.body.value) { + let storageAccounts: Model.StorageAccount[] = response.body.value; + result = result.concat(storageAccounts); + } + + if (response.body.nextLink) { + var nextResult = await this.client.accumulateResultFromPagedResult(response.body.nextLink); + if (nextResult.error) { + deferred.reject(nextResult.error); + } + + let storageAccounts: Model.StorageAccount[] = nextResult.result; + result = result.concat(storageAccounts); + } + + deferred.resolve(result); + } + else { + deferred.reject(azureServiceClient.ToError(response)); + } + }).catch(function(error) { + deferred.reject(error); + }); + + return deferred.promise; + } + + public async listKeys(resourceGroupName: string, accountName: string, options, storageAccountType?: string): Promise { + if (resourceGroupName === null || resourceGroupName === undefined || typeof resourceGroupName.valueOf() !== 'string') { + throw new Error(tl.loc("ResourceGroupCannotBeNull")); + } + + if (accountName === null || accountName === undefined || typeof accountName.valueOf() !== 'string') { + throw new Error(tl.loc("StorageAccountCannotBeNull")); + } + + var apiVersion = "2017-06-01"; + var resourceProvider = "Microsoft.Storage"; + if (!!storageAccountType && storageAccountType.toLowerCase().indexOf("classicstorage") > 0) { + resourceProvider = "Microsoft.ClassicStorage"; + apiVersion = "2015-12-01"; + } + + var httpRequest = new webClient.WebRequest(); + httpRequest.method = 'POST'; + httpRequest.headers = this.client.setCustomHeaders(options); + httpRequest.uri = this.client.getRequestUri( + '//subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{provider}/storageAccounts/{storageAccountName}/listKeys', + { + '{resourceGroupName}': resourceGroupName, + '{storageAccountName}': accountName, + '{provider}': resourceProvider + }, + [], + apiVersion + ); + + var deferred = Q.defer(); + var accessKeys: string[] = []; + this.client.beginRequest(httpRequest).then((response) => { + if (response.statusCode == 200) { + if (resourceProvider === "Microsoft.ClassicStorage") { + accessKeys[0] = response.body.primaryKey; + accessKeys[1] = response.body.secondaryKey; + } else if (response.body.keys) { + let keys = response.body.keys; + for (let i = 0; i < keys.length; i++) { + accessKeys[i] = keys[i]["value"]; + } + } + + deferred.resolve(accessKeys); + } else { + deferred.reject(azureServiceClient.ToError(response)); + } + }).catch(function (error) { + deferred.reject(error); + }); + + return deferred.promise; + } + + public async get(storageAccountName: string): Promise { + let storageAccounts = await this.list(null); + let index = storageAccounts.findIndex(account => account.name.toLowerCase() === storageAccountName.toLowerCase()); + + if (index < 0) { + throw new Error(tl.loc("StorageAccountDoesNotExist", storageAccountName)); + } + + return storageAccounts[index]; + } + + public static getResourceGroupNameFromUri(resourceUri: string): string { + if (this.isNonEmptyInternal(resourceUri)) { + resourceUri = resourceUri.toLowerCase(); + return resourceUri.substring(resourceUri.indexOf("resourcegroups/") + "resourcegroups/".length, resourceUri.indexOf("/providers")); + } + return ""; + } + + private static isNonEmptyInternal(str: string): boolean { + return (!!str && !!str.trim()); + } +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/azureModels.ts b/websiteVersion/azure-arm-rest/azureModels.ts new file mode 100644 index 0000000..fc05049 --- /dev/null +++ b/websiteVersion/azure-arm-rest/azureModels.ts @@ -0,0 +1,355 @@ +import { ApplicationTokenCredentials } from "./azure-arm-common"; + +export interface AzureBaseObject { + name?: string; + id: string; +} + +export interface LoadBalancerProperties { + inboundNatRules: InboundNatRule[]; + backendAddressPools: BackendAddressPool[]; + frontendIPConfigurations: IPConfiguration[] +} + +export interface InboundNatRuleProperties { + frontendPort: number; + backendPort: number; + backendIPConfiguration?: IPConfiguration; + frontendIPConfiguration: IPConfiguration; + protocol: string; + idleTimeoutInMinutes: number; + enableFloatingIP: boolean; +} + +export interface BackendAddressPoolProperties { + backendIPConfigurations: IPConfiguration[]; +} + +export interface NetworkInterfaceProperties { + ipConfigurations: IPConfiguration[] +} + +export interface IPConfigurationProperties { + publicIPAddress: PublicIPAddress; + loadBalancerInboundNatRules: InboundNatRule[]; +} + +export interface PublicIPAddressProperties { + ipAddress: string; + dnsSettings: DnsSettings; +} + +export interface VMProperties { + networkProfile: NetworkProfile; + instanceView: InstanceView; + storageProfile: StorageProfile +} + +export interface VirtualMachineProfile { + networkProfile?: NetworkProfile; + instanceView?: InstanceView; + storageProfile?: StorageProfile; + extensionProfile?:ExtensionProfile; +} + +export interface VMSSProperties { + virtualMachineProfile: VirtualMachineProfile; + provisioningState?: string; +} + +export interface VMExtensionProperties { + provisioningState?: string; + type: string; + publisher: string; + typeHandlerVersion: string; + autoUpgradeMinorVersion?: boolean; + settings?: Object; + protectedSettings?: Object; +} + +export interface StorageProfile{ + imageReference?: Map; + osDisk: OSDisk; + dataDisks?: Map[]; +} + +export interface OSDisk{ + osType: string; + name: string; + createOption: string; + caching: string; + image: ImageUrl; +} + +export interface ImageUrl{ + uri: string; +} + +export interface DnsSettings { + fqdn: string; +} + +export interface NetworkProfile { + networkInterfaces: NetworkInterface[] +} + +export interface ExtensionProfile { + extensions: VMExtension[]; +} + +export interface InstanceView { + statuses: Status[]; +} + +export interface Status{ + code: string; +} + +export interface LoadBalancer extends AzureBaseObject { + location: string; + properties: LoadBalancerProperties +} + +export interface VM extends AzureBaseObject { + properties: VMProperties, + location: string, + tags?: string ; +} + +export interface VMSS extends AzureBaseObject { + properties?: VMSSProperties, + location?: string, + tags?: string ; +} + +export interface VMExtension { + name?: string; + id?: string; + properties: VMExtensionProperties, + sku?: VMSku; +} + +export interface VMSku { + name?: string, + tier?: string; + capacity?: string; +} + +export interface NetworkInterface extends AzureBaseObject { + properties: NetworkInterfaceProperties +} + +export interface InboundNatRule extends AzureBaseObject { + properties: InboundNatRuleProperties +} + +export interface IPConfiguration extends AzureBaseObject { + properties?: IPConfigurationProperties; +} + +export interface BackendAddressPool extends AzureBaseObject { + properties: BackendAddressPoolProperties +} + +export interface PublicIPAddress extends AzureBaseObject { + properties: PublicIPAddressProperties; +} + +export interface VMExtensionMetadata { + type: string; + publisher: string; + typeHandlerVersion: string; +} + +export enum ComputeResourceType { + VirtualMachine, + VirtualMachineScaleSet +} + +export enum Scheme { + ManagedServiceIdentity, + SPN +} + +export interface StorageAccountSku { + name: string; + tier?: string; +} + +export interface StorageAccountEndpoints { + blob?: string; + table?: string; + file?: string; + queue?: string; +} + +export interface StorageAccountProperties { + creationTime?: string; + primaryLocation?: string; + primaryEndpoints?: StorageAccountEndpoints; + provisioningState?: string; + secondaryLocation?: string; + secondaryndpoints?: StorageAccountEndpoints; + statusOfPrimary?: string; + statusOfSecondary?: string; + supportsHttpsTrafficOnly?: boolean; +} + +export interface StorageAccount extends AzureBaseObject { + type: string; + location?: string; + sku?: StorageAccountSku; + kind?: string; + tags?: Map; + properties?: StorageAccountProperties; +} + +export interface AzureEndpoint { + subscriptionID: string; + subscriptionName: string; + servicePrincipalClientID?: string; + authenticationType?: string; + servicePrincipalKey?: string; + servicePrincipalCertificate?: string; + servicePrincipalCertificatePath?: string + tenantID: string; + environmentAuthorityUrl: string; + url: string; + environment: string; + activeDirectoryResourceID: string; + activeDirectoryAuthority?: string; + graphEndpoint?: string; + galleryUrl?: string; + portalEndpoint?: string; + azureKeyVaultDnsSuffix?: string; + azureKeyVaultServiceEndpointResourceId?: string; + msiClientId?: string; + scheme?: string; + applicationTokenCredentials: ApplicationTokenCredentials; + isADFSEnabled?: boolean; +} + +export interface AzureAppServiceConfigurationDetails { + id: string; + name: string; + type: string; + kind?: string; + location: string; + tags: string; + properties?: {[key: string]: any}; +} + +export interface WebJob { + name: string; + status: string; + runCommand: string; + log_url: string; + url: string; + type: string; +} + +export interface SiteExtension { + id: string; + title: string; + description: string; + extension_url: string; + local_path: string; + version: string; + project_url: string; + authors: Array; + provisioningState: string; + local_is_latest_version: boolean; +} + +export interface WebTest { + id?: string; + name: string; + type: string; + location: string; + tags: {[key: string]: string}, + kind?: string, + etag?: string; + properties?: {[key: string]: any}; +} + + +export interface ApplicationInsights { + id?: string; + name: string; + type: string; + location: string; + tags: {[key: string]: string}, + kind?: string, + etag?: string; + properties?: {[key: string]: any}; +} + +export interface AKSClusterProperties { + provisioningState: string; + kubernetesVersion: string; +} + +export interface AKSCluster extends AzureBaseObject { + properties: AKSClusterProperties +} + +export interface AKSClusterAccessProfileProperties { + kubeConfig: string; +} + +export interface AKSClusterAccessProfile extends AzureBaseObject { + properties: AKSClusterAccessProfileProperties +} + +export interface IThresholdRuleConditionDataSource { + "odata.type": string; + resourceUri: string; + metricName: string; +} + +export interface IThresholdRuleCondition { + "odata.type": string; // "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition" + dataSource: IThresholdRuleConditionDataSource; + threshold: string; + operator: string; + windowSize: string; +} + +export interface IAzureMetricAlertRequestBodyProperties { + name: string; + description?: string; + isEnabled: boolean; + condition: IThresholdRuleCondition; + actions: IRuleEmailAction[]; +} + +export interface IRuleEmailAction { + "odata.type": string; //"Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + sendToServiceOwners: boolean; + customEmails: string[] +} + +export interface IAzureMetricAlertRequestBody { + location: string; + tags: { [key: string] : string }; + properties: IAzureMetricAlertRequestBodyProperties; +} + +export interface IMetric { + value: string; + displayValue: string; + unit: string; +} + +export interface IAzureMetricAlertRule { + alertName: string; + metric: IMetric; + thresholdCondition: string; + thresholdValue: string; + timePeriod: string; +} + +export interface IAzureMetricAlertRulesList { + resourceId: string; + rules: IAzureMetricAlertRule[]; +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/constants.ts b/websiteVersion/azure-arm-rest/constants.ts new file mode 100644 index 0000000..dac071d --- /dev/null +++ b/websiteVersion/azure-arm-rest/constants.ts @@ -0,0 +1,29 @@ +export const AzureEnvironments = { + AzureStack: 'azurestack' +}; + +export const APPLICATION_INSIGHTS_EXTENSION_NAME: string = "Microsoft.ApplicationInsights.AzureWebSites"; + +export const productionSlot: string = "production"; + +export const mysqlApiVersion: string = '2017-12-01'; + +export const APIVersions = { + azure_arm_appinsights: '2015-05-01', + azure_arm_metric_alerts: '2016-03-01' +} + +export const KUDU_DEPLOYMENT_CONSTANTS = { + SUCCESS: 4, + FAILED: 3 +} + +export const AzureServicePrinicipalAuthentications = { + "servicePrincipalKey": "spnKey", + "servicePrincipalCertificate": "spnCertificate" +} + +export const AzureRmEndpointAuthenticationScheme = { + "ServicePrincipal": "serviceprincipal", + "ManagedServiceIdentity": "managedserviceidentity" +} \ No newline at end of file diff --git a/websiteVersion/azure-arm-rest/webClient.ts b/websiteVersion/azure-arm-rest/webClient.ts new file mode 100644 index 0000000..a7a20dc --- /dev/null +++ b/websiteVersion/azure-arm-rest/webClient.ts @@ -0,0 +1,117 @@ +import tl = require('azure-pipelines-task-lib'); +import util = require("util"); +import fs = require('fs'); +import httpClient = require("typed-rest-client/HttpClient"); +import httpInterfaces = require("typed-rest-client/Interfaces"); + +let proxyUrl: string = tl.getVariable("agent.proxyurl"); +var requestOptions: httpInterfaces.IRequestOptions = proxyUrl ? { + proxy: { + proxyUrl: proxyUrl, + proxyUsername: tl.getVariable("agent.proxyusername"), + proxyPassword: tl.getVariable("agent.proxypassword"), + proxyBypassHosts: tl.getVariable("agent.proxybypasslist") ? JSON.parse(tl.getVariable("agent.proxybypasslist")) : null + } +} : {}; + +let ignoreSslErrors: string = tl.getVariable("VSTS_ARM_REST_IGNORE_SSL_ERRORS"); +requestOptions.ignoreSslError = ignoreSslErrors && ignoreSslErrors.toLowerCase() == "true"; + +var httpCallbackClient = new httpClient.HttpClient(tl.getVariable("AZURE_HTTP_USER_AGENT"), null, requestOptions); + +export class WebRequest { + public method: string; + public uri: string; + // body can be string or ReadableStream + public body: string | NodeJS.ReadableStream; + public headers: any; +} + +export class WebResponse { + public statusCode: number; + public statusMessage: string; + public headers: any; + public body: any; +} + +export class WebRequestOptions { + public retriableErrorCodes: string[]; + public retryCount: number; + public retryIntervalInSeconds: number; + public retriableStatusCodes: number[]; + public retryRequestTimedout: boolean; +} + +export async function sendRequest(request: WebRequest, options?: WebRequestOptions): Promise { + let i = 0; + let retryCount = options && options.retryCount ? options.retryCount : 5; + let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; + let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; + let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; + let timeToWait: number = retryIntervalInSeconds; + while (true) { + try { + if (request.body && typeof(request.body) !== 'string' && !request.body["readable"]) { + request.body = fs.createReadStream(request.body["path"]); + } + + let response: WebResponse = await sendRequestInternal(request); + if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { + tl.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + continue; + } + + return response; + } + catch (error) { + if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { + tl.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + } + else { + if (error.code) { + console.log("##vso[task.logissue type=error;code=" + error.code + ";]"); + } + + throw error; + } + } + } +} + +export function sleepFor(sleepDurationInSeconds): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); +} + +async function sendRequestInternal(request: WebRequest): Promise { + tl.debug(util.format("[%s]%s", request.method, request.uri)); + var response: httpClient.HttpClientResponse = await httpCallbackClient.request(request.method, request.uri, request.body, request.headers); + return await toWebResponse(response); +} + +async function toWebResponse(response: httpClient.HttpClientResponse): Promise { + var res = new WebResponse(); + if (response) { + res.statusCode = response.message.statusCode; + res.statusMessage = response.message.statusMessage; + res.headers = response.message.headers; + var body = await response.readBody(); + if (body) { + try { + res.body = JSON.parse(body); + } + catch (error) { + tl.debug("Could not parse response: " + JSON.stringify(error)); + tl.debug("Response: " + JSON.stringify(res.body)); + res.body = body; + } + } + } + + return res; +} \ No newline at end of file diff --git a/websiteVersion/icon.png b/websiteVersion/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1a199b961ba0e18eaebb962cb88f921118642bf3 GIT binary patch literal 20423 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 literal 0 HcmV?d00001 diff --git a/websiteVersion/package.json b/websiteVersion/package.json new file mode 100644 index 0000000..b6077dd --- /dev/null +++ b/websiteVersion/package.json @@ -0,0 +1,29 @@ +{ + "name": "websiteversioning", + "version": "1.0.0", + "description": "An Azure DevOps task allowing the versioning of websites. Compatible with Azure Storage with Static Website.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://dev.azure.com/nventive/DevOps/_git/Build.Tasks" + }, + "author": "nventive", + "license": "ISC", + "keywords": [ + "Azure Pipelines", + "Azure DevOps", + "Task" + ], + "dependencies": { + "azure-pipelines-task-lib": "^2.9.3", + "azure-storage": "^2.10.3", + "jsonwebtoken": "^8.5.1", + "typed-rest-client": "^1.7.3" + }, + "devDependencies": { + "@types/node": "^12.12.14", + "@types/q": "^1.5.2" + } +} diff --git a/websiteVersion/task.json b/websiteVersion/task.json new file mode 100644 index 0000000..23547dd --- /dev/null +++ b/websiteVersion/task.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", + "id": "6d3bc6dd-afd3-4c2f-913d-14ec309c770e", + "name": "websiteVersion", + "friendlyName": "Website Versioning", + "description": "Make mutliple versions of a website available at once.", + "helpMarkDown": "Currently only compatible with an Azure Storage Static Website", + "category": "Deploy", + "author": "nventive", + "version": { + "Major": 0, + "Minor": 0, + "Patch": 0 + }, + "visibility": [ + "Build", + "Release" + ], + "demands": [ + "azureps" + ], + "instanceNameFormat": "Deploy $(WebsitePath) to $(AzureStorageAccount)", + "groups": [ + { + "displayName": "Advanced", + "name": "Advanced", + "isExpanded": false + } + ], + "inputs": [ + { + "name": "WebsitePath", + "type": "filePath", + "label": "Website location", + "defaultValue": "", + "required": true, + "helpMarkDown": "Absolute path of the folder where the website is located." + }, + { + "name": "AzureSubscription", + "type": "connectedService:AzureRM", + "label": "Azure Subscription", + "defaultValue": "", + "required": true, + "helpMarkDown": "Azure Resource Manager subscription to target for copying the files." + }, + { + "name": "AzureStorageAccount", + "type": "pickList", + "label": "RM Storage Account", + "defaultValue": "", + "required": true, + "helpMarkDown": "Azure Storage Account to target for copying the files.", + "properties": { + "EditableOptions": "True" + } + }, + { + "name": "VersionsFolderName", + "type": "string", + "label": "Versions folder", + "defaultValue": "versions", + "required": false, + "helpMarkDown": "The name of the folder where to store the different versions.", + "groupName": "Advanced" + } + ], + "dataSourceBindings": [ + { + "target": "AzureStorageAccount", + "endpointId": "$(AzureSubscription)", + "dataSourceName": "AzureStorageAccountRM" + } + ], + "execution": { + "Node10": { + "target": "task.js" + } + } +} \ No newline at end of file diff --git a/websiteVersion/task.ts b/websiteVersion/task.ts new file mode 100644 index 0000000..685551d --- /dev/null +++ b/websiteVersion/task.ts @@ -0,0 +1,266 @@ +import tl = require("azure-pipelines-task-lib"); +import path = require("path"); +import azure = require("azure-storage"); + +import armStorage = require('./azure-arm-rest/azure-arm-storage'); +import { AzureRMEndpoint } from './azure-arm-rest/azure-arm-endpoint'; +import { AzureEndpoint, StorageAccount } from './azure-arm-rest/azureModels'; + +import * as fs from 'fs'; + +class FileToUpload { + localPath: string; + name: string; + remotePath: string; + + constructor(localPath: string, remotePrefix?: string, name?: string, ) { + this.localPath = localPath; + + if (!name) { + name = path.basename(localPath); + } + + this.name = name; + + if (remotePrefix) { + this.remotePath = remotePrefix + '/' + name; + } else { + this.remotePath = name; + } + } +} + +const VersionFileName = 'Version.txt' +const ContainerName = '$web'; +const IndexFileName = 'index.html'; + +let VersionsFolderName: string; +let BlobService: azure.BlobService; + +async function run(): Promise { + try { + BlobService = await createBlobService(); + + let websitePath = tl.getInput("WebsitePath", true); + VersionsFolderName = tl.getInput("VersionsFolderName", true); + + let currentVersion = await retrieveCurrentVersion(websitePath); + + await pushCurrentVersion(websitePath, currentVersion); + + await updateVersionsIndex(); + await updateRootIndex(currentVersion); + } + catch (ex) { + tl.error(ex); + tl.setResult(tl.TaskResult.Failed, ex.message); + } +} + +async function createBlobService() : Promise { + let subscription = tl.getInput('AzureSubscription', true); + let storageAccountName = tl.getInput('AzureStorageAccount', true); + + var azureEndpoint: AzureEndpoint = await new AzureRMEndpoint(subscription).getEndpoint(); + const storageArmClient = new armStorage.StorageManagementClient(azureEndpoint.applicationTokenCredentials, azureEndpoint.subscriptionID); + + let storageAccount: StorageAccount = await storageArmClient.storageAccounts.get(storageAccountName); + let storageAccountResourceGroupName = getResourceGroupNameFromUri(storageAccount.id); + + let accessKeys = await storageArmClient.storageAccounts.listKeys(storageAccountResourceGroupName, storageAccountName, null); + let accessKey: string = accessKeys[0]; + + return azure.createBlobService(storageAccountName, accessKey); +} + +async function retrieveCurrentVersion(folderPath: string): Promise { + console.log("Retrieving website version"); + + let fullVersionFilePath = path.join(folderPath, VersionFileName); + if (fs.existsSync(fullVersionFilePath)) { + let version = fs.readFileSync(fullVersionFilePath, "utf8").trim(); + + if (version) { + + console.log("Version " + version + " found."); + + return version; + } + } + + throw "Could not determine version of the website. Ensure that " + VersionFileName + " exists and is correctly updated."; +} + +async function listExistingVersions(): Promise { + + console.log("Retrieving existing versions"); + + var promise = new Promise((resolve, reject) => { + BlobService.listBlobDirectoriesSegmentedWithPrefix(ContainerName, VersionsFolderName + "/", null, function (error, result, response) { + if (!!error) { + reject(error); + } else { + let versions = new Array(); + result.entries.forEach(directory => { + let version = directory.name.replace(VersionsFolderName, "").replace(/\//gi, ""); + + console.log("Found version " + version); + + versions.push(version); + }); + + resolve(versions); + } + }) + }); + + return promise; +} + +async function pushCurrentVersion(localPath: string, version: string) { + var files = listFiles(localPath, VersionsFolderName + "/" + version); + + for (var i in files) { + let file = files[i]; + await uploadFile(file); + + if(file.name.endsWith('.wasm')) { + await updateFile(file, { contentType: "application/wasm" }); + } + } +} + +async function updateVersionsIndex() { + + let availableVersions = await listExistingVersions(); + + let html = ''; + html += '\n' + ''; + html += '\n' + 'Available version'; + html += '\n' + ''; + html += '\n' + ''; + html += '\n' + '

'; + html += '\n' + '

Available Versions for ' + tl.getVariable("Build.DefinitionName") + '

'; + html += '\n' + '
    '; + + availableVersions + .sort((a, b) => -compareVersions(a, b)) + .forEach(v => { + html += '\n' + '
  • ' + v + '
  • '; + }); + + html += '\n' + '
'; + html += '\n' + '
'; + html += '\n' + ''; + + console.log("Generated " + VersionsFolderName + IndexFileName); + console.log(html); + + let indexPath = path.join(__dirname, IndexFileName); + fs.writeFileSync(indexPath, html); + + await uploadFile(new FileToUpload(indexPath, VersionsFolderName)); +} + +async function updateRootIndex(currentVersion: string) { + let html = ''; + html += '\n' + ''; + html += '\n' + ''; + html += '\n' + ''; + html += '\n' + ''; + + console.log("Generated " + IndexFileName); + console.log(html); + + let indexPath = path.join(__dirname, IndexFileName); + fs.writeFileSync(indexPath, html); + + await uploadFile(new FileToUpload(indexPath)); +} + +async function uploadFile(file: FileToUpload) { + + let upload = new Promise((resolve, reject) => { + BlobService.createBlockBlobFromLocalFile(ContainerName, file.remotePath, file.localPath, function (error, result, response) { + if (!error) { + console.log("Pushed " + result.name); + resolve(); + } else { + reject(error); + } + }); + }); + + await upload; +} + +async function updateFile(file: FileToUpload, options: azure.BlobService.SetBlobPropertiesRequestOptions) { + + let upload = new Promise((resolve, reject) => { + BlobService.setBlobProperties(ContainerName, file.remotePath, options, function (error, result, response) { + if (!error) { + console.log("Updated " + result.name); + resolve(); + } else { + reject(error); + } + }); + }); + + await upload; +} +function listFiles(folderPath: string, prefix: string): FileToUpload[] { + let files = new Array(); + + fs.readdirSync(folderPath).forEach(item => { + let itemPath = path.join(folderPath, item); + if (fs.statSync(itemPath).isDirectory()) { + listFiles(itemPath, prefix + "/" + path.basename(item)).forEach(i => files.push(i)); + } else { + files.push(new FileToUpload(itemPath, prefix)); + } + }); + + return files; +} + +function compareVersions(versionA: string, versionB: string): number { + let partsA = versionA.split("."); + let partsB = versionB.split("."); + + let partsSize = Math.max(partsA.length, partsB.length); + + for (var i = 0; i < partsSize; i++) { + let a = tryGetNumber(partsA, i, 0); + let b = tryGetNumber(partsB, i, 0); + + if (a == b) { + continue; + } + + return a - b; + } + + return 0; +} + +function tryGetNumber(values: string[], index: number, defaultValue: number): number { + if (index >= values.length) { + return defaultValue; + } else { + return +values[index]; + } +} + + +function getResourceGroupNameFromUri(resourceUri: string): string { + if (!!resourceUri && !!resourceUri.trim()) { + resourceUri = resourceUri.toLowerCase(); + return resourceUri.substring(resourceUri.indexOf("resourcegroups/") + "resourcegroups/".length, resourceUri.indexOf("/providers")); + } + + return ""; +} +run(); \ No newline at end of file From ea0f45c13013d67e7f95aa86ec8e71a5bdc9104f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Levesque Date: Wed, 2 Aug 2023 12:21:01 -0400 Subject: [PATCH 03/15] chore: Bump version to 6 to mark the change to open source. --- GitVersion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GitVersion.yml b/GitVersion.yml index 7b42093..804d5d8 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ assembly-versioning-scheme: MajorMinorPatch mode: Mainline -next-version: 5.0 +next-version: 6.0 ignore: sha: [] From 516b68941d1d6e2952e007bb20de784b8dfa01ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20L=C3=A9vesque?= Date: Wed, 2 Aug 2023 12:24:22 -0400 Subject: [PATCH 04/15] chore: Create README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..abf2986 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# nventive-Build-Tools + +This is the code for the [nventive Build Tools](https://marketplace.visualstudio.com/items?itemName=nventivecorp.nventive) Azure Pipelines extension. From 5a51125710b420e97c2316a7423a769776052323 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Levesque Date: Wed, 2 Aug 2023 12:28:37 -0400 Subject: [PATCH 05/15] chore: Add standard information to readme. --- BREAKING_CHANGES.md | 11 ++++++ CODE_OF_CONDUCT.md | 78 ++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 86 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 21 +++++++++++ 4 files changed, 196 insertions(+) create mode 100644 BREAKING_CHANGES.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md new file mode 100644 index 0000000..cb35164 --- /dev/null +++ b/BREAKING_CHANGES.md @@ -0,0 +1,11 @@ +# Breaking Changes + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..79037a6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,78 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and +expression, level of experience, education, socio-economic status, nationality, +personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at info@nventive.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an +incident. Further details of specific enforcement policies may be posted +separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..164f871 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# How to Contribute + +We'd love to accept your patches, contributions and suggestions to this project. +Here are a few small guidelines you need to follow. + +## Code of conduct + +To better foster an open, innovative and inclusive community please refer to our +[Code of Conduct](CODE_OF_CONDUCT.md) when contributing. + +### Report a bug + +If you think you've found a bug, please log a new issue in the [GitHub issue +tracker. When filing issues, please use our [issue +template](.github/ISSUE_TEMPLATE.md). The best way to get your bug fixed is to +be as detailed as you can be about the problem. Providing a minimal project with +steps to reproduce the problem is ideal. Here are questions you can answer +before you file a bug to make sure you're not missing any important information. + +1. Did you read the documentation? +2. Did you include the snippet of broken code in the issue? +3. What are the *EXACT* steps to reproduce this problem? +4. What specific version or build are you using? +5. What operating system are you using? + +GitHub supports +[markdown](https://help.github.com/articles/github-flavored-markdown/), so when +filing bugs make sure you check the formatting before clicking submit. + +### Make a suggestion + +If you have an idea for a new feature or enhancement let us know by filing an +issue. To help us understand and prioritize your idea please provide as much +detail about your scenario and why the feature or enhancement would be useful. + +## Contributing code and content + +This is an open source project and we welcome code and content contributions +from the community. + +**Identifying the scale** + +If you would like to contribute to this project, first identify the scale of +what you would like to contribute. If it is small (grammar/spelling or a bug +fix) feel free to start working on a fix. + +If you are submitting a feature or substantial code contribution, please discuss +it with the team. You might also read these two blogs posts on contributing +code: [Open Source Contribution +Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza +and [Don't "Push" Your Pull +Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by +Ilya Grigorik. Note that all code submissions will be rigorously reviewed and +tested by the project team, and only those that meet an extremely high bar for +both quality and design/roadmap appropriateness will be merged into the source. + +**Obtaining the source code** + +If you are an outside contributor, please fork the repository to your account. +See the GitHub documentation for [forking a +repo](https://help.github.com/articles/fork-a-repo/) if you have any questions +about this. + +**Submitting a pull request** + +If you don't know what a pull request is read this article: +https://help.github.com/articles/using-pull-requests. Make sure the repository +can build and all tests pass, as well as follow the current coding guidelines. +When submitting a pull request, please use our [pull request +template](.github/PULL_REQUEST_TEMPLATE.md). + +Pull requests should all be done to the **master** branch. + +--- + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult [GitHub +Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google.com/conduct/). \ No newline at end of file diff --git a/README.md b/README.md index abf2986..0845ed5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # nventive-Build-Tools This is the code for the [nventive Build Tools](https://marketplace.visualstudio.com/items?itemName=nventivecorp.nventive) 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 From 23bef8b4f46c50f6e5d026d6a400a190bda308f1 Mon Sep 17 00:00:00 2001 From: Nick Randolph Date: Thu, 3 Aug 2023 23:21:13 +1000 Subject: [PATCH 06/15] feat: Adding update properties file --- canaryUpdater/task.json | 25 +++++++++++++++++++++++++ canaryUpdater/task.ts | 12 ++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/canaryUpdater/task.json b/canaryUpdater/task.json index 3d14d15..c5e94cc 100644 --- a/canaryUpdater/task.json +++ b/canaryUpdater/task.json @@ -325,6 +325,31 @@ "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": [ diff --git a/canaryUpdater/task.ts b/canaryUpdater/task.ts index 002a596..215c048 100644 --- a/canaryUpdater/task.ts +++ b/canaryUpdater/task.ts @@ -161,6 +161,8 @@ async function nugetUpdate(): Promise { 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"); @@ -177,6 +179,7 @@ async function nugetUpdate(): Promise { let summaryFileArg = null; let versionOverridesFileArg = null; + let updatePropertiesFileArg = null; let resultFileArg = null; let feedArg = null; let downgradeArg = null; @@ -191,7 +194,11 @@ async function nugetUpdate(): Promise { versionOverridesFileArg = "--versionOverrides=" + versionOverridesFile; } - if(resultFile) { + if (useUpdateProperties && updatePropertiesFile) { + updatePropertiesFileArg = "--updateProperties=" + updatePropertiesFile; + } + + if (resultFile) { resultFileArg = "--result=" + resultFile; } @@ -222,7 +229,8 @@ async function nugetUpdate(): Promise { summaryFileArg, resultFileArg, strictArg, - versionOverridesFileArg + versionOverridesFileArg, + updatePropertiesFileArg ] .concat(targetVersions.map(v => "--version=" + v)) .concat(getListArgument("ignorePackages", ";", "ignore")) From e95f69bc5f44dc50b82f97b8645dfe34ed582b6f Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 17 May 2024 15:38:15 -0400 Subject: [PATCH 07/15] chore: Move to sub folder --- .../azuredevops/.azure-pipelines.yml | 0 .../azuredevops/.gitignore | 0 .../azuredevops/BREAKING_CHANGES.md | 0 .../azuredevops/CODE_OF_CONDUCT.md | 2 +- .../azuredevops/CONTRIBUTING.md | 0 .../azuredevops/GitVersion.yml | 0 LICENSE => extensions/azuredevops/LICENSE | 0 README.md => extensions/azuredevops/README.md | 4 ++-- .../azuredevops/canaryUpdater}/Readme.md | 6 +++--- .../azuredevops/canaryUpdater}/icon.png | Bin .../canaryUpdater}/nuget/commandhelper.ts | 0 .../canaryUpdater}/nuget/locationUtilities.ts | 0 .../canaryUpdater}/nuget/provenance.ts | 0 .../canaryUpdater}/nuget/utility.ts | 0 .../azuredevops/canaryUpdater}/package.json | 4 ++-- .../azuredevops/canaryUpdater}/task.json | 6 +++--- .../azuredevops/canaryUpdater}/task.ts | 2 +- .../azuredevops/dependencies.js | 0 logo.png => extensions/azuredevops/logo.png | Bin .../azuredevops/overview.md | 2 +- .../azuredevops/package.json | 8 ++++---- .../releaseNotesCompiler}/Readme.md | 2 +- .../azuredevops/releaseNotesCompiler}/icon.png | Bin .../releaseNotesCompiler}/package.json | 4 ++-- .../releaseNotesCompiler}/task.json | 4 ++-- .../azuredevops/releaseNotesCompiler}/task.ts | 0 .../src/.vs/NvGet/DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 320236 bytes .../ProjectEvaluation/nvget.metadata.v8.bin | Bin 0 -> 2660 bytes .../ProjectEvaluation/nvget.projects.v8.bin | Bin 0 -> 253072 bytes .../.vs/ProjectEvaluation/nvget.strings.v8.bin | Bin 0 -> 239052 bytes .../azuredevops/tsconfig.json | 0 .../azuredevops/vss-extension.json | 14 +++++++------- .../azuredevops/websiteVersion}/Readme.md | 2 +- .../azure-arm-rest/AzureServiceClient.ts | 0 .../azure-arm-rest/azure-arm-common.ts | 0 .../azure-arm-rest/azure-arm-endpoint.ts | 0 .../azure-arm-rest/azure-arm-storage.ts | 0 .../azure-arm-rest/azureModels.ts | 0 .../azure-arm-rest/constants.ts | 0 .../azure-arm-rest/webClient.ts | 0 .../azuredevops/websiteVersion}/icon.png | Bin .../azuredevops/websiteVersion}/package.json | 4 ++-- .../azuredevops/websiteVersion}/task.json | 2 +- .../azuredevops/websiteVersion}/task.ts | 0 44 files changed, 33 insertions(+), 33 deletions(-) rename .azure-pipelines.yml => extensions/azuredevops/.azure-pipelines.yml (100%) rename .gitignore => extensions/azuredevops/.gitignore (100%) rename BREAKING_CHANGES.md => extensions/azuredevops/BREAKING_CHANGES.md (100%) rename CODE_OF_CONDUCT.md => extensions/azuredevops/CODE_OF_CONDUCT.md (97%) rename CONTRIBUTING.md => extensions/azuredevops/CONTRIBUTING.md (100%) rename GitVersion.yml => extensions/azuredevops/GitVersion.yml (100%) rename LICENSE => extensions/azuredevops/LICENSE (100%) rename README.md => extensions/azuredevops/README.md (72%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/Readme.md (83%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/icon.png (100%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/nuget/commandhelper.ts (100%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/nuget/locationUtilities.ts (100%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/nuget/provenance.ts (100%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/nuget/utility.ts (100%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/package.json (78%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/task.json (98%) rename {canaryUpdater => extensions/azuredevops/canaryUpdater}/task.ts (98%) rename dependencies.js => extensions/azuredevops/dependencies.js (100%) rename logo.png => extensions/azuredevops/logo.png (100%) rename overview.md => extensions/azuredevops/overview.md (89%) rename package.json => extensions/azuredevops/package.json (70%) rename {releaseNotesCompiler => extensions/azuredevops/releaseNotesCompiler}/Readme.md (97%) rename {releaseNotesCompiler => extensions/azuredevops/releaseNotesCompiler}/icon.png (100%) rename {releaseNotesCompiler => extensions/azuredevops/releaseNotesCompiler}/package.json (79%) rename {releaseNotesCompiler => extensions/azuredevops/releaseNotesCompiler}/task.json (97%) rename {releaseNotesCompiler => extensions/azuredevops/releaseNotesCompiler}/task.ts (100%) create mode 100644 extensions/azuredevops/src/.vs/NvGet/DesignTimeBuild/.dtbcache.v2 create mode 100644 extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.metadata.v8.bin create mode 100644 extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.projects.v8.bin create mode 100644 extensions/azuredevops/src/.vs/ProjectEvaluation/nvget.strings.v8.bin rename tsconfig.json => extensions/azuredevops/tsconfig.json (100%) rename vss-extension.json => extensions/azuredevops/vss-extension.json (76%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/Readme.md (96%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/AzureServiceClient.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/azure-arm-common.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/azure-arm-endpoint.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/azure-arm-storage.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/azureModels.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/constants.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/azure-arm-rest/webClient.ts (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/icon.png (100%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/package.json (86%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/task.json (98%) rename {websiteVersion => extensions/azuredevops/websiteVersion}/task.ts (100%) diff --git a/.azure-pipelines.yml b/extensions/azuredevops/.azure-pipelines.yml similarity index 100% rename from .azure-pipelines.yml rename to extensions/azuredevops/.azure-pipelines.yml diff --git a/.gitignore b/extensions/azuredevops/.gitignore similarity index 100% rename from .gitignore rename to extensions/azuredevops/.gitignore diff --git a/BREAKING_CHANGES.md b/extensions/azuredevops/BREAKING_CHANGES.md similarity index 100% rename from BREAKING_CHANGES.md rename to extensions/azuredevops/BREAKING_CHANGES.md diff --git a/CODE_OF_CONDUCT.md b/extensions/azuredevops/CODE_OF_CONDUCT.md similarity index 97% rename from CODE_OF_CONDUCT.md rename to extensions/azuredevops/CODE_OF_CONDUCT.md index 79037a6..a8939c1 100644 --- a/CODE_OF_CONDUCT.md +++ b/extensions/azuredevops/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at info@nventive.com. All +reported by contacting the project team at info@unoplatform.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an diff --git a/CONTRIBUTING.md b/extensions/azuredevops/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to extensions/azuredevops/CONTRIBUTING.md diff --git a/GitVersion.yml b/extensions/azuredevops/GitVersion.yml similarity index 100% rename from GitVersion.yml rename to extensions/azuredevops/GitVersion.yml diff --git a/LICENSE b/extensions/azuredevops/LICENSE similarity index 100% rename from LICENSE rename to extensions/azuredevops/LICENSE diff --git a/README.md b/extensions/azuredevops/README.md similarity index 72% rename from README.md rename to extensions/azuredevops/README.md index 0845ed5..abe5a3c 100644 --- a/README.md +++ b/extensions/azuredevops/README.md @@ -1,6 +1,6 @@ -# nventive-Build-Tools +# unoplatform-Build-Tools -This is the code for the [nventive Build Tools](https://marketplace.visualstudio.com/items?itemName=nventivecorp.nventive) Azure Pipelines extension. +This is the code for the [unoplatform Build Tools](https://marketplace.visualstudio.com/items?itemName=nventivecorp.unoplatform) Azure Pipelines extension. ## Features diff --git a/canaryUpdater/Readme.md b/extensions/azuredevops/canaryUpdater/Readme.md similarity index 83% rename from canaryUpdater/Readme.md rename to extensions/azuredevops/canaryUpdater/Readme.md index 4b93180..0ef50fb 100644 --- a/canaryUpdater/Readme.md +++ b/extensions/azuredevops/canaryUpdater/Readme.md @@ -1,6 +1,6 @@ -# nventive Canary Updater task +# unoplatform Canary Updater task -This task is meant to be used by nventive Canary process. The flow of this task is as follows +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. @@ -8,7 +8,7 @@ Once the branch is fetched, and once again if requested, it is pushed to the cur 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/nventive/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. +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. diff --git a/canaryUpdater/icon.png b/extensions/azuredevops/canaryUpdater/icon.png similarity index 100% rename from canaryUpdater/icon.png rename to extensions/azuredevops/canaryUpdater/icon.png diff --git a/canaryUpdater/nuget/commandhelper.ts b/extensions/azuredevops/canaryUpdater/nuget/commandhelper.ts similarity index 100% rename from canaryUpdater/nuget/commandhelper.ts rename to extensions/azuredevops/canaryUpdater/nuget/commandhelper.ts diff --git a/canaryUpdater/nuget/locationUtilities.ts b/extensions/azuredevops/canaryUpdater/nuget/locationUtilities.ts similarity index 100% rename from canaryUpdater/nuget/locationUtilities.ts rename to extensions/azuredevops/canaryUpdater/nuget/locationUtilities.ts diff --git a/canaryUpdater/nuget/provenance.ts b/extensions/azuredevops/canaryUpdater/nuget/provenance.ts similarity index 100% rename from canaryUpdater/nuget/provenance.ts rename to extensions/azuredevops/canaryUpdater/nuget/provenance.ts diff --git a/canaryUpdater/nuget/utility.ts b/extensions/azuredevops/canaryUpdater/nuget/utility.ts similarity index 100% rename from canaryUpdater/nuget/utility.ts rename to extensions/azuredevops/canaryUpdater/nuget/utility.ts diff --git a/canaryUpdater/package.json b/extensions/azuredevops/canaryUpdater/package.json similarity index 78% rename from canaryUpdater/package.json rename to extensions/azuredevops/canaryUpdater/package.json index fbdc835..17a5435 100644 --- a/canaryUpdater/package.json +++ b/extensions/azuredevops/canaryUpdater/package.json @@ -11,12 +11,12 @@ }, "repository": { "type": "git", - "url": "https://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + "url": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" }, "keywords": [ "VSTS", "Build", "Task" ], - "author": "nventive" + "author": "unoplatform" } diff --git a/canaryUpdater/task.json b/extensions/azuredevops/canaryUpdater/task.json similarity index 98% rename from canaryUpdater/task.json rename to extensions/azuredevops/canaryUpdater/task.json index c5e94cc..ad70a4e 100644 --- a/canaryUpdater/task.json +++ b/extensions/azuredevops/canaryUpdater/task.json @@ -3,9 +3,9 @@ "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": "[nventive](http://www.nventive.com/)", + "helpMarkDown": "[unoplatform](http://www.unoplatform.com/)", "category": "Build", - "author": "nventive", + "author": "unoplatform", "version": { "Major": 0, "Minor": 0, @@ -212,7 +212,7 @@ "groupName": "updater", "defaultValue": "2.1.1", "required": true, - "helpMarkDown": "The of the NuGet updater to use. See https://www.nuget.org/packages/nventive.nuget.updater.tool for the list of available versions", + "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" } diff --git a/canaryUpdater/task.ts b/extensions/azuredevops/canaryUpdater/task.ts similarity index 98% rename from canaryUpdater/task.ts rename to extensions/azuredevops/canaryUpdater/task.ts index 215c048..4782180 100644 --- a/canaryUpdater/task.ts +++ b/extensions/azuredevops/canaryUpdater/task.ts @@ -395,7 +395,7 @@ async function installNuGetUpdater(): Promise { let dotnet = tl.tool(dotnetPath); let installationTool = dotnet - .arg([ "tool", "install", "nventive.NuGet.Updater.Tool", "--version", toolVersion, "--tool-path", tl.getVariable("Agent.TempDirectory"), "--ignore-failed-sources" ]); + .arg([ "tool", "install", "unoplatform.NuGet.Updater.Tool", "--version", toolVersion, "--tool-path", tl.getVariable("Agent.TempDirectory"), "--ignore-failed-sources" ]); await installationTool.exec({ outStream: getConsoleStream(false) }); diff --git a/dependencies.js b/extensions/azuredevops/dependencies.js similarity index 100% rename from dependencies.js rename to extensions/azuredevops/dependencies.js diff --git a/logo.png b/extensions/azuredevops/logo.png similarity index 100% rename from logo.png rename to extensions/azuredevops/logo.png diff --git a/overview.md b/extensions/azuredevops/overview.md similarity index 89% rename from overview.md rename to extensions/azuredevops/overview.md index 76a4004..8f25686 100644 --- a/overview.md +++ b/extensions/azuredevops/overview.md @@ -4,7 +4,7 @@ This extensions gives acess the following build tasks : 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/nventive/NuGet.Updater/blob/develop/src/NvGet.Tools.Updater/Readme.md) to update the packages to the latest matching version +- 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 # Release Notes Compiler A task generating a simple set of release notes in the markdown format. By default, these notes contain the following information: diff --git a/package.json b/extensions/azuredevops/package.json similarity index 70% rename from package.json rename to extensions/azuredevops/package.json index 6b625ce..023e0be 100644 --- a/package.json +++ b/extensions/azuredevops/package.json @@ -1,7 +1,7 @@ { - "name": "nventive.buildtools", + "name": "unoplatform.buildtools", "version": "0.1.0", - "description": "Build tools for nventive", + "description": "Build tools for unoplatform", "scripts": { "preinstall": "node dependencies.js" }, @@ -16,12 +16,12 @@ }, "repository": { "type": "git", - "url": "https://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + "url": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" }, "keywords": [ "VSTS", "Build", "Task" ], - "author": "nventive" + "author": "unoplatform" } diff --git a/releaseNotesCompiler/Readme.md b/extensions/azuredevops/releaseNotesCompiler/Readme.md similarity index 97% rename from releaseNotesCompiler/Readme.md rename to extensions/azuredevops/releaseNotesCompiler/Readme.md index 75856b1..5cd7461 100644 --- a/releaseNotesCompiler/Readme.md +++ b/extensions/azuredevops/releaseNotesCompiler/Readme.md @@ -1,4 +1,4 @@ -# nventive Release Notes compiler task +# unoplatform Release Notes compiler task This straightforward task is used to help with the generation of basic release notes in the markdown format. The primary use case for those notes was AppCenter, but it can be adapted to fit any use. The flow is pretty simple, and consists mostly of gathering information on the pipeline run and writing those in a file. Here's a list of the information included in the resulting file: - An optional environment passed as a parameter to the task (`EnvironmentName`) - useful to make sure that we're using the right build diff --git a/releaseNotesCompiler/icon.png b/extensions/azuredevops/releaseNotesCompiler/icon.png similarity index 100% rename from releaseNotesCompiler/icon.png rename to extensions/azuredevops/releaseNotesCompiler/icon.png diff --git a/releaseNotesCompiler/package.json b/extensions/azuredevops/releaseNotesCompiler/package.json similarity index 79% rename from releaseNotesCompiler/package.json rename to extensions/azuredevops/releaseNotesCompiler/package.json index 8db842f..303581a 100644 --- a/releaseNotesCompiler/package.json +++ b/extensions/azuredevops/releaseNotesCompiler/package.json @@ -11,12 +11,12 @@ }, "repository": { "type": "git", - "url": "https://nventive.visualstudio.com/DevOps/_git/Build.Tasks" + "url": "https://unoplatform.visualstudio.com/DevOps/_git/Build.Tasks" }, "keywords": [ "Azure DevOps", "Azure Pipelines", "Task" ], - "author": "nventive" + "author": "unoplatform" } diff --git a/releaseNotesCompiler/task.json b/extensions/azuredevops/releaseNotesCompiler/task.json similarity index 97% rename from releaseNotesCompiler/task.json rename to extensions/azuredevops/releaseNotesCompiler/task.json index c343d44..4531df2 100644 --- a/releaseNotesCompiler/task.json +++ b/extensions/azuredevops/releaseNotesCompiler/task.json @@ -3,9 +3,9 @@ "name": "nventiveReleaseNotesCompiler", "friendlyName": "Release Notes Compiler", "description": "A task to generate a markdown file containing release notes for a given release.", - "helpMarkDown": "[nventive](http://www.nventive.com/)", + "helpMarkDown": "[unoplatform](http://www.unoplatform.com/)", "category": "Azure Pipelines", - "author": "nventive", + "author": "unoplatform", "version": { "Major": 0, "Minor": 0, diff --git a/releaseNotesCompiler/task.ts b/extensions/azuredevops/releaseNotesCompiler/task.ts similarity index 100% rename from releaseNotesCompiler/task.ts rename to extensions/azuredevops/releaseNotesCompiler/task.ts 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 0000000000000000000000000000000000000000..7dd7d3193b42bae811d8a5f955150237f66c2d59 GIT binary patch literal 320236 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c4f6ddc613db4de81776c3edb3c42559dd6e7516 GIT binary patch literal 239052 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 Date: Fri, 17 May 2024 15:41:52 -0400 Subject: [PATCH 08/15] chore: Adjust main branch --- .github/workflows/main.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1893457..1704014 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,14 +3,12 @@ name: CI on: push: branches: - - develop - - master + - main pull_request: types: [opened, synchronize, reopened] branches: - - develop - - master + - main jobs: build: From 0690d52f5bc70e5655b08a7cd00833716af6da93 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 17 May 2024 15:57:27 -0400 Subject: [PATCH 09/15] chore: Remove unused files, build extension --- .github/workflows/main.yml | 39 +- extensions/azuredevops/BREAKING_CHANGES.md | 11 - extensions/azuredevops/CODE_OF_CONDUCT.md | 78 ---- extensions/azuredevops/CONTRIBUTING.md | 86 ----- extensions/azuredevops/GitVersion.yml | 5 - extensions/azuredevops/overview.md | 20 +- .../releaseNotesCompiler/Readme.md | 13 - .../azuredevops/releaseNotesCompiler/icon.png | Bin 20423 -> 0 bytes .../releaseNotesCompiler/package.json | 22 -- .../releaseNotesCompiler/task.json | 130 ------- .../azuredevops/releaseNotesCompiler/task.ts | 191 ---------- extensions/azuredevops/vss-extension.json | 34 +- .../azuredevops/websiteVersion/Readme.md | 11 - .../azure-arm-rest/AzureServiceClient.ts | 275 -------------- .../azure-arm-rest/azure-arm-common.ts | 283 -------------- .../azure-arm-rest/azure-arm-endpoint.ts | 152 -------- .../azure-arm-rest/azure-arm-storage.ts | 201 ---------- .../azure-arm-rest/azureModels.ts | 355 ------------------ .../azure-arm-rest/constants.ts | 29 -- .../azure-arm-rest/webClient.ts | 117 ------ .../azuredevops/websiteVersion/icon.png | Bin 20423 -> 0 bytes .../azuredevops/websiteVersion/package.json | 29 -- .../azuredevops/websiteVersion/task.json | 80 ---- extensions/azuredevops/websiteVersion/task.ts | 266 ------------- 24 files changed, 40 insertions(+), 2387 deletions(-) delete mode 100644 extensions/azuredevops/BREAKING_CHANGES.md delete mode 100644 extensions/azuredevops/CODE_OF_CONDUCT.md delete mode 100644 extensions/azuredevops/CONTRIBUTING.md delete mode 100644 extensions/azuredevops/GitVersion.yml delete mode 100644 extensions/azuredevops/releaseNotesCompiler/Readme.md delete mode 100644 extensions/azuredevops/releaseNotesCompiler/icon.png delete mode 100644 extensions/azuredevops/releaseNotesCompiler/package.json delete mode 100644 extensions/azuredevops/releaseNotesCompiler/task.json delete mode 100644 extensions/azuredevops/releaseNotesCompiler/task.ts delete mode 100644 extensions/azuredevops/websiteVersion/Readme.md delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/AzureServiceClient.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-common.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-storage.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/azureModels.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/constants.ts delete mode 100644 extensions/azuredevops/websiteVersion/azure-arm-rest/webClient.ts delete mode 100644 extensions/azuredevops/websiteVersion/icon.png delete mode 100644 extensions/azuredevops/websiteVersion/package.json delete mode 100644 extensions/azuredevops/websiteVersion/task.json delete mode 100644 extensions/azuredevops/websiteVersion/task.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1704014..54e12d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,8 +11,8 @@ on: - main jobs: - build: - name: Build + build-tools: + name: Build Tools runs-on: windows-2022 steps: - name: Checkout @@ -58,12 +58,43 @@ 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: 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 tfx-cli@0.7.x -g --no-audit --no-fund + + - 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.$(GitVersion.MajorMinorPatch).vsix --override "{\"version\":\"$(GitVersion.MajorMinorPatch)\"}" + + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: extensions + path: .\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/BREAKING_CHANGES.md b/extensions/azuredevops/BREAKING_CHANGES.md deleted file mode 100644 index cb35164..0000000 --- a/extensions/azuredevops/BREAKING_CHANGES.md +++ /dev/null @@ -1,11 +0,0 @@ -# Breaking Changes - - diff --git a/extensions/azuredevops/CODE_OF_CONDUCT.md b/extensions/azuredevops/CODE_OF_CONDUCT.md deleted file mode 100644 index a8939c1..0000000 --- a/extensions/azuredevops/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,78 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and -expression, level of experience, education, socio-economic status, nationality, -personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at info@unoplatform.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted -separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 1.4, available at -https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/extensions/azuredevops/CONTRIBUTING.md b/extensions/azuredevops/CONTRIBUTING.md deleted file mode 100644 index 164f871..0000000 --- a/extensions/azuredevops/CONTRIBUTING.md +++ /dev/null @@ -1,86 +0,0 @@ -# How to Contribute - -We'd love to accept your patches, contributions and suggestions to this project. -Here are a few small guidelines you need to follow. - -## Code of conduct - -To better foster an open, innovative and inclusive community please refer to our -[Code of Conduct](CODE_OF_CONDUCT.md) when contributing. - -### Report a bug - -If you think you've found a bug, please log a new issue in the [GitHub issue -tracker. When filing issues, please use our [issue -template](.github/ISSUE_TEMPLATE.md). The best way to get your bug fixed is to -be as detailed as you can be about the problem. Providing a minimal project with -steps to reproduce the problem is ideal. Here are questions you can answer -before you file a bug to make sure you're not missing any important information. - -1. Did you read the documentation? -2. Did you include the snippet of broken code in the issue? -3. What are the *EXACT* steps to reproduce this problem? -4. What specific version or build are you using? -5. What operating system are you using? - -GitHub supports -[markdown](https://help.github.com/articles/github-flavored-markdown/), so when -filing bugs make sure you check the formatting before clicking submit. - -### Make a suggestion - -If you have an idea for a new feature or enhancement let us know by filing an -issue. To help us understand and prioritize your idea please provide as much -detail about your scenario and why the feature or enhancement would be useful. - -## Contributing code and content - -This is an open source project and we welcome code and content contributions -from the community. - -**Identifying the scale** - -If you would like to contribute to this project, first identify the scale of -what you would like to contribute. If it is small (grammar/spelling or a bug -fix) feel free to start working on a fix. - -If you are submitting a feature or substantial code contribution, please discuss -it with the team. You might also read these two blogs posts on contributing -code: [Open Source Contribution -Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza -and [Don't "Push" Your Pull -Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by -Ilya Grigorik. Note that all code submissions will be rigorously reviewed and -tested by the project team, and only those that meet an extremely high bar for -both quality and design/roadmap appropriateness will be merged into the source. - -**Obtaining the source code** - -If you are an outside contributor, please fork the repository to your account. -See the GitHub documentation for [forking a -repo](https://help.github.com/articles/fork-a-repo/) if you have any questions -about this. - -**Submitting a pull request** - -If you don't know what a pull request is read this article: -https://help.github.com/articles/using-pull-requests. Make sure the repository -can build and all tests pass, as well as follow the current coding guidelines. -When submitting a pull request, please use our [pull request -template](.github/PULL_REQUEST_TEMPLATE.md). - -Pull requests should all be done to the **master** branch. - ---- - -## Code reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult [GitHub -Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. - -## Community Guidelines - -This project follows [Google's Open Source Community -Guidelines](https://opensource.google.com/conduct/). \ No newline at end of file diff --git a/extensions/azuredevops/GitVersion.yml b/extensions/azuredevops/GitVersion.yml deleted file mode 100644 index 804d5d8..0000000 --- a/extensions/azuredevops/GitVersion.yml +++ /dev/null @@ -1,5 +0,0 @@ -assembly-versioning-scheme: MajorMinorPatch -mode: Mainline -next-version: 6.0 -ignore: - sha: [] diff --git a/extensions/azuredevops/overview.md b/extensions/azuredevops/overview.md index 8f25686..b2c5f04 100644 --- a/extensions/azuredevops/overview.md +++ b/extensions/azuredevops/overview.md @@ -1,25 +1,7 @@ -This extensions gives acess the following build tasks : +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 - -# Release Notes Compiler -A task generating a simple set of release notes in the markdown format. By default, these notes contain the following information: -- The name and a link to the branch from which the build was run -- The commit id and a link to it -- A link the pipeline run where this task was executed -- Optionally, the name of an environment provided to the task -- Another release note file can also be appended to the notes generated. - -A truncated version of the release notes can be generated to accomodate limitations from certain services. -# Website versioning -A task providing a custom solution for website versioning. It works as follows: -- At the location where the website is stored, a "versions" folder is created, in which a version-specific folder is created for the current version of the website being deployed -- The current version of the website is uploaded in the version folder -- The index page at the root of website is configured to redirect to the version folder -- Another index file is present in the versions folder, allowing access to all previous versions - -This task currently only supports a website hosted in an Azure Storage account. \ No newline at end of file diff --git a/extensions/azuredevops/releaseNotesCompiler/Readme.md b/extensions/azuredevops/releaseNotesCompiler/Readme.md deleted file mode 100644 index 5cd7461..0000000 --- a/extensions/azuredevops/releaseNotesCompiler/Readme.md +++ /dev/null @@ -1,13 +0,0 @@ -# unoplatform Release Notes compiler task - -This straightforward task is used to help with the generation of basic release notes in the markdown format. The primary use case for those notes was AppCenter, but it can be adapted to fit any use. The flow is pretty simple, and consists mostly of gathering information on the pipeline run and writing those in a file. Here's a list of the information included in the resulting file: -- An optional environment passed as a parameter to the task (`EnvironmentName`) - useful to make sure that we're using the right build -- The name of the source branch, including a link to Azure DevOps -- The ID of the source commit, including a link to Azure DevOps -- The URL of the pipeline run, including a link to Azure DevOps -- Additional release notes taken from another file specified in the parameters (`AdditionalReleaseNotesFile`) - -This task is able to produce a truncated version of the resulting release note file. This is very useful since some services (especially AppCenter) have a limit on the number of characters that can be included in the release notes. To do so, the `CreateTruncatedVersion`, `CharacterLimit` and `TruncatedOutputFilePath` parameters must be set. -It is also possible to remove the hyperlinks present in the release notes if they are not deemed necessary using the `RemoveHyperlinks` parameter. - -The full-size file will be generated under the path specified in the `OutputFilePath` parameter. The task also provides an output variable called `ReleaseNotesPath` to use in subsequent steps. \ No newline at end of file diff --git a/extensions/azuredevops/releaseNotesCompiler/icon.png b/extensions/azuredevops/releaseNotesCompiler/icon.png deleted file mode 100644 index 1a199b961ba0e18eaebb962cb88f921118642bf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20423 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 diff --git a/extensions/azuredevops/releaseNotesCompiler/package.json b/extensions/azuredevops/releaseNotesCompiler/package.json deleted file mode 100644 index 303581a..0000000 --- a/extensions/azuredevops/releaseNotesCompiler/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "releasenotes.compiler", - "description": "Task allowing the compiling of release notes", - "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": [ - "Azure DevOps", - "Azure Pipelines", - "Task" - ], - "author": "unoplatform" -} diff --git a/extensions/azuredevops/releaseNotesCompiler/task.json b/extensions/azuredevops/releaseNotesCompiler/task.json deleted file mode 100644 index 4531df2..0000000 --- a/extensions/azuredevops/releaseNotesCompiler/task.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "id": "b4cb4ecf-9677-4a0a-bc98-45fc0dd140c1", - "name": "nventiveReleaseNotesCompiler", - "friendlyName": "Release Notes Compiler", - "description": "A task to generate a markdown file containing release notes for a given release.", - "helpMarkDown": "[unoplatform](http://www.unoplatform.com/)", - "category": "Azure Pipelines", - "author": "unoplatform", - "version": { - "Major": 0, - "Minor": 0, - "Patch": 0 - }, - "instanceNameFormat": "Compile release notes", - "groups": [ - { - "name": "input", - "displayName": "Input", - "isExpanded": true - }, - { - "name": "output", - "displayName": "Output", - "isExpanded": true - } - ], - "inputs": [ - { - "name": "EnvironmentName", - "type": "string", - "label": "Application environment", - "defaultValue": "", - "group": "input", - "required": false, - "helpMarkDown": "The name of the environment targeted by the application (Staging, Production, etc.)", - "properties": { - "DisableManageLink": "True" - } - }, - { - "name": "AdditionalReleaseNotesFile", - "type": "filePath", - "label": "Additional release notes file", - "group": "input", - "defaultValue": "", - "required": false, - "helpMarkDown": "A file containing additional release notes to append at the end of the ones generated.", - "properties": { - "DisableManageLink": "True" - } - }, - { - "name": "OutputFilePath", - "type": "filePath", - "label": "Output file path", - "group": "output", - "defaultValue": "", - "required": true, - "helpMarkDown": "A file where to save the release notes.", - "properties": { - "DisableManageLink": "True" - } - }, - { - "name": "CreateTruncatedVersion", - "type": "boolean", - "label": "Create a truncated version of the release notes", - "group": "output", - "defaultValue": false, - "helpMarkDown": "Creating a truncated version of the release notes can be helpful when using services with a character limit on release notes markdonwn (like AppCenter); this will cause 2 files to be generated." - }, - { - "name": "TruncatedOutputFilePath", - "type": "filePath", - "label": "Truncated output file path", - "group": "output", - "defaultValue": "", - "required": true, - "visibleRule": "CreateTruncatedVersion = true", - "helpMarkDown": "A file where to save the truncated release notes.", - "properties": { - "DisableManageLink": "True" - } - }, - { - "name": "CharacterLimit", - "type": "string", - "label": "Character limit", - "group": "output", - "defaultValue": "", - "required": true, - "visibleRule": "CreateTruncatedVersion = true", - "helpMarkDown": "The maximum number of character the markdown file should have.", - "properties": { - "DisableManageLink": "True" - } - }, - { - "name": "RemoveHyperlinks", - "type": "bool", - "label": "Remove hyperlinks from release notes", - "group": "output", - "defaultValue": false, - "required": true, - "visibleRule": "CreateTruncatedVersion = true", - "helpMarkDown": "Replace all markdown hyperlinks with only the name (ex: `[Check this out](https://dev.azure.com/)` will be replace with Check this out.", - "properties": { - "DisableManageLink": "True" - } - } - ], - "dataSourceBindings": [], - "sourceDefinitions": [], - "OutputVariables": [ - { - "name" : "ReleaseNotesPath", - "description" : "The path to the full release notes." - }, - { - "name" : "TruncatedReleaseNotesPath", - "description" : "The path to the truncated release notes." - } - ], - "execution": - { - "Node": { - "target": "task.js" - } - } -} diff --git a/extensions/azuredevops/releaseNotesCompiler/task.ts b/extensions/azuredevops/releaseNotesCompiler/task.ts deleted file mode 100644 index b5682c4..0000000 --- a/extensions/azuredevops/releaseNotesCompiler/task.ts +++ /dev/null @@ -1,191 +0,0 @@ -import tl = require("azure-pipelines-task-lib"); -import fs = require('fs'); - -async function run() { - let output = addLine("", "# Details"); - - output = addLine(output, getEnvironmentContent()); - - output = addLine(output, getSourceBranchContent()); - - output = addLine(output, getSourceCommitContent()); - - output = addLine(output, getPipelineUrl()); - - output = addLine(output, getAdditionalReleaseNotesContent()); - - let outputFile = tl.getInput("OutputFilePath"); - - console.log("Generating release notes in " + outputFile); - - await writeOutputToFile(output, outputFile); - - tl.setVariable("ReleaseNotesPath", outputFile); - - if(tl.getBoolInput("CreateTruncatedVersion")) { - let characterLimit = parseInt(tl.getInput("CharacterLimit")); - let truncatedFilePath = tl.getInput("TruncatedOutputFilePath"); - - console.log("Generating release notes truncated at " + characterLimit + " characters in " + truncatedFilePath); - - await writeOutputToFile(trimOutput(output, characterLimit, "..."), truncatedFilePath); - - tl.setVariable("TruncatedReleaseNotesPath", truncatedFilePath); - } -} - -function getEnvironmentContent() : string { - let environment = tl.getInput("EnvironmentName"); - - if(environment) { - return "- Environment: " + environment; - } - - return null; -} - -function getSourceBranchContent() : string { - let branchName = tl.getVariable("Build.SourceBranch").replace("refs/heads/",""); - let repositoryUrl = GetRepositoryUrl(); - let branchUrl: string; - - if(repositoryUrl) { - branchUrl = repositoryUrl + "?version=GB" + encodeURIComponent(branchName); - } - - return "- Source branch: " + getHyperlinkMarkdown(branchName, branchUrl); -} - -function getSourceCommitContent() : string { - let sourceVersion = tl.getVariable("Build.SourceVersion"); - let repositoryUrl = GetRepositoryUrl(); - let commitUrl: string; - - if(repositoryUrl) { - commitUrl = repositoryUrl + "/commit/" + sourceVersion + "?refName=" + encodeURIComponent(tl.getVariable("Build.SourceBranch")); - } - - return "- Source commit: " + getHyperlinkMarkdown(sourceVersion, commitUrl); -} - -function getPipelineUrl() : string { - let buildNumber = tl.getVariable("Build.BuildNumber"); - let buildId = tl.getVariable("Build.BuildId"); - let projectUrl = GetProjectUrl(); - let pipelineUrl: string; - - if(projectUrl && buildNumber) { - pipelineUrl = projectUrl + "/_build/results?buildId=" + buildId + "&view=results"; - } - - return "- Pipeline run : " + getHyperlinkMarkdown(buildNumber, pipelineUrl); -} - -function getAdditionalReleaseNotesContent() : string { - let additionalNotes = tl.getInput("AdditionalReleaseNotesFile"); - if(additionalNotes && fs.existsSync(additionalNotes)) { - return fs.readFileSync(additionalNotes).toString(); - } - - return null; -} - -function GetRepositoryUrl() : string { - let repositoryUrl = tl.getVariable("Build.Repository.Uri"); - if(repositoryUrl) { - repositoryUrl = repositoryUrl.replace(/https:\/\/.*@/gi, "https://"); //Remove user@ in the repo URL - } - - return repositoryUrl; -} - -function GetProjectUrl() : string { - let collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri"); - let project = tl.getVariable("System.TeamProject"); - - return collectionUrl + project.replace(" ", "%20"); -} - -function getHyperlinkMarkdown(text: string, url: string) : string { - if(url) { - return "[" + text + "](" + url + ")"; - } - else { - return text; - } -} - -function trimOutput(output: string, limit: number, trimCharacters: string) : string { - if(tl.getBoolInput("RemoveHyperlinks")) { - output = output.replace(/\[(.*?)\]\(.*?\)/g, '$1'); - } - - let characterCount = 0; - let trimmedOutput : string[] = []; - let limitWithCharacter = limit - trimCharacters.length; - - for(var line of output.split('\n')) { - var newCharacterCount = characterCount + line.length + 1; //Adding 1 for the newline character we will add at the end - //new line doesn't fit - if(newCharacterCount > limit) { - tl.debug("Cannot fit current line; character count is " + characterCount); - var lineLimit = limitWithCharacter - characterCount; - //Check if we have enough space to put the lines + the trim characters - if(lineLimit >= 0) { - tl.debug("Trimming current line to " + lineLimit + " characters"); - trimmedOutput.push(line.substring(0, lineLimit) + trimCharacters); - } - //otherwise, we add the trim characters to the previous eligible line - else if(trimmedOutput.length > 0) { - var previousLine = trimmedOutput.pop(); - var previousLineLimit = limitWithCharacter - characterCount - previousLine.length; - - tl.debug("Trimming previous line to " + previousLineLimit + " characters"); - - trimmedOutput.push(previousLine.substring(0, previousLineLimit) + trimCharacters); - } - break; - } - - characterCount = newCharacterCount; - - trimmedOutput.push(line); - } - return trimmedOutput.join('\n'); -} - -function addLine(input: string, line: string) : string { - if(line) { - return input + line + '\n'; - } - else { - return input; - } -} - -async function writeOutputToFile(output: string, outputFilePath: string): Promise { - if(!outputFilePath) { - return; - } - - try { - if(fs.existsSync(outputFilePath)) { - const fileStates = fs.statSync(outputFilePath); - var file = fs.createWriteStream(outputFilePath, { flags: "r+", autoClose: true, start: fileStates.size }); - - file.write(output); - } else { - var file = fs.createWriteStream(outputFilePath, { autoClose: true }); - - file.write(output); - } - - return true; - } catch(ex) - { - tl.error(ex); - return false; - } -} - -run(); \ No newline at end of file diff --git a/extensions/azuredevops/vss-extension.json b/extensions/azuredevops/vss-extension.json index 4c445d4..202a4b1 100644 --- a/extensions/azuredevops/vss-extension.json +++ b/extensions/azuredevops/vss-extension.json @@ -1,10 +1,10 @@ { "manifestVersion": 1, - "id": "unoplatform", + "id": "unoplatform-build-tools", "version": "0.0.0", "name": "unoplatform Build Tools", - "description": "A set of build tools developed by unoplatform", - "publisher": "nventivecorp", + "description": "The Canary Updater tool by Uno Platform", + "publisher": "unoplatform", "targets": [ { "id": "Microsoft.VisualStudio.Services" @@ -35,26 +35,6 @@ "properties": { "name": "canaryUpdater" } - }, - { - "id": "unoplatform.releaseNotesCompiler", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "releaseNotesCompiler" - } - }, - { - "id": "unoplatform.websiteVersion", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "websiteVersion" - } } ], "scopes": [ @@ -64,14 +44,8 @@ { "path": "canaryUpdater" }, - { - "path": "releaseNotesCompiler" - }, - { - "path": "websiteVersion" - }, { "path": "node_modules" } ] -} \ No newline at end of file +} diff --git a/extensions/azuredevops/websiteVersion/Readme.md b/extensions/azuredevops/websiteVersion/Readme.md deleted file mode 100644 index 894866f..0000000 --- a/extensions/azuredevops/websiteVersion/Readme.md +++ /dev/null @@ -1,11 +0,0 @@ -# unoplatform Website versioning - -This task is meant to help with versioning a static website. It was developed as a mean to maintain multiple versions of WASM-based Uno applications. The concept is very simple: -- The contents of the website are stored in an Azure Blob storage -- All the versions of the website can be found under a `Versions` folder -- The list of the available versions can be found by navigating to /Versions -- When navigating to the root of the website, the user is immediately redirected to the latest version, using `http-equiv="refresh"` - -This task is in charge of uploading the files to the storage account and generating the 2 HTML files - the root index and the versions index. - -The website should be somehow versioned for this to work properly, using a file named Version.txt placed at the root of the website. \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/AzureServiceClient.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/AzureServiceClient.ts deleted file mode 100644 index 17c2151..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/AzureServiceClient.ts +++ /dev/null @@ -1,275 +0,0 @@ -import tl = require('azure-pipelines-task-lib'); -import util = require("util") -import msRestAzure = require("./azure-arm-common"); -import webClient = require("./webClient"); - -export class ApiResult { - public error; - public result; - public request; - public response; - - constructor(error, result?, request?, response?) { - this.error = error; - this.result = result; - this.request = request; - this.response = response; - } -} - -export class AzureError { - public code; - public message; - public statusCode; - public details; -} - -export interface ApiCallback { - (error: any, result?: any, request?: any, response?: any): void -} - -export function ToError(response: webClient.WebResponse): AzureError { - var error = new AzureError(); - error.statusCode = response.statusCode; - error.message = response.body - if (response.body && response.body.error) { - error.code = response.body.error.code; - error.message = response.body.error.message; - error.details = response.body.error.details; - - console.log("##vso[task.logissue type=error;code="+error.code+";]"); - } - - return error; -} - -export class ServiceClient { - private credentials: msRestAzure.ApplicationTokenCredentials; - protected apiVersion: string; - protected baseUri: string; - protected acceptLanguage: string; - protected longRunningOperationRetryTimeout: number; - protected generateClientRequestId: boolean; - - public subscriptionId: string; - - constructor(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string, timeout?: number) { - this.validateInputs(credentials, subscriptionId); - - this.credentials = credentials; - this.subscriptionId = subscriptionId - this.baseUri = this.credentials.baseUrl; - this.longRunningOperationRetryTimeout = !!timeout ? timeout : 0; // In minutes - } - - protected validateInputs(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string) { - if (!credentials) { - throw new Error(tl.loc("CredentialsCannotBeNull")); - } - if (!subscriptionId) { - throw new Error(tl.loc("SubscriptionIdCannotBeNull")); - } - } - - public getCredentials(): msRestAzure.ApplicationTokenCredentials { - return this.credentials; - } - - public getRequestUri(uriFormat: string, parameters: {}, queryParameters?: string[], apiVersion?: string): string { - return this.getRequestUriForBaseUri(this.baseUri, uriFormat, parameters, queryParameters, apiVersion); - } - - public getRequestUriForBaseUri(baseUri: string, uriFormat: string, parameters: {}, queryParameters?: string[], apiVersion?: string): string { - var requestUri = baseUri + uriFormat; - requestUri = requestUri.replace('{subscriptionId}', encodeURIComponent(this.subscriptionId)); - for (var key in parameters) { - requestUri = requestUri.replace(key, encodeURIComponent(parameters[key])); - } - - // trim all duplicate forward slashes in the url - var regex = /([^:]\/)\/+/gi; - requestUri = requestUri.replace(regex, '$1'); - - // process query paramerters - queryParameters = queryParameters || []; - queryParameters.push('api-version=' + encodeURIComponent(apiVersion || this.apiVersion)); - if (queryParameters.length > 0) { - requestUri += '?' + queryParameters.join('&'); - } - - return requestUri - } - - public setCustomHeaders(options: Object): {} { - var headers = {}; - if (options) { - for (var headerName in options['customHeaders']) { - if (options['customHeaders'].hasOwnProperty(headerName)) { - headers[headerName] = options['customHeaders'][headerName]; - } - } - } - return headers; - } - - public async beginRequest(request: webClient.WebRequest): Promise { - var token = await this.credentials.getToken(); - - request.headers = request.headers || {}; - request.headers["Authorization"] = "Bearer " + token; - if (this.acceptLanguage) { - request.headers['accept-language'] = this.acceptLanguage; - } - request.headers['Content-Type'] = 'application/json; charset=utf-8'; - - var httpResponse = null; - - try - { - httpResponse = await webClient.sendRequest(request); - if (httpResponse.statusCode === 401 && httpResponse.body && httpResponse.body.error && httpResponse.body.error.code === "ExpiredAuthenticationToken") { - // The access token might have expire. Re-issue the request after refreshing the token. - token = await this.credentials.getToken(true); - request.headers["Authorization"] = "Bearer " + token; - httpResponse = await webClient.sendRequest(request); - } - } catch(exception) { - let exceptionString: string = exception.toString(); - if(exceptionString.indexOf("Hostname/IP doesn't match certificates's altnames") != -1 - || exceptionString.indexOf("unable to verify the first certificate") != -1 - || exceptionString.indexOf("unable to get local issuer certificate") != -1) { - tl.warning(tl.loc('ASE_SSLIssueRecommendation')); - } - - throw exception; - } - - return httpResponse; - } - - public async getLongRunningOperationResult(response: webClient.WebResponse, timeoutInMinutes?: number): Promise { - timeoutInMinutes = timeoutInMinutes || this.longRunningOperationRetryTimeout; - var timeout = new Date().getTime() + timeoutInMinutes * 60 * 1000; - var waitIndefinitely = timeoutInMinutes == 0; - var request = new webClient.WebRequest(); - request.method = "GET"; - request.uri = response.headers["azure-asyncoperation"] || response.headers["location"]; - if (!request.uri) { - throw new Error(tl.loc("InvalidResponseLongRunningOperation")); - } - - while (true) { - response = await this.beginRequest(request); - tl.debug(`Response status code : ${response.statusCode}`); - if (response.statusCode === 202 || (response.body && (response.body.status == "Accepted" || response.body.status == "Running" || response.body.status == "InProgress"))) { - // If timeout; throw; - if (!waitIndefinitely && timeout < new Date().getTime()) { - throw new Error(tl.loc("TimeoutWhileWaiting")); - } - - // Retry after given interval. - var sleepDuration = 15; - if (response.headers["retry-after"]) { - sleepDuration = parseInt(response.headers["retry-after"]); - } - await this.sleepFor(sleepDuration); - } else { - break; - } - } - - return response; - } - - public async beginRequestExpBackoff(request: webClient.WebRequest, maxAttempt: number): Promise { - var sleepDuration = 1; - for(var i = 1; true; i++) { - var response : webClient.WebResponse = await this.beginRequest(request); - //not a server error; - if(response.statusCode <500) { - return response; - } - - // response of last attempt - if(i == maxAttempt) { - return response; - } - - // Retry after given interval. - sleepDuration = sleepDuration + i; - if (response.headers["retry-after"]) { - sleepDuration = parseInt(response.headers["retry-after"]); - } - - tl.debug(tl.loc("RetryingRequest", sleepDuration)); - await this.sleepFor(sleepDuration); - } - } - - public async accumulateResultFromPagedResult(nextLinkUrl: string): Promise { - var result = []; - while (nextLinkUrl) { - var nextRequest = new webClient.WebRequest(); - nextRequest.method = 'GET'; - nextRequest.uri = nextLinkUrl; - var response = await this.beginRequest(nextRequest); - if (response.statusCode == 200 && response.body) { - if (response.body.value) { - result = result.concat(response.body.value); - } - - nextLinkUrl = response.body.nextLink; - } - else { - return new ApiResult(ToError(response)); - } - } - - return new ApiResult(null, result); - } - - public isValidResourceGroupName(resourceGroupName: string) { - if (!resourceGroupName === null || resourceGroupName === undefined || typeof resourceGroupName.valueOf() !== 'string') { - throw new Error(tl.loc("ResourceGroupCannotBeNull")); - } - if (resourceGroupName !== null && resourceGroupName !== undefined) { - if (resourceGroupName.length > 90) { - throw new Error(tl.loc("ResourceGroupExceededLength")); - } - if (resourceGroupName.length < 1) { - throw new Error(tl.loc("ResourceGroupDeceededLength")); - } - if (resourceGroupName.match(/^[-\w\._\(\)]+$/) === null) { - throw new Error(tl.loc("ResourceGroupDoesntMatchPattern")); - } - } - } - - public isNameValid(name: string): boolean { - if (name === null || name === undefined || typeof name.valueOf() !== 'string') { - return false; - }else{ - return true; - } - } - - public getFormattedError(error: any): string { - if(error && error.message) { - if(error.statusCode) { - var errorMessage = typeof error.message.valueOf() == 'string' ? error.message - : (error.message.Code || error.message.code) + " - " + (error.message.Message || error.message.message) - error.message = `${errorMessage} (CODE: ${error.statusCode})` - } - - return error.message; - } - - return error; - } - - private sleepFor(sleepDurationInSeconds): Promise { - return new Promise((resolve, reeject) => { - setTimeout(resolve, sleepDurationInSeconds * 1000); - }); - } -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-common.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-common.ts deleted file mode 100644 index 6a2a96a..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-common.ts +++ /dev/null @@ -1,283 +0,0 @@ -import tl = require('azure-pipelines-task-lib'); -import Q = require('q'); -import querystring = require('querystring'); -import webClient = require('./webClient'); -import AzureModels = require('./azureModels'); -import constants = require('./constants'); -import path = require('path'); -import fs = require('fs'); -var jwt = require('jsonwebtoken'); - -export class ApplicationTokenCredentials { - private clientId: string; - private domain: string; - private authType: string; - private secret?: string; - private certFilePath?: string; - private isADFSEnabled?: boolean; - public baseUrl: string; - public authorityUrl: string; - public activeDirectoryResourceId: string; - public isAzureStackEnvironment: boolean; - public scheme: number; - public msiClientId: string; - private token_deferred: Q.Promise; - - constructor(clientId: string, domain: string, secret: string, baseUrl: string, authorityUrl: string, activeDirectoryResourceId: string, isAzureStackEnvironment: boolean, scheme?: string, msiClientId?: string, authType?: string, certFilePath?: string, isADFSEnabled?: boolean) { - - if (!Boolean(domain) || typeof domain.valueOf() !== 'string') { - throw new Error(tl.loc("DomainCannotBeEmpty")); - } - - if((!scheme ||scheme ==='ServicePrincipal')){ - if (!Boolean(clientId) || typeof clientId.valueOf() !== 'string') { - throw new Error(tl.loc("ClientIdCannotBeEmpty")); - } - - if(!authType || authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { - if (!Boolean(secret) || typeof secret.valueOf() !== 'string') { - throw new Error(tl.loc("SecretCannotBeEmpty")); - } - } - else { - if (!Boolean(certFilePath) || typeof certFilePath.valueOf() !== 'string') { - throw new Error(tl.loc("InvalidCertFileProvided")); - } - } - - } - - if (!Boolean(baseUrl) || typeof baseUrl.valueOf() !== 'string') { - throw new Error(tl.loc("armUrlCannotBeEmpty")); - } - - if (!Boolean(authorityUrl) || typeof authorityUrl.valueOf() !== 'string') { - throw new Error(tl.loc("authorityUrlCannotBeEmpty")); - } - - if (!Boolean(activeDirectoryResourceId) || typeof activeDirectoryResourceId.valueOf() !== 'string') { - throw new Error(tl.loc("activeDirectoryResourceIdUrlCannotBeEmpty")); - } - - if(!Boolean(isAzureStackEnvironment) || typeof isAzureStackEnvironment.valueOf() != 'boolean') { - isAzureStackEnvironment = false; - } - - this.clientId = clientId; - this.domain = domain; - this.baseUrl = baseUrl; - this.authorityUrl = authorityUrl; - this.activeDirectoryResourceId = activeDirectoryResourceId; - this.isAzureStackEnvironment = isAzureStackEnvironment; - - this.scheme = scheme ? AzureModels.Scheme[scheme] : AzureModels.Scheme['ServicePrincipal'] ; - this.msiClientId = msiClientId ; - if(this.scheme == AzureModels.Scheme['ServicePrincipal']) { - this.authType = authType ? authType : constants.AzureServicePrinicipalAuthentications.servicePrincipalKey; - if(this.authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { - this.secret = secret; - } - else { - this.certFilePath = certFilePath; - } - } - - this.isADFSEnabled = isADFSEnabled; - - } - - public getToken(force?: boolean): Q.Promise { - if (!this.token_deferred || force) { - if(this.scheme === AzureModels.Scheme.ManagedServiceIdentity) - { - this.token_deferred = this._getMSIAuthorizationToken(0, 0); - } - else - { - this.token_deferred = this._getSPNAuthorizationToken(); - } - } - - return this.token_deferred; - } - - public getDomain(): string { - return this.domain; - } - - public getClientId(): string { - return this.clientId; - } - - private _getMSIAuthorizationToken(retyCount: number ,timeToWait: number): Q.Promise { - var deferred = Q.defer(); - let webRequest = new webClient.WebRequest(); - webRequest.method = "GET"; - let apiVersion = "2018-02-01"; - const retryLimit = 5; - let msiClientId = this.msiClientId ? "&client_id=" + this.msiClientId : ""; - webRequest.uri = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=" + apiVersion + "&resource="+ this.baseUrl + msiClientId; - webRequest.headers = { - "Metadata": true - }; - - webClient.sendRequest(webRequest).then( - (response: webClient.WebResponse) => { - if (response.statusCode == 200) - { - deferred.resolve(response.body.access_token); - } - else if (response.statusCode == 429 || response.statusCode == 500) - { - if(retyCount < retryLimit) - { - let waitedTime = 2000 + timeToWait * 2; - retyCount +=1; - setTimeout(() => { - deferred.resolve(this._getMSIAuthorizationToken(retyCount, waitedTime)); - }, waitedTime); - } - else - { - deferred.reject(tl.loc('CouldNotFetchAccessTokenforMSIStatusCode', response.statusCode, response.statusMessage)); - } - - } - else - { - deferred.reject(tl.loc('CouldNotFetchAccessTokenforMSIDueToMSINotConfiguredProperlyStatusCode', response.statusCode, response.statusMessage)); - } - }, - (error) => { - deferred.reject(error) - } - ); - - return deferred.promise; - } - - private _getSPNAuthorizationToken(): Q.Promise { - if(this.authType == constants.AzureServicePrinicipalAuthentications.servicePrincipalKey) { - return this._getSPNAuthorizationTokenFromKey(); - } - - return this._getSPNAuthorizationTokenFromCertificate() - } - - private _getSPNAuthorizationTokenFromCertificate(): Q.Promise { - var deferred = Q.defer(); - let webRequest = new webClient.WebRequest(); - webRequest.method = "POST"; - webRequest.uri = this.authorityUrl + (this.isADFSEnabled ? "" : this.domain) + "/oauth2/token/"; - webRequest.body = querystring.stringify({ - resource: this.activeDirectoryResourceId, - client_id: this.clientId, - grant_type: "client_credentials", - client_assertion: this._getSPNCertificateAuthorizationToken(), - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - }); - - webClient.sendRequest(webRequest).then( - (response: webClient.WebResponse) => { - if (response.statusCode == 200) { - deferred.resolve(response.body.access_token); - } - else { - deferred.reject(tl.loc('CouldNotFetchAccessTokenforAzureStatusCode', response.statusCode, response.statusMessage)); - } - }, - (error) => { - deferred.reject(error) - } - ); - return deferred.promise; - } - - - private _getSPNAuthorizationTokenFromKey(): Q.Promise { - var deferred = Q.defer(); - let webRequest = new webClient.WebRequest(); - webRequest.method = "POST"; - webRequest.uri = this.authorityUrl + this.domain + "/oauth2/token/"; - webRequest.body = querystring.stringify({ - resource: this.activeDirectoryResourceId, - client_id: this.clientId, - grant_type: "client_credentials", - client_secret: this.secret - }); - webRequest.headers = { - "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" - }; - - webClient.sendRequest(webRequest).then( - (response: webClient.WebResponse) => { - if (response.statusCode == 200) - { - deferred.resolve(response.body.access_token); - } - else - { - deferred.reject(tl.loc('CouldNotFetchAccessTokenforAzureStatusCode', response.statusCode, response.statusMessage)); - } - }, - (error) => { - deferred.reject(error) - } - ); - - return deferred.promise; - } - - private _getSPNCertificateAuthorizationToken(): string { - var openSSLPath = tl.osType().match(/^Win/) ? tl.which(path.join(__dirname, 'openssl', 'openssl')) : tl.which('openssl'); - var openSSLArgsArray= [ - "x509", - "-noout", - "-in" , - this.certFilePath, - "-fingerprint" - ]; - - var pemExecutionResult = tl.execSync(openSSLPath, openSSLArgsArray); - var additionalHeaders = { - "alg": "RS256", - "typ": "JWT", - }; - - if(pemExecutionResult.code == 0) { - tl.debug("FINGERPRINT CREATION SUCCESSFUL"); - let shaFingerprint = pemExecutionResult.stdout; - let shaFingerPrintHashCode = shaFingerprint.split("=")[1].replace(new RegExp(":", 'g'), ""); - let fingerPrintHashBase64: string = Buffer.from( - shaFingerPrintHashCode.match(/\w{2}/g).map(function(a) { - return String.fromCharCode(parseInt(a, 16)); - } ).join(""), - 'binary' - ).toString('base64'); - additionalHeaders["x5t"] = fingerPrintHashBase64; - } - else { - console.log(pemExecutionResult); - throw new Error(pemExecutionResult.stderr); - } - - return getJWT(this.authorityUrl, this.clientId, this.domain, this.certFilePath, additionalHeaders, this.isADFSEnabled); - } - -} - -function getJWT(url: string, clientId: string, tenantId: string, pemFilePath: string, additionalHeaders, isADFSEnabled: boolean) { - - var pemFileContent = fs.readFileSync(pemFilePath); - var jwtObject = { - "aud": (`${url}/${!isADFSEnabled ? tenantId : ""}/oauth2/token`).replace(/([^:]\/)\/+/g, "$1"), - "iss": clientId, - "sub": clientId, - "jti": "" + Math.random(), - "nbf": (Math.floor(Date.now()/1000)-1000), - "exp": (Math.floor(Date.now()/1000)+8640000) - }; - - var token = jwt.sign(jwtObject, pemFileContent,{ algorithm: 'RS256', header :additionalHeaders }); - return token; -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts deleted file mode 100644 index 5c177f6..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-endpoint.ts +++ /dev/null @@ -1,152 +0,0 @@ -import tl = require('azure-pipelines-task-lib'); -import Q = require('q'); -import webClient = require("./webClient"); -import { AzureEndpoint } from "./azureModels"; -import { ApplicationTokenCredentials } from './azure-arm-common'; -import constants = require('./constants'); -import fs = require('fs'); -import path = require('path'); -const certFilePath: string = path.join(tl.getVariable('Agent.TempDirectory'), 'spnCert.pem'); - -export class AzureRMEndpoint { - public endpoint: AzureEndpoint; - private _connectedServiceName: string; - private applicationTokenCredentials: ApplicationTokenCredentials; - - // Add an entry here and separate function for each new environment - private _environments = { - 'AzureStack': 'azurestack' - } - - constructor(connectedServiceName: string) { - this._connectedServiceName = connectedServiceName; - this.endpoint = null; - } - - public async getEndpoint(): Promise { - if(!!this.endpoint) { - return this.endpoint; - } - else { - this.endpoint = { - subscriptionID: tl.getEndpointDataParameter(this._connectedServiceName, 'subscriptionid', true), - subscriptionName: tl.getEndpointDataParameter(this._connectedServiceName, 'subscriptionname', true), - servicePrincipalClientID: tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'serviceprincipalid', true), - environmentAuthorityUrl: tl.getEndpointDataParameter(this._connectedServiceName, 'environmentAuthorityUrl', true), - tenantID: tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'tenantid', false), - url: tl.getEndpointUrl(this._connectedServiceName, true), - environment: tl.getEndpointDataParameter(this._connectedServiceName, 'environment', true), - scheme: tl.getEndpointAuthorizationScheme(this._connectedServiceName, true), - msiClientId: tl.getEndpointDataParameter(this._connectedServiceName, 'msiclientId', true), - activeDirectoryResourceID: tl.getEndpointDataParameter(this._connectedServiceName, 'activeDirectoryServiceEndpointResourceId', true), - azureKeyVaultServiceEndpointResourceId: tl.getEndpointDataParameter(this._connectedServiceName, 'AzureKeyVaultServiceEndpointResourceId', true), - azureKeyVaultDnsSuffix: tl.getEndpointDataParameter(this._connectedServiceName, 'AzureKeyVaultDnsSuffix', true), - } as AzureEndpoint; - - this.endpoint.authenticationType = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'authenticationType', true); - - // if scheme is null, we assume the scheme to be ServicePrincipal - let isServicePrincipalAuthenticationScheme = !this.endpoint.scheme || this.endpoint.scheme.toLowerCase() == constants.AzureRmEndpointAuthenticationScheme.ServicePrincipal; - if (isServicePrincipalAuthenticationScheme) { - if(this.endpoint.authenticationType && this.endpoint.authenticationType == constants.AzureServicePrinicipalAuthentications.servicePrincipalCertificate) { - tl.debug('certificate spn endpoint'); - this.endpoint.servicePrincipalCertificate = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'servicePrincipalCertificate', false); - this.endpoint.servicePrincipalCertificatePath = certFilePath; - fs.writeFileSync(this.endpoint.servicePrincipalCertificatePath, this.endpoint.servicePrincipalCertificate); - } - else { - tl.debug('credentials spn endpoint'); - this.endpoint.servicePrincipalKey = tl.getEndpointAuthorizationParameter(this._connectedServiceName, 'serviceprincipalkey', false); - } - } - - var isADFSEnabled = tl.getEndpointDataParameter(this._connectedServiceName, 'EnableAdfsAuthentication', true); - this.endpoint.isADFSEnabled = isADFSEnabled && (isADFSEnabled.toLowerCase() == "true"); - - if(!!this.endpoint.environment && this.endpoint.environment.toLowerCase() == this._environments.AzureStack) { - if(!this.endpoint.environmentAuthorityUrl || !this.endpoint.activeDirectoryResourceID) { - this.endpoint = await this._updateAzureStackData(this.endpoint); - } - } - else { - this.endpoint.environmentAuthorityUrl = (!!this.endpoint.environmentAuthorityUrl) ? this.endpoint.environmentAuthorityUrl : "https://login.windows.net/"; - this.endpoint.activeDirectoryResourceID = this.endpoint.url; - } - - this.endpoint.applicationTokenCredentials = new ApplicationTokenCredentials(this.endpoint.servicePrincipalClientID, this.endpoint.tenantID, this.endpoint.servicePrincipalKey, - this.endpoint.url, this.endpoint.environmentAuthorityUrl, this.endpoint.activeDirectoryResourceID, !!this.endpoint.environment && this.endpoint.environment.toLowerCase() == constants.AzureEnvironments.AzureStack, this.endpoint.scheme, this.endpoint.msiClientId, this.endpoint.authenticationType, this.endpoint.servicePrincipalCertificatePath, this.endpoint.isADFSEnabled); - } - - tl.debug(JSON.stringify(this.endpoint)); - return this.endpoint; - } - - private async _updateAzureStackData(endpoint: AzureEndpoint): Promise { - let dataDeferred = Q.defer(); - let webRequest = new webClient.WebRequest(); - webRequest.uri = `${endpoint.url}metadata/endpoints?api-version=2015-01-01`; - webRequest.method = 'GET'; - webRequest.headers = { - 'Content-Type': 'application/json' - } - - let azureStackResult; - try { - let response: webClient.WebResponse = await webClient.sendRequest(webRequest); - if(response.statusCode != 200) { - tl.debug("Action: _updateAzureStackData, Response: " + JSON.stringify(response)); - throw new Error(response.statusCode + ' ' + response.statusMessage) - } - - azureStackResult = response.body; - } - catch(error) { - throw new Error(tl.loc("FailedToFetchAzureStackDependencyData", error.toString())); - } - - endpoint.graphEndpoint = azureStackResult.graphEndpoint; - endpoint.galleryUrl = azureStackResult.galleryUrl; - endpoint.portalEndpoint = azureStackResult.portalEndpoint; - var authenticationData = azureStackResult.authentication; - if(!!authenticationData) { - var loginEndpoint: string = authenticationData.loginEndpoint; - if(!!loginEndpoint) { - loginEndpoint += (loginEndpoint[loginEndpoint.length - 1] == "/") ? "" : "/"; - endpoint.activeDirectoryAuthority = loginEndpoint; - endpoint.environmentAuthorityUrl = loginEndpoint; - endpoint.isADFSEnabled = loginEndpoint.endsWith('/adfs/'); - } - else { - // change to login endpoint - throw new Error(tl.loc('UnableToFetchAuthorityURL')); - } - - var audiences = authenticationData.audiences; - if(audiences && audiences.length > 0) { - endpoint.activeDirectoryResourceID = audiences[0]; - } - - try { - var endpointUrl = endpoint.url; - endpointUrl += (endpointUrl[endpointUrl.length-1] == "/") ? "" : "/"; - var index = endpointUrl.indexOf('.'); - var domain = endpointUrl.substring(index+1); - domain = (domain.lastIndexOf("/") == domain.length-1) ? domain.substring(0, domain.length-1): domain; - endpoint.azureKeyVaultDnsSuffix = ("vault." + domain).toLowerCase(); - endpoint.azureKeyVaultServiceEndpointResourceId = ("https://vault." + domain).toLowerCase(); - } - catch(error) { - throw new Error(tl.loc("SpecifiedAzureRmEndpointIsInvalid", endpointUrl)); - } - } - - return endpoint; - } -} - -export function dispose() { - if(tl.exist(certFilePath)) { - tl.rmRF(certFilePath); - tl.debug('Removed cert endpoint file'); - } -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-storage.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-storage.ts deleted file mode 100644 index d393b9c..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/azure-arm-storage.ts +++ /dev/null @@ -1,201 +0,0 @@ -import msRestAzure = require("./azure-arm-common"); -import azureServiceClient = require("./AzureServiceClient"); -import Model = require("./azureModels"); -import webClient = require("./webClient"); -import tl = require('azure-pipelines-task-lib'); -import Q = require('q'); -import util = require("util"); - -export class StorageManagementClient extends azureServiceClient.ServiceClient { - public storageAccounts: StorageAccounts; - - constructor(credentials: msRestAzure.ApplicationTokenCredentials, subscriptionId: string, baseUri?: any, options?: any) { - super(credentials, subscriptionId); - - this.acceptLanguage = 'en-US'; - this.generateClientRequestId = true; - this.apiVersion = (credentials.isAzureStackEnvironment) ? '2015-06-15' : '2017-06-01'; - - if (!options) - options = {}; - - if (baseUri) { - this.baseUri = baseUri; - } - - if (options.acceptLanguage) { - this.acceptLanguage = options.acceptLanguage; - } - if (options.longRunningOperationRetryTimeout) { - this.longRunningOperationRetryTimeout = options.longRunningOperationRetryTimeout; - } - if (options.generateClientRequestId) { - this.generateClientRequestId = options.generateClientRequestId; - } - this.storageAccounts = new StorageAccounts(this); - } -} - -export class StorageAccounts { - private client: StorageManagementClient; - - constructor(client) { - this.client = client; - } - - public async list(options): Promise { - var httpRequest = new webClient.WebRequest(); - httpRequest.method = 'GET'; - httpRequest.headers = this.client.setCustomHeaders(options); - // Getting all azure rm storage accounts (along with resource group names) for the given subscription. - httpRequest.uri = this.client.getRequestUri('//subscriptions/{subscriptionId}/providers/Microsoft.Storage/storageAccounts', {}); - - var deferred = Q.defer(); - var result = []; - this.client.beginRequest(httpRequest).then(async (response) => { - if (response.statusCode == 200) { - if (response.body.value) { - let storageAccounts: Model.StorageAccount[] = response.body.value; - result = result.concat(storageAccounts); - } - - if (response.body.nextLink) { - var nextResult = await this.client.accumulateResultFromPagedResult(response.body.nextLink); - if (nextResult.error) { - deferred.reject(nextResult.error); - } - - let storageAccounts: Model.StorageAccount[] = nextResult.result; - result = result.concat(storageAccounts); - } - - deferred.resolve(result); - } - else { - deferred.reject(azureServiceClient.ToError(response)); - } - }).catch(function (error) { - deferred.reject(error); - }); - - return deferred.promise; - } - - public async listClassicAndRMAccounts(options): Promise { - var httpRequest = new webClient.WebRequest(); - httpRequest.method = 'GET'; - httpRequest.headers = this.client.setCustomHeaders(options); - - // Getting all storage accounts (azure rm and classic, along with resource group names) for the given subscription. - httpRequest.uri = "https://management.azure.com/resources?api-version=2014-04-01-preview&%24filter=(subscriptionId%20eq%20'{subscriptionId}')%20and%20(resourceType%20eq%20'microsoft.storage%2Fstorageaccounts'%20or%20resourceType%20eq%20'microsoft.classicstorage%2Fstorageaccounts')"; - httpRequest.uri = httpRequest.uri.replace('{subscriptionId}', this.client.subscriptionId); - - var deferred = Q.defer(); - var result = []; - this.client.beginRequest(httpRequest).then(async (response) => { - if (response.statusCode == 200) { - if (response.body.value) { - let storageAccounts: Model.StorageAccount[] = response.body.value; - result = result.concat(storageAccounts); - } - - if (response.body.nextLink) { - var nextResult = await this.client.accumulateResultFromPagedResult(response.body.nextLink); - if (nextResult.error) { - deferred.reject(nextResult.error); - } - - let storageAccounts: Model.StorageAccount[] = nextResult.result; - result = result.concat(storageAccounts); - } - - deferred.resolve(result); - } - else { - deferred.reject(azureServiceClient.ToError(response)); - } - }).catch(function(error) { - deferred.reject(error); - }); - - return deferred.promise; - } - - public async listKeys(resourceGroupName: string, accountName: string, options, storageAccountType?: string): Promise { - if (resourceGroupName === null || resourceGroupName === undefined || typeof resourceGroupName.valueOf() !== 'string') { - throw new Error(tl.loc("ResourceGroupCannotBeNull")); - } - - if (accountName === null || accountName === undefined || typeof accountName.valueOf() !== 'string') { - throw new Error(tl.loc("StorageAccountCannotBeNull")); - } - - var apiVersion = "2017-06-01"; - var resourceProvider = "Microsoft.Storage"; - if (!!storageAccountType && storageAccountType.toLowerCase().indexOf("classicstorage") > 0) { - resourceProvider = "Microsoft.ClassicStorage"; - apiVersion = "2015-12-01"; - } - - var httpRequest = new webClient.WebRequest(); - httpRequest.method = 'POST'; - httpRequest.headers = this.client.setCustomHeaders(options); - httpRequest.uri = this.client.getRequestUri( - '//subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{provider}/storageAccounts/{storageAccountName}/listKeys', - { - '{resourceGroupName}': resourceGroupName, - '{storageAccountName}': accountName, - '{provider}': resourceProvider - }, - [], - apiVersion - ); - - var deferred = Q.defer(); - var accessKeys: string[] = []; - this.client.beginRequest(httpRequest).then((response) => { - if (response.statusCode == 200) { - if (resourceProvider === "Microsoft.ClassicStorage") { - accessKeys[0] = response.body.primaryKey; - accessKeys[1] = response.body.secondaryKey; - } else if (response.body.keys) { - let keys = response.body.keys; - for (let i = 0; i < keys.length; i++) { - accessKeys[i] = keys[i]["value"]; - } - } - - deferred.resolve(accessKeys); - } else { - deferred.reject(azureServiceClient.ToError(response)); - } - }).catch(function (error) { - deferred.reject(error); - }); - - return deferred.promise; - } - - public async get(storageAccountName: string): Promise { - let storageAccounts = await this.list(null); - let index = storageAccounts.findIndex(account => account.name.toLowerCase() === storageAccountName.toLowerCase()); - - if (index < 0) { - throw new Error(tl.loc("StorageAccountDoesNotExist", storageAccountName)); - } - - return storageAccounts[index]; - } - - public static getResourceGroupNameFromUri(resourceUri: string): string { - if (this.isNonEmptyInternal(resourceUri)) { - resourceUri = resourceUri.toLowerCase(); - return resourceUri.substring(resourceUri.indexOf("resourcegroups/") + "resourcegroups/".length, resourceUri.indexOf("/providers")); - } - return ""; - } - - private static isNonEmptyInternal(str: string): boolean { - return (!!str && !!str.trim()); - } -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/azureModels.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/azureModels.ts deleted file mode 100644 index fc05049..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/azureModels.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { ApplicationTokenCredentials } from "./azure-arm-common"; - -export interface AzureBaseObject { - name?: string; - id: string; -} - -export interface LoadBalancerProperties { - inboundNatRules: InboundNatRule[]; - backendAddressPools: BackendAddressPool[]; - frontendIPConfigurations: IPConfiguration[] -} - -export interface InboundNatRuleProperties { - frontendPort: number; - backendPort: number; - backendIPConfiguration?: IPConfiguration; - frontendIPConfiguration: IPConfiguration; - protocol: string; - idleTimeoutInMinutes: number; - enableFloatingIP: boolean; -} - -export interface BackendAddressPoolProperties { - backendIPConfigurations: IPConfiguration[]; -} - -export interface NetworkInterfaceProperties { - ipConfigurations: IPConfiguration[] -} - -export interface IPConfigurationProperties { - publicIPAddress: PublicIPAddress; - loadBalancerInboundNatRules: InboundNatRule[]; -} - -export interface PublicIPAddressProperties { - ipAddress: string; - dnsSettings: DnsSettings; -} - -export interface VMProperties { - networkProfile: NetworkProfile; - instanceView: InstanceView; - storageProfile: StorageProfile -} - -export interface VirtualMachineProfile { - networkProfile?: NetworkProfile; - instanceView?: InstanceView; - storageProfile?: StorageProfile; - extensionProfile?:ExtensionProfile; -} - -export interface VMSSProperties { - virtualMachineProfile: VirtualMachineProfile; - provisioningState?: string; -} - -export interface VMExtensionProperties { - provisioningState?: string; - type: string; - publisher: string; - typeHandlerVersion: string; - autoUpgradeMinorVersion?: boolean; - settings?: Object; - protectedSettings?: Object; -} - -export interface StorageProfile{ - imageReference?: Map; - osDisk: OSDisk; - dataDisks?: Map[]; -} - -export interface OSDisk{ - osType: string; - name: string; - createOption: string; - caching: string; - image: ImageUrl; -} - -export interface ImageUrl{ - uri: string; -} - -export interface DnsSettings { - fqdn: string; -} - -export interface NetworkProfile { - networkInterfaces: NetworkInterface[] -} - -export interface ExtensionProfile { - extensions: VMExtension[]; -} - -export interface InstanceView { - statuses: Status[]; -} - -export interface Status{ - code: string; -} - -export interface LoadBalancer extends AzureBaseObject { - location: string; - properties: LoadBalancerProperties -} - -export interface VM extends AzureBaseObject { - properties: VMProperties, - location: string, - tags?: string ; -} - -export interface VMSS extends AzureBaseObject { - properties?: VMSSProperties, - location?: string, - tags?: string ; -} - -export interface VMExtension { - name?: string; - id?: string; - properties: VMExtensionProperties, - sku?: VMSku; -} - -export interface VMSku { - name?: string, - tier?: string; - capacity?: string; -} - -export interface NetworkInterface extends AzureBaseObject { - properties: NetworkInterfaceProperties -} - -export interface InboundNatRule extends AzureBaseObject { - properties: InboundNatRuleProperties -} - -export interface IPConfiguration extends AzureBaseObject { - properties?: IPConfigurationProperties; -} - -export interface BackendAddressPool extends AzureBaseObject { - properties: BackendAddressPoolProperties -} - -export interface PublicIPAddress extends AzureBaseObject { - properties: PublicIPAddressProperties; -} - -export interface VMExtensionMetadata { - type: string; - publisher: string; - typeHandlerVersion: string; -} - -export enum ComputeResourceType { - VirtualMachine, - VirtualMachineScaleSet -} - -export enum Scheme { - ManagedServiceIdentity, - SPN -} - -export interface StorageAccountSku { - name: string; - tier?: string; -} - -export interface StorageAccountEndpoints { - blob?: string; - table?: string; - file?: string; - queue?: string; -} - -export interface StorageAccountProperties { - creationTime?: string; - primaryLocation?: string; - primaryEndpoints?: StorageAccountEndpoints; - provisioningState?: string; - secondaryLocation?: string; - secondaryndpoints?: StorageAccountEndpoints; - statusOfPrimary?: string; - statusOfSecondary?: string; - supportsHttpsTrafficOnly?: boolean; -} - -export interface StorageAccount extends AzureBaseObject { - type: string; - location?: string; - sku?: StorageAccountSku; - kind?: string; - tags?: Map; - properties?: StorageAccountProperties; -} - -export interface AzureEndpoint { - subscriptionID: string; - subscriptionName: string; - servicePrincipalClientID?: string; - authenticationType?: string; - servicePrincipalKey?: string; - servicePrincipalCertificate?: string; - servicePrincipalCertificatePath?: string - tenantID: string; - environmentAuthorityUrl: string; - url: string; - environment: string; - activeDirectoryResourceID: string; - activeDirectoryAuthority?: string; - graphEndpoint?: string; - galleryUrl?: string; - portalEndpoint?: string; - azureKeyVaultDnsSuffix?: string; - azureKeyVaultServiceEndpointResourceId?: string; - msiClientId?: string; - scheme?: string; - applicationTokenCredentials: ApplicationTokenCredentials; - isADFSEnabled?: boolean; -} - -export interface AzureAppServiceConfigurationDetails { - id: string; - name: string; - type: string; - kind?: string; - location: string; - tags: string; - properties?: {[key: string]: any}; -} - -export interface WebJob { - name: string; - status: string; - runCommand: string; - log_url: string; - url: string; - type: string; -} - -export interface SiteExtension { - id: string; - title: string; - description: string; - extension_url: string; - local_path: string; - version: string; - project_url: string; - authors: Array; - provisioningState: string; - local_is_latest_version: boolean; -} - -export interface WebTest { - id?: string; - name: string; - type: string; - location: string; - tags: {[key: string]: string}, - kind?: string, - etag?: string; - properties?: {[key: string]: any}; -} - - -export interface ApplicationInsights { - id?: string; - name: string; - type: string; - location: string; - tags: {[key: string]: string}, - kind?: string, - etag?: string; - properties?: {[key: string]: any}; -} - -export interface AKSClusterProperties { - provisioningState: string; - kubernetesVersion: string; -} - -export interface AKSCluster extends AzureBaseObject { - properties: AKSClusterProperties -} - -export interface AKSClusterAccessProfileProperties { - kubeConfig: string; -} - -export interface AKSClusterAccessProfile extends AzureBaseObject { - properties: AKSClusterAccessProfileProperties -} - -export interface IThresholdRuleConditionDataSource { - "odata.type": string; - resourceUri: string; - metricName: string; -} - -export interface IThresholdRuleCondition { - "odata.type": string; // "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition" - dataSource: IThresholdRuleConditionDataSource; - threshold: string; - operator: string; - windowSize: string; -} - -export interface IAzureMetricAlertRequestBodyProperties { - name: string; - description?: string; - isEnabled: boolean; - condition: IThresholdRuleCondition; - actions: IRuleEmailAction[]; -} - -export interface IRuleEmailAction { - "odata.type": string; //"Microsoft.Azure.Management.Insights.Models.RuleEmailAction", - sendToServiceOwners: boolean; - customEmails: string[] -} - -export interface IAzureMetricAlertRequestBody { - location: string; - tags: { [key: string] : string }; - properties: IAzureMetricAlertRequestBodyProperties; -} - -export interface IMetric { - value: string; - displayValue: string; - unit: string; -} - -export interface IAzureMetricAlertRule { - alertName: string; - metric: IMetric; - thresholdCondition: string; - thresholdValue: string; - timePeriod: string; -} - -export interface IAzureMetricAlertRulesList { - resourceId: string; - rules: IAzureMetricAlertRule[]; -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/constants.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/constants.ts deleted file mode 100644 index dac071d..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const AzureEnvironments = { - AzureStack: 'azurestack' -}; - -export const APPLICATION_INSIGHTS_EXTENSION_NAME: string = "Microsoft.ApplicationInsights.AzureWebSites"; - -export const productionSlot: string = "production"; - -export const mysqlApiVersion: string = '2017-12-01'; - -export const APIVersions = { - azure_arm_appinsights: '2015-05-01', - azure_arm_metric_alerts: '2016-03-01' -} - -export const KUDU_DEPLOYMENT_CONSTANTS = { - SUCCESS: 4, - FAILED: 3 -} - -export const AzureServicePrinicipalAuthentications = { - "servicePrincipalKey": "spnKey", - "servicePrincipalCertificate": "spnCertificate" -} - -export const AzureRmEndpointAuthenticationScheme = { - "ServicePrincipal": "serviceprincipal", - "ManagedServiceIdentity": "managedserviceidentity" -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/azure-arm-rest/webClient.ts b/extensions/azuredevops/websiteVersion/azure-arm-rest/webClient.ts deleted file mode 100644 index a7a20dc..0000000 --- a/extensions/azuredevops/websiteVersion/azure-arm-rest/webClient.ts +++ /dev/null @@ -1,117 +0,0 @@ -import tl = require('azure-pipelines-task-lib'); -import util = require("util"); -import fs = require('fs'); -import httpClient = require("typed-rest-client/HttpClient"); -import httpInterfaces = require("typed-rest-client/Interfaces"); - -let proxyUrl: string = tl.getVariable("agent.proxyurl"); -var requestOptions: httpInterfaces.IRequestOptions = proxyUrl ? { - proxy: { - proxyUrl: proxyUrl, - proxyUsername: tl.getVariable("agent.proxyusername"), - proxyPassword: tl.getVariable("agent.proxypassword"), - proxyBypassHosts: tl.getVariable("agent.proxybypasslist") ? JSON.parse(tl.getVariable("agent.proxybypasslist")) : null - } -} : {}; - -let ignoreSslErrors: string = tl.getVariable("VSTS_ARM_REST_IGNORE_SSL_ERRORS"); -requestOptions.ignoreSslError = ignoreSslErrors && ignoreSslErrors.toLowerCase() == "true"; - -var httpCallbackClient = new httpClient.HttpClient(tl.getVariable("AZURE_HTTP_USER_AGENT"), null, requestOptions); - -export class WebRequest { - public method: string; - public uri: string; - // body can be string or ReadableStream - public body: string | NodeJS.ReadableStream; - public headers: any; -} - -export class WebResponse { - public statusCode: number; - public statusMessage: string; - public headers: any; - public body: any; -} - -export class WebRequestOptions { - public retriableErrorCodes: string[]; - public retryCount: number; - public retryIntervalInSeconds: number; - public retriableStatusCodes: number[]; - public retryRequestTimedout: boolean; -} - -export async function sendRequest(request: WebRequest, options?: WebRequestOptions): Promise { - let i = 0; - let retryCount = options && options.retryCount ? options.retryCount : 5; - let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; - let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; - let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; - let timeToWait: number = retryIntervalInSeconds; - while (true) { - try { - if (request.body && typeof(request.body) !== 'string' && !request.body["readable"]) { - request.body = fs.createReadStream(request.body["path"]); - } - - let response: WebResponse = await sendRequestInternal(request); - if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { - tl.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); - await sleepFor(timeToWait); - timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; - continue; - } - - return response; - } - catch (error) { - if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { - tl.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); - await sleepFor(timeToWait); - timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; - } - else { - if (error.code) { - console.log("##vso[task.logissue type=error;code=" + error.code + ";]"); - } - - throw error; - } - } - } -} - -export function sleepFor(sleepDurationInSeconds): Promise { - return new Promise((resolve, reject) => { - setTimeout(resolve, sleepDurationInSeconds * 1000); - }); -} - -async function sendRequestInternal(request: WebRequest): Promise { - tl.debug(util.format("[%s]%s", request.method, request.uri)); - var response: httpClient.HttpClientResponse = await httpCallbackClient.request(request.method, request.uri, request.body, request.headers); - return await toWebResponse(response); -} - -async function toWebResponse(response: httpClient.HttpClientResponse): Promise { - var res = new WebResponse(); - if (response) { - res.statusCode = response.message.statusCode; - res.statusMessage = response.message.statusMessage; - res.headers = response.message.headers; - var body = await response.readBody(); - if (body) { - try { - res.body = JSON.parse(body); - } - catch (error) { - tl.debug("Could not parse response: " + JSON.stringify(error)); - tl.debug("Response: " + JSON.stringify(res.body)); - res.body = body; - } - } - } - - return res; -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/icon.png b/extensions/azuredevops/websiteVersion/icon.png deleted file mode 100644 index 1a199b961ba0e18eaebb962cb88f921118642bf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20423 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 diff --git a/extensions/azuredevops/websiteVersion/package.json b/extensions/azuredevops/websiteVersion/package.json deleted file mode 100644 index b06eb54..0000000 --- a/extensions/azuredevops/websiteVersion/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "websiteversioning", - "version": "1.0.0", - "description": "An Azure DevOps task allowing the versioning of websites. Compatible with Azure Storage with Static Website.", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "https://dev.azure.com/unoplatform/DevOps/_git/Build.Tasks" - }, - "author": "unoplatform", - "license": "ISC", - "keywords": [ - "Azure Pipelines", - "Azure DevOps", - "Task" - ], - "dependencies": { - "azure-pipelines-task-lib": "^2.9.3", - "azure-storage": "^2.10.3", - "jsonwebtoken": "^8.5.1", - "typed-rest-client": "^1.7.3" - }, - "devDependencies": { - "@types/node": "^12.12.14", - "@types/q": "^1.5.2" - } -} diff --git a/extensions/azuredevops/websiteVersion/task.json b/extensions/azuredevops/websiteVersion/task.json deleted file mode 100644 index b8684db..0000000 --- a/extensions/azuredevops/websiteVersion/task.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json", - "id": "6d3bc6dd-afd3-4c2f-913d-14ec309c770e", - "name": "websiteVersion", - "friendlyName": "Website Versioning", - "description": "Make mutliple versions of a website available at once.", - "helpMarkDown": "Currently only compatible with an Azure Storage Static Website", - "category": "Deploy", - "author": "unoplatform", - "version": { - "Major": 0, - "Minor": 0, - "Patch": 0 - }, - "visibility": [ - "Build", - "Release" - ], - "demands": [ - "azureps" - ], - "instanceNameFormat": "Deploy $(WebsitePath) to $(AzureStorageAccount)", - "groups": [ - { - "displayName": "Advanced", - "name": "Advanced", - "isExpanded": false - } - ], - "inputs": [ - { - "name": "WebsitePath", - "type": "filePath", - "label": "Website location", - "defaultValue": "", - "required": true, - "helpMarkDown": "Absolute path of the folder where the website is located." - }, - { - "name": "AzureSubscription", - "type": "connectedService:AzureRM", - "label": "Azure Subscription", - "defaultValue": "", - "required": true, - "helpMarkDown": "Azure Resource Manager subscription to target for copying the files." - }, - { - "name": "AzureStorageAccount", - "type": "pickList", - "label": "RM Storage Account", - "defaultValue": "", - "required": true, - "helpMarkDown": "Azure Storage Account to target for copying the files.", - "properties": { - "EditableOptions": "True" - } - }, - { - "name": "VersionsFolderName", - "type": "string", - "label": "Versions folder", - "defaultValue": "versions", - "required": false, - "helpMarkDown": "The name of the folder where to store the different versions.", - "groupName": "Advanced" - } - ], - "dataSourceBindings": [ - { - "target": "AzureStorageAccount", - "endpointId": "$(AzureSubscription)", - "dataSourceName": "AzureStorageAccountRM" - } - ], - "execution": { - "Node10": { - "target": "task.js" - } - } -} \ No newline at end of file diff --git a/extensions/azuredevops/websiteVersion/task.ts b/extensions/azuredevops/websiteVersion/task.ts deleted file mode 100644 index 685551d..0000000 --- a/extensions/azuredevops/websiteVersion/task.ts +++ /dev/null @@ -1,266 +0,0 @@ -import tl = require("azure-pipelines-task-lib"); -import path = require("path"); -import azure = require("azure-storage"); - -import armStorage = require('./azure-arm-rest/azure-arm-storage'); -import { AzureRMEndpoint } from './azure-arm-rest/azure-arm-endpoint'; -import { AzureEndpoint, StorageAccount } from './azure-arm-rest/azureModels'; - -import * as fs from 'fs'; - -class FileToUpload { - localPath: string; - name: string; - remotePath: string; - - constructor(localPath: string, remotePrefix?: string, name?: string, ) { - this.localPath = localPath; - - if (!name) { - name = path.basename(localPath); - } - - this.name = name; - - if (remotePrefix) { - this.remotePath = remotePrefix + '/' + name; - } else { - this.remotePath = name; - } - } -} - -const VersionFileName = 'Version.txt' -const ContainerName = '$web'; -const IndexFileName = 'index.html'; - -let VersionsFolderName: string; -let BlobService: azure.BlobService; - -async function run(): Promise { - try { - BlobService = await createBlobService(); - - let websitePath = tl.getInput("WebsitePath", true); - VersionsFolderName = tl.getInput("VersionsFolderName", true); - - let currentVersion = await retrieveCurrentVersion(websitePath); - - await pushCurrentVersion(websitePath, currentVersion); - - await updateVersionsIndex(); - await updateRootIndex(currentVersion); - } - catch (ex) { - tl.error(ex); - tl.setResult(tl.TaskResult.Failed, ex.message); - } -} - -async function createBlobService() : Promise { - let subscription = tl.getInput('AzureSubscription', true); - let storageAccountName = tl.getInput('AzureStorageAccount', true); - - var azureEndpoint: AzureEndpoint = await new AzureRMEndpoint(subscription).getEndpoint(); - const storageArmClient = new armStorage.StorageManagementClient(azureEndpoint.applicationTokenCredentials, azureEndpoint.subscriptionID); - - let storageAccount: StorageAccount = await storageArmClient.storageAccounts.get(storageAccountName); - let storageAccountResourceGroupName = getResourceGroupNameFromUri(storageAccount.id); - - let accessKeys = await storageArmClient.storageAccounts.listKeys(storageAccountResourceGroupName, storageAccountName, null); - let accessKey: string = accessKeys[0]; - - return azure.createBlobService(storageAccountName, accessKey); -} - -async function retrieveCurrentVersion(folderPath: string): Promise { - console.log("Retrieving website version"); - - let fullVersionFilePath = path.join(folderPath, VersionFileName); - if (fs.existsSync(fullVersionFilePath)) { - let version = fs.readFileSync(fullVersionFilePath, "utf8").trim(); - - if (version) { - - console.log("Version " + version + " found."); - - return version; - } - } - - throw "Could not determine version of the website. Ensure that " + VersionFileName + " exists and is correctly updated."; -} - -async function listExistingVersions(): Promise { - - console.log("Retrieving existing versions"); - - var promise = new Promise((resolve, reject) => { - BlobService.listBlobDirectoriesSegmentedWithPrefix(ContainerName, VersionsFolderName + "/", null, function (error, result, response) { - if (!!error) { - reject(error); - } else { - let versions = new Array(); - result.entries.forEach(directory => { - let version = directory.name.replace(VersionsFolderName, "").replace(/\//gi, ""); - - console.log("Found version " + version); - - versions.push(version); - }); - - resolve(versions); - } - }) - }); - - return promise; -} - -async function pushCurrentVersion(localPath: string, version: string) { - var files = listFiles(localPath, VersionsFolderName + "/" + version); - - for (var i in files) { - let file = files[i]; - await uploadFile(file); - - if(file.name.endsWith('.wasm')) { - await updateFile(file, { contentType: "application/wasm" }); - } - } -} - -async function updateVersionsIndex() { - - let availableVersions = await listExistingVersions(); - - let html = ''; - html += '\n' + ''; - html += '\n' + 'Available version'; - html += '\n' + ''; - html += '\n' + ''; - html += '\n' + '

'; - html += '\n' + ''; - - console.log("Generated " + VersionsFolderName + IndexFileName); - console.log(html); - - let indexPath = path.join(__dirname, IndexFileName); - fs.writeFileSync(indexPath, html); - - await uploadFile(new FileToUpload(indexPath, VersionsFolderName)); -} - -async function updateRootIndex(currentVersion: string) { - let html = ''; - html += '\n' + ''; - html += '\n' + ''; - html += '\n' + ''; - html += '\n' + ''; - - console.log("Generated " + IndexFileName); - console.log(html); - - let indexPath = path.join(__dirname, IndexFileName); - fs.writeFileSync(indexPath, html); - - await uploadFile(new FileToUpload(indexPath)); -} - -async function uploadFile(file: FileToUpload) { - - let upload = new Promise((resolve, reject) => { - BlobService.createBlockBlobFromLocalFile(ContainerName, file.remotePath, file.localPath, function (error, result, response) { - if (!error) { - console.log("Pushed " + result.name); - resolve(); - } else { - reject(error); - } - }); - }); - - await upload; -} - -async function updateFile(file: FileToUpload, options: azure.BlobService.SetBlobPropertiesRequestOptions) { - - let upload = new Promise((resolve, reject) => { - BlobService.setBlobProperties(ContainerName, file.remotePath, options, function (error, result, response) { - if (!error) { - console.log("Updated " + result.name); - resolve(); - } else { - reject(error); - } - }); - }); - - await upload; -} -function listFiles(folderPath: string, prefix: string): FileToUpload[] { - let files = new Array(); - - fs.readdirSync(folderPath).forEach(item => { - let itemPath = path.join(folderPath, item); - if (fs.statSync(itemPath).isDirectory()) { - listFiles(itemPath, prefix + "/" + path.basename(item)).forEach(i => files.push(i)); - } else { - files.push(new FileToUpload(itemPath, prefix)); - } - }); - - return files; -} - -function compareVersions(versionA: string, versionB: string): number { - let partsA = versionA.split("."); - let partsB = versionB.split("."); - - let partsSize = Math.max(partsA.length, partsB.length); - - for (var i = 0; i < partsSize; i++) { - let a = tryGetNumber(partsA, i, 0); - let b = tryGetNumber(partsB, i, 0); - - if (a == b) { - continue; - } - - return a - b; - } - - return 0; -} - -function tryGetNumber(values: string[], index: number, defaultValue: number): number { - if (index >= values.length) { - return defaultValue; - } else { - return +values[index]; - } -} - - -function getResourceGroupNameFromUri(resourceUri: string): string { - if (!!resourceUri && !!resourceUri.trim()) { - resourceUri = resourceUri.toLowerCase(); - return resourceUri.substring(resourceUri.indexOf("resourcegroups/") + "resourcegroups/".length, resourceUri.indexOf("/providers")); - } - - return ""; -} -run(); \ No newline at end of file From 55461025bab8a36ee28d68493519b14e7607b5a3 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 17 May 2024 15:59:58 -0400 Subject: [PATCH 10/15] chore: Update node install --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 54e12d8..9e9292d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,11 @@ on: types: [opened, synchronize, reopened] branches: - main - + +concurrency: + group: ${{github.workflow}} - ${{github.ref}} + cancel-in-progress: true + jobs: build-tools: name: Build Tools @@ -73,7 +77,7 @@ jobs: - name: npm install working-directory: extensions/azuredevops - run: npm install tfx-cli@0.7.x -g --no-audit --no-fund + run: npm install - name: Compile working-directory: extensions/azuredevops From d2fb1b1615254758699abcac6b6da1355cf4bb2e Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Fri, 17 May 2024 16:03:06 -0400 Subject: [PATCH 11/15] chore: Update versions --- .github/workflows/main.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9e9292d..b946376 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,6 +71,23 @@ jobs: with: fetch-depth: 0 + - 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 @@ -85,7 +102,7 @@ jobs: - name: Package Extension working-directory: extensions/azuredevops - run: tfx extension create --json --no-color --output-path .\artifacts\Build.Tasks.$(GitVersion.MajorMinorPatch).vsix --override "{\"version\":\"$(GitVersion.MajorMinorPatch)\"}" + 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 From debb6e66265ae04b41a91db7ab142e45324e38c8 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 28 May 2024 08:22:08 -0400 Subject: [PATCH 12/15] chore: Adjust gitversion --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b946376..e8fdfe6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,9 @@ concurrency: group: ${{github.workflow}} - ${{github.ref}} cancel-in-progress: true +env: + GitVersion_Version: '5.6.x' + jobs: build-tools: name: Build Tools @@ -37,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 From a98636fb5c2c95e5298e071cb46ecd5485df37ab Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 28 May 2024 08:43:29 -0400 Subject: [PATCH 13/15] chore: Adjust .NET setup --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e8fdfe6..bc1342c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,11 @@ jobs: 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 From 94433b612aa7a953c80ac114fc82926f999159d0 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 28 May 2024 08:54:19 -0400 Subject: [PATCH 14/15] chore: Adjust document formatting --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc1342c..c4d79fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -110,7 +110,7 @@ jobs: - 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 }}\"}" + 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 From 68bdcf52a8d2cd178b75c153721cb08dc92eae74 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 28 May 2024 09:03:03 -0400 Subject: [PATCH 15/15] chore: Adjust publishing --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4d79fa..5b435e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -116,7 +116,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: extensions - path: .\artifacts + path: extensions/azuredevops/artifacts publish-tools: name: Publish